"use strict"; // Tables tab (DAT Editor): the client's .dat tables (resource/tables in the packs) and the server's // (ls_gamesvr\config\tables) as a spreadsheet, read and written by src/main/dattable.js. The columns // come from the file and can't change (the game checks the header version); rows and values can. // Grid: click / drag / shift to select, type or F2 / double-click to edit, Enter / Tab to move, // Ctrl+C / Ctrl+V with Excel (tab-separated), Delete clears, Ctrl+Z / Ctrl+Y, Ctrl+S saves. (() => { const T = { lists: { client: [], server: [] }, cur: null, meta: null, busy: false }; window.DT = T; // (dev probes) const LS = (k, v) => { try { if (v === undefined) return localStorage.getItem(k) || ""; localStorage.setItem(k, v); } catch {} return ""; }; const RH = 26; // ---------- table list ---------- const clientSrc = () => T.src || profileSource(); async function listTables() { const src = clientSrc(); const server = LS("dt:server"); try { T.lists.client = src ? await call("dat:client", src) : []; } catch (err) { T.lists.client = []; toast(err.message, true); } T.lists.server = server ? await call("dat:folder", server) : []; $("dt-source").textContent = [src ? `client: ${src.folder}` : "no game folder in this profile", server ? `server: ${server}` : "no server folder"].join(" · "); renderList(); } function renderList() { const group = (title, items, kind, empty) => [ el("div", { class: "dt-group", text: title }), ...(items.length ? items.map((it) => el( "button", { class: `dt-item${T.cur && T.cur.refKey === `${kind}|${it.key}` ? " on" : ""}`, title: it.path, onclick: () => openTable(kind === "client" ? { kind: "pack", src: clientSrc(), key: it.key } : { kind: "file", file: it.path }, `${kind}|${it.key}`), }, [el("span", { text: it.name.replace(/\.dat$/i, "") }), kind === "client" && it.pack ? el("small", { class: "muted", text: it.pack }) : null], ), ) : [el("div", { class: "dt-none muted", text: empty })]), ]; const opened = T.cur && T.cur.ref.kind === "file" && !T.lists.server.some((x) => x.path === T.cur.ref.file) ? group("File", [{ key: T.cur.ref.file, name: T.cur.ref.file.split(/[\\/]/).pop(), path: T.cur.ref.file }], "file", "") : []; $("dt-list").replaceChildren(...group("Client (packs)", T.lists.client, "client", "No resource/tables/*.dat in the packs."), ...group("Server", T.lists.server, "server", "Pick the server's tables folder."), ...opened); } // ---------- opening ---------- async function leave() { if (!T.cur?.dirty()) return true; return confirmBox("Unsaved changes", `${T.cur.name} has changes that aren't saved. Drop them?`, "Drop changes", "danger"); } async function openTable(ref, refKey, { encoding = "auto" } = {}) { if (T.cur && T.cur.refKey !== refKey && !(await leave())) return; let d; try { d = await call("dat:open", ref, encoding); } catch (err) { return toast(err.message, true); } if (T.cur) call("dat:close", T.cur.id); const folder = LS("dt:maker"); let meta = null; try { meta = folder ? await call("dat:meta", folder, d.name) : null; } catch {} T.cur = new Sheet(d, ref, refKey, meta); $("dt-enc").value = d.encoding; $("dt-search").value = ""; renderList(); T.cur.mount($("dt-grid")); status(); } // ---------- the sheet ---------- class Sheet { constructor(d, ref, refKey, meta) { this.id = d.id; this.ref = ref; this.refKey = refKey; this.name = d.name; this.info = d; this.meta = meta; this.cols = d.cols.map((c) => ({ ...c, meta: meta?.cols?.[c.name] || null, enums: meta?.enums?.[c.name] || null })); this.setData(d.rows); this.undo = []; this.redo = []; this.sort = null; this.filter = ""; this.act = { r: 0, c: 0 }; this.anchor = { r: 0, c: 0 }; this.widths = this.cols.map((c, i) => this.guessWidth(c, i)); } setData(rows) { this.orig = rows; this.rows = rows.map((v, o) => ({ o, v: [...v] })); this.view = this.rows.map((_, i) => i); } guessWidth(c, i) { let w = c.name.length * 7.5 + 26; for (const r of this.rows.slice(0, 200)) w = Math.max(w, String(r.v[i]).length * 7 + 18); return Math.round(Math.min(Math.max(w, 56), c.text ? 320 : 150)); } dirty() { if (this.rows.length !== this.orig.length) return true; return this.rows.some((r, i) => r.o !== i || r.v.some((v, c) => v !== this.orig[i][c])); } cellDirty(row, c) { return row.o < 0 || row.v[c] !== this.orig[row.o]?.[c]; } // ----- view ----- applyView() { const q = this.filter.toLowerCase(); let idx = this.rows.map((_, i) => i); if (q) idx = idx.filter((i) => this.rows[i].v.some((v) => String(v).toLowerCase().includes(q))); if (this.sort) { const { c, desc } = this.sort; const num = !this.cols[c].text; idx.sort((a, b) => { const x = this.rows[a].v[c]; const y = this.rows[b].v[c]; const r = num ? Number(x) - Number(y) : String(x).localeCompare(String(y)); return (desc ? -r : r) || a - b; }); } this.view = idx; this.act.r = Math.min(this.act.r, Math.max(0, idx.length - 1)); this.anchor = { ...this.act }; } // ----- DOM ----- mount(host) { this.host = host; host.replaceChildren(); host.tabIndex = 0; this.head = el("div", { class: "dt-head" }); this.body = el("div", { class: "dt-rows" }); host.append(this.head, this.body); host.onscroll = () => this.draw(); host.onkeydown = (e) => this.key(e); host.onmousedown = (e) => this.mouse(e); host.ondblclick = (e) => { const cell = e.target.closest(".dt-cell"); if (cell) this.edit(); }; host.oncopy = (e) => this.copy(e); host.onpaste = (e) => this.paste(e); this.drawHead(); this.layout(); host.focus(); } colLeft(c) { let x = 56; for (let i = 0; i < c; i++) x += this.widths[i]; return x; } layout() { const total = this.colLeft(this.cols.length); this.head.style.width = `${total}px`; this.body.style.width = `${total}px`; this.body.style.height = `${this.view.length * RH}px`; this.draw(); } drawHead() { const cells = [el("div", { class: "dt-h dt-rn", text: "#", style: "width:56px" })]; this.cols.forEach((c, i) => { const tip = [`${c.name} — ${c.typeName}`, c.meta?.desc, c.meta?.export ? `used by: ${c.meta.export}` : null, c.enums ? `enum: ${c.enums.map((e) => `${e.value} ${e.name}`).join(", ")}` : null].filter(Boolean).join("\n"); const arrow = this.sort?.c === i ? (this.sort.desc ? " ▼" : " ▲") : ""; const h = el("div", { class: "dt-h", title: tip, style: `width:${this.widths[i]}px` }, [ el("b", { text: c.name + arrow }), el("small", { text: c.meta?.desc ? `${c.typeName} · ${c.meta.desc}` : c.typeName }), el("i", { class: "dt-resize", onmousedown: (e) => this.resize(e, i) }), ]); h.addEventListener("click", (e) => { if (e.target.classList.contains("dt-resize")) return; this.sort = this.sort?.c !== i ? { c: i, desc: false } : this.sort.desc ? null : { c: i, desc: true }; this.applyView(); this.drawHead(); this.layout(); status(); }); cells.push(h); }); this.head.replaceChildren(...cells); } resize(e, i) { e.preventDefault(); e.stopPropagation(); const x0 = e.clientX; const w0 = this.widths[i]; const move = (ev) => { this.widths[i] = Math.max(40, w0 + ev.clientX - x0); this.drawHead(); this.layout(); }; const up = () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); }; window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); } sel() { return { r0: Math.min(this.act.r, this.anchor.r), r1: Math.max(this.act.r, this.anchor.r), c0: Math.min(this.act.c, this.anchor.c), c1: Math.max(this.act.c, this.anchor.c) }; } display(c, v) { if (c.enums && v !== "") { const e = c.enums.find((x) => String(x.value) === String(v)); if (e) return [el("span", { text: v }), el("small", { class: "dt-enum", text: e.name })]; } return [document.createTextNode(v)]; } draw() { if (!this.host) return; const top = this.host.scrollTop - 34; const h = this.host.clientHeight; const first = Math.max(0, Math.floor(top / RH) - 4); const last = Math.min(this.view.length, Math.ceil((top + h) / RH) + 4); const s = this.sel(); const frag = document.createDocumentFragment(); for (let vr = first; vr < last; vr++) { const row = this.rows[this.view[vr]]; const line = el("div", { class: `dt-row${row.o < 0 ? " new" : ""}`, style: `top:${vr * RH}px` }); line.append(el("div", { class: "dt-cell dt-rn", text: String(this.view[vr] + 1), style: "width:56px", "data-r": vr, "data-c": -1 })); this.cols.forEach((c, ci) => { const inSel = vr >= s.r0 && vr <= s.r1 && ci >= s.c0 && ci <= s.c1; const cls = ["dt-cell", c.text ? "txt" : "num", this.cellDirty(row, ci) ? "dirty" : "", inSel ? "sel" : "", vr === this.act.r && ci === this.act.c ? "act" : ""].filter(Boolean).join(" "); line.append(el("div", { class: cls, style: `width:${this.widths[ci]}px`, "data-r": vr, "data-c": ci }, this.display(c, row.v[ci]))); }); frag.append(line); } this.body.replaceChildren(frag); } scrollTo(vr, c) { const y = vr * RH; const hh = 34; if (y < this.host.scrollTop) this.host.scrollTop = y; else if (y + RH > this.host.scrollTop + this.host.clientHeight - hh) this.host.scrollTop = y + RH - this.host.clientHeight + hh; if (c >= 0) { const x = this.colLeft(c); const w = this.widths[c]; if (x - 56 < this.host.scrollLeft) this.host.scrollLeft = x - 56; else if (x + w > this.host.scrollLeft + this.host.clientWidth) this.host.scrollLeft = x + w - this.host.clientWidth; } } // ----- selection ----- cellAt(e) { const cell = e.target.closest?.(".dt-cell"); if (!cell) return null; return { r: Number(cell.dataset.r), c: Number(cell.dataset.c) }; } mouse(e) { if (e.button !== 0 || e.target.closest(".dt-h") || e.target.closest(".dt-editor")) return; const at = this.cellAt(e); if (!at) return; this.commitEdit(); if (at.c < 0) { // row number: whole rows if (!e.shiftKey) this.anchor = { r: at.r, c: 0 }; this.act = { r: at.r, c: this.cols.length - 1 }; this.anchor.c = 0; } else { this.act = at; if (!e.shiftKey) this.anchor = { ...at }; } this.draw(); status(); const move = (ev) => { const t = document.elementFromPoint(ev.clientX, ev.clientY)?.closest?.(".dt-cell"); if (!t || !this.host.contains(t)) return; const r = Number(t.dataset.r); const c = Number(t.dataset.c); if (r === this.act.r && (c === this.act.c || c < 0)) return; this.act = { r, c: c < 0 ? this.act.c : c }; this.draw(); status(); }; const up = () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); }; window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); } move(dr, dc, extend) { this.act = { r: Math.max(0, Math.min(this.view.length - 1, this.act.r + dr)), c: Math.max(0, Math.min(this.cols.length - 1, this.act.c + dc)) }; if (!extend) this.anchor = { ...this.act }; this.scrollTo(this.act.r, this.act.c); this.draw(); status(); } key(e) { if (this.editor) return; const ctrl = e.ctrlKey || e.metaKey; const k = e.key; const page = Math.max(1, Math.floor(this.host.clientHeight / RH) - 2); if (k === "ArrowDown") this.move(ctrl ? this.view.length : 1, 0, e.shiftKey); else if (k === "ArrowUp") this.move(ctrl ? -this.view.length : -1, 0, e.shiftKey); else if (k === "ArrowRight") this.move(0, ctrl ? this.cols.length : 1, e.shiftKey); else if (k === "ArrowLeft") this.move(0, ctrl ? -this.cols.length : -1, e.shiftKey); else if (k === "PageDown") this.move(page, 0, e.shiftKey); else if (k === "PageUp") this.move(-page, 0, e.shiftKey); else if (k === "Home") this.move(ctrl ? -this.view.length : 0, -this.cols.length, e.shiftKey); else if (k === "End") this.move(ctrl ? this.view.length : 0, this.cols.length, e.shiftKey); else if (k === "Tab") this.move(0, e.shiftKey ? -1 : 1, false); else if (k === "Enter" && ctrl) addRow(); else if (k === "Enter" || k === "F2") this.edit(); else if (k === "Delete" && ctrl) deleteRows(); else if (k === "Delete" || k === "Backspace") this.clear(); else if (ctrl && k.toLowerCase() === "a") { this.anchor = { r: 0, c: 0 }; this.act = { r: this.view.length - 1, c: this.cols.length - 1 }; this.draw(); status(); } else if (ctrl && k.toLowerCase() === "z") this.doUndo(); else if (ctrl && k.toLowerCase() === "y") this.doRedo(); else if (ctrl && k.toLowerCase() === "s") save(); else if (ctrl && k.toLowerCase() === "d") duplicateRows(); else if (ctrl && k.toLowerCase() === "f") $("dt-search").focus(); else if (!ctrl && !e.altKey && k.length === 1) { this.edit(k); e.preventDefault(); return; } else return; e.preventDefault(); } // ----- editing ----- edit(start) { if (!this.view.length) return; const vr = this.act.r; const c = this.act.c; const row = this.rows[this.view[vr]]; this.scrollTo(vr, c); const col = this.cols[c]; const input = el("input", { class: "dt-editor", type: "text", spellcheck: "false" }); input.value = start ?? row.v[c]; input.style.left = `${this.colLeft(c)}px`; input.style.top = `${vr * RH}px`; input.style.width = `${Math.max(this.widths[c], col.text ? 260 : 120)}px`; if (col.enums) input.setAttribute("list", this.enumList(col)); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); this.commitEdit(); this.move(e.shiftKey ? -1 : 1, 0, false); this.host.focus(); } else if (e.key === "Tab") { e.preventDefault(); this.commitEdit(); this.move(0, e.shiftKey ? -1 : 1, false); this.host.focus(); } else if (e.key === "Escape") { e.preventDefault(); this.cancelEdit(); } e.stopPropagation(); }); input.addEventListener("blur", () => this.commitEdit()); this.body.append(input); this.editor = { input, row, c }; input.focus(); if (start === undefined) input.select(); } enumList(col) { const id = `dt-enum-${col.name}`; if (!document.getElementById(id)) document.body.append(el("datalist", { id }, col.enums.map((e) => el("option", { value: String(e.value), label: e.name })))); return id; } commitEdit() { const ed = this.editor; if (!ed) return; this.editor = null; let v = ed.input.value; const col = this.cols[ed.c]; // an enum can be typed by its name if (col.enums && !/^-?\d+$/.test(v.trim())) { const e = col.enums.find((x) => x.name.toLowerCase() === v.trim().toLowerCase()); if (e) v = String(e.value); } if (!col.text) v = v.trim(); ed.input.remove(); if (v !== ed.row.v[ed.c]) this.change([{ row: ed.row, c: ed.c, v }]); else this.draw(); } cancelEdit() { const ed = this.editor; if (!ed) return; this.editor = null; ed.input.remove(); this.host.focus(); this.draw(); } /** cell changes [{ row, c, v }] as one undo step */ change(list) { const step = { cells: list.map((x) => ({ row: x.row, c: x.c, old: x.row.v[x.c], v: x.v })) }; for (const x of step.cells) x.row.v[x.c] = x.v; this.undo.push(step); this.redo = []; this.draw(); status(); } /** a row operation: the rows array before and after */ rowsStep(after) { this.undo.push({ before: this.rows, after }); this.redo = []; this.rows = after; this.applyView(); this.layout(); status(); } doUndo() { const s = this.undo.pop(); if (!s) return; if (s.cells) for (const x of s.cells) x.row.v[x.c] = x.old; else { this.rows = s.before; this.applyView(); this.layout(); } this.redo.push(s); this.draw(); status(); } doRedo() { const s = this.redo.pop(); if (!s) return; if (s.cells) for (const x of s.cells) x.row.v[x.c] = x.v; else { this.rows = s.after; this.applyView(); this.layout(); } this.undo.push(s); this.draw(); status(); } clear() { const s = this.sel(); const list = []; for (let vr = s.r0; vr <= s.r1; vr++) for (let c = s.c0; c <= s.c1; c++) list.push({ row: this.rows[this.view[vr]], c, v: this.cols[c].text ? "" : "0" }); this.change(list); } // ----- clipboard (tab-separated, like Excel) ----- copy(e) { if (this.editor) return; const s = this.sel(); const lines = []; for (let vr = s.r0; vr <= s.r1; vr++) { const row = this.rows[this.view[vr]]; lines.push(row.v.slice(s.c0, s.c1 + 1).map((v) => (/[\t\n"]/.test(v) ? `"${String(v).replace(/"/g, '""')}"` : v)).join("\t")); } e.clipboardData.setData("text/plain", lines.join("\r\n")); e.preventDefault(); toast(`Copied ${s.r1 - s.r0 + 1} × ${s.c1 - s.c0 + 1} cells.`); } paste(e) { if (this.editor) return; e.preventDefault(); const text = e.clipboardData.getData("text/plain"); if (!text) return; const grid = parseTsv(text); if (!grid.length) return; const s = this.sel(); // one value into a bigger selection fills it; otherwise the block starts at the top-left cell const fill = grid.length === 1 && grid[0].length === 1 && (s.r1 > s.r0 || s.c1 > s.c0); const list = []; let after = null; const need = fill ? 0 : s.r0 + grid.length - this.view.length; if (need > 0) { // more rows than there are below: new rows at the end after = [...this.rows]; for (let i = 0; i < need; i++) after.push({ o: -1, v: this.cols.map((c) => (c.text ? "" : "0")) }); this.rows = after; this.applyView(); } const rowsR = fill ? s.r1 - s.r0 + 1 : grid.length; const colsR = fill ? s.c1 - s.c0 + 1 : Math.min(grid[0].length, this.cols.length - s.c0); for (let i = 0; i < rowsR; i++) { const row = this.rows[this.view[s.r0 + i]]; if (!row) break; for (let j = 0; j < colsR; j++) { const c = s.c0 + j; if (c >= this.cols.length) break; const v = fill ? grid[0][0] : grid[i][j] ?? ""; list.push({ row, c, v: this.cols[c].text ? v : v.trim() }); } } if (after) { // rows added and filled: one undo step for both const before = this.rows.slice(0, this.rows.length - need); for (const x of list) x.row.v[x.c] = x.v; this.undo.push({ before, after: this.rows }); this.redo = []; this.layout(); } else this.change(list); this.anchor = { r: s.r0, c: s.c0 }; this.act = { r: s.r0 + rowsR - 1, c: s.c0 + colsR - 1 }; this.draw(); status(); toast(`Pasted ${rowsR} × ${colsR} cells${need > 0 ? ` (${need} new rows)` : ""}.`); } selectedRows() { const s = this.sel(); return this.view.slice(s.r0, s.r1 + 1); } } function parseTsv(text) { const rows = []; let row = []; let cell = ""; let q = false; for (let i = 0; i < text.length; i++) { const ch = text[i]; if (q) { if (ch === '"' && text[i + 1] === '"') (cell += '"'), i++; else if (ch === '"') q = false; else cell += ch; } else if (ch === '"' && cell === "") q = true; else if (ch === "\t") row.push(cell), (cell = ""); else if (ch === "\n") row.push(cell.replace(/\r$/, "")), rows.push(row), (row = []), (cell = ""); else cell += ch; } if (cell !== "" || row.length) row.push(cell.replace(/\r$/, "")), rows.push(row); return rows; } // ---------- row commands ---------- function addRow() { const S = T.cur; if (!S) return; const at = S.view.length ? S.view[S.sel().r1] + 1 : S.rows.length; const blank = { o: -1, v: S.cols.map((c) => (c.text ? "" : "0")) }; // a new key: the first column one above the largest when it's a number const key = S.cols[0]; if (key && !key.text) blank.v[0] = String(Math.max(0, ...S.rows.map((r) => Number(r.v[0]) || 0)) + 1); const after = [...S.rows.slice(0, at), blank, ...S.rows.slice(at)]; S.rowsStep(after); const vr = S.view.indexOf(at); if (vr >= 0) { S.act = { r: vr, c: 0 }; S.anchor = { ...S.act }; S.scrollTo(vr, 0); S.draw(); } } function duplicateRows() { const S = T.cur; if (!S || !S.view.length) return; const pick = S.selectedRows().sort((a, b) => a - b); const at = pick[pick.length - 1] + 1; let next = Math.max(0, ...S.rows.map((r) => Number(r.v[0]) || 0)) + 1; const copies = pick.map((i) => { const v = [...S.rows[i].v]; if (!S.cols[0].text) v[0] = String(next++); return { o: -1, v }; }); S.rowsStep([...S.rows.slice(0, at), ...copies, ...S.rows.slice(at)]); toast(`Duplicated ${copies.length} row(s)${!S.cols[0].text ? ` with new ${S.cols[0].name} values` : ""}.`); } async function deleteRows() { const S = T.cur; if (!S || !S.view.length) return; const pick = new Set(S.selectedRows()); if (pick.size > 1 && !(await confirmBox("Delete rows", `Delete ${pick.size} rows?`, "Delete", "danger"))) return; S.rowsStep(S.rows.filter((_, i) => !pick.has(i))); } // ---------- save / import / export ---------- async function save() { const S = T.cur; if (!S || T.busy || !S.dirty()) return; S.commitEdit(); T.busy = true; try { const enums = Object.fromEntries(S.cols.filter((c) => c.enums).map((c) => [c.name, c.enums])); const r = await call( "dat:save", S.id, S.rows.map((x) => ({ o: x.o, v: x.v })), { enums }, ); S.info = r; S.setData(r.rows); S.undo = []; S.redo = []; S.applyView(); S.layout(); toast(`Saved ${S.name}: ${r.rows.length} rows${r.backup ? ` (backup: ${r.backup.split(/[\\/]/).pop()})` : ""}.`); } catch (err) { toast(err.message, true); } finally { T.busy = false; status(); } } async function exportTable() { const S = T.cur; if (!S) return; const file = await call("dialog:saveAs", `${S.name}.xlsx`, "xlsx"); if (!file) return; try { const rows = S.rows.map((r) => r.v); const out = /\.csv$/i.test(file) ? await call("dat:exportCsv", S.id, rows, file) : await call("dat:exportXlsx", S.id, rows, file); toast(`Exported ${rows.length} rows to ${out}`); } catch (err) { toast(err.message, true); } } async function importTable() { const S = T.cur; if (!S) return; const file = await call("dat:pickFile", "Import rows from .xlsx / .csv", "sheet"); if (!file) return; let r; try { r = await call("dat:import", S.id, file); } catch (err) { return toast(err.message, true); } const notes = [r.missing.length ? `Columns not in the file (left empty / 0): ${r.missing.join(", ")}.` : null, r.extra.length ? `Columns in the file that the table doesn't have (ignored): ${r.extra.join(", ")}.` : null].filter(Boolean); const ok = await confirmBox("Import", [`Replace the ${S.rows.length} rows of ${S.name} with the ${r.rows.length} rows of ${file.split(/[\\/]/).pop()}? You can undo it, and nothing is written until you Save.`, ...notes].join("\n\n"), "Replace"); if (!ok) return; // rows that match an existing key keep their original (so unchanged cells stay byte-identical) const byKey = new Map(S.rows.map((x) => [x.v[0], x.o])); S.rowsStep(r.rows.map((v) => ({ o: byKey.has(v[0]) ? byKey.get(v[0]) : -1, v: v.map((x, i) => (S.cols[i].text ? x : x.trim() || "0")) }))); } // ---------- status ---------- function status() { const S = T.cur; $("dt-save").disabled = !S || !S.dirty() || T.busy; $("dt-undo").disabled = !S?.undo.length; $("dt-redo").disabled = !S?.redo.length; for (const id of ["dt-add", "dt-dup", "dt-del", "dt-import", "dt-export", "dt-enc", "dt-search"]) $(id).disabled = !S; if (!S) return ($("dt-status").textContent = ""); const s = S.sel(); const changed = S.rows.filter((r) => r.o < 0 || r.v.some((v, c) => v !== S.orig[r.o]?.[c])).length; const col = S.cols[S.act.c]; const row = S.rows[S.view[S.act.r]]; $("dt-status").textContent = [ `${S.name} · ${S.info.label}`, `${S.rows.length} rows${S.view.length !== S.rows.length ? ` (${S.view.length} shown)` : ""} × ${S.cols.length} columns · ${S.info.rowSize} bytes/row · header ${S.info.headerVersion}`, changed || S.rows.length !== S.orig.length ? `${changed} changed row(s)${S.rows.length !== S.orig.length ? `, ${S.rows.length - S.orig.length >= 0 ? "+" : ""}${S.rows.length - S.orig.length} rows` : ""}` : "no changes", col && row ? `${col.name} (${col.typeName})${col.meta?.desc ? ` — ${col.meta.desc}` : ""}${s.r1 > s.r0 || s.c1 > s.c0 ? ` · ${s.r1 - s.r0 + 1} × ${s.c1 - s.c0 + 1} selected` : ""}` : "", S.meta?.source ? `source: ${S.meta.source}` : "", ] .filter(Boolean) .join(" · "); } // ---------- wiring ---------- $("dt-add").addEventListener("click", addRow); $("dt-dup").addEventListener("click", duplicateRows); $("dt-del").addEventListener("click", deleteRows); $("dt-undo").addEventListener("click", () => T.cur?.doUndo()); $("dt-redo").addEventListener("click", () => T.cur?.doRedo()); $("dt-save").addEventListener("click", save); $("dt-export").addEventListener("click", exportTable); $("dt-import").addEventListener("click", importTable); $("dt-search").addEventListener("input", () => { if (!T.cur) return; T.cur.filter = $("dt-search").value.trim(); T.cur.applyView(); T.cur.layout(); status(); }); $("dt-enc").addEventListener("change", async () => { const S = T.cur; if (!S) return; if (S.dirty() && !(await confirmBox("Code page", "Reading the texts in another code page drops the unsaved changes.", "Switch", "danger"))) { $("dt-enc").value = S.info.encoding; return; } const d = await call("dat:encoding", S.id, $("dt-enc").value); S.info = { ...S.info, ...d }; S.setData(d.rows); S.undo = []; S.redo = []; S.applyView(); S.layout(); status(); }); $("dt-client").addEventListener("click", async () => { const dir = await call("dialog:folder", "Game folder (with the .iop packs)"); if (!dir) return; T.src = { kind: "packs", folder: dir, name: dir }; listTables(); }); $("dt-server").addEventListener("click", async () => { const dir = await call("dialog:folder", "The game server's tables folder (ls_gamesvr\\config\\tables)"); if (!dir) return; LS("dt:server", dir); listTables(); }); $("dt-file").addEventListener("click", async () => { const file = await call("dat:pickFile", "Open a .dat table", "dat"); if (file) openTable({ kind: "file", file }, `file|${file}`); }); $("dt-maker").addEventListener("click", async () => { const dir = await call("dialog:folder", "ioDataMaker folder (with Config.xlsx and ExcelData)"); if (!dir) return; LS("dt:maker", dir); toast("Column descriptions and enum names come from there now (reopen the table)."); }); $("dt-reload").addEventListener("click", listTables); window.addEventListener("beforeunload", () => T.cur && call("dat:close", T.cur.id)); document.querySelector('#tabs [data-view="tables"]').addEventListener("click", () => { if (!T.listed) { T.listed = true; listTables(); status(); } }); window.addEventListener("profile-changed", () => { T.listed = false; T.src = null; if ($("view-tables").classList.contains("on")) { T.listed = true; listTables(); } }); })();