mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-10 01:11:45 -10:00
239 lines
11 KiB
C++
239 lines
11 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
#include "../tools/engine/menu_assets.h"
|
|
#include <zlib.h>
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <string>
|
|
|
|
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<std::u16string>(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<char>(in)), std::istreambuf_iterator<char>());
|
|
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");
|
|
}
|