Metin2 game client (P0–P11) + mobile asset pipeline

Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
This commit is contained in:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+83
View File
@@ -0,0 +1,83 @@
#pragma once
// EterPack — reader/writer for this m2dev fork's single-file asset pack
// (PackLib/Pack.cpp). NOT the classic Metin2 .eix/.epk format.
//
// Layout:
// [Header: u64 entry_num, u64 data_begin, u8 nonce[24]] (40 bytes)
// [Entry x entry_num] each XChaCha20-encrypted with Header.nonce + PACK_KEY
// Entry: char name[NAME_FIELD], u64 offset, u64 file_size,
// u64 compressed_size, u8 encryption, u8 nonce[24]
// [data blob @ data_begin]
// per file @ data_begin+offset, compressed_size bytes:
// encryption==0 -> zstd frame -> zstd-decompress to file_size
// encryption==1 -> XChaCha20(entry.nonce)-> zstd-decompress to file_size
//
// NAME_FIELD is FILENAME_MAX+1 on the packer's platform (MSVC PackMaker = 261).
// The reader derives it from (data_begin - 40) / entry_num, falling back to the
// known platform values, so it stays compatible with real packs.
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace mtpack {
inline constexpr int PACK_NONCE_SIZE = 24;
inline constexpr int PACK_KEY_SIZE = 32;
// PackLib/config.h PACK_KEY — the fork ships this hardcoded.
inline constexpr uint8_t PACK_KEY[PACK_KEY_SIZE] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10
};
// MSVC PackMaker.exe: FILENAME_MAX == 260, so the name field is 261 bytes.
inline constexpr int PACK_NAME_FIELD_DEFAULT = 261;
struct Entry {
std::string name;
uint64_t offset = 0;
uint64_t file_size = 0;
uint64_t compressed_size = 0;
uint8_t encryption = 0;
uint8_t nonce[PACK_NONCE_SIZE] = {};
};
class EterPack {
public:
// Load the index (mmaps the file; keeps it open for read()).
bool open(const std::string &path, std::string *err = nullptr);
void close();
bool has(const std::string &name) const { return m_by_name.count(norm(name)) != 0; }
std::vector<std::string> names() const;
size_t count() const { return m_index.size(); }
// Decompress (+decrypt) one file into `out`.
bool read(const std::string &name, std::vector<uint8_t> &out, std::string *err = nullptr) const;
int name_field() const { return m_name_field; }
private:
static std::string norm(std::string s); // '\\'->'/', lowercase
std::vector<uint8_t> m_file; // whole pack in memory (simple; mmap later)
uint64_t m_data_begin = 0;
std::vector<Entry> m_index;
std::unordered_map<std::string, size_t> m_by_name;
int m_name_field = PACK_NAME_FIELD_DEFAULT;
};
// Build a pack from (name, bytes) pairs. `encrypt` -> per-file encryption==1.
// Uses PACK_NAME_FIELD_DEFAULT so real clients / PackLib can read it back.
struct InputFile {
std::string name;
std::vector<uint8_t> data;
};
bool write_pack(const std::string &out_path, const std::vector<InputFile> &files, bool encrypt,
std::string *err = nullptr);
} // namespace mtpack