Created
April 19, 2025 01:11
-
-
Save l3laze/09abae62b9232ad4b5ffa1df50631c30 to your computer and use it in GitHub Desktop.
Attempt to convert https://github.com/nathan-b/rmse to a modern web app using File/System/Access APIs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <title>RPGMaker Save Editor</title> | |
| <style> | |
| :root { | |
| font-size: 16px; | |
| --xs: 0.25rem; | |
| --sm: 0.5rem; | |
| --md: 0.75rem; | |
| --one: 1rem; | |
| --lg: 1.25rem; | |
| --xl: 1.75rem; | |
| --xxl: 2.5rem; | |
| --fifth: 20%; | |
| --quarter: 25%; | |
| --third: 33.333333%; | |
| --half: 50%; | |
| --three-quarters: 75%; | |
| --four-fifths: 80%; | |
| --whole: 100%; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| } | |
| body { | |
| background-color: black; | |
| color: white; | |
| padding: var(--md); | |
| font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| font-size: var(--one); | |
| font-weight: bold; | |
| } | |
| select { | |
| padding: var(--xs) 0; | |
| } | |
| select, button, option { | |
| cursor: pointer; | |
| } | |
| header { | |
| width: var(--whole); | |
| margin-bottom: var(--lg); | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| grid-template-areas: | |
| "drop game game file actions" | |
| "drop title title save buttons"; | |
| .drop-container { | |
| grid-area: drop; | |
| display: flex; | |
| flex-direction: column; | |
| place-content: center; | |
| width: var(--four-fifths); | |
| padding: var(--sm); | |
| background-color: darkgreen; | |
| color: black; | |
| font-weight: bold; | |
| text-align: center; | |
| cursor: pointer; | |
| } | |
| .game-label, .file-label, .actions-label { | |
| place-self: center; | |
| text-align: center; | |
| } | |
| .game-label { | |
| grid-area: game; | |
| } | |
| .file-label { | |
| grid-area: file; | |
| width: var(--three-quarters); | |
| } | |
| .actions-label { | |
| grid-area: actions; | |
| } | |
| .game-title, .save-selector, .save-actions { | |
| place-self: center; | |
| } | |
| .game-title { | |
| grid-area: title; | |
| width: var(--whole); | |
| } | |
| .save-selector { | |
| grid-area: save; | |
| width: var(--three-quarters); | |
| } | |
| .save-actions { | |
| grid-area: buttons; | |
| display: flex; | |
| flex-flow: row wrap; | |
| gap: var(--xs); | |
| width: var(--whole); | |
| button { | |
| padding: var(--xs); | |
| border-width: 1px; | |
| &:active { | |
| filter: invert(); | |
| } | |
| } | |
| } | |
| } | |
| .section-controls { | |
| width: var(--whole); | |
| text-align: center; | |
| .section-selector { | |
| width: var(--fifth); | |
| margin-left: var(--xxl); | |
| } | |
| } | |
| .editables { | |
| display: grid; | |
| grid-template-columns: repeat(12, 1fr); | |
| grid-template-rows: repeat(auto, 1fr); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="drop-container"> | |
| <span>Drop Game<br>Folder Here</span> | |
| </div> | |
| <span class="game-label">Game</span> | |
| <span class="game-title">No game loaded.</span> | |
| <span class="file-label">Save File</span> | |
| <select class="save-selector" size="1"> | |
| <option>None</option> | |
| </select> | |
| <span class="actions-label">Actions</span> | |
| <span class="save-actions"> | |
| <button type="button">Save</button> | |
| <button type="button">Save As</button> | |
| <button type="button">Reload</button> | |
| </span> | |
| </header> | |
| <div class="section-controls"> | |
| <span>Save Data Section</span> | |
| <select class="section-selector" size="1"> | |
| <option>Shared</option> | |
| <option>Party</option> | |
| <option>Global</option> | |
| </select> | |
| </div> | |
| <section class="editables"> | |
| </section> | |
| <script async> | |
| const rmse = { | |
| entries: [], | |
| context: [], | |
| title: document.getElementsByClassName('game-title')[0], | |
| encode: async (f) => { | |
| if (typeof this.pako !== 'undefined') { | |
| return this.pako.deflate(await loadFile(f), { | |
| to: 'string', | |
| level: 1 | |
| }) | |
| } else if (typeof this.lzstring !== 'undefined') { | |
| return this.lzstring.compressToBase64(await loadFile(f)) | |
| } | |
| }, | |
| decode: async (f) => { | |
| if (/\.json/.test(f.name)) { | |
| return JSON.parse(await loadFile(f)) | |
| } else if (typeof this.pako !== 'undefined') { | |
| return this.pako.inflate(await loadFile(f), { | |
| to: 'string' | |
| }) | |
| } else if (typeof this.lzstring !== 'undefined') { | |
| return this.lzstring.decompressFromBase64(await loadFile(f)) | |
| } | |
| } | |
| } | |
| async function loadGameDirectory (handle) { | |
| if (typeof handle?.values === 'undefined') { | |
| alert('Target must be a directory.') | |
| return | |
| } | |
| let isRMMZ = false | |
| let hasPackageJSON = false | |
| let hasConfig = false | |
| let hasFileN = false | |
| let hasGlobal = false | |
| rmse.root = handle | |
| rmse.entries = [] | |
| for await (const e of recursiveReaddir(handle)) { | |
| if (e.name === 'rmmz_core.js') { | |
| isRMMZ = true | |
| } else if (/config\.rmmzsave/.test(e.name)) { | |
| hasConfig = true | |
| } else if (/file\d+\.rmmzsave/.test(e.name)) { | |
| hasFileN = true | |
| rmse.entries.push(e) | |
| } else if (/(Items|Armors|Weapons|System)\.json/.test(e.name)) { | |
| rmse.context.push(e) | |
| } else if (/global\.rmmzsave/.test(e.name)) { | |
| hasGlobal = true | |
| } else if (!rmse.pako && /pako\.min\.js/.test(e.name)) { | |
| await borrowRMMZLib(e) | |
| rmse.pako = window.pako | |
| } else if (!rmse.lzstring && /lzstring\.min\.js/.test(e.name)) { | |
| await borrowRMMZLib(e) | |
| rmse.lzstring = window.lzstring | |
| } else if (/package\.json/.test(e.name)) { | |
| hasPackageJSON = true | |
| rmse.title.innerText = JSON.parse(await (loadFile(e))).window.title | |
| } | |
| } | |
| if (!isRMMZ) { | |
| alert('This is not a valid RPGMaker MZ game directory.') | |
| return | |
| } | |
| if (!hasConfig || !hasFileN || !hasGlobal) { | |
| alert('Please make sure you have a save folder with at least global, config, and a single "file#", each with the ".rmmzsave" extension.') | |
| return | |
| } | |
| rmse.title.innerText = rmse?.title.innerText ?? handle.name | |
| await updateSaveSelector() | |
| } | |
| async function* recursiveReaddir (handle) { | |
| const entries = [] | |
| for await (const entry of handle.values()) { | |
| if (entry.kind === 'file' && /pako|lzstring|rmmz_core|rmmzsave|(Items|Armors|Weapons|System|package).json/.test(entry.name)) { | |
| yield entry | |
| } else if (entry.kind === 'directory') { | |
| for await (const nested of recursiveReaddir(entry)) { | |
| yield nested | |
| } | |
| } | |
| } | |
| } | |
| async function borrowRMMZLib (path) { | |
| const script = document.createElement('script') | |
| try { | |
| script.textContent = await loadFile(path) | |
| } catch (err) { | |
| console.error(err) | |
| return | |
| } | |
| document.body.appendChild(script) | |
| } | |
| async function loadFile (file) { | |
| console.log('Loading file...', file.name) | |
| return await (await file.getFile()).text() | |
| } | |
| async function updateSaveSelector () { | |
| const saveSelector = document.querySelector('.save-selector') | |
| saveSelector.innerHTML = '' | |
| const none = document.createElement('option') | |
| none.appendChild(document.createTextNode('None')) | |
| saveSelector.add(none) | |
| for (const e of rmse.entries) { | |
| const option = document.createElement('option') | |
| option.value = e.name | |
| option.innerText = e.name | |
| saveSelector.add(option) | |
| } | |
| } | |
| async function openDirectoryPicker () { | |
| const handle = await showDirectoryPicker() | |
| if (!handle) { | |
| return | |
| } | |
| loadGameDirectory(handle) | |
| } | |
| async function dropHandler (ev) { | |
| ev.preventDefault() | |
| const item = ev.dataTransfer.items[0] | |
| const handle = await item.getAsFileSystemHandle() | |
| await loadGameDirectory(handle) | |
| } | |
| async function dragHandler (ev) { | |
| ev.preventDefault() | |
| ev.stopPropagation() | |
| } | |
| async function clickHandler (ev) { | |
| openDirectoryPicker() | |
| } | |
| async function loadContext (path) { | |
| const contextFiles = ['Items', 'Armors', 'Weapons', 'System'] | |
| const context = { | |
| rm_root: path, | |
| savefile: path.name | |
| } | |
| for (const cf of contextFiles) { | |
| context[cf] = await rmse.decode(rmse.context.find((c) => c.name === cf + '.json')) | |
| } | |
| return context | |
| } | |
| async function loadSaveFile (saveIndex) { | |
| const saveData = await rmse.decode(rmse.entries[saveIndex]) | |
| const context = await loadContext(rmse.entries[saveIndex]) | |
| console.log(saveData) | |
| console.log(context) | |
| const mapped = {} | |
| const dataKey = '_data' | |
| const saved = saveData.variables[dataKey] | |
| for (let i = 0; i < context.System.variables.length; i++) { | |
| if (context.System.variables[i] !== '') { | |
| mapped[context.System.variables[i]] = saved[i] | |
| } | |
| } | |
| console.log() | |
| } | |
| window.addEventListener('DOMContentLoaded', (event) => { | |
| const dropContainer = document.querySelector('.drop-container') | |
| const saveSelector = document.querySelector('.save-selector') | |
| dropContainer.addEventListener('drop', dropHandler) | |
| dropContainer.addEventListener('dragover', dragHandler) | |
| dropContainer.addEventListener('click', clickHandler) | |
| saveSelector.addEventListener('change', async (ev) => { | |
| const index = ev.target.selectedIndex - 1 | |
| const value = ev.target.options[index + 1].value | |
| if (value === 'None') { | |
| return | |
| } else { | |
| console.log(`loading save ${value}`) | |
| await loadSaveFile(index) | |
| } | |
| }) | |
| }) | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment