Skip to content

Instantly share code, notes, and snippets.

@l3laze
Created April 19, 2025 01:11
Show Gist options
  • Select an option

  • Save l3laze/09abae62b9232ad4b5ffa1df50631c30 to your computer and use it in GitHub Desktop.

Select an option

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
<!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