diff --git a/data/Map006.json b/data/Map006.json index 8751700..fb3e027 100644 --- a/data/Map006.json +++ b/data/Map006.json @@ -14538,7 +14538,7 @@ "code": 401, "indent": 0, "parameters": [ - "\"Phew... It took more time than I expected. Good shit sakura and kaoss\"" + "THE GOAT" ] }, { diff --git a/data/System.json b/data/System.json index 3b63643..ce9a512 100644 --- a/data/System.json +++ b/data/System.json @@ -524,7 +524,7 @@ "Sort", "Save", "Quit Game", - "Options", + "Suc", "Weapon", "Armor", "Key Items", @@ -532,7 +532,7 @@ "Optimize", "Remove All", "New Game", - "Good Job", + "Continue Not", null, "To Title", "Quit", diff --git a/js/plugins/TLInspector.js b/js/plugins/TLInspector.js index 56dc4ce..3eeb625 100644 --- a/js/plugins/TLInspector.js +++ b/js/plugins/TLInspector.js @@ -24,6 +24,18 @@ * "Open in VSCode" button (runs `code -g file:line`) and an **edit** button to * change the text and save it back to the source file without leaving the game. * + * Live refresh (NW.js playtest only): + * In-game overlay "save to file" reloads that JSON into memory immediately. + * VSCode / external saves: we watch data/*.json; once the file mtime is stable + * (save finished), reload instantly. The game cannot hook Ctrl+S in VSCode itself. + * F9 — manual reload of all database JSON + the current map + * + * Limitations: + * - NW.js desktop playtest only (not browser) + * - Text already shown by a running event won't update until re-triggered + * (in-game overlay edit still updates the current message box) + * - Plugin .js / plugins.js changes still need F5 + * * ---------------------------------------------------------------------------- * Install: drop this file in www/js/plugins/ and add one line to plugins.js: * { "name": "TLInspector", "status": true, "description": "TL source inspector", "parameters": {} } @@ -52,6 +64,10 @@ captureUiText: true, // capture plugin / menu / UI text drawn via Bitmap.drawText // (so e.g. status-window strings from plugins.js are locatable) liveEdit: true, // allow saving edited text back to the source file in-game + liveRefresh: true, // auto-reload data/*.json when files change on disk (NW.js) + liveRefreshStableMs: 120,// VSCode save: reload once mtime unchanged this long + liveRefreshPollMs: 400, // how often we check data/*.json for external saves + liveRefreshHotkey: 'F9',// manual reload: all DB JSON + current map dataDirOverride: null // set an absolute path to force the data dir }; @@ -366,6 +382,7 @@ fs.writeFileSync(file, merged, 'utf8'); } catch (e) { return { ok: false, err: String(e.message || e) }; } invalidateFileCache(file); + markSelfWrite(file); return { ok: true }; } @@ -453,6 +470,23 @@ return true; } + function isDataJsonFile(file) { + if (!file || !path) { return false; } + try { + var ap = absPath(file); + var dir = absPath(DATA_DIR); + if (process.platform === 'win32') { + return ap.toLowerCase().indexOf(dir.toLowerCase()) === 0 && /\.json$/i.test(ap); + } + return ap.indexOf(dir) === 0 && /\.json$/i.test(ap); + } catch (e) { return false; } + } + + function applyReloadAfterOverlaySave(file) { + if (!CFG.liveRefresh || !isDataJsonFile(file)) { return false; } + return reloadFile(file, { silent: true }); + } + function saveRecordEdit(rec, newText) { if (!nodeOk) { toast('Live edit requires the desktop NW.js build'); return false; } if (newText === rec.text) { toast('No changes'); return false; } @@ -460,10 +494,6 @@ var r; if (rec.file && rec.tail) { r = writeAtPath(rec.file, rec.tail, newText, expected); - if (r.ok) { - var root = memoryRootForFile(rec.file); - if (root) { setAtPath(root, rec.tail, newText); } - } } else if (rec.file && rec._editSite) { r = writeAtEditSite(rec.file, rec._editSite, newText, expected); } else if (rec.file && (rec._editLine || rec.uiLine)) { @@ -484,8 +514,10 @@ rec._key = 't\u0000' + rec.kind + '\u0000' + newText + '\u0000' + (rec.file || '') + '\u0000' + (rec.tail ? JSON.stringify(rec.tail) : ''); applyLiveMessage(rec, newText); - var note = rec.kind === 'ui' ? ' — reload scene to refresh UI' : ''; - toast('Saved to file' + note); + var reloaded = applyReloadAfterOverlaySave(rec.file); + if (!reloaded) { refreshSceneWindows(); } + var note = rec.kind === 'ui' && !reloaded ? ' — UI may need scene change' : ''; + toast(reloaded ? 'Saved & reloaded' : ('Saved to file' + note)); scheduleRefresh(); return true; } @@ -1411,7 +1443,8 @@ }); ['keydown', 'keyup', 'keypress'].forEach(function (t) { root.addEventListener(t, function (ev) { - if (ev.key === CFG.hotkey) { return; } // F10 toggle handled globally + if (ev.key === CFG.hotkey || + (CFG.liveRefresh && ev.key === CFG.liveRefreshHotkey)) { return; } ev.stopPropagation(); }, false); }); @@ -1856,6 +1889,301 @@ } } + //========================================================================= + // Live refresh — hot-reload data/*.json from external editor saves (NW.js) + //========================================================================= + var _selfWrittenFiles = {}; // absPath -> timestamp (skip watcher echo from overlay saves) + var _pendingStable = {}; // absPath -> { lastMt, stableSince, timer } + var _watcherStarted = false; + var _pollTimer = null; + var _watchScanTimer = 0; + var _knownMtimes = {}; // absPath -> mtimeMs (poll-based change detection) + var DB_FILES = {}; // 'Items.json' -> { global: '$dataItems', meta: true } + + function absPath(file) { + if (!file || !path) { return file; } + try { return path.resolve(file); } catch (e) { return file; } + } + + function syncMtimeCache(file) { + if (!nodeOk || !file) { return; } + try { + _knownMtimes[absPath(file)] = fs.statSync(absPath(file)).mtimeMs; + } catch (e) { /* ignore */ } + } + + function markSelfWrite(file) { + if (!CFG.liveRefresh || !file) { return; } + _selfWrittenFiles[absPath(file)] = Date.now(); + syncMtimeCache(file); + } + + function isSelfWrite(file) { + if (!file) { return false; } + var key = absPath(file); + var t = _selfWrittenFiles[key]; + if (!t) { return false; } + var windowMs = (CFG.liveRefreshStableMs || 120) + 600; + if (Date.now() - t < windowMs) { return true; } + delete _selfWrittenFiles[key]; + return false; + } + + function buildDbFileMap() { + DB_FILES = {}; + var entries = (typeof DataManager !== 'undefined' && DataManager._databaseFiles) + ? DataManager._databaseFiles : null; + if (entries && entries.length) { + entries.forEach(function (entry) { + DB_FILES[entry.src] = { + global: entry.name, + meta: entry.src !== 'System.json' + }; + }); + return; + } + // Fallback before DataManager is ready (MV/MZ standard database list) + [ + ['Actors.json', '$dataActors', true], + ['Classes.json', '$dataClasses', true], + ['Skills.json', '$dataSkills', true], + ['Items.json', '$dataItems', true], + ['Weapons.json', '$dataWeapons', true], + ['Armors.json', '$dataArmors', true], + ['Enemies.json', '$dataEnemies', true], + ['Troops.json', '$dataTroops', true], + ['States.json', '$dataStates', true], + ['Animations.json', '$dataAnimations', true], + ['Tilesets.json', '$dataTilesets', true], + ['CommonEvents.json', '$dataCommonEvents', true], + ['System.json', '$dataSystem', false], + ['MapInfos.json', '$dataMapInfos', true] + ].forEach(function (row) { + DB_FILES[row[0]] = { global: row[1], meta: row[2] }; + }); + } + + function parseMapId(fname) { + var m = /^Map(\d+)\.json$/i.exec(fname || ''); + return m ? parseInt(m[1], 10) : null; + } + + function loadJsonFromDisk(file) { + var ap = absPath(file); + invalidateFileCache(ap); + return JSON.parse(fs.readFileSync(ap, 'utf8')); + } + + function applyDatabaseGlobal(globalName, data, useMeta) { + window[globalName] = data; + if (useMeta && typeof DataManager !== 'undefined' && DataManager.extractArrayMetadata) { + DataManager.extractArrayMetadata(data); + } + } + + function refreshSceneWindows() { + try { + var scene = (typeof SceneManager !== 'undefined') && SceneManager._scene; + if (!scene || !scene._windowLayer) { return; } + var kids = scene._windowLayer.children; + if (!kids) { return; } + for (var i = 0; i < kids.length; i++) { + var w = kids[i]; + if (w && typeof w.refresh === 'function') { + try { w.refresh(); } catch (e) { /* ignore */ } + } + } + } catch (e) { log('refreshSceneWindows', e); } + } + + function reloadFile(filePath, options) { + options = options || {}; + if (!nodeOk || !fs || !path) { + if (!options.silent) { toast('Live refresh requires NW.js playtest'); } + return false; + } + var ap = absPath(filePath); + if (!fs.existsSync(ap)) { + if (!options.silent) { toast('File not found: ' + path.basename(ap)); } + return false; + } + var fname = path.basename(ap); + if (!/\.json$/i.test(fname)) { return false; } + + try { + var mapId = parseMapId(fname); + if (mapId != null) { + var currentMap = (typeof $gameMap !== 'undefined' && $gameMap && $gameMap.mapId) + ? $gameMap.mapId() : 0; + if (currentMap !== mapId) { + syncMtimeCache(ap); + if (!options.silent) { toast('Map file saved (switch maps to apply)'); } + return true; + } + if (typeof $dataMap !== 'undefined') { + $dataMap = loadJsonFromDisk(ap); + } + syncMtimeCache(ap); + if (!options.silent) { toast('Reloaded ' + fname); } + refreshSceneWindows(); + return true; + } + + if (!Object.keys(DB_FILES).length) { buildDbFileMap(); } + var spec = DB_FILES[fname]; + if (spec) { + applyDatabaseGlobal(spec.global, loadJsonFromDisk(ap), spec.meta); + syncMtimeCache(ap); + if (!options.silent) { toast('Reloaded ' + fname); } + refreshSceneWindows(); + return true; + } + + log('reload skip (unknown data file)', fname); + return false; + } catch (e) { + log('reload failed', fname, e); + if (!options.silent) { toast('Reload failed: ' + fname); } + return false; + } + } + + function reloadAllForPlaytest() { + if (!nodeOk) { toast('Live refresh requires NW.js playtest'); return; } + if (!Object.keys(DB_FILES).length) { buildDbFileMap(); } + var count = 0; + Object.keys(DB_FILES).forEach(function (fname) { + if (reloadFile(dataFile(fname), { silent: true })) { count++; } + }); + var mapId = (typeof $gameMap !== 'undefined' && $gameMap && $gameMap.mapId) + ? $gameMap.mapId() : 0; + if (mapId > 0 && reloadFile(mapFile(mapId), { silent: true })) { count++; } + refreshSceneWindows(); + toast('Reloaded database + current map (' + count + ' files)'); + } + + // VSCode (etc.) saves land on disk; wait until mtime stops changing, then reload. + function scheduleFileReload(filePath) { + if (!CFG.liveRefresh || !nodeOk) { return; } + waitForStableThenReload(absPath(filePath)); + } + + function waitForStableThenReload(ap) { + if (isSelfWrite(ap)) { return; } + var stableMs = CFG.liveRefreshStableMs || 120; + var checkMs = 50; + if (!_pendingStable[ap]) { + _pendingStable[ap] = { lastMt: null, stableSince: 0, timer: 0 }; + } + if (_pendingStable[ap].timer) { clearTimeout(_pendingStable[ap].timer); } + + function tick() { + if (isSelfWrite(ap)) { + delete _pendingStable[ap]; + return; + } + try { + if (!fs.existsSync(ap)) { + _pendingStable[ap].timer = setTimeout(tick, checkMs); + return; + } + var mt = fs.statSync(ap).mtimeMs; + var p = _pendingStable[ap]; + if (p.lastMt !== mt) { + p.lastMt = mt; + p.stableSince = Date.now(); + } else if (Date.now() - p.stableSince >= stableMs) { + delete _pendingStable[ap]; + reloadFile(ap); + return; + } + p.timer = setTimeout(tick, checkMs); + } catch (e) { + delete _pendingStable[ap]; + } + } + + _pendingStable[ap].timer = setTimeout(tick, checkMs); + } + + function seedMtimes() { + if (!nodeOk) { return; } + try { + fs.readdirSync(DATA_DIR).forEach(function (fname) { + if (!/\.json$/i.test(fname)) { return; } + syncMtimeCache(path.join(DATA_DIR, fname)); + }); + } catch (e) { log('seedMtimes', e); } + } + + function pollDataDirForChanges() { + if (!CFG.liveRefresh || !nodeOk) { return; } + try { + var names = fs.readdirSync(DATA_DIR); + for (var i = 0; i < names.length; i++) { + var fname = names[i]; + if (!/\.json$/i.test(fname)) { continue; } + var ap = path.join(DATA_DIR, fname); + try { + var mt = fs.statSync(ap).mtimeMs; + var prev = _knownMtimes[ap]; + if (prev != null && mt !== prev && !isSelfWrite(ap)) { + scheduleFileReload(ap); + } else { + _knownMtimes[ap] = mt; + } + } catch (e2) { /* ignore */ } + } + } catch (e) { /* ignore */ } + } + + function startLiveRefreshPoll() { + if (_pollTimer || !CFG.liveRefresh || !nodeOk) { return; } + seedMtimes(); + var ms = CFG.liveRefreshPollMs || 400; + _pollTimer = setInterval(pollDataDirForChanges, ms); + log('live refresh polling every', ms + 'ms'); + } + + function scheduleDataDirScan() { + if (!CFG.liveRefresh) { return; } + clearTimeout(_watchScanTimer); + _watchScanTimer = setTimeout(pollDataDirForChanges, 200); + } + + function startLiveRefreshWatcher() { + if (!CFG.liveRefresh || !nodeOk || _watcherStarted) { return; } + _watcherStarted = true; + buildDbFileMap(); + startLiveRefreshPoll(); + try { + fs.watch(DATA_DIR, function (eventType, filename) { + if (filename && /\.json$/i.test(filename)) { + scheduleFileReload(path.join(DATA_DIR, filename)); + } else { + // Windows fs.watch often fires with a null filename + scheduleDataDirScan(); + } + }); + log('live refresh watching', DATA_DIR); + } catch (e) { + log('live refresh watch failed (poll-only)', e); + } + } + + if (CFG.liveRefresh && nodeOk) { + if (typeof DataManager !== 'undefined') { + startLiveRefreshWatcher(); + } else { + var _waitDbTimer = setInterval(function () { + if (typeof DataManager !== 'undefined') { + clearInterval(_waitDbTimer); + startLiveRefreshWatcher(); + } + }, 100); + } + } + //========================================================================= // Toast //========================================================================= @@ -1880,6 +2208,10 @@ e.preventDefault(); e.stopPropagation(); toggle(); + } else if (CFG.liveRefresh && e.key === CFG.liveRefreshHotkey) { + e.preventDefault(); + e.stopPropagation(); + reloadAllForPlaytest(); } else if (e.key === 'Escape' && ui.pick) { e.preventDefault(); e.stopPropagation(); @@ -1902,7 +2234,8 @@ if (!ui.open || !ui.freeze) { return; } var isKey = (e.type === 'keydown' || e.type === 'keyup' || e.type === 'keypress'); if (isKey) { - if (e.key === CFG.hotkey || e.key === 'Escape') { return; } // toggles always work + if (e.key === CFG.hotkey || e.key === 'Escape' || + (CFG.liveRefresh && e.key === CFG.liveRefreshHotkey)) { return; } // Typing in the panel / edit box must keep normal browser behaviour // (Backspace, Enter for newlines, etc.) — only block keys aimed at the game. if (inPanel(e.target) || isEditableInPanel(document.activeElement)) { return; } @@ -1926,10 +2259,16 @@ window.TLInspector = { open: function () { toggle(true); }, close: function () { toggle(false); }, + reload: function (file) { return reloadFile(file); }, + reloadAll: function () { reloadAllForPlaytest(); return true; }, records: textRecords, images: imageLog, cfg: CFG }; - log('ready - press ' + CFG.hotkey + ' to open'); + var _readyMsg = 'ready - ' + CFG.hotkey + ' inspector'; + if (CFG.liveRefresh && nodeOk) { + _readyMsg += ', ' + CFG.liveRefreshHotkey + ' reload data (auto-watch on)'; + } + log(_readyMsg); })();