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
33C4Language Languages;
34
35C4Language::C4Language()
36{
37 PackGroupLocation[0] = 0;
38}
39
40C4Language::~C4Language()
41{
42 Clear();
43}
44
45bool 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
85void 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
99C4GroupSet &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
179void 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
199void 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
236const 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
250bool 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
267bool 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
295bool 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
322void C4Language::ClearLanguage()
323{
324 // Clear resource string table
325 Application.ResStrTable.reset();
326}
327