| 1 | /* |
| 2 | * LegacyClonk |
| 3 | * |
| 4 | * Copyright (c) 1998-2000, Matthes Bender (RedWolf Design) |
| 5 | * Copyright (c) 2017-2021, The LegacyClonk Team and contributors |
| 6 | * |
| 7 | * Distributed under the terms of the ISC license; see accompanying file |
| 8 | * "COPYING" for details. |
| 9 | * |
| 10 | * "Clonk" is a registered trademark of Matthes Bender, used with permission. |
| 11 | * See accompanying file "TRADEMARK" for details. |
| 12 | * |
| 13 | * To redistribute this file separately, substitute the full license texts |
| 14 | * for the above references. |
| 15 | */ |
| 16 | |
| 17 | /* |
| 18 | Language module |
| 19 | - handles external language packs |
| 20 | - provides info on selectable languages by scanning string tables |
| 21 | - loads and sets a language string table (ResStrTable) based on a specified language sequence |
| 22 | */ |
| 23 | |
| 24 | #include <C4Include.h> |
| 25 | #include <C4Language.h> |
| 26 | |
| 27 | #include <C4Components.h> |
| 28 | #include <C4Log.h> |
| 29 | #include <C4Config.h> |
| 30 | #include <C4Game.h> |
| 31 | #include "C4TextEncoding.h" |
| 32 | |
| 33 | C4Language Languages; |
| 34 | |
| 35 | C4Language::C4Language() |
| 36 | { |
| 37 | PackGroupLocation[0] = 0; |
| 38 | } |
| 39 | |
| 40 | C4Language::~C4Language() |
| 41 | { |
| 42 | Clear(); |
| 43 | } |
| 44 | |
| 45 | bool C4Language::Init() |
| 46 | { |
| 47 | // Clear (to allow clean re-init) |
| 48 | Clear(); |
| 49 | |
| 50 | // Make sure Language.c4g is unpacked |
| 51 | if (ItemExists(C4CFN_Languages)) |
| 52 | if (!DirectoryExists(C4CFN_Languages)) |
| 53 | C4Group_UnpackDirectory(C4CFN_Languages); |
| 54 | |
| 55 | // Look for available language packs in Language.c4g |
| 56 | C4Group *pPack; |
| 57 | char strPackFilename[_MAX_FNAME + 1], strEntry[_MAX_FNAME + 1]; |
| 58 | if (PackDirectory.Open(C4CFN_Languages)) |
| 59 | while (PackDirectory.FindNextEntry(szWildCard: "*.c4g" , sFileName: strEntry)) |
| 60 | { |
| 61 | FormatWithNull(buf&: strPackFilename, fmt: "{}" DirSep "{}" , args: +C4CFN_Languages, args: +strEntry); |
| 62 | pPack = new C4Group(); |
| 63 | if (pPack->Open(szGroupName: strPackFilename)) |
| 64 | { |
| 65 | Packs.RegisterGroup(rGroup&: *pPack, fOwnGrp: true, C4GSCnt_Language, Contents: false); |
| 66 | } |
| 67 | else |
| 68 | { |
| 69 | delete pPack; |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | // Now create a pack group for each language pack (these pack groups are child groups |
| 74 | // that browse along each pack to access requested data) |
| 75 | for (int iPack = 0; pPack = Packs.GetGroup(iIndex: iPack); iPack++) |
| 76 | PackGroups.RegisterGroup(rGroup&: *(new C4Group), fOwnGrp: true, C4GSPrio_Base, C4GSCnt_Language); |
| 77 | |
| 78 | // Load language infos by scanning string tables (the engine doesn't really need this at the moment) |
| 79 | InitInfos(); |
| 80 | |
| 81 | // Done |
| 82 | return true; |
| 83 | } |
| 84 | |
| 85 | void C4Language::Clear() |
| 86 | { |
| 87 | // Clear pack groups |
| 88 | PackGroups.Clear(); |
| 89 | // Clear packs |
| 90 | Packs.Clear(); |
| 91 | // Close pack directory |
| 92 | PackDirectory.Close(); |
| 93 | // Clear infos |
| 94 | Infos.clear(); |
| 95 | } |
| 96 | |
| 97 | // Returns a set of groups at the specified relative path within all open language packs. |
| 98 | |
| 99 | C4GroupSet &C4Language::GetPackGroups(const char *strRelativePath) |
| 100 | { |
| 101 | // Variables |
| 102 | char strTargetLocation[_MAX_PATH + 1]; |
| 103 | char strPackPath[_MAX_PATH + 1]; |
| 104 | char strPackGroupLocation[_MAX_PATH + 1]; |
| 105 | char strAdvance[_MAX_PATH + 1]; |
| 106 | |
| 107 | // Store wanted target location |
| 108 | SCopy(szSource: strRelativePath, sTarget: strTargetLocation, _MAX_PATH); |
| 109 | |
| 110 | // Adjust location by scenario origin |
| 111 | if (Game.C4S.Head.Origin.getLength() && SEqualNoCase(szStr1: GetExtension(fname: Game.C4S.Head.Origin.getData()), szStr2: "c4s" )) |
| 112 | { |
| 113 | const char *szScenarioRelativePath = GetRelativePathS(strPath: strRelativePath, strRelativeTo: Config.AtExeRelativePath(szFilename: Game.ScenarioFilename)); |
| 114 | if (szScenarioRelativePath != strRelativePath) |
| 115 | { |
| 116 | // this is a path within the scenario! Change to origin. |
| 117 | size_t iRestPathLen = SLen(sptr: szScenarioRelativePath); |
| 118 | if (Game.C4S.Head.Origin.getLength() + 1 + iRestPathLen <= _MAX_PATH) |
| 119 | { |
| 120 | SCopy(szSource: Game.C4S.Head.Origin.getData(), sTarget: strTargetLocation); |
| 121 | if (iRestPathLen) |
| 122 | { |
| 123 | SAppendChar(DirectorySeparator, szStr: strTargetLocation); |
| 124 | SAppend(szSource: szScenarioRelativePath, szTarget: strTargetLocation); |
| 125 | } |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // Target location has not changed: return last list of pack groups |
| 131 | if (SEqualNoCase(szStr1: strTargetLocation, szStr2: PackGroupLocation)) |
| 132 | return PackGroups; |
| 133 | |
| 134 | // Process all language packs (and their respective pack groups) |
| 135 | C4Group *pPack, *pPackGroup; |
| 136 | for (int iPack = 0; (pPack = Packs.GetGroup(iIndex: iPack)) && (pPackGroup = PackGroups.GetGroup(iIndex: iPack)); iPack++) |
| 137 | { |
| 138 | // Get current pack group position within pack |
| 139 | SCopy(szSource: pPack->GetFullName().getData(), sTarget: strPackPath, _MAX_PATH); |
| 140 | GetRelativePath(strPath: pPackGroup->GetFullName().getData(), strRelativeTo: strPackPath, strBuffer: strPackGroupLocation); |
| 141 | |
| 142 | // Pack group is at correct position within pack: continue with next pack |
| 143 | if (SEqualNoCase(szStr1: strPackGroupLocation, szStr2: strTargetLocation)) |
| 144 | continue; |
| 145 | |
| 146 | // Try to backtrack until we can reach the target location as a relative child |
| 147 | while (strPackGroupLocation[0] |
| 148 | && !GetRelativePath(strPath: strTargetLocation, strRelativeTo: strPackGroupLocation, strBuffer: strAdvance) |
| 149 | && pPackGroup->OpenMother()) |
| 150 | { |
| 151 | // Update pack group location |
| 152 | GetRelativePath(strPath: pPackGroup->GetFullName().getData(), strRelativeTo: strPackPath, strBuffer: strPackGroupLocation); |
| 153 | } |
| 154 | |
| 155 | // We can reach the target location as a relative child |
| 156 | if (strPackGroupLocation[0] && GetRelativePath(strPath: strTargetLocation, strRelativeTo: strPackGroupLocation, strBuffer: strAdvance)) |
| 157 | { |
| 158 | // Advance pack group to relative child |
| 159 | pPackGroup->OpenChild(strEntry: strAdvance); |
| 160 | } |
| 161 | |
| 162 | // Cannot reach by advancing: need to close and reopen (rewinding group file) |
| 163 | else |
| 164 | { |
| 165 | // Close pack group (if it is open at all) |
| 166 | pPackGroup->Close(); |
| 167 | // Reopen pack group to relative position in language pack if possible |
| 168 | pPackGroup->OpenAsChild(pMother: pPack, szEntryName: strTargetLocation); |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | // Store new target location |
| 173 | SCopy(szSource: strTargetLocation, sTarget: PackGroupLocation, _MAX_FNAME); |
| 174 | |
| 175 | // Return currently open pack groups |
| 176 | return PackGroups; |
| 177 | } |
| 178 | |
| 179 | void C4Language::InitInfos() |
| 180 | { |
| 181 | C4Group hGroup; |
| 182 | // First, look in System.c4g |
| 183 | if (hGroup.Open(C4CFN_System)) |
| 184 | { |
| 185 | LoadInfos(hGroup); |
| 186 | hGroup.Close(); |
| 187 | } |
| 188 | // Now look through the registered packs |
| 189 | C4Group *pPack; |
| 190 | for (int iPack = 0; pPack = Packs.GetGroup(iIndex: iPack); iPack++) |
| 191 | // Does it contain a System.c4g child group? |
| 192 | if (hGroup.OpenAsChild(pMother: pPack, C4CFN_System)) |
| 193 | { |
| 194 | LoadInfos(hGroup); |
| 195 | hGroup.Close(); |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | void C4Language::LoadInfos(C4Group &hGroup) |
| 200 | { |
| 201 | char strEntry[_MAX_FNAME + 1]; |
| 202 | char *strTable; |
| 203 | // Look for language string tables |
| 204 | hGroup.ResetSearch(); |
| 205 | while (hGroup.FindNextEntry(C4CFN_Language, sFileName: strEntry)) |
| 206 | // For now, we will only load info on the first string table found for a given |
| 207 | // language code as there is currently no handling for selecting different string tables |
| 208 | // of the same code - the system always loads the first string table found for a given code |
| 209 | if (!FindInfo(code: GetFilenameOnly(strFilename: strEntry) + SLen(sptr: GetFilenameOnly(strFilename: strEntry)) - 2)) |
| 210 | // Load language string table |
| 211 | if (hGroup.LoadEntry(szEntryName: strEntry, lpbpBuf: &strTable, ipSize: nullptr, iAppendZeros: 1)) |
| 212 | { |
| 213 | // New language info |
| 214 | C4LanguageInfo info; |
| 215 | // Get language code by entry name |
| 216 | std::strncpy(dest: info.Code.data(), src: GetFilenameOnly(strFilename: strEntry) + SLen(sptr: GetFilenameOnly(strFilename: strEntry)) - 2, n: 2); |
| 217 | info.Code[0] = CharCapital(cChar: info.Code[0]); |
| 218 | info.Code[1] = CharCapital(cChar: info.Code[1]); |
| 219 | // Get language name, info, fallback from table |
| 220 | C4ResStrTable table{std::string_view{}, strTable}; |
| 221 | info.Name = table.GetEntry(key: C4ResStrTableKey::IDS_LANG_NAME); |
| 222 | info.Info = table.GetEntry(key: C4ResStrTableKey::IDS_LANG_INFO); |
| 223 | info.Fallback = table.GetEntry(key: C4ResStrTableKey::IDS_LANG_FALLBACK); |
| 224 | info.Charset = table.GetEntry(key: C4ResStrTableKey::IDS_LANG_CHARSET); |
| 225 | // Safety: pipe character is not allowed in any language info string |
| 226 | SReplaceChar(str: info.Name.data(), fc: '|', tc: ' '); |
| 227 | SReplaceChar(str: info.Info.data(), fc: '|', tc: ' '); |
| 228 | SReplaceChar(str: info.Fallback.data(), fc: '|', tc: ' '); |
| 229 | // Delete table |
| 230 | delete[] strTable; |
| 231 | // Add info to list |
| 232 | Infos.emplace_back(args: std::move(info)); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | const C4LanguageInfo *C4Language::FindInfo(const char *const code) |
| 237 | { |
| 238 | if (const auto it = std::find_if(first: begin(), last: end(), pred: [code](auto &info) |
| 239 | { |
| 240 | if (CharCapital(info.Code[0]) != CharCapital(cChar: *code)) return false; |
| 241 | return *code && CharCapital(info.Code[1]) == CharCapital(cChar: code[1]); |
| 242 | }); it != end()) |
| 243 | { |
| 244 | return &*it; |
| 245 | } |
| 246 | |
| 247 | return nullptr; |
| 248 | } |
| 249 | |
| 250 | bool C4Language::LoadLanguage(const char *strLanguages) |
| 251 | { |
| 252 | // Clear old string table |
| 253 | ClearLanguage(); |
| 254 | // Try to load string table according to language sequence |
| 255 | char strLanguageCode[2 + 1]; |
| 256 | for (int i = 0; SCopySegment(fstr: strLanguages, segn: i, tstr: strLanguageCode, sepa: ',', iMaxL: 2, fSkipWhitespace: true); i++) |
| 257 | if (InitStringTable(strCode: strLanguageCode)) |
| 258 | return true; |
| 259 | // No matching string table found: hardcoded fallback to US |
| 260 | if (InitStringTable(strCode: "US" )) |
| 261 | return true; |
| 262 | // No string table present: this is really bad |
| 263 | LogNTr(level: spdlog::level::err, message: "Error loading language string table." ); |
| 264 | return false; |
| 265 | } |
| 266 | |
| 267 | bool C4Language::InitStringTable(const char *strCode) |
| 268 | { |
| 269 | C4Group hGroup; |
| 270 | // First, look in System.c4g |
| 271 | if (hGroup.Open(C4CFN_System)) |
| 272 | { |
| 273 | if (LoadStringTable(hGroup, strCode)) |
| 274 | { |
| 275 | hGroup.Close(); return true; |
| 276 | } |
| 277 | hGroup.Close(); |
| 278 | } |
| 279 | // Now look through the registered packs |
| 280 | C4Group *pPack; |
| 281 | for (int iPack = 0; pPack = Packs.GetGroup(iIndex: iPack); iPack++) |
| 282 | // Does it contain a System.c4g child group? |
| 283 | if (hGroup.OpenAsChild(pMother: pPack, C4CFN_System)) |
| 284 | { |
| 285 | if (LoadStringTable(hGroup, strCode)) |
| 286 | { |
| 287 | hGroup.Close(); return true; |
| 288 | } |
| 289 | hGroup.Close(); |
| 290 | } |
| 291 | // No matching string table found |
| 292 | return false; |
| 293 | } |
| 294 | |
| 295 | bool C4Language::LoadStringTable(C4Group &hGroup, const char *strCode) |
| 296 | { |
| 297 | // Compose entry name |
| 298 | char strEntry[_MAX_FNAME + 1]; |
| 299 | FormatWithNull(buf&: strEntry, fmt: "Language{}.txt" , args&: strCode); // ...should use C4CFN_Language here |
| 300 | // Load string table |
| 301 | StdStrBuf strTable; |
| 302 | if (!hGroup.LoadEntryString(szEntryName: strEntry, Buf&: strTable)) |
| 303 | { |
| 304 | hGroup.Close(); return false; |
| 305 | } |
| 306 | // Set string table |
| 307 | Application.ResStrTable.emplace(args&: strCode, args: strTable.getData()); |
| 308 | // Close group |
| 309 | hGroup.Close(); |
| 310 | // Set the internal charset |
| 311 | SCopy(szSource: LoadResStr(id: C4ResStrTableKey::IDS_LANG_CHARSET), sTarget: Config.General.LanguageCharset); |
| 312 | |
| 313 | #ifdef _WIN32 |
| 314 | Application.LogSystem.SetConsoleInputCharset(C4Config::GetCharsetCodePage(Config.General.LanguageCharset)); |
| 315 | #else |
| 316 | TextEncodingConverter.CreateConverters(charsetCodeName: C4Config::GetCharsetCodeName(charset: Config.General.LanguageCharset)); |
| 317 | #endif |
| 318 | // Success |
| 319 | return true; |
| 320 | } |
| 321 | |
| 322 | void C4Language::ClearLanguage() |
| 323 | { |
| 324 | // Clear resource string table |
| 325 | Application.ResStrTable.reset(); |
| 326 | } |
| 327 | |