Fetch forge from len

This commit is contained in:
DazedAnon 2026-06-13 16:48:55 -05:00
parent 79e099ad08
commit aa6f147db6
12 changed files with 2446 additions and 185 deletions

3
.gitignore vendored
View file

@ -23,6 +23,9 @@ __pycache__
!util/tl_inspector/TLInspector.js
!util/forge/Forge_MZ.js
!util/forge/Forge_MV.js
!util/forge/upstream/Forge_MZ.js
!util/forge/upstream/Forge_MV.js
util/forge/.forge_version.json
util/ace/*.exe
util/ace/.tools_version.json
!util/ace/offline/*.exe

View file

@ -186,6 +186,13 @@ if errorlevel 1 (
)
echo.
echo Checking Forge plugins...
python -m util.forge.update_tools
if errorlevel 1 (
echo WARNING: Forge update failed. Playtest Forge features use the bundled upstream copy.
)
echo.
:: Launch the GUI
echo ==========================================
echo Launching DazedMTLTool GUI...

View file

@ -226,6 +226,12 @@ if ! python -m util.ace.update_tools; then
fi
echo
echo "Checking Forge plugins..."
if ! python -m util.forge.update_tools; then
echo "WARNING: Forge update failed. Playtest Forge features use the bundled upstream copy."
fi
echo
echo "=========================================="
echo " Launching DazedMTLTool GUI..."
echo "=========================================="

View file

@ -138,6 +138,12 @@ class UpdateThread(QThread):
ensure_ace_tools(log_fn=lambda m: self.progress.emit(m))
except Exception as exc:
self.progress.emit(f"Ace tool update skipped: {exc}")
try:
from util.forge.update_tools import ensure_forge_plugins
self.progress.emit("Updating Forge plugins…")
ensure_forge_plugins(log_fn=lambda m: self.progress.emit(m))
except Exception as exc:
self.progress.emit(f"Forge update skipped: {exc}")
self.finished.emit(True, f"updated:{latest_sha[:8]}")

View file

@ -49,6 +49,12 @@ def main():
ensure_ace_tools()
except Exception as exc:
print(f"Warning: Ace tool setup failed ({exc}). Ace features may be unavailable.")
try:
from util.forge.update_tools import ensure_forge_plugins
ensure_forge_plugins()
except Exception as exc:
print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.")
# Import and run GUI
try:

View file

@ -3,8 +3,7 @@
//=============================================================================
/*:
* @plugindesc Forge modern in-game editor: variables, switches, items, actors, gold, teleport, common events, freeze/lock & battle cheats. (MV)
* @author len
* @url https://gitgud.io/zero64801/forge-mvmz
* @author len (Forge - clean-room, MTool-inspired)
*
* @param Hotkey
* @desc Key to toggle the panel.
@ -22,10 +21,6 @@
* @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.
@ -35,8 +30,6 @@
*
* All edits are RAM-only and never written to save files. No external requests.
* Plugin command: MTC open (toggles the panel)
*
* Credits: len https://gitgud.io/zero64801/forge-mvmz
*/
/* ===================================================================================
@ -384,7 +377,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, _uiScale: 'auto' };
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 };
if (root) { root.Forge = MTC; root.__FORGE_CORE__ = true; }
if (typeof module !== 'undefined' && module.exports) module.exports = MTC;
@ -408,37 +401,6 @@
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);
@ -451,7 +413,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', scaleStyle(props[k], uiFx));
else if (k === 'style') n.setAttribute('style', props[k]);
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]);
}
@ -537,17 +499,19 @@
.fg-confirm__box{background:var(--fg-bg-elev);border:1px solid var(--fg-border);border-radius:var(--rad-lg);padding:var(--sp-4);max-width:340px;box-shadow:var(--sh-panel)}\
.fg-confirm__box p{margin:0 0 var(--sp-4)}.fg-confirm__row{display:flex;justify-content:flex-end;gap:var(--sp-2)}\
input::placeholder{color:var(--fg-text-faint)}\
.fg-list::-webkit-scrollbar,.fg-rail::-webkit-scrollbar,.fg-detail::-webkit-scrollbar{width:9px}\
.fg-list::-webkit-scrollbar-thumb,.fg-detail::-webkit-scrollbar-thumb{background:var(--fg-border-strong);border-radius:5px}";
::-webkit-scrollbar{width:9px;height:9px}\
::-webkit-scrollbar-track{background:transparent}\
::-webkit-scrollbar-thumb{background:var(--fg-border-strong);border-radius:5px}\
::-webkit-scrollbar-thumb:hover{background: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;overflow:visible!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';
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: scalePxCss(CSS, uiFx) }));
shadow.appendChild(el('style', { text: CSS }));
shadow.appendChild(rootEl);
// Swallow events so panel interaction never leaks to the game. BUBBLE phase: the panel's own
@ -582,8 +546,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: 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)));
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));
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';
@ -606,8 +570,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 - 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 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 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(); });
@ -620,7 +584,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 - scalePx(30, uiFx), 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 - 30, 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);
});
@ -628,7 +592,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(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 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 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);
});
@ -640,7 +604,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 || scalePx(30, uiFx), pool = [], model = [], view = [], query = '';
var rowH = opts.rowH || 30, 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(); }
@ -1104,7 +1068,6 @@ 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(); }
}

View file

@ -4,8 +4,8 @@
/*:
* @target MZ
* @plugindesc Forge modern in-game editor: variables, switches, items, actors, gold, teleport, common events, freeze/lock & battle cheats. (MZ)
* @author len
* @url https://gitgud.io/zero64801/forge-mvmz
* @author len (Forge - clean-room, MTool-inspired)
* @url
*
* @param hotkey
* @text Toggle Hotkey
@ -32,12 +32,6 @@
* @min 0
* @default 0
*
* @param uiScale
* @text UI Scale
* @desc Overlay size. Use auto to match game width, or a number like 1.5 or 2.
* @type string
* @default auto
*
* @command open
* @text Open / Toggle Panel
* @desc Toggle the Forge overlay.
@ -51,8 +45,6 @@
*
* All edits are RAM-only and never written to save files. No external requests.
* Plugin command "Open / Toggle Panel" toggles the overlay from events.
*
* Credits: len https://gitgud.io/zero64801/forge-mvmz
*/
/* ===================================================================================
@ -400,7 +392,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, _uiScale: 'auto' };
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 };
if (root) { root.Forge = MTC; root.__FORGE_CORE__ = true; }
if (typeof module !== 'undefined' && module.exports) module.exports = MTC;
@ -424,37 +416,6 @@
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);
@ -467,7 +428,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', scaleStyle(props[k], uiFx));
else if (k === 'style') n.setAttribute('style', props[k]);
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]);
}
@ -553,17 +514,19 @@
.fg-confirm__box{background:var(--fg-bg-elev);border:1px solid var(--fg-border);border-radius:var(--rad-lg);padding:var(--sp-4);max-width:340px;box-shadow:var(--sh-panel)}\
.fg-confirm__box p{margin:0 0 var(--sp-4)}.fg-confirm__row{display:flex;justify-content:flex-end;gap:var(--sp-2)}\
input::placeholder{color:var(--fg-text-faint)}\
.fg-list::-webkit-scrollbar,.fg-rail::-webkit-scrollbar,.fg-detail::-webkit-scrollbar{width:9px}\
.fg-list::-webkit-scrollbar-thumb,.fg-detail::-webkit-scrollbar-thumb{background:var(--fg-border-strong);border-radius:5px}";
::-webkit-scrollbar{width:9px;height:9px}\
::-webkit-scrollbar-track{background:transparent}\
::-webkit-scrollbar-thumb{background:var(--fg-border-strong);border-radius:5px}\
::-webkit-scrollbar-thumb:hover{background: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;overflow:visible!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';
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: scalePxCss(CSS, uiFx) }));
shadow.appendChild(el('style', { text: CSS }));
shadow.appendChild(rootEl);
// Swallow events so panel interaction never leaks to the game. BUBBLE phase: the panel's own
@ -598,8 +561,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: 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)));
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));
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';
@ -622,8 +585,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 - 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 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 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(); });
@ -636,7 +599,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 - scalePx(30, uiFx), 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 - 30, 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);
});
@ -644,7 +607,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(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 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 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);
});
@ -656,7 +619,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 || scalePx(30, uiFx), pool = [], model = [], view = [], query = '';
var rowH = opts.rowH || 30, 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(); }
@ -1119,7 +1082,6 @@ 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();
try { if (!localStorage.getItem('forge:speedKey')) window.Forge._speedKey = (P.speedKey || 'Control').trim(); } catch (e) { window.Forge._speedKey = (P.speedKey || 'Control').trim(); }
}
if (typeof PluginManager !== 'undefined' && PluginManager.registerCommand) {

View file

@ -1,21 +1 @@
"""Forge — in-game cheat & editor overlay for RPG Maker MV/MZ (plugin by len)."""
from util.forge.installer import (
apply_config,
bundled_plugin_path,
detect_engine,
detect_mz,
install,
status,
uninstall,
)
__all__ = [
"apply_config",
"bundled_plugin_path",
"detect_engine",
"detect_mz",
"install",
"status",
"uninstall",
]

View file

@ -1,4 +1,4 @@
"""Forge plugin build — hotkey and UI scale injection at install/apply time."""
"""Forge plugin build — hotkey injection at install/apply time."""
from __future__ import annotations
@ -34,21 +34,21 @@ def _js_literal(value) -> str:
def plugin_entry(engine: str, hotkey: str, ui_scale: str = "auto") -> str:
_ = ui_scale # reserved; vanilla Forge has no UI scale param yet
hk = _js_literal(hotkey.strip() or "F10")
scale = _js_literal(ui_scale.strip() or "auto")
name = PLUGIN_BY_ENGINE[engine]
if engine == "MZ":
return (
f' {{ "name": "{name}", "status": true, '
f'"description": "Forge — in-game cheat & editor overlay", '
f'"parameters": {{ "hotkey": {hk}, "speedKey": "Control", '
f'"startOpen": "false", "itemMaxOverride": "0", "uiScale": {scale} }} }}'
f'"startOpen": "false", "itemMaxOverride": "0" }} }}'
)
return (
f' {{ "name": "{name}", "status": true, '
f'"description": "Forge — in-game cheat & editor overlay", '
f'"parameters": {{ "Hotkey": {hk}, "SpeedKey": "Control", '
f'"StartOpen": "false", "ItemMaxOverride": "0", "UiScale": {scale} }} }}'
f'"StartOpen": "false", "ItemMaxOverride": "0" }} }}'
)
@ -76,80 +76,38 @@ def _patch_forge_hotkey(forge_text: str, hotkey: str, engine: str) -> str:
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:
def _patch_forge_runtime_hotkey(forge_text: str, hotkey: str, engine: 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(
forge_text, n = re.subn(
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,
)
if n == 0:
raise ValueError("Could not patch runtime hotkey in Forge_MZ.js")
return forge_text
forge_text = re.sub(
forge_text, n = re.subn(
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,
)
if n == 0:
raise ValueError("Could not patch runtime hotkey in Forge_MV.js")
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 UI scale."""
"""Build Forge_MV.js or Forge_MZ.js with configured hotkey."""
src = source or bundled_plugin_path(engine)
effective = {**load_config(), **(cfg or {})}
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)
text = src.read_text(encoding="utf-8")
text = _patch_forge_hotkey(text, hotkey, engine)
text = _patch_forge_runtime_hotkey(text, hotkey, engine)
return text

191
util/forge/update_tools.py Normal file
View file

@ -0,0 +1,191 @@
"""Download / update Forge plugins from len's upstream repo (gitgud.io).
Upstream: https://gitgud.io/zero64801/forge-mvmz
Offline copies: util/forge/upstream/
Active plugins: util/forge/Forge_MZ.js, util/forge/Forge_MV.js
"""
from __future__ import annotations
import json
import shutil
import sys
import urllib.parse
import urllib.request
from pathlib import Path
_PKG_ROOT = Path(__file__).resolve().parent
UPSTREAM_DIR = _PKG_ROOT / "upstream"
PLUGIN_BY_ENGINE = {
"MV": "Forge_MV",
"MZ": "Forge_MZ",
}
FORGE_PROJECT = "zero64801/forge-mvmz"
FORGE_BRANCH = "master"
GITGUD_API = "https://gitgud.io/api/v4"
VERSION_FILE = _PKG_ROOT / ".forge_version.json"
USER_AGENT = "DazedMTLTool"
def bundled_plugin_path(engine: str) -> Path:
name = PLUGIN_BY_ENGINE.get(engine)
if not name:
raise ValueError(f"Unsupported engine: {engine}")
return _PKG_ROOT / f"{name}.js"
def upstream_plugin_path(engine: str) -> Path:
return UPSTREAM_DIR / f"{PLUGIN_BY_ENGINE[engine]}.js"
def _load_versions() -> dict:
if not VERSION_FILE.is_file():
return {}
try:
return json.loads(VERSION_FILE.read_text(encoding="utf-8"))
except Exception:
return {}
def _save_versions(data: dict) -> None:
VERSION_FILE.write_text(json.dumps(data, indent=2), encoding="utf-8")
def _api_json(url: str) -> dict | list:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode("utf-8"))
def _download(url: str, dest: Path) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
tmp = dest.with_suffix(dest.suffix + ".part")
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp, open(tmp, "wb") as fh:
shutil.copyfileobj(resp, fh)
tmp.replace(dest)
def _project_id() -> str:
return urllib.parse.quote(FORGE_PROJECT, safe="")
def _upstream_commit() -> str:
url = f"{GITGUD_API}/projects/{_project_id()}/repository/commits/{FORGE_BRANCH}"
data = _api_json(url)
commit = data.get("id") or data.get("sha") or ""
if not commit:
raise RuntimeError(f"Could not resolve upstream commit for {FORGE_PROJECT}")
return commit
def _raw_file_url(filename: str) -> str:
enc = urllib.parse.quote(filename, safe="")
return (
f"{GITGUD_API}/projects/{_project_id()}/repository/files/{enc}/raw"
f"?ref={FORGE_BRANCH}"
)
def _log(msg: str, log_fn) -> None:
if log_fn:
log_fn(msg)
else:
print(msg, flush=True)
def _seed_from_offline(engine: str, log_fn) -> bool:
"""Copy bundled offline copy into the active plugin path if missing."""
dest = bundled_plugin_path(engine)
if dest.is_file():
return True
offline = upstream_plugin_path(engine)
if not offline.is_file():
return False
shutil.copy2(offline, dest)
_log(f"Using offline bundled {dest.name}", log_fn)
return True
def _fetch_plugin(engine: str, log_fn) -> None:
name = PLUGIN_BY_ENGINE[engine]
_log(f"Downloading {name}.js from {FORGE_PROJECT}...", log_fn)
data = _download_bytes(_raw_file_url(f"{name}.js"))
bundled_plugin_path(engine).write_bytes(data)
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
upstream_plugin_path(engine).write_bytes(data)
def _download_bytes(url: str) -> bytes:
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(req, timeout=600) as resp:
return resp.read()
def refresh_forge_plugins(log_fn=print) -> bool:
"""Download len's latest Forge_MV.js and Forge_MZ.js."""
try:
commit = _upstream_commit()
except Exception as exc:
_log(f"ERROR: could not contact upstream ({exc})", log_fn)
return False
ok = True
for engine in PLUGIN_BY_ENGINE:
try:
_fetch_plugin(engine, log_fn)
except Exception as exc:
_log(f"ERROR: download failed for {PLUGIN_BY_ENGINE[engine]} ({exc})", log_fn)
ok = False
if not ok:
return False
versions = _load_versions()
versions["commit"] = commit
versions["branch"] = FORGE_BRANCH
_save_versions(versions)
_log(f"Forge plugins updated ({commit[:12]})", log_fn)
return True
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
"""Ensure Forge plugins are present; fetch upstream when missing or stale."""
for engine in PLUGIN_BY_ENGINE:
_seed_from_offline(engine, log_fn)
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
versions = _load_versions()
local_commit = versions.get("commit", "")
need_fetch = bool(missing) or force
if not need_fetch:
try:
need_fetch = _upstream_commit() != local_commit
except Exception as exc:
if missing:
_log(f"ERROR: Forge upstream unavailable ({exc})", log_fn)
return False
_log(f"Warning: could not check Forge update ({exc}); using local copy.", log_fn)
if need_fetch:
if refresh_forge_plugins(log_fn=log_fn):
return True
return all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
return True
def main() -> int:
if "--refresh-offline" in sys.argv:
UPSTREAM_DIR.mkdir(parents=True, exist_ok=True)
ok = refresh_forge_plugins()
return 0 if ok else 1
force = "--force" in sys.argv or "-f" in sys.argv
return 0 if ensure_forge_plugins(force=force) else 1
if __name__ == "__main__":
raise SystemExit(main())

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff