// SPDX-License-Identifier: GPL-3.0-or-later #include "../tools/engine/menu_assets.h" #include #include #include #include #include #include #include #include using namespace menu_assets; Bytes operator+(Bytes a, const Bytes& b) { a.insert(a.end(), b.begin(), b.end()); return a; } namespace { void Le(Bytes& b, std::uint64_t v, int n) { for (int i = 0; i < n; ++i) b.push_back(std::uint8_t(v >> (8 * i))); } Bytes Ascii(const std::string& s) { return Bytes(s.begin(), s.end()); } Bytes Wide(const std::u16string& s) { Bytes b; for (char16_t c : s) Le(b, c, 2); return b; } std::size_t Count(const Bytes& b, const std::string& what) { std::size_t n = 0; for (auto it = b.begin(); (it = std::search(it, b.end(), what.begin(), what.end())) != b.end(); ++it) ++n; return n; } Binder Bundle(const Fmg& fmg) { Binder b; b.header.assign(0x40, 0); std::memcpy(b.header.data(), "BND4", 4); b.header[0x20] = 0x24; b.header[0x30] = 1; b.entry_bytes = 0x24; b.wide_names = true; for (std::uint32_t id : {200u, 201u, 78u, 204u, 30u, 999u}) { BinderFile f; f.flags = 0xffffffff00000040ull; f.id = id; f.name = Wide(u"N:\\msg\\测试" + std::u16string(1, char16_t(u'0' + id % 10)) + u".fmg"); f.data = FmgWrite(fmg); b.files.push_back(f); } return b; } Bytes Packed(const Bytes& data) { Bytes h(0x4c, 0); std::memcpy(h.data(), "DCX\0", 4); std::memcpy(h.data() + 0x18, "DCS\0", 4); std::memcpy(h.data() + 0x24, "DCP\0DFLT", 8); std::memcpy(h.data() + 0x44, "DCA\0", 4); h[0x4b] = 8; return DcxWrite(menu_assets::Dcx{h, data}); } Bytes Tag(int code, const Bytes& body) { Bytes t; if (body.size() < 0x3f) Le(t, code << 6 | body.size(), 2); else Le(t, code << 6 | 0x3f, 2), Le(t, body.size(), 4); t.insert(t.end(), body.begin(), body.end()); return t; } Bytes Translate(int tx, int ty) { Bytes b(5, 0); std::uint64_t bits = 0; int at = 0; auto put = [&](std::uint64_t v, int n) { for (int i = n - 1; i >= 0; --i, ++at) bits |= (v >> i & 1) << (39 - at); }; put(0, 2), put(16, 5), put(std::uint16_t(tx), 16), put(std::uint16_t(ty), 16); for (int i = 0; i < 5; ++i) b[i] = std::uint8_t(bits >> (32 - 8 * i)); return b; } Bytes Place(int depth, int character, int tx, int ty, const std::string& name) { Bytes b{std::uint8_t((character ? 0x02 : 0x01) | 0x04 | (name.empty() ? 0 : 0x20))}; Le(b, depth, 2); if (character) Le(b, character, 2); const Bytes m = Translate(tx, ty); b.insert(b.end(), m.begin(), m.end()); if (!name.empty()) b.insert(b.end(), name.begin(), name.end()), b.push_back(0); return Tag(26, b); } Bytes Sprite(int id, int frames, const Bytes& tags) { Bytes b; Le(b, id, 2); Le(b, frames, 2); b.insert(b.end(), tags.begin(), tags.end()); b.insert(b.end(), {0, 0}); return Tag(39, b); } Bytes Movie() { const Bytes tags = Tag(2, {1, 0, 0}); Bytes title = Place(1, 1, 40, 40, "StaticText_113020") + Tag(1, {}); Bytes donor; for (int r = 0; r < 6; ++r) donor = donor + Place(56 + 9 * r, 1, 1840, 1060 + 1143 * r, "Item_" + std::to_string(r) + "_0"); donor = donor + Place(110, 1, 10820, 9415, "Item_6_0"); Bytes p3{0x06, 0x00, 117, 0, 2, 0}; const Bytes m = Translate(8937, 980); p3.insert(p3.end(), m.begin(), m.end()); donor = donor + Tag(70, p3) + Tag(1, {}); for (int r = 0; r < 6; ++r) donor = donor + Place(56 + 9 * r, 0, 1840, 1080 + 1143 * r, ""); donor = donor + Place(110, 0, 10820, 9435, "") + Tag(1, {}); Bytes top; for (int r = 0; r < 5; ++r) top = top + Place(31 + 7 * r, 1, 1481, -20 + 1353 * r, "Item_" + std::to_string(r) + "_0"); top = top + Tag(1, {}); Bytes list = Place(59, 1, 6211, 361, "ScrollBarV") + Tag(1, {}); Bytes classes; Le(classes, 1, 2), Le(classes, 3, 2); const std::string cls = "OptionSetting_fla._ControllSetting"; classes.insert(classes.end(), cls.begin(), cls.end()), classes.push_back(0); Bytes body = tags + Sprite(2, 1, title) + Sprite(3, 2, donor) + Sprite(4, 1, list) + Sprite(5, 1, top) + Place(382, 3, 10046, 4134, "ControllSetting") + Place(500, 5, 13754, 6089, "Top") + Tag(76, classes) + Tag(1, {}) + Tag(0, {}); Bytes head = Ascii("GFX"); head.push_back(11); Le(head, 0, 4); head.insert(head.end(), {0x08, 0x00}); Le(head, 30 << 8, 2), Le(head, 1, 2); Bytes all = head + body; for (int i = 0; i < 4; ++i) all[4 + i] = std::uint8_t(all.size() >> (8 * i)); return all; } } int main(int argc, char** argv) { const auto english = PcMessages(), chinese = PcMessages(true), russian = PcMessages(Language::Russian); assert(english.size() == chinese.size() && english.size() == russian.size() && english.size() > 150); assert(LanguageOf("rusru") == Language::Russian && LanguageOf("zhocn") == Language::Chinese && LanguageOf("enggb") == Language::English); assert(russian.at(110010).text == "Эффекты (ПК)" && russian.at(110015).help == "Закрыть игру и вернуться на рабочий стол"); assert(russian.at(119100).text == "Вперёд" && russian.at(119215).text.rfind(". Enter", 0) == 0); assert(russian.at(920000).text.find('\n') != std::string::npos); assert(russian.at(117010).text == "1280x720" && russian.at(116030).text == "30"); // untranslated rows stay English assert(russian.at(116020).help.empty()); std::size_t translated = 0; for (const auto& [id, m] : russian) translated += m.text != english.at(id).text; assert(translated > 120); // the table is translated, not copied from English assert(english.at(110010).text == "PC Effects" && chinese.at(110010).text == "PC 特效"); assert(chinese.at(110015).help == "关闭游戏并返回桌面。"); assert(chinese.at(117010).text == "1280x720"); assert(english.at(920000).text.find('\n') != std::string::npos); assert(chinese.at(119100).text == "向前移动" && chinese.at(119129).text == "调试菜单" && chinese.at(119204).text == "其他"); assert(english.at(119215).text.front() == ' '); assert(KindOf(110006) == Kind::Menu && KindOf(920001) == Kind::Question && KindOf(14000336) == Kind::Event); assert(Utf16("a😀") == std::u16string(u"a\U0001F600") && Utf16("한글") == u"한글"); Fmg fmg; fmg.header.assign(0x28, 0); fmg.header[2] = 2, fmg.header[8] = 1, fmg.header[0x14] = 0xff; fmg.text = {{42, u"原有文字 한글 😀"}, {43, std::nullopt}, {44, u"A가"}, {90, u"A가"}}; const Bytes fmg_bytes = FmgWrite(fmg); const Fmg back = FmgRead(fmg_bytes); assert(back.text == fmg.text && back.header[0x14] == 0xff && FmgWrite(back) == fmg_bytes); assert(fmg_bytes[0x0c] == 2 && fmg_bytes[0x10] == 4); const Binder bundle = Bundle(fmg); const Bytes packed = BinderWrite(bundle); const Binder reread = BinderRead(packed); assert(reread.files.size() == 6 && reread.wide_names); for (std::size_t i = 0; i < 6; ++i) { const std::size_t e = 0x40 + i * 0x24; assert((packed[e + 0x18] & 15) == 0 && reread.files[i].name == bundle.files[i].name && reread.files[i].data == bundle.files[i].data && reread.files[i].flags == bundle.files[i].flags); } assert(BinderWrite(reread) == packed); const Bytes dcx = Packed(packed); assert(DcxRead(dcx).data == packed); Bytes short_dcx = dcx; short_dcx[0x1f] ^= 1; bool refused = false; try { DcxRead(short_dcx); } catch (const Error&) { refused = true; } assert(refused); for (const Language language : {Language::English, Language::Chinese, Language::Russian}) { for (const bool in_dcx : {false, true}) { const Bytes made = MenuMessages(in_dcx ? dcx : packed, language); const Binder out = BinderRead(in_dcx ? DcxRead(made).data : made); assert(out.files.size() == 6); const auto& expect = language == Language::Chinese ? chinese : language == Language::Russian ? russian : english; for (const BinderFile& f : out.files) { if (f.id == 999) { assert(f.data == bundle.files.back().data); continue; } const Fmg got = FmgRead(f.data); assert(got.text.at(42) == fmg.text.at(42) && !got.text.at(43) && got.text.at(90) == u"A가"); const Kind kind = f.id == 30 ? Kind::Event : (f.id == 78 || f.id == 204) ? Kind::Question : Kind::Menu; for (const auto& [id, m] : expect) { if (KindOf(id) != kind) { assert(!got.text.count(id)); continue; } const std::string& s = f.id == 201 ? m.help : m.text; assert(got.text.at(id) == (s.empty() ? std::nullopt : std::optional(Utf16(s)))); } } } } Fmg taken = fmg; taken.text[110006] = u"mine"; refused = false; try { MenuMessages(BinderWrite(Bundle(taken))); } catch (const Error&) { refused = true; } assert(refused); const Bytes movie = OptionMovie(Movie()); for (const char* name : {"PCGraphics", "PCControls", "PCKeys", "PCEffects", "PCCamera", "PCEnhance", "PCColour", "PCLighting", "PCDetail", "PCLooks"}) assert(Count(movie, std::string(name) + '\0') == 1); for (const char* title : {"117020", "118020", "119020", "120020", "121020", "124020", "125020", "126020", "127020", "128020"}) assert(Count(movie, std::string("StaticText_") + title) == 1); assert(Count(movie, "StaticText_113020") == 1); assert(Count(movie, "Item_7_0") == 4); assert(Count(movie, "Item_6_0") == 1 + 1 + 4); assert(Count(movie, "Item_3_0") == 1 + 1 + 9); assert(Count(movie, "ScrollBarV") == 2); assert(Count(movie, "OptionSetting_fla._ControllSetting") == 11); assert(movie[4] == (movie.size() & 0xff) && movie[5] == (movie.size() >> 8 & 0xff)); refused = false; try { OptionMovie(movie); } catch (const Error&) { refused = true; } assert(refused); if (argc > 1) { std::ifstream in(argv[1], std::ios::binary); const Bytes dump((std::istreambuf_iterator(in)), std::istreambuf_iterator()); const Bytes made = OptionMovie(dump); assert(made.size() > dump.size() && Count(made, "StaticText_119020") == 1 && Count(made, "ScrollBarV") == 2); std::printf("the dump's movie: %zu -> %zu bytes\n", dump.size(), made.size()); } std::puts("PASS: menu assets: PC strings, FMG, BND4, DCX, messages in every group, the option movie"); }