layout: restructure into src/ tests/ android/ scripts/ tools/

- extension/src/{port,platform,codepage} -> src/; native_render -> src/host
  (+ dxt, shaders/); libgr2 -> src/gr2; extension/third_party -> third_party
- extension/tests -> tests/port, libgr2/tests -> tests/gr2
- android-native -> android (build.sh, push-client.sh moved in)
- script -> scripts; tools/40250 -> tools/server; oracle -> tools/granny-oracle;
  perf tools -> tools/perf
- all build trees under build/ (native, release, android, port-gate)
- xrender:: CMake aliases -> mt::; port-map ledger impl paths rewritten

No code changes. ctest 15/15, port_gate macos+android PASS, port_map check 0 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-29 19:08:19 +09:00
co-authored by Claude Opus 5.5
parent 70710477cf
commit a46093104c
2817 changed files with 13728 additions and 13744 deletions
+56
View File
@@ -0,0 +1,56 @@
#include <gr2/types.h>
#include <cmath>
#include <cstdio>
#include <vector>
int main() {
// ScaleShearCurve uses 9 values per control point. A quadratic span reads
// three control points, so this is the regression case for the former 3*4
// stack buffer in Curve::eval().
gr2::Curve curve;
curve.degree = 2;
curve.dim = 9;
curve.knots = {0.0f, 1.0f, 2.0f};
curve.controls.resize(27);
for (size_t i = 0; i < curve.controls.size(); ++i) {
curve.controls[i] = 1.0f + static_cast<float>(i) * 0.01f;
}
float out[9] = {};
curve.eval(0.5f, out);
for (float value : out) {
if (!std::isfinite(value)) {
std::fprintf(stderr, "non-finite 9D curve output\n");
return 1;
}
}
// BoneTrack::sample fills SRT components and flags; sample_local = compose(sample).
gr2::BoneTrack track;
track.position.degree = 1;
track.position.dim = 3;
track.position.knots = {0.0f, 1.0f};
track.position.controls = {0.0f, 0.0f, 0.0f, 2.0f, 4.0f, 6.0f};
track.orientation.degree = 0;
track.orientation.dim = 4;
track.orientation.knots = {0.0f};
track.orientation.controls = {0.0f, 0.0f, 0.70710678f, 0.70710678f}; // 90 deg about Z
gr2::Transform x;
track.sample(0.5f, x);
if (x.flags != 3 || std::fabs(x.position[0] - 1.0f) > 1e-5f || std::fabs(x.position[2] - 3.0f) > 1e-5f) {
std::fprintf(stderr, "BoneTrack::sample flags=%u pos=%f,%f,%f\n", x.flags, x.position[0], x.position[1], x.position[2]);
return 1;
}
gr2::Animation anim;
anim.tracks.push_back(track);
std::vector<gr2::Mat4> local;
anim.sample_local(0.5f, local);
const gr2::Mat4 m = gr2::compose(x);
// Row-major composite: row 0 is the rotated X axis (0,1,0); translation in 12..14.
if (local.size() != 1 || local[0] != m || std::fabs(m[1] - 1.0f) > 1e-5f || std::fabs(m[12] - 1.0f) > 1e-5f) {
std::fprintf(stderr, "compose mismatch m[1]=%f m[12]=%f\n", m[1], m[12]);
return 1;
}
return 0;
}
+63
View File
@@ -0,0 +1,63 @@
#include <gr2/gr2.h>
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
namespace {
void wr32(std::vector<uint8_t> &bytes, size_t off, uint32_t value) {
bytes[off + 0] = uint8_t(value);
bytes[off + 1] = uint8_t(value >> 8);
bytes[off + 2] = uint8_t(value >> 16);
bytes[off + 3] = uint8_t(value >> 24);
}
std::vector<uint8_t> granny_header(size_t size, uint32_t section_count) {
std::vector<uint8_t> bytes(size, 0);
const uint32_t magic[] = { 0xCAB067B8, 0x0FB16DF8, 0x7E8C7284, 0x1E00195E };
for (int i = 0; i < 4; ++i) {
wr32(bytes, size_t(i) * 4, magic[i]);
}
constexpr size_t h = 32;
wr32(bytes, h + 0, 6); // Version
wr32(bytes, h + 4, uint32_t(size));
wr32(bytes, h + 12, 40); // section table starts at byte 72
wr32(bytes, h + 16, section_count);
// Root type/object remain section 0, offset 0. With no section this must be
// rejected by the FileInfo stage rather than returned as an empty File.
return bytes;
}
bool expect_failure(const std::vector<uint8_t> &bytes, const char *stage) {
gr2::LoadError err;
auto file = gr2::File::load(bytes.data(), bytes.size(), &err);
if (file || err.stage != stage) {
std::fprintf(stderr, "expected failure stage '%s', got file=%d stage='%s' message='%s'\n",
stage, file.has_value(), err.stage.c_str(), err.message.c_str());
return false;
}
return true;
}
} // namespace
int main() {
bool ok = true;
ok &= expect_failure(std::vector<uint8_t>(80, 0), "header");
ok &= expect_failure(granny_header(80, 0), "fileinfo");
auto unsupported = granny_header(160, 1);
constexpr size_t section = 72;
wr32(unsupported, section + 0, 4); // unsupported BitKnit
wr32(unsupported, section + 4, uint32_t(unsupported.size()));
wr32(unsupported, section + 12, 1); // non-empty expanded section
ok &= expect_failure(unsupported, "section");
auto bad_offset = granny_header(160, 1);
wr32(bad_offset, section + 4, UINT32_MAX);
ok &= expect_failure(bad_offset, "section");
return ok ? 0 : 1;
}
+323
View File
@@ -0,0 +1,323 @@
// Ported from ClientVS22/source/EterBase/cipher.cpp (Crypto++ 8.4.0).
#include "classic_cipher.h"
#include <cryptopp/cryptlib.h>
#include <cryptopp/modes.h>
#include <cryptopp/nbtheory.h>
#include <cryptopp/osrng.h>
#include <cryptopp/secblock.h>
#include <cryptopp/dh.h>
#include <cryptopp/dh2.h>
#include <cryptopp/aes.h>
#include <cryptopp/blowfish.h>
#include <cryptopp/camellia.h>
#include <cryptopp/cast.h>
#include <cryptopp/des.h>
#include <cryptopp/idea.h>
#include <cryptopp/mars.h>
#include <cryptopp/rc5.h>
#include <cryptopp/rc6.h>
#include <cryptopp/seed.h>
#include <cryptopp/serpent.h>
#include <cryptopp/shacal2.h>
#include <cryptopp/tea.h>
#include <cryptopp/twofish.h>
#include <algorithm>
#include <cstring>
#include <memory>
using namespace CryptoPP;
namespace mtnet::classic {
namespace {
// Block cipher algorithm selector abstract base class.
struct BlockCipherAlgorithm {
enum {
kDefault, // to give more chances to default algorithm
kRC6,
kMARS,
kTwofish,
kSerpent,
kCAST256,
kIDEA,
k3DES, // DES-EDE2
kCamellia,
kSEED,
kRC5,
kBlowfish,
kTEA,
kSHACAL2,
kMaxAlgorithms
};
BlockCipherAlgorithm() = default;
virtual ~BlockCipherAlgorithm() = default;
static BlockCipherAlgorithm *Pick(int hint);
virtual int GetBlockSize() const = 0;
virtual int GetDefaultKeyLength() const = 0;
virtual SymmetricCipher *CreateEncoder(const CryptoPP::byte *key, size_t keylen,
const CryptoPP::byte *iv) const = 0;
virtual SymmetricCipher *CreateDecoder(const CryptoPP::byte *key, size_t keylen,
const CryptoPP::byte *iv) const = 0;
};
template <class T>
struct BlockCipherDetail : public BlockCipherAlgorithm {
int GetBlockSize() const override { return T::BLOCKSIZE; }
int GetDefaultKeyLength() const override { return T::DEFAULT_KEYLENGTH; }
SymmetricCipher *CreateEncoder(const CryptoPP::byte *key, size_t keylen,
const CryptoPP::byte *iv) const override {
return new typename CTR_Mode<T>::Encryption(key, keylen, iv);
}
SymmetricCipher *CreateDecoder(const CryptoPP::byte *key, size_t keylen,
const CryptoPP::byte *iv) const override {
return new typename CTR_Mode<T>::Decryption(key, keylen, iv);
}
};
BlockCipherAlgorithm *BlockCipherAlgorithm::Pick(int hint) {
BlockCipherAlgorithm *detail;
int selector = hint % kMaxAlgorithms;
switch (selector) {
case kRC6: detail = new BlockCipherDetail<RC6>(); break;
case kMARS: detail = new BlockCipherDetail<MARS>(); break;
case kTwofish: detail = new BlockCipherDetail<Twofish>(); break;
case kSerpent: detail = new BlockCipherDetail<Serpent>(); break;
case kCAST256: detail = new BlockCipherDetail<CAST256>(); break;
case kIDEA: detail = new BlockCipherDetail<IDEA>(); break;
case k3DES: detail = new BlockCipherDetail<DES_EDE2>(); break;
case kCamellia: detail = new BlockCipherDetail<Camellia>(); break;
case kSEED: detail = new BlockCipherDetail<SEED>(); break;
case kRC5: detail = new BlockCipherDetail<RC5>(); break;
case kBlowfish: detail = new BlockCipherDetail<Blowfish>(); break;
case kTEA: detail = new BlockCipherDetail<TEA>(); break;
case kSHACAL2: detail = new BlockCipherDetail<SHACAL2>(); break;
case kDefault:
default: detail = new BlockCipherDetail<Twofish>(); break; // default algorithm
}
return detail;
}
} // namespace
// Key agreement scheme abstract class.
class KeyAgreement {
public:
KeyAgreement() = default;
virtual ~KeyAgreement() = default;
virtual size_t Prepare(void *buffer, size_t *length) = 0;
virtual bool Agree(size_t agreed_length, const void *buffer, size_t length) = 0;
const SecByteBlock &shared() const { return shared_; }
protected:
SecByteBlock shared_;
};
namespace {
// Crypto++ Unified Diffie-Hellman key agreement scheme.
class DH2KeyAgreement : public KeyAgreement {
public:
DH2KeyAgreement() : dh_(), dh2_(dh_) {}
size_t Prepare(void *buffer, size_t *length) override;
bool Agree(size_t agreed_length, const void *buffer, size_t length) override;
private:
DH dh_;
DH2 dh2_;
SecByteBlock spriv_key_;
SecByteBlock epriv_key_;
};
size_t DH2KeyAgreement::Prepare(void *buffer, size_t *length) {
// RFC 5114, 1024-bit MODP Group with 160-bit Prime Order Subgroup.
Integer p("0xB10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C6"
"9A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C0"
"13ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD70"
"98488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0"
"A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708"
"DF1FB2BC2E4A4371");
Integer g("0xA4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507F"
"D6406CFF14266D31266FEA1E5C41564B777E690F5504F213"
"160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1"
"909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28A"
"D662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24"
"855E6EEB22B3B2E5");
Integer q("0xF518AA8781A8DF278ABA4E7D64B7CB9D49462353");
AutoSeededRandomPool rnd;
dh_.AccessGroupParameters().Initialize(p, q, g);
if (!dh_.GetGroupParameters().ValidateGroup(rnd, 3)) {
return 0;
}
p = dh_.GetGroupParameters().GetModulus();
q = dh_.GetGroupParameters().GetSubgroupOrder();
g = dh_.GetGroupParameters().GetGenerator();
Integer v = ModularExponentiation(g, q, p);
if (v != Integer::One()) {
return 0;
}
spriv_key_.New(dh2_.StaticPrivateKeyLength());
epriv_key_.New(dh2_.EphemeralPrivateKeyLength());
SecByteBlock spub_key(dh2_.StaticPublicKeyLength());
SecByteBlock epub_key(dh2_.EphemeralPublicKeyLength());
dh2_.GenerateStaticKeyPair(rnd, spriv_key_, spub_key);
dh2_.GenerateEphemeralKeyPair(rnd, epriv_key_, epub_key);
const size_t spub_key_length = spub_key.size();
const size_t epub_key_length = epub_key.size();
const size_t data_length = spub_key_length + epub_key_length;
if (*length < data_length) {
return 0;
}
*length = data_length;
CryptoPP::byte *buf = (CryptoPP::byte *)buffer;
std::memcpy(buf, spub_key.BytePtr(), spub_key_length);
std::memcpy(buf + spub_key_length, epub_key.BytePtr(), epub_key_length);
return dh2_.AgreedValueLength();
}
bool DH2KeyAgreement::Agree(size_t agreed_length, const void *buffer, size_t length) {
if (agreed_length != dh2_.AgreedValueLength()) {
return false;
}
const size_t spub_key_length = dh2_.StaticPublicKeyLength();
const size_t epub_key_length = dh2_.EphemeralPublicKeyLength();
if (length != (spub_key_length + epub_key_length)) {
return false;
}
shared_.New(dh2_.AgreedValueLength());
const CryptoPP::byte *buf = (const CryptoPP::byte *)buffer;
if (!dh2_.Agree(shared_, spriv_key_, epriv_key_, buf, buf + spub_key_length)) {
return false;
}
return true;
}
} // namespace
// --------------------------------------------------------------------- ClassicCipher
ClassicCipher::ClassicCipher() = default;
ClassicCipher::~ClassicCipher() {
clean_up();
}
void ClassicCipher::clean_up() {
delete encoder_;
encoder_ = nullptr;
delete decoder_;
decoder_ = nullptr;
delete key_agreement_;
key_agreement_ = nullptr;
activated_ = false;
}
void ClassicCipher::encrypt(void *buffer, size_t length) {
if (!activated_ || !encoder_) {
return;
}
encoder_->ProcessData((CryptoPP::byte *)buffer, (const CryptoPP::byte *)buffer, length);
}
void ClassicCipher::decrypt(void *buffer, size_t length) {
if (!activated_ || !decoder_) {
return;
}
decoder_->ProcessData((CryptoPP::byte *)buffer, (const CryptoPP::byte *)buffer, length);
}
size_t ClassicCipher::prepare(void *buffer, size_t *length) {
assert(key_agreement_ == nullptr);
key_agreement_ = new DH2KeyAgreement();
size_t agreed_length = key_agreement_->Prepare(buffer, length);
if (agreed_length == 0) {
delete key_agreement_;
key_agreement_ = nullptr;
}
return agreed_length;
}
bool ClassicCipher::activate(bool polarity, size_t agreed_length, const void *buffer,
size_t length) {
assert(!activated_);
if (!key_agreement_) {
return false;
}
bool result = false;
if (key_agreement_->Agree(agreed_length, buffer, length)) {
result = set_up(polarity);
}
// NOTE: unlike the upstream Cipher, we keep key_agreement_ alive until here
// only; set_up() reads shared() before we free it below.
delete key_agreement_;
key_agreement_ = nullptr;
return result;
}
bool ClassicCipher::set_up(bool polarity) {
const SecByteBlock &shared = key_agreement_->shared();
if (shared.size() < 2) {
return false;
}
int hint_0 = shared.BytePtr()[*(shared.BytePtr()) % shared.size()];
int hint_1 = shared.BytePtr()[*(shared.BytePtr() + 1) % shared.size()];
std::unique_ptr<BlockCipherAlgorithm> algorithm_0(BlockCipherAlgorithm::Pick(hint_0));
std::unique_ptr<BlockCipherAlgorithm> algorithm_1(BlockCipherAlgorithm::Pick(hint_1));
const size_t key_length_0 = algorithm_0->GetDefaultKeyLength();
const size_t iv_length_0 = algorithm_0->GetBlockSize();
if (shared.size() < key_length_0 || shared.size() < iv_length_0) {
return false;
}
const size_t key_length_1 = algorithm_1->GetDefaultKeyLength();
const size_t iv_length_1 = algorithm_1->GetBlockSize();
if (shared.size() < key_length_1 || shared.size() < iv_length_1) {
return false;
}
SecByteBlock key_0(key_length_0), iv_0(iv_length_0);
SecByteBlock key_1(key_length_1), iv_1(iv_length_1);
size_t offset;
key_0.Assign(shared, key_length_0);
offset = std::min(key_length_0, shared.size() - key_length_1);
key_1.Assign(shared.BytePtr() + offset, key_length_1);
offset = shared.size() - iv_length_0;
iv_0.Assign(shared.BytePtr() + offset, iv_length_0);
offset = (offset < iv_length_1 ? 0 : offset - iv_length_1);
iv_1.Assign(shared.BytePtr() + offset, iv_length_1);
if (polarity) {
encoder_ = algorithm_1->CreateEncoder(key_1, key_1.size(), iv_1);
decoder_ = algorithm_0->CreateDecoder(key_0, key_0.size(), iv_0);
} else {
encoder_ = algorithm_0->CreateEncoder(key_0, key_0.size(), iv_0);
decoder_ = algorithm_1->CreateDecoder(key_1, key_1.size(), iv_1);
}
return encoder_ != nullptr && decoder_ != nullptr;
}
} // namespace mtnet::classic
+52
View File
@@ -0,0 +1,52 @@
#pragma once
// ClassicCipher — 40250 _IMPROVED_PACKET_ENCRYPTION_ (docs/CLIENT-40250-PORT.md §3).
// Ported near-verbatim from ClientVS22/source/EterBase/cipher.{h,cpp}
// (Themida / stdafx / logging stripped). Needs Crypto++ 8.4.0 (mt3p::cryptopp).
//
// Prepare(buf,&len) -> generate this end's DH2 public blob (spub||epub),
// returns the agreed-value length (0 = failure).
// Activate(polarity, agreed_len, peer_blob, peer_len)
// -> DH2 Agree() -> derive 2 hint-selected block ciphers in
// CTR mode, wire encoder_/decoder_ per polarity.
// **client polarity = true**, server = false.
// Encrypt/Decrypt(buf,len) in place, no padding (CTR stream).
// activated() flips true only after set_activated(true) (on GC_KEY_AGREEMENT_COMPLETED).
#include <cassert>
#include <cstddef>
namespace CryptoPP {
class SymmetricCipher;
}
namespace mtnet::classic {
class KeyAgreement;
class ClassicCipher {
public:
ClassicCipher();
~ClassicCipher();
void clean_up();
size_t prepare(void *buffer, size_t *length);
bool activate(bool polarity, size_t agreed_length, const void *buffer, size_t length);
void encrypt(void *buffer, size_t length);
void decrypt(void *buffer, size_t length);
bool activated() const { return activated_; }
void set_activated(bool value) { activated_ = value; }
bool key_ready() const { return encoder_ != nullptr && decoder_ != nullptr; }
private:
bool set_up(bool polarity);
bool activated_ = false;
CryptoPP::SymmetricCipher *encoder_ = nullptr;
CryptoPP::SymmetricCipher *decoder_ = nullptr;
KeyAgreement *key_agreement_ = nullptr;
};
} // namespace mtnet::classic
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+214
View File
@@ -0,0 +1,214 @@
// codepage_test — mt_codepage against Microsoft's WindowsBestFit source tables, plus spot checks of the
// characters each shipped 40250 locale needs.
//
// Argument: the directory holding bestfit<cp>.txt (scripts/gen_codepage_tables.py downloads them to
// build/bestfit). Without it only the spot checks run and the test exits 77 (ctest SKIP), unless
// MT_ASSETS_STRICT=1.
#include "codepage.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static std::u32string dec(unsigned cp, const char* bytes, bool* ok = nullptr)
{
std::u32string out;
const bool r = mt_codepage::decode(cp, reinterpret_cast<const unsigned char*>(bytes), std::strlen(bytes), out);
if (ok) *ok = r;
return out;
}
static std::string enc(unsigned cp, std::u32string in, bool best_fit = false, bool* used_default = nullptr)
{
std::string out;
bool used = false;
mt_codepage::encode(cp, in, out, best_fit, &used);
if (used_default) *used_default = used;
return out;
}
static void spot_checks()
{
// zh (936): "中文" D6D0 CEC4, and the Windows-only single byte 0x80 = euro.
CHECK(dec(936, "\xd6\xd0\xce\xc4") == U"中文");
CHECK(enc(936, U"中文") == "\xd6\xd0\xce\xc4");
CHECK(dec(936, "\x80") == U"€");
// GBK extension beyond GB2312 (U+4E02 = 81 40), both directions.
CHECK(dec(936, "\x81\x40") == U"丂");
CHECK(enc(936, U"丂") == "\x81\x40");
// A lead byte with nothing after it is one default char and reports failure.
bool ok = true;
CHECK(dec(936, "a\xd6", &ok) == U"a?" && !ok);
// An unmapped pair is consumed whole (932 uses U+30FB as its default).
CHECK(dec(932, "\x85\x40") == U"・");
CHECK(dec(932, "\x82\xa0") == U"あ");
// The shipped European locales.
CHECK(dec(1252, "\x80\xe9") == U"€é"); // en/de/fr/...: euro, é
CHECK(dec(1252, "\x81") == U"\u0081"); // undefined byte keeps its value, as Windows
CHECK(dec(1250, "\xb9\xb3\x9c") == U"ąłś"); // pl/cz/hu/ro: ą ł ś
CHECK(dec(1251, "\xc6\xe8") == U"Жи"); // ru: Жи
CHECK(dec(1253, "\xd9\xe1") == U"Ωα"); // gr: Ωα
CHECK(dec(1254, "\xf0\xfe\xdd") == U"ğşİ"); // tr: ğ ş İ
CHECK(enc(1250, U"ąłś") == "\xb9\xb3\x9c");
CHECK(enc(1251, U"Жи") == "\xc6\xe8");
CHECK(enc(1253, U"Ωα") == "\xd9\xe1");
CHECK(enc(1254, U"ğşİ") == "\xf0\xfe\xdd");
// Best fit only when WC_NO_BEST_FIT_CHARS is absent: 1252 has no "Ā", its best fit is "A".
bool used = false;
CHECK(enc(1252, U"Ā", true, &used) == "A" && !used);
CHECK(enc(1252, U"Ā", false, &used) == "?" && used);
// Unmapped either way; beyond the BMP too.
CHECK(enc(1251, U"中", true, &used) == "?" && used);
CHECK(enc(936, U"\U0001F600", true, &used) == "?" && used);
CHECK(mt_codepage::is_lead_byte(936, 0x81) && !mt_codepage::is_lead_byte(936, 0x80));
CHECK(mt_codepage::is_lead_byte(932, 0xe0) && !mt_codepage::is_lead_byte(932, 0xa1));
CHECK(!mt_codepage::is_lead_byte(1250, 0xb9));
CHECK(!mt_codepage::supported(65001) && !mt_codepage::supported(437));
}
struct SourceTable {
unsigned mb_default = 0x3f;
std::map<unsigned, unsigned> decode; // bytes (pair = lead << 8 | trail) -> wc
std::map<unsigned, unsigned> wctable;
std::vector<std::pair<unsigned, unsigned>> leads;
};
static bool load(const std::string& path, SourceTable& t)
{
std::ifstream in(path, std::ios::binary);
if (!in) return false;
std::string raw, section;
unsigned lead = 0;
while (std::getline(in, raw)) {
std::string line = raw.substr(0, raw.find(';'));
std::istringstream words(line);
std::string head;
if (!(words >> head)) continue;
if (head == "CPINFO") {
std::string size, byte_default, wc_default;
words >> size >> byte_default >> wc_default;
t.mb_default = unsigned(std::stoul(wc_default, nullptr, 16));
continue;
}
if (head == "MBTABLE" || head == "WCTABLE" || head == "DBCSRANGE") { section = head; continue; }
if (head == "DBCSTABLE") {
section = head;
lead = unsigned(std::stoul(raw.substr(raw.find("0x", raw.find("LeadByte"))), nullptr, 16));
continue;
}
if (head == "ENDCODEPAGE") break;
if (head.rfind("0x", 0) != 0) continue;
std::string second;
words >> second;
const unsigned a = unsigned(std::stoul(head, nullptr, 16));
const unsigned b = unsigned(std::stoul(second, nullptr, 16));
if (raw.find("Lead Byte Range") != std::string::npos) t.leads.push_back({a, b});
else if (section == "MBTABLE") t.decode[a] = b;
else if (section == "DBCSTABLE") t.decode[lead << 8 | a] = b;
else if (section == "WCTABLE") t.wctable[a] = b;
}
return !t.decode.empty() && !t.wctable.empty();
}
static std::string bytes_of(unsigned mb)
{
return mb > 0xff ? std::string{char(mb >> 8), char(mb & 0xff)} : std::string(1, char(mb));
}
// Every byte / pair the source defines decodes to it, every other one to the default char; every
// WCTABLE entry encodes to it with best fit, and without best fit only when it round-trips.
static int against_source(const std::string& dir)
{
const unsigned cps[] = {874, 932, 936, 949, 950, 1250, 1251, 1252, 1253, 1254, 1255, 1256, 1257, 1258};
int loaded = 0;
for (unsigned cp : cps) {
SourceTable t;
if (!load(dir + "/bestfit" + std::to_string(cp) + ".txt", t)) continue;
++loaded;
const int before = g_failures;
auto is_lead = [&](unsigned b) {
for (auto& r : t.leads) if (b >= r.first && b <= r.second) return true;
return false;
};
for (unsigned b = 0; b < 256; ++b) {
CHECK(mt_codepage::is_lead_byte(cp, (unsigned char)b) == is_lead(b));
if (is_lead(b)) {
for (unsigned trail = 1; trail < 256; ++trail) {
const unsigned char pair[2] = {(unsigned char)b, (unsigned char)trail};
std::u32string out;
const bool ok = mt_codepage::decode(cp, pair, 2, out);
auto it = t.decode.find(b << 8 | trail);
const unsigned want = it == t.decode.end() ? t.mb_default : it->second;
if (out.size() != 1 || out[0] != want || ok != (it != t.decode.end())) {
std::fprintf(stderr, "cp%u %02x%02x -> %x, want %x\n", cp, b, trail,
out.empty() ? 0 : unsigned(out[0]), want);
++g_failures;
}
}
} else if (b != 0) {
const unsigned char one[1] = {(unsigned char)b};
std::u32string out;
const bool ok = mt_codepage::decode(cp, one, 1, out);
auto it = t.decode.find(b);
const unsigned want = it == t.decode.end() ? t.mb_default : it->second;
if (out.size() != 1 || out[0] != want || ok != (it != t.decode.end())) {
std::fprintf(stderr, "cp%u %02x -> %x, want %x\n", cp, b, out.empty() ? 0 : unsigned(out[0]), want);
++g_failures;
}
}
}
for (unsigned wc = 1; wc < 0x10000; ++wc) {
if (wc >= 0xd800 && wc <= 0xdfff) continue;
auto it = t.wctable.find(wc);
bool used = false;
const std::string fit = enc(cp, std::u32string(1, char32_t(wc)), true, &used);
const std::string want_fit = it == t.wctable.end() ? "?" : bytes_of(it->second);
const bool exact = it != t.wctable.end() && t.decode.count(it->second) && t.decode[it->second] == wc;
const std::string strict = enc(cp, std::u32string(1, char32_t(wc)), false);
const std::string want_strict = exact ? bytes_of(it->second) : "?";
if (fit != want_fit || used != (it == t.wctable.end()) || strict != want_strict) {
std::fprintf(stderr, "cp%u U+%04X -> best fit %zu bytes / strict %zu bytes, mismatch\n", cp, wc,
fit.size(), strict.size());
++g_failures;
}
}
std::printf("cp%u: %zu byte sequences, %zu WCTABLE entries: %s\n", cp, t.decode.size(), t.wctable.size(),
g_failures == before ? "ok" : "FAIL");
}
return loaded;
}
int main(int argc, char** argv)
{
spot_checks();
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const int loaded = argc > 1 ? against_source(argv[1]) : 0;
if (g_failures) {
std::fprintf(stderr, "%d failure(s)\n", g_failures);
return 1;
}
if (loaded < 14) {
std::printf("bestfit sources: %d of 14 found (run scripts/gen_codepage_tables.py)\n", loaded);
return is_strict ? 1 : 77;
}
std::printf("ok\n");
return 0;
}
+105
View File
@@ -0,0 +1,105 @@
// eterpack_pack — build a 40250 pack (<db>.eix/.epk) from a directory with the ported CEterPack, the way
// the original locale packs are made: every file COMPRESS (LZO), index LZO + TEA encrypted.
//
// eterpack_pack <db path without extension> <source dir> <pack prefix, e.g. locale/zh/>
//
// Pack names are <prefix><path relative to the source dir>, lowercased with '/' (inlineConvertPackFilename).
// An existing <db>.eix/.epk is replaced. The result is reopened read-only and every file read back
// through CEterPack::Get and compared with its source.
#include "EterPack/StdAfx.h"
#include "EterPack/EterPack.h"
#include "EterPack/EterPackManager.h"
#include "EterBase/MappedFile.h"
#include "../../src/platform/UserInterface/UserInterface.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static std::vector<char> read_file(const fs::path& path)
{
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}
int main(int argc, char** argv)
{
if (argc != 4)
{
std::fprintf(stderr, "usage: %s <db> <source dir> <pack prefix>\n", argv[0]);
return 2;
}
const std::string db = argv[1];
const fs::path source = argv[2];
std::string prefix = argv[3];
if (!prefix.empty() && prefix.back() != '/')
prefix += '/';
std::vector<std::pair<std::string, fs::path>> files;
for (const auto& e : fs::recursive_directory_iterator(source))
if (e.is_regular_file())
files.emplace_back(prefix + e.path().lexically_relative(source).generic_string(), e.path());
std::sort(files.begin(), files.end());
if (files.empty())
{
std::fprintf(stderr, "no files under %s\n", source.c_str());
return 1;
}
PackSingletons();
std::error_code ec;
fs::remove(db + ".eix", ec);
fs::remove(db + ".epk", ec);
{
CEterFileDict dict;
CEterPack pack;
if (!pack.Create(dict, db.c_str(), "", false))
{
std::fprintf(stderr, "cannot create %s.eix/.epk\n", db.c_str());
return 1;
}
for (const auto& [name, path] : files)
{
const std::vector<char> data = read_file(path);
if (name.size() > FILENAME_MAX_LEN || !pack.Put(name.c_str(), data.data(), long(data.size()), COMPRESSED_TYPE_COMPRESS))
{
std::fprintf(stderr, "cannot put %s\n", name.c_str());
return 1;
}
}
if (!pack.EncryptIndexFile())
return 1;
}
// Read back as the client does.
CEterFileDict dict;
CEterPack pack;
if (!pack.Create(dict, db.c_str(), prefix.c_str(), true))
{
std::fprintf(stderr, "cannot reopen %s\n", db.c_str());
return 1;
}
int failures = 0;
for (const auto& [name, path] : files)
{
const std::vector<char> want = read_file(path);
CMappedFile mapped;
LPCVOID data = nullptr;
if (!pack.Get(mapped, name.c_str(), &data) || mapped.Size() != want.size() ||
(!want.empty() && std::memcmp(data, want.data(), want.size()) != 0))
{
std::fprintf(stderr, "read-back mismatch: %s\n", name.c_str());
++failures;
}
}
if (failures)
return 1;
std::printf("%s: %zu files, COMPRESS, encrypted index\n", db.c_str(), files.size());
return 0;
}
+148
View File
@@ -0,0 +1,148 @@
// Offline CP936 name overlay for the 40250 item/mob proto format.
// Runtime loading remains the original 40250 path and binary format.
#include "GameLib/StdAfx.h"
#include "GameLib/ItemData.h"
#include "UserInterface/StdAfx.h"
#include "UserInterface/PythonNonPlayer.h"
#include "EterBase/lzo.h"
#include <array>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iostream>
#include <map>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
using Item = CItemData::TItemTable;
using Mob = CPythonNonPlayer::TMobTable;
constexpr std::array<DWORD, 4> item_key{{173217, 72619434, 408587239, 27973291}};
constexpr std::array<DWORD, 4> mob_key{{4813894, 18955, 552631, 6822045}};
std::vector<BYTE> read_file(const char* path) {
std::ifstream stream(path, std::ios::binary);
if (!stream) throw std::runtime_error(std::string("cannot read ") + path);
return {std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
}
void write_file(const char* path, const std::vector<BYTE>& bytes) {
std::ofstream stream(path, std::ios::binary | std::ios::trunc);
if (!stream || !stream.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()))
throw std::runtime_error(std::string("cannot write ") + path);
}
DWORD word(const std::vector<BYTE>& bytes, std::size_t offset) {
if (offset + 4 > bytes.size()) throw std::runtime_error("truncated proto header");
DWORD value;
std::memcpy(&value, bytes.data() + offset, 4);
return value;
}
void put_word(std::vector<BYTE>& bytes, std::size_t offset, DWORD value) {
std::memcpy(bytes.data() + offset, &value, 4);
}
std::map<DWORD, std::string> read_names(const char* path) {
std::ifstream stream(path, std::ios::binary);
if (!stream) throw std::runtime_error(std::string("cannot read ") + path);
std::map<DWORD, std::string> names;
std::string line;
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
const std::size_t tab = line.find('\t');
if (tab == std::string::npos || tab == 0) continue;
try {
std::size_t end = 0;
const unsigned long value = std::stoul(line.substr(0, tab), &end);
if (end != tab || value > UINT32_MAX) continue;
const std::string name = line.substr(tab + 1);
if (!name.empty() && name.find('\0') == std::string::npos)
names[static_cast<DWORD>(value)] = name;
} catch (const std::exception&) { /* header or malformed row */ }
}
return names;
}
template <class Record>
std::vector<BYTE> patch(const char* source, const char* names_path,
std::array<DWORD, 4> key, const char* magic, bool versioned) {
const auto input = read_file(source);
const std::size_t count_offset = versioned ? 12 : 4;
const std::size_t payload_offset = versioned ? 20 : 12;
if (input.size() < payload_offset || std::memcmp(input.data(), magic, 4) != 0)
throw std::runtime_error("unexpected proto signature");
if (versioned && (word(input, 4) != 1 || word(input, 8) != sizeof(Record)))
throw std::runtime_error("unexpected item proto version or stride");
const DWORD count = word(input, count_offset);
const DWORD payload_size = word(input, count_offset + 4);
if (payload_size > input.size() - payload_offset)
throw std::runtime_error("truncated compressed proto");
CLZObject decoded;
if (!CLZO::Instance().Decompress(decoded, input.data() + payload_offset, key.data()) ||
decoded.GetSize() != std::size_t(count) * sizeof(Record))
throw std::runtime_error("invalid proto payload size or key");
std::vector<BYTE> plain(decoded.GetBuffer(), decoded.GetBuffer() + decoded.GetSize());
const auto original = plain;
const auto names = read_names(names_path);
std::size_t translated = 0, missing = 0, too_long = 0;
for (DWORD i = 0; i < count; ++i) {
Record record;
std::memcpy(&record, plain.data() + std::size_t(i) * sizeof(Record), sizeof(Record));
const auto found = names.find(record.dwVnum);
if (found == names.end()) { ++missing; continue; }
if (found->second.size() >= sizeof(record.szLocaleName)) { ++too_long; continue; }
std::memset(record.szLocaleName, 0, sizeof(record.szLocaleName));
std::memcpy(record.szLocaleName, found->second.data(), found->second.size());
std::memcpy(plain.data() + std::size_t(i) * sizeof(Record), &record, sizeof(Record));
++translated;
}
// No item stats, VNUM, canonical name, or mob gameplay fields may change.
for (DWORD i = 0; i < count; ++i) {
const std::size_t start = std::size_t(i) * sizeof(Record);
const std::size_t name_start = start + offsetof(Record, szLocaleName);
const std::size_t name_end = name_start + sizeof(Record::szLocaleName);
for (std::size_t byte = start; byte < start + sizeof(Record); ++byte) {
if ((byte < name_start || byte >= name_end) && plain[byte] != original[byte])
throw std::runtime_error("non-name proto data changed");
}
}
CLZObject encoded;
if (!CLZO::Instance().CompressEncryptedMemory(encoded, plain.data(), plain.size(), key.data()))
throw std::runtime_error("cannot encode proto");
std::vector<BYTE> result(input.begin(), input.begin() + payload_offset);
result.insert(result.end(), encoded.GetBuffer(), encoded.GetBuffer() + encoded.GetSize());
put_word(result, count_offset + 4, encoded.GetSize());
CLZObject verified;
if (!CLZO::Instance().Decompress(verified, result.data() + payload_offset, key.data()) ||
verified.GetSize() != plain.size() ||
std::memcmp(verified.GetBuffer(), plain.data(), plain.size()) != 0)
throw std::runtime_error("encoded proto failed round-trip verification");
std::cout << magic << " count=" << count << " translated=" << translated
<< " missing=" << missing << " too_long=" << too_long << '\n';
return result;
}
} // namespace
int main(int argc, char** argv) {
if (argc != 7) {
std::cerr << "usage: locale_proto_patch item_proto mob_proto item_names_gbk mob_names_gbk out_item out_mob\n";
return 2;
}
try {
static CLZO lzo;
const auto item = patch<Item>(argv[1], argv[3], item_key, "MIPX", true);
const auto mob = patch<Mob>(argv[2], argv[4], mob_key, "MMPT", false);
write_file(argv[5], item);
write_file(argv[6], mob);
return 0;
} catch (const std::exception& error) {
std::cerr << "locale_proto_patch: " << error.what() << '\n';
return 1;
}
}
+367
View File
@@ -0,0 +1,367 @@
// 批次 2V0-f: the real 40250 system.py driven through the host lifecycle PythonBoot gives Godot.
//
// prototype.RunApp() runs app.Create, mainStream.SetLogoPhase() and then blocks in app.Loop() until
// the window closes (40250 PythonApplication.cpp:980). Here the script runs on PythonBoot's script
// thread and every PythonBoot::AppFrame() is one CPythonApplication::Process(); this test is what the
// Godot surface does each frame, minus Godot:
//
// 1. RunMainScript returns with the script parked in app.Loop(), the logo phase open;
// 2. frames advance introLogo.LogoWindow.OnUpdate through both logo videos into the login phase
// (40250's appLogoOpen/Update are themselves commented out: open succeeds, update reports done);
// 3. the popup that networkModule.MainStream created opens, draws, and closes from an ESC key
// (PopupDialog.OnPressEscapeKey) and from a click on its accept button;
// 4. Stop() ends Loop() and RunApp() runs mainStream.Destroy() before the thread is joined.
//
// port_app_loop_test <40250 Client dir> <python27.zip>
//
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
#include "ScriptLib/StdAfx.h"
#include "../../src/platform/ScriptLib/PythonBoot.h"
#include "../../src/platform/ScriptLib/PythonHost.h"
#include "../../src/platform/UserInterface/UserInterface.h"
#include "../../src/platform/EterLib/UIRenderCommands.h"
#include "EterLib/GrpScreen.h"
#include "EterPythonLib/PythonGraphic.h"
#include <dinput.h> // DIK_ESCAPE
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static bool py(const char* source)
{
std::string error;
if (PythonBoot::RunLine(source, &error))
return true;
std::fprintf(stderr, "python: %s\n%s\n", source, error.c_str());
return false;
}
// Evaluates a Python expression on the host thread without raising (a raise would log every frame).
static bool py_true(const char* expression)
{
PyObject* globals = PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
PyObject* result = PyRun_String(expression, Py_eval_input, globals, globals);
if (!result)
{
PyErr_Clear();
return false;
}
const bool truth = PyObject_IsTrue(result) == 1;
Py_DECREF(result);
return truth;
}
// CGraphicTextInstance draws one quad per glyph from a CGraphicFontTexture page ("mem:<id>@<rev>").
static int glyph_quads()
{
int count = 0;
for (const auto& command : UIRenderCommands())
count += command.kind == UIRenderCommand::Image && command.text.rfind("mem:", 0) == 0;
return count;
}
// Some glyph page holds inked pixels (A4R4G4B4 coverage in the alpha channel).
static bool glyph_page_has_ink()
{
for (const auto& command : UIRenderCommands())
{
UIMemoryTexture page;
if (command.kind != UIRenderCommand::Image || command.text.rfind("mem:", 0) != 0 ||
!UIRenderMemoryTexture(command.text, &page))
continue;
for (std::uint32_t argb : page.argb)
if ((argb >> 24) != 0)
return true;
}
return false;
}
static bool near(float a, float b)
{
return std::fabs(a - b) < 0.01f;
}
static const UIRenderCommand* image_command(const char* suffix)
{
const std::string tail = suffix;
for (const auto& command : UIRenderCommands())
if (command.kind == UIRenderCommand::Image && command.quad && command.text.size() >= tail.size() &&
command.text.compare(command.text.size() - tail.size(), tail.size(), tail) == 0)
return &command;
return nullptr;
}
static void frames(int count)
{
for (int i = 0; i < count && PythonBoot::IsAppLooping(); ++i)
PythonBoot::UIUpdate();
}
static void drawing_primitives()
{
UIRenderSetSize(800, 600);
UIRenderBeginFrame();
CScreen screen;
screen.SetDiffuseColor(0xFF123456u);
screen.RenderBox2d(10, 20, 30, 40);
screen.RenderGradationBar2d(50, 60, 70, 80, 0xFF102030u, 0xFF405060u);
const auto& commands = UIRenderCommands();
CHECK(commands.size() == 5);
if (commands.size() == 5)
{
CHECK(commands[0].kind == UIRenderCommand::Line && near(commands[0].x1, 10) && near(commands[0].y2, 20));
CHECK(commands[3].kind == UIRenderCommand::Line && near(commands[3].x2, 31));
CHECK(commands[4].kind == UIRenderCommand::GradientBar && commands[4].argb == 0xFF102030u &&
commands[4].end_argb == 0xFF405060u);
}
// 40250 CPythonGraphic::RenderCoolTimeBox: fTime 0.25 leaves 6 whole fan triangles from the top
// centre anticlockwise round to (+1, 0); fTime 0.3 has 5 whole ones and a sixth 60% of the way
// from (+1, +1) to (+1, 0).
UIRenderBeginFrame();
{
CPythonGraphic graphic;
graphic.RenderCoolTimeBox(100, 100, 10, 0.25f);
CHECK(commands.size() == 6);
if (commands.size() == 6)
{
CHECK(commands[0].kind == UIRenderCommand::Bar && commands[0].quad && commands[0].argb == 0x80000000u);
CHECK(near(commands[0].qx[0], 100) && near(commands[0].qy[0], 100));
CHECK(near(commands[0].qx[1], 100) && near(commands[0].qy[1], 90));
CHECK(near(commands[0].qx[2], 90) && near(commands[0].qy[2], 90));
CHECK(near(commands[5].qx[2], 110) && near(commands[5].qy[2], 100));
}
UIRenderBeginFrame();
graphic.RenderCoolTimeBox(100, 100, 10, 0.3f);
CHECK(commands.size() == 6);
if (commands.size() == 6)
CHECK(near(commands[5].qx[1], 110) && near(commands[5].qy[1], 110) &&
near(commands[5].qx[2], 110) && near(commands[5].qy[2], 104));
UIRenderBeginFrame();
graphic.RenderCoolTimeBox(100, 100, 10, 1.0f);
CHECK(commands.empty());
// A cooldown triangle crossing the viewport must have clipped vertices, not only a clipped bbox.
UIRenderSetClip(95, 95, 10, 10);
graphic.RenderCoolTimeBox(100, 100, 10, 0.25f);
CHECK(!commands.empty());
bool touched_clip_edge = false;
for (const auto& command : commands)
{
CHECK(command.kind == UIRenderCommand::Bar && command.quad);
for (int vertex = 0; vertex < 3; ++vertex)
{
CHECK(command.qx[vertex] >= 95 && command.qx[vertex] <= 105);
CHECK(command.qy[vertex] >= 95 && command.qy[vertex] <= 105);
touched_clip_edge |= near(command.qx[vertex], 95) || near(command.qx[vertex], 105) ||
near(command.qy[vertex], 95) || near(command.qy[vertex], 105);
}
}
CHECK(touched_clip_edge);
UIRenderRestoreClip();
UIRenderBeginFrame();
// CPythonGraphic::SetViewport also clips the UI commands to the viewport until RestoreViewport.
graphic.SetViewport(10, 20, 30, 40);
screen.RenderBar2d(0, 0, 5, 5);
graphic.RestoreViewport();
screen.RenderBar2d(0, 0, 5, 5);
CHECK(commands.size() == 2);
if (commands.size() == 2)
{
CHECK(near(commands[0].clip_x1, 10) && near(commands[0].clip_y1, 20) &&
near(commands[0].clip_x2, 40) && near(commands[0].clip_y2, 60));
CHECK(near(commands[1].clip_x1, 0) && near(commands[1].clip_x2, 800) && near(commands[1].clip_y2, 600));
}
}
UIRenderSetSize(0, 0);
UIRenderBeginFrame();
}
int main(int argc, char** argv)
{
drawing_primitives();
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string client = argc > 1 ? argv[1] : "";
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
{
std::fprintf(stderr, "port_app_loop_test: no 40250 Client/pack at '%s'\n", client.c_str());
return is_strict ? 1 : 77;
}
PackSingletons();
CHECK(PackInitialize("pack"));
std::string error;
CHECK(PythonBoot::Start(stdlib.c_str(), &error));
if (!error.empty())
std::fprintf(stderr, "Start: %s\n", error.c_str());
// app.Create has no device without a canvas, and RunApp reports it the 40250 way.
CHECK(!PythonBoot::RunMainScript("", &error));
CHECK(error.find("Sorry, Your system does not support 3D graphics") != std::string::npos);
CHECK(!PythonBoot::IsAppLooping());
PythonBoot::Stop();
CHECK(PythonBoot::Start(stdlib.c_str(), &error));
PythonBoot::SetUISize(800, 600);
error.clear();
const bool ran = PythonBoot::RunMainScript("", &error);
if (!ran)
std::fprintf(stderr, "RunMainScript: %s\n", error.c_str());
CHECK(ran);
CHECK(PythonBoot::IsAppLooping());
// The script thread is parked in app.Loop(); the host thread can run Python in between.
CHECK(py("import __main__, networkModule\n"
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]\n"
"import introLogo\n"
"assert isinstance(_stream.curPhaseWindow, introLogo.LogoWindow), _stream.curPhaseWindow"));
// The phase switch waits for the curtain fade; give it a bounded number of frames.
for (int i = 0; i < 600 && PythonBoot::IsAppLooping(); ++i)
{
PythonBoot::UIUpdate();
if (py_true("__import__('introLogin').LoginWindow.__instancecheck__(_stream.curPhaseWindow)"))
break;
}
CHECK(PythonBoot::IsAppLooping());
CHECK(py("import introLogin\n"
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow), _stream.curPhaseWindow"));
// ThinBoard.BOARD_COLOR is grp.GenerateColor(0, 0, 0, 0.51): the server list board's Bar draws it.
CHECK(py("import grp\nassert grp.GenerateColor(0.0, 0.0, 0.0, 0.51) & 0xffffffff == 0x82000000"));
frames(1);
bool has_board = false;
for (const auto& command : UIRenderCommands())
has_board |= command.kind == UIRenderCommand::Bar && command.argb == 0x82000000u;
CHECK(has_board);
// PopupDialog: open with a message, render, close with ESC through the window manager's key path.
// The message is 11 glyphs of the default font (app.SetDefaultFontName, a Tahoma cell here).
const int glyphs_before = glyph_quads();
CHECK(py("_stream.popupWindow.Open('POPUP_PROBE')\nassert _stream.popupWindow.IsShow()"));
frames(1);
CHECK(glyph_quads() >= glyphs_before + 11);
CHECK(glyph_page_has_ink());
CHECK(py("assert _stream.popupWindow.GetChild('message').GetTextSize()[0] > 0"));
PythonBoot::UIKey(DIK_ESCAPE, true);
PythonBoot::UIKey(DIK_ESCAPE, false);
frames(1);
CHECK(py("assert not _stream.popupWindow.IsShow()"));
CHECK(glyph_quads() == glyphs_before);
// ... and with a click on its accept button, which runs the close event. Off-centre: the button's
// caption is a 0x0 TextLine at its exact centre and CWindow::IsIn is inclusive, so in 40250 too
// that one pixel picks the caption instead of the button.
CHECK(py("_popup_closed = []\n"
"_stream.popupWindow.Open('POPUP_PROBE', lambda: _popup_closed.append(1))\n"
"_accept = _stream.popupWindow.GetChild('accept')\n"
"_ax, _ay = _accept.GetGlobalPosition()\n"
"__main__._accept_point = (_ax + 4, _ay + _accept.GetHeight() // 2)"));
frames(1);
PyObject* point = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), "_accept_point");
CHECK(point && PyTuple_Check(point));
if (point && PyTuple_Check(point))
{
const int x = int(PyInt_AsLong(PyTuple_GetItem(point, 0)));
const int y = int(PyInt_AsLong(PyTuple_GetItem(point, 1)));
PythonBoot::UIMouseMove(x, y);
PythonBoot::UIMouseButton(1, true, x, y);
PythonBoot::UIMouseButton(1, false, x, y);
}
frames(1);
CHECK(py("assert not _stream.popupWindow.IsShow() and _popup_closed == [1]"));
// ExpandedImageBox (CGraphicExpandedImageInstance::OnRender): scale + rendering rect, then rotation.
CHECK(py("import ui\n"
"_ex = ui.ExpandedImageBox()\n"
"_ex.LoadImage('d:/ymir work/ui/pattern/board_base.tga')\n"
"_ex.SetPosition(100, 50)\n"
"_ex.SetScale(2.0, 1.0)\n"
"_ex.SetRenderingRect(0.0, 0.0, -0.25, 0.0)\n"
"_ex.Show()"));
frames(1);
const UIRenderCommand* ex = image_command("board_base.tga");
CHECK(ex);
if (ex)
{
// Unscaled image size from the texture coordinates: eu = (w + right) / w, right = long(w * -0.25).
const float w = ex->qx[1] - ex->qx[0] + 0.0f; // 2w + right
const float h = ex->qy[2] - ex->qy[0];
const float image_w = std::round(w / (1.0f + ex->eu)); // 2w + w*(eu-1) = w*(1+eu)
CHECK(near(ex->qx[0], 100.0f) && near(ex->qy[0], 50.0f));
CHECK(near(ex->qx[2], 100.0f) && near(ex->qy[1], 50.0f));
CHECK(image_w > 0 && near(w, 2 * image_w + long(image_w * -0.25f)));
CHECK(near(ex->su, 0) && near(ex->sv, 0) && near(ex->ev, 1));
CHECK(near(ex->eu, (image_w + long(image_w * -0.25f)) / image_w));
CHECK(h > 0 && near(ex->qy[3], 50.0f + h));
}
CHECK(py("_ex.SetRenderingRect(0.0, 0.0, 0.0, 0.0)\n_ex.SetRotation(90.0)"));
frames(1);
ex = image_command("board_base.tga");
CHECK(ex);
if (ex)
{
// Rotated about the origin (the image centre, set on load): the top edge now points down.
const float cx = (ex->qx[0] + ex->qx[3]) / 2, cy = (ex->qy[0] + ex->qy[3]) / 2;
const float top_x = ex->qx[1] - ex->qx[0], top_y = ex->qy[1] - ex->qy[0];
CHECK(near(top_x, 0) && top_y > 0);
CHECK(near(ex->eu, 1) && near(ex->ev, 1));
CHECK(py("__main__._ex_origin = (100.0 + _ex.GetWidth() / 2.0 / 2.0, 50.0 + _ex.GetHeight() / 2.0)"));
PyObject* origin = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), "_ex_origin");
CHECK(origin && near(cx, float(PyFloat_AsDouble(PyTuple_GetItem(origin, 0)))) &&
near(cy, float(PyFloat_AsDouble(PyTuple_GetItem(origin, 1)))));
}
CHECK(py("_ex.Hide()\n_ex = None"));
// A .sub draws its parent texture through the sub rect (40250 CGraphicSubImage shares the D3D texture).
CHECK(py("_sub = ui.ImageBox()\n"
"_sub.LoadImage('d:/ymir work/ui/public/large_button_01.sub')\n"
"_sub.SetPosition(10, 10)\n"
"_sub.Show()"));
frames(1);
const UIRenderCommand* sub = nullptr;
for (const auto& command : UIRenderCommands())
if (command.kind == UIRenderCommand::Image && near(command.qx[0], 10) && near(command.qy[0], 10))
sub = &command;
CHECK(sub);
if (sub)
{
CHECK(sub->text.find(".sub") == std::string::npos && !sub->text.empty());
CHECK(sub->su >= 0 && sub->eu <= 1 && sub->sv >= 0 && sub->ev <= 1);
CHECK(sub->eu - sub->su < 1 || sub->ev - sub->sv < 1);
}
CHECK(py("_sub.Hide()\n_sub = None"));
// app.Exit() is 40250's PostQuitMessage: the next frame leaves Loop() and RunApp() finishes.
CHECK(py("import app\napp.Exit()"));
frames(1);
CHECK(!PythonBoot::IsAppLooping());
CHECK(py("assert _stream.curPhaseWindow == 0 and _stream.popupWindow == 0"));
PythonBoot::Stop();
CHECK(!PythonBoot::IsRunning());
if (g_failures)
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
else
std::printf("port_app_loop_test: ok\n");
return g_failures ? 1 : 0;
}
+104
View File
@@ -0,0 +1,104 @@
// port/common: Win32 widths and MSVC CRT semantics the ported 40250 logic relies on.
#include "common/StdAfx.h"
#include "common/D3DXMath.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
int main()
{
// MSVC _snprintf: shorter output is terminated.
char buf[8];
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 8, "%s", "abc") == 3);
CHECK(std::strcmp(buf, "abc") == 0);
// Exactly `count` bytes: written unterminated, returns count.
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 4, "%s", "abcd") == 4);
CHECK(std::memcmp(buf, "abcdx", 5) == 0);
// Longer: `count` bytes written unterminated, returns -1.
std::memset(buf, 'x', sizeof(buf));
CHECK(_snprintf(buf, 4, "%d", 123456) == -1);
CHECK(std::memcmp(buf, "1234x", 5) == 0);
CHECK(stricmp("PLAYER", "player") == 0);
CHECK(_strnicmp("d:/ymir work/A", "D:/YMIR WORK/b", 12) == 0);
char s[] = "MixEd";
CHECK(std::strcmp(_strlwr(s), "mixed") == 0);
CHECK(MAKELONG(0x1234, 0xABCD) == static_cast<LONG>(0xABCD1234u));
CHECK(LOWORD(0xABCD1234u) == 0x1234 && HIWORD(0xABCD1234u) == 0xABCD);
CHECK(MAKEWORD(0x34, 0x12) == 0x1234);
// DWORD arithmetic wraps like the 32-bit original.
DWORD t = 0xFFFFFFF0u;
t += 0x20;
CHECK(t == 0x10);
const DWORD a = timeGetTime();
const DWORD b = timeGetTime();
CHECK(static_cast<DWORD>(b - a) < 1000);
// D3DX8 math: row vectors, left-handed rotations, D3DX multiply order.
const auto near = [](float x, float y) { return std::fabs(x - y) < 1e-5f; };
D3DXVECTOR3 v(3.0f, 0.0f, 4.0f);
CHECK(D3DXVec3Length(&v) == 5.0f && D3DXVec3LengthSq(&v) == 25.0f);
D3DXVECTOR3 n;
D3DXVec3Normalize(&n, &v);
CHECK(near(n.x, 0.6f) && n.y == 0.0f && near(n.z, 0.8f));
D3DXVECTOR3 zero(0.0f, 0.0f, 0.0f);
D3DXVec3Normalize(&n, &zero);
CHECK(n == zero);
CHECK((v / 2.0f) == D3DXVECTOR3(1.5f, 0.0f, 2.0f) && (2.0f * v) == D3DXVECTOR3(6.0f, 0.0f, 8.0f));
CHECK(near(D3DXToRadian(180.0f), D3DX_PI) && near(D3DXToDegree(D3DX_PI / 2.0f), 90.0f));
D3DXMATRIX rz, tr, m;
D3DXMatrixRotationZ(&rz, D3DX_PI / 2.0f); // x axis -> +y
D3DXVECTOR3 p(1.0f, 0.0f, 0.0f), q;
D3DXVec3TransformCoord(&q, &p, &rz);
CHECK(near(q.x, 0.0f) && near(q.y, 1.0f) && near(q.z, 0.0f));
D3DXMatrixTranslation(&tr, 10.0f, 20.0f, 30.0f);
m = rz * tr; // rotate, then translate
D3DXVec3TransformCoord(&q, &p, &m);
CHECK(near(q.x, 10.0f) && near(q.y, 21.0f) && near(q.z, 30.0f));
D3DXMATRIX inv, id;
CHECK(D3DXMatrixInverse(&inv, nullptr, &m) != nullptr);
D3DXMatrixMultiply(&id, &m, &inv);
for (int i = 0; i < 4; ++i)
for (int j = 0; j < 4; ++j)
CHECK(near(id.m[i][j], i == j ? 1.0f : 0.0f));
D3DXQUATERNION qz;
D3DXVECTOR3 zAxis(0.0f, 0.0f, 1.0f);
D3DXQuaternionRotationAxis(&qz, &zAxis, D3DX_PI / 2.0f);
D3DXMATRIX mq;
D3DXMatrixRotationQuaternion(&mq, &qz);
for (int i = 0; i < 16; ++i)
CHECK(near((&mq._11)[i], (&rz._11)[i]));
D3DXMATRIX ypr, ypr2;
D3DXQUATERNION qypr;
D3DXMatrixRotationYawPitchRoll(&ypr, 0.3f, 0.5f, 0.7f);
D3DXQuaternionRotationYawPitchRoll(&qypr, 0.3f, 0.5f, 0.7f);
D3DXMatrixRotationQuaternion(&ypr2, &qypr);
for (int i = 0; i < 16; ++i)
CHECK(near((&ypr._11)[i], (&ypr2._11)[i]));
CHECK(static_cast<DWORD>(D3DXCOLOR(0xFF804020u)) == 0xFF804020u);
CHECK(static_cast<DWORD>(D3DXCOLOR(2.0f, -1.0f, 0.5f, 1.0f)) == 0xFFFF0080u);
if (g_failures)
return EXIT_FAILURE;
std::puts("port_common_test: ok");
return EXIT_SUCCESS;
}
+186
View File
@@ -0,0 +1,186 @@
// port/EterPack against the real 40250 Client/pack (batch 2D). Registers the packs through the platform
// PackInitialize("pack") (40250 UserInterface.cpp), after the singletons Main() creates, then checks the numbers
// batch 2R measured with tools/epk_scan (audit/packs/2R-scan.json): 103 packs + root / 54891 entries / 52609 paths,
// first-registered pack wins, and exactly the 4 short-layout SECURITY files 40250 rejects fail to load.
//
// port_eterpack_test <40250 Client dir>
//
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which turns that into a failure.
#include "EterPack/StdAfx.h"
#include "EterPack/EterPackManager.h"
#include "EterBase/CRC32.h"
#include "EterBase/lzo.h"
#include "../../src/platform/UserInterface/UserInterface.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <dirent.h>
#include <strings.h>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
// Index names are spelled as on the NTFS original ("Etc" for ETC.eix); find the on-disk spelling.
static std::string on_disk_name(const std::string& dir, const std::string& name)
{
std::string found = name;
if (DIR* d = opendir(dir.c_str()))
{
const std::string want = name + ".eix";
while (dirent* e = readdir(d))
if (strcasecmp(e->d_name, want.c_str()) == 0)
found = std::string(e->d_name, want.size() - 4);
closedir(d);
}
return found;
}
static std::vector<std::string> read_lines(const std::string& path)
{
std::vector<std::string> lines;
std::ifstream in(path, std::ios::binary);
std::string line;
while (std::getline(in, line))
{
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
lines.push_back(line);
}
return lines;
}
int main(int argc, char** argv)
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string client = argc > 1 ? argv[1] : "";
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
{
std::fprintf(stderr, "port_eterpack_test: no 40250 Client/pack at '%s'\n", client.c_str());
return is_strict ? 1 : 77;
}
PackSingletons();
// A 32-bit pack header must round-trip through liblzo's native-width output parameter.
const char sample[] = "40250 pack LZO length on a 64-bit host";
CLZObject compressed;
compressed.BeginCompress(sample, sizeof(sample));
CHECK(compressed.Compress());
CLZObject restored;
CHECK(restored.BeginDecompress(compressed.GetBuffer()));
CHECK(restored.Decompress());
CHECK(restored.GetSize() == sizeof(sample));
CHECK(std::memcmp(restored.GetBuffer(), sample, sizeof(sample)) == 0);
CHECK(PackInitialize("pack"));
CEterPackManager& mgr = CEterPackManager::Instance();
// Expected registration order, from pack/Index independently of PackInitialize: line 0 is PACK, then
// (folder, name) pairs; every name also gets a _texcache pack; a pack is registered where it exists on disk.
const std::vector<std::string> lines = read_lines("pack/Index");
CHECK(!lines.empty() && lines[0] == "PACK");
std::vector<std::string> registered;
for (size_t i = 1; i + 1 < lines.size(); i += 2)
for (const std::string& name : {lines[i + 1], lines[i + 1] + "_texcache"})
{
const std::string db = "pack/" + on_disk_name("pack", name);
if (access((db + ".eix").c_str(), 0) == 0 && access((db + ".epk").c_str(), 0) == 0)
registered.push_back(db);
}
// 124 Index pairs; 119 registrations find a pack (some names are listed twice), 103 distinct packs, + root.
CHECK(registered.size() == 119);
CHECK(std::set<std::string>(registered.begin(), registered.end()).size() == 103);
// The whole dict (one entry per pack index record, overrides included).
struct Access : CEterPackManager
{
static size_t packs(CEterPackManager& m) { return static_cast<Access&>(m).m_PackMap.size(); }
static const CEterFileDict::TDict& dict(CEterPackManager& m) { return static_cast<Access&>(m).m_FileDict.GetDict(); }
static CEterFileDict::Item* item(CEterPackManager& m, DWORD h, const char* n) { return static_cast<Access&>(m).m_FileDict.GetItem(h, n); }
};
CHECK(Access::packs(mgr) == 103);
const CEterFileDict::TDict& dict = Access::dict(mgr);
CHECK(dict.size() == 54891);
std::map<std::string, int> order;
for (size_t i = 0; i < registered.size(); ++i)
order.emplace(registered[i], static_cast<int>(i)); // first registration
// First registration per path, and CRC32 of the name equal to the stored filename_crc.
std::map<std::string, std::string> first;
for (const auto& kv : dict)
{
const std::string name = kv.second.pkInfo->filename;
const std::string pack = kv.second.pkPack->GetDBName();
CHECK(GetCRC32(name.c_str(), name.size()) == kv.first);
auto it = first.find(name);
if (it == first.end() || order[pack] < order[it->second])
first[name] = pack;
}
CHECK(first.size() == 52609);
size_t overridden = 0, wrong_winner = 0;
std::map<std::string, int> per_name;
for (const auto& kv : dict)
++per_name[kv.second.pkInfo->filename];
for (const auto& [name, count] : per_name)
{
if (count < 2)
continue;
overridden += count - 1;
CEterFileDict::Item* item = Access::item(mgr, GetCRC32(name.c_str(), name.size()), name.c_str());
if (!item || first[name] != item->pkPack->GetDBName())
{
if (wrong_winner < 5)
std::fprintf(stderr, "winner %s: %s, expected %s\n", name.c_str(),
item ? item->pkPack->GetDBName() : "(none)", first[name].c_str());
++wrong_winner;
}
}
CHECK(overridden == 2282);
CHECK(wrong_winner == 0);
// Every path loads through CEterPackManager::Get, except the 4 short-layout SECURITY files.
const std::set<std::string> rejected_expected = {
"d:/ymir work/npc/mineral2/wait.msa",
"d:/ymir work/effect/background/metinstone_loop_redblack.mse",
"d:/ymir work/monster2/spider_spawn/wait.msa",
"d:/ymir work/monster2/spider_spawn/line1.mse",
};
std::set<std::string> rejected;
unsigned long long bytes = 0;
for (const auto& [name, pack] : first)
{
CMappedFile file;
LPCVOID data = nullptr;
if (mgr.Get(file, name.c_str(), &data))
bytes += file.Size();
else
rejected.insert(name);
}
for (const std::string& name : rejected)
if (!rejected_expected.count(name))
std::fprintf(stderr, "unexpected load failure: %s\n", name.c_str());
CHECK(rejected == rejected_expected);
std::printf("packs %zu, entries %zu, paths %zu, overrides %zu, loaded %zu (%llu bytes), rejected %zu\n",
registered.size(), dict.size(), first.size(), overridden, first.size() - rejected.size(), bytes,
rejected.size());
if (g_failures)
std::fprintf(stderr, "%d failure(s)\n", g_failures);
return g_failures ? 1 : 0;
}
@@ -0,0 +1,34 @@
// The fake 40250 auth + game server of port_login_flow_test as its own process, for runs that drive the
// real client from Godot (scripts/python_game_render_test.sh). Prints "ports <auth> <game>" once it
// listens and serves until SIGTERM or SIGINT; the milestones go to stderr on the way out.
#include "port_login_flow_server.h"
#include <csignal>
#include <cstdio>
#include <unistd.h>
static volatile std::sig_atomic_t g_stop = 0;
static void on_signal(int) { g_stop = 1; }
int main()
{
std::signal(SIGTERM, on_signal);
std::signal(SIGINT, on_signal);
FakeLoginServer server;
if (!server.Start())
{
std::fprintf(stderr, "port_fake_login_server: cannot listen on loopback\n");
return 1;
}
std::printf("ports %d %d\n", server.AuthPort(), server.GamePort());
std::fflush(stdout);
while (!g_stop)
pause();
std::fprintf(stderr, "port_fake_login_server events:");
for (const auto& e : server.Events())
std::fprintf(stderr, " %s", e.c_str());
std::fprintf(stderr, "\nport_fake_login_server error: %s\n", server.Error().c_str());
server.Stop();
return server.Failed() ? 1 : 0;
}
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
#pragma once
// The server half of port_login_flow_test: a loopback auth server and game server that answer the way
// the 40250 game server (game/src/desc.cpp, input.cpp, input_login.cpp, input_db.cpp) does for the
// login → select → loading path. It is built from mtnet's classic wire layouts, sequence table and
// cipher (tests/classic/), which were checked 1:1 against the 40250 server on 192.168.21.203, and shares
// no code with the ported client it talks to. Kept out of the client's translation unit: Packet.h's
// macros collide with wire_classic.h's constants.
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class FakeLoginServer
{
public:
static constexpr const char* kLogin = "fakeuser";
static constexpr const char* kPassword = "fakepass";
static constexpr const char* kCharacterName = "FakeWarrior";
static constexpr std::uint32_t kLoginKey = 0x13572468;
static constexpr std::uint32_t kMainVID = 0x00012345;
static constexpr std::int32_t kX = 469300, kY = 964200;
// A ground item in view at entergame (CItem::EncodeInsertPacket): vnum 19, owned by the character.
static constexpr std::uint32_t kGroundItemVID = 0x00054321;
static constexpr std::uint32_t kGroundItemVnum = 19;
static constexpr std::int32_t kGroundItemX = kX + 150, kGroundItemY = kY;
static constexpr std::uint32_t kExchangeItemVnum = 19, kExchangeGold = 1000;
// A stray dog (npclist 101) for the combat step; three hits kill it. The spawn point lies in the village's
// ATTRIBUTE_BANPK disc (CInstanceBase::IsInSafe blocks every hit there), so the dog stands 18 m north of it.
static constexpr std::uint32_t kMobVID = 0x00023456, kMobRace = 101;
static constexpr std::int32_t kMobX = kX, kMobY = kY - 1800;
static constexpr int kMobHits = 3;
// A general store (npclist 9003 "goods") 4 m east of the dog, for the shop step. Its one shelf item is a
// small red potion; selling it back pays kShopSellPrice (CShopManager::Sell: proto price / 5 minus 3 % tax).
static constexpr std::uint32_t kShopVID = 0x00034567, kShopRace = 9003;
static constexpr std::int32_t kShopX = kMobX + 400, kShopY = kMobY;
static constexpr std::uint32_t kShopItemVnum = 27001, kShopItemPrice = 50;
static constexpr std::int32_t kStartGold = 10000, kShopSellPrice = 9;
// An old man (npclist 20009) 0.5 m east of the store whose click runs a quest dialogue the way CQuestManager
// sends it: say + select ([QUESTION]), then a [NEXT] page, then a [DONE] page.
static constexpr std::uint32_t kQuestVID = 0x00045678, kQuestRace = 20009;
static constexpr std::int32_t kQuestX = kShopX + 50, kQuestY = kShopY;
// A storekeeper (npclist 9005) 1.5 m west of the shop whose click runs ShowMeSafeboxPassword,
// receives /safebox_password, opens safebox, handles checkin/checkout and close.
static constexpr std::uint32_t kSafeboxVID = 0x00056789, kSafeboxRace = 9005;
static constexpr std::int32_t kSafeboxX = kShopX - 150, kSafeboxY = kShopY;
static constexpr std::uint8_t kSafeboxSize = 9;
static constexpr std::uint32_t kSafeboxItemVnum = 10;
static constexpr std::int32_t kSafeboxMoney = 500;
// Messenger & Guild constants (批次 4-f)
static constexpr const char* kFriendName = "TestFriend";
static constexpr std::uint32_t kGuildID = 1;
static constexpr const char* kGuildName = "TestGuild";
~FakeLoginServer();
// Listens on two loopback ports and serves them from a thread until Stop().
bool Start();
void Stop();
int AuthPort() const { return m_auth_port; }
int GamePort() const { return m_game_port; }
// A third game-server port that only answers intrologin's ServerStateChecker (CG_STATE_CHECKER).
int StatePort() const { return m_state_port; }
// The channel port the state answer reports besides StatePort(), with status kOtherChannelStatus.
static constexpr int kOtherChannelPort = 13002;
static constexpr int kOtherChannelStatus = 1;
// Milestones in arrival order ("auth:login3", "game1:login2", ...), and the first protocol error.
std::vector<std::string> Events();
std::string Error();
bool Failed() { return !Error().empty(); }
bool Has(const std::string& event);
struct Connection;
private:
void Run();
bool ServeAuth(Connection& c);
bool ServeGame(Connection& c, int index);
bool Handshake(Connection& c, std::uint8_t phase_after);
void ServeState();
bool Accept(int listener, Connection& c);
void Event(const std::string& event);
bool Fail(const std::string& error);
int m_auth_listener = -1, m_game_listener = -1;
int m_state_listener = -1;
int m_auth_port = 0, m_game_port = 0, m_state_port = 0;
std::thread m_thread, m_state_thread;
std::atomic<bool> m_stop{false};
std::mutex m_mutex;
std::vector<std::string> m_events;
std::string m_error;
};
File diff suppressed because it is too large Load Diff
+173
View File
@@ -0,0 +1,173 @@
// 批次 2V1-a: the 40250 EterLib network layer (CNetworkStream over the winsock shim) against a
// loopback peer: non-blocking connect, buffered send/recv, the connect-failure timeout, and the
// Crypto++ key agreement (_IMPROVED_PACKET_ENCRYPTION_) with a second Cipher playing the server.
#include "EterLib/StdAfx.h"
#include "EterLib/NetDevice.h"
#include "EterLib/NetStream.h"
#include <chrono>
#include <cstdio>
#include <cstring>
#include <thread>
#include <vector>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
class TestStream : public CNetworkStream
{
public:
int successes = 0, failures = 0, remote_disconnects = 0;
using CNetworkStream::Prepare;
using CNetworkStream::Activate;
using CNetworkStream::ActivateCipher;
protected:
void OnConnectSuccess() override { ++successes; }
void OnConnectFailure() override { ++failures; }
void OnRemoteDisconnect() override { ++remote_disconnects; }
};
static int listen_loopback(int* port)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
socklen_t len = sizeof(addr);
getsockname(fd, reinterpret_cast<sockaddr*>(&addr), &len);
listen(fd, 1);
*port = ntohs(addr.sin_port);
return fd;
}
template <class F>
static bool pump(TestStream& stream, F done, int ms = 2000)
{
for (int i = 0; i < ms; ++i)
{
stream.Process();
if (done())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return false;
}
static bool read_exact(int fd, void* buf, size_t len)
{
char* p = static_cast<char*>(buf);
while (len)
{
ssize_t n = recv(fd, p, len, 0);
if (n <= 0)
return false;
p += n;
len -= static_cast<size_t>(n);
}
return true;
}
static void connect_and_exchange()
{
int port = 0;
int listener = listen_loopback(&port);
TestStream stream;
stream.SetRecvBufferSize(4096);
stream.SetSendBufferSize(4096);
CHECK(stream.Connect("127.0.0.1", port));
CHECK(pump(stream, [&] { return stream.IsOnline(); }));
CHECK(stream.successes == 1 && stream.failures == 0);
int peer = accept(listener, nullptr, nullptr);
CHECK(peer >= 0);
// Plain bytes both ways; Send only queues, Process flushes.
CHECK(stream.Send(5, "hello"));
pump(stream, [] { return true; }, 1);
char got[16] = {};
CHECK(read_exact(peer, got, 5) && std::memcmp(got, "hello", 5) == 0);
send(peer, "world!", 6, 0);
CHECK(pump(stream, [&] { return stream.GetRecvBufferSize() >= 6; }));
char peek[6] = {};
CHECK(stream.Peek(6, peek) && std::memcmp(peek, "world!", 6) == 0);
CHECK(stream.Recv(6, got) && stream.GetRecvBufferSize() == 0);
CHECK(!stream.Recv(1, got));
// Key agreement: each side prepares, swaps its public value and activates with the opposite
// polarity (the 40250 game server calls Activate(false, ...)).
Cipher server;
std::vector<unsigned char> client_pub(1024), server_pub(1024);
size_t client_len = client_pub.size(), server_len = server_pub.size();
size_t client_agreed = stream.Prepare(client_pub.data(), &client_len);
size_t server_agreed = server.Prepare(server_pub.data(), &server_len);
CHECK(client_agreed > 0 && client_agreed == server_agreed);
CHECK(stream.Activate(client_agreed, server_pub.data(), server_len));
CHECK(server.Activate(false, server_agreed, client_pub.data(), client_len));
stream.ActivateCipher();
server.set_activated(true);
CHECK(stream.IsSecurityMode());
// Client → server: ciphertext on the wire, the server's decoder restores it.
const char secret[] = "secret-login-packet";
CHECK(stream.Send(sizeof(secret), secret));
pump(stream, [] { return true; }, 1);
char wire[sizeof(secret)] = {};
CHECK(read_exact(peer, wire, sizeof(wire)));
CHECK(std::memcmp(wire, secret, sizeof(secret)) != 0);
server.Decrypt(wire, sizeof(wire));
CHECK(std::memcmp(wire, secret, sizeof(secret)) == 0);
// Server → client.
char reply[] = "phase-select";
server.Encrypt(reply, sizeof(reply));
send(peer, reply, sizeof(reply), 0);
CHECK(pump(stream, [&] { return stream.GetRecvBufferSize() >= static_cast<int>(sizeof(reply)); }));
CHECK(stream.Recv(sizeof(reply), got) && std::strcmp(got, "phase-select") == 0);
// The peer hangs up: recv answers 0, Process reports it once and clears the stream.
close(peer);
CHECK(pump(stream, [&] { return stream.remote_disconnects > 0; }));
CHECK(stream.remote_disconnects == 1 && !stream.IsOnline() && !stream.IsSecurityMode());
close(listener);
}
static void connect_refused()
{
// A port nobody listens on: Winsock reports the failed connect only in exceptfds, which
// CNetworkStream::Process does not pass, so the stream waits out the limit and then fails.
int port = 0;
int listener = listen_loopback(&port);
close(listener);
TestStream stream;
CNetworkAddress addr;
addr.Set("127.0.0.1", port);
CHECK(stream.Connect(addr, 1));
CHECK(pump(stream, [&] { return stream.failures > 0; }, 3000));
CHECK(stream.successes == 0 && stream.failures == 1 && !stream.IsOnline());
}
int main()
{
CNetworkDevice device;
CHECK(device.Create());
connect_and_exchange();
connect_refused();
if (g_failures)
{
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
return 1;
}
std::printf("port_net_test: ok\n");
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
// 批次 2V1: the ported 40250 UserInterface/Packet.h against the classic wire layouts in wire_classic.h,
// which the mtnet backend checked 1:1 against the 40250 server on 192.168.21.203. Win32 `long` is 32 bits and
// Packet.h now says LONG; a 64-bit long, or a missing #pragma pack(1), shows up here as a size mismatch.
// Covers the packets the login/select/loading phases send and read. The two headers cannot share a translation unit
// (Packet.h's macros such as SKILL_MAX_NUM collide with wire_classic.h's constants), so the wire sizes come from
// port_packet_test_wire.cpp.
#include "UserInterface/StdAfx.h"
#include "UserInterface/Packet.h"
#include <cstdio>
#include <cstddef>
struct WireSize { const char* name; std::size_t size; };
extern const WireSize kWireSizes[];
static const WireSize kPortSizes[] = {
// HandShake phase
{"TPacketGCHandshake", sizeof(TPacketGCHandshake)},
{"TPacketGCPhase", sizeof(TPacketGCPhase)},
{"TPacketGCBindUDP", sizeof(TPacketGCBindUDP)},
{"TPacketKeyAgreement", sizeof(TPacketKeyAgreement)},
{"TPacketKeyAgreementCompleted", sizeof(TPacketKeyAgreementCompleted)},
{"TPacketGCPing", sizeof(TPacketGCPing)},
{"TPacketCGPong", sizeof(TPacketCGPong)},
// Login phase (and the auth server, CAccountConnector)
{"TPacketCGLogin", sizeof(TPacketCGLogin)},
{"TPacketCGLogin2", sizeof(TPacketCGLogin2)},
{"TPacketCGLogin3", sizeof(TPacketCGLogin3)},
{"TPacketGCLoginKey", sizeof(TPacketGCLoginKey)},
{"TPacketGCAuthSuccess", sizeof(TPacketGCAuthSuccess)},
{"TSimplePlayerInformation", sizeof(TSimplePlayerInformation)},
{"TPacketGCLoginSuccess3", sizeof(TPacketGCLoginSuccess3)},
{"TPacketGCLoginSuccess4", sizeof(TPacketGCLoginSuccess4)},
{"TPacketGCLoginFailure", sizeof(TPacketGCLoginFailure)},
{"TPacketGCEmpire", sizeof(TPacketGCEmpire)},
{"TPacketCGEmpire", sizeof(TPacketCGEmpire)},
// Select phase
{"TPacketCGSelectCharacter", sizeof(TPacketCGSelectCharacter)},
{"TPacketCGCreateCharacter", sizeof(TPacketCGCreateCharacter)},
{"TPacketCGDestroyCharacter", sizeof(TPacketCGDestroyCharacter)},
{"TPacketCGChangeName", sizeof(TPacketCGChangeName)},
{"TPacketGCPlayerCreateSuccess", sizeof(TPacketGCPlayerCreateSuccess)},
{"TPacketGCCreateFailure", sizeof(TPacketGCCreateFailure)},
{"TPacketGCDestroyCharacterSuccess", sizeof(TPacketGCDestroyCharacterSuccess)},
{"TPacketGCChangeName", sizeof(TPacketGCChangeName)},
// Loading phase
{"TPacketGCMainCharacter", sizeof(TPacketGCMainCharacter)},
{"TPacketGCMainCharacter2_EMPIRE", sizeof(TPacketGCMainCharacter2_EMPIRE)},
{"TPacketGCMainCharacter3_BGM", sizeof(TPacketGCMainCharacter3_BGM)},
{"TPacketGCMainCharacter4_BGM_VOL", sizeof(TPacketGCMainCharacter4_BGM_VOL)},
{"TPacketGCPoints", sizeof(TPacketGCPoints)},
{"TPacketGCPointChange", sizeof(TPacketGCPointChange)},
{"TPacketGCItemSet2", sizeof(TPacketGCItemSet2)},
{"TPacketGCQuickSlotAdd", sizeof(TPacketGCQuickSlotAdd)},
{"TPacketGCCharacterUpdate", sizeof(TPacketGCCharacterUpdate)},
{"TPacketCGClientVersion2", sizeof(TPacketCGClientVersion2)},
{"TPacketCGEnterFrontGame", sizeof(TPacketCGEnterFrontGame)},
{nullptr, 0},
};
int main()
{
int failures = 0;
for (int i = 0; kPortSizes[i].name; ++i)
{
if (kPortSizes[i].size != kWireSizes[i].size)
{
std::fprintf(stderr, "%s is %zu bytes, %s is %zu\n", kPortSizes[i].name, kPortSizes[i].size,
kWireSizes[i].name, kWireSizes[i].size);
++failures;
}
}
if (failures)
return 1;
std::printf("port_packet_test: ok\n");
return 0;
}
+49
View File
@@ -0,0 +1,49 @@
// The classic wire sizes for port_packet_test.cpp, in the same order as its kPortSizes.
#include "classic/wire_classic.h"
#include <cstddef>
struct WireSize { const char* name; std::size_t size; };
namespace w = mtnet::classic;
extern const WireSize kWireSizes[] = {
{"Handshake", sizeof(w::Handshake)},
{"Phase_", sizeof(w::Phase_)},
{"BindUDP", sizeof(w::BindUDP)},
{"KeyAgreement", sizeof(w::KeyAgreement)},
{"KeyAgreementCompleted", sizeof(w::KeyAgreementCompleted)},
{"Blank", sizeof(w::Blank)},
{"Blank", sizeof(w::Blank)},
{"CGLogin", sizeof(w::CGLogin)},
{"CGLogin2", sizeof(w::CGLogin2)},
{"CGLogin3", sizeof(w::CGLogin3)},
{"GCLoginKey", sizeof(w::GCLoginKey)},
{"GCAuthSuccess", sizeof(w::GCAuthSuccess)},
{"SimplePlayer", sizeof(w::SimplePlayer)},
{"GCLoginSuccess3", sizeof(w::GCLoginSuccess3)},
{"GCLoginSuccess", sizeof(w::GCLoginSuccess)},
{"GCLoginFailure", sizeof(w::GCLoginFailure)},
{"GCEmpire", sizeof(w::GCEmpire)},
{"CGEmpire", sizeof(w::CGEmpire)},
{"CGPlayerSelect", sizeof(w::CGPlayerSelect)},
{"CGPlayerCreate", sizeof(w::CGPlayerCreate)},
{"CGPlayerDelete", sizeof(w::CGPlayerDelete)},
{"CGChangeName", sizeof(w::CGChangeName)},
{"GCPlayerCreateSuccess", sizeof(w::GCPlayerCreateSuccess)},
{"GCPlayerCreateFailure", sizeof(w::GCPlayerCreateFailure)},
{"GCPlayerDeleteSuccess", sizeof(w::GCPlayerDeleteSuccess)},
{"GCChangeName", sizeof(w::GCChangeName)},
{"GCMainCharacterOld", sizeof(w::GCMainCharacterOld)},
{"GCMainCharacter", sizeof(w::GCMainCharacter)},
{"GCMainCharacter3BGM", sizeof(w::GCMainCharacter3BGM)},
{"GCMainCharacter4BGM", sizeof(w::GCMainCharacter4BGM)},
{"GCPoints", sizeof(w::GCPoints)},
{"GCPointChange", sizeof(w::GCPointChange)},
{"GCItemSet", sizeof(w::GCItemSet)},
{"GCQuickSlotAdd", sizeof(w::GCQuickSlotAdd)},
{"GCCharacterUpdate", sizeof(w::GCCharacterUpdate)},
{"CGClientVersion2", sizeof(w::CGClientVersion2)},
{"CGEnterGame", sizeof(w::CGEnterGame)},
{nullptr, 0},
};
+207
View File
@@ -0,0 +1,207 @@
// port/ScriptLib CPythonLauncher on the embedded interpreter (批次 2P step 2), driven exactly as
// 40250 UserInterface.cpp does it: Main() creates the launcher, calls Create(), and RunMainScript()
// then sets __DEBUG__ and __COMMAND_LINE__ with RunLine before RunFile("system.py") reads the script
// out of the registered packs (UserInterface.cpp:241-353, 419-434).
//
// The script layer's own modules (initapp, initpack, initwndMgr, ...) arrive with the 2V0 slice and the
// stdlib with step 3, so system.py is expected to stop at its first import ("No module named app") —
// what this test proves is the launcher: the interpreter starts, RunLine/RunMemoryTextFile/
// RunCompiledFile run code, and RunFile reaches the real script through CEterPackManager.
//
// port_python_launcher_test <40250 Client dir>
//
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
#include "ScriptLib/StdAfx.h"
#include "ScriptLib/PythonLauncher.h"
#include "EterPack/EterPackManager.h"
#include "UserInterface/StdAfx.h" // initpack
#include "../../src/platform/ScriptLib/PythonHost.h"
#include "../../src/platform/UserInterface/UserInterface.h"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
// The dictionary the launcher runs everything in, and the globals it leaves behind there.
static PyObject* main_dict()
{
return PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
}
static long main_long(const char* name)
{
PyObject* main_module = PyImport_AddModule((char*) "__main__");
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
return value && PyInt_Check(value) ? PyInt_AsLong(value) : -1;
}
static std::string main_string(const char* name)
{
PyObject* main_module = PyImport_AddModule((char*) "__main__");
PyObject* value = PyDict_GetItemString(PyModule_GetDict(main_module), name);
return value && PyString_Check(value) ? PyString_AS_STRING(value) : "";
}
int main(int argc, char** argv)
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string client = argc > 1 ? argv[1] : "";
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
{
std::fprintf(stderr, "port_python_launcher_test: no 40250 Client/pack at '%s'\n", client.c_str());
return is_strict ? 1 : 77;
}
PackSingletons();
CHECK(PackInitialize("pack"));
// What a real python27 installation did for 40250, the host does here: flags before Py_Initialize,
// stdlib on sys.path after it (platform/ScriptLib/PythonHost.cpp).
PythonHost::Configure();
{
CPythonLauncher launcher; // ctor runs Py_Initialize, as 40250 Main() does on the stack
CHECK(Py_IsInitialized());
CHECK(launcher.Create());
std::string stdlib_error;
CHECK(PythonHost::InstallStdLib(stdlib.c_str(), &stdlib_error));
if (!stdlib_error.empty())
std::fprintf(stderr, "InstallStdLib: %s\n", stdlib_error.c_str());
// The stdlib is what the script layer's imports and the CP949/cp1252 text in the packs need.
CHECK(launcher.RunLine("import os, string, types, codecs\nassert u'\\xe4'.encode('cp1252') == '\\xe4'"));
// RunMainScript's first line (UserInterface.cpp:243): the pack module, which is how system.py's
// import hook and the window system read root/ and uiscript/ out of the registered packs.
initpack();
CHECK(launcher.RunLine("import pack"));
CHECK(launcher.RunLine("assert pack.Exist('system.py') == 1"));
CHECK(launcher.RunLine("assert pack.Exist('no/such.py') == 0"));
CHECK(launcher.RunLine("__pack_len__ = len(pack.Get('system.py'))"));
CHECK(launcher.RunLine("assert len(pack.Get('uiscript/selectcharacterwindow.py')) > 0"));
CHECK(launcher.RunLine("assert len(pack.Get('locale/en/locale_game.txt')) > 0"));
// Only .py/.pyc/.txt come out of it, and a miss is None rather than an exception
// (packGet returns Py_BuildException(), which clears the error and builds None).
CHECK(launcher.RunLine("assert pack.Get('icon/item/00000.tga') is None"));
CHECK(launcher.RunLine("assert pack.Get('no/such.py') is None"));
// RegisterDebugFlag / RegisterCommandLine, verbatim from RunMainScript.
CHECK(launcher.RunLine("__DEBUG__ = 0"));
CHECK(launcher.RunLine("__COMMAND_LINE__ = \"\""));
CHECK(main_long("__DEBUG__") == 0);
CHECK(main_string("__COMMAND_LINE__") == "");
// Create() adds TRUE/FALSE to __builtin__, which the 40250 scripts use unqualified.
CHECK(launcher.RunLine("__ok__ = (TRUE, FALSE)"));
CHECK(launcher.RunLine("assert TRUE == 1 and FALSE == 0"));
// A failing line reports through Traceback(), which fetches (and so clears) the exception.
CHECK(!launcher.RunLine("raise RuntimeError('port test')"));
CHECK(PyErr_Occurred() == NULL);
// GetError() reads the pending exception's value with neither normalization nor a NULL check,
// so it is only defined while an error really is pending and was set with a string value — after
// RunLine it never is, because Traceback() got there first. Kept verbatim: it is what 40250 does.
PyErr_SetString(PyExc_RuntimeError, "port test error");
CHECK(std::string(launcher.GetError()) == "port test error");
PyErr_Clear();
// RunMemoryTextFile wraps the text in exec(compile(...)) after dropping CR, so a CRLF script
// from the pack compiles and the file name it is given reaches the code object.
const char crlf_script[] = "__mem__ = 1\r\ndef f():\r\n\treturn f.func_code.co_filename\r\n__mem__ = f()\r\n";
CHECK(launcher.RunMemoryTextFile("crlf.py", sizeof(crlf_script) - 1, crlf_script));
CHECK(main_string("__mem__") == "crlf.py");
// RunCompiledFile reads a .pyc through the ported PythonMarshal readers: magic, timestamp,
// then the code object.
const std::string pyc = std::string(std::getenv("TMPDIR") ? std::getenv("TMPDIR") : "/tmp") +
"/port_python_launcher_test.pyc";
PyObject* code = Py_CompileString("__pyc__ = 7\n", "compiled.py", Py_file_input);
CHECK(code != NULL);
if (code)
{
FILE* fp = fopen(pyc.c_str(), "wb");
CHECK(fp != NULL);
if (fp)
{
PyMarshal_WriteLongToFile(PyImport_GetMagicNumber(), fp, Py_MARSHAL_VERSION);
PyMarshal_WriteLongToFile(0, fp, Py_MARSHAL_VERSION); // mtime, ignored on read
PyMarshal_WriteObjectToFile(code, fp, Py_MARSHAL_VERSION);
fclose(fp);
CHECK(launcher.RunCompiledFile(pyc.c_str()));
CHECK(main_long("__pyc__") == 7);
}
Py_DECREF(code);
std::remove(pyc.c_str());
}
CHECK(!launcher.RunCompiledFile("no/such.pyc"));
// RunFile pulls the script out of the packs. system.py is the 40250 entry point; without the
// stdlib (step 3) and the C++ modules (2V0) it stops at its first import, which is still proof
// that the pack read path and the compile reached Python.
{
CMappedFile file;
const VOID* data = NULL;
CHECK(CEterPackManager::Instance().Get(file, "system.py", &data));
CHECK(file.Size() > 0);
CHECK(main_long("__pack_len__") == (long) file.Size()); // pack.Get returned the same entry
// Where it stops, spelled out: RunFile reports through Traceback(), which fetches the
// exception and so leaves nothing behind, and LogBoxf is a platform stub. Running the same
// source here once names it: today "No module named app", i.e. system.py compiles and runs
// until the first of the C++ modules RunMainScript registers (initapp, 2V0 slice) — the
// stdlib imports behind it come with step 3.
std::string source((const char*) data, file.Size());
source.erase(std::remove(source.begin(), source.end(), '\r'), source.end());
PyObject* result = PyRun_String(source.c_str(), Py_file_input, main_dict(), main_dict());
CHECK(result == NULL);
if (result)
Py_DECREF(result);
else
{
PyObject *exc = NULL, *value = NULL, *tb = NULL;
PyErr_Fetch(&exc, &value, &tb);
PyErr_NormalizeException(&exc, &value, &tb);
PyObject* text = value ? PyObject_Str(value) : NULL;
std::printf("system.py stops at %s: %s\n",
exc ? ((PyTypeObject*) exc)->tp_name : "?",
text ? PyString_AsString(text) : "");
CHECK(exc == PyExc_ImportError);
Py_XDECREF(text);
Py_XDECREF(exc);
Py_XDECREF(value);
Py_XDECREF(tb);
}
}
// Whatever it stops at, RunLine's Traceback() has already reported and cleared it, so there is
// nothing left for GetError() to read here either.
const bool ran = launcher.RunFile("system.py");
CHECK(!ran);
CHECK(PyErr_Occurred() == NULL);
CHECK(!launcher.RunFile("no/such/script.py")); // not in any pack: no file, no exception
launcher.Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
}
CHECK(!Py_IsInitialized());
if (g_failures)
std::fprintf(stderr, "%d failure(s)\n", g_failures);
else
std::printf("port_python_launcher_test: PASS\n");
return g_failures ? 1 : 0;
}
@@ -0,0 +1,47 @@
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonWindowManager.h"
#include "ScriptLib/PythonLauncher.h"
#include "UserInterface/StdAfx.h"
extern void initapp();
#include "../../src/platform/ScriptLib/PythonHost.h"
#include <cstdio>
#include <string>
int main(int argc, char** argv)
{
const std::string stdlib = argc > 1 ? argv[1] : PythonHost::DefaultStdLibPath();
PythonHost::Configure();
CPythonLauncher launcher;
if (!launcher.Create())
return 1;
std::string error;
if (!PythonHost::InstallStdLib(stdlib.c_str(), &error))
{
std::fprintf(stderr, "InstallStdLib: %s\n", error.c_str());
return 1;
}
{
UI::CWindowManager manager;
initgrp();
initgrpImage();
initgrpText();
initgrpThing();
initwndMgr();
initapp();
if (!launcher.RunLine("import wndMgr, grp, grpImage, grpText, grpThing, app"))
return 1;
if (!launcher.RunLine(
"class Handler: pass\n"
"window = wndMgr.Register(Handler(), 'UI')\n"
"assert window > 0\n"
"wndMgr.SetName(window, 'roundtrip')\n"
"wndMgr.SetWindowSize(window, 100, 50)\n"
"assert wndMgr.GetWindowWidth(window) == 100\n"
"wndMgr.Destroy(window)\n"))
return 1;
manager.Update();
}
launcher.Clear();
return 0;
}
+145
View File
@@ -0,0 +1,145 @@
// 批次 2V0-g: the 40250 font path (CGraphicText → CGraphicFontTexture → CGraphicDib) over the GDI
// emulation in platform/Win32Gdi.cpp, and the code-page conversions CGraphicTextInstance runs text through.
//
// Needs a system Tahoma (macOS ships /System/Library/Fonts/Supplemental/Tahoma.ttf); exit 77 (ctest
// SKIP) without one, unless MT_ASSETS_STRICT=1.
#include "EterLib/StdAfx.h"
#include "EterLib/GrpText.h"
#include "EterLib/GrpTextInstance.h"
#include "EterLib/Util.h"
#include "EterLib/ResourceManager.h"
#include "../../src/platform/EterLib/UIRenderCommands.h"
#include "../../src/platform/UserInterface/UserInterface.h"
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
static void code_pages()
{
// CP949 "굴림" (B1BC B8B2) → U+AD74 U+B9BC, and back.
const char gulim[] = "\xb1\xbc\xb8\xb2";
WCHAR wide[8] = {};
CHECK(MultiByteToWideChar(949, 0, gulim, -1, nullptr, 0) == 3);
CHECK(MultiByteToWideChar(949, 0, gulim, -1, wide, 8) == 3);
CHECK(wide[0] == 0xAD74 && wide[1] == 0xB9BC && wide[2] == 0);
char narrow[8] = {};
CHECK(WideCharToMultiByte(949, 0, wide, -1, narrow, sizeof(narrow), nullptr, nullptr) == 5);
CHECK(std::string(narrow) == gulim);
// UTF-8 and the Windows size rules: a too-small buffer answers 0.
CHECK(MultiByteToWideChar(CP_UTF8, 0, "\xea\xb5\xb4", 3, wide, 8) == 1 && wide[0] == 0xAD74);
CHECK(MultiByteToWideChar(CP_UTF8, 0, "abc", 3, wide, 2) == 0);
// CP1252 0x80 is the euro sign.
CHECK(MultiByteToWideChar(1252, 0, "\x80", 1, wide, 8) == 1 && wide[0] == 0x20AC);
// 40250 Util.cpp face table.
CHECK(SetDefaultCodePage(949) && std::string(GetDefaultFontFace()) == "GulimChe");
CHECK(GetCharsetFromCodePage(949) == HANGUL_CHARSET);
CHECK(SetDefaultCodePage(1252) && std::string(GetDefaultFontFace()) == "Arial");
CHECK(GetDefaultCodePage() == 1252);
}
static int glyph_quads(float* min_x, float* max_x)
{
int count = 0;
*min_x = 1e9f;
*max_x = -1e9f;
for (const auto& command : UIRenderCommands())
{
if (command.kind != UIRenderCommand::Image || command.text.rfind("mem:", 0) != 0)
continue;
++count;
for (float x : command.qx)
{
*min_x = std::fmin(*min_x, x);
*max_x = std::fmax(*max_x, x);
}
}
return count;
}
int main()
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
PackSingletons();
CResourceManager resource_manager; // CResource::Release queues the resource here
code_pages();
if (access("/System/Library/Fonts/Supplemental/Tahoma.ttf", 0) != 0)
{
std::fprintf(stderr, "port_text_test: no system Tahoma\n");
return g_failures ? 1 : (is_strict ? 1 : 77);
}
// Not in any pack: CResource::Load falls through to OnLoad(0, NULL), as for every 40250 .fnt.
CGraphicText* text = new CGraphicText("Tahoma:12.fnt");
text->AddReference();
text->Load();
CHECK(!text->IsEmpty());
CGraphicTextInstance instance;
instance.SetTextPointer(text);
instance.SetValue("Hello");
instance.Update();
int width = 0, height = 0;
instance.GetTextSize(&width, &height);
// GDI lays a 12-pixel LOGFONT cell out as Tahoma's win ascent + descent at 10 ppem: 12 rows.
CHECK(height == 12);
CHECK(width > 20 && width < 40);
CHECK(instance.PixelPositionToCharacterPosition(0) == 0);
CHECK(instance.PixelPositionToCharacterPosition(width - 1) == 4);
CHECK(instance.PixelPositionToCharacterPosition(width + 5) == -1);
UIRenderSetSize(800, 600);
UIRenderBeginFrame();
instance.SetPosition(100.0f, 50.0f);
instance.Render();
float left = 0, right = 0;
CHECK(glyph_quads(&left, &right) == 5);
CHECK(std::fabs(left - 100.0f) <= 1.5f);
// Centred: the text straddles the position.
instance.SetHorizonalAlign(CGraphicTextInstance::HORIZONTAL_ALIGN_CENTER);
instance.Update();
UIRenderBeginFrame();
instance.Render();
CHECK(glyph_quads(&left, &right) == 5);
CHECK(left < 100.0f && right > 100.0f && std::fabs((left + right) * 0.5f - 100.0f) <= 2.0f);
// The glyph page has ink.
bool inked = false;
for (const auto& command : UIRenderCommands())
{
UIMemoryTexture page;
if (command.text.rfind("mem:", 0) == 0 && UIRenderMemoryTexture(command.text, &page))
for (std::uint32_t argb : page.argb)
inked |= (argb >> 24) != 0;
}
CHECK(inked);
instance.Destroy();
text->Release();
if (g_failures)
{
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
return 1;
}
std::printf("port_text_test: ok\n");
return 0;
}
+61
View File
@@ -0,0 +1,61 @@
#include "EterBase/StdAfx.h"
#include "EterBase/Utils.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
int main()
{
int failures = 0;
auto check = [&failures](bool ok, const char *name) {
if (!ok) {
std::fprintf(stderr, "FAIL %s\n", name);
++failures;
}
};
check(MIN(-3, 8) == -3 && MIN(8, -3) == -3, "MIN order/sign");
check(MAX(-3, 8) == 8 && MAX(8, -3) == 8, "MAX order/sign");
check(MINMAX(2, -5, 9) == 2, "MINMAX lower");
check(MINMAX(2, 20, 9) == 9, "MINMAX upper");
check(MINMAX(9, 4, 2) == 9, "MINMAX reversed bounds");
check(fMIN(-3.5f, 8.5f) == -3.5f, "fMIN sign");
check(fMAX(200.0f, 0.0f) == 200.0f, "fMAX camera floor");
check(fMINMAX(2.0f, -5.0f, 9.0f) == 2.0f, "fMINMAX lower");
check(fMINMAX(2.0f, 20.0f, 9.0f) == 9.0f, "fMINMAX upper");
// Path helpers (40250 bodies): CFlyingData::LoadScriptFile resolves a relative attach .mse against
// GetOnlyPathName of the .msf unless IsGlobalFileName (a drive letter) says it is already absolute.
check(std::string(GetOnlyPathName("d:/ymir work/effect/a.msf")) == "d:/ymir work/effect/", "GetOnlyPathName dir");
check(std::string(GetOnlyPathName("a.msf")).empty(), "GetOnlyPathName bare");
check(IsGlobalFileName("d:/ymir work/a.mse") && !IsGlobalFileName("etc/a.mse"), "IsGlobalFileName");
std::string name;
GetOnlyFileName("d:\\ymir work\\a.mse", name);
check(name == "a.mse", "GetOnlyFileName backslash");
std::string path, stem, ext;
GetFilePathNameExtension("d:/x/y.tar.gz", (int) strlen("d:/x/y.tar.gz"), &path, &stem, &ext);
check(path == "d:/x/" && stem == "y.tar" && ext == "gz", "GetFilePathNameExtension");
ext.clear();
GetFileExtension("a/b.mse", 7, &ext);
check(ext == "mse", "GetFileExtension");
char p[64] = {}, n[64] = {}, e[64] = {};
GetFileNameParts("d:/x/y.dds", 10, p, n, e);
check(!strcmp(p, "d:/x/") && !strcmp(n, "y") && !strcmp(e, "dds"), "GetFileNameParts");
std::vector<std::string> tokens;
check(SplitLine("a \"b c\" d", " ", &tokens) && tokens.size() == 3 && tokens[1] == "b c", "SplitLine quotes");
std::string except = "a-b-c";
StringExceptCharacter(&except, "-");
check(except == "abc", "StringExceptCharacter");
char indexed[32] = "tex";
GetIndexingName(indexed, 12);
GetOldIndexingName(indexed, 7);
check(!strcmp(indexed, "tex127"), "GetIndexingName/GetOldIndexingName");
std::string local;
check(GetLocalFileName("D:\\ymir\\", "d:/ymir/a.dds", &local) && local == "a.dds", "GetLocalFileName");
int argc = 0;
PCHAR* argv = CommandLineToArgv(const_cast<PCHAR>("x \"y z\" w"), &argc);
check(argc == 3 && !strcmp(argv[1], "y z"), "CommandLineToArgv");
free(argv);
return failures ? 1 : 0;
}
+322
View File
@@ -0,0 +1,322 @@
// port/EterPythonLib 的窗口树(批次 2V0-b):UI::CWindow / UI::CWindowManager 在真解释器上跑一遍。
//
// 这一层是 40250 整个 UI 的骨架:Python 侧每个窗口对象背后都是一个 CWindow,布局(UpdateRect 的
// 对齐规则)、命中(PickWindow 的 flag 语义)、拖动、焦点、按键捕获全在 C++ 里,Python 只收事件。
// wndMgr 模块(2V0-c)只是把这些函数包一层,所以这里直接按 C++ API 驱动,验的是同一份逻辑。
//
// 渲染不碰:Render()/OnRender() 需要 CPythonGraphic 和图形设备,那是 2V0-f 的事。解释器是必须的 ——
// CWindowManager 的构造函数就 Py_BuildValue("()"),事件也确实要发到 Python 处理器上。
//
// port_window_tree_test <python27.zip>
//
// 缺 python27.zip 时退 77(ctest SKIP),MT_ASSETS_STRICT=1 时改成失败。
#include "EterPythonLib/StdAfx.h"
#include "EterPythonLib/PythonWindow.h"
#include "EterPythonLib/PythonWindowManager.h"
#include "ScriptLib/PythonLauncher.h"
#include "../../src/platform/ScriptLib/PythonHost.h"
#include <cstdio>
#include <cstdlib>
#include <string>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
// Python 侧的处理器:每个事件往 LOG 里记一条,C++ 这边取出来比对。这正是 40250 的 ui.py 做的事情
// (Window 类的 OnMouseOverIn / OnKeyDown ... 都是普通的 Python 方法)。
static const char* const kHandlerScript =
"LOG = []\n"
"class Handler:\n"
" def __init__(self, name, eat = 0):\n"
" self.name = name\n"
" self.eat = eat\n"
" def OnMouseOverIn(self): LOG.append(self.name + ':in')\n"
" def OnMouseOverOut(self): LOG.append(self.name + ':out')\n"
" def OnMouseLeftButtonDown(self): LOG.append(self.name + ':down')\n"
" def OnMouseLeftButtonUp(self): LOG.append(self.name + ':up')\n"
" def OnSetFocus(self): LOG.append(self.name + ':focus')\n"
" def OnKillFocus(self): LOG.append(self.name + ':blur')\n"
" def OnUpdate(self): LOG.append(self.name + ':update')\n"
" def OnKeyDown(self, key):\n"
" LOG.append('%s:kd%d' % (self.name, key))\n"
" return self.eat\n"
" def OnKeyUp(self, key):\n"
" LOG.append('%s:ku%d' % (self.name, key))\n"
" return 1\n"
"A = Handler('A')\n"
"B = Handler('B')\n"
"C = Handler('C', 1)\n";
static PyObject* main_dict()
{
return PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
}
static PyObject* handler(const char* name)
{
PyObject* object = PyDict_GetItemString(main_dict(), name); // Borrowed; __main__ 一直持有
if (!object)
std::fprintf(stderr, "handler('%s') missing\n", name);
return object;
}
// LOG 的内容,取完清空 —— 每个检查点只看自己那一段。
static std::string take_log()
{
PyObject* log = PyDict_GetItemString(main_dict(), "LOG");
if (!log || !PyList_Check(log))
return "<no LOG>";
std::string text;
for (Py_ssize_t i = 0; i < PyList_Size(log); ++i)
{
PyObject* item = PyList_GetItem(log, i);
if (!text.empty())
text += ' ';
text += PyString_Check(item) ? PyString_AS_STRING(item) : "?";
}
PyList_SetSlice(log, 0, PyList_Size(log), NULL);
return text;
}
static bool rect_is(UI::CWindow* window, long left, long top, long right, long bottom)
{
const RECT& rect = window->GetRect();
if (rect.left == left && rect.top == top && rect.right == right && rect.bottom == bottom)
return true;
std::fprintf(stderr, "rect(%s) = %ld,%ld,%ld,%ld, expected %ld,%ld,%ld,%ld\n", window->GetName(),
(long) rect.left, (long) rect.top, (long) rect.right, (long) rect.bottom,
left, top, right, bottom);
return false;
}
static void run(UI::CWindowManager& wm)
{
// 40250 是先 SetScreenSize 再 SetResolution(UserInterface 的 OnSizeChange)。分辨率为 0 时
// SetMousePosition 直接 return,鼠标坐标永远是 0 —— 这条顺序是有意义的。
wm.SetScreenSize(800, 600);
wm.SetResolution(800, 600);
CHECK(wm.GetScreenWidth() == 800 && wm.GetScreenHeight() == 600);
long hres = 0, vres = 0;
wm.GetResolution(hres, vres);
CHECK(hres == 800 && vres == 600);
CHECK(wm.GetAspect() == 800.0f / 600.0f);
// ---- 层与树 ----
UI::CWindow* a = wm.RegisterWindow(handler("A"), "UI");
a->SetName("A");
CHECK(std::string(a->GetParent()->GetName()) == "UI");
// 五个层(GAME/UI_BOTTOM/UI/TOP_MOST/CURTAIN)是 CLayer,IsWindow() 为 FALSE,所以注册到层上的
// 窗口自己就是 root —— Python 侧的 GetRoot() 从来拿不到层。
CHECK(a->GetRoot() == a);
CHECK(!a->IsShow());
a->SetSize(100, 50);
a->SetPosition(10, 20);
a->Show();
CHECK(rect_is(a, 10, 20, 110, 70));
UI::CWindow* b = wm.RegisterWindow(handler("B"), "UI");
b->SetName("B");
wm.SetParent(b, a);
CHECK(b->GetParent() == a && a->GetChildCount() == 1);
CHECK(b->GetRoot() == a);
b->SetSize(20, 10);
b->SetPosition(5, 5);
b->Show();
CHECK(rect_is(b, 15, 25, 35, 35));
// ---- UpdateRect 的对齐规则(父窗口 100x50 @ 10,20,子窗口 20x10 @ 5,5)----
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_RIGHT); // left = |100| - 5 + 10
CHECK(rect_is(b, 105, 25, 125, 35));
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_CENTER); // left = 100/2 - 20/2 + 5 + 10
CHECK(rect_is(b, 55, 25, 75, 35));
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_BOTTOM); // top = 50 - 5 + 20
CHECK(rect_is(b, 55, 65, 75, 75));
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_CENTER); // top = (50 - 10)/2 + 5 + 20
CHECK(rect_is(b, 55, 45, 75, 55));
b->SetHorizontalAlign(UI::CWindow::HORIZONTAL_ALIGN_LEFT);
b->SetVerticalAlign(UI::CWindow::VERTICAL_ALIGN_TOP);
CHECK(rect_is(b, 15, 25, 35, 35));
// 父窗口移动,子窗口跟着走(UpdateRect 会 for_each 到所有子窗口)。
a->SetPosition(20, 30);
CHECK(rect_is(a, 20, 30, 120, 80) && rect_is(b, 25, 35, 45, 45));
a->SetPosition(10, 20);
// IsRendering:自己 Show 不够,链路上每一级都要 Show。
CHECK(a->IsRendering() && b->IsRendering());
a->Hide();
CHECK(b->IsShow() && !b->IsRendering());
a->Show();
// ---- 命中 ----
UI::CWindow* c = wm.RegisterWindow(handler("C"), "UI");
c->SetName("C");
c->SetSize(100, 50);
c->SetPosition(10, 20);
c->Show();
take_log();
// 同层后注册的在上面(PickWindow 反向遍历子链表)。
wm.RunMouseMove(50, 40);
CHECK(wm.GetPointWindow() == c);
CHECK(take_log() == "C:in");
// 鼠标坐标按 屏幕尺寸/分辨率 换算,这里 1:1。
long mx = 0, my = 0;
wm.GetMousePosition(mx, my);
CHECK(mx == 50 && my == 40);
// C 藏起来,命中落到 A 的子树上。
c->Hide();
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == b);
CHECK(take_log() == "C:out B:in");
// FLAG_NOT_PICK:这个窗口自己不被选中,但仍然往下找子窗口 —— 结果落回父窗口。
b->AddFlag(UI::CWindow::FLAG_NOT_PICK);
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == a);
CHECK(take_log() == "B:out A:in");
b->RemoveFlag(UI::CWindow::FLAG_NOT_PICK);
// FLAG_IGNORE_SIZE:不做 IsIn 判断,只要父窗口被命中就轮到它。
b->AddFlag(UI::CWindow::FLAG_IGNORE_SIZE);
wm.RunMouseMove(100, 65); // 在 A 里,不在 B 的矩形里
CHECK(wm.GetPointWindow() == b);
b->RemoveFlag(UI::CWindow::FLAG_IGNORE_SIZE);
wm.RunMouseMove(100, 65);
CHECK(wm.GetPointWindow() == a);
take_log();
// 层外面不命中任何窗口。
wm.RunMouseMove(700, 500);
CHECK(wm.GetPointWindow() == NULL);
take_log();
// SetPickAlways:不管挡在前面的是谁,只要在矩形里就先拿到(工具提示、拖动图标用的)。
c->Show();
wm.SetPickAlways(b);
wm.RunMouseMove(20, 28);
CHECK(wm.GetPointWindow() == b);
take_log();
// ---- SetTop:只对 FLAG_FLOAT 的窗口生效 ----
CHECK(!a->IsFlag(UI::CWindow::FLAG_FLOAT));
wm.SetTop(a); // 没有 FLAG_FLOAT,什么也不做
a->AddFlag(UI::CWindow::FLAG_FLOAT);
wm.SetTop(a); // A 挪到层的链表末尾,压过 C
wm.RunMouseMove(90, 65); // A 和 C 都覆盖这里
CHECK(wm.GetPointWindow() == a);
take_log();
// ---- 拖动:FLAG_MOVABLE 时窗口管理器直接改坐标 ----
a->AddFlag(UI::CWindow::FLAG_MOVABLE);
wm.RunMouseLeftButtonDown(90, 65);
CHECK(take_log() == "A:down");
wm.RunMouseMove(100, 75); // 位移 +10,+10
CHECK(a->GetPositionX() == 20 && a->GetPositionY() == 30);
// FLAG_LIMIT:窗口不许离开屏幕,LimitBias 是四边的留白。
a->AddFlag(UI::CWindow::FLAG_LIMIT);
a->SetLimitBias(0, 0, 0, 0);
wm.RunMouseMove(2000, 2000);
CHECK(a->GetPositionX() == 800 - a->GetWidth() && a->GetPositionY() == 600 - a->GetHeight());
wm.RunMouseMove(-2000, -2000);
CHECK(a->GetPositionX() == 0 && a->GetPositionY() == 0);
wm.RunMouseLeftButtonUp(-2000, -2000);
a->RemoveFlag(UI::CWindow::FLAG_LIMIT);
a->RemoveFlag(UI::CWindow::FLAG_MOVABLE);
a->SetPosition(10, 20);
take_log();
// ---- 焦点 ----
CHECK(!wm.IsFocus());
wm.ActivateWindow(a);
CHECK(wm.IsFocus() && wm.IsFocusWindow(a) && wm.GetActivateWindow() == a);
CHECK(take_log() == "A:focus");
wm.ActivateWindow(c);
CHECK(wm.IsFocusWindow(c) && !wm.IsFocusWindow(a));
CHECK(take_log() == "A:blur C:focus");
wm.DeactivateWindow();
CHECK(wm.IsFocusWindow(a));
CHECK(take_log() == "C:blur A:focus");
wm.DeactivateWindow();
CHECK(!wm.IsFocus());
CHECK(take_log() == "A:blur");
// ---- 按键:RunKeyDownEvent 从 root 往下走,第一个 OnKeyDown 返回真的窗口吃掉它并被记成捕获者 ----
a->Hide(); // A 的处理器返回 0,藏起来让这段日志只剩 C
wm.RunKeyDown(65);
CHECK(take_log() == "C:kd65");
// KeyUp 只发给捕获者,不再重新查找。
wm.RunKeyUp(65);
CHECK(take_log() == "C:ku65");
// 没人捕获的键:RunKeyUpEvent 从 root 往下发一轮(C 的 OnKeyUp 返回 1,到此为止)。
wm.RunKeyUp(66);
CHECK(take_log() == "C:ku66");
a->Show();
// ---- Update 沿着树走,Hide 的分支整支跳过 ----
take_log();
wm.Update();
CHECK(take_log() == "C:update A:update B:update");
b->Hide();
wm.Update();
CHECK(take_log() == "C:update A:update");
b->Show();
// ---- 销毁:先从管理器的各个指针里摘干净,再从父窗口摘掉,真正的 delete 留到下一次 Update ----
wm.RunMouseMove(90, 65);
CHECK(wm.GetPointWindow() == a);
wm.ActivateWindow(a);
wm.RunKeyDown(65); // A 的处理器返回 0,捕获者还是 C
take_log();
wm.DestroyWindow(c);
CHECK(wm.GetActivateWindow() != c);
wm.DestroyWindow(a); // B 是 A 的子窗口,Clear() 只断链不删
CHECK(wm.GetPointWindow() == NULL);
CHECK(!b->HasParent() && !b->IsShow());
wm.Update(); // 这里才 delete,a/c 从此不可用
CHECK(take_log() == "");
delete b; // 40250 也不管子窗口,Python 那边各自 Destroy
}
int main(int argc, char** argv)
{
const char* strict = std::getenv("MT_ASSETS_STRICT");
const bool is_strict = strict && std::string(strict) == "1";
const std::string stdlib = argc > 1 ? argv[1] : PythonHost::DefaultStdLibPath();
if (stdlib.empty() || std::fopen(stdlib.c_str(), "rb") == NULL)
{
std::fprintf(stderr, "port_window_tree_test: no python27.zip at '%s'\n", stdlib.c_str());
return is_strict ? 1 : 77;
}
PythonHost::Configure();
{
CPythonLauncher launcher; // 和 40250 Main() 一样在栈上,构造即 Py_Initialize
CHECK(launcher.Create());
std::string error;
CHECK(PythonHost::InstallStdLib(stdlib.c_str(), &error));
if (!error.empty())
std::fprintf(stderr, "InstallStdLib: %s\n", error.c_str());
CHECK(launcher.RunLine(kHandlerScript));
{
UI::CWindowManager wm; // 构造函数建 root + 五个层,析构函数 Py_DECREF 空元组
run(wm);
}
CHECK(PyErr_Occurred() == NULL);
launcher.Clear();
}
CHECK(!Py_IsInitialized());
std::printf("%s: %d failure(s)\n", argv[0], g_failures);
return g_failures == 0 ? 0 : 1;
}