Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
LanguageResources.cpp
Go to the documentation of this file.
2
3#include <charconv>
4#include <iterator>
5#include <utility>
6
7namespace monopoly::data
8{
9 namespace
10 {
11 constexpr std::u16string_view MissingMessageLiteral =
12 u" message not available. "
13 u"(1:^1, 2:^2, 3:^3, A:^A, P:^P, S:^S)";
14
15
16 [[nodiscard]] std::uint16_t readU16Le(
17 const std::byte* bytes) noexcept
18 {
19 return
20 static_cast<std::uint16_t>(
21 std::to_integer<std::uint8_t>(bytes[0])) |
22 static_cast<std::uint16_t>(
23 std::to_integer<std::uint8_t>(bytes[1]) << 8U);
24 }
25
26
27 [[nodiscard]] DataError textError(
28 DataErrorCode code,
29 std::string detail,
30 std::optional<DataTag> tag = std::nullopt)
31 {
32 return { code, {}, tag, std::move(detail) };
33 }
34
35
36 void appendDecimal(std::u16string& output, std::uint32_t value)
37 {
38 char buffer[16]{};
39 const auto result = std::to_chars(
40 std::begin(buffer),
41 std::end(buffer),
42 value);
43
44 for (auto cursor = std::begin(buffer); cursor != result.ptr; ++cursor)
45 {
46 output.push_back(static_cast<char16_t>(*cursor));
47 }
48 }
49 }
50
51
52 std::expected<std::u16string, DataError> decodeLegacyUtf16Le(
53 std::span<const std::byte> bytes)
54 {
55 if (bytes.empty() || bytes.size() % 2 != 0)
56 {
57 return std::unexpected(textError(
59 "legacy UTF-16LE payload must have a non-zero even size"));
60 }
61
62 std::u16string decoded;
63 decoded.reserve(bytes.size() / 2);
64 bool terminated = false;
65
66 for (std::size_t offset = 0; offset < bytes.size(); offset += 2)
67 {
68 const char16_t codeUnit =
69 static_cast<char16_t>(readU16Le(bytes.data() + offset));
70
71 if (terminated)
72 {
73 if (codeUnit != u'\0')
74 {
75 return std::unexpected(textError(
77 "non-zero data follows the UTF-16 terminator"));
78 }
79
80 continue;
81 }
82
83 if (codeUnit == u'\0')
84 {
85 terminated = true;
86 continue;
87 }
88
89 if (codeUnit >= 0xD800U && codeUnit <= 0xDBFFU)
90 {
91 if (offset + 3 >= bytes.size())
92 {
93 return std::unexpected(textError(
95 "high surrogate is not followed by a code unit"));
96 }
97
98 const char16_t low = static_cast<char16_t>(
99 readU16Le(bytes.data() + offset + 2));
100
101 if (low < 0xDC00U || low > 0xDFFFU)
102 {
103 return std::unexpected(textError(
105 "high surrogate is not followed by a low surrogate"));
106 }
107
108 decoded.push_back(codeUnit);
109 decoded.push_back(low);
110 offset += 2;
111 continue;
112 }
113
114 if (codeUnit >= 0xDC00U && codeUnit <= 0xDFFFU)
115 {
116 return std::unexpected(textError(
118 "unpaired low surrogate in UTF-16 payload"));
119 }
120
121 decoded.push_back(codeUnit);
122 }
123
124 if (!terminated)
125 {
126 return std::unexpected(textError(
128 "legacy string payload has no UTF-16 NUL terminator"));
129 }
130
131 return decoded;
132 }
133
134
135 std::expected<std::shared_ptr<LanguageCatalog>, DataError>
137 LanguageId language,
138 std::shared_ptr<LegacyDataArchive> textArchive)
139 {
140 if (findLanguageBankTriplet(language) == nullptr)
141 {
142 return std::unexpected(textError(
144 "language ID must be in the source-defined range 1..10"));
145 }
146
147 if (!textArchive || !textArchive->isOpen())
148 {
149 return std::unexpected(textError(
151 "language text archive is absent or closed"));
152 }
153
154 if (textArchive->group() !=
156 {
157 return std::unexpected(textError(
159 "language text archive must use DAT_LANG group 9"));
160 }
161
162 auto indexMetadata = textArchive->metadata(0);
163
164 if (!indexMetadata)
165 {
166 return std::unexpected(indexMetadata.error());
167 }
168
170 {
171 return std::unexpected(textError(
173 "language item tag 0 must be an index table",
174 DataTag{0}));
175 }
176
177 auto indexBytes = textArchive->load(0);
178
179 if (!indexBytes)
180 {
181 return std::unexpected(indexBytes.error());
182 }
183
184 auto parsedIndex = DataIndexTable::parse(**indexBytes);
185
186 if (!parsedIndex)
187 {
188 return std::unexpected(parsedIndex.error());
189 }
190
191 // L_Data ne validait ces references qu'au moment de l'usage. Le
192 // snapshot moderne les valide avant publication transactionnelle.
193 for (const auto& entry : parsedIndex->entries())
194 {
195 auto target = textArchive->metadata(entry.dataTag);
196
197 if (!target)
198 {
199 auto error = target.error();
200 error.detail =
201 "language index references a tag outside the DAT bank";
202 return std::unexpected(std::move(error));
203 }
204
205 if (target->type != LegacyDataType::String)
206 {
207 return std::unexpected(textError(
209 "language index target must be a DataString item",
210 entry.dataTag));
211 }
212 }
213
214 auto catalog = std::shared_ptr<LanguageCatalog>(
215 new LanguageCatalog());
216 catalog->language_ = language;
217 catalog->legacyArchive_ = std::move(textArchive);
218 catalog->maximumMessageId_ =
219 parsedIndex->entries().back().indexValue;
220 catalog->index_ = std::move(*parsedIndex);
221 return catalog;
222 }
223
224
225 std::expected<std::shared_ptr<LanguageCatalog>, DataError>
227 LanguageId language,
228 std::shared_ptr<const DataSource> source)
229 {
230 if (findLanguageBankTriplet(language) == nullptr)
231 {
232 return std::unexpected(textError(
234 "language ID must be in the source-defined range 1..10"));
235 }
236
237 if (!source)
238 {
239 return std::unexpected(textError(
241 "language DATA source is absent"));
242 }
243
244 const auto languageId = [](DataTag tag)
245 {
247 };
248
249 auto indexMetadata = source->metadata(languageId(0));
250 if (!indexMetadata)
251 return std::unexpected(indexMetadata.error());
252
254 {
255 return std::unexpected(textError(
257 "language item tag 0 must be an index table",
258 DataTag{0}));
259 }
260
261 auto indexBytes = source->load(languageId(0));
262 if (!indexBytes)
263 return std::unexpected(indexBytes.error());
264
265 auto parsedIndex = DataIndexTable::parse(**indexBytes);
266 if (!parsedIndex)
267 return std::unexpected(parsedIndex.error());
268
269 for (const auto& entry : parsedIndex->entries())
270 {
271 auto target = source->metadata(languageId(entry.dataTag));
272 if (!target)
273 {
274 auto error = target.error();
275 error.detail =
276 "language index references a tag outside the DATA source";
277 return std::unexpected(std::move(error));
278 }
279
280 if (target->type != LegacyDataType::String)
281 {
282 return std::unexpected(textError(
284 "language index target must be a DataString item",
285 entry.dataTag));
286 }
287 }
288
289 auto catalog = std::shared_ptr<LanguageCatalog>(
290 new LanguageCatalog());
291 catalog->language_ = language;
292 catalog->source_ = std::move(source);
293 catalog->maximumMessageId_ =
294 parsedIndex->entries().back().indexValue;
295 catalog->index_ = std::move(*parsedIndex);
296 return catalog;
297 }
298
299
301 {
302 return language_;
303 }
304
305
307 {
308 return maximumMessageId_;
309 }
310
311
312 std::expected<std::optional<SharedLanguageText>, DataError>
314 {
315 std::scoped_lock lock(mutex_);
316
317 if (const auto cached = cache_.find(messageId);
318 cached != cache_.end())
319 {
320 return std::optional<SharedLanguageText>{ cached->second };
321 }
322
323 const auto tag = index_.find(messageId);
324
325 if (!tag)
326 {
327 return std::optional<SharedLanguageText>{};
328 }
329
330 std::expected<SharedDataBytes, DataError> bytes =
331 source_
332 ? source_->load(packDataId(
334 : legacyArchive_->load(*tag);
335
336 if (!bytes)
337 {
338 return std::unexpected(bytes.error());
339 }
340
342
343 if (!decoded)
344 {
345 auto error = decoded.error();
346 if (legacyArchive_)
347 error.path = legacyArchive_->path();
348 error.tag = *tag;
349 return std::unexpected(std::move(error));
350 }
351
352 auto text = std::make_shared<const std::u16string>(
353 std::move(*decoded));
354 cache_.emplace(messageId, text);
355 return std::optional<SharedLanguageText>{ std::move(text) };
356 }
357
358
359 std::expected<SharedLanguageText, DataError>
361 {
362 auto direct = lookup(messageId);
363
364 if (!direct)
365 {
366 return std::unexpected(direct.error());
367 }
368
369 if (*direct)
370 {
371 return **direct;
372 }
373
374 std::u16string fallback;
375 fallback.push_back(u'#');
377
378 if (messageId != 0)
379 {
380 auto defaultMessage = lookup(0);
381
382 if (!defaultMessage)
383 {
384 return std::unexpected(defaultMessage.error());
385 }
386
387 if (*defaultMessage)
388 {
389 fallback.append(***defaultMessage);
390 }
391 else
392 {
394 }
395 }
396 else
397 {
399 }
400
401 return std::make_shared<const std::u16string>(std::move(fallback));
402 }
403
404
405 std::expected<SharedLanguageText, DataError>
407 {
408 auto source = message(messageId);
409
410 if (!source)
411 {
412 return std::unexpected(source.error());
413 }
414
415 auto cleaned = std::make_shared<std::u16string>(**source);
416
417 // LANG_CleanString reculait le pointeur seulement tant qu'il etait
418 // strictement apres le premier code unit. Une chaine entierement
419 // blanche conserve donc historiquement son premier caractere.
420 while (cleaned->size() > 1)
421 {
422 const char16_t character = cleaned->back();
423
424 if (character >= 32 && character != u' ')
425 {
426 break;
427 }
428
429 cleaned->pop_back();
430 }
431
432 return std::const_pointer_cast<const std::u16string>(cleaned);
433 }
434
435
436 std::expected<void, DataError> LanguageService::select(
437 const std::filesystem::path& legacyRoot,
438 LanguageId language,
439 ArchiveOpenOptions options)
440 {
441 const auto* definitions = findLanguageBankTriplet(language);
442
443 if (definitions == nullptr)
444 {
445 return std::unexpected(textError(
447 "language ID must be in the source-defined range 1..10"));
448 }
449
451 for (const auto* definition :
452 { &definitions->text, &definitions->graphics, &definitions->dialog })
453 {
454 auto mounted = staged.mount(
455 legacyRoot / std::filesystem::path(definition->legacyPath),
456 legacyGroupValue(definition->group), options);
457 if (!mounted)
458 {
459 return std::unexpected(mounted.error());
460 }
461 }
462 return select(staged, language);
463 }
464
465
466 std::expected<void, DataError> LanguageService::select(
468 LanguageId language)
469 {
470 const auto* definitions = findLanguageBankTriplet(language);
471 if (definitions == nullptr)
472 {
473 return std::unexpected(textError(
475 "language ID must be in the source-defined range 1..10"));
476 }
477
478 auto text = registry.archive(
479 legacyGroupValue(definitions->text.group));
480
481 if (!text)
482 {
483 return std::unexpected(text.error());
484 }
485
486 auto media = registry.archive(
487 legacyGroupValue(definitions->graphics.group));
488
489 if (!media)
490 {
491 return std::unexpected(media.error());
492 }
493
494 auto dialog = registry.archive(
495 legacyGroupValue(definitions->dialog.group));
496
497 if (!dialog)
498 {
499 return std::unexpected(dialog.error());
500 }
501
502 if (!(*media)->isOpen() || !(*dialog)->isOpen())
503 {
504 return std::unexpected(textError(
506 "language media or dialog archive is closed"));
507 }
508
509 auto catalog = LanguageCatalog::open(language, *text);
510
511 if (!catalog)
512 {
513 auto error = catalog.error();
514 error.path = (*text)->path();
515 return std::unexpected(std::move(error));
516 }
517
518 auto staged = std::make_shared<LanguageSnapshot>(LanguageSnapshot
519 {
520 language,
521 *text,
522 *media,
523 *dialog,
524 *catalog
525 });
526
527 std::scoped_lock lock(mutex_);
528 active_ = std::move(staged);
529 return {};
530 }
531
532
533 std::expected<void, DataError> LanguageService::select(
535 std::shared_ptr<const DataSource> source,
536 LanguageId language)
537 {
538 const auto* definitions = findLanguageBankTriplet(language);
539 if (definitions == nullptr)
540 {
541 return std::unexpected(textError(
543 "language ID must be in the source-defined range 1..10"));
544 }
545
546 if (!source)
547 {
548 return std::unexpected(textError(
550 "language DATA source is absent"));
551 }
552
553 auto text = registry.archive(
554 legacyGroupValue(definitions->text.group));
555 if (!text)
556 return std::unexpected(text.error());
557
558 auto media = registry.archive(
559 legacyGroupValue(definitions->graphics.group));
560 if (!media)
561 return std::unexpected(media.error());
562
563 auto dialog = registry.archive(
564 legacyGroupValue(definitions->dialog.group));
565 if (!dialog)
566 return std::unexpected(dialog.error());
567
568 if (!(*text)->isOpen() ||
569 !(*media)->isOpen() ||
570 !(*dialog)->isOpen())
571 {
572 return std::unexpected(textError(
574 "language archive compatibility owners are closed"));
575 }
576
577 auto catalog = LanguageCatalog::open(
578 language,
579 std::move(source));
580 if (!catalog)
581 {
582 auto error = catalog.error();
583 if (error.path.empty())
584 error.path = (*text)->path();
585 return std::unexpected(std::move(error));
586 }
587
588 auto staged = std::make_shared<LanguageSnapshot>(LanguageSnapshot
589 {
590 language,
591 *text,
592 *media,
593 *dialog,
594 *catalog
595 });
596
597 std::scoped_lock lock(mutex_);
598 active_ = std::move(staged);
599 return {};
600 }
601
602
603 std::shared_ptr<const LanguageSnapshot>
605 {
606 std::scoped_lock lock(mutex_);
607 return active_;
608 }
609
610
612 {
613 std::scoped_lock lock(mutex_);
614 active_.reset();
615 }
616}
std::size_t offset
std::string_view text
std::vector< std::byte > bytes
std::shared_ptr< const data::HmdTextureImage > source
unsigned index
std::string error
std::deque< Bytes > output
std::uint16_t value
Definition Timers.cpp:16
std::expected< std::shared_ptr< LegacyDataArchive >, DataError > mount(const std::filesystem::path &path, std::uint16_t group, ArchiveOpenOptions options={})
std::optional< DataTag > find(std::uint32_t indexValue) const noexcept
static std::expected< DataIndexTable, DataError > parse(std::span< const std::byte > bytes)
LanguageId language() const noexcept
std::uint32_t maximumMessageId() const noexcept
static std::expected< std::shared_ptr< LanguageCatalog >, DataError > open(LanguageId language, std::shared_ptr< LegacyDataArchive > textArchive)
std::expected< SharedLanguageText, DataError > cleanMessage(std::uint32_t messageId) const
std::expected< SharedLanguageText, DataError > message(std::uint32_t messageId) const
std::expected< std::optional< SharedLanguageText >, DataError > lookup(std::uint32_t messageId) const
std::shared_ptr< const LanguageSnapshot > snapshot() const noexcept
std::expected< void, DataError > select(const std::filesystem::path &legacyRoot, LanguageId language, ArchiveOpenOptions options={})
std::uint16_t DataTag
Definition DataBanks.hpp:11
constexpr std::uint16_t legacyGroupValue(LegacyGroupId group) noexcept
Definition DataBanks.hpp:30
std::expected< std::u16string, DataError > decodeLegacyUtf16Le(std::span< const std::byte > bytes)
const LanguageBankTriplet * findLanguageBankTriplet(LanguageId language) noexcept
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39