From f04ff1a7ababb95c46417212c8b68970f73c9012 Mon Sep 17 00:00:00 2001 From: Martin Gerhardy Date: Wed, 21 Oct 2026 18:52:58 +0200 Subject: [PATCH] MACS2: support for adding multiple translation dat files to the game dir even the demo supports those translations now - except the one demo-only chapter at the end --- dists/engine-data/README | 3 - dists/engine-data/engine_data.mk | 3 - dists/engine-data/macs2_translation.dat | Bin 183 -> 0 bytes dists/irix/scummvm.idb | 1 - engines/macs2/actionbar.cpp | 2 +- engines/macs2/detection.h | 5 +- engines/macs2/detection_tables.h | 14 +-- engines/macs2/dialogs.cpp | 40 ++++++++- engines/macs2/dialogs.h | 4 + engines/macs2/macs2.cpp | 55 ++++++++---- engines/macs2/module.mk | 1 + engines/macs2/text.cpp | 15 ++++ engines/macs2/translation.cpp | 113 ++++++++++++++++++++++++ engines/macs2/translation.h | 46 ++++++++++ 14 files changed, 258 insertions(+), 44 deletions(-) delete mode 100644 dists/engine-data/macs2_translation.dat create mode 100644 engines/macs2/translation.cpp create mode 100644 engines/macs2/translation.h diff --git a/dists/engine-data/README b/dists/engine-data/README index 7b3b202f176..15c2dec6087 100644 --- a/dists/engine-data/README +++ b/dists/engine-data/README @@ -81,9 +81,6 @@ scripts, from. lure.dat: This file contains essential game data used by Lure of the Temptress game. -macs2_translation.dat: -This file contains game translations for the MACS2 engine. - macgui.dat: The file contains set of offsets for recreation of Classic Mac OS GUI, used by Director, MacVenture and WAGE engines. diff --git a/dists/engine-data/engine_data.mk b/dists/engine-data/engine_data.mk index e93eb634dcc..25341375dbd 100644 --- a/dists/engine-data/engine_data.mk +++ b/dists/engine-data/engine_data.mk @@ -50,9 +50,6 @@ endif ifdef ENABLE_PRINCE DIST_FILES_LIST += dists/engine-data/prince_translation.dat endif -ifdef ENABLE_MACS2 -DIST_FILES_LIST += dists/engine-data/macs2_translation.dat -endif ifdef ENABLE_QUEEN DIST_FILES_LIST += dists/engine-data/queen.tbl endif diff --git a/dists/engine-data/macs2_translation.dat b/dists/engine-data/macs2_translation.dat deleted file mode 100644 index 61df7641bbb1a33ee176847fe8235b297f96e709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmWlSyAHxI5Cks;f|f^8FF%lik`5suQBXlcY48Py7+X5qSpIuVwav`z<#^pS)DY2( z7PP9{(Md*jI&5+G37-@qjQVm*$_gu8Fgc$cvMDG&wG_Rd5Dmk6_rlvJaa=ZJSh&Z* zX`Jy?4w0?xs2cl!SHQ#IB?jct+9f?0xdIT*N;K.dat msgctxt "uilabel". Common::String uiText(const char *source) { if (g_engine) return g_engine->translateUiLabel(source); diff --git a/engines/macs2/detection.h b/engines/macs2/detection.h index f2605fa2ebe..5733f38d141 100644 --- a/engines/macs2/detection.h +++ b/engines/macs2/detection.h @@ -53,10 +53,7 @@ extern const ADGameDescription gameDescriptions[]; #define GAMEOPTION_ORIGINAL_SAVELOAD GUIO_GAMEOPTIONS1 #define GAMEOPTION_TTS GUIO_GAMEOPTIONS2 #define GAMEOPTION_ENHANCEMENTS GUIO_GAMEOPTIONS3 - -enum { - GF_TRANSLATED = 1 << 0, -}; +#define GAMEOPTION_TRANSLATION GUIO_GAMEOPTIONS4 } // End of namespace Macs2 diff --git a/engines/macs2/detection_tables.h b/engines/macs2/detection_tables.h index b68607c592e..f7254e8b276 100644 --- a/engines/macs2/detection_tables.h +++ b/engines/macs2/detection_tables.h @@ -36,7 +36,7 @@ const ADGameDescription gameDescriptions[] = { Common::DE_DEU, Common::kPlatformDOS, ADGF_TESTING, - GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)}, + GUIO4(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_TRANSLATION)}, // GMACS II Interpreter V1.00 - Written & Copyright (C) 1993 by Arndt Hasch - Copyright by LINEL {"sis", @@ -45,17 +45,7 @@ const ADGameDescription gameDescriptions[] = { Common::DE_DEU, Common::kPlatformDOS, ADGF_DEMO | ADGF_TESTING, - GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)}, - - // English fan translation (original + macs2_translation.dat) - {"sis", - "w/translation", - AD_ENTRY2s("RESOURCE.MCS", "b8646d5cc2e45442a414220b1a65ba73", 8621636, - "macs2_translation.dat", nullptr, AD_NO_SIZE), - Common::EN_ANY, - Common::kPlatformDOS, - GF_TRANSLATED | ADGF_TESTING, - GUIO3(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS)}, + GUIO4(GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_TTS, GAMEOPTION_ENHANCEMENTS, GAMEOPTION_TRANSLATION)}, // Amiga demo - DataA (MXMF) + Mdir (MXDR). GMACS II / LINEL. {"sis", diff --git a/engines/macs2/dialogs.cpp b/engines/macs2/dialogs.cpp index ac3d15e769c..4eef49ca9a9 100644 --- a/engines/macs2/dialogs.cpp +++ b/engines/macs2/dialogs.cpp @@ -21,15 +21,27 @@ #include "macs2/dialogs.h" #include "common/config-manager.h" +#include "common/gui_options.h" #include "common/translation.h" #include "engines/enhancements.h" #include "gui/widget.h" +#include "macs2/detection.h" namespace Macs2 { Macs2OptionsWidget::Macs2OptionsWidget(GuiObject *boss, const Common::String &name, const Common::String &domain) : OptionsContainerWidget(boss, name, "Macs2GameOptionsDialog", domain) { + if (Common::checkGameGUIOption(GAMEOPTION_TRANSLATION, ConfMan.get("guioptions", domain))) { + GUI::StaticTextWidget *label = new GUI::StaticTextWidget(widgetsBoss(), _dialogLayout + ".translationLabel", _("Translation:")); + label->setAlign(Graphics::kTextAlignRight); + _translationPopUp = new GUI::PopUpWidget(widgetsBoss(), _dialogLayout + ".translation"); + _translationPopUp->appendEntry(_("Original"), 0); + _translationPackages = listTranslationPackages(Common::FSNode(ConfMan.getPath("path", domain)), ConfMan.get("gameid", domain)); + for (uint i = 0; i < _translationPackages.size(); ++i) + _translationPopUp->appendEntry(_translationPackages[i].displayName, i + 1); + } + _originalMenusCheckbox = new GUI::CheckboxWidget(widgetsBoss(), _dialogLayout + ".originalMenus", _("Use original save/load screens"), _("Use the original save/load screens instead of the ScummVM ones")); @@ -77,6 +89,17 @@ Macs2OptionsWidget::Macs2OptionsWidget(GuiObject *boss, const Common::String &na } void Macs2OptionsWidget::load() { + if (_translationPopUp) { + _translationPopUp->setSelectedTag(0); + const Common::String code = ConfMan.get(kTranslationConfigKey, _domain); + for (uint i = 0; i < _translationPackages.size(); ++i) { + if (_translationPackages[i].code.equalsIgnoreCase(code)) { + _translationPopUp->setSelectedTag(i + 1); + break; + } + } + } + _originalMenusCheckbox->setState(ConfMan.getBool("original_menus", _domain)); #ifdef USE_TTS @@ -112,6 +135,14 @@ void Macs2OptionsWidget::load() { } bool Macs2OptionsWidget::save() { + if (_translationPopUp) { + const uint32 selected = _translationPopUp->getSelectedTag(); + if (selected > 0 && selected <= _translationPackages.size()) + ConfMan.set(kTranslationConfigKey, _translationPackages[selected - 1].code, _domain); + else + ConfMan.removeKey(kTranslationConfigKey, _domain); + } + ConfMan.setBool("original_menus", _originalMenusCheckbox->getState(), _domain); #ifdef USE_TTS @@ -153,7 +184,14 @@ bool Macs2OptionsWidget::save() { void Macs2OptionsWidget::defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const { layouts.addDialog(layoutName, overlayedLayout) .addLayout(GUI::ThemeLayout::kLayoutVertical, 5) - .addPadding(0, 0, 0, 0) + .addPadding(0, 0, 0, 0); + if (_translationPopUp) { + layouts.addLayout(GUI::ThemeLayout::kLayoutHorizontal) + .addWidget("translationLabel", "OptionsLabel") + .addWidget("translation", "PopUp") + .closeLayout(); + } + layouts .addWidget("originalMenus", "Checkbox") #ifdef USE_TTS .addWidget("ttsEnabled", "Checkbox") diff --git a/engines/macs2/dialogs.h b/engines/macs2/dialogs.h index a2d791d7b38..dcf2a086fb9 100644 --- a/engines/macs2/dialogs.h +++ b/engines/macs2/dialogs.h @@ -24,6 +24,8 @@ #include "gui/ThemeEval.h" #include "gui/widget.h" +#include "gui/widgets/popup.h" +#include "macs2/translation.h" namespace Macs2 { @@ -46,6 +48,8 @@ private: void defineLayout(GUI::ThemeEval &layouts, const Common::String &layoutName, const Common::String &overlayedLayout) const override; GUI::CheckboxWidget *_originalMenusCheckbox = nullptr; + GUI::PopUpWidget *_translationPopUp = nullptr; + Common::Array _translationPackages; #ifdef USE_TTS GUI::CheckboxWidget *_ttsCheckbox = nullptr; #endif diff --git a/engines/macs2/macs2.cpp b/engines/macs2/macs2.cpp index e91fcdcfad7..b92162da54b 100644 --- a/engines/macs2/macs2.cpp +++ b/engines/macs2/macs2.cpp @@ -48,6 +48,7 @@ #include "macs2/detection.h" #include "macs2/hotspot_names.h" #include "macs2/music.h" +#include "macs2/translation.h" #include "macs2/view1.h" namespace Macs2 { @@ -2455,9 +2456,23 @@ int Macs2Engine::computeStringIndex(Common::MemoryReadStream *stream, int target } void Macs2Engine::loadTranslation() { - Common::SeekableReadStream *f = SearchMan.createReadStreamForMember("macs2_translation.dat"); + const Common::String code = ConfMan.get(kTranslationConfigKey); + if (code.empty() || isAmiga()) + return; + + const Common::FSNode gameDataDir(ConfMan.getPath("path")); + const Common::Array packages = listTranslationPackages(gameDataDir, getGameId()); + Common::String fileName; + for (const TranslationPackage &package : packages) { + if (package.code.equalsIgnoreCase(code)) { + fileName = package.fileName; + break; + } + } + + Common::SeekableReadStream *f = fileName.empty() ? nullptr : gameDataDir.getChild(fileName).createReadStream(); if (!f) { - warning("Cannot open macs2_translation.dat"); + warning("Cannot open %s translation package for '%s'", getGameId().c_str(), code.c_str()); return; } @@ -2465,14 +2480,14 @@ void Macs2Engine::loadTranslation() { char magic[4]; f->read(magic, 4); if (memcmp(magic, "MCS2", 4) != 0) { - warning("Invalid macs2_translation.dat magic"); + warning("Invalid %s magic", fileName.c_str()); delete f; return; } const uint16 version = f->readUint16LE(); if (version != 1) { - warning("Unsupported macs2_translation.dat version %u", version); + warning("Unsupported %s version %u", fileName.c_str(), version); delete f; return; } @@ -2515,7 +2530,7 @@ void Macs2Engine::loadTranslation() { for (uint16 k = 0; k < len; k++) { s += (char)f->readByte(); } - entry.strings.push_back(s); + entry.strings.push_back(normalizeTranslationString(s)); } stringDataEnd = MAX(stringDataEnd, (uint32)f->pos()); _sceneTranslations[sceneIndex[i].id] = entry; @@ -2531,7 +2546,7 @@ void Macs2Engine::loadTranslation() { for (uint16 k = 0; k < len; k++) { s += (char)f->readByte(); } - entry.strings.push_back(s); + entry.strings.push_back(normalizeTranslationString(s)); } stringDataEnd = MAX(stringDataEnd, (uint32)f->pos()); _objectTranslations[objectIndex[i].id] = entry; @@ -2550,7 +2565,7 @@ void Macs2Engine::loadTranslation() { val += (char)f->readByte(); } if (!key.empty() && !val.empty()) { - out[key] = val; + out[key] = normalizeTranslationString(val); } } }; @@ -2564,12 +2579,12 @@ void Macs2Engine::loadTranslation() { readLabelMap(numUiLabels, _uiLabelTranslations); delete f; - debug("Loaded macs2_translation.dat: %u scenes, %u objects, %u overlay labels, %u UI labels", - numScenes, numObjects, (uint)_hotspotLabelTranslations.size(), (uint)_uiLabelTranslations.size()); + debug("Loaded %s: %u scenes, %u objects, %u overlay labels, %u UI labels", + fileName.c_str(), numScenes, numObjects, (uint)_hotspotLabelTranslations.size(), (uint)_uiLabelTranslations.size()); } Common::String Macs2Engine::translateHotspotLabel(const Common::String &cp850Name) const { - if (cp850Name.empty() || !(getFeatures() & GF_TRANSLATED)) { + if (cp850Name.empty()) { return cp850Name; } auto it = _hotspotLabelTranslations.find(cp850Name); @@ -2580,7 +2595,7 @@ Common::String Macs2Engine::translateHotspotLabel(const Common::String &cp850Nam } Common::String Macs2Engine::translateUiLabel(const Common::String &source) const { - if (source.empty() || !(getFeatures() & GF_TRANSLATED)) { + if (source.empty()) { return source; } auto it = _uiLabelTranslations.find(source); @@ -2620,17 +2635,22 @@ Common::StringArray Macs2Engine::decodeStrings(Common::MemoryReadStream *stream, } // Apply translation if available - if (getFeatures() & GF_TRANSLATED) { + if (!_sceneTranslations.empty() || !_objectTranslations.empty()) { const int baseIndex = computeStringIndex(stream, offset); const TranslationEntry *entry = nullptr; - if (objectId != 0 && _objectTranslations.contains(objectId)) { - entry = &_objectTranslations[objectId]; + const int translationObjectId = isDemo() ? remapDemoTranslationObjectId(objectId) : objectId; + bool sceneTranslation = false; + if (translationObjectId != 0 && _objectTranslations.contains(translationObjectId)) { + entry = &_objectTranslations[translationObjectId]; } else if (sceneId != 0 && _sceneTranslations.contains(sceneId)) { entry = &_sceneTranslations[sceneId]; + sceneTranslation = true; } if (entry) { for (int i = 0; i < numStrings; i++) { - const int idx = baseIndex + i; + int idx = baseIndex + i; + if (isDemo() && sceneTranslation) + idx = remapDemoTranslationSceneStringIndex(sceneId, idx); if (idx >= 0 && idx < (int)entry->strings.size() && !entry->strings[idx].empty()) { result[i] = entry->strings[idx]; } @@ -3026,10 +3046,7 @@ Common::Error Macs2Engine::run() { loadBootstrapResources(); initInventoryIconIndices(); - // Load translation data if available - if (getFeatures() & GF_TRANSLATED) { - loadTranslation(); - } + loadTranslation(); // Initialize graphics mode (taller framebuffer when action bar verb UI is enabled) int gfxH = screenHeight(); diff --git a/engines/macs2/module.mk b/engines/macs2/module.mk index 5caf65419a5..edecd594a2a 100644 --- a/engines/macs2/module.mk +++ b/engines/macs2/module.mk @@ -18,6 +18,7 @@ MODULE_OBJS = \ saveload.o \ scriptexecutor.o \ text.o \ + translation.o \ actionbar.o \ view1.o diff --git a/engines/macs2/text.cpp b/engines/macs2/text.cpp index 5d44376083f..080c386850d 100644 --- a/engines/macs2/text.cpp +++ b/engines/macs2/text.cpp @@ -52,6 +52,21 @@ bool Text::findGlyph(char c, GlyphData &out, const GlyphData *glyphs, uint16 num } void Text::addDialogueFontFallbacks() { + GlyphData existingPunctuation, colon, comma; + if (_numGlyphs < kMaxGlyphs && !findGlyph(';', existingPunctuation) && + findGlyph(':', colon) && findGlyph(',', comma) && + colon._width == comma._width && colon._height > comma._height / 2) { + // Combine the colon's upper dot with the comma's tail. + GlyphData &semicolon = _glyphs[_numGlyphs++]; + semicolon = comma; + semicolon._ascii = ';'; + const uint rows = comma._height / 2 + 1; + for (uint y = 0; y < rows; ++y) { + for (uint x = 0; x < comma._width; ++x) + semicolon._data[y * comma._width + x] = colon._data[y * colon._width + x]; + } + } + // Swedish CP850 characters missing from the original DOS dialogue font. // Match its transparent/outline/foreground palette entries and cell // dimensions to preserve text alignment and line spacing. diff --git a/engines/macs2/translation.cpp b/engines/macs2/translation.cpp new file mode 100644 index 00000000000..caefcffa80e --- /dev/null +++ b/engines/macs2/translation.cpp @@ -0,0 +1,113 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM 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 . + * + */ + +#include "macs2/translation.h" +#include "common/algorithm.h" +#include "common/language.h" +#include "common/ptr.h" +#include "common/stream.h" + +namespace Macs2 { + +Common::String normalizeTranslationString(const Common::String &text) { + Common::String result; + for (char c : text) { + if ((byte)c != 0xf0) // CP850 soft hyphen; also used to pad unused dialogue lines. + result += c; + } + // An empty DAT entry falls back to German; keep an intentional blank nonempty. + if (!text.empty() && result.empty()) + result = " "; + return result; +} + +uint16 remapDemoTranslationObjectId(uint16 objectId) { + // The DOS demo renumbers inventory objects, retaining the NPC IDs. + static const uint16 fullGameIds[] = { + 0, 1, 85, 87, 96, 97, 6, 98, 99, 101, 102, 103, 12, 107, 108, 15, + 109, 113, 114, 19, 115, 117, 118, 119, 120, 126, 130, 134, 138, 139, 141 + }; + return objectId < ARRAYSIZE(fullGameIds) ? fullGameIds[objectId] : objectId; +} + +int remapDemoTranslationSceneStringIndex(int sceneId, int stringIndex) { + // The DOS demo omits some full-game dialogue and inserts a chapter-end message. + if (sceneId == 30 && stringIndex == 249) + return -1; + + static const struct { + int sceneId; + int firstString; + int offset; + } ranges[] = { + {30, 250, -1}, {30, 297, 1}, {30, 299, 3}, {30, 304, 5}, + {30, 308, 23}, {30, 313, 26}, {30, 318, 32}, + {31, 77, 17}, {31, 80, 25}, + {32, 3, 2}, {32, 4, 5}, {32, 66, 10}, {32, 70, 17}, + {32, 73, 21}, {32, 75, 25}, + {33, 6, 3}, {33, 82, 11} + }; + int offset = 0; + for (const auto &range : ranges) { + if (range.sceneId == sceneId && stringIndex >= range.firstString) + offset = range.offset; + } + return stringIndex + offset; +} + +Common::Array listTranslationPackages(const Common::FSNode &gameDataDir, const Common::String &gameId) { + Common::Array packages; + Common::FSList files; + if (!gameDataDir.getChildren(files, Common::FSNode::kListFilesOnly)) + return packages; + + Common::String prefix = gameId + "-translation-"; + prefix.toLowercase(); + const Common::String suffix = ".dat"; + for (const Common::FSNode &file : files) { + Common::String name = file.getName(); + name.toLowercase(); + if (!name.hasPrefix(prefix) || !name.hasSuffix(suffix)) + continue; + + const Common::String code = name.substr(prefix.size(), name.size() - prefix.size() - suffix.size()); + if (code.empty()) + continue; + + Common::ScopedPtr stream(file.createReadStream()); + if (!stream || stream->size() < 14 || stream->readUint32BE() != MKTAG('M', 'C', 'S', '2') || stream->readUint16LE() != 1) + continue; + + TranslationPackage package; + package.code = code; + const char *description = Common::getLanguageDescription(Common::parseLanguage(code)); + package.displayName = description ? description : code; + package.fileName = file.getName(); + packages.push_back(package); + } + + Common::sort(packages.begin(), packages.end(), [](const TranslationPackage &a, const TranslationPackage &b) { + return a.code < b.code; + }); + return packages; +} + +} // End of namespace Macs2 diff --git a/engines/macs2/translation.h b/engines/macs2/translation.h new file mode 100644 index 00000000000..045aed50696 --- /dev/null +++ b/engines/macs2/translation.h @@ -0,0 +1,46 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM 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 . + * + */ + +#ifndef MACS2_TRANSLATION_H +#define MACS2_TRANSLATION_H + +#include "common/array.h" +#include "common/fs.h" +#include "common/str.h" + +namespace Macs2 { + +static const char *const kTranslationConfigKey = "macs2_translation"; + +struct TranslationPackage { + Common::String code; + Common::String displayName; + Common::String fileName; +}; + +Common::Array listTranslationPackages(const Common::FSNode &gameDataDir, const Common::String &gameId); +Common::String normalizeTranslationString(const Common::String &text); +uint16 remapDemoTranslationObjectId(uint16 objectId); +int remapDemoTranslationSceneStringIndex(int sceneId, int stringIndex); + +} // End of namespace Macs2 + +#endif // MACS2_TRANSLATION_H