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
This commit is contained in:
Martin Gerhardy committed 2026-10-21 19:04:03 +02:00
1 parent cdbe4b55eb
commit f04ff1a7ab
14 files changed
+258 -44

No files matched your search

-3
View File
@@ -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.
-3
View File
@@ -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
Binary file not shown.
-1
View File
@@ -41,7 +41,6 @@ f 0644 root sys usr/ScummVM/share/scummvm/kyra.dat kyra.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/lure.dat lure.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/macgui.dat macgui.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/macventure.dat macventure.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/macs2_translation.dat macs2_translation.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/monkey4-patch.m4b monkey4-patch.m4b scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/mort.dat mort.dat scummvm.sw.eoe
f 0644 root sys usr/ScummVM/share/scummvm/myst3.dat myst3.dat scummvm.sw.eoe
+1 -1
View File
@@ -37,7 +37,7 @@ namespace Macs2 {
namespace {
// German source keys for macs2_translation.dat msgctxt "uilabel".
// German source keys for sis-translation-<lang>.dat msgctxt "uilabel".
Common::String uiText(const char *source) {
if (g_engine)
return g_engine->translateUiLabel(source);
+1 -4
View File
@@ -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
+2 -12
View File
@@ -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",
+39 -1
View File
@@ -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")
+4
View File
@@ -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<TranslationPackage> _translationPackages;
#ifdef USE_TTS
GUI::CheckboxWidget *_ttsCheckbox = nullptr;
#endif
+36 -19
View File
@@ -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<TranslationPackage> 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();
+1
View File
@@ -18,6 +18,7 @@ MODULE_OBJS = \
saveload.o \
scriptexecutor.o \
text.o \
translation.o \
actionbar.o \
view1.o
+15
View File
@@ -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.
+113
View File
@@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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<TranslationPackage> listTranslationPackages(const Common::FSNode &gameDataDir, const Common::String &gameId) {
Common::Array<TranslationPackage> 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<Common::SeekableReadStream> 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
+46
View File
@@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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<TranslationPackage> 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