Skip to content

Instantly share code, notes, and snippets.

@qnighy
Last active April 6, 2024 09:42
Show Gist options
  • Save qnighy/71fd328386cfc543be405ac5f6c3fde9 to your computer and use it in GitHub Desktop.
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
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