diff options
| -rw-r--r-- | tools/mzsurvey.c | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/tools/mzsurvey.c b/tools/mzsurvey.c new file mode 100644 index 0000000..61253a6 --- /dev/null +++ b/tools/mzsurvey.c @@ -0,0 +1,400 @@ +/* SPDX-License-Identifier: MIT */ +/* + * mzsurvey.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/* + * usage: mzsurvey <data folder> [plugins.js] + * reads an exported RPG Maker MV or MZ game and says what is in it and how much of it the engine can do + */ + +#define KONJSON_IMPLEMENTATION +#include "konjson.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/*** counters ***/ + +typedef struct count { + char key[120]; + int n; +} count_t; + +typedef struct table { + count_t *rows; + int count; + int capacity; +} table_t; + +static void tally(table_t *t, const char *key) +{ + int i; + + for (i = 0; i < t->count; i++) { + if (!strcmp(t->rows[i].key, key)) { + t->rows[i].n++; + return; + } + } + if (t->count == t->capacity) { + int grown = t->capacity ? t->capacity * 2 : 64; + count_t *rows = realloc(t->rows, (size_t)grown * sizeof *rows); + + if (!rows) return; + t->rows = rows; + t->capacity = grown; + } + snprintf(t->rows[t->count].key, sizeof t->rows[t->count].key, "%s", key); + t->rows[t->count].n = 1; + t->count++; +} + +static int byCount(const void *a, const void *b) +{ + const count_t *x = a, *y = b; + + if (x->n != y->n) return y->n - x->n; + return strcmp(x->key, y->key); +} + +static void show(table_t *t, const char *title, int limit) +{ + int i; + + if (!t->count) return; + qsort(t->rows, (size_t)t->count, sizeof *t->rows, byCount); + printf("\n%s (%d)\n", title, t->count); + for (i = 0; i < t->count && i < limit; i++) printf(" %6d %s\n", t->rows[i].n, t->rows[i].key); + if (t->count > limit) printf(" ... and %d more\n", t->count - limit); +} + +/*** event commands ***/ + +enum { TODO, PARTIAL, SCRIPT }; + +typedef struct command { + int code; + const char *name; + int status; +} command_t; + +static const command_t commands[] = { + {0, "end of list", SCRIPT}, {101, "show text", SCRIPT}, {102, "show choices", PARTIAL}, {103, "input number", TODO}, + {104, "select item", TODO}, {105, "show scrolling text", TODO}, {108, "comment", SCRIPT}, {109, "skip", TODO}, + {111, "conditional branch", PARTIAL}, {112, "loop", TODO}, {113, "break loop", TODO}, {115, "exit event processing", SCRIPT}, + {116, "erase event", TODO}, {117, "common event", SCRIPT}, {118, "label", SCRIPT}, {119, "jump to label", SCRIPT}, + {121, "control switches", SCRIPT}, {122, "control variables", PARTIAL}, {123, "control self switch", TODO}, {124, "control timer", TODO}, + {125, "change gold", TODO}, {126, "change items", TODO}, {127, "change weapons", TODO}, {128, "change armor", TODO}, + {129, "change party member", TODO}, {132, "change battle bgm", TODO}, {133, "change victory me", TODO}, {134, "change save access", TODO}, + {135, "change menu access", TODO}, {136, "change encounter", TODO}, {137, "change formation access", TODO}, {138, "change window color", TODO}, + {139, "change defeat me", TODO}, {140, "change vehicle bgm", TODO}, {201, "transfer player", TODO}, {202, "set vehicle location", TODO}, + {203, "set event location", TODO}, {204, "scroll map", TODO}, {205, "set movement route", TODO}, {206, "get on or off vehicle", TODO}, + {211, "change transparency", TODO}, {212, "show animation", TODO}, {213, "show balloon icon", TODO}, {214, "erase event", TODO}, + {216, "change player followers", TODO}, {217, "gather followers", TODO}, {221, "fadeout screen", TODO}, {222, "fadein screen", TODO}, + {223, "tint screen", TODO}, {224, "flash screen", TODO}, {225, "shake screen", TODO}, {230, "wait", SCRIPT}, + {231, "show picture", TODO}, {232, "move picture", TODO}, {233, "rotate picture", TODO}, {234, "tint picture", TODO}, + {235, "erase picture", TODO}, {236, "set weather effect", TODO}, {241, "play bgm", TODO}, {242, "fadeout bgm", TODO}, + {243, "save bgm", TODO}, {244, "resume bgm", TODO}, {245, "play bgs", TODO}, {246, "fadeout bgs", TODO}, + {249, "play me", TODO}, {250, "play se", TODO}, {251, "stop se", TODO}, {261, "play movie", TODO}, + {281, "change map name display", TODO}, {282, "change tileset", TODO}, {283, "change battle back", TODO}, {284, "change parallax", TODO}, + {285, "get location info", TODO}, {301, "battle processing", TODO}, {302, "shop processing", TODO}, {303, "name input processing", TODO}, + {311, "change hp", TODO}, {312, "change mp", TODO}, {313, "change state", TODO}, {314, "recover all", TODO}, + {315, "change exp", TODO}, {316, "change level", TODO}, {317, "change parameter", TODO}, {318, "change skill", TODO}, + {319, "change equipment", TODO}, {320, "change name", TODO}, {321, "change class", TODO}, {322, "change actor images", TODO}, + {323, "change vehicle image", TODO}, {324, "change nickname", TODO}, {325, "change profile", TODO}, {326, "change tp", TODO}, + {331, "change enemy hp", TODO}, {332, "change enemy mp", TODO}, {333, "change enemy state", TODO}, {334, "enemy recover all", TODO}, + {335, "enemy appear", TODO}, {336, "enemy transform", TODO}, {337, "show battle animation", TODO}, {339, "force action", TODO}, + {340, "abort battle", TODO}, {342, "change enemy tp", TODO}, {351, "open menu screen", TODO}, {352, "open save screen", TODO}, + {353, "game over", TODO}, {354, "return to title screen", TODO}, {355, "script", TODO}, {356, "plugin command (mv)", TODO}, + {357, "plugin command", TODO}, {401, "text line", SCRIPT}, {402, "when choice", PARTIAL}, {403, "when cancel", PARTIAL}, + {404, "end of choices", PARTIAL}, {405, "scrolling text line", TODO}, {408, "comment line", SCRIPT}, {411, "else", PARTIAL}, + {412, "end of branch", PARTIAL}, {413, "repeat above", TODO}, {505, "movement route step", TODO}, {601, "if win", TODO}, + {602, "if escape", TODO}, {603, "if lose", TODO}, {604, "end of battle result", TODO}, {605, "shop item line", TODO}, + {655, "script line", TODO}, {657, "plugin command line", TODO} +}; + +static const command_t *commandFor(int code) +{ + size_t i; + + for (i = 0; i < sizeof commands / sizeof *commands; i++) { + if (commands[i].code == code) return &commands[i]; + } + return NULL; +} + +/*** survey ***/ + +static table_t codes, pluginCommands, scriptCalls, noteTags, tilesets; +static int mapCount, eventCount, pageCount, listCount, conditionScripts, variableScripts, parallelPages, autorunPages; + +static void tagNotes(const char *note) +{ + const char *p = note; + + while (note && (p = strchr(p, '<'))) { + const char *q = p + 1; + char key[64]; + size_t n = 0; + + if (*q == '/') { + p = q; + continue; + } + while (*q && *q != '>' && *q != ':' && *q != ' ' && *q != '\n' && n < sizeof key - 1) key[n++] = *q++; + key[n] = '\0'; + if (n && *q) tally(¬eTags, key); + p = q; + } +} + +static void noteTable(const kon_json_t *list) +{ + int i; + + for (i = 0; i < kon_jsonCount(list); i++) tagNotes(kon_jsonString(kon_jsonGet(kon_jsonAt(list, i), "note"), NULL)); +} + +static void list(const kon_json_t *commandList) +{ + int i; + + if (!commandList) return; + listCount++; + for (i = 0; i < kon_jsonCount(commandList); i++) { + const kon_json_t *c = kon_jsonAt(commandList, i); + const kon_json_t *p = kon_jsonGet(c, "parameters"); + int code = kon_jsonInt(kon_jsonGet(c, "code"), -1); + char key[16]; + + snprintf(key, sizeof key, "%d", code); + tally(&codes, key); + if (code == 357) { + char name[120]; + + snprintf(name, sizeof name, "%s : %s", kon_jsonString(kon_jsonAt(p, 0), "?"), kon_jsonString(kon_jsonAt(p, 1), "?")); + tally(&pluginCommands, name); + } else if (code == 356) { + char name[120]; + const char *text = kon_jsonString(kon_jsonAt(p, 0), "?"); + size_t n = strcspn(text, " "); + + snprintf(name, sizeof name, "%.*s", (int)(n < 100 ? n : 100), text); + tally(&pluginCommands, name); + } else if (code == 355) { + char name[120]; + + snprintf(name, sizeof name, "%.100s", kon_jsonString(kon_jsonAt(p, 0), "?")); + tally(&scriptCalls, name); + } else if (code == 111 && kon_jsonInt(kon_jsonAt(p, 0), 0) == 12) { + conditionScripts++; + } else if (code == 122 && kon_jsonInt(kon_jsonAt(p, 3), 0) == 4) { + variableScripts++; + } + } +} + +static void pages(const kon_json_t *eventPages) +{ + int i; + + for (i = 0; i < kon_jsonCount(eventPages); i++) { + const kon_json_t *page = kon_jsonAt(eventPages, i); + int trigger = kon_jsonInt(kon_jsonGet(page, "trigger"), 0); + + pageCount++; + if (trigger == 3) autorunPages++; + if (trigger == 4) parallelPages++; + list(kon_jsonGet(page, "list")); + } +} + +static int survey(const char *folder) +{ + char path[1024], error[120]; + const kon_json_t *system; + kon_json_t *json; + int id; + + snprintf(path, sizeof path, "%s/System.json", folder); + json = kon_jsonParseFile(path, error, sizeof error); + if (!json) { + fprintf(stderr, "%s: %s\n", path, error); + return 1; + } + system = json; + printf("%s\n", kon_jsonString(kon_jsonGet(system, "gameTitle"), "(untitled)")); + printf(" %s, window %dx%d, tiles %d, %s battles\n", + kon_jsonGet(system, "advanced") ? "RPG Maker MZ" : "RPG Maker MV", + kon_jsonInt(kon_jsonGet(kon_jsonGet(system, "advanced"), "screenWidth"), 816), + kon_jsonInt(kon_jsonGet(kon_jsonGet(system, "advanced"), "screenHeight"), 624), + kon_jsonInt(kon_jsonGet(system, "tileSize"), 48), + kon_jsonBool(kon_jsonGet(system, "optSideView"), 0) ? "side view" : "front view"); + printf(" switches %d, variables %d, start map %d\n", kon_jsonCount(kon_jsonGet(system, "switches")) - 1, kon_jsonCount(kon_jsonGet(system, "variables")) - 1, kon_jsonInt(kon_jsonGet(system, "startMapId"), 0)); + kon_jsonFree(json); + + { + static const char *const names[] = {"Actors", "Classes", "Skills", "Items", "Weapons", "Armors", "Enemies", "Troops", "States", "Animations", "Tilesets", "CommonEvents"}; + size_t i; + + printf("\ndatabase\n"); + for (i = 0; i < sizeof names / sizeof *names; i++) { + snprintf(path, sizeof path, "%s/%s.json", folder, names[i]); + json = kon_jsonParseFile(path, error, sizeof error); + if (!json) { + printf(" %-13s %s\n", names[i], error); + continue; + } + printf(" %-13s %d\n", names[i], kon_jsonCount(json) - 1); + if (!strcmp(names[i], "CommonEvents") || !strcmp(names[i], "Troops")) { + int k; + + for (k = 1; k < kon_jsonCount(json); k++) { + list(kon_jsonGet(kon_jsonAt(json, k), "list")); + pages(kon_jsonGet(kon_jsonAt(json, k), "pages")); + } + } else if (strcmp(names[i], "Animations") && strcmp(names[i], "Tilesets")) { + noteTable(json); + } + kon_jsonFree(json); + } + } + + for (id = 1; id < 2000; id++) { + const kon_json_t *events; + int k; + char tile[32]; + + snprintf(path, sizeof path, "%s/Map%03d.json", folder, id); + json = kon_jsonParseFile(path, error, sizeof error); + if (!json) { + if (strncmp(error, "cannot open", 11)) fprintf(stderr, "%s: %s\n", path, error); + continue; + } + mapCount++; + snprintf(tile, sizeof tile, "tileset %d", kon_jsonInt(kon_jsonGet(json, "tilesetId"), 0)); + tally(&tilesets, tile); + events = kon_jsonGet(json, "events"); + for (k = 1; k < kon_jsonCount(events); k++) { + const kon_json_t *event = kon_jsonAt(events, k); + + if (event->type != KON_JSON_OBJECT) continue; + eventCount++; + tagNotes(kon_jsonString(kon_jsonGet(event, "note"), NULL)); + pages(kon_jsonGet(event, "pages")); + } + tagNotes(kon_jsonString(kon_jsonGet(json, "note"), NULL)); + kon_jsonFree(json); + } + + printf("\nmaps %d, events %d, pages %d, command lists %d\n", mapCount, eventCount, pageCount, listCount); + printf("autorun pages %d, parallel process pages %d\n", autorunPages, parallelPages); + return 0; +} + +static void commandReport(void) +{ + int done = 0, partial = 0, todo = 0, unknown = 0, i; + + qsort(codes.rows, (size_t)codes.count, sizeof *codes.rows, byCount); + printf("\nevent commands\n"); + for (i = 0; i < codes.count; i++) { + int code = atoi(codes.rows[i].key); + const command_t *c = commandFor(code); + int n = codes.rows[i].n; + + if (!c) unknown += n; + else if (c->status == SCRIPT) done += n; + else if (c->status == PARTIAL) partial += n; + else todo += n; + printf(" %6d %-4d %-28s %s\n", n, code, c ? c->name : "unknown", !c ? "?" : c->status == SCRIPT ? "konscript" : c->status == PARTIAL ? "partly" : "not yet"); + } + printf("\ncommands konscript has today: %d, partly: %d, not yet: %d, unknown: %d\n", done, partial, todo, unknown); + printf("script conditions %d, script variable values %d\n", conditionScripts, variableScripts); +} + +/*** plugins ***/ + +static void plugins(const char *path) +{ + FILE *f = fopen(path, "rb"); + char *text, *a, *b, error[120]; + long size; + kon_json_t *json; + int i; + + if (!f) { + printf("\nno plugins.js at %s\n", path); + return; + } + fseek(f, 0, SEEK_END); + size = ftell(f); + fseek(f, 0, SEEK_SET); + text = malloc((size_t)size + 1); + if (!text) { + fclose(f); + return; + } + size = (long)fread(text, 1, (size_t)size, f); + fclose(f); + text[size] = '\0'; + + a = strchr(text, '['); + b = strrchr(text, ']'); + json = a && b && b > a ? kon_jsonParse(a, (size_t)(b - a + 1), error, sizeof error) : NULL; + if (!json) { + printf("\nplugins.js: %s\n", a && b ? error : "no list found"); + free(text); + return; + } + + printf("\nplugins (%d)\n", kon_jsonCount(json)); + for (i = 0; i < kon_jsonCount(json); i++) { + const kon_json_t *p = kon_jsonAt(json, i); + + printf(" %-3s %s\n", kon_jsonBool(kon_jsonGet(p, "status"), 0) ? "on" : "off", kon_jsonString(kon_jsonGet(p, "name"), "?")); + } + kon_jsonFree(json); + free(text); +} + +int main(int argc, char **argv) +{ + char path[1024]; + + if (argc < 2) { + fprintf(stderr, "usage: mzsurvey <data folder> [plugins.js]\n"); + return 2; + } + if (survey(argv[1])) return 1; + commandReport(); + show(&pluginCommands, "plugin commands", 40); + show(&scriptCalls, "script calls", 25); + show(¬eTags, "note tags", 40); + show(&tilesets, "tilesets used by maps", 20); + snprintf(path, sizeof path, "%s/../js/plugins.js", argv[1]); + plugins(argc > 2 ? argv[2] : path); + return 0; +} |
