Last active
April 6, 2024 09:42
-
-
Save qnighy/71fd328386cfc543be405ac5f6c3fde9 to your computer and use it in GitHub Desktop.
RPG Maker MZ case-sensitive file finder; useful for running the games on Linux
This file contains 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
const isMZ = true; | |
const actors: ActorsJson = JSON.parse(await Deno.readTextFile("data/Actors.json")); | |
const commonEvents: CommonEventsJson = JSON.parse(await Deno.readTextFile("data/CommonEvents.json")); | |
const enemies: EnemiesJson = JSON.parse(await Deno.readTextFile("data/Enemies.json")); | |
const systemData: SystemJson = JSON.parse(await Deno.readTextFile("data/System.json")); | |
const mapInfo: MapInfosJson = JSON.parse(await Deno.readTextFile("data/MapInfos.json")); | |
const tilesets: TilesetsJson = JSON.parse(await Deno.readTextFile("data/Tilesets.json")); | |
function modifyPath(path: string): string { | |
const dot = path.lastIndexOf("."); | |
if (dot === -1) { | |
return path; | |
} | |
const base = path.substring(0, dot); | |
const ext = path.substring(dot + 1); | |
let resourceType: "image" | "audio" | "other" = "other"; | |
let mvAltExt = ""; | |
switch (ext) { | |
case "png": | |
resourceType = "image"; | |
mvAltExt = "rpgmvp"; | |
break; | |
case "m4a": | |
resourceType = "audio"; | |
mvAltExt = "rpgmvm"; | |
break; | |
case "ogg": | |
resourceType = "audio"; | |
mvAltExt = "rpgmvo"; | |
break; | |
} | |
const isEncrypted = resourceType === "image" && systemData.hasEncryptedImages || resourceType === "audio" && systemData.hasEncryptedAudio; | |
if (!isEncrypted) { | |
return path; | |
} else if (isMZ) { | |
return `${path}_`; | |
} else { | |
return `${base}.${mvAltExt}`; | |
} | |
} | |
const dirCache: Map<string, Map<string, string>> = new Map(); | |
async function readDir(path: string): Promise<Map<string, string>> { | |
const cached = dirCache.get(path); | |
if (cached) { | |
return cached; | |
} | |
const result = new Map<string, string>(); | |
for await (const entry of Deno.readDir(path)) { | |
result.set(entry.name.toLowerCase(), entry.name); | |
} | |
dirCache.set(path, result); | |
return result; | |
} | |
class NotFoundError extends Error { | |
static { | |
this.prototype.name = "NotFoundError"; | |
} | |
} | |
async function resolveCaseInsensitivePath(ciPath: string): Promise<string> { | |
const parts: string[] = []; | |
for (const ciPart of ciPath.split("/")) { | |
const dir = parts.join("/") || "."; | |
const dirMap = await readDir(dir); | |
const part = dirMap.get(ciPart.toLowerCase()); | |
if (!part) { | |
throw new NotFoundError(`File not found: ${[...parts, ciPath].join("/")}`); | |
} | |
parts.push(part); | |
} | |
return parts.join("/"); | |
} | |
async function checkPath(origPath: string): Promise<void> { | |
const ciPath = modifyPath(origPath); | |
try { | |
const csPath = await resolveCaseInsensitivePath(ciPath); | |
if (ciPath !== csPath) { | |
console.log(`cp ${csPath} ${ciPath}`); | |
} | |
} catch (e) { | |
if (e instanceof NotFoundError) { | |
console.error(`Resource not found: ${origPath}`); | |
} else { | |
throw e; | |
} | |
} | |
} | |
async function checkImage(imageType: string, imageName: string): Promise<void> { | |
if (imageName) { | |
await checkPath(`img/${imageType}/${imageName}.png`); | |
} | |
} | |
async function checkAudio(audioType: string, audioName: string): Promise<void> { | |
if (audioName) { | |
await checkPath(`audio/${audioType}/${audioName}.ogg`); | |
} | |
} | |
async function processCommands(commands: Command[]): Promise<void> { | |
for (const command of commands) { | |
if (command.code === 101) { | |
const faceName = command.parameters[0]; | |
if (typeof faceName === "string") { | |
await checkImage("faces", faceName) | |
} | |
} else if (command.code === 231) { | |
const pictureName = command.parameters[1]; | |
if (typeof pictureName === "string") { | |
await checkImage("pictures", pictureName); | |
} | |
} | |
} | |
} | |
for (const systemImage of [ | |
"Balloon", | |
"ButtonSet", | |
"GameOver", | |
"IconSet", | |
"Shadow1", | |
"Shadow2", | |
"States", | |
"Weapons1", | |
"Weapons2", | |
"Weapons3", | |
"Window", | |
]) { | |
await checkImage("system", systemImage); | |
} | |
await checkImage("titles1", systemData.title1Name); | |
await checkImage("titles2", systemData.title2Name); | |
for (const vehicleName of ["airship", "boat", "ship"] as const) { | |
const vehicle = systemData[vehicleName]; | |
await checkImage("characters", vehicle.characterName); | |
await checkAudio("bgm", vehicle.bgm.name); | |
} | |
for (const actor of actors) { | |
if (!actor) continue; | |
await checkImage("characters", actor.characterName); | |
if (systemData.optSideView) { | |
await checkImage("sv_actors", actor.battlerName); | |
} | |
await checkImage("faces", actor.faceName); | |
} | |
await checkImage(systemData.optSideView ? "sv_enemies" : "enemies", systemData.battlerName); | |
for (const enemy of enemies) { | |
if (!enemy) continue; | |
await checkImage(systemData.optSideView ? "sv_enemies" : "enemies", enemy.battlerName); | |
} | |
await checkImage("battlebacks1", systemData.battleback1Name); | |
await checkImage("battlebacks2", systemData.battleback2Name); | |
for (const mapData of mapInfo) { | |
if (!mapData) continue; | |
const map: MapJson = JSON.parse(await Deno.readTextFile(`data/Map${`${mapData.id}`.padStart(3, "0")}.json`)); | |
await checkImage("battlebacks1", map.battleback1Name); | |
await checkImage("battlebacks2", map.battleback2Name); | |
await checkAudio("bgm", map.bgm.name); | |
await checkAudio("bgs", map.bgs.name); | |
await checkImage("parallaxes", map.parallaxName); | |
for (const event of map.events) { | |
if (!event) continue; | |
for (const page of event.pages) { | |
await checkImage("characters", page.image.characterName); | |
await processCommands(page.list); | |
} | |
} | |
} | |
for (const tileset of tilesets) { | |
if (!tileset) continue; | |
for (const name of tileset.tilesetNames) { | |
await checkImage("tilesets", name); | |
} | |
} | |
for (const commonEvent of commonEvents) { | |
if (!commonEvent) continue; | |
await processCommands(commonEvent.list); | |
} | |
type ActorsJson = ({ | |
battlerName: string; | |
characterName: string; | |
faceName: string; | |
} | null)[]; | |
type CommonEventsJson = ({ | |
list: Command[]; | |
} | null)[]; | |
type EnemiesJson = ({ | |
battlerName: string; | |
} | null)[]; | |
type MapJson = { | |
battleback1Name: string; | |
battleback2Name: string; | |
bgm: AudioData; | |
bgs: AudioData; | |
parallaxName: string; | |
tilesetId: number; | |
events: ({ | |
pages: { | |
image: { | |
tileId: number, | |
characterName: string, | |
}; | |
list: Command[]; | |
}[]; | |
} | null)[]; | |
}; | |
type MapInfosJson = ({ | |
id: number; | |
} | null)[]; | |
type SystemJson = { | |
hasEncryptedImages: boolean; | |
hasEncryptedAudio: boolean; | |
battleback1Name: string; | |
battleback2Name: string; | |
battlerName: string; | |
airship: { bgm: AudioData, characterName: string }; | |
boat: { bgm: AudioData, characterName: string }; | |
ship: { bgm: AudioData, characterName: string }; | |
optSideView: boolean; | |
title1Name: string; | |
title2Name: string; | |
}; | |
type TilesetsJson = ({ | |
tilesetNames: string[]; | |
} | null)[]; | |
type AudioData = { | |
name: string; | |
}; | |
type Command = { | |
code: number; | |
indent: number; | |
parameters: unknown[]; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment