From 79e099ad085885a48479861be0e4a7658d6b0e5d Mon Sep 17 00:00:00 2001 From: DazedAnon Date: Sat, 13 Jun 2026 16:39:54 -0500 Subject: [PATCH] Working scaling --- gui/workflow_tab.py | 2 +- util/forge/Forge_MV.js | 58 +++++++++++++++---- util/forge/Forge_MZ.js | 45 ++++++++++----- util/forge/config.py | 95 ++++++++++++++++++++++++------- util/forge/installer.py | 97 +++++++++++++++++++++++++++----- util/tl_inspector/TLInspector.js | 5 +- 6 files changed, 238 insertions(+), 64 deletions(-) diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 466f5e5..d130995 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -2561,7 +2561,7 @@ class WorkflowTab(QWidget): self._pt_ui_scale_combo = QComboBox() self._pt_ui_scale_combo.setToolTip( - "In-game overlay size. Auto scales from game resolution and display DPI." + "In-game overlay size. Auto scales from window size and display DPI." ) for label, value in ( ("Auto (match game width)", "auto"), diff --git a/util/forge/Forge_MV.js b/util/forge/Forge_MV.js index 5ba769c..d97a886 100644 --- a/util/forge/Forge_MV.js +++ b/util/forge/Forge_MV.js @@ -22,6 +22,10 @@ * @desc Value used by the "Max" button for item stacks (0 = use engine maxItems). * @default 0 * + * @param UiScale + * @desc Overlay size. Use auto to match the window, or a number like 1.5 or 2. + * @default auto + * * @help * Forge — drop-in cheat & editor for RPG Maker MV. * Place in js/plugins/ and enable. Press F10 in-game to open the overlay. @@ -380,7 +384,7 @@ }; // expose the assembled API (UI is attached below if a real DOM is present) - var MTC = { env: { IS_MZ: IS_MZ }, ops: ops, locks: locks, cheats: cheats, LOCK: LOCK, CHEAT: CHEAT, PARAM_NAMES: PARAM_NAMES, _itemMaxOverride: 0, _hotkey: 'F10', _speedKey: 'Control', _speedBoost: 2 }; + var MTC = { env: { IS_MZ: IS_MZ }, ops: ops, locks: locks, cheats: cheats, LOCK: LOCK, CHEAT: CHEAT, PARAM_NAMES: PARAM_NAMES, _itemMaxOverride: 0, _hotkey: 'F10', _speedKey: 'Control', _speedBoost: 2, _uiScale: 'auto' }; if (root) { root.Forge = MTC; root.__FORGE_CORE__ = true; } if (typeof module !== 'undefined' && module.exports) module.exports = MTC; @@ -404,6 +408,37 @@ var LS = 'forge:'; function store(k, v) { try { localStorage.setItem(LS + k, JSON.stringify(v)); } catch (e) {} } function load(k, d) { try { var s = localStorage.getItem(LS + k); return s == null ? d : JSON.parse(s); } catch (e) { return d; } } + function resolveUiScale(v) { + if (v !== 'auto' && v != null && String(v).trim() !== '') { + var n = parseFloat(v); + if (!isNaN(n) && n > 0) return Math.max(0.75, Math.min(3, n)); + } + var base = 816; + var gameW = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : 0; + var viewW = window.innerWidth || document.documentElement.clientWidth || base; + var w = Math.max(gameW || base, viewW); + var scale = w / base; + var dpr = window.devicePixelRatio || 1; + if (dpr > 1.15) scale *= Math.min(2, 0.75 + dpr * 0.35); + return Math.max(1, Math.min(2.75, scale)); + } + function scalePx(n, fx) { + if (!fx || fx === 1) return n; + return Math.round(n * fx); + } + function scalePxCss(css, fx) { + if (!fx || fx === 1) return css; + return css.replace(/(\d+(?:\.\d+)?)px/g, function (_, n) { + return scalePx(parseFloat(n), fx) + 'px'; + }); + } + function scaleStyle(style, fx) { + if (!style || !fx || fx === 1) return style; + return style.replace(/(\d+(?:\.\d+)?)px/g, function (_, n) { + return scalePx(parseFloat(n), fx) + 'px'; + }); + } + var uiFx = resolveUiScale(API._uiScale || 'auto'); // persisted speed settings (UI-controlled; override core/param defaults if the user changed them) API._speedKey = load('speedKey', API._speedKey || 'Control'); API._speedUseKey = load('speedUseKey', true); @@ -416,7 +451,7 @@ if (k === 'class') n.className = props[k]; else if (k === 'text') n.textContent = props[k]; else if (k === 'html') n.innerHTML = props[k]; - else if (k === 'style') n.setAttribute('style', props[k]); + else if (k === 'style') n.setAttribute('style', scaleStyle(props[k], uiFx)); else if (k.slice(0, 2) === 'on' && typeof props[k] === 'function') n.addEventListener(k.slice(2).toLowerCase(), props[k]); else if (props[k] != null) n.setAttribute(k, props[k]); } @@ -507,12 +542,12 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- shadow host ---------- var host = el('div', { id: 'forge-host' }); - host.style.cssText = 'position:fixed!important;top:0!important;left:0!important;width:0!important;height:0!important;z-index:2147483647!important;margin:0!important;padding:0!important;border:0!important;pointer-events:none!important'; + host.style.cssText = 'position:fixed!important;top:0!important;left:0!important;width:0!important;height:0!important;z-index:2147483647!important;margin:0!important;padding:0!important;border:0!important;pointer-events:none!important;overflow:visible!important'; document.body.appendChild(host); var shadow = host.attachShadow({ mode: 'open' }); var rootEl = el('div', { id: 'forge-root' }); rootEl.setAttribute('data-theme', load('theme', 'dark')); - shadow.appendChild(el('style', { text: CSS })); + shadow.appendChild(el('style', { text: scalePxCss(CSS, uiFx) })); shadow.appendChild(rootEl); // Swallow events so panel interaction never leaks to the game. BUBBLE phase: the panel's own @@ -547,8 +582,8 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- panel skeleton ---------- var vw = window.innerWidth || 816, vh = window.innerHeight || 624; // Default size fits the window (many MV games are ~816x624); leave a margin. - var st = load('panel', { x: 16, y: 16, w: Math.min(880, vw - 32), h: Math.min(560, vh - 32) }); - st.w = Math.max(420, Math.min(st.w, vw - 8)); st.h = Math.max(320, Math.min(st.h, vh - 8)); + var st = load('panel', { x: scalePx(16, uiFx), y: scalePx(16, uiFx), w: Math.min(scalePx(880, uiFx), vw - scalePx(32, uiFx)), h: Math.min(scalePx(560, uiFx), vh - scalePx(32, uiFx)) }); + st.w = Math.max(scalePx(420, uiFx), Math.min(st.w, vw - scalePx(8, uiFx))); st.h = Math.max(scalePx(320, uiFx), Math.min(st.h, vh - scalePx(8, uiFx))); st.x = Math.max(0, Math.min(st.x, vw - st.w)); st.y = Math.max(0, Math.min(st.y, vh - st.h)); // keep fully on-screen var panel = el('div', { class: 'fg-panel' }); panel.style.left = st.x + 'px'; panel.style.top = st.y + 'px'; panel.style.width = st.w + 'px'; panel.style.height = st.h + 'px'; @@ -571,8 +606,8 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- launcher (docks to bottom-right corner by default; 'launcherPos' is a fresh key so // any stale mid-screen position from older builds is ignored) ---------- - var lp = load('launcherPos', { x: vw - 56, y: vh - 56 }); - lp.x = Math.max(4, Math.min(lp.x, vw - 46)); lp.y = Math.max(4, Math.min(lp.y, vh - 46)); + var lp = load('launcherPos', { x: vw - scalePx(56, uiFx), y: vh - scalePx(56, uiFx) }); + lp.x = Math.max(scalePx(4, uiFx), Math.min(lp.x, vw - scalePx(46, uiFx))); lp.y = Math.max(scalePx(4, uiFx), Math.min(lp.y, vh - scalePx(46, uiFx))); var launcher = el('button', { class: 'fg-launcher', text: '⚒', title: 'Forge (F10)' }); launcher.style.left = lp.x + 'px'; launcher.style.top = lp.y + 'px'; launcher.addEventListener('click', function (e) { if (!launcher._dragged) toggle(); }); @@ -585,7 +620,7 @@ input::placeholder{color:var(--fg-text-faint)}\ if (e.target.closest && e.target.closest('input,button.fg-iconbtn')) return; e.preventDefault(); e.stopPropagation(); var sx = e.clientX, sy = e.clientY, ox = parseInt(target.style.left) || 0, oy = parseInt(target.style.top) || 0, moved = false; if (isLauncher) target._dragged = false; - function mv(ev) { ev.stopPropagation(); var dx = ev.clientX - sx, dy = ev.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - target.offsetWidth, ox + dx)), ny = Math.max(0, Math.min(window.innerHeight - 30, oy + dy)); target.style.left = nx + 'px'; target.style.top = ny + 'px'; if (isLauncher) target._dragged = moved; } + function mv(ev) { ev.stopPropagation(); var dx = ev.clientX - sx, dy = ev.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - target.offsetWidth, ox + dx)), ny = Math.max(0, Math.min(window.innerHeight - scalePx(30, uiFx), oy + dy)); target.style.left = nx + 'px'; target.style.top = ny + 'px'; if (isLauncher) target._dragged = moved; } function up(ev) { if (ev) { ev.stopPropagation(); ev.preventDefault(); } document.removeEventListener('mousemove', mv, true); document.removeEventListener('mouseup', up, true); if (onEnd) onEnd(parseInt(target.style.left), parseInt(target.style.top)); setTimeout(function () { if (isLauncher) target._dragged = false; }, 0); } document.addEventListener('mousemove', mv, true); document.addEventListener('mouseup', up, true); }); @@ -593,7 +628,7 @@ input::placeholder{color:var(--fg-text-faint)}\ makeDraggable(titlebar, panel, function (x, y) { st.x = x; st.y = y; store('panel', st); }); resize.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); var sx = e.clientX, sy = e.clientY, ow = panel.offsetWidth, oh = panel.offsetHeight; - function mv(ev) { ev.stopPropagation(); var w = Math.max(480, ow + ev.clientX - sx), h = Math.max(360, oh + ev.clientY - sy); panel.style.width = w + 'px'; panel.style.height = h + 'px'; if (activeRenderer && activeRenderer.onResize) activeRenderer.onResize(); } + function mv(ev) { ev.stopPropagation(); var w = Math.max(scalePx(480, uiFx), ow + ev.clientX - sx), h = Math.max(scalePx(360, uiFx), oh + ev.clientY - sy); panel.style.width = w + 'px'; panel.style.height = h + 'px'; if (activeRenderer && activeRenderer.onResize) activeRenderer.onResize(); } function up(ev) { if (ev) { ev.stopPropagation(); ev.preventDefault(); } document.removeEventListener('mousemove', mv, true); document.removeEventListener('mouseup', up, true); st.w = panel.offsetWidth; st.h = panel.offsetHeight; store('panel', st); } document.addEventListener('mousemove', mv, true); document.addEventListener('mouseup', up, true); }); @@ -605,7 +640,7 @@ input::placeholder{color:var(--fg-text-faint)}\ var sizer = el('div', { class: 'fg-list__sizer' }); var win = el('div', { class: 'fg-list__window' }); sizer.appendChild(win); listEl.appendChild(sizer); - var rowH = opts.rowH || 30, pool = [], model = [], view = [], query = ''; + var rowH = opts.rowH || scalePx(30, uiFx), pool = [], model = [], view = [], query = ''; // ONE delegated row click (recycled rows must not accumulate per-draw listeners). if (opts.onRowClick) listEl.addEventListener('click', function (e) { var r = e.target && e.target.closest ? e.target.closest('.fg-row') : null; if (r && r._item) opts.onRowClick(r._item); }); function rebuild() { model = opts.getModel() || []; applyFilter(); } @@ -1069,6 +1104,7 @@ input::placeholder{color:var(--fg-text-faint)}\ if (window.Forge) { window.Forge._hotkey = (P.Hotkey || 'F10').trim(); window.Forge._itemMaxOverride = Number(P.ItemMaxOverride) || 0; + window.Forge._uiScale = (P.UiScale || 'auto').trim(); // speed key: plugin-param is the first-run default; a key rebound in the panel (persisted) wins try { if (!localStorage.getItem('forge:speedKey')) window.Forge._speedKey = (P.SpeedKey || 'Control').trim(); } catch (e) { window.Forge._speedKey = (P.SpeedKey || 'Control').trim(); } } diff --git a/util/forge/Forge_MZ.js b/util/forge/Forge_MZ.js index 9ba72a7..4cc739f 100644 --- a/util/forge/Forge_MZ.js +++ b/util/forge/Forge_MZ.js @@ -400,7 +400,7 @@ }; // expose the assembled API (UI is attached below if a real DOM is present) - var MTC = { env: { IS_MZ: IS_MZ }, ops: ops, locks: locks, cheats: cheats, LOCK: LOCK, CHEAT: CHEAT, PARAM_NAMES: PARAM_NAMES, _itemMaxOverride: 0, _hotkey: 'F10', _speedKey: 'Control', _speedBoost: 2 }; + var MTC = { env: { IS_MZ: IS_MZ }, ops: ops, locks: locks, cheats: cheats, LOCK: LOCK, CHEAT: CHEAT, PARAM_NAMES: PARAM_NAMES, _itemMaxOverride: 0, _hotkey: 'F10', _speedKey: 'Control', _speedBoost: 2, _uiScale: 'auto' }; if (root) { root.Forge = MTC; root.__FORGE_CORE__ = true; } if (typeof module !== 'undefined' && module.exports) module.exports = MTC; @@ -430,12 +430,31 @@ if (!isNaN(n) && n > 0) return Math.max(0.75, Math.min(3, n)); } var base = 816; - var w = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : (window.innerWidth || base); + var gameW = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : 0; + var viewW = window.innerWidth || document.documentElement.clientWidth || base; + var w = Math.max(gameW || base, viewW); var scale = w / base; var dpr = window.devicePixelRatio || 1; if (dpr > 1.15) scale *= Math.min(2, 0.75 + dpr * 0.35); return Math.max(1, Math.min(2.75, scale)); } + function scalePx(n, fx) { + if (!fx || fx === 1) return n; + return Math.round(n * fx); + } + function scalePxCss(css, fx) { + if (!fx || fx === 1) return css; + return css.replace(/(\d+(?:\.\d+)?)px/g, function (_, n) { + return scalePx(parseFloat(n), fx) + 'px'; + }); + } + function scaleStyle(style, fx) { + if (!style || !fx || fx === 1) return style; + return style.replace(/(\d+(?:\.\d+)?)px/g, function (_, n) { + return scalePx(parseFloat(n), fx) + 'px'; + }); + } + var uiFx = resolveUiScale(API._uiScale || 'auto'); // persisted speed settings (UI-controlled; override core/param defaults if the user changed them) API._speedKey = load('speedKey', API._speedKey || 'Control'); API._speedUseKey = load('speedUseKey', true); @@ -448,7 +467,7 @@ if (k === 'class') n.className = props[k]; else if (k === 'text') n.textContent = props[k]; else if (k === 'html') n.innerHTML = props[k]; - else if (k === 'style') n.setAttribute('style', props[k]); + else if (k === 'style') n.setAttribute('style', scaleStyle(props[k], uiFx)); else if (k.slice(0, 2) === 'on' && typeof props[k] === 'function') n.addEventListener(k.slice(2).toLowerCase(), props[k]); else if (props[k] != null) n.setAttribute(k, props[k]); } @@ -539,14 +558,12 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- shadow host ---------- var host = el('div', { id: 'forge-host' }); - host.style.cssText = 'position:fixed!important;top:0!important;left:0!important;width:0!important;height:0!important;z-index:2147483647!important;margin:0!important;padding:0!important;border:0!important;pointer-events:none!important'; + host.style.cssText = 'position:fixed!important;top:0!important;left:0!important;width:0!important;height:0!important;z-index:2147483647!important;margin:0!important;padding:0!important;border:0!important;pointer-events:none!important;overflow:visible!important'; document.body.appendChild(host); var shadow = host.attachShadow({ mode: 'open' }); var rootEl = el('div', { id: 'forge-root' }); rootEl.setAttribute('data-theme', load('theme', 'dark')); - var uiScale = resolveUiScale(API._uiScale || 'auto'); - if (uiScale !== 1) rootEl.style.zoom = String(uiScale); - shadow.appendChild(el('style', { text: CSS })); + shadow.appendChild(el('style', { text: scalePxCss(CSS, uiFx) })); shadow.appendChild(rootEl); // Swallow events so panel interaction never leaks to the game. BUBBLE phase: the panel's own @@ -581,8 +598,8 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- panel skeleton ---------- var vw = window.innerWidth || 816, vh = window.innerHeight || 624; // Default size fits the window (many MV games are ~816x624); leave a margin. - var st = load('panel', { x: 16, y: 16, w: Math.min(880, vw - 32), h: Math.min(560, vh - 32) }); - st.w = Math.max(420, Math.min(st.w, vw - 8)); st.h = Math.max(320, Math.min(st.h, vh - 8)); + var st = load('panel', { x: scalePx(16, uiFx), y: scalePx(16, uiFx), w: Math.min(scalePx(880, uiFx), vw - scalePx(32, uiFx)), h: Math.min(scalePx(560, uiFx), vh - scalePx(32, uiFx)) }); + st.w = Math.max(scalePx(420, uiFx), Math.min(st.w, vw - scalePx(8, uiFx))); st.h = Math.max(scalePx(320, uiFx), Math.min(st.h, vh - scalePx(8, uiFx))); st.x = Math.max(0, Math.min(st.x, vw - st.w)); st.y = Math.max(0, Math.min(st.y, vh - st.h)); // keep fully on-screen var panel = el('div', { class: 'fg-panel' }); panel.style.left = st.x + 'px'; panel.style.top = st.y + 'px'; panel.style.width = st.w + 'px'; panel.style.height = st.h + 'px'; @@ -605,8 +622,8 @@ input::placeholder{color:var(--fg-text-faint)}\ // ---------- launcher (docks to bottom-right corner by default; 'launcherPos' is a fresh key so // any stale mid-screen position from older builds is ignored) ---------- - var lp = load('launcherPos', { x: vw - 56, y: vh - 56 }); - lp.x = Math.max(4, Math.min(lp.x, vw - 46)); lp.y = Math.max(4, Math.min(lp.y, vh - 46)); + var lp = load('launcherPos', { x: vw - scalePx(56, uiFx), y: vh - scalePx(56, uiFx) }); + lp.x = Math.max(scalePx(4, uiFx), Math.min(lp.x, vw - scalePx(46, uiFx))); lp.y = Math.max(scalePx(4, uiFx), Math.min(lp.y, vh - scalePx(46, uiFx))); var launcher = el('button', { class: 'fg-launcher', text: '⚒', title: 'Forge (F10)' }); launcher.style.left = lp.x + 'px'; launcher.style.top = lp.y + 'px'; launcher.addEventListener('click', function (e) { if (!launcher._dragged) toggle(); }); @@ -619,7 +636,7 @@ input::placeholder{color:var(--fg-text-faint)}\ if (e.target.closest && e.target.closest('input,button.fg-iconbtn')) return; e.preventDefault(); e.stopPropagation(); var sx = e.clientX, sy = e.clientY, ox = parseInt(target.style.left) || 0, oy = parseInt(target.style.top) || 0, moved = false; if (isLauncher) target._dragged = false; - function mv(ev) { ev.stopPropagation(); var dx = ev.clientX - sx, dy = ev.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - target.offsetWidth, ox + dx)), ny = Math.max(0, Math.min(window.innerHeight - 30, oy + dy)); target.style.left = nx + 'px'; target.style.top = ny + 'px'; if (isLauncher) target._dragged = moved; } + function mv(ev) { ev.stopPropagation(); var dx = ev.clientX - sx, dy = ev.clientY - sy; if (Math.abs(dx) + Math.abs(dy) > 3) moved = true; var nx = Math.max(0, Math.min(window.innerWidth - target.offsetWidth, ox + dx)), ny = Math.max(0, Math.min(window.innerHeight - scalePx(30, uiFx), oy + dy)); target.style.left = nx + 'px'; target.style.top = ny + 'px'; if (isLauncher) target._dragged = moved; } function up(ev) { if (ev) { ev.stopPropagation(); ev.preventDefault(); } document.removeEventListener('mousemove', mv, true); document.removeEventListener('mouseup', up, true); if (onEnd) onEnd(parseInt(target.style.left), parseInt(target.style.top)); setTimeout(function () { if (isLauncher) target._dragged = false; }, 0); } document.addEventListener('mousemove', mv, true); document.addEventListener('mouseup', up, true); }); @@ -627,7 +644,7 @@ input::placeholder{color:var(--fg-text-faint)}\ makeDraggable(titlebar, panel, function (x, y) { st.x = x; st.y = y; store('panel', st); }); resize.addEventListener('mousedown', function (e) { e.preventDefault(); e.stopPropagation(); var sx = e.clientX, sy = e.clientY, ow = panel.offsetWidth, oh = panel.offsetHeight; - function mv(ev) { ev.stopPropagation(); var w = Math.max(480, ow + ev.clientX - sx), h = Math.max(360, oh + ev.clientY - sy); panel.style.width = w + 'px'; panel.style.height = h + 'px'; if (activeRenderer && activeRenderer.onResize) activeRenderer.onResize(); } + function mv(ev) { ev.stopPropagation(); var w = Math.max(scalePx(480, uiFx), ow + ev.clientX - sx), h = Math.max(scalePx(360, uiFx), oh + ev.clientY - sy); panel.style.width = w + 'px'; panel.style.height = h + 'px'; if (activeRenderer && activeRenderer.onResize) activeRenderer.onResize(); } function up(ev) { if (ev) { ev.stopPropagation(); ev.preventDefault(); } document.removeEventListener('mousemove', mv, true); document.removeEventListener('mouseup', up, true); st.w = panel.offsetWidth; st.h = panel.offsetHeight; store('panel', st); } document.addEventListener('mousemove', mv, true); document.addEventListener('mouseup', up, true); }); @@ -639,7 +656,7 @@ input::placeholder{color:var(--fg-text-faint)}\ var sizer = el('div', { class: 'fg-list__sizer' }); var win = el('div', { class: 'fg-list__window' }); sizer.appendChild(win); listEl.appendChild(sizer); - var rowH = opts.rowH || 30, pool = [], model = [], view = [], query = ''; + var rowH = opts.rowH || scalePx(30, uiFx), pool = [], model = [], view = [], query = ''; // ONE delegated row click (recycled rows must not accumulate per-draw listeners). if (opts.onRowClick) listEl.addEventListener('click', function (e) { var r = e.target && e.target.closest ? e.target.closest('.fg-row') : null; if (r && r._item) opts.onRowClick(r._item); }); function rebuild() { model = opts.getModel() || []; applyFilter(); } diff --git a/util/forge/config.py b/util/forge/config.py index ac894d1..3457e51 100644 --- a/util/forge/config.py +++ b/util/forge/config.py @@ -1,4 +1,4 @@ -"""Forge plugin build — hotkey (and MZ UI scale) injection at install time.""" +"""Forge plugin build — hotkey and UI scale injection at install/apply time.""" from __future__ import annotations @@ -35,9 +35,9 @@ def _js_literal(value) -> str: def plugin_entry(engine: str, hotkey: str, ui_scale: str = "auto") -> str: hk = _js_literal(hotkey.strip() or "F10") + scale = _js_literal(ui_scale.strip() or "auto") name = PLUGIN_BY_ENGINE[engine] if engine == "MZ": - scale = _js_literal(ui_scale.strip() or "auto") return ( f' {{ "name": "{name}", "status": true, ' f'"description": "Forge — in-game cheat & editor overlay", ' @@ -48,7 +48,7 @@ def plugin_entry(engine: str, hotkey: str, ui_scale: str = "auto") -> str: f' {{ "name": "{name}", "status": true, ' f'"description": "Forge — in-game cheat & editor overlay", ' f'"parameters": {{ "Hotkey": {hk}, "SpeedKey": "Control", ' - f'"StartOpen": "false", "ItemMaxOverride": "0" }} }}' + f'"StartOpen": "false", "ItemMaxOverride": "0", "UiScale": {scale} }} }}' ) @@ -63,8 +63,6 @@ def _patch_forge_hotkey(forge_text: str, hotkey: str, engine: str) -> str: forge_text, n = re.subn(pattern, rf"\g<1>{hk}", forge_text, count=1) if n == 0: raise ValueError("Could not patch @default hotkey in Forge_MZ.js") - forge_text = re.sub(r"\(P\.hotkey \|\| 'F10'\)", f"(P.hotkey || '{hk}')", forge_text) - forge_text = re.sub(r"API\._hotkey \|\| 'F10'", f"API._hotkey || '{hk}'", forge_text) return forge_text pattern = ( @@ -75,28 +73,83 @@ def _patch_forge_hotkey(forge_text: str, hotkey: str, engine: str) -> str: forge_text, n = re.subn(pattern, rf"\g<1>{hk}", forge_text, count=1) if n == 0: raise ValueError("Could not patch @default Hotkey in Forge_MV.js") - forge_text = re.sub(r"\(P\.Hotkey \|\| 'F10'\)", f"(P.Hotkey || '{hk}')", forge_text) + return forge_text + + +def _patch_forge_ui_scale_default(forge_text: str, ui_scale: str, engine: str) -> str: + scale = ui_scale.strip() or "auto" + if engine == "MZ": + pattern = ( + r"(\* @param uiScale\s*\n(?:\s*\*[^\n]*\n)*?\s*\* @default )auto" + ) + if scale != "auto": + forge_text, n = re.subn(pattern, rf"\g<1>{scale}", forge_text, count=1) + if n == 0: + raise ValueError("Could not patch @default uiScale in Forge_MZ.js") + return forge_text + + pattern = ( + r"(\* @param UiScale\s*\n(?:\s*\*[^\n]*\n)*?\s*\* @default )auto" + ) + if scale != "auto": + forge_text, n = re.subn(pattern, rf"\g<1>{scale}", forge_text, count=1) + if n == 0: + raise ValueError("Could not patch @default UiScale in Forge_MV.js") + return forge_text + + +def _patch_forge_mtc_defaults(forge_text: str, hotkey: str, ui_scale: str) -> str: + hk = json.dumps(hotkey.strip() or "F10") + scale = json.dumps(str(ui_scale or "auto").strip() or "auto") + forge_text = re.sub(r"_hotkey: 'F10'", f"_hotkey: {hk}", forge_text, count=1) + forge_text = re.sub(r"_uiScale: 'auto'", f"_uiScale: {scale}", forge_text, count=1) + return forge_text + + +def _patch_forge_runtime(forge_text: str, hotkey: str, ui_scale: str, engine: str) -> str: + """Bake hotkey/scale into the plugin file (plugins.js params alone are not enough).""" + hk = json.dumps(hotkey.strip() or "F10") + scale = json.dumps(str(ui_scale or "auto").strip() or "auto") + if engine == "MZ": + forge_text = re.sub( + r"window\.Forge\._hotkey = \(P\.hotkey \|\| '[^']*'\)\.trim\(\);", + f"window.Forge._hotkey = {hk};", + forge_text, + count=1, + ) + forge_text = re.sub( + r"window\.Forge\._uiScale = \(P\.uiScale \|\| '[^']*'\)\.trim\(\);", + f"window.Forge._uiScale = {scale};", + forge_text, + count=1, + ) + return forge_text + + forge_text = re.sub( + r"window\.Forge\._hotkey = \(P\.Hotkey \|\| '[^']*'\)\.trim\(\);", + f"window.Forge._hotkey = {hk};", + forge_text, + count=1, + ) + forge_text = re.sub( + r"window\.Forge\._uiScale = \(P\.UiScale \|\| '[^']*'\)\.trim\(\);", + f"window.Forge._uiScale = {scale};", + forge_text, + count=1, + ) return forge_text def prepare_forge_js(engine: str, source: Path | None = None, cfg: dict | None = None) -> str: - """Build Forge_MV.js or Forge_MZ.js with configured hotkey (and MZ UI scale).""" + """Build Forge_MV.js or Forge_MZ.js with configured hotkey and UI scale.""" src = source or bundled_plugin_path(engine) effective = {**load_config(), **(cfg or {})} - text = _patch_forge_hotkey(src.read_text(encoding="utf-8"), effective["forgeHotkey"], engine) - if engine != "MZ": - return text - - scale = effective.get("uiScale", "auto") - text, n = re.subn( - r"(\* @param uiScale\s*\n(?:\s*\*[^\n]*\n)*?\s*\* @default )auto", - rf"\g<1>{scale}", - text, - count=1, - ) - if n == 0: - raise ValueError("Could not patch @default uiScale in Forge_MZ.js") - text = re.sub(r"\(P\.uiScale \|\| 'auto'\)", f"(P.uiScale || '{scale}')", text) + hotkey = effective.get("forgeHotkey", "F10") + ui_scale = effective.get("uiScale", "auto") + text = _patch_forge_hotkey(src.read_text(encoding="utf-8"), hotkey, engine) + text = _patch_forge_ui_scale_default(text, str(ui_scale), engine) + text = _patch_forge_mtc_defaults(text, hotkey, str(ui_scale)) + text = _patch_forge_runtime(text, hotkey, str(ui_scale), engine) return text diff --git a/util/forge/installer.py b/util/forge/installer.py index cd340ab..beea151 100644 --- a/util/forge/installer.py +++ b/util/forge/installer.py @@ -89,6 +89,81 @@ def status(game_root: Path) -> dict: } +def _find_plugin_block_span(content: str, plugin_name: str) -> tuple[int, int] | None: + """Return [start, end) span of the plugin object in plugins.js, or None.""" + m = re.search(rf'"name"\s*:\s*"{re.escape(plugin_name)}"', content) + if not m: + return None + start = content.rfind("{", 0, m.start()) + if start < 0: + return None + depth = 0 + for i in range(start, len(content)): + ch = content[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + end = i + 1 + while end < len(content) and content[end] in " \t\r\n": + end += 1 + if end < len(content) and content[end] == ",": + end += 1 + return start, end + return None + + +def _remove_plugin_block(content: str, plugin_name: str) -> str: + compact = rf"\{{[^{{}}]*\"name\"\s*:\s*\"{re.escape(plugin_name)}\"[^{{}}]*\}}\s*,?" + content = re.sub(compact, "", content) + while True: + span = _find_plugin_block_span(content, plugin_name) + if span is None: + break + start, end = span + before = content[:start].rstrip() + after = content[end:].lstrip() + if before.endswith(","): + before = before[:-1].rstrip() + elif after.startswith(","): + after = after[1:].lstrip() + gap = "\n" if before and after and not before.endswith("\n") else "" + content = before + gap + after + lines = [ + line + for line in re.split(r"\r?\n", content) + if not re.search(rf"\"name\"\s*:\s*\"{re.escape(plugin_name)}\"", line) + ] + content = "\n".join(lines) + # Leftover fragments from a previously corrupted multi-line entry. + content = re.sub( + r"\{\s*\"status\"\s*:\s*true\s*,\s*\"description\"\s*:\s*\"Forge[^\"]*\"" + r"\s*,\s*\"parameters\"\s*:\s*\{[^{}]*\}\s*\}\s*,?", + "", + content, + ) + return content + + +def _upsert_plugin_entry(content: str, plugin_name: str, entry: str, nl: str) -> str: + """Insert or replace a Forge plugin entry in plugins.js.""" + span = _find_plugin_block_span(content, plugin_name) + if span is not None: + start, end = span + before = content[:start].rstrip() + after = content[end:].lstrip() + sep = nl if before.endswith(",") or not before else "," + nl + return before + sep + entry + nl + after + idx = content.rfind("];") + if idx < 0: + raise ValueError("Could not find plugin list end ( ]; ) in plugins.js.") + before = content[:idx].rstrip() + after = content[idx:] + sep = nl if before.endswith(",") else "," + nl + return before + sep + entry + nl + " " + after + + def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = None) -> tuple[bool, str]: """Copy Forge_MV.js or Forge_MZ.js into the game and declare it in plugins.js.""" info = detect_engine(game_root) @@ -113,15 +188,11 @@ def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = N hotkey = (cfg or {}).get("forgeHotkey", "F10") ui_scale = (cfg or {}).get("uiScale", "auto") entry = plugin_entry(engine, hotkey, ui_scale) - if not _is_declared(content, plugin_name): - idx = content.rfind("];") - if idx < 0: - return False, "Could not find plugin list end ( ]; ) in plugins.js." - before = content[:idx].rstrip() - after = content[idx:] - sep = nl if before.endswith(",") else "," + nl - content = before + sep + entry + nl + " " + after + try: + content = _upsert_plugin_entry(content, plugin_name, entry, nl) plugins_js.write_text(content, encoding="utf-8", newline="") + except ValueError as exc: + return False, str(exc) return True, f"Forge installed for RPG Maker {engine}. Press {hotkey} in-game to open." @@ -138,13 +209,9 @@ def uninstall(game_root: Path) -> tuple[bool, str]: content, nl = _read_plugins_js(plugins_js) if _is_declared(content, plugin_name): - kept = [ - line for line in re.split(r"\r?\n", content) - if not re.search(rf'"name"\s*:\s*"{re.escape(plugin_name)}"', line) - ] - new_text = nl.join(kept) - new_text = re.sub(r",(\s*)\];", r"\1];", new_text) - plugins_js.write_text(new_text, encoding="utf-8", newline="") + content = _remove_plugin_block(content, plugin_name) + content = re.sub(r",(\s*)\];", r"\1];", content) + plugins_js.write_text(content, encoding="utf-8", newline="") if target.is_file(): target.unlink() diff --git a/util/tl_inspector/TLInspector.js b/util/tl_inspector/TLInspector.js index 8c7b2b0..ba03d32 100644 --- a/util/tl_inspector/TLInspector.js +++ b/util/tl_inspector/TLInspector.js @@ -84,8 +84,9 @@ if (!isNaN(n) && n > 0) { return Math.max(0.75, Math.min(3, n)); } } var base = 816; - var w = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : - (window.innerWidth || base); + var gameW = (typeof Graphics !== 'undefined' && Graphics.width) ? Graphics.width : 0; + var viewW = window.innerWidth || document.documentElement.clientWidth || base; + var w = Math.max(gameW || base, viewW); var scale = w / base; var dpr = window.devicePixelRatio || 1; if (dpr > 1.15) { scale *= Math.min(2, 0.75 + dpr * 0.35); }