From b62d95ecb3538ce02b22b5ae07ef0e7096fbe7c6 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Sat, 6 Jun 2026 17:55:58 +0200 Subject: [PATCH 01/11] MACS2: extract translations --- engines/macs2/TRANSLATE.md | 29 + engines/macs2/create_macs2_translation.cpp | 632 +++++++++++++++++++++ 2 files changed, 661 insertions(+) create mode 100644 engines/macs2/TRANSLATE.md create mode 100644 engines/macs2/create_macs2_translation.cpp diff --git a/engines/macs2/TRANSLATE.md b/engines/macs2/TRANSLATE.md new file mode 100644 index 00000000..08cb74ce --- /dev/null +++ b/engines/macs2/TRANSLATE.md @@ -0,0 +1,29 @@ +# MACS2 Tools + +Tools for the MACS2 engine (Schatz im Silbersee). + +## Translation Workflow + +The game was originally released in German only. Translations are managed via standard PO files. + +### 1. Extract strings to a PO template + +```bash +create_macs2_translation extract RESOURCE.MCS macs2.pot +``` + +This produces a `.pot` file with all game strings grouped by dialog context. The format uses `msgctxt` to identify the source (scene or object) and `\n` to separate lines within a dialog unit. + +The `\n` separators correspond to individual display lines in the game. Keep the same number of lines as the original. + +### 3. Pack into binary + +```bash +create_macs2_translation pack en.po macs2_translation.dat +``` + +### 4. Install + +Place `macs2_translation.dat` in the game directory alongside `RESOURCE.MCS`. ScummVM will detect the translation and offer the translated language variant. + +The file is also distributed via `dists/engine-data/macs2_translation.dat` in the ScummVM source tree. diff --git a/engines/macs2/create_macs2_translation.cpp b/engines/macs2/create_macs2_translation.cpp new file mode 100644 index 00000000..f46ea2ae --- /dev/null +++ b/engines/macs2/create_macs2_translation.cpp @@ -0,0 +1,632 @@ +/* ScummVM Tools + * + * ScummVM Tools is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/* + * MACS2 Translation File Creator + * + * Workflow: + * 1. Extract: create_macs2_translation extract + * 2. Translate the .po file (Poedit, Weblate, etc.) + * 3. Pack: create_macs2_translation pack + * + * PO format: + * msgctxt "scene:2" (or "object:42") + * msgid "line1\nline2\nline3" + * msgstr "translated1\ntranslated2\ntranslated3" + * + * Each msgid groups consecutive strings that form one dialog/description unit. + * The \n separates individual lines that the engine displays separately. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static FILE *resFile = nullptr; + +static uint32_t readU32(FILE *f) { + uint8_t buf[4]; + fread(buf, 1, 4, f); + return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); +} + +static uint16_t readU16(FILE *f) { + uint8_t buf[2]; + fread(buf, 1, 2, f); + return buf[0] | (buf[1] << 8); +} + +static void writeU32(FILE *f, uint32_t v) { + uint8_t buf[4] = {(uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF), + (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 24) & 0xFF)}; + fwrite(buf, 1, 4, f); +} + +static void writeU16(FILE *f, uint16_t v) { + uint8_t buf[2] = {(uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF)}; + fwrite(buf, 1, 2, f); +} + +// CP850 to UTF-8 +static std::string cp850ToUtf8(const std::string &s) { + static const uint16_t cp850map[128] = { + 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, + 0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5, + 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, + 0x00FF,0x00D6,0x00DC,0x00F8,0x00A3,0x00D8,0x00D7,0x0192, + 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, + 0x00BF,0x00AE,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, + 0x2591,0x2592,0x2593,0x2502,0x2524,0x00C1,0x00C2,0x00C0, + 0x00A9,0x2563,0x2551,0x2557,0x255D,0x00A2,0x00A5,0x2510, + 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x00E3,0x00C3, + 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x00A4, + 0x00F0,0x00D0,0x00CA,0x00CB,0x00C8,0x0131,0x00CD,0x00CE, + 0x00CF,0x2518,0x250C,0x2588,0x2584,0x00A6,0x00CC,0x2580, + 0x00D3,0x00DF,0x00D4,0x00D2,0x00F5,0x00D5,0x00B5,0x00FE, + 0x00DE,0x00DA,0x00DB,0x00D9,0x00FD,0x00DD,0x00AF,0x00B4, + 0x00AD,0x00B1,0x2017,0x00BE,0x00B6,0x00A7,0x00F7,0x00B8, + 0x00B0,0x00A8,0x00B7,0x00B9,0x00B3,0x00B2,0x25A0,0x00A0, + }; + std::string out; + for (unsigned char c : s) { + if (c < 0x80) { + out += (char)c; + } else { + uint16_t u = cp850map[c - 0x80]; + if (u < 0x80) { + out += (char)u; + } else if (u < 0x800) { + out += (char)(0xC0 | (u >> 6)); + out += (char)(0x80 | (u & 0x3F)); + } else { + out += (char)(0xE0 | (u >> 12)); + out += (char)(0x80 | ((u >> 6) & 0x3F)); + out += (char)(0x80 | (u & 0x3F)); + } + } + } + return out; +} + +static std::string decryptString(const uint8_t *data, uint16_t length) { + std::string result; + for (int i = 1; i <= length; i++) { + uint8_t x = (uint8_t)(i * i * 0x0C); + uint8_t y = (uint8_t)(data[i - 1] ^ i); + uint8_t r = (uint8_t)(x ^ y); + result += (char)r; + } + return result; +} + +static std::vector readStringsFromBlob(uint32_t blobOffset) { + std::vector result; + fseek(resFile, blobOffset, SEEK_SET); + uint16_t totalSize = readU16(resFile); + if (totalSize == 0) + return result; + std::vector data(totalSize); + fread(data.data(), 1, totalSize, resFile); + uint32_t pos = 0; + while (pos + 2 <= totalSize) { + uint16_t len = data[pos] | (data[pos + 1] << 8); + pos += 2; + if (len == 0 || pos + len > totalSize) + break; + result.push_back(decryptString(data.data() + pos, len)); + pos += len; + } + return result; +} + +// Compute byte offset -> string index mapping for a blob +static std::vector computeStringOffsets(uint32_t blobOffset) { + std::vector offsets; // offsets[stringIndex] = byte offset + fseek(resFile, blobOffset, SEEK_SET); + uint16_t totalSize = readU16(resFile); + if (totalSize == 0) + return offsets; + std::vector data(totalSize); + fread(data.data(), 1, totalSize, resFile); + uint32_t pos = 0; + while (pos + 2 <= totalSize) { + offsets.push_back(pos); + uint16_t len = data[pos] | (data[pos + 1] << 8); + pos += 2; + if (len == 0 || pos + len > totalSize) + break; + pos += len; + } + return offsets; +} + +// A string reference found in a script: (byteOffset, numLines) +struct StringRef { + uint16_t offset; + uint16_t numLines; +}; + +// Parse a scene/object script to find all string references (offset, numLines) +static std::vector parseScriptStringRefs(uint32_t scriptDataOffset) { + std::vector refs; + fseek(resFile, scriptDataOffset, SEEK_SET); + + // Skip 0x80 bytes of special anim offsets + fseek(resFile, 0x80, SEEK_CUR); + uint16_t scriptSize = readU16(resFile); + if (scriptSize == 0) + return refs; + + std::vector script(scriptSize); + fread(script.data(), 1, scriptSize, resFile); + + uint32_t pos = 0; + while (pos + 1 < scriptSize) { + uint8_t opcode = script[pos++]; + if (opcode == 0x00) + continue; + if (pos >= scriptSize) + break; + uint8_t length = script[pos++]; + uint32_t endPos = pos + length; + if (endPos > scriptSize) + break; + + // Opcodes that contain string references: + // 0x0A (printStringLeft): 2x value(3) + offset(2) + numLines(2) = 10 + // 0x0D (dialogue): 4x value(3) + offset(2) + numLines(2) = 16 + // 0x16 (addDialogueChoice): 1x value(3) + offset(2) + numLines(2) = 7 + // 0x30 (printStringRight): same as 0x0A = 10 + // 0x3A (overlayText): 3x value(3) + offset(2) + numLines(2) = 13 + + if (opcode == 0x0A || opcode == 0x30) { + // 2x value(3) + uint16 offset + uint16 numLines = 10 + if (length >= 10) { + uint16_t off = script[pos + 6] | (script[pos + 7] << 8); + uint16_t num = script[pos + 8] | (script[pos + 9] << 8); + if (num > 0 && num < 200) + refs.push_back({off, num}); + } + } else if (opcode == 0x0D) { + // 4x value(3) + uint16 offset + uint16 numLines + if (length >= 16) { + uint16_t off = script[pos + 12] | (script[pos + 13] << 8); + uint16_t num = script[pos + 14] | (script[pos + 15] << 8); + if (num > 0 && num < 200) + refs.push_back({off, num}); + } + } else if (opcode == 0x16) { + // 1x value(3) + uint16 offset + uint16 numLines + if (length >= 7) { + uint16_t off = script[pos + 3] | (script[pos + 4] << 8); + uint16_t num = script[pos + 5] | (script[pos + 6] << 8); + if (num > 0 && num < 200) + refs.push_back({off, num}); + } + } else if (opcode == 0x3A) { + // 3x value(3) + uint16 offset + uint16 entryType + // Here entryType is numLines=1 always, offset references 1 string + if (length >= 13) { + uint16_t off = script[pos + 9] | (script[pos + 10] << 8); + refs.push_back({off, 1}); + } + } + + pos = endPos; + } + return refs; +} + +// Group string indices by their dialog references +// Returns vector of (startIndex, count) pairs, sorted by startIndex +static std::vector> groupStrings( + const std::vector &stringOffsets, + const std::vector &scriptRefs, + int totalStrings) { + + // Map byte offset -> string index + std::map offsetToIndex; + for (int i = 0; i < (int)stringOffsets.size(); i++) + offsetToIndex[stringOffsets[i]] = i; + + // Collect which string indices are the start of a group + std::set groupStarts; + std::map groupSizes; // startIndex -> numLines + + for (const auto &ref : scriptRefs) { + auto it = offsetToIndex.find(ref.offset); + if (it != offsetToIndex.end()) { + int idx = it->second; + groupStarts.insert(idx); + // Keep the largest numLines for this start + if (groupSizes.find(idx) == groupSizes.end() || ref.numLines > (uint16_t)groupSizes[idx]) + groupSizes[idx] = ref.numLines; + } + } + + // Build groups. Strings not referenced by any opcode get grouped with + // the nearest preceding group start, or form their own single-line group. + std::vector> groups; + if (groupStarts.empty()) { + // No script refs found - put all strings in one group + if (totalStrings > 0) + groups.push_back({0, totalStrings}); + return groups; + } + + for (auto it = groupStarts.begin(); it != groupStarts.end(); ++it) { + int start = *it; + int count = groupSizes[start]; + if (start + count > totalStrings) + count = totalStrings - start; + groups.push_back({start, count}); + } + + // Add any orphan strings (not covered by any group) as single-line entries + std::set covered; + for (const auto &g : groups) + for (int i = g.first; i < g.first + g.second; i++) + covered.insert(i); + + for (int i = 0; i < totalStrings; i++) { + if (covered.find(i) == covered.end()) + groups.push_back({i, 1}); + } + + // Sort by start index + std::sort(groups.begin(), groups.end()); + + // Remove duplicates/overlaps + std::vector> merged; + for (const auto &g : groups) { + if (!merged.empty() && g.first < merged.back().first + merged.back().second) + continue; // skip overlap + merged.push_back(g); + } + return merged; +} + +static std::string poEscape(const std::string &s) { + std::string out; + for (char c : s) { + if (c == '\\') out += "\\\\"; + else if (c == '"') out += "\\\""; + else if (c == '\n') out += "\\n"; + else out += c; + } + return out; +} + +static std::string poUnescape(const std::string &s) { + std::string out; + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '\\' && i + 1 < s.size()) { + if (s[i + 1] == 'n') { out += '\n'; i++; } + else if (s[i + 1] == '\\') { out += '\\'; i++; } + else if (s[i + 1] == '"') { out += '"'; i++; } + else out += s[i]; + } else { + out += s[i]; + } + } + return out; +} + +static void writePoEntry(FILE *out, const char *ctx, int startIdx, + const std::vector &strings, int offset, int count) { + fprintf(out, "msgctxt \"%s:%d\"\n", ctx, startIdx); + fprintf(out, "msgid \"\"\n"); + for (int i = 0; i < count; i++) { + std::string line = poEscape(cp850ToUtf8(strings[offset + i])); + if (i < count - 1) + fprintf(out, "\"%s\\n\"\n", line.c_str()); + else + fprintf(out, "\"%s\"\n", line.c_str()); + } + fprintf(out, "msgstr \"\"\n\n"); +} + +static int doExtract(const char *resPath, const char *outPath) { + resFile = fopen(resPath, "rb"); + if (!resFile) { + fprintf(stderr, "Error: Cannot open '%s'\n", resPath); + return 1; + } + + FILE *out = fopen(outPath, "w"); + if (!out) { + fprintf(stderr, "Error: Cannot create '%s'\n", outPath); + fclose(resFile); + return 1; + } + + fprintf(out, "# MACS2 - Schatz im Silbersee translation file\n"); + fprintf(out, "# Copyright (C) ScummVM Team\n"); + fprintf(out, "#\n"); + fprintf(out, "msgid \"\"\n"); + fprintf(out, "msgstr \"\"\n"); + fprintf(out, "\"Project-Id-Version: macs2\\n\"\n"); + fprintf(out, "\"Report-Msgid-Bugs-To: scummvm-devel@lists.scummvm.org\\n\"\n"); + fprintf(out, "\"MIME-Version: 1.0\\n\"\n"); + fprintf(out, "\"Content-Type: text/plain; charset=UTF-8\\n\"\n"); + fprintf(out, "\"Content-Transfer-Encoding: 8bit\\n\"\n\n"); + + int totalEntries = 0; + + // Extract scene strings grouped by dialog + for (int scene = 1; scene <= 512; scene++) { + // Get strings offset + uint32_t strOff; + fseek(resFile, (uint32_t)scene * 0xC + 0xC + 0x4 - 0x4, SEEK_SET); + strOff = readU32(resFile); + if (strOff == 0) + continue; + + // Get script/data offset + fseek(resFile, (uint32_t)scene * 0xC + 0xC + 0x4 - 0x8, SEEK_SET); + uint32_t dataOff = readU32(resFile); + + std::vector strings = readStringsFromBlob(strOff); + if (strings.empty()) + continue; + + std::vector offsets = computeStringOffsets(strOff); + std::vector refs; + if (dataOff != 0) + refs = parseScriptStringRefs(dataOff); + + auto groups = groupStrings(offsets, refs, (int)strings.size()); + + char ctx[64]; + snprintf(ctx, sizeof(ctx), "scene:%d", scene); + for (const auto &g : groups) { + writePoEntry(out, ctx, g.first, strings, g.first, g.second); + totalEntries++; + } + } + + // Extract object strings grouped by dialog + for (int obj = 1; obj <= 512; obj++) { + // Engine formula: index * 0xC + 0xC + 0x4 + 0x17FC = strings offset address + uint32_t strAddr = (uint32_t)obj * 0xC + 0xC + 0x4 + 0x17FC; + + fseek(resFile, strAddr, SEEK_SET); + uint32_t strOff = readU32(resFile); + if (strOff == 0) + continue; + + // Script/data offset is 4 bytes before strings offset in the table + fseek(resFile, strAddr - 4, SEEK_SET); + uint32_t dataOff = readU32(resFile); + + std::vector strings = readStringsFromBlob(strOff); + if (strings.empty()) + continue; + + std::vector offsets = computeStringOffsets(strOff); + std::vector refs; + if (dataOff != 0) + refs = parseScriptStringRefs(dataOff); + + auto groups = groupStrings(offsets, refs, (int)strings.size()); + + char ctx[64]; + snprintf(ctx, sizeof(ctx), "object:%d", obj); + for (const auto &g : groups) { + writePoEntry(out, ctx, g.first, strings, g.first, g.second); + totalEntries++; + } + } + + fclose(out); + fclose(resFile); + printf("Extracted %d dialog entries to %s\n", totalEntries, outPath); + return 0; +} + +struct TranslationBlock { + uint16_t id; + std::vector strings; +}; + +static int doPack(const char *poPath, const char *outPath) { + FILE *in = fopen(poPath, "r"); + if (!in) { + fprintf(stderr, "Error: Cannot open '%s'\n", poPath); + return 1; + } + + // Parse PO: msgctxt "scene:N:startIdx" or "object:N:startIdx" + // msgstr contains \n-separated translated lines + std::map>> sceneStrings; + std::map>> objectStrings; + + char line[8192]; + bool isScene = false; + uint16_t currentId = 0; + int currentStartIdx = 0; + bool hasCtx = false; + bool inMsgstr = false; + bool inMsgid = false; + std::string currentMsgstr; + + auto flushEntry = [&]() { + if (hasCtx && !currentMsgstr.empty()) { + std::string text = poUnescape(currentMsgstr); + std::vector lines; + size_t start = 0; + while (start < text.size()) { + size_t nl = text.find('\n', start); + if (nl == std::string::npos) { + lines.push_back(text.substr(start)); + break; + } + lines.push_back(text.substr(start, nl - start)); + start = nl + 1; + } + if (!lines.empty()) { + if (isScene) + sceneStrings[currentId][currentStartIdx] = lines; + else + objectStrings[currentId][currentStartIdx] = lines; + } + } + hasCtx = false; + currentMsgstr.clear(); + inMsgstr = false; + inMsgid = false; + }; + + while (fgets(line, sizeof(line), in)) { + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; + + if (strncmp(line, "msgctxt \"", 9) == 0) { + flushEntry(); + int id = 0, idx = 0; + if (sscanf(line + 9, "scene:%d:%d", &id, &idx) == 2) { + isScene = true; currentId = (uint16_t)id; currentStartIdx = idx; hasCtx = true; + } else if (sscanf(line + 9, "object:%d:%d", &id, &idx) == 2) { + isScene = false; currentId = (uint16_t)id; currentStartIdx = idx; hasCtx = true; + } + continue; + } + if (strncmp(line, "msgstr ", 7) == 0) { + inMsgstr = true; inMsgid = false; + char *s = strchr(line + 7, '"'); + if (s) { s++; char *e = strrchr(s, '"'); if (e) currentMsgstr = std::string(s, e - s); } + continue; + } + if (strncmp(line, "msgid ", 6) == 0) { inMsgid = true; inMsgstr = false; continue; } + if (line[0] == '"' && inMsgstr) { + char *s = line + 1; char *e = strrchr(s, '"'); + if (e) currentMsgstr += std::string(s, e - s); + continue; + } + if (line[0] == '"' && inMsgid) { continue; } // skip msgid continuation + if (line[0] == '\0') flushEntry(); + } + flushEntry(); + fclose(in); + + // Build translation blocks - flatten indexed groups into sequential strings + std::vector sceneBlocks; + for (auto &kv : sceneStrings) { + TranslationBlock block; + block.id = kv.first; + // Find max string index + int maxIdx = 0; + for (auto &gv : kv.second) + for (int i = 0; i < (int)gv.second.size(); i++) + if (gv.first + i > maxIdx) maxIdx = gv.first + i; + block.strings.resize(maxIdx + 1); + for (auto &gv : kv.second) + for (int i = 0; i < (int)gv.second.size(); i++) + block.strings[gv.first + i] = gv.second[i]; + sceneBlocks.push_back(block); + } + + std::vector objectBlocks; + for (auto &kv : objectStrings) { + TranslationBlock block; + block.id = kv.first; + int maxIdx = 0; + for (auto &gv : kv.second) + for (int i = 0; i < (int)gv.second.size(); i++) + if (gv.first + i > maxIdx) maxIdx = gv.first + i; + block.strings.resize(maxIdx + 1); + for (auto &gv : kv.second) + for (int i = 0; i < (int)gv.second.size(); i++) + block.strings[gv.first + i] = gv.second[i]; + objectBlocks.push_back(block); + } + + // Write binary DAT + FILE *out = fopen(outPath, "wb"); + if (!out) { fprintf(stderr, "Error: Cannot create '%s'\n", outPath); return 1; } + + fwrite("MCS2", 1, 4, out); + writeU16(out, 1); + writeU16(out, (uint16_t)sceneBlocks.size()); + writeU16(out, (uint16_t)objectBlocks.size()); + + long indexStart = ftell(out); + uint32_t indexSize = ((uint32_t)sceneBlocks.size() + (uint32_t)objectBlocks.size()) * 8; + for (uint32_t i = 0; i < indexSize; i++) fputc(0, out); + + std::vector sceneOffsets; + for (const auto &block : sceneBlocks) { + sceneOffsets.push_back((uint32_t)ftell(out)); + for (const auto &s : block.strings) { + writeU16(out, (uint16_t)s.size()); + if (!s.empty()) fwrite(s.data(), 1, s.size(), out); + } + } + std::vector objectOffsets; + for (const auto &block : objectBlocks) { + objectOffsets.push_back((uint32_t)ftell(out)); + for (const auto &s : block.strings) { + writeU16(out, (uint16_t)s.size()); + if (!s.empty()) fwrite(s.data(), 1, s.size(), out); + } + } + + fseek(out, indexStart, SEEK_SET); + for (size_t i = 0; i < sceneBlocks.size(); i++) { + writeU16(out, sceneBlocks[i].id); + writeU16(out, (uint16_t)sceneBlocks[i].strings.size()); + writeU32(out, sceneOffsets[i]); + } + for (size_t i = 0; i < objectBlocks.size(); i++) { + writeU16(out, objectBlocks[i].id); + writeU16(out, (uint16_t)objectBlocks[i].strings.size()); + writeU32(out, objectOffsets[i]); + } + + fclose(out); + printf("Packed %zu scene + %zu object blocks into %s\n", + sceneBlocks.size(), objectBlocks.size(), outPath); + return 0; +} + +static void printHelp(const char *bin) { + printf("MACS2 Translation File Creator\n\n"); + printf(" %s extract \n", bin); + printf(" %s pack \n", bin); +} + +int main(int argc, char **argv) { + if (argc < 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { + printHelp(argv[0]); + return 1; + } + if (!strcmp(argv[1], "extract")) return doExtract(argv[2], argv[3]); + if (!strcmp(argv[1], "pack")) return doPack(argv[2], argv[3]); + printHelp(argv[0]); + return 1; +} From b804063b07c05a57d13fff253f3ecc8e9ea37c87 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Sat, 6 Jun 2026 18:02:27 +0200 Subject: [PATCH 02/11] MACS2: fixed demacs opcode handling --- engines/macs2/create_macs2_translation.cpp | 266 ++++++++++++++++----- engines/macs2/demacs2.cpp | 4 +- 2 files changed, 212 insertions(+), 58 deletions(-) diff --git a/engines/macs2/create_macs2_translation.cpp b/engines/macs2/create_macs2_translation.cpp index f46ea2ae..54aec097 100644 --- a/engines/macs2/create_macs2_translation.cpp +++ b/engines/macs2/create_macs2_translation.cpp @@ -19,7 +19,7 @@ * */ -/* +/** * MACS2 Translation File Creator * * Workflow: @@ -36,15 +36,15 @@ * The \n separates individual lines that the engine displays separately. */ +#include +#include +#include #include #include #include #include -#include #include #include -#include -#include static FILE *resFile = nullptr; @@ -62,7 +62,7 @@ static uint16_t readU16(FILE *f) { static void writeU32(FILE *f, uint32_t v) { uint8_t buf[4] = {(uint8_t)(v & 0xFF), (uint8_t)((v >> 8) & 0xFF), - (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 24) & 0xFF)}; + (uint8_t)((v >> 16) & 0xFF), (uint8_t)((v >> 24) & 0xFF)}; fwrite(buf, 1, 4, f); } @@ -74,22 +74,134 @@ static void writeU16(FILE *f, uint16_t v) { // CP850 to UTF-8 static std::string cp850ToUtf8(const std::string &s) { static const uint16_t cp850map[128] = { - 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, - 0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5, - 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, - 0x00FF,0x00D6,0x00DC,0x00F8,0x00A3,0x00D8,0x00D7,0x0192, - 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, - 0x00BF,0x00AE,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, - 0x2591,0x2592,0x2593,0x2502,0x2524,0x00C1,0x00C2,0x00C0, - 0x00A9,0x2563,0x2551,0x2557,0x255D,0x00A2,0x00A5,0x2510, - 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x00E3,0x00C3, - 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x00A4, - 0x00F0,0x00D0,0x00CA,0x00CB,0x00C8,0x0131,0x00CD,0x00CE, - 0x00CF,0x2518,0x250C,0x2588,0x2584,0x00A6,0x00CC,0x2580, - 0x00D3,0x00DF,0x00D4,0x00D2,0x00F5,0x00D5,0x00B5,0x00FE, - 0x00DE,0x00DA,0x00DB,0x00D9,0x00FD,0x00DD,0x00AF,0x00B4, - 0x00AD,0x00B1,0x2017,0x00BE,0x00B6,0x00A7,0x00F7,0x00B8, - 0x00B0,0x00A8,0x00B7,0x00B9,0x00B3,0x00B2,0x25A0,0x00A0, + 0x00C7, + 0x00FC, + 0x00E9, + 0x00E2, + 0x00E4, + 0x00E0, + 0x00E5, + 0x00E7, + 0x00EA, + 0x00EB, + 0x00E8, + 0x00EF, + 0x00EE, + 0x00EC, + 0x00C4, + 0x00C5, + 0x00C9, + 0x00E6, + 0x00C6, + 0x00F4, + 0x00F6, + 0x00F2, + 0x00FB, + 0x00F9, + 0x00FF, + 0x00D6, + 0x00DC, + 0x00F8, + 0x00A3, + 0x00D8, + 0x00D7, + 0x0192, + 0x00E1, + 0x00ED, + 0x00F3, + 0x00FA, + 0x00F1, + 0x00D1, + 0x00AA, + 0x00BA, + 0x00BF, + 0x00AE, + 0x00AC, + 0x00BD, + 0x00BC, + 0x00A1, + 0x00AB, + 0x00BB, + 0x2591, + 0x2592, + 0x2593, + 0x2502, + 0x2524, + 0x00C1, + 0x00C2, + 0x00C0, + 0x00A9, + 0x2563, + 0x2551, + 0x2557, + 0x255D, + 0x00A2, + 0x00A5, + 0x2510, + 0x2514, + 0x2534, + 0x252C, + 0x251C, + 0x2500, + 0x253C, + 0x00E3, + 0x00C3, + 0x255A, + 0x2554, + 0x2569, + 0x2566, + 0x2560, + 0x2550, + 0x256C, + 0x00A4, + 0x00F0, + 0x00D0, + 0x00CA, + 0x00CB, + 0x00C8, + 0x0131, + 0x00CD, + 0x00CE, + 0x00CF, + 0x2518, + 0x250C, + 0x2588, + 0x2584, + 0x00A6, + 0x00CC, + 0x2580, + 0x00D3, + 0x00DF, + 0x00D4, + 0x00D2, + 0x00F5, + 0x00D5, + 0x00B5, + 0x00FE, + 0x00DE, + 0x00DA, + 0x00DB, + 0x00D9, + 0x00FD, + 0x00DD, + 0x00AF, + 0x00B4, + 0x00AD, + 0x00B1, + 0x2017, + 0x00BE, + 0x00B6, + 0x00A7, + 0x00F7, + 0x00B8, + 0x00B0, + 0x00A8, + 0x00B7, + 0x00B9, + 0x00B3, + 0x00B2, + 0x25A0, + 0x00A0, }; std::string out; for (unsigned char c : s) { @@ -243,10 +355,10 @@ static std::vector parseScriptStringRefs(uint32_t scriptDataOffset) { // Group string indices by their dialog references // Returns vector of (startIndex, count) pairs, sorted by startIndex -static std::vector> groupStrings( - const std::vector &stringOffsets, - const std::vector &scriptRefs, - int totalStrings) { +static std::vector > groupStrings( + const std::vector &stringOffsets, + const std::vector &scriptRefs, + int totalStrings) { // Map byte offset -> string index std::map offsetToIndex; @@ -270,7 +382,7 @@ static std::vector> groupStrings( // Build groups. Strings not referenced by any opcode get grouped with // the nearest preceding group start, or form their own single-line group. - std::vector> groups; + std::vector > groups; if (groupStarts.empty()) { // No script refs found - put all strings in one group if (totalStrings > 0) @@ -301,7 +413,7 @@ static std::vector> groupStrings( std::sort(groups.begin(), groups.end()); // Remove duplicates/overlaps - std::vector> merged; + std::vector > merged; for (const auto &g : groups) { if (!merged.empty() && g.first < merged.back().first + merged.back().second) continue; // skip overlap @@ -313,10 +425,14 @@ static std::vector> groupStrings( static std::string poEscape(const std::string &s) { std::string out; for (char c : s) { - if (c == '\\') out += "\\\\"; - else if (c == '"') out += "\\\""; - else if (c == '\n') out += "\\n"; - else out += c; + if (c == '\\') + out += "\\\\"; + else if (c == '"') + out += "\\\""; + else if (c == '\n') + out += "\\n"; + else + out += c; } return out; } @@ -325,10 +441,17 @@ static std::string poUnescape(const std::string &s) { std::string out; for (size_t i = 0; i < s.size(); i++) { if (s[i] == '\\' && i + 1 < s.size()) { - if (s[i + 1] == 'n') { out += '\n'; i++; } - else if (s[i + 1] == '\\') { out += '\\'; i++; } - else if (s[i + 1] == '"') { out += '"'; i++; } - else out += s[i]; + if (s[i + 1] == 'n') { + out += '\n'; + i++; + } else if (s[i + 1] == '\\') { + out += '\\'; + i++; + } else if (s[i + 1] == '"') { + out += '"'; + i++; + } else + out += s[i]; } else { out += s[i]; } @@ -337,7 +460,7 @@ static std::string poUnescape(const std::string &s) { } static void writePoEntry(FILE *out, const char *ctx, int startIdx, - const std::vector &strings, int offset, int count) { + const std::vector &strings, int offset, int count) { fprintf(out, "msgctxt \"%s:%d\"\n", ctx, startIdx); fprintf(out, "msgid \"\"\n"); for (int i = 0; i < count; i++) { @@ -462,8 +585,8 @@ static int doPack(const char *poPath, const char *outPath) { // Parse PO: msgctxt "scene:N:startIdx" or "object:N:startIdx" // msgstr contains \n-separated translated lines - std::map>> sceneStrings; - std::map>> objectStrings; + std::map > > sceneStrings; + std::map > > objectStrings; char line[8192]; bool isScene = false; @@ -510,26 +633,47 @@ static int doPack(const char *poPath, const char *outPath) { flushEntry(); int id = 0, idx = 0; if (sscanf(line + 9, "scene:%d:%d", &id, &idx) == 2) { - isScene = true; currentId = (uint16_t)id; currentStartIdx = idx; hasCtx = true; + isScene = true; + currentId = (uint16_t)id; + currentStartIdx = idx; + hasCtx = true; } else if (sscanf(line + 9, "object:%d:%d", &id, &idx) == 2) { - isScene = false; currentId = (uint16_t)id; currentStartIdx = idx; hasCtx = true; + isScene = false; + currentId = (uint16_t)id; + currentStartIdx = idx; + hasCtx = true; } continue; } if (strncmp(line, "msgstr ", 7) == 0) { - inMsgstr = true; inMsgid = false; + inMsgstr = true; + inMsgid = false; char *s = strchr(line + 7, '"'); - if (s) { s++; char *e = strrchr(s, '"'); if (e) currentMsgstr = std::string(s, e - s); } + if (s) { + s++; + char *e = strrchr(s, '"'); + if (e) + currentMsgstr = std::string(s, e - s); + } + continue; + } + if (strncmp(line, "msgid ", 6) == 0) { + inMsgid = true; + inMsgstr = false; continue; } - if (strncmp(line, "msgid ", 6) == 0) { inMsgid = true; inMsgstr = false; continue; } if (line[0] == '"' && inMsgstr) { - char *s = line + 1; char *e = strrchr(s, '"'); - if (e) currentMsgstr += std::string(s, e - s); + char *s = line + 1; + char *e = strrchr(s, '"'); + if (e) + currentMsgstr += std::string(s, e - s); continue; } - if (line[0] == '"' && inMsgid) { continue; } // skip msgid continuation - if (line[0] == '\0') flushEntry(); + if (line[0] == '"' && inMsgid) { + continue; + } // skip msgid continuation + if (line[0] == '\0') + flushEntry(); } flushEntry(); fclose(in); @@ -543,7 +687,8 @@ static int doPack(const char *poPath, const char *outPath) { int maxIdx = 0; for (auto &gv : kv.second) for (int i = 0; i < (int)gv.second.size(); i++) - if (gv.first + i > maxIdx) maxIdx = gv.first + i; + if (gv.first + i > maxIdx) + maxIdx = gv.first + i; block.strings.resize(maxIdx + 1); for (auto &gv : kv.second) for (int i = 0; i < (int)gv.second.size(); i++) @@ -558,7 +703,8 @@ static int doPack(const char *poPath, const char *outPath) { int maxIdx = 0; for (auto &gv : kv.second) for (int i = 0; i < (int)gv.second.size(); i++) - if (gv.first + i > maxIdx) maxIdx = gv.first + i; + if (gv.first + i > maxIdx) + maxIdx = gv.first + i; block.strings.resize(maxIdx + 1); for (auto &gv : kv.second) for (int i = 0; i < (int)gv.second.size(); i++) @@ -568,7 +714,10 @@ static int doPack(const char *poPath, const char *outPath) { // Write binary DAT FILE *out = fopen(outPath, "wb"); - if (!out) { fprintf(stderr, "Error: Cannot create '%s'\n", outPath); return 1; } + if (!out) { + fprintf(stderr, "Error: Cannot create '%s'\n", outPath); + return 1; + } fwrite("MCS2", 1, 4, out); writeU16(out, 1); @@ -577,14 +726,16 @@ static int doPack(const char *poPath, const char *outPath) { long indexStart = ftell(out); uint32_t indexSize = ((uint32_t)sceneBlocks.size() + (uint32_t)objectBlocks.size()) * 8; - for (uint32_t i = 0; i < indexSize; i++) fputc(0, out); + for (uint32_t i = 0; i < indexSize; i++) + fputc(0, out); std::vector sceneOffsets; for (const auto &block : sceneBlocks) { sceneOffsets.push_back((uint32_t)ftell(out)); for (const auto &s : block.strings) { writeU16(out, (uint16_t)s.size()); - if (!s.empty()) fwrite(s.data(), 1, s.size(), out); + if (!s.empty()) + fwrite(s.data(), 1, s.size(), out); } } std::vector objectOffsets; @@ -592,7 +743,8 @@ static int doPack(const char *poPath, const char *outPath) { objectOffsets.push_back((uint32_t)ftell(out)); for (const auto &s : block.strings) { writeU16(out, (uint16_t)s.size()); - if (!s.empty()) fwrite(s.data(), 1, s.size(), out); + if (!s.empty()) + fwrite(s.data(), 1, s.size(), out); } } @@ -610,7 +762,7 @@ static int doPack(const char *poPath, const char *outPath) { fclose(out); printf("Packed %zu scene + %zu object blocks into %s\n", - sceneBlocks.size(), objectBlocks.size(), outPath); + sceneBlocks.size(), objectBlocks.size(), outPath); return 0; } @@ -625,8 +777,10 @@ int main(int argc, char **argv) { printHelp(argv[0]); return 1; } - if (!strcmp(argv[1], "extract")) return doExtract(argv[2], argv[3]); - if (!strcmp(argv[1], "pack")) return doPack(argv[2], argv[3]); + if (!strcmp(argv[1], "extract")) + return doExtract(argv[2], argv[3]); + if (!strcmp(argv[1], "pack")) + return doPack(argv[2], argv[3]); printHelp(argv[0]); return 1; } diff --git a/engines/macs2/demacs2.cpp b/engines/macs2/demacs2.cpp index 4fd9a0db..8979cb35 100644 --- a/engines/macs2/demacs2.cpp +++ b/engines/macs2/demacs2.cpp @@ -338,7 +338,8 @@ static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { indent++; break; } - case 0x0A: { + case 0x0A: + case 0x30: { std::string x = formatValue(); std::string y = formatValue(); uint16_t strOffset = readWord(); @@ -650,7 +651,6 @@ static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { case 0x1C: case 0x1D: case 0x28: - case 0x30: case 0x36: case 0x37: case 0x39: From b0d1ba6b1ec1573c140f48e84aa6a0054277c898 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Tue, 9 Jun 2026 19:46:14 +0200 Subject: [PATCH 03/11] MACS2: fixed opcode name --- engines/macs2/demacs2.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engines/macs2/demacs2.cpp b/engines/macs2/demacs2.cpp index 8979cb35..2e0b4871 100644 --- a/engines/macs2/demacs2.cpp +++ b/engines/macs2/demacs2.cpp @@ -203,7 +203,7 @@ static const char *getOpcodeName(uint8_t opcode) { case 0x30: return "printStringRight"; case 0x31: - return "setVolume"; + return "setPaletteDarkness"; case 0x32: return "setObjectClickable"; case 0x33: From cbc7469ba73c44d75089d613f8abbf9e65b9921b Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Tue, 9 Jun 2026 19:55:30 +0200 Subject: [PATCH 04/11] MACS2: also extract the object code --- engines/macs2/demacs2.cpp | 276 +++++++++++++++++++++++++++++++++++--- 1 file changed, 260 insertions(+), 16 deletions(-) diff --git a/engines/macs2/demacs2.cpp b/engines/macs2/demacs2.cpp index 2e0b4871..9a9c0d2f 100644 --- a/engines/macs2/demacs2.cpp +++ b/engines/macs2/demacs2.cpp @@ -32,6 +32,63 @@ static uint8_t *scriptData = nullptr; static uint32_t scriptSize = 0; static uint32_t pos = 0; +static uint8_t *stringData = nullptr; +static uint32_t stringDataSize = 0; + +static void appendUtf8(std::string &out, uint8_t ch) { + if (ch < 0x80) { + out += (char)ch; + return; + } + // CP850 to Unicode mapping for 0x80-0xFF + static const uint16_t cp850[128] = { + 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, + 0x00EA,0x00EB,0x00E8,0x00EF,0x00EC,0x00ED,0x00C4,0x00C5, + 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, + 0x00FF,0x00D6,0x00DC,0x00F8,0x00A3,0x00D8,0x00D7,0x0192, + 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, + 0x00BF,0x00AE,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, + 0x2591,0x2592,0x2593,0x2502,0x2524,0x00C1,0x00C2,0x00C0, + 0x00A9,0x2563,0x2551,0x2557,0x255D,0x00A2,0x00A5,0x2510, + 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x00E3,0x00C3, + 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x00A4, + 0x00F0,0x00D0,0x00CA,0x00CB,0x00C8,0x0131,0x00CD,0x00CE, + 0x00CF,0x2518,0x250C,0x2588,0x2584,0x00A6,0x00CC,0x2580, + 0x00D3,0x00DF,0x00D4,0x00D2,0x00F5,0x00D5,0x00B5,0x00FE, + 0x00DE,0x00DA,0x00DB,0x00D9,0x00FD,0x00DD,0x00AF,0x00B4, + 0x00AD,0x00B1,0x2017,0x00BE,0x00B6,0x00A7,0x00F7,0x00B8, + 0x00B0,0x00A8,0x00B7,0x00B9,0x00B3,0x00B2,0x25A0,0x00A0, + }; + uint16_t u = cp850[ch - 0x80]; + if (u < 0x800) { + out += (char)(0xC0 | (u >> 6)); + out += (char)(0x80 | (u & 0x3F)); + } else { + out += (char)(0xE0 | (u >> 12)); + out += (char)(0x80 | ((u >> 6) & 0x3F)); + out += (char)(0x80 | (u & 0x3F)); + } +} + +static std::string decodeString(uint32_t offset, uint16_t numLines) { + if (!stringData || offset >= stringDataSize) + return ""; + uint32_t p = offset; + std::string result; + for (uint16_t line = 0; line < numLines && p + 2 <= stringDataSize; line++) { + uint16_t length = (uint16_t)stringData[p] | ((uint16_t)stringData[p + 1] << 8); + p += 2; + for (int i = 1; i <= length && p < stringDataSize; i++, p++) { + uint8_t x = (uint8_t)(i * i * 0x0C); + uint8_t y = (uint8_t)(stringData[p] ^ i); + appendUtf8(result, x ^ y); + } + if (line + 1 < numLines) + result += " / "; + } + return result; +} + static uint8_t readByte() { if (pos >= scriptSize) return 0; @@ -296,7 +353,7 @@ struct DecompiledLine { static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { std::string result; - char buf[256]; + char buf[1024]; switch (opcode) { case 0x01: case 0x02: { @@ -344,7 +401,11 @@ static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { std::string y = formatValue(); uint16_t strOffset = readWord(); uint16_t numLines = readWord(); - snprintf(buf, sizeof(buf), " pos=(%s,%s) str=%u lines=%u", x.c_str(), y.c_str(), strOffset, numLines); + std::string decoded = decodeString(strOffset, numLines); + if (!decoded.empty()) + snprintf(buf, sizeof(buf), " pos=(%s,%s) str=%u lines=%u \"%s\"", x.c_str(), y.c_str(), strOffset, numLines, decoded.c_str()); + else + snprintf(buf, sizeof(buf), " pos=(%s,%s) str=%u lines=%u", x.c_str(), y.c_str(), strOffset, numLines); result = buf; break; } @@ -372,8 +433,13 @@ static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { std::string side = formatValue(); uint16_t strOffset = readWord(); uint16_t numLines = readWord(); - snprintf(buf, sizeof(buf), " obj=%s pos=(%s,%s) side=%s str=%u lines=%u", - obj.c_str(), x.c_str(), y.c_str(), side.c_str(), strOffset, numLines); + std::string decoded = decodeString(strOffset, numLines); + if (!decoded.empty()) + snprintf(buf, sizeof(buf), " obj=%s pos=(%s,%s) side=%s str=%u lines=%u \"%s\"", + obj.c_str(), x.c_str(), y.c_str(), side.c_str(), strOffset, numLines, decoded.c_str()); + else + snprintf(buf, sizeof(buf), " obj=%s pos=(%s,%s) side=%s str=%u lines=%u", + obj.c_str(), x.c_str(), y.c_str(), side.c_str(), strOffset, numLines); result = buf; break; } @@ -417,7 +483,11 @@ static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { std::string idx = formatValue(); uint16_t strOffset = readWord(); uint16_t numLines = readWord(); - snprintf(buf, sizeof(buf), " idx=%s str=%u lines=%u", idx.c_str(), strOffset, numLines); + std::string decoded = decodeString(strOffset, numLines); + if (!decoded.empty()) + snprintf(buf, sizeof(buf), " idx=%s str=%u lines=%u \"%s\"", idx.c_str(), strOffset, numLines, decoded.c_str()); + else + snprintf(buf, sizeof(buf), " idx=%s str=%u lines=%u", idx.c_str(), strOffset, numLines); result = buf; break; } @@ -745,9 +815,9 @@ static std::string jsonEscape(const std::string &s) { return out; } -static void disassembleJson(int sceneIndex) { +static void disassembleJson(int index, const char *type = "scene") { auto lines = decompileToLines(); - printf(" {\"scene\": %d, \"size\": %u, \"instructions\": [\n", sceneIndex, scriptSize); + printf(" {\"type\": \"%s\", \"index\": %d, \"size\": %u, \"instructions\": [\n", type, index, scriptSize); for (size_t i = 0; i < lines.size(); i++) { const auto &l = lines[i]; std::string text = std::string(getOpcodeName(l.opcode)) + l.params; @@ -761,8 +831,10 @@ static void disassembleJson(int sceneIndex) { static void printHelp(const char *bin) { printf("MACS2 Script Decompiler\n\n"); - printf("Usage: %s [--json] [scene_index]\n\n", bin); + printf("Usage: %s [--json] [--objects] [scene_index]\n\n", bin); printf(" --json - Output as JSON\n"); + printf(" --objects - Also decompile object scripts for the scene(s)\n"); + printf(" Objects with scene=0 (inventory/global) are included\n"); printf(" game_data_file - The main game data file\n"); printf(" scene_index - Scene number to decompile (1-based)\n"); printf(" If omitted, decompiles all scenes\n\n"); @@ -801,6 +873,103 @@ static bool loadSceneScript(FILE *f, uint16_t sceneIndex) { scriptSize = size; pos = 0; + + // Load scene strings + uint32_t strTableOffset = (uint32_t)sceneIndex * 0xC + 0xC + 0x4 - 0x4; + fseek(f, strTableOffset, SEEK_SET); + uint32_t strDataOffset; + if (fread(&strDataOffset, 4, 1, f) == 1 && strDataOffset != 0) { + fseek(f, strDataOffset, SEEK_SET); + uint16_t strSize; + if (fread(&strSize, 2, 1, f) == 1 && strSize > 0) { + stringData = (uint8_t *)malloc(strSize); + if (stringData) { + if (fread(stringData, 1, strSize, f) == strSize) + stringDataSize = strSize; + else { + free(stringData); + stringData = nullptr; + } + } + } + } + + return true; +} + +// Returns the scene index for an object, or 0 if the object doesn't exist +static uint16_t getObjectSceneIndex(FILE *f, uint16_t objectIndex) { + // Object data table: offset at 0x17F4 + 0xC + 0x4 + objectIndex * 0xC + uint32_t addressOffset = 0x17F4 + (0xC + 0x04) + (uint32_t)objectIndex * 0xC; + fseek(f, addressOffset, SEEK_SET); + uint32_t objectOffset; + if (fread(&objectOffset, 4, 1, f) != 1 || objectOffset == 0) + return 0; + // sceneIndex is at +4 in the object data (after x:u16, y:u16) + fseek(f, objectOffset + 4, SEEK_SET); + uint16_t sceneIndex; + if (fread(&sceneIndex, 2, 1, f) != 1) + return 0; + return sceneIndex; +} + +static bool loadObjectScript(FILE *f, uint16_t objectIndex) { + // Object script table: each entry is 12 bytes, script offset is at +4 + uint32_t addressOffset = 0x17F8 + (0xC + 0x04) + (uint32_t)objectIndex * 0xC; + fseek(f, addressOffset, SEEK_SET); + + uint32_t objectOffset; + if (fread(&objectOffset, 4, 1, f) != 1) + return false; + + if (objectOffset == 0) + return false; + + fseek(f, objectOffset, SEEK_SET); + + // Skip 32 resource offsets (128 bytes) + fseek(f, 128, SEEK_CUR); + + uint16_t size; + if (fread(&size, 2, 1, f) != 1) + return false; + + if (size == 0) + return false; + + scriptData = (uint8_t *)malloc(size); + if (!scriptData) + return false; + + if (fread(scriptData, 1, size, f) != size) { + free(scriptData); + scriptData = nullptr; + return false; + } + + scriptSize = size; + pos = 0; + + // Load object strings + uint32_t strTableOffset = (uint32_t)objectIndex * 0xC + 0xC + 0x4 + 0x17FC; + fseek(f, strTableOffset, SEEK_SET); + uint32_t strDataOffset; + if (fread(&strDataOffset, 4, 1, f) == 1 && strDataOffset != 0) { + fseek(f, strDataOffset, SEEK_SET); + uint16_t strSize; + if (fread(&strSize, 2, 1, f) == 1 && strSize > 0) { + stringData = (uint8_t *)malloc(strSize); + if (stringData) { + if (fread(stringData, 1, strSize, f) == strSize) + stringDataSize = strSize; + else { + free(stringData); + stringData = nullptr; + } + } + } + } + return true; } @@ -811,9 +980,17 @@ int main(int argc, char **argv) { } bool jsonMode = false; + bool objectsMode = false; int argIdx = 1; - if (!strcmp(argv[argIdx], "--json")) { - jsonMode = true; + while (argIdx < argc && argv[argIdx][0] == '-') { + if (!strcmp(argv[argIdx], "--json")) { + jsonMode = true; + } else if (!strcmp(argv[argIdx], "--objects")) { + objectsMode = true; + } else { + printHelp(argv[0]); + return 1; + } argIdx++; } @@ -829,15 +1006,12 @@ int main(int argc, char **argv) { } argIdx++; - int startScene = -1; - int endScene = -1; + int startScene = 1; + int endScene = 512; if (argIdx < argc) { startScene = atoi(argv[argIdx]); endScene = startScene; - } else { - startScene = 1; - endScene = 512; } if (jsonMode) { @@ -848,10 +1022,32 @@ int main(int argc, char **argv) { if (!first) printf(",\n"); first = false; - disassembleJson(scene); + disassembleJson(scene, "scene"); free(scriptData); scriptData = nullptr; scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + if (objectsMode) { + for (int obj = 1; obj <= 0x200; obj++) { + uint16_t objScene = getObjectSceneIndex(f, (uint16_t)obj); + if (objScene != (uint16_t)scene && objScene != 0) + continue; + if (loadObjectScript(f, (uint16_t)obj)) { + if (!first) + printf(",\n"); + first = false; + disassembleJson(obj, objScene == 0 ? "inventory" : "object"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + } } } printf("\n]}\n"); @@ -874,6 +1070,54 @@ int main(int argc, char **argv) { free(scriptData); scriptData = nullptr; scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + if (objectsMode) { + bool hasSceneObjs = false; + bool hasInventoryObjs = false; + // Scene objects first + for (int obj = 1; obj <= 0x200; obj++) { + uint16_t objScene = getObjectSceneIndex(f, (uint16_t)obj); + if (objScene != (uint16_t)scene) + continue; + if (!hasSceneObjs) { + printf(" [Scene Objects]\n"); + hasSceneObjs = true; + } + if (loadObjectScript(f, (uint16_t)obj)) { + printf("--- Object 0x%x (size: %u bytes) ---\n", obj, scriptSize); + disassemble(); + printf("\n"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + } + // Inventory/global objects (scene == 0) + for (int obj = 1; obj <= 0x200; obj++) { + if (getObjectSceneIndex(f, (uint16_t)obj) != 0) + continue; + if (!hasInventoryObjs) { + printf(" [Inventory/Global Objects]\n"); + hasInventoryObjs = true; + } + if (loadObjectScript(f, (uint16_t)obj)) { + printf("--- Object 0x%x [inventory] (size: %u bytes) ---\n", obj, scriptSize); + disassemble(); + printf("\n"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + } } } } From 5eef83d441a2ff56721ba85cc716ac455097f382 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Wed, 10 Jun 2026 21:54:08 +0200 Subject: [PATCH 05/11] MACS2: extract item images --- engines/macs2/extract_macs2.cpp | 138 ++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/engines/macs2/extract_macs2.cpp b/engines/macs2/extract_macs2.cpp index 9a9b4a1b..1967ba1c 100644 --- a/engines/macs2/extract_macs2.cpp +++ b/engines/macs2/extract_macs2.cpp @@ -505,6 +505,138 @@ static void extractSceneData(uint16_t sceneIndex, const char *outDir) { printf(" Wrote %s + 4 map files\n", path); } +// Extract inventory icon images for all objects that have one (blob slot 0x13) +static void extractItems(const char *outDir) { + // Get scene 1 palette as fallback for icon rendering + uint8_t palette[768] = {}; + uint32_t bgOffset = getSceneBgOffset(1); + if (bgOffset != 0) { + fseek(resFile, bgOffset, SEEK_SET); + // Skip background RLE image to reach palette + for (int y = 0; y < 200; y++) { + uint16_t length = readU16(resFile); + fseek(resFile, length, SEEK_CUR); + } + fread(palette, 1, 768, resFile); + } + + printf("Extracting inventory items...\n"); + int count = 0; + for (int i = 1; i <= 0x200; i++) { + uint32_t addressOffset = 0x17F4 + 0xC + 0x04 + i * 0xC; + fseek(resFile, addressOffset, SEEK_SET); + uint32_t objectOffset = readU32(resFile); + if (objectOffset == 0) + continue; + + fseek(resFile, objectOffset, SEEK_SET); + // Skip: x(2), y(2), sceneIndex(2), orientation(2), verticalOffsetScale(2) + fseek(resFile, 10, SEEK_CUR); + + // Read through 21 blob slots to reach slot 0x13 (index 19, 0-based) + std::vector iconBlob; + for (int j = 0; j < 0x15; j++) { + fseek(resFile, 2, SEEK_CUR); // animID + fseek(resFile, 2, SEEK_CUR); // sourceKey + uint32_t dataSize = readU32(resFile); + if (j == 0x13) { + if (dataSize > 0) { + iconBlob.resize(dataSize); + fread(iconBlob.data(), 1, dataSize, resFile); + } + } else { + fseek(resFile, dataSize, SEEK_CUR); + } + fseek(resFile, 4, SEEK_CUR); // speed(2) + mirrorFlag(1) + padding(1) + } + + if (iconBlob.empty()) + continue; + + // Parse the animation blob to get frame 1 data + // Blob header: 12 bytes (6 words), then sequence table + // Sequence length stored as (len-1) at offset 10 + if (iconBlob.size() < 12) + continue; + uint16_t seqLen = (uint16_t)(iconBlob[10] | (iconBlob[11] << 8)) + 1; + uint32_t frameTableOffset = 0xB + seqLen; // matches engine: stream.seek(0xB + bp0E) + if (frameTableOffset + 2 > iconBlob.size()) + continue; + uint16_t frameCount = iconBlob[frameTableOffset] | (iconBlob[frameTableOffset + 1] << 8); + if (frameCount == 0) + continue; + uint32_t frameDataOffset = frameTableOffset + 2; + // Frame data: offsetX(2), offsetY(2), unknown(2), width(2), height(2), pixels(w*h) + if (frameDataOffset + 10 > iconBlob.size()) + continue; + // Skip frame header (6 bytes: offsetX, offsetY, unknown) to get width/height/pixels + uint32_t p = frameDataOffset + 6; + if (p + 4 > iconBlob.size()) + continue; + uint16_t w = iconBlob[p] | (iconBlob[p + 1] << 8); + uint16_t h = iconBlob[p + 2] | (iconBlob[p + 3] << 8); + p += 4; + if (w == 0 || h == 0 || p + (uint32_t)w * h > iconBlob.size()) + continue; + + // Write BMP + char path[512]; + snprintf(path, sizeof(path), "%s/item_%03d.bmp", outDir, i); + FILE *f = fopen(path, "wb"); + if (!f) + continue; + + uint32_t rowStride = (w + 3) & ~3; + uint32_t imageSize = rowStride * h; + uint32_t paletteSize = 256 * 4; + uint32_t dataOff = 14 + 40 + paletteSize; + uint32_t fileSize = dataOff + imageSize; + + fputc('B', f); + fputc('M', f); + uint8_t hdr[12] = {}; + hdr[0] = fileSize & 0xFF; + hdr[1] = (fileSize >> 8) & 0xFF; + hdr[2] = (fileSize >> 16) & 0xFF; + hdr[3] = (fileSize >> 24) & 0xFF; + hdr[8] = dataOff & 0xFF; + hdr[9] = (dataOff >> 8) & 0xFF; + hdr[10] = (dataOff >> 16) & 0xFF; + hdr[11] = (dataOff >> 24) & 0xFF; + fwrite(hdr, 1, 12, f); + + uint8_t dib[40] = {}; + dib[0] = 40; + dib[4] = w & 0xFF; + dib[5] = (w >> 8) & 0xFF; + dib[8] = h & 0xFF; + dib[9] = (h >> 8) & 0xFF; + dib[12] = 1; + dib[14] = 8; + fwrite(dib, 1, 40, f); + + for (int c = 0; c < 256; c++) { + uint8_t r = (palette[c * 3 + 0] * 259 + 33) >> 6; + uint8_t g = (palette[c * 3 + 1] * 259 + 33) >> 6; + uint8_t b = (palette[c * 3 + 2] * 259 + 33) >> 6; + uint8_t bgra[4] = {b, g, r, 0}; + fwrite(bgra, 1, 4, f); + } + + // BMP rows must be 4-byte aligned + uint8_t pad[4] = {0}; + for (int y = h - 1; y >= 0; y--) { + fwrite(iconBlob.data() + p + y * w, 1, w, f); + if (rowStride > w) + fwrite(pad, 1, rowStride - w, f); + } + fclose(f); + printf(" item %d: %ux%u -> %s\n", i, w, h, path); + count++; + } + printf("Extracted %d inventory item icons.\n", count); +} + static void printHelp(const char *bin) { printf("MACS2 Resource Extractor\n\n"); printf("Usage: %s [scene_index]\n\n", bin); @@ -513,6 +645,7 @@ static void printHelp(const char *bin) { printf(" sounds - Extract sound/music resource blobs\n"); printf(" strings - Extract and decrypt text strings\n"); printf(" scenedata - Extract scene metadata as JSON (pathfinding, hotspots, walk params)\n"); + printf(" items - Extract inventory item icons as BMP (with object ID)\n"); printf(" all - Extract everything\n"); printf("\n"); printf("If scene_index is omitted, extracts from all scenes.\n"); @@ -545,6 +678,11 @@ int main(int argc, char **argv) { bool doSounds = !strcmp(mode, "sounds") || !strcmp(mode, "all"); bool doStrings = !strcmp(mode, "strings") || !strcmp(mode, "all"); bool doSceneData = !strcmp(mode, "scenedata") || !strcmp(mode, "all"); + bool doItems = !strcmp(mode, "items") || !strcmp(mode, "all"); + + if (doItems) { + extractItems(outDir); + } for (int scene = startScene; scene <= endScene; scene++) { bool hasData = false; From 70167ff71806b7d133f31d7f0878207371b103b8 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Thu, 11 Jun 2026 06:55:49 +0200 Subject: [PATCH 06/11] MACS2: add makefile targets --- Makefile | 6 ++++++ Makefile.common | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/Makefile b/Makefile index 047c4042..2b4428e2 100644 --- a/Makefile +++ b/Makefile @@ -109,6 +109,9 @@ endif $(STRIP) descumm$(EXEEXT) -o $(WIN32PATH)/tools/descumm$(EXEEXT) $(STRIP) desword2$(EXEEXT) -o $(WIN32PATH)/tools/desword2$(EXEEXT) $(STRIP) detwine$(EXEEXT) -o $(WIN32PATH)/tools/detwine$(EXEEXT) + $(STRIP) demacs2$(EXEEXT) -o $(WIN32PATH)/tools/demacs2$(EXEEXT) + $(STRIP) extract_macs2$(EXEEXT) -o $(WIN32PATH)/tools/extract_macs2$(EXEEXT) + $(STRIP) create_macs2_translation$(EXEEXT) -o $(WIN32PATH)/tools/create_macs2_translation$(EXEEXT) $(STRIP) extract_mohawk$(EXEEXT) -o $(WIN32PATH)/tools/extract_mohawk$(EXEEXT) $(STRIP) gob_loadcalc$(EXEEXT) -o $(WIN32PATH)/tools/gob_loadcalc$(EXEEXT) $(STRIP) grim_animb2txt$(EXEEXT) -o $(WIN32PATH)/tools/grim_animb2txt$(EXEEXT) @@ -308,6 +311,9 @@ endif $(STRIP) descumm$(EXEEXT) -o $(AMIGAOSPATH)/descumm$(EXEEXT) $(STRIP) desword2$(EXEEXT) -o $(AMIGAOSPATH)/desword2$(EXEEXT) $(STRIP) detwine$(EXEEXT) -o $(AMIGAOSPATH)/detwine$(EXEEXT) + $(STRIP) demacs2$(EXEEXT) -o $(AMIGAOSPATH)/demacs2$(EXEEXT) + $(STRIP) extract_macs2$(EXEEXT) -o $(AMIGAOSPATH)/extract_macs2$(EXEEXT) + $(STRIP) create_macs2_translation$(EXEEXT) -o $(AMIGAOSPATH)/create_macs2_translation$(EXEEXT) $(STRIP) extract_mohawk$(EXEEXT) -o $(AMIGAOSPATH)/extract_mohawk$(EXEEXT) $(STRIP) extract_ngi$(EXEEXT) -o $(AMIGAOSPATH)/extract_ngi$(EXEEXT) $(STRIP) gob_loadcalc$(EXEEXT) -o $(AMIGAOSPATH)/gob_loadcalc$(EXEEXT) diff --git a/Makefile.common b/Makefile.common index b3bfc179..9fa390f1 100644 --- a/Makefile.common +++ b/Makefile.common @@ -35,6 +35,7 @@ PROGRAMS = \ extract_gob_cdi \ extract_mohawk \ extract_macs2 \ + create_macs2_translation \ extract_ngi \ construct_mohawk \ msn_convert_mod \ @@ -145,6 +146,9 @@ demacs2_OBJS := \ extract_macs2_OBJS := \ engines/macs2/extract_macs2.o +create_macs2_translation_OBJS := \ + engines/macs2/create_macs2_translation.o + extract_hadesch_OBJS := \ engines/hadesch/extract_hadesch.o \ $(UTILS) From 4aa202a088fbf0a8aef048e6bff7f437a8cb33f8 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Thu, 11 Jun 2026 06:56:05 +0200 Subject: [PATCH 07/11] MACS2: also extract object strings --- engines/macs2/extract_macs2.cpp | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/engines/macs2/extract_macs2.cpp b/engines/macs2/extract_macs2.cpp index 1967ba1c..bce8f023 100644 --- a/engines/macs2/extract_macs2.cpp +++ b/engines/macs2/extract_macs2.cpp @@ -274,6 +274,48 @@ static void extractStrings(uint16_t sceneIndex, const char *outDir) { printf(" Wrote %s (%d strings)\n", path, stringIndex); } +// Extract strings for an object +static void extractObjectStrings(uint16_t objectIndex, const char *outDir) { + uint32_t strTableOffset = (uint32_t)objectIndex * 0xC + 0xC + 0x4 + 0x17FC; + fseek(resFile, strTableOffset, SEEK_SET); + uint32_t strDataOffset = readU32(resFile); + if (strDataOffset == 0) + return; + + fseek(resFile, strDataOffset, SEEK_SET); + uint16_t totalSize = readU16(resFile); + if (totalSize == 0) + return; + + std::vector strData(totalSize); + fread(strData.data(), 1, totalSize, resFile); + + char path[512]; + snprintf(path, sizeof(path), "%s/strings_object%03d.txt", outDir, objectIndex); + FILE *out = fopen(path, "w"); + if (!out) + return; + + fprintf(out, "; Object %d strings\n", objectIndex); + fprintf(out, "; Total data size: %u bytes\n\n", totalSize); + + uint32_t pos = 0; + int stringIndex = 0; + while (pos + 2 <= totalSize) { + uint16_t len = strData[pos] | (strData[pos + 1] << 8); + pos += 2; + if (len == 0 || pos + len > totalSize) + break; + std::string decoded = decryptString(strData.data() + pos, len); + fprintf(out, "[%d] (offset=%u) %s\n", stringIndex, (unsigned)(pos - 2), decoded.c_str()); + pos += len; + stringIndex++; + } + + fclose(out); + printf(" Wrote %s (%d strings)\n", path, stringIndex); +} + static void mkdirp(const char *path) { #ifdef _WIN32 mkdir(path); @@ -732,6 +774,12 @@ int main(int argc, char **argv) { } } + if (doStrings) { + for (int obj = 1; obj <= 0x200; obj++) { + extractObjectStrings(obj, outDir); + } + } + fclose(resFile); return 0; } From 6dc436d2a561bb8bdeee4afc2f7475cdf6c0800b Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Thu, 11 Jun 2026 07:10:32 +0200 Subject: [PATCH 08/11] MACS2: extract more resources from game files --- engines/macs2/extract_macs2.cpp | 342 +++++++++++++++++++++++++++++++- 1 file changed, 336 insertions(+), 6 deletions(-) diff --git a/engines/macs2/extract_macs2.cpp b/engines/macs2/extract_macs2.cpp index bce8f023..363f1c37 100644 --- a/engines/macs2/extract_macs2.cpp +++ b/engines/macs2/extract_macs2.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #ifdef WIN32 @@ -96,6 +97,61 @@ static bool decodeRLEImage(uint32_t offset, uint8_t *pixels) { return true; } +// Write a BMP file (8-bit indexed, arbitrary dimensions) +static void writeBMPEx(const char *path, const uint8_t *pixels, uint16_t w, uint16_t h, const uint8_t *palette) { + FILE *f = fopen(path, "wb"); + if (!f) { + fprintf(stderr, "Cannot write %s\n", path); + return; + } + + uint32_t rowStride = (w + 3) & ~3; + uint32_t imageSize = rowStride * h; + uint32_t paletteSize = 256 * 4; + uint32_t dataOffset = 14 + 40 + paletteSize; + uint32_t fileSize = dataOffset + imageSize; + + fputc('B', f); + fputc('M', f); + uint8_t hdr[12] = {}; + hdr[0] = fileSize & 0xFF; + hdr[1] = (fileSize >> 8) & 0xFF; + hdr[2] = (fileSize >> 16) & 0xFF; + hdr[3] = (fileSize >> 24) & 0xFF; + hdr[8] = dataOffset & 0xFF; + hdr[9] = (dataOffset >> 8) & 0xFF; + hdr[10] = (dataOffset >> 16) & 0xFF; + hdr[11] = (dataOffset >> 24) & 0xFF; + fwrite(hdr, 1, 12, f); + + uint8_t dib[40] = {}; + dib[0] = 40; + dib[4] = w & 0xFF; + dib[5] = (w >> 8) & 0xFF; + dib[8] = h & 0xFF; + dib[9] = (h >> 8) & 0xFF; + dib[12] = 1; + dib[14] = 8; + fwrite(dib, 1, 40, f); + + for (int i = 0; i < 256; i++) { + uint8_t r = (palette[i * 3 + 0] * 259 + 33) >> 6; + uint8_t g = (palette[i * 3 + 1] * 259 + 33) >> 6; + uint8_t b = (palette[i * 3 + 2] * 259 + 33) >> 6; + uint8_t bgra[4] = {b, g, r, 0}; + fwrite(bgra, 1, 4, f); + } + + uint8_t pad[4] = {0}; + for (int y = h - 1; y >= 0; y--) { + fwrite(pixels + y * w, 1, w, f); + if (rowStride > w) + fwrite(pad, 1, rowStride - w, f); + } + fclose(f); + printf(" Wrote %s\n", path); +} + // Write a BMP file (8-bit indexed) static void writeBMP(const char *path, const uint8_t *pixels, const uint8_t *palette) { FILE *f = fopen(path, "wb"); @@ -155,6 +211,158 @@ static void writeBMP(const char *path, const uint8_t *pixels, const uint8_t *pal // Forward declaration static bool decodeRLEMap(FILE *f, uint8_t *pixels); +// Extract cursor/icon images (33 entries at file offset 0x3B10) +static void extractCursors(const char *outDir) { + // Read global palette at 0x3010 + uint8_t palette[768]; + fseek(resFile, 0xC + 0x4 + 0x3000, SEEK_SET); + fread(palette, 1, 768, resFile); + + fseek(resFile, 0xC + 0x4 + 0x3000 + 0x300 + 0x800, SEEK_SET); + printf("Extracting cursors...\n"); + int count = 0; + for (int i = 0; i < 0x21; i++) { + uint32_t length = readU32(resFile); + if (length == 0) + continue; + long blobStart = ftell(resFile); + fseek(resFile, 2, SEEK_CUR); // skip 2 bytes + uint16_t w = readU16(resFile); + uint16_t h = readU16(resFile); + if (w == 0 || h == 0 || (uint32_t)w * h > length) { + fseek(resFile, blobStart + length, SEEK_SET); + continue; + } + std::vector pixels(w * h); + fread(pixels.data(), 1, w * h, resFile); + + char path[512]; + snprintf(path, sizeof(path), "%s/cursor_%02d.bmp", outDir, i); + writeBMPEx(path, pixels.data(), w, h, palette); + count++; + fseek(resFile, blobStart + length, SEEK_SET); + } + printf("Extracted %d cursor images.\n", count); +} + +// Render a font's glyphs into an atlas BMP +static void renderFontAtlas(const char *path, FILE *f, uint16_t glyphCount, const uint8_t *palette) { + struct Glyph { + uint16_t w, h; + std::vector data; + }; + std::vector glyphs; + uint16_t maxH = 0; + uint32_t totalW = 0; + for (int i = 0; i < glyphCount; i++) { + Glyph g; + fseek(f, 1, SEEK_CUR); // ascii byte + g.w = readU16(f); + g.h = readU16(f); + if (g.w > 320 || g.h > 200) + return; + g.data.resize((uint32_t)g.w * g.h); + fread(g.data.data(), 1, g.data.size(), f); + if (g.h > maxH) + maxH = g.h; + totalW += g.w + 1; + glyphs.push_back(g); + } + if (glyphs.empty() || totalW == 0) + return; + + uint16_t atlasW = (uint16_t)(totalW - 1); + std::vector atlas((uint32_t)atlasW * maxH, 0); + uint32_t x = 0; + for (const auto &g : glyphs) { + for (uint16_t gy = 0; gy < g.h; gy++) + for (uint16_t gx = 0; gx < g.w; gx++) + atlas[gy * atlasW + x + gx] = g.data[gy * g.w + gx]; + x += g.w + 1; + } + writeBMPEx(path, atlas.data(), atlasW, maxH, palette); +} + +// Extract all fonts: global fonts (2) + overlay fonts from scene/object resources +static void extractFonts(const char *outDir) { + // Read global palette + uint8_t palette[768]; + fseek(resFile, 0xC + 0x4 + 0x3000, SEEK_SET); + fread(palette, 1, 768, resFile); + + // Seek past cursor images + fseek(resFile, 0xC + 0x4 + 0x3000 + 0x300 + 0x800, SEEK_SET); + for (int i = 0; i < 0x21; i++) { + uint32_t length = readU32(resFile); + if (length > 0) + fseek(resFile, length, SEEK_CUR); + } + + printf("Extracting fonts...\n"); + int fontIdx = 0; + + // Global Font 1 (dialog font) + readU32(resFile); // size field + uint16_t glyphCount = readU16(resFile); + if (glyphCount > 0 && glyphCount <= 256) { + char path[512]; + snprintf(path, sizeof(path), "%s/font_%d_dialog.bmp", outDir, fontIdx); + renderFontAtlas(path, resFile, glyphCount, palette); + fontIdx++; + } + + // Global Font 2 (save/load panel font) + readU32(resFile); + glyphCount = readU16(resFile); + if (glyphCount > 0 && glyphCount <= 256) { + char path[512]; + snprintf(path, sizeof(path), "%s/font_%d_panel.bmp", outDir, fontIdx); + renderFontAtlas(path, resFile, glyphCount, palette); + fontIdx++; + } + + // Overlay fonts from scene resources (loaded by opcode 0x38) + // These are at resourceOffset + 0x10: uint16 glyphCount + glyph data + std::set extractedOffsets; + for (int scene = 1; scene <= 512; scene++) { + uint32_t dataOffset = getSceneDataOffset(scene); + if (dataOffset == 0) + continue; + fseek(resFile, dataOffset, SEEK_SET); + uint32_t resourceTable[32]; + for (int i = 0; i < 32; i++) + resourceTable[i] = readU32(resFile); + + for (int i = 0; i < 32; i++) { + if (resourceTable[i] == 0) + continue; + if (extractedOffsets.count(resourceTable[i])) + continue; + + // Check if resource has AHFFFONT magic header (at offset+4, after size field) + fseek(resFile, resourceTable[i] + 4, SEEK_SET); + uint8_t magic[8]; + fread(magic, 1, 8, resFile); + if (memcmp(magic, "AHFF", 4) != 0 || memcmp(magic + 4, "FONT", 4) != 0) + continue; + + // Font data at offset+0x10: uint16 glyphCount + glyph data + fseek(resFile, resourceTable[i] + 0x10, SEEK_SET); + uint16_t gc = readU16(resFile); + if (gc == 0 || gc > 256) + continue; + + // Extract font + extractedOffsets.insert(resourceTable[i]); + char path[512]; + snprintf(path, sizeof(path), "%s/font_%d_scene%03d_res%02d.bmp", outDir, fontIdx, scene, i + 1); + renderFontAtlas(path, resFile, gc, palette); + fontIdx++; + } + } + printf("Extracted %d font atlas images.\n", fontIdx); +} + // Extract background image for a scene static void extractImage(uint16_t sceneIndex, const char *outDir) { uint32_t bgOffset = getSceneBgOffset(sceneIndex); @@ -547,6 +755,111 @@ static void extractSceneData(uint16_t sceneIndex, const char *outDir) { printf(" Wrote %s + 4 map files\n", path); } +// Read the map scene offsets table (256 uint32 entries) by walking the file layout +static bool readMapSceneOffsets(uint32_t offsets[256]) { + // File layout: header(0xC) + actorIdx/sceneIdx(4) + sceneTable(0x3000) + palette(0x300) + shading(0x800) + fseek(resFile, 0xC + 0x4 + 0x3000 + 0x300 + 0x800, SEEK_SET); + + // Skip 33 image resources (each: uint32 length, then if >0: 2+w(2)+h(2)+w*h bytes) + for (int i = 0; i < 0x21; i++) { + uint32_t length = readU32(resFile); + if (length > 0) + fseek(resFile, length, SEEK_CUR); + } + + // Skip Font 1: uint32 sizeField + uint16 glyphCount + glyphs (each: 1+2+2+w*h) + readU32(resFile); // size field + uint16_t glyphCount = readU16(resFile); + for (int i = 0; i < glyphCount; i++) { + fseek(resFile, 1, SEEK_CUR); // ascii + uint16_t w = readU16(resFile); + uint16_t h = readU16(resFile); + fseek(resFile, (uint32_t)w * h, SEEK_CUR); + } + + // Skip Font 2: same structure + readU32(resFile); + glyphCount = readU16(resFile); + for (int i = 0; i < glyphCount; i++) { + fseek(resFile, 1, SEEK_CUR); + uint16_t w = readU16(resFile); + uint16_t h = readU16(resFile); + fseek(resFile, (uint32_t)w * h, SEEK_CUR); + } + + // Now at map scene offsets: 256 x uint32 + for (int i = 0; i < 256; i++) + offsets[i] = readU32(resFile); + return true; +} + +// Extract help/map panel images +static void extractHelpImages(const char *outDir) { + uint32_t offsets[256]; + if (!readMapSceneOffsets(offsets)) + return; + + printf("Extracting help/map panel images...\n"); + int count = 0; + for (int i = 0; i < 256; i++) { + if (offsets[i] == 0) + continue; + + uint8_t pixels[320 * 200]; + if (!decodeRLEImage(offsets[i], pixels)) + continue; + + // Palette follows immediately after the image + uint8_t palette[768]; + fread(palette, 1, 768, resFile); + + char path[512]; + snprintf(path, sizeof(path), "%s/help_%03d.bmp", outDir, i); + writeBMP(path, pixels, palette); + count++; + } + printf("Extracted %d help/map panel images.\n", count); +} + +// Extract music data from scene resource tables +static void extractMusic(const char *outDir) { + printf("Extracting music...\n"); + int count = 0; + for (int scene = 1; scene <= 512; scene++) { + uint32_t dataOffset = getSceneDataOffset(scene); + if (dataOffset == 0) + continue; + + fseek(resFile, dataOffset, SEEK_SET); + uint32_t resourceTable[32]; + for (int i = 0; i < 32; i++) + resourceTable[i] = readU32(resFile); + + for (int i = 0; i < 32; i++) { + if (resourceTable[i] == 0) + continue; + fseek(resFile, resourceTable[i], SEEK_SET); + uint32_t size = readU32(resFile); + if (size == 0 || size > 0x100000) + continue; + + std::vector data(size); + fread(data.data(), 1, size, resFile); + + char path[512]; + snprintf(path, sizeof(path), "%s/music_scene%03d_%02d.bin", outDir, scene, i + 1); + FILE *out = fopen(path, "wb"); + if (out) { + fwrite(data.data(), 1, size, out); + fclose(out); + printf(" Wrote %s (%u bytes)\n", path, size); + count++; + } + } + } + printf("Extracted %d music/sound resources.\n", count); +} + // Extract inventory icon images for all objects that have one (blob slot 0x13) static void extractItems(const char *outDir) { // Get scene 1 palette as fallback for icon rendering @@ -683,12 +996,14 @@ static void printHelp(const char *bin) { printf("MACS2 Resource Extractor\n\n"); printf("Usage: %s [scene_index]\n\n", bin); printf("Modes:\n"); - printf(" images - Extract background images as BMP files\n"); - printf(" sounds - Extract sound/music resource blobs\n"); - printf(" strings - Extract and decrypt text strings\n"); - printf(" scenedata - Extract scene metadata as JSON (pathfinding, hotspots, walk params)\n"); - printf(" items - Extract inventory item icons as BMP (with object ID)\n"); - printf(" all - Extract everything\n"); + printf(" images - Extract background images as BMP files\n"); + printf(" sounds - Extract sound/music resource blobs\n"); + printf(" music - Extract music resources from scene data\n"); + printf(" strings - Extract and decrypt text strings\n"); + printf(" scenedata - Extract scene metadata as JSON (pathfinding, hotspots, walk params)\n"); + printf(" items - Extract inventory item icons as BMP (with object ID)\n"); + printf(" helpimages - Extract help/map panel images as BMP\n"); + printf(" all - Extract everything\n"); printf("\n"); printf("If scene_index is omitted, extracts from all scenes.\n"); } @@ -718,14 +1033,29 @@ int main(int argc, char **argv) { bool doImages = !strcmp(mode, "images") || !strcmp(mode, "all"); bool doSounds = !strcmp(mode, "sounds") || !strcmp(mode, "all"); + bool doMusic = !strcmp(mode, "music") || !strcmp(mode, "all"); bool doStrings = !strcmp(mode, "strings") || !strcmp(mode, "all"); bool doSceneData = !strcmp(mode, "scenedata") || !strcmp(mode, "all"); bool doItems = !strcmp(mode, "items") || !strcmp(mode, "all"); + bool doHelpImages = !strcmp(mode, "helpimages") || !strcmp(mode, "all"); if (doItems) { extractItems(outDir); } + if (doHelpImages) { + extractHelpImages(outDir); + } + + if (doMusic) { + extractMusic(outDir); + } + + if (doImages) { + extractCursors(outDir); + extractFonts(outDir); + } + for (int scene = startScene; scene <= endScene; scene++) { bool hasData = false; From b4079730bedfe4336ed3dd8fab9a465adcbab2e8 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Tue, 9 Jun 2026 19:55:30 +0200 Subject: [PATCH 09/11] MACS2: also extract the object code --- engines/macs2/demacs2.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/engines/macs2/demacs2.cpp b/engines/macs2/demacs2.cpp index 9a9c0d2f..1a7db6bd 100644 --- a/engines/macs2/demacs2.cpp +++ b/engines/macs2/demacs2.cpp @@ -140,6 +140,11 @@ static std::string formatValue() { uint8_t type = readByte(); uint16_t val = readWord(); if (type == 0x00) { + if (val > 0x400 && val <= 0x600) { + char buf[32]; + snprintf(buf, sizeof(buf), "obj_0x%x", val - 0x400); + return buf; + } char buf[32]; snprintf(buf, sizeof(buf), "%u", val); return buf; From 8f88a1223c9ff6f082b6dd3e473d50883ad5ed76 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Thu, 11 Jun 2026 19:46:48 +0200 Subject: [PATCH 10/11] MACS2: also allow to hand over the game dir --- engines/macs2/extract_macs2.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/engines/macs2/extract_macs2.cpp b/engines/macs2/extract_macs2.cpp index 363f1c37..48a96fd0 100644 --- a/engines/macs2/extract_macs2.cpp +++ b/engines/macs2/extract_macs2.cpp @@ -1018,11 +1018,27 @@ int main(int argc, char **argv) { const char *resPath = argv[2]; const char *outDir = argv[3]; - resFile = fopen(resPath, "rb"); + // If path is a directory, look for RESOURCE.MCS inside it + std::string resolvedPath = resPath; + struct stat st; + if (stat(resPath, &st) == 0 && S_ISDIR(st.st_mode)) { + resolvedPath = std::string(resPath) + "/RESOURCE.MCS"; + } + + resFile = fopen(resolvedPath.c_str(), "rb"); if (!resFile) { - fprintf(stderr, "Error: Cannot open '%s'\n", resPath); + fprintf(stderr, "Error: Cannot open '%s'\n", resolvedPath.c_str()); + return 1; + } + + // Validate the file header + uint8_t magic[12]; + if (fread(magic, 1, 12, resFile) != 12 || memcmp(magic, "AHFFMACS0100", 12) != 0) { + fprintf(stderr, "Error: '%s' is not a valid MACS2 resource file\n", resolvedPath.c_str()); + fclose(resFile); return 1; } + fseek(resFile, 0, SEEK_SET); mkdirp(outDir); From f6cdf71eb5e9511c127a05c314c4d3e5d6908c63 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Mon, 22 Jun 2026 18:27:48 +0200 Subject: [PATCH 11/11] MACS2: extract amiga and allow to generate c code --- engines/macs2/demacs2.cpp | 1410 ++++++++++++++++++++++++++++++- engines/macs2/extract_macs2.cpp | 393 ++++++++- 2 files changed, 1779 insertions(+), 24 deletions(-) diff --git a/engines/macs2/demacs2.cpp b/engines/macs2/demacs2.cpp index 1a7db6bd..66b7d21b 100644 --- a/engines/macs2/demacs2.cpp +++ b/engines/macs2/demacs2.cpp @@ -35,6 +35,8 @@ static uint32_t pos = 0; static uint8_t *stringData = nullptr; static uint32_t stringDataSize = 0; +static bool amigaMode = false; + static void appendUtf8(std::string &out, uint8_t ch) { if (ch < 0x80) { out += (char)ch; @@ -42,22 +44,134 @@ static void appendUtf8(std::string &out, uint8_t ch) { } // CP850 to Unicode mapping for 0x80-0xFF static const uint16_t cp850[128] = { - 0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7, - 0x00EA,0x00EB,0x00E8,0x00EF,0x00EC,0x00ED,0x00C4,0x00C5, - 0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9, - 0x00FF,0x00D6,0x00DC,0x00F8,0x00A3,0x00D8,0x00D7,0x0192, - 0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA, - 0x00BF,0x00AE,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB, - 0x2591,0x2592,0x2593,0x2502,0x2524,0x00C1,0x00C2,0x00C0, - 0x00A9,0x2563,0x2551,0x2557,0x255D,0x00A2,0x00A5,0x2510, - 0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x00E3,0x00C3, - 0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x00A4, - 0x00F0,0x00D0,0x00CA,0x00CB,0x00C8,0x0131,0x00CD,0x00CE, - 0x00CF,0x2518,0x250C,0x2588,0x2584,0x00A6,0x00CC,0x2580, - 0x00D3,0x00DF,0x00D4,0x00D2,0x00F5,0x00D5,0x00B5,0x00FE, - 0x00DE,0x00DA,0x00DB,0x00D9,0x00FD,0x00DD,0x00AF,0x00B4, - 0x00AD,0x00B1,0x2017,0x00BE,0x00B6,0x00A7,0x00F7,0x00B8, - 0x00B0,0x00A8,0x00B7,0x00B9,0x00B3,0x00B2,0x25A0,0x00A0, + 0x00C7, + 0x00FC, + 0x00E9, + 0x00E2, + 0x00E4, + 0x00E0, + 0x00E5, + 0x00E7, + 0x00EA, + 0x00EB, + 0x00E8, + 0x00EF, + 0x00EC, + 0x00ED, + 0x00C4, + 0x00C5, + 0x00C9, + 0x00E6, + 0x00C6, + 0x00F4, + 0x00F6, + 0x00F2, + 0x00FB, + 0x00F9, + 0x00FF, + 0x00D6, + 0x00DC, + 0x00F8, + 0x00A3, + 0x00D8, + 0x00D7, + 0x0192, + 0x00E1, + 0x00ED, + 0x00F3, + 0x00FA, + 0x00F1, + 0x00D1, + 0x00AA, + 0x00BA, + 0x00BF, + 0x00AE, + 0x00AC, + 0x00BD, + 0x00BC, + 0x00A1, + 0x00AB, + 0x00BB, + 0x2591, + 0x2592, + 0x2593, + 0x2502, + 0x2524, + 0x00C1, + 0x00C2, + 0x00C0, + 0x00A9, + 0x2563, + 0x2551, + 0x2557, + 0x255D, + 0x00A2, + 0x00A5, + 0x2510, + 0x2514, + 0x2534, + 0x252C, + 0x251C, + 0x2500, + 0x253C, + 0x00E3, + 0x00C3, + 0x255A, + 0x2554, + 0x2569, + 0x2566, + 0x2560, + 0x2550, + 0x256C, + 0x00A4, + 0x00F0, + 0x00D0, + 0x00CA, + 0x00CB, + 0x00C8, + 0x0131, + 0x00CD, + 0x00CE, + 0x00CF, + 0x2518, + 0x250C, + 0x2588, + 0x2584, + 0x00A6, + 0x00CC, + 0x2580, + 0x00D3, + 0x00DF, + 0x00D4, + 0x00D2, + 0x00F5, + 0x00D5, + 0x00B5, + 0x00FE, + 0x00DE, + 0x00DA, + 0x00DB, + 0x00D9, + 0x00FD, + 0x00DD, + 0x00AF, + 0x00B4, + 0x00AD, + 0x00B1, + 0x2017, + 0x00BE, + 0x00B6, + 0x00A7, + 0x00F7, + 0x00B8, + 0x00B0, + 0x00A8, + 0x00B7, + 0x00B9, + 0x00B3, + 0x00B2, + 0x25A0, + 0x00A0, }; uint16_t u = cp850[ch - 0x80]; if (u < 0x800) { @@ -70,7 +184,11 @@ static void appendUtf8(std::string &out, uint8_t ch) { } } +static std::string decodeStringAmiga(uint32_t offset, uint16_t numLines); + static std::string decodeString(uint32_t offset, uint16_t numLines) { + if (amigaMode) + return decodeStringAmiga(offset, numLines); if (!stringData || offset >= stringDataSize) return ""; uint32_t p = offset; @@ -353,6 +471,7 @@ struct DecompiledLine { uint8_t opcode; int indent; std::string params; + std::vector args; }; static std::string decodeParams(uint8_t opcode, uint32_t endPos, int &indent) { @@ -778,7 +897,10 @@ static std::vector decompileToLines() { line.offset = instrAddr; line.opcode = opcode; line.indent = indent; + uint32_t argsStart = pos; line.params = decodeParams(opcode, endPos, indent); + uint32_t argsEnd = pos; + line.args.assign(scriptData + argsStart, scriptData + argsEnd); lines.push_back(line); if (pos != endPos) @@ -826,21 +948,27 @@ static void disassembleJson(int index, const char *type = "scene") { for (size_t i = 0; i < lines.size(); i++) { const auto &l = lines[i]; std::string text = std::string(getOpcodeName(l.opcode)) + l.params; - printf(" {\"offset\": %u, \"opcode\": %u, \"name\": \"%s\", \"indent\": %d, \"text\": \"%s\"}%s\n", + printf(" {\"offset\": %u, \"opcode\": %u, \"name\": \"%s\", \"indent\": %d, \"text\": \"%s\", \"args\": [", l.offset, l.opcode, jsonEscape(getOpcodeName(l.opcode)).c_str(), - l.indent, jsonEscape(text).c_str(), - (i + 1 < lines.size()) ? "," : ""); + l.indent, jsonEscape(text).c_str()); + for (size_t j = 0; j < l.args.size(); j++) { + if (j) + printf(","); + printf("%u", l.args[j]); + } + printf("]}%s\n", (i + 1 < lines.size()) ? "," : ""); } printf(" ]}"); } static void printHelp(const char *bin) { printf("MACS2 Script Decompiler\n\n"); - printf("Usage: %s [--json] [--objects] [scene_index]\n\n", bin); + printf("Usage: %s [--json] [--cpp] [--objects] [scene_index]\n\n", bin); printf(" --json - Output as JSON\n"); + printf(" --cpp - Output as C++ source (navigable via IDE)\n"); printf(" --objects - Also decompile object scripts for the scene(s)\n"); printf(" Objects with scene=0 (inventory/global) are included\n"); - printf(" game_data_file - The main game data file\n"); + printf(" game_data - RESOURCE.MCS file (DOS) or directory with DataA+Mdir (Amiga)\n"); printf(" scene_index - Scene number to decompile (1-based)\n"); printf(" If omitted, decompiles all scenes\n\n"); printf("The decompiled script will be written to stdout.\n"); @@ -978,6 +1106,1071 @@ static bool loadObjectScript(FILE *f, uint16_t objectIndex) { return true; } +// ============================================================================ +// Amiga PP20 decompression + MXOO resource loading +// ============================================================================ + +#ifndef _WIN32 +#include +#include +#endif + +static bool pp20Decompress(const uint8_t *src, uint32_t srcLen, std::vector &dst) { + if (srcLen < 12 || memcmp(src, "PP20", 4) != 0) + return false; + const uint8_t *eff = src + 4; + const uint8_t *packed = src + 8; + uint32_t packedLen = srcLen - 12; + const uint8_t *trailer = src + srcLen - 4; + uint32_t origSize = ((uint32_t)trailer[0] << 16) | ((uint32_t)trailer[1] << 8) | trailer[2]; + uint8_t bitrot = trailer[3]; + if (origSize == 0) + return false; + dst.resize(origSize); + + uint8_t rev[256]; + for (int a = 0; a < 256; a++) { + uint8_t b = (uint8_t)a; + b = (uint8_t)(((b & 0x0f) << 4) | ((b >> 4) & 0x0f)); + b = (uint8_t)(((b & 0x33) << 2) | ((b >> 2) & 0x33)); + b = (uint8_t)(((b & 0x55) << 1) | ((b >> 1) & 0x55)); + rev[a] = b; + } + + uint32_t outPos = origSize; + int32_t inPos = (int32_t)packedLen; + uint32_t code = 0, shift = 32; + +#define PP_PEEK(x) \ + while (shift > 32u - (x)) { \ + if (inPos <= 0) \ + goto done; \ + shift -= 8; \ + inPos--; \ + code += (uint32_t)rev[packed[inPos]] << shift; \ + } +#define PP_SHIFT(x) \ + do { \ + shift += (x); \ + code = (code << (x)) & 0xFFFFFFFF; \ + } while (0) + + PP_PEEK(bitrot); + PP_SHIFT(bitrot); + while (outPos > 0) { + PP_PEEK(1); + uint32_t bit = code >> 31; + PP_SHIFT(1); + if (bit == 0) { + PP_PEEK(2); + uint32_t length = (code >> 30) + 1; + PP_SHIFT(2); + if (length == 4) { + for (;;) { + PP_PEEK(2); + uint32_t b2 = code >> 30; + PP_SHIFT(2); + length += b2; + if (b2 != 3) + break; + } + } + for (uint32_t i = 0; i < length && outPos > 0; i++) { + PP_PEEK(8); + outPos--; + dst[outPos] = (uint8_t)(code >> 24); + PP_SHIFT(8); + } + if (outPos == 0) + break; + } + PP_PEEK(2); + uint32_t cv = code >> 30; + PP_SHIFT(2); + uint32_t length, nbits; + if (cv == 0) { + length = 2; + nbits = eff[0]; + } else if (cv == 1) { + length = 3; + nbits = eff[1]; + } else if (cv == 2) { + length = 4; + nbits = eff[2]; + } else { + PP_PEEK(1); + uint32_t extra = code >> 31; + PP_SHIFT(1); + length = 5; + nbits = (extra == 0) ? 7 : eff[3]; + } + PP_PEEK(nbits); + uint32_t ptr = (code >> (32 - nbits)) + 1; + PP_SHIFT(nbits); + if (length == 5) { + for (;;) { + PP_PEEK(3); + uint32_t b3 = code >> 29; + PP_SHIFT(3); + length += b3; + if (b3 != 7) + break; + } + } + for (uint32_t i = 0; i < length && outPos > 0; i++) { + outPos--; + dst[outPos] = dst[outPos + ptr]; + } + } +done: +#undef PP_PEEK +#undef PP_SHIFT + return outPos == 0; +} + +static uint32_t readU32BE(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } +static uint16_t readU16BE(const uint8_t *p) { return ((uint16_t)p[0] << 8) | p[1]; } + +static bool isAmigaDir(const char *path) { +#ifndef _WIN32 + struct stat st; + if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) + return false; + std::string da = std::string(path) + "/DataA"; + std::string md = std::string(path) + "/Mdir"; + return stat(da.c_str(), &st) == 0 && stat(md.c_str(), &st) == 0; +#else + return false; +#endif +} + +// Load script+strings from an MXOO resource blob (decompressed PP20 data) +static bool loadAmigaMXOO(const uint8_t *data, uint32_t dataLen) { + if (dataLen < 12 || memcmp(data, "MXOO", 4) != 0) + return false; + + uint32_t scriptOff = readU32BE(data + 4); + uint32_t stringOff = readU32BE(data + 8); + + // Load script: at scriptOff there's padding(2) + size_BE(2) + bytecode + if (scriptOff + 4 > dataLen) + return false; + uint16_t sz = readU16BE(data + scriptOff + 2); + if (sz == 0 || scriptOff + 4 + sz > dataLen) + return false; + + scriptData = (uint8_t *)malloc(sz); + memcpy(scriptData, data + scriptOff + 4, sz); + scriptSize = sz; + pos = 0; + + // Load strings: at stringOff there's padding(2) + totalSize_BE(2) + string entries + if (stringOff + 4 <= dataLen) { + uint16_t strSz = readU16BE(data + stringOff + 2); + if (strSz > 0 && stringOff + 4 + strSz <= dataLen) { + // Convert Amiga string format (u16BE length + plaintext) to DOS format (u16LE length + encrypted) + // Since the decompiler's decodeString expects DOS encrypted format, + // we store raw and override string decoding for Amiga. + // Actually: store the raw Amiga string block and handle in decode. + stringData = (uint8_t *)malloc(strSz); + memcpy(stringData, data + stringOff + 4, strSz); + stringDataSize = strSz; + } + } + return true; +} + +// Override string decode for Amiga (plaintext with u16BE lengths) + +static std::string decodeStringAmiga(uint32_t offset, uint16_t numLines) { + if (!stringData || offset >= stringDataSize) + return ""; + uint32_t p = offset; + std::string result; + for (uint16_t line = 0; line < numLines && p + 2 <= stringDataSize; line++) { + uint16_t length = readU16BE(stringData + p); + p += 2; + if (length == 0 || p + length > stringDataSize) + break; + for (uint16_t i = 0; i < length && p < stringDataSize; i++, p++) { + appendUtf8(result, stringData[p]); + } + if (line + 1 < numLines) + result += " / "; + } + return result; +} + +static int runAmiga(const char *dir, bool jsonMode, bool objectsMode) { + std::string dataAPath = std::string(dir) + "/DataA"; + + FILE *df = fopen(dataAPath.c_str(), "rb"); + if (!df) { + fprintf(stderr, "Cannot open %s\n", dataAPath.c_str()); + return 1; + } + fseek(df, 0, SEEK_END); + uint32_t fileSize = (uint32_t)ftell(df); + fseek(df, 0, SEEK_SET); + + // Read MXMF header + uint8_t hdr[14]; + fread(hdr, 1, 14, df); + if (memcmp(hdr, "MXMF", 4) != 0) { + fprintf(stderr, "Not MXMF\n"); + fclose(df); + return 1; + } + uint16_t totalRes = readU16BE(hdr + 8); + uint32_t firstBlockSize = readU32BE(hdr + 10); + + amigaMode = true; + + if (!jsonMode) { + printf("; MACS2 Amiga Script Decompiler\n"); + printf("; Scripts are byte-identical to DOS version\n"); + printf("; Strings are plaintext (not encrypted)\n;\n\n"); + } else { + printf("{\"resources\": [\n"); + } + + // Iterate all resources sequentially + fseek(df, 14 + firstBlockSize, SEEK_SET); + bool first = true; + int resIdx = 0; + for (uint16_t i = 0; i < totalRes - 1; i++) { + uint8_t ehdr[8]; + if (fread(ehdr, 1, 8, df) != 8) + break; + char eType[3] = {(char)ehdr[0], (char)ehdr[1], '\0'}; + uint16_t eId = readU16BE(ehdr + 2); + uint32_t eCompSize = readU32BE(ehdr + 4); + if (eCompSize == 0 || eCompSize > fileSize) + break; + + std::vector payload(eCompSize); + if (fread(payload.data(), 1, eCompSize, df) != eCompSize) + break; + + // Only process OO (object) resources that contain PP20 + if (memcmp(payload.data(), "PP20", 4) != 0) + continue; + // Skip non-OO types (MM=music, OS=sound, FF=font) + if (strcmp(eType, "OO") != 0 && strcmp(eType, "FF") != 0) + continue; + + std::vector dec; + if (!pp20Decompress(payload.data(), eCompSize, dec)) + continue; + if (dec.size() < 12 || memcmp(dec.data(), "MXOO", 4) != 0) + continue; + + // Check if this MXOO has a script (scriptOff != 0 and has valid script size) + uint32_t scriptOff = readU32BE(dec.data() + 4); + if (scriptOff + 4 > dec.size()) + continue; + uint16_t sz = readU16BE(dec.data() + scriptOff + 2); + if (sz == 0) + continue; + + if (loadAmigaMXOO(dec.data(), (uint32_t)dec.size())) { + if (jsonMode) { + if (!first) + printf(",\n"); + first = false; + disassembleJson(eId, eType); + } else { + printf("=== %s_%04d (size: %u bytes) ===\n", eType, eId, scriptSize); + disassemble(); + printf("\n"); + } + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + resIdx++; + } + + if (jsonMode) + printf("\n]}\n"); + fclose(df); + return 0; +} + +// ============================================================================ +// C source output mode +// ============================================================================ + +#include + +static std::set usedObjects; +static std::set usedVars; +static uint16_t maxVarIdx = 0; + +static const char *getVarName(uint16_t idx) { + switch (idx) { + case 3: + return "VAR_MUSIC_SLOT"; + case 10: + return "VAR_KNIFE_FIGHT_DONE"; + case 11: + return "VAR_CHAPTER"; + case 12: + return "VAR_SCENE_PROGRESS"; + case 13: + return "VAR_TEMP_X"; + case 14: + return "VAR_BGANIM_TOGGLED"; + case 15: + return "VAR_TALKED_TO_NPC"; + case 16: + return "VAR_HAS_ITEM"; + case 18: + return "VAR_HAS_PLOT_ITEM"; + case 19: + return "VAR_MET_CHARACTER"; + case 22: + return "VAR_CUTSCENE_TRIGGERED"; + case 51: + return "VAR_KNIFE_SEPARATED"; + case 263: + return "VAR_DIALOGUE_CHOICE"; + default: + return nullptr; + } +} + +static std::string formatVarAccess(uint16_t idx) { + const char *name = getVarName(idx); + if (name) + return std::string("_vars[") + name + "]"; + char buf[32]; + snprintf(buf, sizeof(buf), "_vars[%u]", idx); + return buf; +} + +static const char *getObjectName(uint16_t idx) { + // Full game object names from Macs2::GameObjects::init() + // Characters/NPCs + switch (idx) { + case 1: + return "OBJ_PLAYER"; + case 2: + return "OBJ_CAPTAIN"; + case 3: + return "OBJ_AUNT"; + case 6: + return "OBJ_CORNEL"; + case 7: + return "OBJ_BEAR"; + case 9: + return "OBJ_WOLF"; + case 12: + return "OBJ_INDIAN"; + case 13: + return "OBJ_PATTERSON"; + case 15: + return "OBJ_ROLLINS"; + case 19: + return "OBJ_HILTON"; + // Items (full game, indices from _objectNames) + case 0x08: + return "OBJ_BOARD"; + case 0x0E: + return "OBJ_BUCKET_WATER"; + case 0x10: + return "OBJ_BARREL"; + case 0x11: + return "OBJ_BOWIE_KNIFE"; + case 0x14: + return "OBJ_BUCKET_WATER2"; + case 0x17: + return "OBJ_HATBOX_OPEN"; + case 0x18: + return "OBJ_HAT"; + case 0x19: + return "OBJ_HATBOX_CLOSED"; + case 0x1A: + return "OBJ_METAL_BUCKET"; + case 0x1B: + return "OBJ_FIRE_POKER"; + case 0x1C: + return "OBJ_POT_HOLDER"; + case 0x1D: + return "OBJ_BOARD_WRAPPED"; + case 0x1E: + return "OBJ_COAL_SHOVEL"; + case 0x20: + return "OBJ_COCKROACH"; + case 0x22: + return "OBJ_CUP_FULL"; + case 0x23: + return "OBJ_CUP_EMPTY"; + case 0x24: + return "OBJ_AXE"; + case 0x25: + return "OBJ_AXE_BLADE"; + case 0x26: + return "OBJ_AXE_HANDLE"; + case 0x28: + return "OBJ_LETTER"; + case 0x29: + return "OBJ_BREAD_MOLDY"; + case 0x2A: + return "OBJ_BREAD_STALE"; + case 0x2B: + return "OBJ_ENVELOPE_OPEN"; + case 0x2C: + return "OBJ_ENVELOPE_SEALED"; + case 0x2D: + return "OBJ_RACCOON_FUR"; + case 0x2E: + return "OBJ_WHISKY_GLASS"; + case 0x2F: + return "OBJ_LEATHER_BELT"; + case 0x30: + return "OBJ_POKER"; + case 0x34: + return "OBJ_BOARD_SOLID"; + case 0x36: + return "OBJ_BIRDCAGE_BIRD"; + case 0x37: + return "OBJ_BIRDCAGE_BROKEN"; + case 0x38: + return "OBJ_BIRDCAGE_EMPTY"; + case 0x3A: + return "OBJ_MAP"; + case 0x3B: + return "OBJ_CANDLE"; + case 0x3C: + return "OBJ_PEBBLES"; + case 0x3D: + return "OBJ_SUITCASE_DYNAMITE"; + case 0x3E: + return "OBJ_BANKNOTES"; + case 0x3F: + return "OBJ_LEATHER_POUCH"; + case 0x40: + return "OBJ_SLINGSHOT"; + case 0x41: + return "OBJ_FIRECRACKERS"; + case 0x42: + return "OBJ_SUITCASE_OPEN"; + case 0x43: + return "OBJ_SUITCASE_CLOSED"; + case 0x44: + return "OBJ_PAPER"; + case 0x47: + return "OBJ_KNIFE_RUSTY"; + case 0x4B: + return "OBJ_REEDS_DRY"; + case 0x4C: + return "OBJ_REEDS"; + case 0x4F: + return "OBJ_LIQUOR_BOTTLE"; + case 0x51: + return "OBJ_CLOTH_BAG"; + case 0x52: + return "OBJ_CLOTH_BAG_MARBLES"; + case 0x53: + return "OBJ_BELLOWS"; + case 0x54: + return "OBJ_FIREWOOD"; + case 0x55: + return "OBJ_BREAD_SLICED"; + case 0x56: + return "OBJ_BREAD_KNIFE"; + case 0x62: + return "OBJ_PICKAXE"; + case 0x6B: + return "OBJ_CORKSCREW"; + case 0x6F: + return "OBJ_MARBLES"; + case 0x70: + return "OBJ_MUSKETS"; + case 0x71: + return "OBJ_WOODEN_STAKE"; + case 0x74: + return "OBJ_SHOVEL"; + case 0x78: + return "OBJ_BRASS_KEY"; + case 0x7E: + return "OBJ_GUNPOWDER"; + case 0x7F: + return "OBJ_SULPHUR"; + case 0x80: + return "OBJ_HEMP_ROPE"; + case 0x82: + return "OBJ_ROPES_TIED"; + case 0x84: + return "OBJ_SPATULA"; + case 0x86: + return "OBJ_MATCH"; + case 0x94: + return "OBJ_MINE_CART"; + case 0x99: + return "OBJ_CROWBAR"; + case 0x9B: + return "OBJ_DYNAMITE"; + case 0x9C: + return "OBJ_TORCH_LIT"; + case 0x9D: + return "OBJ_TORCH"; + case 0xA1: + return "OBJ_PULLEY"; + case 0xA2: + return "OBJ_HOOK"; + case 0xA3: + return "OBJ_HOOK_ROPE"; + case 0xAE: + return "OBJ_HEMP_ROPE2"; + case 0xB2: + return "OBJ_SCREWDRIVER"; + case 0xB3: + return "OBJ_IRON_BAR"; + case 0xB4: + return "OBJ_TOMAHAWK"; + default: + return nullptr; + } +} + +static std::string formatValueC() { + uint8_t type = readByte(); + uint16_t val = readWord(); + char buf[64]; + if (type == 0x00) { + if (val > 0x400 && val <= 0x600) { + uint16_t idx = val - 0x400; + usedObjects.insert(idx); + const char *name = getObjectName(idx); + if (name) + return name; + snprintf(buf, sizeof(buf), "_objects[%u]", idx); + return buf; + } + snprintf(buf, sizeof(buf), "%d", (int)(int16_t)val); + return buf; + } else if (type == 0xFF) { + const char *name = getSpecialName(val); + if (name) + return std::string("_rt.") + name; + if (val >= 0x0E && val <= 0x22) { + snprintf(buf, sizeof(buf), "%u", val - 0x0D); + return buf; + } + switch (val) { + case 0x06: + return "TRUE"; + case 0x07: + return "FALSE"; + case 0x08: + return "FADE_CUT"; + case 0x09: + return "SIDE_LEFT"; + case 0x0A: + return "SIDE_RIGHT"; + case 0x0C: + return "LOOP"; + } + snprintf(buf, sizeof(buf), "_rt.special[0x%02x]", val); + return buf; + } + usedVars.insert(val); + if (val > maxVarIdx) + maxVarIdx = val; + return formatVarAccess(val); +} + +static std::string decodeCLine(uint8_t opcode, uint32_t endPos, int &indent) { + char buf[1024]; + switch (opcode) { + case 0x01: { + readByte(); + uint16_t v = readWord(); + usedVars.insert(v); + if (v > maxVarIdx) + maxVarIdx = v; + std::string val = formatValueC(); + std::string dest = formatVarAccess(v); + snprintf(buf, sizeof(buf), "%s = %s;", dest.c_str(), val.c_str()); + return buf; + } + case 0x02: { + readByte(); + uint16_t v = readWord(); + usedVars.insert(v); + if (v > maxVarIdx) + maxVarIdx = v; + std::string val = formatValueC(); + std::string dest = formatVarAccess(v); + snprintf(buf, sizeof(buf), "%s |= %s;", dest.c_str(), val.c_str()); + return buf; + } + case 0x03: { + std::string val = formatValueC(); + snprintf(buf, sizeof(buf), "if (!%s) {", val.c_str()); + indent++; + return buf; + } + case 0x04: { + std::string val = formatValueC(); + snprintf(buf, sizeof(buf), "if (%s) {", val.c_str()); + indent++; + return buf; + } + case 0x05: { + uint8_t op = readByte(); + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "if (%s %s %s) {", a.c_str(), cmpOpName(op), b.c_str()); + indent++; + return buf; + } + case 0x06: { + uint8_t sub = readByte(); + std::string i = formatValueC(); + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "if (%sifInteraction(%s, %s, %s)) {", sub == 2 ? "!" : "", i.c_str(), a.c_str(), b.c_str()); + indent++; + return buf; + } + case 0x07: + return "}"; + case 0x08: + indent++; + return "} else {"; + case 0x0A: + case 0x30: { + std::string x = formatValueC(); + std::string y = formatValueC(); + uint16_t s = readWord(); + uint16_t n = readWord(); + std::string decoded = decodeString(s, n); + std::string result2 = std::string(opcode == 0x0A ? "printStringLeft(" : "printStringRight(") + x + ", " + y + ", " + std::to_string(s) + ", " + std::to_string(n) + ");"; + if (!decoded.empty()) + result2 += " // \"" + decoded + "\""; + return result2; + } + case 0x0B: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + if (s == "0" && x == "0" && y == "0") + snprintf(buf, sizeof(buf), "removeFromScene(%s);", o.c_str()); + else + snprintf(buf, sizeof(buf), "moveObject(%s, %s, %s, %s);", o.c_str(), s.c_str(), x.c_str(), y.c_str()); + return buf; + } + case 0x0C: { + std::string s = formatValueC(); + std::string m = formatValueC(); + std::string sp = formatValueC(); + snprintf(buf, sizeof(buf), "changeScene(%s, %s, %s);", s.c_str(), m.c_str(), sp.c_str()); + return buf; + } + case 0x0D: { + std::string o = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + std::string s = formatValueC(); + uint16_t so = readWord(); + uint16_t n = readWord(); + std::string decoded = decodeString(so, n); + std::string result2 = std::string("showDialogue(") + o + ", " + x + ", " + y + ", " + s + ", " + std::to_string(so) + ", " + std::to_string(n) + ");"; + if (!decoded.empty()) + result2 += " // \"" + decoded + "\""; + return result2; + } + case 0x0E: + return "changeAnimation();"; + case 0x0F: { + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "frameWait(%s);", v.c_str()); + return buf; + } + case 0x10: { + std::string o = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + snprintf(buf, sizeof(buf), "walkToPosition(%s, %s, %s);", o.c_str(), x.c_str(), y.c_str()); + return buf; + } + case 0x11: { + std::string o = formatValueC(); + snprintf(buf, sizeof(buf), "waitForWalk(%s);", o.c_str()); + return buf; + } + case 0x12: { + std::string a = formatValueC(); + std::string b = formatValueC(); + std::string c = formatValueC(); + snprintf(buf, sizeof(buf), "setPathfinding(%s, %s, %s);", a.c_str(), b.c_str(), c.c_str()); + return buf; + } + case 0x13: + return "skipUntil14();"; + case 0x14: { + uint16_t w = readWord(); + snprintf(buf, sizeof(buf), "skipWord(0x%04x);", w); + return buf; + } + case 0x15: + return "clearDialogueChoices();"; + case 0x16: { + std::string i = formatValueC(); + uint16_t s = readWord(); + uint16_t n = readWord(); + std::string decoded = decodeString(s, n); + std::string result2 = std::string("addDialogueChoice(") + i + ", " + std::to_string(s) + ", " + std::to_string(n) + ");"; + if (!decoded.empty()) + result2 += " // \"" + decoded + "\""; + return result2; + } + case 0x17: { + std::string o = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + std::string s = formatValueC(); + snprintf(buf, sizeof(buf), "showDialogueChoice(%s, %s, %s, %s);", o.c_str(), x.c_str(), y.c_str(), s.c_str()); + return buf; + } + case 0x18: + return "dismissPanel();"; + case 0x19: { + std::string a = formatValueC(); + std::string o = formatValueC(); + snprintf(buf, sizeof(buf), "walkToAndPickup(%s, %s);", a.c_str(), o.c_str()); + return buf; + } + case 0x1A: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string e = formatValueC(); + snprintf(buf, sizeof(buf), "setPickupFrames(%s, %s, %s);", o.c_str(), s.c_str(), e.c_str()); + return buf; + } + case 0x1B: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string sp = formatValueC(); + snprintf(buf, sizeof(buf), "setupObject(%s, %s, %s);", o.c_str(), s.c_str(), sp.c_str()); + return buf; + } + case 0x1C: + return "setSkippable();"; + case 0x1D: + return "clearSkippable();"; + case 0x1E: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string f = formatValueC(); + snprintf(buf, sizeof(buf), "playAnimation(%s, %s, %s);", o.c_str(), s.c_str(), f.c_str()); + return buf; + } + case 0x1F: { + std::string o = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + snprintf(buf, sizeof(buf), "_rt.pathWalkable = testPathfinding(%s, %s, %s);", o.c_str(), x.c_str(), y.c_str()); + return buf; + } + case 0x20: { + std::string o = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "setYOffset(%s, %s);", o.c_str(), v.c_str()); + return buf; + } + case 0x21: { + std::string o = formatValueC(); + std::string t = formatValueC(); + std::string d = formatValueC(); + std::string di = formatValueC(); + snprintf(buf, sizeof(buf), "setMotion(%s, %s, %s, %s);", o.c_str(), t.c_str(), d.c_str(), di.c_str()); + return buf; + } + case 0x22: { + std::string o = formatValueC(); + std::string a = formatValueC(); + // Replace numeric orientation with named constant + static const char *orientNames[] = {nullptr, "ORIENT_WALK_N", "ORIENT_WALK_NE", "ORIENT_WALK_E", "ORIENT_WALK_SE", "ORIENT_WALK_S", "ORIENT_WALK_SW", "ORIENT_WALK_W", "ORIENT_WALK_NW", "ORIENT_STAND_N", "ORIENT_STAND_NE", "ORIENT_STAND_E", "ORIENT_STAND_SE", "ORIENT_STAND_S", "ORIENT_STAND_SW", "ORIENT_STAND_W", "ORIENT_STAND_NW", "ORIENT_PICKUP"}; + int ov = atoi(a.c_str()); + if (ov >= 1 && ov <= 17) + a = orientNames[ov]; + snprintf(buf, sizeof(buf), "setOrientation(%s, %s);", o.c_str(), a.c_str()); + return buf; + } + case 0x23: { + std::string o = formatValueC(); + std::string x = formatValueC(); + std::string y = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "moveToPosition(%s, %s, %s, %s);", o.c_str(), x.c_str(), y.c_str(), v.c_str()); + return buf; + } + case 0x24: + case 0x25: { + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "%s %s= %s;", a.c_str(), opcode == 0x24 ? "+" : "-", b.c_str()); + return buf; + } + case 0x26: { + std::string o = formatValueC(); + std::string d = formatValueC(); + uint8_t i = readByte(); + snprintf(buf, sizeof(buf), "loadSpecialAnim(%s, %s, %u);", o.c_str(), d.c_str(), i); + return buf; + } + case 0x27: { + std::string o = formatValueC(); + std::string m = formatValueC(); + snprintf(buf, sizeof(buf), "setDirection(%s, %s);", o.c_str(), m.c_str()); + return buf; + } + case 0x28: + return "stopAnimation();"; + case 0x29: { + std::string o = formatValueC(); + snprintf(buf, sizeof(buf), "openInventory(%s);", o.c_str()); + return buf; + } + case 0x2A: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string d = formatValueC(); + uint8_t i = readByte(); + snprintf(buf, sizeof(buf), "loadObjectAnim(%s, %s, %s, %u);", o.c_str(), s.c_str(), d.c_str(), i); + return buf; + } + case 0x2B: { + std::string o = formatValueC(); + snprintf(buf, sizeof(buf), "checkObjectData(%s);", o.c_str()); + return buf; + } + case 0x2C: { + std::string o = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "_rt.invCheck = checkInventory(%s, %s);", o.c_str(), v.c_str()); + return buf; + } + case 0x2D: { + std::string o = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "setSnapToTarget(%s, %s);", o.c_str(), v.c_str()); + return buf; + } + case 0x2E: { + std::string a = formatValueC(); + std::string lo = formatValueC(); + std::string hi = formatValueC(); + snprintf(buf, sizeof(buf), "_rt.animBlobRange = testSceneAnimFrame(%s, %s, %s);", a.c_str(), lo.c_str(), hi.c_str()); + return buf; + } + case 0x2F: { + std::string o = formatValueC(); + std::string s = formatValueC(); + std::string lo = formatValueC(); + std::string hi = formatValueC(); + snprintf(buf, sizeof(buf), "_rt.animBlobRange = testObjectAnimFrame(%s, %s, %s, %s);", o.c_str(), s.c_str(), lo.c_str(), hi.c_str()); + return buf; + } + case 0x31: { + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "setPaletteDarkness(%s);", v.c_str()); + return buf; + } + case 0x32: { + std::string o = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "setObjectClickable(%s, %s);", o.c_str(), v.c_str()); + return buf; + } + case 0x33: { + std::string o = formatValueC(); + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "setObjectVisible(%s, %s);", o.c_str(), v.c_str()); + return buf; + } + case 0x34: { + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "setHotspotOverride(%s, %s);", a.c_str(), b.c_str()); + return buf; + } + case 0x35: { + std::string o = formatValueC(); + std::string p = formatValueC(); + std::string a = formatValueC(); + std::string b = formatValueC(); + std::string c = formatValueC(); + snprintf(buf, sizeof(buf), "setObjectBounds(%s, %s, %s, %s, %s);", o.c_str(), p.c_str(), a.c_str(), b.c_str(), c.c_str()); + return buf; + } + case 0x36: + return "dismissAllPanels();"; + case 0x37: + return "resetToSceneScript();"; + case 0x38: { + uint8_t r = readByte(); + snprintf(buf, sizeof(buf), "loadOverlayFont(%u);", r); + return buf; + } + case 0x39: + return "endOverlayText();"; + case 0x3A: { + std::string x = formatValueC(); + std::string y = formatValueC(); + std::string a = formatValueC(); + uint16_t s = readWord(); + uint16_t t = readWord(); + snprintf(buf, sizeof(buf), "addOverlayTextEntry(%s, %s, %s, %u, %u);", x.c_str(), y.c_str(), a.c_str(), s, t); + return buf; + } + case 0x3B: + return "clearOverlayText();"; + case 0x3C: { + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "fadeToBlack(%s);", v.c_str()); + return buf; + } + case 0x3D: { + std::string v = formatValueC(); + snprintf(buf, sizeof(buf), "fadeFromBlack(%s);", v.c_str()); + return buf; + } + case 0x3E: { + uint8_t r = readByte(); + snprintf(buf, sizeof(buf), "loadPcmSound(%u);", r); + return buf; + } + case 0x3F: + return "freePcmSound();"; + case 0x40: + return "playPcmSound();"; + case 0x41: + return "waitForSound();"; + case 0x42: + return "stopPcmSound();"; + case 0x43: { + std::string s = formatValueC(); + uint8_t r = readByte(); + snprintf(buf, sizeof(buf), "loadMusicSlot(%s, %u);", s.c_str(), r); + return buf; + } + case 0x44: + case 0x45: { + std::string s = formatValueC(); + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "%s(%s, %s, %s);", opcode == 0x44 ? "playMusicSlot" : "stopMusicSlot", s.c_str(), a.c_str(), b.c_str()); + return buf; + } + case 0x46: { + std::string s = formatValueC(); + snprintf(buf, sizeof(buf), "freeMusicSlot(%s);", s.c_str()); + return buf; + } + case 0x47: { + std::string o = formatValueC(); + snprintf(buf, sizeof(buf), "waitForMusic(%s);", o.c_str()); + return buf; + } + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: { + std::string o = formatValueC(); + if (pos + 3 <= endPos) { + readByte(); + uint16_t v = readWord(); + usedVars.insert(v); + if (v > maxVarIdx) + maxVarIdx = v; + snprintf(buf, sizeof(buf), "%s = %s(%s);", formatVarAccess(v).c_str(), getOpcodeName(opcode), o.c_str()); + } else { + snprintf(buf, sizeof(buf), "%s(%s);", getOpcodeName(opcode), o.c_str()); + } + return buf; + } + case 0x4C: + return "clearActorInventory();"; + case 0x4D: { + std::string a = formatValueC(); + std::string b = formatValueC(); + snprintf(buf, sizeof(buf), "setPathfindingRemap(%s, %s);", a.c_str(), b.c_str()); + return buf; + } + case 0x4E: + return "waitForAdlib();"; + default: + snprintf(buf, sizeof(buf), "/* unknown opcode 0x%02x */", opcode); + return buf; + } +} + +static void disassembleC(int sceneIndex, const char *type = "scene") { + // First pass: collect used objects and vars + usedObjects.clear(); + usedVars.clear(); + maxVarIdx = 0; + + uint32_t savedPos = pos; + uint32_t savedSize = scriptSize; + // Dry run to collect references + int dummyIndent = 0; + while (pos < scriptSize - 1) { + uint8_t opcode = readByte(); + if (opcode == 0x00) + continue; + uint8_t length = readByte(); + uint32_t endPos = pos + length; + if (opcode == 0x07 && dummyIndent > 0) + dummyIndent--; + if (opcode == 0x08 && dummyIndent > 0) + dummyIndent--; + decodeCLine(opcode, endPos, dummyIndent); + if (pos != endPos) + pos = endPos; + } + + // Second pass: emit C + pos = savedPos; + scriptSize = savedSize; + + printf("// --- %s %d ---\n", type, sceneIndex); + + // Function body + printf("void %s_%d_script(void) {\n", type, sceneIndex); + + int indent = 0; + while (pos < scriptSize - 1) { + uint8_t opcode = readByte(); + if (opcode == 0x00) + continue; + uint8_t length = readByte(); + uint32_t endPos = pos + length; + if (opcode == 0x07 && indent > 0) + indent--; + if (opcode == 0x08 && indent > 0) + indent--; + std::string line = decodeCLine(opcode, endPos, indent); + printf("\t"); + for (int i = 0; i < indent - (opcode == 0x03 || opcode == 0x04 || opcode == 0x05 || opcode == 0x06 || opcode == 0x08 ? 1 : 0); i++) + printf("\t"); + printf("%s\n", line.c_str()); + if (pos != endPos) + pos = endPos; + } + + printf("}\n"); +} + int main(int argc, char **argv) { if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) { printHelp(argv[0]); @@ -985,11 +2178,14 @@ int main(int argc, char **argv) { } bool jsonMode = false; + bool cMode = false; bool objectsMode = false; int argIdx = 1; while (argIdx < argc && argv[argIdx][0] == '-') { if (!strcmp(argv[argIdx], "--json")) { jsonMode = true; + } else if (!strcmp(argv[argIdx], "--c") || !strcmp(argv[argIdx], "--cpp")) { + cMode = true; } else if (!strcmp(argv[argIdx], "--objects")) { objectsMode = true; } else { @@ -1004,11 +2200,26 @@ int main(int argc, char **argv) { return 1; } + // Check if it's an Amiga data directory first + if (isAmigaDir(argv[argIdx])) { + return runAmiga(argv[argIdx], jsonMode, objectsMode); + } + FILE *f = fopen(argv[argIdx], "rb"); if (!f) { fprintf(stderr, "Error: Cannot open file '%s'\n", argv[argIdx]); return 1; } + + // Also check: if it opens but starts with MXMF, redirect to Amiga mode + uint8_t magic[4]; + fread(magic, 1, 4, f); + fseek(f, 0, SEEK_SET); + if (memcmp(magic, "MXMF", 4) == 0) { + fclose(f); + fprintf(stderr, "Error: '%s' is an Amiga DataA file. Pass the directory containing it.\n", argv[argIdx]); + return 1; + } argIdx++; int startScene = 1; @@ -1019,7 +2230,162 @@ int main(int argc, char **argv) { endScene = startScene; } - if (jsonMode) { + if (cMode) { + printf("// MACS2 auto-decompiled scripts\n"); + printf("#define DEMACS2\n"); + printf("#include \"engines/macs2/scriptexecutor.h\"\n\n"); + printf("using namespace Macs2::Script;\n\n"); + printf("static ScriptExecutor _executor;\n\n"); + printf("typedef uint16_t Object;\ntypedef uint16_t Value;\n\n"); + printf("static const Value TRUE = 1;\n"); + printf("static const Value FALSE = 0;\n"); + printf("static const Value FADE_CUT = 0;\n"); + printf("static const Value SIDE_LEFT = 0;\n"); + printf("static const Value SIDE_RIGHT = 1;\n"); + printf("static const Value LOOP = 1;\n\n"); + printf("/* Orientation constants (1-8=walking, 9-16=standing, 17=pickup) */\n"); + printf("#define ORIENT_WALK_N 1\n#define ORIENT_WALK_NE 2\n#define ORIENT_WALK_E 3\n#define ORIENT_WALK_SE 4\n"); + printf("#define ORIENT_WALK_S 5\n#define ORIENT_WALK_SW 6\n#define ORIENT_WALK_W 7\n#define ORIENT_WALK_NW 8\n"); + printf("#define ORIENT_STAND_N 9\n#define ORIENT_STAND_NE 10\n#define ORIENT_STAND_E 11\n#define ORIENT_STAND_SE 12\n"); + printf("#define ORIENT_STAND_S 13\n#define ORIENT_STAND_SW 14\n#define ORIENT_STAND_W 15\n#define ORIENT_STAND_NW 16\n"); + printf("#define ORIENT_PICKUP 17\n\n"); + printf("/* Known script variable indices */\n"); + for (uint16_t i = 0; i <= 512; i++) { + const char *n = getVarName(i); + if (n) + printf("#define %s %u\n", n, i); + } + printf("\n"); + printf("static struct {\n"); + for (const auto &s : kSpecialNames) + printf("\tValue %s;\n", s.name); + printf("} _rt;\n\n"); + printf("static Object _objects[512];\nstatic Value _vars[512];\n\n"); + printf("/* Named objects */\n"); + for (uint16_t i = 1; i <= 0xBF; i++) { + const char *n = getObjectName(i); + if (n) + printf("#define %s 0x%02x\n", n, i); + } + printf("\n"); + printf("/* Wrappers to ScriptExecutor::scriptXXX */\n"); + printf("static void moveObject(Object obj, Value scene, Value x, Value y) { _executor.scriptMoveObject(); }\n"); + printf("static void removeFromScene(Object obj) { _executor.scriptMoveObject(); }\n"); + printf("static void changeScene(Value scene, Value mode, Value speed) { _executor.scriptChangeScene(); }\n"); + printf("static void showDialogue(Object obj, Value x, Value y, Value side, uint16_t s, uint16_t n) { _executor.scriptShowDialogue(); }\n"); + printf("static void printStringLeft(Value x, Value y, uint16_t s, uint16_t n) { _executor.scriptPrintStringLeft(); }\n"); + printf("static void printStringRight(Value x, Value y, uint16_t s, uint16_t n) { _executor.scriptPrintStringRight(); }\n"); + printf("static void frameWait(Value t) { _executor.scriptFrameWait(); }\n"); + printf("static void walkToPosition(Object o, Value x, Value y) { _executor.scriptWalkToPosition(); }\n"); + printf("static void waitForWalk(Object o) { _executor.scriptWaitForWalk(); }\n"); + printf("static void setPathfinding(Value a, Value b, Value c) { _executor.scriptSetPathfinding(); }\n"); + printf("static void skipWord(uint16_t w) { _executor.scriptSkipWord(); }\n"); + printf("static void clearDialogueChoices() { _executor.scriptClearDialogueChoices(); }\n"); + printf("static void addDialogueChoice(Value i, uint16_t s, uint16_t n) { _executor.scriptAddDialogueChoice(); }\n"); + printf("static void showDialogueChoice(Object o, Value x, Value y, Value s) { _executor.scriptShowDialogueChoice(); }\n"); + printf("static void dismissPanel() { _executor.scriptDismissPanel(); }\n"); + printf("static void walkToAndPickup(Object a, Object o) { _executor.scriptWalkToAndPickup(); }\n"); + printf("static void setPickupFrames(Object o, Value s, Value e) { _executor.scriptSetPickupFrames(); }\n"); + printf("static void setupObject(Object o, Value s, Value sp) { _executor.scriptSetupObject(); }\n"); + printf("static void setSkippable() { _executor.scriptSetSkippable(); }\n"); + printf("static void clearSkippable() { _executor.scriptClearSkippable(); }\n"); + printf("static void playAnimation(Object o, Value s, Value f) { _executor.scriptPlayAnimation(); }\n"); + printf("static Value testPathfinding(Object o, Value x, Value y) { _executor.scriptTestPathfinding(); return 0; }\n"); + printf("static void setYOffset(Object o, Value v) { _executor.scriptSetYOffset(); }\n"); + printf("static void setMotion(Object o, Value t, Value d, Value di) { _executor.scriptSetMotion(); }\n"); + printf("static void setOrientation(Object o, Value a) { _executor.scriptSetOrientation(); }\n"); + printf("static void moveToPosition(Object o, Value x, Value y, Value v) { _executor.scriptMoveToPosition(); }\n"); + printf("static void loadSpecialAnim(Object o, Value d, uint8_t i) { _executor.scriptLoadSpecialAnim(); }\n"); + printf("static void setDirection(Object o, Value m) { _executor.scriptSetDirection(); }\n"); + printf("static void stopAnimation() { _executor.scriptStopAnimation(); }\n"); + printf("static void changeAnimation() { _executor.scriptChangeAnimation(); }\n"); + printf("static void openInventory(Object o) { _executor.scriptOpenInventory(); }\n"); + printf("static void loadObjectAnim(Object o, Value s, Value d, uint8_t i) { _executor.scriptLoadObjectAnim(); }\n"); + printf("static void checkObjectData(Object o) { _executor.scriptCheckObjectData(); }\n"); + printf("static Value checkInventory(Object o, Value v) { _executor.scriptCheckInventory(); return 0; }\n"); + printf("static void setSnapToTarget(Object o, Value v) { _executor.scriptSetSnapToTarget(); }\n"); + printf("static Value testSceneAnimFrame(Value a, Value lo, Value hi) { _executor.scriptTestSceneAnimFrame(); return 0; }\n"); + printf("static Value testObjectAnimFrame(Object o, Value s, Value lo, Value hi) { _executor.scriptTestObjectAnimFrame(); return 0; }\n"); + printf("static void setPaletteDarkness(Value v) { _executor.scriptSetPaletteDarkness(); }\n"); + printf("static void setObjectClickable(Object o, Value v) { _executor.scriptSetObjectClickable(); }\n"); + printf("static void setObjectVisible(Object o, Value v) { _executor.scriptSetObjectVisible(); }\n"); + printf("static void setHotspotOverride(Value a, Value b) { _executor.scriptSetHotspotOverride(); }\n"); + printf("static void setObjectBounds(Object o, Object p, Value a, Value b, Value c) { _executor.scriptSetObjectBounds(); }\n"); + printf("static void dismissAllPanels() { _executor.scriptDismissAllPanels(); }\n"); + printf("static void resetToSceneScript() { _executor.scriptResetToSceneScript(); }\n"); + printf("static void loadOverlayFont(uint8_t r) { _executor.scriptLoadOverlayFont(); }\n"); + printf("static void endOverlayText() { _executor.scriptEndOverlayText(); }\n"); + printf("static void addOverlayTextEntry(Value x, Value y, Value a, uint16_t s, uint16_t t) { _executor.scriptAddOverlayTextEntry(); }\n"); + printf("static void clearOverlayText() { _executor.scriptClearOverlayText(); }\n"); + printf("static void fadeToBlack(Value s) { _executor.scriptFadeToBlack(); }\n"); + printf("static void fadeFromBlack(Value s) { _executor.scriptFadeFromBlack(); }\n"); + printf("static void loadPcmSound(uint8_t r) { _executor.scriptLoadPcmSound(); }\n"); + printf("static void freePcmSound() { _executor.scriptFreePcmSound(); }\n"); + printf("static void playPcmSound() { _executor.scriptPlayPcmSound(); }\n"); + printf("static void waitForSound() { _executor.scriptWaitForSound(); }\n"); + printf("static void stopPcmSound() { _executor.scriptStopPcmSound(); }\n"); + printf("static void loadMusicSlot(Value s, uint8_t r) { _executor.scriptLoadMusicSlot(); }\n"); + printf("static void playMusicSlot(Value s, Value a, Value b) { _executor.scriptPlayMusicSlot(); }\n"); + printf("static void stopMusicSlot(Value s, Value a, Value b) { _executor.scriptStopMusicSlot(); }\n"); + printf("static void freeMusicSlot(Value s) { _executor.scriptFreeMusicSlot(); }\n"); + printf("static void waitForMusic(Object o) { _executor.scriptWaitForMusic(); }\n"); + printf("static Value getObjectX(Object o) { _executor.scriptGetObjectX(); return 0; }\n"); + printf("static Value getObjectY(Object o) { _executor.scriptGetObjectY(); return 0; }\n"); + printf("static Value getObjectField8(Object o) { _executor.scriptGetObjectField8(); return 0; }\n"); + printf("static Value getObjectOrientation(Object o) { _executor.scriptGetObjectOrientation(); return 0; }\n"); + printf("static void clearActorInventory() { _executor.scriptClearActorInventory(); }\n"); + printf("static void setPathfindingRemap(Value a, Value b) { _executor.scriptSetPathfindingRemap(); }\n"); + printf("static void waitForAdlib() { _executor.scriptWaitForAdlib(); }\n"); + printf("static void skipUntil14() { _executor.scriptSkipUntil14(); }\n"); + printf("static int ifInteraction(Value i, Value a, Value b) { _executor.scriptIfInteraction(); return 0; }\n"); + printf("static void nop() { _executor.scriptNop09(); }\n\n"); + + for (int scene = startScene; scene <= endScene; scene++) { + if (loadSceneScript(f, (uint16_t)scene)) { + disassembleC(scene, "scene"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + if (objectsMode) { + for (int obj = 1; obj <= 0x200; obj++) { + uint16_t objScene = getObjectSceneIndex(f, (uint16_t)obj); + if (objScene != (uint16_t)scene) + continue; + if (loadObjectScript(f, (uint16_t)obj)) { + printf("\n"); + disassembleC(obj, "object"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + } + } + } + if (objectsMode) { + for (int obj = 1; obj <= 0x200; obj++) { + uint16_t objScene = getObjectSceneIndex(f, (uint16_t)obj); + if (objScene != 0) + continue; + if (loadObjectScript(f, (uint16_t)obj)) { + printf("\n"); + disassembleC(obj, "inventory"); + free(scriptData); + scriptData = nullptr; + scriptSize = 0; + free(stringData); + stringData = nullptr; + stringDataSize = 0; + } + } + } + } else if (jsonMode) { printf("{\"scenes\": [\n"); bool first = true; for (int scene = startScene; scene <= endScene; scene++) { diff --git a/engines/macs2/extract_macs2.cpp b/engines/macs2/extract_macs2.cpp index 48a96fd0..60b7a5a7 100644 --- a/engines/macs2/extract_macs2.cpp +++ b/engines/macs2/extract_macs2.cpp @@ -992,9 +992,386 @@ static void extractItems(const char *outDir) { printf("Extracted %d inventory item icons.\n", count); } +// ============================================================================ +// Amiga PP20 (PowerPacker) decompression +// ============================================================================ + +static bool pp20Decompress(const uint8_t *src, uint32_t srcLen, uint8_t *dst, uint32_t dstLen) { + if (srcLen < 12 || memcmp(src, "PP20", 4) != 0) + return false; + + const uint8_t *eff = src + 4; // efficiency table (4 bytes) + const uint8_t *packed = src + 8; + uint32_t packedLen = srcLen - 12; // exclude magic(4) + eff(4) + trailer(4) + + // Trailer: last 4 bytes = decrunch info + const uint8_t *trailer = src + srcLen - 4; + uint32_t origSize = ((uint32_t)trailer[0] << 16) | ((uint32_t)trailer[1] << 8) | trailer[2]; + uint8_t bitrot = trailer[3]; + + if (origSize != dstLen || origSize == 0) + return false; + + // Build bit-reverse table + uint8_t rev[256]; + for (int a = 0; a < 256; a++) { + uint8_t b = (uint8_t)a; + b = (uint8_t)(((b & 0x0f) << 4) | ((b >> 4) & 0x0f)); + b = (uint8_t)(((b & 0x33) << 2) | ((b >> 2) & 0x33)); + b = (uint8_t)(((b & 0x55) << 1) | ((b >> 1) & 0x55)); + rev[a] = b; + } + + uint32_t outPos = dstLen; + int32_t inPos = (int32_t)packedLen; + uint32_t code = 0; + uint32_t shift = 32; + + // Lambda-like macros for bit reading +#define PP_PEEK(x) \ + while (shift > 32u - (x)) { \ + if (inPos <= 0) goto pp_done; \ + shift -= 8; \ + inPos--; \ + code += (uint32_t)rev[packed[inPos]] << shift; \ + } + +#define PP_SHIFT(x) do { shift += (x); code = (code << (x)) & 0xFFFFFFFF; } while(0) + + PP_PEEK(bitrot); + PP_SHIFT(bitrot); + + while (outPos > 0) { + PP_PEEK(1); + uint32_t bit = code >> 31; + PP_SHIFT(1); + + if (bit == 0) { + // Literal run + PP_PEEK(2); + uint32_t length = (code >> 30) + 1; + PP_SHIFT(2); + if (length == 4) { + for (;;) { + PP_PEEK(2); + uint32_t b2 = code >> 30; + PP_SHIFT(2); + length += b2; + if (b2 != 3) break; + } + } + for (uint32_t i = 0; i < length && outPos > 0; i++) { + PP_PEEK(8); + outPos--; + dst[outPos] = (uint8_t)(code >> 24); + PP_SHIFT(8); + } + if (outPos == 0) break; + } + + // LZ match + PP_PEEK(2); + uint32_t cv = code >> 30; + PP_SHIFT(2); + + uint32_t length, nbits; + if (cv == 0) { length = 2; nbits = eff[0]; } + else if (cv == 1) { length = 3; nbits = eff[1]; } + else if (cv == 2) { length = 4; nbits = eff[2]; } + else { + PP_PEEK(1); + uint32_t extra = code >> 31; + PP_SHIFT(1); + length = 5; + nbits = (extra == 0) ? 7 : eff[3]; + } + + PP_PEEK(nbits); + uint32_t ptr = (code >> (32 - nbits)) + 1; + PP_SHIFT(nbits); + + if (length == 5) { + for (;;) { + PP_PEEK(3); + uint32_t b3 = code >> 29; + PP_SHIFT(3); + length += b3; + if (b3 != 7) break; + } + } + + for (uint32_t i = 0; i < length && outPos > 0; i++) { + outPos--; + dst[outPos] = dst[outPos + ptr]; + } + } + +pp_done: +#undef PP_PEEK +#undef PP_SHIFT + return outPos == 0; +} + +// Get decompressed size from PP20 trailer +static uint32_t pp20GetOrigSize(const uint8_t *data, uint32_t len) { + if (len < 12) return 0; + const uint8_t *t = data + len - 4; + return ((uint32_t)t[0] << 16) | ((uint32_t)t[1] << 8) | t[2]; +} + +// ============================================================================ +// Amiga DataA (MXMF) + Mdir (MXDR) extraction +// ============================================================================ + +static uint32_t readU32BE(const uint8_t *p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; +} + +static uint16_t readU16BE(const uint8_t *p) { + return ((uint16_t)p[0] << 8) | p[1]; +} + +static bool isAmigaDataDir(const char *path) { + // Check if path is a directory containing DataA and Mdir + std::string dataA = std::string(path) + "/DataA"; + std::string mdir = std::string(path) + "/Mdir"; + struct stat st; + return (stat(dataA.c_str(), &st) == 0 && S_ISREG(st.st_mode) && + stat(mdir.c_str(), &st) == 0 && S_ISREG(st.st_mode)); +} + +static int extractAmiga(const char *gameDir, const char *outDir) { + std::string dataAPath = std::string(gameDir) + "/DataA"; + std::string mdirPath = std::string(gameDir) + "/Mdir"; + + // Read Mdir + FILE *mf = fopen(mdirPath.c_str(), "rb"); + if (!mf) { + fprintf(stderr, "Error: Cannot open '%s'\n", mdirPath.c_str()); + return 1; + } + fseek(mf, 0, SEEK_END); + uint32_t mdirSize = (uint32_t)ftell(mf); + fseek(mf, 0, SEEK_SET); + std::vector mdirData(mdirSize); + fread(mdirData.data(), 1, mdirSize, mf); + fclose(mf); + + if (mdirSize < 18 || memcmp(mdirData.data(), "MXDR", 4) != 0) { + fprintf(stderr, "Error: '%s' is not a valid MXDR file\n", mdirPath.c_str()); + return 1; + } + + uint16_t dirEntryCount = (uint16_t)((mdirSize - 18) / 10); + printf("Mdir: %u directory entries\n", dirEntryCount); + + // Parse directory entries (offset 18, each 10 bytes: type(2) + id(2) + disk(2) + offset(4)) + struct DirEntry { + char type[3]; + uint16_t id; + uint16_t disk; + uint32_t offset; + }; + std::vector entries(dirEntryCount); + for (uint16_t i = 0; i < dirEntryCount; i++) { + const uint8_t *p = mdirData.data() + 18 + i * 10; + entries[i].type[0] = (char)p[0]; + entries[i].type[1] = (char)p[1]; + entries[i].type[2] = '\0'; + entries[i].id = readU16BE(p + 2); + entries[i].disk = readU16BE(p + 4); + entries[i].offset = readU32BE(p + 6); + } + + // Open DataA + FILE *df = fopen(dataAPath.c_str(), "rb"); + if (!df) { + fprintf(stderr, "Error: Cannot open '%s'\n", dataAPath.c_str()); + return 1; + } + fseek(df, 0, SEEK_END); + uint32_t dataASize = (uint32_t)ftell(df); + fseek(df, 0, SEEK_SET); + + // Read and validate MXMF header (14 bytes) + uint8_t mxmfHdr[14]; + fread(mxmfHdr, 1, 14, df); + if (memcmp(mxmfHdr, "MXMF", 4) != 0) { + fprintf(stderr, "Error: '%s' is not a valid MXMF file\n", dataAPath.c_str()); + fclose(df); + return 1; + } + uint16_t totalResources = readU16BE(mxmfHdr + 8); + uint32_t firstBlockSize = readU32BE(mxmfHdr + 10); + printf("DataA: %u resources, file size=%u\n", totalResources, dataASize); + + mkdirp(outDir); + + // Extract first PP20 block (scene table) at offset 14 + { + std::vector pp(firstBlockSize); + fseek(df, 14, SEEK_SET); + fread(pp.data(), 1, firstBlockSize, df); + + uint32_t origSize = pp20GetOrigSize(pp.data(), firstBlockSize); + if (origSize > 0 && memcmp(pp.data(), "PP20", 4) == 0) { + std::vector dec(origSize); + if (pp20Decompress(pp.data(), firstBlockSize, dec.data(), origSize)) { + char path[512]; + snprintf(path, sizeof(path), "%s/scene_table.bin", outDir); + writeRawFile(path, dec.data(), origSize); + printf(" scene_table.bin: %u -> %u bytes\n", firstBlockSize, origSize); + } else { + fprintf(stderr, " WARN: scene_table PP20 decompression failed\n"); + } + } + } + + // Default 32-color Amiga palette (grayscale fallback for sprites without scene palette) + uint8_t defaultPalette[768]; + for (int i = 0; i < 256; i++) { + uint8_t v = (uint8_t)((i < 32) ? (i * 255 / 31) : 0); + defaultPalette[i * 3 + 0] = v; + defaultPalette[i * 3 + 1] = v; + defaultPalette[i * 3 + 2] = v; + } + + // Extract all resources sequentially from offset 14 + firstBlockSize + fseek(df, 14 + firstBlockSize, SEEK_SET); + int extracted = 1; // counting scene_table + int images = 0; + for (uint16_t i = 0; i < totalResources - 1; i++) { + long entryStart = ftell(df); + uint8_t ehdr[8]; + if (fread(ehdr, 1, 8, df) != 8) + break; + + char eType[3] = {(char)ehdr[0], (char)ehdr[1], '\0'}; + uint16_t eId = readU16BE(ehdr + 2); + uint32_t eCompSize = readU32BE(ehdr + 4); + + if (eCompSize == 0 || eCompSize > dataASize) { + fprintf(stderr, " WARN: bad entry at offset 0x%lx, stopping\n", entryStart); + break; + } + + std::vector payload(eCompSize); + if (fread(payload.data(), 1, eCompSize, df) != eCompSize) + break; + + char fname[64]; + char path[512]; + + if (memcmp(payload.data(), "PP20", 4) == 0) { + // Direct PP20 block + uint32_t origSize = pp20GetOrigSize(payload.data(), eCompSize); + if (origSize > 0) { + std::vector dec(origSize); + if (pp20Decompress(payload.data(), eCompSize, dec.data(), origSize)) { + snprintf(fname, sizeof(fname), "%s_%04d.bin", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, dec.data(), origSize); + printf(" %s: %u -> %u bytes\n", fname, eCompSize, origSize); + + // Try to extract sprite image from MXOO + if (origSize >= 32 && memcmp(dec.data(), "MXOO", 4) == 0) { + uint32_t scriptOff = readU32BE(dec.data() + 4); + // Sprite body: from offset 12 to scriptOff + if (scriptOff > 32 && scriptOff <= origSize) { + const uint8_t *body = dec.data() + 12; + uint32_t bodyLen = scriptOff - 12; + // Check sprite signature: body[10:12]=0x0101, body[14:16]>0 + if (bodyLen >= 20 && body[10] == 1 && body[11] == 1) { + uint16_t frames = readU16BE(body + 14); + uint16_t w = readU16BE(body + 16); + uint16_t h = readU16BE(body + 18); + uint32_t rowBytes = (w + 7) / 8; + uint32_t planeSize = rowBytes * h; + uint32_t pixelBytes = bodyLen - 20; + uint32_t numPlanes = (planeSize > 0) ? pixelBytes / planeSize : 0; + if (w > 0 && w <= 320 && h > 0 && h <= 200 && + frames > 0 && numPlanes >= 5 && numPlanes <= 6 && + pixelBytes == planeSize * numPlanes) { + // Convert planar to chunky (5 color planes) + std::vector pixels(w * h, 0); + const uint8_t *pixData = body + 20; + for (uint32_t plane = 0; plane < 5 && plane < numPlanes; plane++) { + uint32_t pOff = plane * planeSize; + for (uint16_t y = 0; y < h; y++) { + for (uint32_t bx = 0; bx < rowBytes; bx++) { + uint8_t b = pixData[pOff + y * rowBytes + bx]; + for (int bit = 0; bit < 8; bit++) { + uint16_t x = (uint16_t)(bx * 8 + bit); + if (x < w && (b & (0x80 >> bit))) + pixels[y * w + x] |= (1 << plane); + } + } + } + } + snprintf(fname, sizeof(fname), "%s_%04d.bmp", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeBMPEx(path, pixels.data(), w, h, defaultPalette); + images++; + } + } + } + } + } else { + snprintf(fname, sizeof(fname), "%s_%04d.pp20", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, payload.data(), eCompSize); + printf(" %s: %u bytes (decompress failed)\n", fname, eCompSize); + } + } + } else if (memcmp(payload.data(), "MXMM", 4) == 0 && eCompSize > 14) { + // Music sub-container: PP20 size at offset 10 + uint32_t ppSize = readU32BE(payload.data() + 10); + if (ppSize > 0 && ppSize <= eCompSize - 14 && memcmp(payload.data() + 14, "PP20", 4) == 0) { + uint32_t origSize = pp20GetOrigSize(payload.data() + 14, ppSize); + if (origSize > 0) { + std::vector dec(origSize); + if (pp20Decompress(payload.data() + 14, ppSize, dec.data(), origSize)) { + snprintf(fname, sizeof(fname), "%s_%04d.bin", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, dec.data(), origSize); + printf(" %s (music): %u -> %u bytes\n", fname, ppSize, origSize); + } else { + snprintf(fname, sizeof(fname), "%s_%04d.mxmm", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, payload.data(), eCompSize); + printf(" %s: %u bytes (music decompress failed)\n", fname, eCompSize); + } + } + } else { + snprintf(fname, sizeof(fname), "%s_%04d.mxmm", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, payload.data(), eCompSize); + printf(" %s: %u bytes (raw music container)\n", fname, eCompSize); + } + } else if (memcmp(payload.data(), "MXOO", 4) == 0) { + // Uncompressed object sub-container + snprintf(fname, sizeof(fname), "%s_%04d.mxoo", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, payload.data(), eCompSize); + printf(" %s: %u bytes (uncompressed object)\n", fname, eCompSize); + } else { + // Unknown format, save raw + snprintf(fname, sizeof(fname), "%s_%04d.raw", eType, eId); + snprintf(path, sizeof(path), "%s/%s", outDir, fname); + writeRawFile(path, payload.data(), eCompSize); + printf(" %s: %u bytes (unknown)\n", fname, eCompSize); + } + extracted++; + } + + fclose(df); + printf("\nExtracted %d resources (%d sprite images) to %s/\n", extracted, images, outDir); + return 0; +} + static void printHelp(const char *bin) { printf("MACS2 Resource Extractor\n\n"); - printf("Usage: %s [scene_index]\n\n", bin); + printf("Usage: %s [scene_index]\n\n", bin); printf("Modes:\n"); printf(" images - Extract background images as BMP files\n"); printf(" sounds - Extract sound/music resource blobs\n"); @@ -1005,6 +1382,12 @@ static void printHelp(const char *bin) { printf(" helpimages - Extract help/map panel images as BMP\n"); printf(" all - Extract everything\n"); printf("\n"); + printf("The game_data_path can be:\n"); + printf(" - RESOURCE.MCS file (DOS version)\n"); + printf(" - Directory containing RESOURCE.MCS (DOS version)\n"); + printf(" - Directory containing DataA + Mdir (Amiga version)\n"); + printf("\n"); + printf("Amiga mode auto-detects and extracts all PP20-compressed resources.\n"); printf("If scene_index is omitted, extracts from all scenes.\n"); } @@ -1018,9 +1401,15 @@ int main(int argc, char **argv) { const char *resPath = argv[2]; const char *outDir = argv[3]; + // Check if this is an Amiga data directory (contains DataA + Mdir) + struct stat st; + if (stat(resPath, &st) == 0 && S_ISDIR(st.st_mode) && isAmigaDataDir(resPath)) { + printf("Detected Amiga data directory.\n"); + return extractAmiga(resPath, outDir); + } + // If path is a directory, look for RESOURCE.MCS inside it std::string resolvedPath = resPath; - struct stat st; if (stat(resPath, &st) == 0 && S_ISDIR(st.st_mode)) { resolvedPath = std::string(resPath) + "/RESOURCE.MCS"; }