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:
co-authored by
Claude Opus 5.5
parent
70710477cf
commit
a46093104c
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user