[Scummvm-git-logs] scummvm master -> 03099dfa9e98cbedd12dbfc58b59259dbe5b9f21

athrxx noreply at scummvm.org
Thu Mar 26 16:26:08 UTC 2026


This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
f49d5ca984 KYRA: Add Korean fan translation support for Hand of Fate
03099dfa9e KYRA: Remove redundant KO_KOR lang overrides in HoF


Commit: f49d5ca984584ed6a88a87fe1ad5c2d8c8ccf433
    https://github.com/scummvm/scummvm/commit/f49d5ca984584ed6a88a87fe1ad5c2d8c8ccf433
Author: Seokjun Kim (colus001 at me.com)
Date: 2026-03-26T17:26:00+01:00

Commit Message:
KYRA: Add Korean fan translation support for Hand of Fate

Changed paths:
  A devtools/create_kyradat/resources/hof_dos_cd_korean.h
    devtools/create_kyradat/games.cpp
    devtools/create_kyradat/resources.cpp
    dists/engine-data/kyra.dat
    engines/kyra/detection_tables.h
    engines/kyra/engine/kyra_hof.cpp
    engines/kyra/engine/kyra_v2.cpp
    engines/kyra/engine/scene_hof.cpp
    engines/kyra/graphics/screen.cpp
    engines/kyra/graphics/screen.h
    engines/kyra/graphics/screen_hof.cpp
    engines/kyra/gui/gui_hof.cpp
    engines/kyra/resource/staticres.cpp
    engines/kyra/sequence/sequences_hof.cpp
    engines/kyra/text/text_hof.cpp


diff --git a/devtools/create_kyradat/games.cpp b/devtools/create_kyradat/games.cpp
index c5b09b6842b..52b7e4994e6 100644
--- a/devtools/create_kyradat/games.cpp
+++ b/devtools/create_kyradat/games.cpp
@@ -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 },
diff --git a/devtools/create_kyradat/resources.cpp b/devtools/create_kyradat/resources.cpp
index b81fbd3b492..0659d41f6c1 100644
--- a/devtools/create_kyradat/resources.cpp
+++ b/devtools/create_kyradat/resources.cpp
@@ -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 },
diff --git a/devtools/create_kyradat/resources/hof_dos_cd_korean.h b/devtools/create_kyradat/resources/hof_dos_cd_korean.h
new file mode 100644
index 00000000000..81fe3343ba1
--- /dev/null
+++ b/devtools/create_kyradat/resources/hof_dos_cd_korean.h
@@ -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 };
diff --git a/dists/engine-data/kyra.dat b/dists/engine-data/kyra.dat
index 7f4813135e3..2edacd78311 100644
Binary files a/dists/engine-data/kyra.dat and b/dists/engine-data/kyra.dat differ
diff --git a/engines/kyra/detection_tables.h b/engines/kyra/detection_tables.h
index ce17c343824..a3486bab5b6 100644
--- a/engines/kyra/detection_tables.h
+++ b/engines/kyra/detection_tables.h
@@ -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",
diff --git a/engines/kyra/engine/kyra_hof.cpp b/engines/kyra/engine/kyra_hof.cpp
index 130b4f358fc..e0eabfcdde9 100644
--- a/engines/kyra/engine/kyra_hof.cpp
+++ b/engines/kyra/engine/kyra_hof.cpp
@@ -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();
 }
diff --git a/engines/kyra/engine/kyra_v2.cpp b/engines/kyra/engine/kyra_v2.cpp
index 24bc86317d3..d84d9649bf7 100644
--- a/engines/kyra/engine/kyra_v2.cpp
+++ b/engines/kyra/engine/kyra_v2.cpp
@@ -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;
diff --git a/engines/kyra/engine/scene_hof.cpp b/engines/kyra/engine/scene_hof.cpp
index c17bc406580..cc472daf104 100644
--- a/engines/kyra/engine/scene_hof.cpp
+++ b/engines/kyra/engine/scene_hof.cpp
@@ -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();
diff --git a/engines/kyra/graphics/screen.cpp b/engines/kyra/graphics/screen.cpp
index 1be63111966..0f80c637685 100644
--- a/engines/kyra/graphics/screen.cpp
+++ b/engines/kyra/graphics/screen.cpp
@@ -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();
 		}
