KYRA: Add Korean fan translation support for Hand of Fate

This commit is contained in:
Seokjun Kim
2026-03-24 13:40:33 +07:00
committed by athrxx
parent bb794fadbf
commit f49d5ca984
15 changed files with 357 additions and 28 deletions
+1
View File
@@ -96,6 +96,7 @@ const Game kyra2Games[] = {
{ kKyra2, kPlatformDOS, kTalkieVersion, ES_ESP },
{ kKyra2, kPlatformDOS, kTalkieVersion, HE_ISR },
{ kKyra2, kPlatformDOS, kTalkieVersion, CS_CZE },
{ kKyra2, kPlatformDOS, kTalkieVersion, KO_KOR },
{ kKyra2, kPlatformFMTowns, kNoSpecial, EN_ANY },
{ kKyra2, kPlatformFMTowns, kNoSpecial, JA_JPN },
+3
View File
@@ -98,6 +98,7 @@
#include "resources/hof_dos_cd_spanish.h"
#include "resources/hof_dos_cd_hebrew.h"
#include "resources/hof_dos_cd_czech.h"
#include "resources/hof_dos_cd_korean.h"
#include "resources/hof_fmtowns.h"
#include "resources/hof_fmtowns_english.h"
@@ -1374,6 +1375,8 @@ static const ResourceProvider resourceProviders[] = {
{ k2SeqplayTlkFiles, kKyra2, kPlatformDOS, kTalkieVersion, ES_ESP, &k2SeqplayTlkFilesDOSCDSpanishProvider },
{ k2SeqplayStrings, kKyra2, kPlatformDOS, kTalkieVersion, CS_CZE, &k2SeqplayStringsDOSCDCzechProvider },
{ k2SeqplayTlkFiles, kKyra2, kPlatformDOS, kTalkieVersion, CS_CZE, &k2SeqplayTlkFilesDOSCDCzechProvider },
{ k2SeqplayStrings, kKyra2, kPlatformDOS, kTalkieVersion, KO_KOR, &k2SeqplayStringsDOSCDKoreanProvider },
{ k2SeqplayTlkFiles, kKyra2, kPlatformDOS, kTalkieVersion, KO_KOR, &k2SeqplayTlkFilesDOSCDKoreanProvider },
{ k2SeqplayPakFiles, kKyra2, kPlatformFMTowns, kNoSpecial, UNK_LANG, &k2SeqplayPakFilesFMTownsProvider },
{ k2SeqplayStrings, kKyra2, kPlatformFMTowns, kNoSpecial, EN_ANY, &k2SeqplayStringsFMTownsEnglishProvider },
{ k2SeqplaySfxFiles, kKyra2, kPlatformFMTowns, kNoSpecial, UNK_LANG, &k2SeqplaySfxFilesFMTownsProvider },
@@ -0,0 +1,127 @@
static const char *const k2SeqplayStringsDOSCDKorean[104] = {
"\xC5\xB0\xB6\xF5\xB5\xF0\xBE\xC6\xB0\xA1 \xBB\xE7\xB6\xF3\xC1\xF6\xB0\xED \xC0\xD6\xB4\xD9!", // "키란디아가 사라지고 있다!"
"\xB9\xD9\xC0\xA7 \xC7\xCF\xB3\xAA\xC7\xCF\xB3\xAA...", // "바위 하나하나..."
"...\xB1\xD7\xB8\xAE\xB0\xED \xB3\xAA\xB9\xAB \xC7\xD1 \xB1\xD7\xB7\xE7 \xC7\xD1 \xB1\xD7\xB7\xE7.", // "...그리고 나무 한 그루 한 그루."
"\xC5\xB0\xB6\xF5\xB5\xF0\xBE\xC6\xB0\xA1 \xBC\xD2\xB8\xEA\xC7\xCF\xB0\xED \xC0\xD6\xB4\xD9!", // "키란디아가 소멸하고 있다!"
"\xBF\xD5\xB1\xC3 \xB8\xB6\xB9\xFD\xBB\xE7\xB5\xE9\xC0\xCC \xB4\xE7\xC8\xB2\xC7\xCF\xB0\xED \xC0\xD6\xB4\xD9.", // "왕궁 마법사들이 당황하고 있다."
"\xB8\xF0\xB5\xE7 \xC2\xFC\xB0\xED\xB9\xAE\xC7\xE5\xC0\xBB \xB4\xD9 \xC3\xA3\xBE\xC6\xBA\xC3\xB4\xD9.", // "모든 참고문헌을 다 찾아봤다."
"\xB8\xB6\xB8\xA3\xC4\xDA\xBF\xCD \xB1\xD7\xC0\xC7 \xBB\xF5 \xBD\xC3\xC1\xBE\xB5\xB5 \xC8\xB8\xC0\xC7\xBF\xA1 \xC2\xFC\xBC\xAE\xC0\xCC \xC7\xE3\xB6\xF4\xB5\xC7\xBE\xFA\xB4\xD9.", // "마르코와 그의 새 시종도 회의에 참석이 허락되었다."
"\xB4\xD9\xC7\xE0\xC8\xF7 \xBF\xEE\xB8\xED\xC0\xC7 \xBC\xD5\xC0\xBA \xC0\xCC\xB7\xB1 \xB9\xAE\xC1\xA6\xBF\xA1 \xB0\xE6\xC7\xE8\xC0\xCC \xC0\xD6\xBE\xFA\xB4\xD9.", // "다행히 운명의 손은 이런 문제에 경험이 있었다."
"\xB1\xD7\xB8\xAE\xB0\xED \xB8\xB6\xC4\xA7\xB3\xBB \xB0\xE8\xC8\xB9\xC0\xCC \xBD\xC2\xC0\xCE\xB5\xC7\xBE\xFA\xB4\xD9...", // "그리고 마침내 계획이 승인되었다..."
"...\xB8\xB6\xB9\xFD\xC0\xC7 \xB4\xE9\xB5\xB9\xC0\xBB...", // "...마법의 닻돌을..."
"...\xBC\xBC\xBB\xF3\xC0\xC7 \xC1\xDF\xBD\xC9\xBF\xA1\xBC\xAD \xC3\xA3\xBE\xC6\xBF\xCD\xBE\xDF \xC7\xDF\xB4\xD9.", // "...세상의 중심에서 찾아와야 했다."
"\xC5\xB0\xB6\xF5\xB5\xF0\xBE\xC6 \xB8\xB6\xB9\xFD\xBB\xE7 \xC1\xDF \xB0\xA1\xC0\xE5 \xC0\xFE\xC0\xBA \xC0\xDC\xC6\xBC\xBE\xC6\xB0\xA1 \xB5\xB9\xC0\xBB \xC3\xA3\xB4\xC2 \xC0\xD3\xB9\xAB\xBF\xA1 \xBC\xB1\xC5\xC3\xB5\xC7\xBE\xFA\xB4\xD9.", // "키란디아 마법사 중 가장 젊은 잔티아가 돌을 찾는 임무에 선택되었다."
"\xBF\xEE\xB8\xED\xC0\xC7 \xBC\xD5\xC0\xBB \xC7\xC3\xB7\xB9\xC0\xCC\xC7\xD8 \xC1\xD6\xBC\xC5\xBC\xAD \xB0\xA8\xBB\xE7\xC7\xD5\xB4\xCF\xB4\xD9.", // "운명의 손을 플레이해 주셔서 감사합니다."
"\xC0\xCC \xC1\xA4\xB5\xB5 \xBA\xED\xB7\xE7\xBA\xA3\xB8\xAE\xB8\xE9 \xBC\xBC\xBB\xF3\xC0\xC7 \xC1\xDF\xBD\xC9\xC0\xB8\xB7\xCE \xB0\xA1\xB4\xC2 \xC6\xF7\xC5\xD0\xC0\xBB \xBF\xAD \xBC\xF6 \xC0\xD6\xC0\xBB \xB0\xC5\xBE\xDF.", // "이 정도 블루베리면 세상의 중심으로 가는 포털을 열 수 있을 거야."
" DUMMY STRING... ",
" DUMMY STRING... ",
"\xC0\xCC\xB7\xB1! \xB3\xBB \xC0\xE5\xBA\xF1\xB0\xA1 \xB4\xD9 \xB5\xB5\xB5\xCF\xB8\xC2\xBE\xD2\xBE\xEE!", // "이런! 내 장비가 다 도둑맞았어!"
" DUMMY STRING... ",
"\xB3\xBB\xB0\xA1 \xC0\xFA \xBE\xC6\xB7\xA1\xB1\xEE\xC1\xF6 \xB0\xC9\xBE\xEE\xB0\xA5 \xB0\xC5\xB6\xF3\xB0\xED \xBB\xFD\xB0\xA2\xC7\xCF\xB8\xE9, \xB9\xCC\xC4\xA5 \xB0\xC5\xBE\xDF!", // "내가 저 아래까지 걸어갈 거라고 생각하면, 미칠 거야!"
" DUMMY STRING... ",
" DUMMY STRING... ",
"\xBC\xAD\xB5\xD1\xB7\xAF \xC6\xC4\xBF\xEE!", // "서둘러 파운!"
"\xBE\xDF, \xC5\xAB\xC0\xCF \xB3\xAF \xBB\xB7\xC7\xDF\xB3\xD7!", // "야, 큰일 날 뻔했네!"
"\xB8\xC2\xBE\xC6. \xB3\xAA\xB4\xC2 \xB4\xD9\xBD\xC5 \xBB\xE7\xB3\xC9\xC0\xBA \xBE\xC8 \xB0\xA5 \xB0\xC5\xBE\xDF!", // "맞아. 나는 다신 사냥은 안 갈 거야!"
"\xB0\xB3\xB1\xBC\xB0\xB3\xB1\xBC.", // "개굴개굴."
"\xB8\xEE \xB9\xF8\xC0\xBB \xB8\xBB\xC7\xD8\xBE\xDF \xBE\xCB\xBE\xC6\xB5\xE9\xBE\xEE? \xB3\xCA\xB4\xC2 \xB5\xCE\xB2\xA8\xBA\xF1\xB6\xF3\xB0\xED.", // "몇 번을 말해야 알아들어? 너는 두꺼비라고."
"\xC0\xCC\xB7\xB1! \xC4\xA1\xC1\xEE\xB0\xA1 \xB4\xD9 \xB6\xB3\xBE\xEE\xC1\xB3\xBE\xEE!", // "이런! 치즈가 다 떨어졌어!"
"\xC0\xCC \xB1\xCD\xC1\xF6\xB8\xA6 \xBD\xE1\xBA\xB8\xC0\xDA. \xC1\xD6\xC8\xB2\xBB\xF6\xC0\xCC\xB3\xD7.", // "이 귀지를 써보자. 주황색이네."
"\xBE\xF6\xB8\xB6, \xB3\xAA\xB4\xC2 \xBE\xF0\xC1\xA6 \xB4\xE3\xC0\xEF\xC0\xCC\xB5\xA2\xB1\xBC\xC0\xBB \xB9\xDE\xBE\xC6\xBF\xE4?", // "엄마, 나는 언제 담쟁이덩굴을 받아요?"
"\xC0\xFA\xB8\xAE \xBA\xF1\xC4\xD1, \xBD\xB5!", // "저리 비켜, 슉!"
"\xB3\xD7\xB0\xA1 \xC0\xDA\xB8\xA3\xB0\xED, \xB3\xBB\xB0\xA1 \xB0\xED\xB8\xA6\xB0\xD4.", // "네가 자르고, 내가 고를게."
"\xBE\xC6\xB4\xCF. \xB3\xD7\xB0\xA1 \xC0\xDA\xB8\xA3\xB0\xED \xB3\xBB\xB0\xA1 \xB0\xED\xB8\xA6\xB0\xD4.", // "아니. 네가 자르고 내가 고를게."
"\xBE\xC6\xC1\xF7\xB5\xB5 \xBE\xE2\xC6\xC5\xC7\xD1 \xB8\xF0\xB9\xE6\xC0\xDB\xC0\xCC\xB6\xF3\xB0\xED \xBB\xFD\xB0\xA2\xC7\xD8.", // "아직도 얄팍한 모방작이라고 생각해."
"\xBE\xEE\xC8\xDE, \xB3\xCD \xBE\xFB\xB5\xA2\xC0\xCC\xB0\xA1 \xB9\xB0\xB7\xC1\xB5\xB5 5\xC0\xBD\xBA\xB8\xB0\xDD\xC0\xCC \xB9\xBA\xC1\xF6 \xB8\xF8 \xBE\xCB\xBE\xC6\xB5\xE9\xC0\xBB \xB0\xC5\xBE\xDF!", // "어휴, 넌 엉덩이가 물려도 5음보격이 뭔지 못 알아들을 거야!"
"Executive Producer",
"Brett W. Sperry",
"Direction & Design",
"Rick Gush",
"Lead Programmer",
"Michael Legg",
"Art Management",
"Louis Castle",
"Joseph B. Hewitt IV",
"Lead Artist",
"Rick Parks",
"Additional Coding by",
"Philip W. Gorrow",
"Mike Grayford",
"Mark McCubbin",
"Artists",
"Cameron Chun",
"Cary Averett",
"Cindy Chinn",
"Elie Arabian",
"Fei Cheng",
"Ferby Miguel",
"Frank Mendeola",
"Jack Martin",
"Jerry Moore",
"DUMMY STRING... ",
"Judith Peterson",
"Larry Miller",
"Lenny Lee",
"Louise Sandoval",
"Ren Olsen",
"Music & Sounds by",
"Paul Mudra",
"Frank Klepacki",
"Dwight Okahara",
"Pat Collins",
"Quality Assurance by",
"Glenn Sperry",
"Michael Lightner",
"William Foster",
"Jesse Clemit",
"Jeff Fillhaber",
"Manual, Package Design",
"& Fulfillment",
"Eydie Laramore",
"Lisa Marcinko",
"Lauren Rifkin",
"\xC3\xE0\xC7\xCF\xC7\xD5\xB4\xCF\xB4\xD9!", // "축하합니다!"
"\xBF\xEE\xB8\xED\xC0\xC7 \xBC\xD5\xC0\xBB \xC7\xC3\xB7\xB9\xC0\xCC\xC7\xD8 \xC1\xD6\xBC\xC5\xBC\xAD \xB0\xA8\xBB\xE7\xC7\xD5\xB4\xCF\xB4\xD9!", // "운명의 손을 플레이해 주셔서 감사합니다!"
"Guest Coding",
"Producer Liaison",
"Scott Duckett",
"Irvine Testers",
"Chris McFarland",
"Paul Moore",
"Chad Soares",
"Jared Brinkley",
"Jon Willliams",
"Chris Toft",
"Joe Kucan's Hair by",
"Theodore A. Morris",
"\xB0\xD4\xC0\xD3 \xBA\xD2\xB7\xAF\xBF\xC0\xB1\xE2", // "게임 불러오기"
"\xC0\xCE\xC6\xAE\xB7\xCE \xBA\xB8\xB1\xE2", // "인트로 보기"
"\xBB\xF5 \xB0\xD4\xC0\xD3 \xBD\xC3\xC0\xDB", // "새 게임 시작"
"\xB0\xD4\xC0\xD3 \xC1\xBE\xB7\xE1", // "게임 종료"
"Special Thanks to",
"Sake Joe Bostic-san",
"Tim Fritz",
"Kenny Dunne",
"\xBF\xEE\xB8\xED\xC0\xC7 \xBC\xD5\xC0\xBB \xC7\xC3\xB7\xB9\xC0\xCC\xC7\xD8 \xC1\xD6\xBC\xC5\xBC\xAD \xB0\xA8\xBB\xE7\xC7\xD5\xB4\xCF\xB4\xD9.\n" // "운명의 손을 플레이해 주셔서 감사합니다.\n"
};
static const StringListProvider k2SeqplayStringsDOSCDKoreanProvider = { ARRAYSIZE(k2SeqplayStringsDOSCDKorean), k2SeqplayStringsDOSCDKorean };
static const char *const k2SeqplayTlkFilesDOSCDKorean[14] = {
"EINTRO1",
"EINTRO2",
"EINTRO3",
"EINTRO4",
"EINTRO5",
"EINTRO6",
"EINTRO7",
"EINTRO8",
"EINTRO9",
"EINTRO10",
"EINTRO11",
"EINTRO12",
"EGLOW",
""
};
static const StringListProvider k2SeqplayTlkFilesDOSCDKoreanProvider = { ARRAYSIZE(k2SeqplayTlkFilesDOSCDKorean), k2SeqplayTlkFilesDOSCDKorean };
Binary file not shown.
+36
View File
@@ -729,6 +729,42 @@ const KYRAGameDescription adGameDescs[] = {
KYRA2_FLOPPY_FAN_FLAGS(Common::RU_RUS, Common::EN_ANY)
},
{ // Korean fan translation (based on FM-TOWNS).
// Detection language is KO_KOR so the entry is distinguishable from the
// base EN_ANY FM-Towns entries (all share the same WSCORE.PAK hash).
// kyra.dat does not carry KO_KOR data; loadStaticResourceFile() falls
// back to replacedLang (EN_ANY) automatically for fan translations.
{
"kyra2",
nullptr,
AD_ENTRY2s("WSCORE.PAK", "c44de1302b67f27d4707409987b7a685", AD_NO_SIZE,
"KOREAN.FNT", "6b32ad695188e82b770a7739ffeb57db", 42300),
Common::KO_KOR,
Common::kPlatformFMTowns,
ADGF_NO_FLAGS,
GUIO3(GUIO_NOSPEECH, GUIO_MIDITOWNS, GUIO_RENDERFMTOWNS)
},
FLAGS_FAN(Common::KO_KOR, Common::EN_ANY, false, false, false, false, false, false, false, false, false, Kyra::GI_KYRA2)
},
{ // Korean fan translation (based on DOS CD), English voice + Korean subtitles.
// Detection language is KO_KOR so the entry is distinguishable from the
// base EN_ANY CD entry (both share the same FATE.PAK hash). kyra.dat does
// not carry KO_KOR data; loadStaticResourceFile() falls back to
// replacedLang (EN_ANY) automatically for fan translations.
{
"kyra2",
"CD",
AD_ENTRY2s("FATE.PAK", "28cbad1c5bf06b2d3825ae57d760d032", AD_NO_SIZE,
"KOREAN.FNT", "6b32ad695188e82b770a7739ffeb57db", 42300),
Common::KO_KOR,
Common::kPlatformDOS,
ADGF_DROPLANGUAGE | ADGF_CD,
GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GUIO_MIDIPCSPK, GUIO_RENDERVGA)
},
KYRA2_CD_FAN_FLAGS(Common::KO_KOR, Common::EN_ANY)
},
{ // CD version
{
"kyra2",
+45 -12
View File
@@ -192,13 +192,11 @@ Common::Error KyraEngine_HoF::init() {
KyraEngine_v1::init();
initStaticResource();
_text = new TextDisplayer_HoF(this, _screen);
assert(_text);
_gui = new GUI_HoF(this);
assert(_gui);
_gui->initStaticData();
_tim = new TIMInterpreter(this, _screen, _system);
assert(_tim);
// Korean fan translation: kyra.dat was loaded with EN_ANY, now switch to KO_KOR
// so that file extensions and text handling all use Korean paths.
// Font loading must happen before initStaticData() which calls getFontHeight().
if (_flags.fanLang == Common::KO_KOR)
_flags.lang = Common::KO_KOR;
if (_flags.isDemo && !_flags.isTalkie) {
_screen->loadFont(_screen->FID_8_FNT, "FONT9P.FNT");
@@ -210,8 +208,22 @@ Common::Error KyraEngine_HoF::init() {
_screen->loadFont(_screen->FID_8_FNT, "8FAT.FNT");
_screen->loadFont(_screen->FID_BOOKFONT_FNT, "BOOKFONT.FNT");
}
if (_flags.lang == Common::KO_KOR) {
_screen->loadFont(Screen::FID_KOREAN_FNT, "KOREAN.FNT");
_defaultFont = Screen::FID_KOREAN_FNT;
_bookFont = Screen::FID_KOREAN_FNT;
}
_screen->setFont(_defaultFont);
_text = new TextDisplayer_HoF(this, _screen);
assert(_text);
_gui = new GUI_HoF(this);
assert(_gui);
_gui->initStaticData();
_tim = new TIMInterpreter(this, _screen, _system);
assert(_tim);
_screen->setAnimBlockPtr(3504);
_screen->setScreenDim(0);
@@ -294,6 +306,9 @@ void KyraEngine_HoF::startup() {
_trackMap = _dosTrackMap;
_trackMapSize = _dosTrackMapSize;
// Restore the default font after intro sequences may have changed it
_screen->setFont(_defaultFont);
allocAnimObjects(1, 10, 30);
_screen->_curPage = 0;
@@ -826,7 +841,7 @@ uint8 *KyraEngine_HoF::getTableEntry(uint8 *buffer, int id) {
Common::String KyraEngine_HoF::getTableString(int id, uint8 *buffer, bool decode) {
Common::String string((char *)getTableEntry(buffer, id));
if (decode && _flags.lang != Common::JA_JPN) {
if (decode && _flags.lang != Common::JA_JPN && _flags.lang != Common::KO_KOR) {
string = Util::decodeString2(Util::decodeString1(string));
}
@@ -881,7 +896,7 @@ void KyraEngine_HoF::showChapterMessage(int id, int16 palIndex) {
void KyraEngine_HoF::updateCommandLineEx(int str1, int str2, int16 palIndex) {
Common::String str = getTableString(str1, _cCodeBuffer, true);
if (_flags.lang != Common::ZH_TWN && _flags.lang != Common::JA_JPN && _flags.lang != Common::HE_ISR) {
if (_flags.lang != Common::ZH_TWN && _flags.lang != Common::JA_JPN && _flags.lang != Common::KO_KOR && _flags.lang != Common::HE_ISR) {
if (uint32 i = (uint32)str.findFirstOf(' ') + 1) {
str.erase(0, i);
str.setChar(toupper(str[0]), 0);
@@ -889,7 +904,7 @@ void KyraEngine_HoF::updateCommandLineEx(int str1, int str2, int16 palIndex) {
}
if (str2 > 0) {
if (_flags.lang != Common::ZH_TWN && _flags.lang != Common::JA_JPN && _flags.lang != Common::HE_ISR)
if (_flags.lang != Common::ZH_TWN && _flags.lang != Common::JA_JPN && _flags.lang != Common::KO_KOR && _flags.lang != Common::HE_ISR)
str += " ";
if (_flags.lang == Common::HE_ISR)
str = getTableString(str2, _cCodeBuffer, 1) + " " + str + ".";
@@ -1008,6 +1023,12 @@ void KyraEngine_HoF::loadNPCScript() {
filename[5] = 'J';
break;
case 5:
// Korean fan translation: no Korean NPC script exists, fall back
// to the English one (_NPCE.EMC) which is present on DOS CD.
filename[5] = 'E';
break;
default:
break;
};
@@ -1928,15 +1949,27 @@ void KyraEngine_HoF::writeSettings() {
_flags.lang = Common::JA_JPN;
break;
case 5:
_flags.lang = Common::KO_KOR;
break;
case 0:
default:
_flags.lang = _langIntern ? Common::ZH_TWN : Common::EN_ANY;
}
if (_flags.lang == _flags.replacedLang && _flags.fanLang != Common::UNK_LANG)
if (_flags.lang == _flags.replacedLang && _flags.fanLang != Common::UNK_LANG
&& _flags.fanLang != Common::KO_KOR)
_flags.lang = _flags.fanLang;
ConfMan.set("language", Common::getLanguageCode(_flags.lang));
// Korean fan translation: the detection entry now uses KO_KOR as the
// detection language with ADGF_DROPLANGUAGE, so we must write "ko" to
// config. Writing "en" would cause the Advanced Detector to prefer the
// base EN_ANY CD entry on next launch, reverting to English.
if (_flags.fanLang == Common::KO_KOR)
ConfMan.set("language", Common::getLanguageCode(Common::KO_KOR));
else
ConfMan.set("language", Common::getLanguageCode(_flags.lang));
KyraEngine_v1::writeSettings();
}
+12 -2
View File
@@ -79,8 +79,14 @@ KyraEngine_v2::KyraEngine_v2(OSystem *system, const GameFlags &flags, const Engi
_lang = 0;
_scriptLang = 0;
Common::Language lang = Common::parseLanguage(ConfMan.get("language"));
if (lang == _flags.fanLang && _flags.replacedLang != Common::UNK_LANG)
lang = _flags.replacedLang;
// Korean fan translation: always use KO_KOR regardless of config value,
// so that _lang is set to 5 and Korean file extensions are selected.
if (_flags.fanLang == Common::KO_KOR) {
lang = Common::KO_KOR;
} else if (lang == _flags.fanLang && _flags.replacedLang != Common::UNK_LANG) {
lang = _flags.replacedLang;
}
if (_flags.extraLang == Common::ZH_TWN)
_langIntern = 1;
@@ -115,6 +121,10 @@ KyraEngine_v2::KyraEngine_v2(OSystem *system, const GameFlags &flags, const Engi
_lang = 4;
break;
case Common::KO_KOR:
_lang = 5;
break;
default:
warning("unsupported language, switching back to English");
_lang = 0;
+11 -1
View File
@@ -427,7 +427,17 @@ void KyraEngine_HoF::startSceneScript(int unk1) {
_sceneCommentString = "Undefined scene comment string!";
_emc->init(&_sceneScriptState, &_sceneScriptData);
filename = Common::String(_sceneList[sceneId].filename1) + "." + _scriptLangExt[(_flags.platform == Common::kPlatformDOS && !_flags.isTalkie) ? 0 : _lang];
// Korean fan translation uses .KMC scripts (Korean-patched EMC files).
// Fall back to .EMC if .KMC is not available. Other langs clamp to valid range.
if (_flags.lang == Common::KO_KOR) {
filename = Common::String(_sceneList[sceneId].filename1) + ".KMC";
if (!_res->exists(filename.c_str())) {
filename = Common::String(_sceneList[sceneId].filename1) + ".EMC";
}
} else {
int scriptLangIdx = (_flags.platform == Common::kPlatformDOS && !_flags.isTalkie) ? 0 : (_lang > 3 ? 0 : _lang);
filename = Common::String(_sceneList[sceneId].filename1) + "." + _scriptLangExt[scriptLangIdx];
}
_res->exists(filename.c_str(), true);
_emc->load(filename.c_str(), &_sceneScriptData, &_opcodes);
runSceneScript7();
+16 -2
View File
@@ -1426,8 +1426,22 @@ bool Screen::loadFont(FontId fontId, const char *filename) {
fnt->load(str);
}
} else if (fontId == FID_KOREAN_FNT) {
const uint16 *lookupTable = _vm->staticres()->loadRawDataBe16(k1TwoByteFontLookupTable, temp);
fnt = new JohabFontLoK(_fonts[FID_8_FNT], lookupTable, temp);
if (_vm->game() == GI_KYRA2) {
Common::Array<Font*> *fa = new Common::Array<Font*>;
fa->push_back(new KoreanOneByteFontHOF(SCREEN_W));
fa->push_back(new KoreanTwoByteFontHOF(SCREEN_W));
fnt = new MultiSubsetFont(fa);
// Load 1-byte (ASCII) font from ENGLISH.FNT first, then
// the main KOREAN.FNT call below will load the 2-byte glyphs.
Common::SeekableReadStream *engFile = _vm->resource()->createReadStream("ENGLISH.FNT");
if (engFile) {
fnt->load(*engFile);
delete engFile;
}
} else {
const uint16 *lookupTable = _vm->staticres()->loadRawDataBe16(k1TwoByteFontLookupTable, temp);
fnt = new JohabFontLoK(_fonts[FID_8_FNT], lookupTable, temp);
}
} else {
fnt = new DOSFont();
}
+20
View File
@@ -410,6 +410,26 @@ private:
void processColorMap() override;
};
class KoreanOneByteFontHOF final : public ChineseFont {
public:
KoreanOneByteFontHOF(int pitch) : ChineseFont(pitch, 8, 9, 8, 10, 0, 0) {}
Type getType() const override { return kJohab; }
private:
bool hasGlyphForCharacter(uint16 c) const override { return !(c & 0x80); }
uint32 getFontOffset(uint16 c) const override { return (c & 0x7F) * 9; }
void processColorMap() override;
};
class KoreanTwoByteFontHOF final : public ChineseFont {
public:
KoreanTwoByteFontHOF(int pitch) : ChineseFont(pitch, 10, 9, 10, 10, 0, 0) {}
Type getType() const override { return kJohab; }
private:
bool hasGlyphForCharacter(uint16 c) const override { return (c & 0x80); }
uint32 getFontOffset(uint16 c) const override;
void processColorMap() override;
};
class MultiSubsetFont final : public Font {
public:
MultiSubsetFont(Common::Array<Font*> *subsets) : Font(), _subsets(subsets) {}
+25
View File
@@ -109,4 +109,29 @@ void ChineseTwoByteFontHOF::processColorMap() {
_pixelColorShading = !(_colorMap[1] == 207 || _colorMap[1] > 240);
}
void KoreanOneByteFontHOF::processColorMap() {
_textColor[0] = _colorMap[1];
_textColor[1] = _colorMap[0] | (_colorMap[0] << 8);
_pixelColorShading = false;
}
uint32 KoreanTwoByteFontHOF::getFontOffset(uint16 c) const {
// fetchChar() stores: first byte (EUC-KR high) in low 8 bits,
// second byte (EUC-KR low) in high 8 bits. So:
// c & 0xFF = first byte (high byte of EUC-KR, 0xB0-0xC8)
// c >> 8 = second byte (low byte of EUC-KR, 0xA1-0xFE)
uint8 high = c & 0xFF;
uint8 low = (c >> 8) & 0xFF;
if (high < 0xB0 || high > 0xC8 || low < 0xA1 || low > 0xFE)
return 0;
uint32 index = (high - 0xB0) * 94 + (low - 0xA1);
return index * 18; // 10x9 pixels, 2 bytes/row, 9 rows = 18 bytes/glyph
}
void KoreanTwoByteFontHOF::processColorMap() {
_textColor[0] = _colorMap[1];
_textColor[1] = _colorMap[0] | (_colorMap[0] << 8);
_pixelColorShading = false;
}
} // End of namespace Kyra
+8
View File
@@ -1021,6 +1021,9 @@ int GUI_HoF::gameOptionsTalkie(Button *caller) {
int GUI_HoF::changeLanguage(Button *caller) {
updateMenuButton(caller);
// Korean fan translation: language is fixed to Korean, do not cycle.
if (_vm->gameFlags().fanLang == Common::KO_KOR)
return 0;
++_vm->_lang;
_vm->_lang %= _vm->_numLang;
setupOptionsButtons();
@@ -1052,6 +1055,11 @@ void GUI_HoF::setupOptionsButtons() {
_gameOptions.item[1].itemId = 33;
break;
case 5:
// Korean fan translation: string 51 in OPTIONS.KOR = "한국어"
_gameOptions.item[1].itemId = 51;
break;
default:
break;
}
+29 -2
View File
@@ -157,7 +157,33 @@ bool StaticResource::loadStaticResourceFile() {
if (!res->loadPakFile(staticDataFilename(), *i))
continue;
if ((setLanguage(_vm->gameFlags().lang) && prefetchId(-1))) {
// Fan translations (e.g. Korean) may provide only a subset of
// resources in kyra.dat (e.g. intro strings). Load the base
// (replaced) language first, then merge fan language entries on
// top — only replacing IDs that the fan language provides.
if (_vm->gameFlags().fanLang != Common::UNK_LANG &&
_vm->gameFlags().replacedLang != Common::UNK_LANG) {
if ((setLanguage(_vm->gameFlags().replacedLang) && prefetchId(-1))) {
foundWorkingKyraDat = true;
// Merge: load the fan language ID map WITHOUT clearing
// existing _dataTable entries, then reload only the
// IDs that changed.
Common::SeekableReadStream *fanMap = loadIdMap(_vm->gameFlags().fanLang);
if (fanMap) {
int numIDs = fanMap->readUint16BE();
while (numIDs--) {
uint16 id2 = fanMap->readUint16BE();
uint8 type = fanMap->readByte();
uint32 filename = fanMap->readUint32BE();
_dataTable[id2] = DataDescriptor(filename, type);
unloadId(id2);
prefetchId(id2);
}
delete fanMap;
}
break;
}
} else if ((setLanguage(_vm->gameFlags().lang) && prefetchId(-1))) {
foundWorkingKyraDat = true;
break;
}
@@ -1455,7 +1481,8 @@ const char *const KyraEngine_HoF::_languageExtension[] = {
"ITA", Italian and Spanish were never included
"SPA"*/
"JPN",
"POL"
"POL",
"KOR"
};
const char *const KyraEngine_HoF::_scriptLangExt[] = {
+5 -3
View File
@@ -389,7 +389,7 @@ SeqPlayer_HOF::SeqPlayer_HOF(KyraEngine_v1 *vm, Screen_v2 *screen, OSystem *syst
memset(_hofDemoItemShapes, 0, sizeof(_hofDemoItemShapes));
_defaultFont = (_vm->gameFlags().lang == Common::ZH_TWN) ? Screen::FID_CHINESE_FNT : ((_vm->gameFlags().lang == Common::JA_JPN) ? Screen::FID_SJIS_FNT : Screen::FID_GOLDFONT_FNT);
_defaultFont = (_vm->gameFlags().lang == Common::ZH_TWN) ? Screen::FID_CHINESE_FNT : ((_vm->gameFlags().lang == Common::JA_JPN) ? Screen::FID_SJIS_FNT : ((_vm->gameFlags().lang == Common::KO_KOR) ? Screen::FID_KOREAN_FNT : Screen::FID_GOLDFONT_FNT));
_creditsFont = (_vm->gameFlags().lang == Common::ZH_TWN) ? Screen::FID_CHINESE_FNT : Screen::FID_8_FNT;
_creditsFont2 = (_vm->gameFlags().lang == Common::ZH_TWN) ? Screen::FID_GOLDFONT_FNT : Screen::FID_8_FNT;
@@ -463,7 +463,9 @@ SeqPlayer_HOF::SeqPlayer_HOF(KyraEngine_v1 *vm, Screen_v2 *screen, OSystem *syst
int8 ls = 0;
Screen::FontId fid = Screen::FID_8_FNT;
if (_vm->gameFlags().lang == Common::JA_JPN) {
if (_vm->gameFlags().lang == Common::KO_KOR) {
fid = Screen::FID_KOREAN_FNT;
} else if (_vm->gameFlags().lang == Common::JA_JPN) {
fid = Screen::FID_SJIS_FNT;
ls = 1;
} else if (_vm->gameFlags().lang == Common::ZH_TWN) {
@@ -504,7 +506,7 @@ SeqPlayer_HOF::~SeqPlayer_HOF() {
delete _menu;
if (_vm->game() != GI_LOL)
_screen->setFont(_vm->gameFlags().lang == Common::ZH_TWN ? Screen::FID_CHINESE_FNT : (_vm->gameFlags().lang == Common::JA_JPN ? Screen::FID_SJIS_FNT : Screen::FID_8_FNT));
_screen->setFont(_vm->gameFlags().lang == Common::ZH_TWN ? Screen::FID_CHINESE_FNT : (_vm->gameFlags().lang == Common::JA_JPN ? Screen::FID_SJIS_FNT : (_vm->gameFlags().lang == Common::KO_KOR ? Screen::FID_KOREAN_FNT : Screen::FID_8_FNT)));
}
int SeqPlayer_HOF::play(SequenceID firstScene, SequenceID loopStartScene) {
+19 -6
View File
@@ -86,6 +86,12 @@ char *TextDisplayer_HoF::preprocessString(const char *str) {
if (_vm->gameFlags().lang == Common::ZH_TWN)
return _talkBuffer;
// Korean fan translation: delegate to base class which already handles
// 2-byte character width measurement with FID_KOREAN_FNT and has
// correct maxTextWidth limits for Korean text.
if (_vm->gameFlags().lang == Common::KO_KOR)
return TextDisplayer::preprocessString(_talkBuffer);
char *p = _talkBuffer;
while (*p) {
if (*p == '\r')
@@ -424,7 +430,7 @@ void KyraEngine_HoF::randomSceneChat() {
void KyraEngine_HoF::updateDlgBuffer() {
static const char suffixTalkie[] = "EFG";
static const char suffixTowns[] = "G J";
static const char suffixTowns[] = "G J K";
if (_currentChapter == _npcTalkChpIndex && _mainCharacter.dlgIndex == _npcTalkDlgIndex)
return;
@@ -434,11 +440,18 @@ void KyraEngine_HoF::updateDlgBuffer() {
Common::String filename = Common::String::format("CH%.02d-S%.02d.DL", _currentChapter, _npcTalkDlgIndex);
const char *suffix = _flags.isTalkie ? suffixTalkie : suffixTowns;
if (_flags.platform != Common::kPlatformDOS || _flags.isTalkie)
filename += suffix[_lang];
else
filename += 'G';
// Korean fan translation always uses .DLK regardless of talkie/platform.
// suffixTalkie[] = "EFG" only covers _lang 0-2; _lang=5 (KO_KOR) would
// be an out-of-bounds read, so we handle it explicitly before the lookup.
if (_flags.lang == Common::KO_KOR) {
filename += 'K';
} else {
const char *suffix = _flags.isTalkie ? suffixTalkie : suffixTowns;
if (_flags.platform != Common::kPlatformDOS || _flags.isTalkie)
filename += suffix[_lang];
else
filename += 'G';
}
delete[] _dlgBuffer;
_dlgBuffer = _res->fileData(filename.c_str(), nullptr);