From 49e3e672e5845b9e4695626ab3d993d37faa2320 Mon Sep 17 00:00:00 2001 From: obeyd Date: Wed, 28 Jan 2026 22:29:18 -0500 Subject: [PATCH 1/2] Added multi character patching --- app.js | 179 +++++++++++++++++++++++++++++++++++++++++++++----- data/mod.json | 15 +++++ 2 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 data/mod.json diff --git a/app.js b/app.js index 6b183df..c3c0292 100644 --- a/app.js +++ b/app.js @@ -521,7 +521,8 @@ class RomPatcher { static FIRST_MOD_BANK = 0x4 + 0x11; // Hashes of known ROMs static KNOWN_ROMS = { - "utmgm9": "STB 2.5", + "utmgm9": { "name": "Super Tilt Bro. v2.5 hash:utmgm9", "isOnlineEnabled": true }, + "itn6my": { "name": "Super Tilt Bro. v2.5 hash:itn6my", "isOnlineEnabled": false } }; constructor(storage, onchange) { @@ -580,7 +581,14 @@ class RomPatcher { // Return name of the ROM, return a hash or a known name static getRomName(romData) { const hash = hashByteArray(romData); - return this.KNOWN_ROMS[hash] || `hash:${hash}`; + if(this.KNOWN_ROMS[hash]) return this.KNOWN_ROMS[hash].name; + return `hash:${hash}`; + } + + static isRomOnlineEnabled(romData) { + const hash = hashByteArray(romData); + if(this.KNOWN_ROMS[hash]) return this.KNOWN_ROMS[hash].isOnlineEnabled + return true; // assume online is enabled } // Write a 16-bit value, return end offset @@ -762,6 +770,8 @@ const app = Vue.createApp({ return { tree: Vue.computed(() => this.tree), conf: Vue.computed(() => this.conf), + characterFileIndex: Vue.computed(() => this.characterFileIndex), + characterUrls: Vue.computed(() => this.characterUrls), } }, @@ -1625,14 +1635,15 @@ app.component('stb-sprite-thumbnail', { }); app.component('stb-animation-thumbnail', { - props: ['animation', 'zoom', 'rect', 'swap'], + props: ['animation', 'zoom', 'rect', 'swap', 'data'], inject: ['tree', 'conf'], methods: { updateAnimation() { const rect = this.rect || Utils.animationRect(this.animation, {}); const swap = this.swap === undefined ? this.conf.colorSwap : this.swap; - const palettes = Utils.getPalettes(this.tree, swap); + const palettes = this.data === undefined ? Utils.getPalettes(this.tree, swap) : Utils.getPalettes(this.data, swap); + const tree = this.data === undefined ? this.tree : this.data; // Prepare the canvas const canvas = this.$refs.canvas; @@ -1645,7 +1656,7 @@ app.component('stb-animation-thumbnail', { const promises = frames.map(frame => { ctx.fillStyle = this.conf.bgColor; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - Utils.drawSingleFrame(ctx, this.tree, frame, { + Utils.drawSingleFrame(ctx, tree, frame, { zoom: this.zoom, palettes, dx: -rect.x, dy: -rect.y, }); return createImageBitmap(canvas); @@ -2603,26 +2614,74 @@ const AiTab = { } const RomTab = { - inject: ['tree'], + inject: ['tree','conf','characterFileIndex','characterUrls'], data() { return { romData: null, romError: null, + romIsOnlineEnabled: true, characterSlot: 0, + // mod data stored in stbeditor.slot.${id} + characters: [ + { "id": 0, "name": "sinbad" }, + { "id": 1, "name": "kiki" }, + { "id": 2, "name": "pepper" }, + { "id": 3, "name": "vgsage" }, + { "id": 4, "name": "sunny" }, + ], + characterSlots: null, + characterFiles: [], + modTemplate: null, } }, created() { this.patcher = new RomPatcher(localStorage); this.romData = this.patcher.loadRomData(); + this.romIsOnlineEnabled = this.romData ? RomPatcher.isRomOnlineEnabled(this.romData) : true; this.characterSlot = this.patcher.getCharacterSlot(); + this.modTemplate = this.getModTemplate(); }, computed: { romName() { return this.romData ? RomPatcher.getRomName(this.romData) : null; - } + }, + characterData() { + this.characterFiles = []; + let idx = 0; + this.characterFiles.push({ "idx": idx++, "id": "" }); + for(charfile in this.characterFileIndex) { + this.characterFiles.push({ "idx": idx++, "id": this.characterFileIndex[charfile].name, "name": this.characterFileIndex[charfile].name, "data": JSON.parse(localStorage.getItem(`stbeditor.data.${this.characterFileIndex[charfile].name}`)), "isDefault": false }); + } + for(charfile in this.characterUrls) { + const entry = { "idx": idx++, "id": "->"+charfile, "name": charfile, "data": this.characterUrls[charfile], "isDefault": true }; + if(entry.data) { + this.characterFiles.push(entry); + fetch(entry.data) + .then(response => response.json()) + .then(data => { + this.characterFiles[entry.idx].data = data; + }) + .catch(err => console.error(err)); + } + else idx--; + } + return this.characterFiles; + }, + characterSlotsFilled() { + if(!this.characterSlots) this.loadCharacterSlots(); + for(i=0; i obj.id === chars.id); + if(charf) { + charf.data = chars.data; + this.characterSlots[i] = charf; + if(!chars.isDefault) { + this.characterSlots[i].data = JSON.parse(localStorage.getItem(`stbeditor.data.${chars.name}`)); + } + } + else this.characterSlots[i] = chars; + } + } + }, + saveCharacterSlot(slot) { + if(this.characterSlots[slot]) { + localStorage.setItem(`stbeditor.slot.${slot}`, JSON.stringify(this.characterSlots[slot])); + } + }, + getPreviewAnimation(data) { + return Utils.getAnimationByName(data, data[MANDATORY_ANIMATION_NAMES[0]].name) + }, + getModTemplate() { + fetch('data/mod.json') + .then(response => response.json()) + .then(data => { this.modTemplate = data; }) + .catch(err => console.info(`cannot load static configuration: ${err}`)); + }, + generateAndDownloadModFile() { + console.debug(`Download mod.json`); + let modFile = cloneData(this.modTemplate); + for(i=0; i

Patch a ROM

+
+

Characters to Patch

+
+

+ + + + + + +    + {{ slot.name }} +

+
+
+
+

Apply Mod Patch

+       Build ROM +
or
+
+
+

Patch an Existing ROM

Write character data to the original ROM. Limitations listed below apply.
Basically graphics, hitboxes and hurtboxes can be modified. Everything else must match the ROM to patch.
@@ -2668,17 +2816,16 @@ const RomTab = {

{{ romError }} - A ROM is loaded ({{ romName }}) + A ROM is loaded ({{ romName }}) + [ Online Enabled?] + No ROM loaded

- -

-

- - (no character file loaded) +

+
`, } diff --git a/data/mod.json b/data/mod.json new file mode 100644 index 0000000..6a0eceb --- /dev/null +++ b/data/mod.json @@ -0,0 +1,15 @@ +{ + "type": "gamemod", + "characters": [ + {"type": "import", "src": "characters/sinbad/sinbad.json"}, + {"type": "import", "src": "characters/kiki/kiki.json"}, + {"type": "import", "src": "characters/pepper/pepper.json"}, + {"type": "import", "src": "characters/vgsage/vgsage.json"}, + {"type": "import", "src": "characters/sunny/sunny.json"} + ], + "tilesets": [ + {"type": "import", "src": "tilesets/ruins.json"}, + {"type": "import", "src": "tilesets/jungle.json"}, + {"type": "import", "src": "tilesets/magma.json"} + ] +} \ No newline at end of file From 922b703d8f06e79337b01fbb2993ae3bf94e68e3 Mon Sep 17 00:00:00 2001 From: obeyd Date: Fri, 30 Jan 2026 10:45:00 -0500 Subject: [PATCH 2/2] Updated list text color to match standard theme. Refactored characterFiles from array to object hash. --- app.js | 51 ++++++++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/app.js b/app.js index c3c0292..9c319ef 100644 --- a/app.js +++ b/app.js @@ -2631,7 +2631,7 @@ const RomTab = { { "id": 4, "name": "sunny" }, ], characterSlots: null, - characterFiles: [], + characterFiles: null, modTemplate: null, } }, @@ -2649,24 +2649,22 @@ const RomTab = { return this.romData ? RomPatcher.getRomName(this.romData) : null; }, characterData() { - this.characterFiles = []; - let idx = 0; - this.characterFiles.push({ "idx": idx++, "id": "" }); + this.characterFiles = {}; + this.characterFiles[""] = { "id": "" }; for(charfile in this.characterFileIndex) { - this.characterFiles.push({ "idx": idx++, "id": this.characterFileIndex[charfile].name, "name": this.characterFileIndex[charfile].name, "data": JSON.parse(localStorage.getItem(`stbeditor.data.${this.characterFileIndex[charfile].name}`)), "isDefault": false }); + this.characterFiles[this.characterFileIndex[charfile].name] = { "id": this.characterFileIndex[charfile].name, "name": this.characterFileIndex[charfile].name, "data": JSON.parse(localStorage.getItem(`stbeditor.data.${this.characterFileIndex[charfile].name}`)), "isDefault": false }; } for(charfile in this.characterUrls) { - const entry = { "idx": idx++, "id": "->"+charfile, "name": charfile, "data": this.characterUrls[charfile], "isDefault": true }; + const entry = { "id": "->"+charfile, "name": charfile, "data": this.characterUrls[charfile], "isDefault": true }; if(entry.data) { - this.characterFiles.push(entry); + this.characterFiles[entry.id] = entry; fetch(entry.data) .then(response => response.json()) .then(data => { - this.characterFiles[entry.idx].data = data; + this.characterFiles[entry.id].data = data; }) .catch(err => console.error(err)); } - else idx--; } return this.characterFiles; }, @@ -2716,10 +2714,11 @@ const RomTab = { }, getCharacterFiles(slot) { const characterSlotName = this.characters[slot].name; - const charD = this.characterData; + const charData = this.characterData; let charDforSlot = []; - for(i=0; i obj.id === chars.id); + const charf = charD[chars.id]; if(charf) { - charf.data = chars.data; - this.characterSlots[i] = charf; - if(!chars.isDefault) { - this.characterSlots[i].data = JSON.parse(localStorage.getItem(`stbeditor.data.${chars.name}`)); - } + charf.data = chars.data; + this.characterSlots[i] = charf; + if(!chars.isDefault) { + this.characterSlots[i].data = JSON.parse(localStorage.getItem(`stbeditor.data.${chars.name}`)); + } + } + else { + if(chars.isDefault) this.characterSlots[i] = chars; + else { + this.characterSlots[i] = null; + localStorage.removeItem(`stbeditor.slot.${i}`); + } } - else this.characterSlots[i] = chars; } } }, @@ -2783,12 +2788,12 @@ const RomTab = {

- - + - +    {{ slot.name }}