diff --git a/engines/kyra/graphics/screen.h b/engines/kyra/graphics/screen.h
index fa9bcd794ab..c1bc073210c 100644
--- a/engines/kyra/graphics/screen.h
+++ b/engines/kyra/graphics/screen.h
@@ -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) {}
diff --git a/engines/kyra/graphics/screen_hof.cpp b/engines/kyra/graphics/screen_hof.cpp
index 1fa7d0db6c6..e592cd4b38a 100644
--- a/engines/kyra/graphics/screen_hof.cpp
+++ b/engines/kyra/graphics/screen_hof.cpp
@@ -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
diff --git a/engines/kyra/gui/gui_hof.cpp b/engines/kyra/gui/gui_hof.cpp
index 0a3a316c751..11dd36eb038 100644
--- a/engines/kyra/gui/gui_hof.cpp
+++ b/engines/kyra/gui/gui_hof.cpp
@@ -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;
 	}
diff --git a/engines/kyra/resource/staticres.cpp b/engines/kyra/resource/staticres.cpp
index 97df8d5e90a..b51c909c574 100644
--- a/engines/kyra/resource/staticres.cpp
+++ b/engines/kyra/resource/staticres.cpp
@@ -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[] = {
diff --git a/engines/kyra/sequence/sequences_hof.cpp b/engines/kyra/sequence/sequences_hof.cpp
index 495f29eeff4..160f51d5990 100644
--- a/engines/kyra/sequence/sequences_hof.cpp
+++ b/engines/kyra/sequence/sequences_hof.cpp
@@ -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) {
diff --git a/engines/kyra/text/text_hof.cpp b/engines/kyra/text/text_hof.cpp
index 34a3197eddc..8056b97049b 100644
--- a/engines/kyra/text/text_hof.cpp
+++ b/engines/kyra/text/text_hof.cpp
@@ -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);


Commit: 03099dfa9e98cbedd12dbfc58b59259dbe5b9f21
    https://github.com/scummvm/scummvm/commit/03099dfa9e98cbedd12dbfc58b59259dbe5b9f21
Author: Seokjun Kim (colus001 at me.com)
Date: 2026-03-26T17:26:00+01:00

Commit Message:
KYRA: Remove redundant KO_KOR lang overrides in HoF

Changed paths:
    engines/kyra/engine/kyra_hof.cpp


diff --git a/engines/kyra/engine/kyra_hof.cpp b/engines/kyra/engine/kyra_hof.cpp
index e0eabfcdde9..92c6dc83bd9 100644
--- a/engines/kyra/engine/kyra_hof.cpp
+++ b/engines/kyra/engine/kyra_hof.cpp
@@ -192,12 +192,6 @@ Common::Error KyraEngine_HoF::init() {
 	KyraEngine_v1::init();
 	initStaticResource();
 
-	// 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");
 	} else if (_flags.lang == Common::ZH_TWN) {
@@ -1958,18 +1952,10 @@ void KyraEngine_HoF::writeSettings() {
 		_flags.lang = _langIntern ? Common::ZH_TWN : Common::EN_ANY;
 	}
 
-	if (_flags.lang == _flags.replacedLang && _flags.fanLang != Common::UNK_LANG
-	        && _flags.fanLang != Common::KO_KOR)
+	if (_flags.lang == _flags.replacedLang && _flags.fanLang != Common::UNK_LANG)
 		_flags.lang = _flags.fanLang;
 
-	// 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));
+	ConfMan.set("language", Common::getLanguageCode(_flags.lang));
 
 	KyraEngine_v1::writeSettings();
 }




More information about the Scummvm-git-logs mailing list