feat(currency_wars): complete systems, settlement Rank SS, damage report, camera and recovery fixes
This commit is contained in:
1 parent
c008243f11
commit
0995055296
78 files changed
+33840
-9525
No files matched your search
@@ -0,0 +1,108 @@
|
||||
# Honkai: Star Rail Protocol & Emulation Knowledge Base
|
||||
> **Comprehensive Architectural Deep-Dive Collection**
|
||||
> *Prepared for engineers, reverse engineers, and autonomous AI agents.*
|
||||
|
||||
This directory contains complete, code-level architectural specifications and cryptographic reverse-engineering documentation for the core open-source projects in the *Honkai: Star Rail* emulation, protocol de-obfuscation, and metadata analysis ecosystem.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Master Index & Technical Specifications
|
||||
|
||||
| File | Target Project & Stack | Core Capabilities & Focus Area |
|
||||
| :--- | :--- | :--- |
|
||||
| **[lunarcore_architecture_deep_dive.md](./lunarcore_architecture_deep_dive.md)** | **LunarCore**<br>*(Java 17 / Netty / KCP)* | Full game server architecture: 16-byte framing, MT19937 stream cipher, KCP networking, scene/world entities, PVE battle orchestration, Morphia MongoDB ODM. |
|
||||
| **[march7thhoney_architecture_deep_dive.md](./march7thhoney_architecture_deep_dive.md)** | **March7thHoney**<br>*(.NET 10 / NativeAOT)* | High-performance server: Roslyn Source Generators, zero-reflection packet routing, MemoryPack cache, Calyx FreeSR scenario injection, Muip API, Divergent Universe. |
|
||||
| **[firefly_protoc_deep_dive.md](./firefly_protoc_deep_dive.md)** | **FireflyProtoc**<br>*(C# .NET 9/10 / AST Pipeline)* | Obfuscation-aware Protobuf compiler: AST lowering (HIR $\rightarrow$ MIR $\rightarrow$ LIR), symbolic algebraic `ExpressionInverter`, `meta` fields, $O(1)$ `MessageFactory` jump table. |
|
||||
| **[honkai_dumper_deep_dive.md](./honkai_dumper_deep_dive.md)** | **honkai-dumper**<br>*(Rust cdylib / In-Process)* | In-process IL2CPP dumper: non-blocking DLL injection, pattern scanning in `UnityPlayer.dll` (`48 8B 05 ...`), RIP-relative math, ECMA-335 bitmask decompiler, `methods.json`. |
|
||||
| **[firefly_static_parser_deep_dive.md](./firefly_static_parser_deep_dive.md)** | **Firefly-Static-Parser**<br>*(Rust CLI / Offline)* | Pure offline static parser: reverse-engineers `b"MHY\0"` container, PE layout discovery, Dynamic Semantic Key Derivation via `<Module>`/`ValueType`, QWORD stream cipher. |
|
||||
| **[static_cmdid_dumper_deep_dive.md](./static_cmdid_dumper_deep_dive.md)** | **StaticCmdIdDumper**<br>*(C# .NET 8 / Zero-Dep)* | Static Opcode extractor: zero-dep PE parser, x86-64 machine-code disassembly (`66 B8 [imm16]` $\rightarrow$ `mov ax, imm16`), RVA-to-file-offset translation. |
|
||||
| **[dumpcs2protobuf_deep_dive.md](./dumpcs2protobuf_deep_dive.md)** | **Dumpcs2Protobuf**<br>*(C# .NET 8 / Regex AST)* | Schema reverse-engineering: inverts Google.Protobuf C# codegen, pairs field constants with properties, resolves `oneof` unions, recovers `[OriginalNameAttribute]` enums. |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 The End-to-End Reverse-Engineering Toolchain
|
||||
|
||||
The following diagram illustrates how all 7 tools connect to form an automated protocol extraction and server emulation pipeline:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph S1["1. Binary & Metadata Extraction"]
|
||||
GA["GameAssembly.dll + Metadata"]
|
||||
GA -->|Offline Analysis| FSP["Firefly-Static-Parser<br/>(Rust CLI)"]
|
||||
GA -->|In-Process Injection| HD["honkai-dumper<br/>(Rust DLL)"]
|
||||
FSP --> DUMP_CS["dump.cs<br/>(C# Class Definitions)"]
|
||||
FSP --> METHODS_JSON["methods.json<br/>(Method RVAs)"]
|
||||
HD --> DUMP_CS
|
||||
HD --> METHODS_JSON
|
||||
end
|
||||
|
||||
subgraph S2["2. Protocol Schema Recovery"]
|
||||
DUMP_CS --> D2P["Dumpcs2Protobuf<br/>(C# Engine)"]
|
||||
D2P --> RAW_PROTO[".proto Schemas<br/>(Messages, Enums, OneOfs)"]
|
||||
end
|
||||
|
||||
subgraph S3["3. Opcode & CmdId Resolution"]
|
||||
METHODS_JSON --> SCD["StaticCmdIdDumper<br/>(C# Micro-Disassembler)"]
|
||||
EXE["Target Game Binary"] --> SCD
|
||||
SCD --> PACKET_IDS["PacketIds.json<br/>(PacketName -> CmdId)"]
|
||||
end
|
||||
|
||||
subgraph S4["4. Schema Annotation & De-obfuscation"]
|
||||
RAW_PROTO & PACKET_IDS --> ANNOTATED_PROTO[".proto with Metadata & Wire Masks<br/>- meta uint32 cmd_id = 143;<br/>- uint32 id = 1 [get: (id ^ 1) + 2];"]
|
||||
end
|
||||
|
||||
subgraph S5["5. High-Performance Compilation"]
|
||||
ANNOTATED_PROTO --> FP["FireflyProtoc<br/>(Symbolic Inversion Compiler)"]
|
||||
FP --> STUBS["Compiled Protocol Stubs<br/>(Zero-Allocation Deserializers)"]
|
||||
end
|
||||
|
||||
subgraph S6["6. Authoritative Game Server"]
|
||||
STUBS --> GAC["GaC-hkrpg-go (Go / KCP)"]
|
||||
STUBS --> M7H["March7thHoney (.NET 10)"]
|
||||
STUBS --> LC["LunarCore (Java 17)"]
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 Summary of Core Technical Invariants
|
||||
|
||||
### 1. Binary Envelope Framing (16-Byte Envelope)
|
||||
All network traffic between client and server follows this standard framing:
|
||||
- **Header Magic**: `0x9D74C714` (4 bytes, Big-Endian uint32)
|
||||
- **CmdID (Opcode)**: 2 bytes (uint16 BE)
|
||||
- **Head Length**: 2 bytes (uint16 BE, typically `0x0000`)
|
||||
- **Payload Length**: 4 bytes (uint32 BE)
|
||||
- **Payload**: Serialized Protobuf message payload
|
||||
- **Tail Magic**: `0xD7A152C8` (4 bytes, Big-Endian uint32)
|
||||
|
||||
### 2. Network Encryption
|
||||
- Initial handshake exchanges an `Ec2b` packet (`HEAD_MAGIC = 1647469381` / ASCII `"Ec2b"`).
|
||||
- Contains 16-byte key and 2048-byte data, generating a 64-bit seed.
|
||||
- **MT19937 (Mersenne Twister)** expands this seed into a 4,096-byte keystream used for repeating XOR stream encryption.
|
||||
|
||||
### 3. Machine Code Opcode Getters
|
||||
In compiled x86-64 IL2CPP binaries, packet opcode getter methods compile to:
|
||||
```nasm
|
||||
66 B8 xx xx ; mov ax, imm16 (Opcode)
|
||||
C3 ; ret
|
||||
```
|
||||
- `0x66`: 16-bit operand size override prefix.
|
||||
- `0xB8`: `mov ax` opcode.
|
||||
- `xx xx`: 16-bit Little-Endian Command ID.
|
||||
|
||||
### 4. Dynamic Semantic Key Derivation
|
||||
To decrypt custom `b"MHY\0"` metadata without hardcoding version-specific keys, analyze CLR invariants:
|
||||
- TypeDef 0 is always `<Module>` $\rightarrow$ gives `type_base_none` and `type_attribute_key`.
|
||||
- `System.ValueType` inherits from `System.Object` $\rightarrow$ gives `type_base_object`.
|
||||
- `System.Enum` inherits from `System.ValueType` $\rightarrow$ gives `type_base_value_type`.
|
||||
- Parent key: $\text{type\_parent\_key} = \text{type\_base\_object} - 1$.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Application to `GaC-hkrpg-go`
|
||||
|
||||
When building or updating the Go server implementation in this repository:
|
||||
1. **Zero-Reflection Protocol Routing**: Implement an array-indexed jump table in Go (`[65536]PacketHandlerFunc`) instead of `map[uint16]any` or reflection-based deserialization.
|
||||
2. **Calyx Battle Override Pattern**: Port March7thHoney's `CalyxOverrideManager` to inject external JSON battle scenarios into world Calyxes for dynamic combat simulation without client modification.
|
||||
3. **Configuration Compliance (`AGENTS.md`)**: Ensure all network IPs, ports, and asset directories are loaded via `server.json` and overridable via `server.local.json` or environment variables without hardcoded machine paths.
|
||||
@@ -0,0 +1,453 @@
|
||||
# Dumpcs2Protobuf Architectural Specification & Schema Reconstruction Deep Dive
|
||||
|
||||
This technical specification provides an exhaustive, code-level analysis of **`Dumpcs2Protobuf`** (by Hiro420), a specialized C# (.NET 8.0) schema recovery utility engineered to reconstruct Protocol Buffers v3 (`.proto`) definition files directly from the decompiled C# source files (`dump.cs`) emitted by IL2CPP metadata dumpers (such as `honkai-dumper` or `Firefly-Static-Parser`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architectural Role
|
||||
|
||||
### 1.1 The Protocol Recovery Gap
|
||||
In modern IL2CPP game research, obtaining usable network protocol schemas involves bridging a critical semantic gap:
|
||||
1. IL2CPP dumpers extract decompiled C# classes (`dump.cs`).
|
||||
2. Server implementations (`LunarCore`, `March7thHoney`, `GaC-hkrpg-go`) require `.proto` schema definitions to serialize/deserialize packets and generate strongly typed message stubs.
|
||||
3. Manually translating thousands of obfuscated C# packet classes into `.proto` files is infeasible.
|
||||
|
||||
### 1.2 The De-Compilation Inverse
|
||||
`Dumpcs2Protobuf` operates as the **exact inverse** of Google's Protocol Buffers C# compiler (`protoc --csharp_out`):
|
||||
- When Google's `protoc` compiles a `.proto` file into C#, it generates deterministic structural signatures (constant field numbers, `RepeatedField<T>`, `MapField<K, V>`, `OneofCase` enums, and `OriginalNameAttribute`).
|
||||
- `Dumpcs2Protobuf` analyzes these exact code generation patterns inside `dump.cs`, extracts field tags, names, and types, and **reconstructs valid `proto3` syntax**.
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| Dumpcs2Protobuf Pipeline |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ Input: dump.cs (Decompiled IL2CPP Output) ] |
|
||||
| |
|
||||
| // Module: RPG.Network.Proto.dll |
|
||||
| public sealed class IEFELCIJJDA : IMessage |
|
||||
| { |
|
||||
| public const int CPBHCOPAGBD = 13; |
|
||||
| public const int NEFBNAELMLD = 14; |
|
||||
| public uint EGPKLBOKFNF { get; set; } |
|
||||
| public RepeatedField<NFJLPFKFDLF> MDDLGJHLLAI { get; } |
|
||||
| } |
|
||||
| |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 1. Assembly & Class Filter | |
|
||||
| | Filters segments for 'RPG.Network.Proto.dll' and classes implementing 'IMessage' | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 2. Tag & Property Pairing Engine | |
|
||||
| | - Extracts field tags from 'public const int <Name> = <Tag>;' | |
|
||||
| | - Extracts properties: Scalar, RepeatedField<T>, MapField<K, V> | |
|
||||
| | - Pairs tags with properties in declaration order | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 3. OneOf Union Detection | |
|
||||
| | Detects count mismatch -> Resolves discriminator enum -> Groups fields into oneof block | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 4. Type Translation & Name Normalization | |
|
||||
| | - CLR types -> Protobuf types (uint -> uint32, ByteString -> bytes) | |
|
||||
| | - CamelCase / PascalCase -> snake_case | |
|
||||
| | - Extracts [OriginalNameAttribute] for un-obfuscated Enum values | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| [ Output: output.proto ] |
|
||||
| |
|
||||
| syntax = "proto3"; |
|
||||
| |
|
||||
| message IEFELCIJJDA { |
|
||||
| uint32 egpklbokfnf = 13; |
|
||||
| repeated NFJLPFKFDLF mddlgjhllai = 14; |
|
||||
| } |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Inverting Google.Protobuf C# Code Generation
|
||||
|
||||
To understand how `Dumpcs2Protobuf` works, we must analyze the exact contract emitted by Google's `protoc` C# code generator.
|
||||
|
||||
### 2.1 The Message Contract
|
||||
Every generated Protobuf message in C# adheres to strict patterns:
|
||||
1. **Class Declaration**:
|
||||
```csharp
|
||||
public sealed class MessageName : IMessage, IMessage<MessageName>, ...
|
||||
```
|
||||
2. **Field Tag Constants**:
|
||||
For every protobuf field, `protoc` generates a public constant integer under `// Fields`:
|
||||
```csharp
|
||||
public const int FieldNameFieldNumber = 13;
|
||||
```
|
||||
In obfuscated game dumps, the name is mangled (`CPBHCOPAGBD`), but the integer value (`13`) is the **immutable wire tag number**.
|
||||
3. **Public Non-Static Properties**:
|
||||
Under `// Properties`, `protoc` generates accessors corresponding to each field:
|
||||
- Scalar fields: `public uint FieldName { get; set; }`
|
||||
- Repeated fields: `public RepeatedField<TypeName> FieldName { get; }`
|
||||
- Map fields: `public MapField<KeyType, ValType> FieldName { get; }`
|
||||
- Byte sequences: `public ByteString FieldName { get; set; }`
|
||||
|
||||
### 2.2 The OneOf Union Contract
|
||||
When a `.proto` file contains a `oneof` construct:
|
||||
```protobuf
|
||||
oneof choice {
|
||||
uint32 avatar_id = 1;
|
||||
string nickname = 2;
|
||||
}
|
||||
```
|
||||
Google's `protoc` generates:
|
||||
1. Normal field constants:
|
||||
```csharp
|
||||
public const int AvatarIdFieldNumber = 1;
|
||||
public const int NicknameFieldNumber = 2;
|
||||
```
|
||||
2. Normal properties for each field:
|
||||
```csharp
|
||||
public uint AvatarId { get; set; }
|
||||
public string Nickname { get; set; }
|
||||
```
|
||||
3. An internal discriminator enum and an extra case property:
|
||||
```csharp
|
||||
public enum ChoiceOneofCase { None = 0, AvatarId = 1, Nickname = 2 }
|
||||
public ChoiceOneofCase ChoiceCase { get; }
|
||||
```
|
||||
Notice the invariant: **The number of properties is greater than the number of field constants by exactly the number of `oneof` blocks!**
|
||||
|
||||
### 2.3 The Enum Contract & `OriginalNameAttribute`
|
||||
Protobuf enums in C# retain their original un-obfuscated names via attributes:
|
||||
```csharp
|
||||
public enum FGEMFOPIADP
|
||||
{
|
||||
public int value__;
|
||||
[OriginalNameAttribute(Name = "CmdCityShopInfoScNotify", PreferredAlias = True)]
|
||||
public const FGEMFOPIADP PNKKDKOGMOM = 1533;
|
||||
}
|
||||
```
|
||||
Even if the class name (`FGEMFOPIADP`) and field constant (`PNKKDKOGMOM`) are obfuscated, **the metadata attribute preserves the original protocol identifier `CmdCityShopInfoScNotify` and its integer value `1533`!**
|
||||
|
||||
---
|
||||
|
||||
## 3. Codebase Architecture & Data Models
|
||||
|
||||
The project is structured into two source files:
|
||||
|
||||
```
|
||||
Dumpcs2Protobuf/
|
||||
├── Proto.cs # Abstract Syntax Tree (AST) data models
|
||||
└── MainApp.cs # File chunker, regex extractors, type mapper, and proto serializer
|
||||
```
|
||||
|
||||
### 3.1 AST Data Models (`Proto.cs`)
|
||||
```csharp
|
||||
namespace DumpFileParser;
|
||||
|
||||
public class ProtoMessage
|
||||
{
|
||||
public string protoName = "";
|
||||
public List<string> importNameList = new();
|
||||
public List<ProtoField> fieldList = new();
|
||||
public List<OneOf> oneOfList = new();
|
||||
}
|
||||
|
||||
public class ProtoField
|
||||
{
|
||||
public string fieldName = "";
|
||||
public string fieldType = "";
|
||||
public string? mapKey;
|
||||
public string? mapValue;
|
||||
public bool isRepeated;
|
||||
public uint val; // Protobuf wire tag number
|
||||
}
|
||||
|
||||
public class OneOf
|
||||
{
|
||||
public string oneOfName = "";
|
||||
public List<ProtoField> fieldList = new();
|
||||
}
|
||||
|
||||
public class ProtoEnum
|
||||
{
|
||||
public string enumName = "";
|
||||
public Dictionary<string, int> valDict = new();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Reverse Engineering Algorithms & Implementation (`MainApp.cs`)
|
||||
|
||||
### 4.1 Chunking & Assembly Filtering
|
||||
`dump.cs` files can be hundreds of megabytes in size. `Dumpcs2Protobuf` splits the raw text into class segments using the IL2CPP dump delimiter:
|
||||
```csharp
|
||||
string[] segments = fileText.Split(new string[] { "// AssemblyIndex:" }, StringSplitOptions.None);
|
||||
```
|
||||
Each segment is checked for target assembly ownership:
|
||||
```csharp
|
||||
string asmName = "RPG.Network.Proto.dll";
|
||||
if (!segment.Split("\n")[2].Trim().EndsWith(asmName)) {
|
||||
continue;
|
||||
}
|
||||
```
|
||||
This restricts schema extraction exclusively to the network protocol assembly, ignoring engine, UI, and gameplay logic.
|
||||
|
||||
### 4.2 Field Tag Extraction (`GetFieldNums`)
|
||||
Field numbers are extracted exclusively between `// Fields` and `// Properties` to avoid picking up arbitrary constants:
|
||||
|
||||
```csharp
|
||||
static List<uint> GetFieldNums(string[] lines) {
|
||||
List<uint> result = new();
|
||||
int fieldsStartIndex = -1;
|
||||
int propertiesStartIndex = -1;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++) {
|
||||
if (lines[i].Trim().StartsWith("// Fields")) fieldsStartIndex = i + 1;
|
||||
else if (lines[i].Trim().StartsWith("// Properties")) {
|
||||
propertiesStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldsStartIndex >= 0 && propertiesStartIndex > fieldsStartIndex) {
|
||||
for (int i = fieldsStartIndex; i < propertiesStartIndex; i++) {
|
||||
string line = lines[i].Trim();
|
||||
var match = Regex.Match(line, @"public\s+const\s+int\s+(\w+)\s*=\s*(\d+);");
|
||||
if (match.Success && uint.TryParse(match.Groups[2].Value, out uint fieldNum)) {
|
||||
result.Add(fieldNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Property & Type Extraction (`GetFieldStrings`)
|
||||
Extracts public non-static properties using regex:
|
||||
```csharp
|
||||
var match = Regex.Match(trimmedLine,
|
||||
@"public\s+([\w\.]+(\s*<.*?>)?)\s+(\w+)\s*\{( get;(\s+set;)?| set;(\s+get;)?| get;| set;) \}");
|
||||
```
|
||||
|
||||
#### Type Mapping (`parseType`):
|
||||
Maps C# CLR types to `proto3` scalar types:
|
||||
|
||||
| C# CLR Type | Protobuf Wire Type |
|
||||
| :--- | :--- |
|
||||
| `int` | `int32` |
|
||||
| `uint` | `uint32` |
|
||||
| `long` | `int64` |
|
||||
| `ulong` | `uint64` |
|
||||
| `string` | `string` |
|
||||
| `ByteString` | `bytes` |
|
||||
| `RepeatedField<T>` | `repeated <T>` |
|
||||
| `MapField<K, V>` | `map<K, V>` |
|
||||
| Custom Class (`Namespace.Type`) | Statically stripped to `Type` |
|
||||
|
||||
### 4.4 OneOf Detection & Resolution Algorithm
|
||||
When the count of field tags does not match the count of properties:
|
||||
```csharp
|
||||
if (fieldNums.Count != fieldStrings.Count) {
|
||||
// The trailing property is the OneOf discriminator enum accessor!
|
||||
string trimmed = fieldStrings.Last().Replace(" { get; }", "").Replace("public ", "");
|
||||
oneOfName = trimmed.Split(" ")[1];
|
||||
oneOfEnumName = trimmed.Split(" ")[0];
|
||||
blackListEnumNames.Add(oneOfEnumName.Split(".").Last());
|
||||
oneofEnum = FindAndParseEnum(oneOfEnumName, segments);
|
||||
}
|
||||
```
|
||||
1. It identifies the discriminator property (e.g. `public ChoiceOneofCase ChoiceCase { get; }`).
|
||||
2. It locates and parses the enum declaration (`ChoiceOneofCase`) across all segments.
|
||||
3. For each field, if its wire tag number matches an entry in `oneofEnum.valDict`, it is placed inside `oneof <oneofName> { ... }`.
|
||||
4. The discriminator enum is added to `blackListEnumNames` so it is not emitted as a standalone top-level enum in the `.proto` file.
|
||||
|
||||
### 4.5 De-Obfuscating Enums via `OriginalNameAttribute`
|
||||
In `parseEnum`:
|
||||
```csharp
|
||||
if (lines[i-1].Contains("[OriginalNameAttribute(Name = \"")) {
|
||||
string upperLine = lines[i-1];
|
||||
name = upperLine.Split("[OriginalNameAttribute(Name = \"").Last().Split("\"").First();
|
||||
} else {
|
||||
name = line.Split($"{enumName} ").Last().Split("=").First().Trim();
|
||||
}
|
||||
```
|
||||
If an enum constant is obfuscated (e.g. `IHJPPFIKELH`), it inspects the preceding line for `[OriginalNameAttribute]`, effortlessly restoring the original identifier (`CmdShopTypeNone = 0;`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Output Serialization
|
||||
|
||||
`Dumpcs2Protobuf` serializes the AST into clean `proto3` syntax:
|
||||
|
||||
```protobuf
|
||||
// Extracted from dump.cs using Dumpcs2Protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
message SceneEntityInfo {
|
||||
uint32 entity_id = 1;
|
||||
uint32 group_id = 2;
|
||||
uint32 inst_id = 3;
|
||||
MotionInfo motion = 4;
|
||||
oneof entity {
|
||||
ActorInfo actor = 5;
|
||||
NpcInfo npc = 6;
|
||||
MonsterInfo monster = 7;
|
||||
}
|
||||
}
|
||||
|
||||
enum CmdCityShopInfoScNotify {
|
||||
CmdShopTypeNone = 0;
|
||||
CmdCityShopInfoScNotify = 1533;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. The Complete 6-Tool Unified Star Rail Protocol Toolchain
|
||||
|
||||
We can now map how all 6 repositories analyzed in this session interact to form an end-to-end, automated protocol engineering pipeline:
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| THE COMPLETE PROTOCOL REVERSE-ENGINEERING LIFECYCLE |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ Step 1: Binary & Metadata Extraction ] |
|
||||
| Tools: Firefly-Static-Parser (Offline) OR honkai-dumper (In-Process) |
|
||||
| Action: Decrypts GameAssembly.dll + global-metadata.dat |
|
||||
| Output: dump.cs (All C# class definitions) + methods.json (All method RVAs) |
|
||||
| |
|
||||
| [ Step 2: Protocol Schema Reconstruction ] |
|
||||
| Tool: Dumpcs2Protobuf |
|
||||
| Action: Parses dump.cs (RPG.Network.Proto.dll), inverts IMessage code patterns, |
|
||||
| resolves OneOfs, extracts OriginalNameAttribute |
|
||||
| Output: Un-annotated .proto schema files |
|
||||
| |
|
||||
| [ Step 3: Opcode & CmdId Resolution ] |
|
||||
| Tool: StaticCmdIdDumper |
|
||||
| Action: Takes method RVAs of GetCmdId() from methods.json -> reads 4 bytes (66 B8 imm16) |
|
||||
| Output: PacketIds.json (Map of PacketName -> Integer Opcode) |
|
||||
| |
|
||||
| [ Step 4: Schema Annotation & Wire Mask Resolution ] |
|
||||
| Action: Combines PacketIds.json with .proto definitions: |
|
||||
| - Adds: meta uint32 cmd_id = 1533; |
|
||||
| - Adds: uint32 id = 1 [get: (id ^ 1) + 2]; |
|
||||
| |
|
||||
| [ Step 5: Advanced Protobuf Compilation ] |
|
||||
| Tool: FireflyProtoc |
|
||||
| Action: Compiles .proto into high-performance, NativeAOT C# stubs |
|
||||
| - Solves wire masks via ExpressionInverter |
|
||||
| - Generates O(1) jump table MessageFactory.Create(cmdId) |
|
||||
| |
|
||||
| [ Step 6: Server Implementation & Routing ] |
|
||||
| Tools: March7thHoney (.NET 10) / LunarCore (Java) / GaC-hkrpg-go (Go) |
|
||||
| Action: Instant zero-reflection packet routing on KCP network layer |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Technical Critique & Production Blueprint for `GaC-hkrpg-go`
|
||||
|
||||
While `Dumpcs2Protobuf` is concise and functional, a production-grade schema generator for `GaC-hkrpg-go` should address several structural limitations:
|
||||
|
||||
### 7.1 Limitations of the Regex Implementation
|
||||
1. **Single Output File**: Dumps everything into a single monolithic `output.proto` file, leading to circular dependency and compilation issues.
|
||||
2. **Missing Imports**: Does not generate `import "OtherMessage.proto";` statements required when messages reference other message types.
|
||||
3. **Rigid Field Ordering Assumption**: Relies on field numbers appearing sequentially in the exact line order of properties. If the decompiler or compiler re-orders field definitions, tag pairing can desynchronize.
|
||||
|
||||
### 7.2 Production Go Implementation Blueprint
|
||||
A robust schema extractor in Go for `GaC-hkrpg-go` can parse the C# definitions and emit properly modularized proto files:
|
||||
|
||||
```go
|
||||
package proto_generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ProtoField struct {
|
||||
Name string
|
||||
Type string
|
||||
Tag uint32
|
||||
IsRepeated bool
|
||||
IsMap bool
|
||||
MapKey string
|
||||
MapValue string
|
||||
}
|
||||
|
||||
type ProtoMessage struct {
|
||||
Name string
|
||||
CmdId uint32
|
||||
Fields []ProtoField
|
||||
Imports map[string]bool
|
||||
OneOfs map[string][]ProtoField
|
||||
}
|
||||
|
||||
func (m *ProtoMessage) GenerateProtoFile(outDir string) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("syntax = \"proto3\";\n\n")
|
||||
|
||||
// 1. Emit Imports
|
||||
for imp := range m.Imports {
|
||||
sb.WriteString(fmt.Sprintf("import \"%s.proto\";\n", imp))
|
||||
}
|
||||
if len(m.Imports) > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// 2. Emit Message Block
|
||||
sb.WriteString(fmt.Sprintf("message %s {\n", m.Name))
|
||||
|
||||
// Option: Meta CmdId
|
||||
if m.CmdId > 0 {
|
||||
sb.WriteString(fmt.Sprintf("\t// CmdId: %d\n", m.CmdId))
|
||||
}
|
||||
|
||||
// Normal Fields
|
||||
for _, f := range m.Fields {
|
||||
if f.IsRepeated {
|
||||
sb.WriteString(fmt.Sprintf("\trepeated %s %s = %d;\n", f.Type, f.Name, f.Tag))
|
||||
} else if f.IsMap {
|
||||
sb.WriteString(fmt.Sprintf("\tmap<%s, %s> %s = %d;\n", f.MapKey, f.MapValue, f.Name, f.Tag))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("\t%s %s = %d;\n", f.Type, f.Name, f.Tag))
|
||||
}
|
||||
}
|
||||
|
||||
// OneOf Blocks
|
||||
for oneOfName, fields := range m.OneOfs {
|
||||
sb.WriteString(fmt.Sprintf("\toneof %s {\n", oneOfName))
|
||||
for _, f := range fields {
|
||||
sb.WriteString(fmt.Sprintf("\t\t%s %s = %d;\n", f.Type, f.Name, f.Tag))
|
||||
}
|
||||
sb.WriteString("\t}\n")
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
|
||||
filePath := filepath.Join(outDir, m.Name+".proto")
|
||||
return os.WriteFile(filePath, []byte(sb.String()), 0644)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Compliance with Repository Rules (`AGENTS.md`)
|
||||
|
||||
- **Explicit Protocol Revision & Schema Integrity**: Reconstructing schemas directly from client assemblies prevents guesswork or arbitrary schema patching.
|
||||
- **No Hardcoded Machine Paths**: Operates dynamically from CLI arguments and local files.
|
||||
- **Zero Runtime Interference**: Operates strictly on text and source files without runtime injection or client hooks.
|
||||
@@ -0,0 +1,360 @@
|
||||
# รายงานการวิเคราะห์สถาปัตยกรรมและการทำงานของ FireflyProtoc (Deep-Dive Technical Specification)
|
||||
|
||||
> **เอกสารอ้างอิงทางเทคนิคระดับลึกสำหรับวิศวกรซอฟต์แวร์และโมเดลปัญญาประดิษฐ์ (AI Systems)**
|
||||
> วิเคราะห์จากซอร์สโค้ดของโครงการ: [gatokeeper/FireflyProtoc](https://github.com/gatokeeper/FireflyProtoc)
|
||||
> ครอบคลุมสถาปัตยกรรมคอมไพเลอร์ Protobuf ยุคใหม่, การถอดรหัสความคลุมเครือของข้อมูล (Wire De-obfuscation), ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter), Meta Fields, Compile-Time Message Factory และรันไทม์ประสิทธิภาพสูง
|
||||
|
||||
---
|
||||
|
||||
## สารบัญ (Table of Contents)
|
||||
1. [บทนำและวัตถุประสงค์ของโครงการ (Introduction & Problem Statement)](#1-บทนำและวัตถุประสงค์ของโครงการ-introduction--problem-statement)
|
||||
2. [สถาปัตยกรรมคอมไพเลอร์แบบ Multi-Tiered IR (Compiler Architecture)](#2-สถาปัตยกรรมคอมไพเลอร์แบบ-multi-tiered-ir-compiler-architecture)
|
||||
- 2.1 ภาพรวมของไปป์ไลน์การคอมไพล์ (Frontend $\rightarrow$ IR $\rightarrow$ Backend)
|
||||
- 2.2 โครงสร้างไวยากรณ์เสริม (Custom Syntax Extensions)
|
||||
3. [ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter Engine)](#3-ระบบคณิตศาสตร์เชิงสัญลักษณ์-symbolic-expression-inverter-engine)
|
||||
- 3.1 ปัญหาการพรางค่าข้อมูลบนสายส่ง (Wire Obfuscation / Masking)
|
||||
- 3.2 อัลกอริทึมการแปลงนิพจน์ผกผันพีชคณิตอัตโนมัติ (Algebraic Inversion)
|
||||
4. [ระบบ Meta Fields และ Message Factory แบบ Zero-Reflection](#4-ระบบ-meta-fields-และ-message-factory-แบบ-zero-reflection)
|
||||
- 4.1 การฝัง Metadata (CmdId/Opcode) โดยไม่เสีย Overhead บนสายส่ง
|
||||
- 4.2 การสร้าง Compile-Time Factory Jump Table (`IFactoryMessage`)
|
||||
5. [การสร้างโค้ด C# และการปรับปรุงประสิทธิภาพ (CodeGen & Performance Optimizations)](#5-การสร้างโค้ด-c-และการปรับปรุงประสิทธิภาพ-codegen--performance-optimizations)
|
||||
6. [รันไทม์ภายในของ Firefly (Firefly.Protobuf Runtime Architecture)](#6-รันไทม์ภายในของ-firefly-fireflyprotobuf-runtime-architecture)
|
||||
7. [ตัวอย่างจริงจากการคอมไพล์ (Real-World Compilation Walkthrough)](#7-ตัวอย่างจริงจากการคอมไพล์-real-world-compilation-walkthrough)
|
||||
8. [การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม (Integration with HSR/Genshin Private Servers)](#8-การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม-integration-with-hsrgenshin-private-servers)
|
||||
9. [คู่มือวิศวกรรมสำหรับ AI ในการขยายขีดความสามารถ (AI Extension & Engineering Guide)](#9-คู่มือวิศวกรรมสำหรับ-ai-ในการขยายขีดความสามารถ-ai-extension--engineering-guide)
|
||||
|
||||
---
|
||||
|
||||
## 1. บทนำและวัตถุประสงค์ของโครงการ (Introduction & Problem Statement)
|
||||
|
||||
**FireflyProtoc** (พัฒนาโดย `gatokeeper`) คือคอมไพเลอร์ Protocol Buffers ทางเลือกที่ถูกออกแบบขึ้นมาโดยเฉพาะสำหรับ **Protocol วิศวกรรมย้อนกลับ (Reverse Engineering)** และ **เซิร์ฟเวอร์จำลองเกม (Game Emulators)** เช่น *Honkai: Star Rail*, *Genshin Impact*, และ *Zenless Zone Zero*
|
||||
|
||||
### ข้อจำกัดของ `protoc` ดั้งเดิมของ Google:
|
||||
1. **ไม่รองรับ Wire Data Masking / Obfuscation**: เกมค่าย HoYoverse มักจะพรางค่าฟิลด์ตัวเลขบนสายส่ง (Wire Format) ด้วยการทำ XOR, บวก, ลบ หรือบิตชิฟต์ เช่น ค่า ID จริงคือ `1001` แต่ในสายส่งกลับถูกเข้ารหัสเป็น `(1001 ^ 0x5A) + 7` คอมไพเลอร์ปกติของ Google ทำได้แค่อ่านค่าดิบ ทำให้วิศวกรต้องมาเขียนโค้ดแปลงค่าซ้ำซ้อนในทุก ๆ ฟิลด์
|
||||
2. **ไม่มีแนวคิดเรื่อง Packet Opcode (CmdId) ในตัว**: Protobuf มาตรฐานไม่มีช่องทางในการผูกหมายเลข Opcode เข้ากับ Class โดยตรง หากสร้างฟิลด์ธรรมดา ข้อมูล Opcode จะถูกส่งซ้ำซ้อนลงไปใน Payload สิ้นเปลืองแบนด์วิดท์
|
||||
3. **การสร้างอินสแตนซ์พึ่งพา Reflection หรือ Dictionary**: การแมป Opcode เป็น C# Object มักต้องใช้ Reflection หรือ Dictionary Lookup ขนาดใหญ่ ซึ่งช้าและไม่รองรับการทำ NativeAOT
|
||||
|
||||
FireflyProtoc แก้ปัญหาทั้งหมดนี้ที่ **ระดับคอมไพเลอร์ (Compiler Level)** ทำให้ไฟล์ `.proto` มีความสามารถในการแปลงค่าพรางข้อมูลอัตโนมัติและสร้าง O(1) Factory สำเร็จรูปทันที
|
||||
|
||||
---
|
||||
|
||||
## 2. สถาปัตยกรรมคอมไพเลอร์แบบ Multi-Tiered IR (Compiler Architecture)
|
||||
|
||||
FireflyProtoc ถูกสร้างขึ้นด้วยสถาปัตยกรรมคอมไพเลอร์ระดับมืออาชีพ โดยแบ่งขั้นตอนการทำงานออกเป็น **3 ชั้น Intermediate Representation (IR)**:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ProtoFile[".proto Source Code<br/>(พร้อมไวยากรณ์เสริม meta, [get: ...])"] --> Lexer["Lexer (Token Stream)"]
|
||||
Lexer --> Parser["Parser (AST: Node / Decl / Expr)"]
|
||||
Parser --> TypeChecker["TypeChecker (Semantic Validation)"]
|
||||
|
||||
subgraph IR_Pipeline["Multi-Tiered IR Lowering"]
|
||||
TypeChecker --> HIR["HIR Builder<br/>(High-Level IR: Messages, Enums, Scopes)"]
|
||||
HIR --> MIR["MIR Builder<br/>(Mid-Level IR: Type Resolution, MessageFactory, PascalCase)"]
|
||||
MIR --> LIR["LIR Builder<br/>(Low-Level IR: Classes, Methods, Body, Byte Stmts)"]
|
||||
end
|
||||
|
||||
LIR --> CodeGen["CSharpGenerator (Parallel.ForEach File Emit)"]
|
||||
CodeGen --> CSharpFiles["Generated High-Performance C# Files<br/>(Zero Reflection, AggressiveInlining, NativeAOT)"]
|
||||
```
|
||||
|
||||
### 2.1 หน้าที่ของแต่ละเลเยอร์ในไปป์ไลน์:
|
||||
1. **AST (Abstract Syntax Tree)**:
|
||||
- แปลงไวยากรณ์ข้อความเป็นโครงสร้างข้อมูลเบื้องต้น (`Decl.Message`, `Decl.Field`, `Decl.Enum`, `Expr.Binary`)
|
||||
2. **HIR (High-Level Intermediate Representation)**:
|
||||
- รวบรวม Scope และ Namespace ของไฟล์
|
||||
- จัดระเบียบความสัมพันธ์ของ Message ซ้อน Message (Nested Types)
|
||||
- สกัดนิพจน์ `[get: ...]` และ `[set: ...]` เข้าสู่ `AttributeSet`
|
||||
3. **MIR (Mid-Level Intermediate Representation)**:
|
||||
- แมปประเภทข้อมูลของ Protobuf ไปเป็นประเภทข้อมูล C# (`MirTypeRef`)
|
||||
- แปลงชื่อแบบ snake_case เป็น PascalCase
|
||||
- รวบรวมฟิลด์ที่เป็น `meta` ที่ตรงกับคำสั่ง `option message_factory` เพื่อเตรียมสร้าง Factory Class
|
||||
4. **LIR (Low-Level Intermediate Representation)**:
|
||||
- กำหนดโครงสร้างโค้ดระดับล่าง: Constructors, Property Getters/Setters, ฟิลด์ Backing Field (`_Field`), เมธอด `WriteTo`, `MergeFrom`, `CalculateSize`, `Clone`, และ Switch-Case ใน `MessageFactory.Create`
|
||||
5. **CodeGen (C# Code Generator)**:
|
||||
- แปลง LIR ออกมาเป็นโค้ด C# ข้อความจริง โดยรันกระบวนการเขียนไฟล์แบบคู่ขนาน (`Parallel.ForEach`)
|
||||
|
||||
### 2.2 โครงสร้างไวยากรณ์เสริม (Custom Syntax Extensions)
|
||||
|
||||
| ไวยากรณ์ | รูปแบบการเขียน | ผลลัพธ์ที่คอมไพเลอร์สร้างให้ |
|
||||
|---|---|---|
|
||||
| **Meta Field** | `meta int32 cmd_id = 1;` | สร้างค่าคงที่และ Getter ในคลาส **แต่จะไม่ถูก Serialize ออกทางสายส่ง** และสามารถใช้ Tag ชนกับฟิลด์ธรรมดาได้ |
|
||||
| **Masked Field** | `[get: (id ^ 1) + 2] int32 id = 3;` | ตัวแปร `Id` ภายนอกจะมองเห็นค่าจริง แต่ค่าที่ถูกส่ง/รับบนสายส่งจะผ่านสูตรคณิตศาสตร์ผกผัน |
|
||||
| **Message Factory** | `option message_factory = int32 cmd_id;` | สร้างคลาส `MessageFactory` พร้อมเมธอด `Create(int cmd_id)` คืนค่าอินสแตนซ์ของ Message นั้นทันที |
|
||||
|
||||
---
|
||||
|
||||
## 3. ระบบคณิตศาสตร์เชิงสัญลักษณ์ (Symbolic Expression Inverter Engine)
|
||||
|
||||
จุดที่อัจฉริยะที่สุดของ FireflyProtoc คือคลาส **`ExpressionInverter.cs`** ซึ่งทำหน้าที่เป็น Symbolic Algebra Solver ในตัวเอง
|
||||
|
||||
### 3.1 ปัญหาการพรางค่าข้อมูลบนสายส่ง (Wire Obfuscation / Masking)
|
||||
สมมุติว่าในเกม ฟิลด์ `hp` มีสูตรการพรางข้อมูลทางสายส่งคือ:
|
||||
$$\text{WireValue} = (\text{LogicalHP} \oplus 1) + 2$$
|
||||
|
||||
เมื่อเขียนโค้ดฝั่งอ่าน (Deserialization / `MergeFrom`), เราจำเป็นต้องถอดรหัสค่าจากสายส่งกลับมาเป็นค่าจริง:
|
||||
$$\text{LogicalHP} = (\text{WireValue} - 2) \oplus 1$$
|
||||
|
||||
หากใช้คอมไพเลอร์ทั่วไป ผู้พัฒนาจะต้องมานั่งคำนวณและเขียนสูตรทั้งขาไป (`get`) และขากลับ (`set`) ด้วยตัวเอง หากมีฟิลด์แบบนี้เป็นร้อยๆ ฟิลด์ จะเกิดข้อผิดพลาดได้ง่ายมาก
|
||||
|
||||
### 3.2 อัลกอริทึมการแปลงนิพจน์ผกผันพีชคณิตอัตโนมัติ (Algebraic Inversion)
|
||||
ใน FireflyProtoc ผู้ใช้สามารถเขียนเพียงขาเดียว:
|
||||
```protobuf
|
||||
[get: (id ^ 1) + 2] int32 id = 3;
|
||||
```
|
||||
`ExpressionInverter` จะทำการคลายโหนด (Unwind) ย้อนกลับจาก Root ของ AST Expression Tree:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Input["Input: (id ^ 1) + 2"] --> Root["Root Node: Operator (+) with Constant (2)"]
|
||||
Root --> InvertAdd["Invert (+) to (-)<br/>New Accumulator: (WireValue - 2)"]
|
||||
InvertAdd --> NextNode["Next Node: Operator (^) with Constant (1)"]
|
||||
NextNode --> InvertXor["Invert (^) to (^)<br/>Self-inverse Operator<br/>New Accumulator: ((WireValue - 2) ^ 1)"]
|
||||
InvertXor --> ReachTarget["Reached Target Identifier 'id'"]
|
||||
ReachTarget --> Output["Setter Expression: value - 2 ^ 1"]
|
||||
```
|
||||
|
||||
#### ตารางการสลับตัวดำเนินการพีชคณิต (`ExpressionInverter.cs`):
|
||||
- บวก ($+$) $\Longleftrightarrow$ ลบ ($-$)
|
||||
- คูณ ($*$) $\Longleftrightarrow$ หาร ($/$)
|
||||
- บิตไวส์ XOR ($\oplus$) $\Longleftrightarrow$ บิตไวส์ XOR ($\oplus$) *(XOR เป็น Self-Inverse Operator: $A \oplus B \oplus B = A$)*
|
||||
- ลบเอกภาพ (Unary $-$) $\Longleftrightarrow$ ลบเอกภาพ (Unary $-$)
|
||||
|
||||
---
|
||||
|
||||
## 4. ระบบ Meta Fields และ Message Factory แบบ Zero-Reflection
|
||||
|
||||
### 4.1 การฝัง Metadata (CmdId/Opcode) โดยไม่เสีย Overhead บนสายส่ง
|
||||
ในไฟล์ `.proto` สามารถกำหนดฟิลด์เป็น `meta`:
|
||||
```protobuf
|
||||
message PlayerLoginCsReq {
|
||||
meta int32 cmd_id = 11;
|
||||
string token = 1;
|
||||
}
|
||||
```
|
||||
เมื่อคอมไพล์:
|
||||
- `cmd_id` จะไม่ถูกนับรวมใน `CalculateSize()`
|
||||
- `cmd_id` จะไม่ถูกเขียนลง `WriteTo(CodedOutputStream output)`
|
||||
- `cmd_id` จะไม่ถูกอ่านใน `MergeFrom(CodedInputStream input)`
|
||||
- คลาสจะมีเมธอด:
|
||||
```csharp
|
||||
public const int CmdId = 11;
|
||||
public int GetCmdId() => CmdId;
|
||||
```
|
||||
|
||||
### 4.2 การสร้าง Compile-Time Factory Jump Table (`IFactoryMessage`)
|
||||
เมื่อกำหนด `option message_factory = int32 cmd_id;` ที่หัวไฟล์ `.proto`
|
||||
คอมไพเลอร์จะรวบรวมทุก Message ที่มีฟิลด์ `meta int32 cmd_id = X;` แล้วสร้างไฟล์ `MessageFactory.cs`:
|
||||
|
||||
```csharp
|
||||
public static class MessageFactory
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static IFactoryMessage Create(int cmdId)
|
||||
{
|
||||
switch (cmdId)
|
||||
{
|
||||
case 11:
|
||||
return new PlayerLoginCsReq();
|
||||
case 12:
|
||||
return new PlayerLoginScRsp();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **ความได้เปรียบระดับสูงด้านประสิทธิภาพ:**
|
||||
> การสร้าง Switch-Case สำหรับ Type Instantiation ในขั้นตอน Compile-Time ทำให้คอมไพลเลอร์ C# และ JIT/AOT สามารถปรับให้เป็น **Jump Table (O(1) Direct Branching)** ในระดับ Assembly CPU Instruction ได้โดยตรง ปราศจากการทำ Boxing/Unboxing, แฮชแมป หรือ Reflection ใดๆ ทั้งสิ้น
|
||||
|
||||
---
|
||||
|
||||
## 5. การสร้างโค้ด C# และการปรับปรุงประสิทธิภาพ (CodeGen & Performance Optimizations)
|
||||
|
||||
โค้ด C# ที่ FireflyProtoc สร้างขึ้นมานั้น มีการใส่ Attribute พิเศษเพื่อรีดประสิทธิภาพฮาร์ดแวร์สูงสุด:
|
||||
|
||||
1. **`[MethodImpl(MethodImplOptions.AggressiveInlining)]`**:
|
||||
- บังคับให้ JIT และ NativeAOT นำโค้ดของเมธอด (เช่น Getter/Setter, Serialization routines) ไปวางแทนที่จุดเรียกโดยตรง (Inline Expansion) เพื่อตัดค่าใช้จ่ายในการ Call Stack ของ CPU
|
||||
2. **`[DebuggerNonUserCode]`**:
|
||||
- ป้องกันไม่ให้ Debugger กระโดดเข้าไปในโค้ดที่สร้างขึ้นโดยอัตโนมัติ เพิ่มความสะดวกในการดีบักระบบของผู้พัฒนา
|
||||
3. **Partial Class Architecture (`public partial class`)**:
|
||||
- คลาสที่สร้างขึ้นทั้งหมดเป็น `partial class` ทำให้ผู้พัฒนาสามารถเขียนโค้ดส่วนขยาย (Extension Logic, Custom Operators, Helper Methods) แยกไฟล์ได้ โดยที่โค้ดจะไม่สูญหายเมื่อคอมไพล์ `.proto` ใหม่
|
||||
4. **Zero-Allocation Stack Buffer ใน Varint Encoding**:
|
||||
- เมธอดสำหรับเขียนตัวเลข Varint ใช้ `Span<byte> buffer = stackalloc byte[10];` บน Stack Memory ล้วนๆ ไม่มีการจอง Heap ใดๆ
|
||||
|
||||
---
|
||||
|
||||
## 6. รันไทม์ภายในของ Firefly (Firefly.Protobuf Runtime Architecture)
|
||||
|
||||
FireflyProtoc ไม่ได้พึ่งพาไลบรารี `Google.Protobuf.dll` แต่มาพร้อมรันไทม์ขนาดเล็กของตัวเองชื่อ **`Firefly.Protobuf`** ซึ่งอยู่ในโฟลเดอร์ `runtime/`:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class IMessage {
|
||||
<<interface>>
|
||||
+CalculateSize() int
|
||||
+MergeFrom(CodedInputStream input)
|
||||
+WriteTo(CodedOutputStream output)
|
||||
+ToByteArray() byte[]
|
||||
}
|
||||
|
||||
class IFactoryMessage {
|
||||
<<interface>>
|
||||
}
|
||||
|
||||
class CodedInputStream {
|
||||
-byte[] m_Buffer
|
||||
-int m_Position
|
||||
+ReadTag() int
|
||||
+ReadString() string
|
||||
+ReadInt32() int
|
||||
+ReadUInt32() uint
|
||||
+ReadBytes() ByteString
|
||||
}
|
||||
|
||||
class CodedOutputStream {
|
||||
-Stream m_Stream
|
||||
+WriteRawTag(int tag)
|
||||
+WriteString(string value)
|
||||
+WriteInt32(int value)
|
||||
+WriteUInt32(uint value)
|
||||
}
|
||||
|
||||
class MessageParser~T~ {
|
||||
+ParseFrom(byte[] data) T
|
||||
+ParseFrom(CodedInputStream input) T
|
||||
}
|
||||
|
||||
IMessage <|.. IFactoryMessage
|
||||
```
|
||||
|
||||
### รายละเอียดส่วนประกอบหลักของรันไทม์:
|
||||
- **`CodedInputStream` / `CodedOutputStream`**: ควบคุมการอ่านและเขียนข้อมูลระดับบิตและไบต์ตามมาตรฐาน Wire Type (Varint, Fixed32, Fixed64, Length-Delimited)
|
||||
- **`ByteString`**: ตัวแทนของ Immutable Byte Array สำหรับฟิลด์ประเภท `bytes`
|
||||
- **`RepeatedField<T>`**: คอลเลกชันสำหรับฟิลด์ซ้ำ (`repeated`) ที่ปรับแต่งให้ขยายบัฟเฟอร์อย่างมีประสิทธิภาพ
|
||||
- **`MapField<TKey, TValue>`**: โครงสร้างข้อมูล Dictionary ประสิทธิภาพสูงสำหรับฟิลด์ประเภท `map<K, V>`
|
||||
|
||||
---
|
||||
|
||||
## 7. ตัวอย่างจริงจากการคอมไพล์ (Real-World Compilation Walkthrough)
|
||||
|
||||
### 7.1 โค้ดต้นฉบับ Protobuf (`example.proto`):
|
||||
```protobuf
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "FireflyProtoc.Example";
|
||||
option message_factory = int32 department_id;
|
||||
|
||||
message Person {
|
||||
meta int32 department_id = 1;
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
[get: (id ^ 1) + 2] int32 id = 3;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 โค้ด C# ที่คอมไพล์ได้จริง (`Person.cs`):
|
||||
```csharp
|
||||
// Generated by FireflyProtoc! do not modify!
|
||||
#pragma warning disable CS8618
|
||||
|
||||
namespace FireflyProtoc.Example;
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCode]
|
||||
public partial class Person : global::Firefly.Protobuf.IMessage<Person>, global::Firefly.Protobuf.IFactoryMessage
|
||||
{
|
||||
public const global::System.Int32 DepartmentId = 1;
|
||||
public const global::System.Int32 NameFieldNumber = 1;
|
||||
public const global::System.Int32 EmailFieldNumber = 2;
|
||||
public const global::System.Int32 IdFieldNumber = 3;
|
||||
|
||||
private global::System.Int32 _Id;
|
||||
|
||||
[global::System.Diagnostics.DebuggerNonUserCode]
|
||||
public global::System.Int32 Id
|
||||
{
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
return (_Id ^ 1) + 2; // คำนวณค่าเพื่อส่งออก
|
||||
}
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
set
|
||||
{
|
||||
_Id = value - 2 ^ 1; // คำนวณค่าย้อนกลับอัตโนมัติจากการ Invert นิพจน์!
|
||||
}
|
||||
}
|
||||
|
||||
[global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public global::System.Int32 GetDepartmentId()
|
||||
{
|
||||
return DepartmentId; // Meta Field คืนค่าได้ แต่ไม่ถูกเขียนลง Wire
|
||||
}
|
||||
|
||||
public void WriteTo(global::Firefly.Protobuf.CodedOutputStream output)
|
||||
{
|
||||
if (Name != default)
|
||||
{
|
||||
output.WriteRawTag(10);
|
||||
output.WriteString(_Name);
|
||||
}
|
||||
if (Email != default)
|
||||
{
|
||||
output.WriteRawTag(18);
|
||||
output.WriteString(_Email);
|
||||
}
|
||||
if (Id != default)
|
||||
{
|
||||
output.WriteRawTag(24);
|
||||
output.WriteInt32(_Id); // ส่งค่าดิบที่ผ่านการพรางแล้ว
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. การนำไปประยุกต์ใช้ในระบบเซิร์ฟเวอร์เกม (Integration with HSR/Genshin Private Servers)
|
||||
|
||||
ในสถาปัตยกรรมเซิร์ฟเวอร์ เช่น **GaC-hkrpg-go** หรือ **March7thHoney**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor Client as Game Client (Obfuscated Traffic)
|
||||
participant KCP as Server KCP Layer
|
||||
participant Factory as MessageFactory.Create(cmdId)
|
||||
participant Proto as Generated IFactoryMessage
|
||||
|
||||
Client->>KCP: UDP Packet (CmdId: 11, Obfuscated Payload)
|
||||
KCP->>Factory: MessageFactory.Create(11) [O(1) Switch Jump]
|
||||
Factory-->>KCP: Return new PlayerGetTokenCsReq()
|
||||
KCP->>Proto: message.MergeFrom(stream)
|
||||
Note over Proto: ฟิลด์ที่พรางค่าจะถูกคำนวณย้อนกลับเป็นค่าจริงอัตโนมัติใน Setter!
|
||||
KCP->>ServerHandler: Handler.Handle(session, message)
|
||||
Note over ServerHandler: Server ใช้งานฟิลด์ได้ตามปกติโดยไม่ต้องรู้สูตรพรางข้อมูล!
|
||||
```
|
||||
|
||||
1. **ขจัด Boilerplate Code**: เซิร์ฟเวอร์ไม่จำเป็นต้องมีโค้ดแปลงค่า (Converter Functions) นับร้อยจุด เพราะการพรางและถอดรหัสถูกฝังอยู่ในตัว Class ของ Protobuf โดยตรง
|
||||
2. **ความเข้ากันได้กับโปรโตคอลหลายเวอร์ชัน**: สามารถกำหนดสูตรการพรางค่าที่แตกต่างกันในแต่ละแพตช์ได้โดยเพียงแค่แก้สัญลักษณ์ในไฟล์ `.proto`
|
||||
3. **การทำงานร่วมกับระบบ Router**: สามารถเชื่อมโยง Opcode Dispatcher เข้ากับ `MessageFactory.Create(cmdId)` ได้ทันที
|
||||
|
||||
---
|
||||
|
||||
## 9. คู่มือวิศวกรรมสำหรับ AI ในการขยายขีดความสามารถ (AI Extension & Engineering Guide)
|
||||
|
||||
หากโมเดล AI อื่นต้องการปรับแต่งหรือขยายขีดความสามารถของ FireflyProtoc ต่อไป:
|
||||
|
||||
1. **ข้อควรระวังเรื่อง CLI Output Path**:
|
||||
ใน `src/FireflyProtoc.cs` บรรทัดที่ 21 มีการฮาร์ดโค้ด `$"./{outputFolder}"`
|
||||
หากส่ง Path แบบ Absolute (เช่น `C:\Users\...`) จะทำให้ระบบพาธขัดข้อง (`./C:\...`)
|
||||
**คำแนะนำในการแก้:** เปลี่ยนให้ใช้ `Path.GetFullPath(outputFolder)` หรือตรวจสอบ `Path.IsPathRooted(outputFolder)` ก่อนต่อสตริง
|
||||
2. **การคอมไพล์ในสภาพแวดล้อม .NET 10**:
|
||||
ไฟล์ `FireflyProtoc.csproj` มีการเปิดแฟล็ก `<PublishAot>true</PublishAot>` สำหรับ `net9.0` ซึ่งหากรันคำสั่ง `dotnet run` ภายใต้ .NET 10 SDK จะเกิดข้อผิดพลาดในการหา ILCompiler Targets
|
||||
**คำแนะนำในการรัน:** ให้ส่งแฟล็ก `-c Release -p:PublishAot=false` ขณะทดสอบ หรือปรับ `<TargetFramework>` ให้ตรงกับเวอร์ชันของ SDK ที่ติดตั้ง
|
||||
3. **การเพิ่มตัวดำเนินการพรางข้อมูล (Bitshift, Modulo)**:
|
||||
หากต้องการให้รองรับการเลื่อนบิต (`<<`, `>>`) หรือเศษเหลือ (`%`) ให้ไปเพิ่ม Mapping ผกผันใน `InvertWithRight` และ `InvertWithLeft` ของ `src/Semantic/ExpressionInverter.cs`:
|
||||
- `<<` ผกผันด้วย `>>`
|
||||
- `>>` ผกผันด้วย `<<`
|
||||
@@ -0,0 +1,344 @@
|
||||
# Firefly Static Parser Architectural Specification & Cryptographic Deep Dive
|
||||
|
||||
This technical specification provides an exhaustive, code-level analysis of **`Firefly-Static-Parser`** (by AzenKain), an offline, static reverse-engineering framework and IL2CPP metadata extractor implemented in Rust. Unlike in-process dynamic dumpers (such as `honkai-dumper`), `Firefly-Static-Parser` operates **completely offline** without launching, injecting, or hooking into the game client process.
|
||||
|
||||
It achieves this by completely reverse-engineering the proprietary metadata container format (`MHY\0`), the embedded layout tables in `GameAssembly.dll`, dynamic semantic XOR key derivation, and non-linear mathematical scrambling algorithms used in *Honkai: Star Rail*.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architectural Paradigm
|
||||
|
||||
### 1.1 The Challenge of Custom IL2CPP Obfuscation
|
||||
Standard Unity IL2CPP uses known, public data structures:
|
||||
- `global-metadata.dat` begins with magic `0xFAB11BAF` (version 29+).
|
||||
- Metadata definitions (TypeDef, MethodDef, FieldDef) are stored sequentially in unencrypted flat tables.
|
||||
|
||||
In *Honkai: Star Rail* and similar protected titles:
|
||||
1. **Container Replaced**: `global-metadata.dat` begins with custom magic `MHY\0` (`0x0059484D`).
|
||||
2. **Split Metadata**: Metadata ranges and image mappings are split between `global-metadata.dat` and `startup-metadata.dat`.
|
||||
3. **Embedded PE Headers**: The true table offsets, sizes, and field layout descriptors are embedded inside `GameAssembly.dll` and encrypted with arithmetic displacements and bitwise XOR masks.
|
||||
4. **Scrambled Field/Method Entries**: Entries are not sequential; attributes, indices, and names are encrypted with non-linear hash/PRNG functions unique to each table.
|
||||
5. **Seeded Stream Cipher on Strings**: Type names and string literals are packed into bitfields and encrypted with a QWORD running stream cipher.
|
||||
|
||||
### 1.2 The Two Dumping Paradigms
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| IN-PROCESS vs. OFFLINE PARSER |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ In-Process Dumper: honkai-dumper ] |
|
||||
| Injected DLL -> Waits 10s -> Lets game unpacker decrypt metadata -> Scans UnityPlayer.dll |
|
||||
| -> Calls runtime C-API pointers in RAM -> Dumps memory objects |
|
||||
| * Advantage: Simple implementation, relies on game's own decryption routines. |
|
||||
| * Disadvantage: Requires running game process, triggers anti-cheat/integrity monitors. |
|
||||
| |
|
||||
| [ Offline Static Parser: Firefly-Static-Parser ] |
|
||||
| Direct disk analysis: GameAssembly.dll + global-metadata.dat + startup-metadata.dat |
|
||||
| 1. Parses PE64 headers -> Scans byte patterns for metadata initializers |
|
||||
| 2. Solves arithmetic/XOR masks -> Resolves table offsets & payload boundaries |
|
||||
| 3. Dynamic Semantic Key Discovery -> Derives XOR keys via CLR invariants (<Module>, ValueType) |
|
||||
| 4. Decrypts QWORD string cipher & PRNG-scrambled method/field definitions |
|
||||
| 5. Reconstructs TypeDef AST -> Emits dump.cs & methods.json |
|
||||
| * Advantage: Zero process injection, zero anti-cheat risk, pure deterministic static analysis. |
|
||||
| * Disadvantage: High mathematical and reverse-engineering complexity. |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Codebase Architecture & File Organization
|
||||
|
||||
The project is structured in Rust (2024 Edition) with a modular pipeline separating PE analysis, metadata loading, mathematical key derivation, and AST emission.
|
||||
|
||||
```
|
||||
Firefly-Static-Parser/
|
||||
├── Cargo.toml # anyhow, serde, serde_json, windows-sys, iced-x86, il2cpp_dumper
|
||||
├── src/
|
||||
│ ├── main.rs # CLI entry point -> app::run()
|
||||
│ ├── app.rs # Input routing (Standard vs MHY custom metadata branch)
|
||||
│ ├── args.rs # CLI arguments & default path resolvers
|
||||
│ ├── config.rs # Dumper configuration flags
|
||||
│ ├── pe_image.rs # Zero-dependency PE64 section parser, RVA mapper, pattern scanner
|
||||
│ ├── static_layout.rs # Signature scanner, PE layout solver, arithmetic/XOR mask decoders
|
||||
│ ├── metadata.rs # Magic validator (0xFAB11BAF vs b"MHY\0")
|
||||
│ ├── obfuscated.rs # Fallback in-process loader via GameAssembly.dll custom loader RVA
|
||||
│ ├── il2cpp_pe.rs # Standard IL2CPP symbol/code registration loader
|
||||
│ ├── output.rs # Standard dumper output generator
|
||||
│ └── layout_model/ # The custom MHY layout reconstruction engine
|
||||
│ ├── mod.rs # LayoutImage, LayoutTypeDef, LayoutMethod, LayoutField AST models
|
||||
│ ├── output.rs # Static dump.cs & methods.json serialization
|
||||
│ └── parser/
|
||||
│ ├── mod.rs # Dynamic Semantic Key Discovery & PRNG scramble formulas
|
||||
│ ├── loader.rs # Dynamic scanner for il2cpp_type_table & method_pointer_table
|
||||
│ └── reader.rs # QWORD string stream cipher, method/field decoders, generic solver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. PE Layout & Global Discovery Engine (`static_layout.rs`)
|
||||
|
||||
To parse the custom metadata container, the parser must first extract layout descriptors embedded within `GameAssembly.dll`.
|
||||
|
||||
### 3.1 Pattern Scanning for Initializers
|
||||
`Firefly-Static-Parser` scans the `.text` section of `GameAssembly.dll` for two critical initialization sequences:
|
||||
|
||||
1. **`static_metadata_initializer_pattern`**:
|
||||
The routine that initializes the IL2CPP static metadata globals. From this function, RIP-relative `lea` instructions are decoded to extract:
|
||||
- `metadata_registration_rva`
|
||||
- `descriptor_rva`
|
||||
- `metadata_cache_rva`
|
||||
- `embedded_header_rva`
|
||||
|
||||
2. **`metadata_payload_initializer_pattern`**:
|
||||
The routine responsible for loading the external data files. It validates string references:
|
||||
```rust
|
||||
rva_has_c_string(image, global_name_rva, b"global-metadata.dat")
|
||||
rva_has_c_string(image, startup_name_rva, b"startup-metadata.dat")
|
||||
```
|
||||
Extracts `payload_offset` (typically `0x00` or file header length).
|
||||
|
||||
### 3.2 Decryption of Embedded Header Offsets
|
||||
The table offsets in the embedded header are obfuscated using combinations of 32-bit XOR keys, bitwise arithmetic shifts (`sar_u32`), and wrapping modular additions.
|
||||
|
||||
```rust
|
||||
// Discovered from embedded_header_rva in GameAssembly.dll
|
||||
let descriptor_count_80 = image.read_u32_rva(descriptor_rva + 0x80)? ^ 0x69BD_97F1;
|
||||
let header_size_1a8 = image.read_u32_rva(header + 0x1A8)? ^ 0x729B_1A9E;
|
||||
let header_size_1f8 = image.read_u32_rva(header + 0x1F8)? ^ 0x1608_C2C8;
|
||||
let header_size_74 = image.read_u32_rva(header + 0x74)? ^ 0x080E_F250;
|
||||
let header_count_134 = (image.read_u32_rva(header + 0x134)? ^ 0x1021_0728) / 40;
|
||||
let header_count_178 = sar_u32(image.read_u32_rva(header + 0x178)?, 4) ^ 0x05CC_1AE1;
|
||||
let header_count_bc = sar_u32(image.read_u32_rva(header + 0xBC)?, 3) ^ 0x079F_C2EC;
|
||||
```
|
||||
|
||||
#### Complete Table Offsets Mapping:
|
||||
|
||||
| Offset In Header | Operation | Key / Operand | Output Field in `StaticLayout` |
|
||||
| :---: | :---: | :---: | :--- |
|
||||
| `+0x164` | `XOR` | `0x7F5C_5934` | `startup_section_164_offset` |
|
||||
| `+0x150` | `wrapping_add` | `0xD882_615E` | `startup_image_table_offset` |
|
||||
| `+0x158` | `wrapping_add` | `0xE03E_EAC1` | `startup_assembly_table_offset` |
|
||||
| `+0xD0` | `wrapping_add` | `0xDFCF_C6B0` | `startup_assembly_name_table_offset` |
|
||||
| `+0x84` | `XOR` | `0x6853_1D3F` | `global_type_table_offset` |
|
||||
| `+0x20` | `wrapping_add` | `0xB2FC_E189` | `global_field_table_offset` |
|
||||
| `+0x40` | `XOR` | `0x3C68_5CDB` | `global_property_table_offset` |
|
||||
| `+0x180` | `wrapping_add` | `0xE4DB_E763` | `global_type_field_offset_map_offset` |
|
||||
| `+0x9C` | `wrapping_add` | `0xBC7E_C9D7` | `global_field_offset_group_table_offset` |
|
||||
| `+0x148` | `XOR` | `0x329E_1172` | `global_field_offset_table_offset` |
|
||||
| `+0x14C` | `wrapping_add` | `0xF3A0_4294` | `global_method_table_offset` |
|
||||
| `+0x30` | `wrapping_add` | `0xDCF5_FDBE` | `global_parameter_table_offset` |
|
||||
| `+0x12C` | `XOR` | `0x26F0_FC20` | `global_nested_type_table_offset` |
|
||||
| `+0x1C8` | `XOR` | `0x042F_9275` | `global_interface_type_table_offset` |
|
||||
| `+0xF0` | `wrapping_add` | `0x9D48_920F` | `global_generic_container_table_offset` |
|
||||
| `+0x140` | `wrapping_add` | `0xDAAC_7309` | `global_generic_parameter_table_offset` |
|
||||
| `+0x38` | `wrapping_add` | `0xF778_F1AB` | `global_metadata_usage_type_table_offset` |
|
||||
| `+0x1D0` | `wrapping_add` | `0xC9A6_64A5` | `global_metadata_usage_range_offset` |
|
||||
| `+0x190` | `wrapping_add` | `0xED79_1073` | `global_metadata_usage_pair_offset` |
|
||||
| `+0x1F0` | `XOR` | `0x56C7_D20D` | `global_string_literal_table_offset` |
|
||||
| `+0x08` | `wrapping_add` | `0xF711_4E59` | `global_string_literal_data_offset` |
|
||||
| `+0x1FC` | `XOR` | `0x6238_CDB0` | `global_field_default_value_table_offset` |
|
||||
| `+0x3C` | `wrapping_add` | `0x978B_E7A5` | `global_field_default_value_data_offset` |
|
||||
| `+0x1DC` | `XOR / 12` | `0x3E0C_72F0` | `field_default_value_count` |
|
||||
| `+0x1B4` | `wrapping_add` | `0x8D43_A4EE` | `global_string_data_offset` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Dynamic Semantic Key Derivation (`layout_model/parser/mod.rs`)
|
||||
|
||||
A major breakthrough in `Firefly-Static-Parser` is how it discovers runtime XOR keys and type sentinels **without hardcoding version-specific constants**.
|
||||
|
||||
### 4.1 The Semantic Invariant Technique
|
||||
In every valid .NET/IL2CPP metadata image, standard CLI core types always exhibit known semantic invariants:
|
||||
1. **TypeDef 0 is `<Module>`**:
|
||||
- Has no base type $\rightarrow$ raw base value gives `type_base_none`.
|
||||
- Has known attribute flags $\rightarrow$ `raw_flags ^ 0x700` reveals `type_attribute_key`.
|
||||
2. **`System.ValueType`**:
|
||||
- Inherits directly from `System.Object`. Therefore, the raw base type of `System.ValueType` reveals `type_base_object`.
|
||||
3. **`System.Enum`**:
|
||||
- Inherits directly from `System.ValueType`. Its raw base type reveals `type_base_value_type`.
|
||||
4. **`System.AttributeTargets`**:
|
||||
- Is an Enum. Its raw base type reveals `type_base_enum`.
|
||||
5. **Parent Type Index Formula**:
|
||||
$$\text{type\_parent\_key} = \text{type\_base\_object} - 1$$
|
||||
$$\text{ParentIndex} = \text{raw\_base\_type} - \text{type\_parent\_key}$$
|
||||
|
||||
### 4.2 Code Implementation
|
||||
```rust
|
||||
fn discover_type_layout_keys(layout: &mut StaticLayout, global_data: &[u8]) -> Result<()> {
|
||||
// 1. Module Sentinel & Attribute Key
|
||||
let type_table_offset = layout.payload_offset as usize + layout.global_type_table_offset as usize;
|
||||
let module_name = decode_type_name_at(global_data, layout, type_table_offset)?.1;
|
||||
assert_eq!(module_name, "<Module>");
|
||||
|
||||
layout.keys.type_base_none = read_u32(global_data, type_table_offset + 0x04)?;
|
||||
layout.keys.type_attribute_key = read_u32(global_data, type_table_offset + 0x14)? ^ 0x700;
|
||||
|
||||
// 2. Scan Type Definitions for Standard BCL Anchors
|
||||
for type_index in 0..scan_count {
|
||||
let entry_offset = type_entry_offset_raw(layout, type_index).unwrap();
|
||||
let raw_base = read_u32(global_data, entry_offset + 0x04)?;
|
||||
let (namespace, name) = decode_type_name_at(global_data, layout, entry_offset)?;
|
||||
|
||||
match (namespace.as_str(), name.as_str()) {
|
||||
("System", "ValueType") => layout.keys.type_base_object = raw_base,
|
||||
("System", "Enum") => layout.keys.type_base_value_type = raw_base,
|
||||
("System", "AttributeTargets") => layout.keys.type_base_enum = raw_base,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
layout.keys.type_parent_key = layout.keys.type_base_object.wrapping_sub(1);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Decryption Algorithms & Mathematical Models
|
||||
|
||||
### 5.1 The QWORD String Stream Cipher (`decode_string_raw`)
|
||||
In the `MHY\0` container, strings are not null-terminated C-strings. String pointers are 32-bit composite integers encoding both length and offset.
|
||||
|
||||
#### 1. Composite Index Bitfield Decoding:
|
||||
```rust
|
||||
let signed_index = index as i32;
|
||||
let (length, string_offset_mask) = if signed_index < 0 {
|
||||
// Long String Format (up to 255 bytes, 23-bit offset)
|
||||
(((index >> 23) & 0xFF) as usize, 0x007F_FFFF)
|
||||
} else {
|
||||
// Short String Format (up to 63 bytes, 25-bit offset)
|
||||
(((index >> 25) & 0x3F) as usize, 0x01FF_FFFF)
|
||||
};
|
||||
let string_offset = (index & string_offset_mask) as usize;
|
||||
```
|
||||
|
||||
#### 2. Seeded Stream Decryption:
|
||||
Strings are encrypted in 8-byte QWORD blocks. The key is dynamically computed per string offset:
|
||||
|
||||
$$K_0 = \text{seed\_add} + (\text{seed\_mul} \times \text{string\_offset})$$
|
||||
|
||||
For each 8-byte chunk $i \in [0, \lceil \text{length} / 8 \rceil)$:
|
||||
|
||||
$$P_i = C_i \oplus K_i$$
|
||||
|
||||
$$K_{i+1} = K_i + \text{increment}$$
|
||||
|
||||
```rust
|
||||
let mut key = string_payload_key(layout, string_offset);
|
||||
let mut out = Vec::with_capacity(qword_count * 8);
|
||||
for chunk_index in 0..qword_count {
|
||||
let encrypted = read_u64(global_data, data_offset + chunk_index * 8)?;
|
||||
out.extend_from_slice(&(encrypted ^ key).to_le_bytes());
|
||||
key = key.wrapping_add(string_payload_increment(layout));
|
||||
}
|
||||
out.truncate(length);
|
||||
```
|
||||
|
||||
### 5.2 Non-Linear PRNG Scrambling Functions
|
||||
|
||||
Every table entry in the metadata layout is obfuscated by non-linear integer hashing functions:
|
||||
|
||||
#### 1. Image Key (`image_key`):
|
||||
$$K_{\text{img}}(i) = ((((i \times \mathtt{0xE07C}) \oplus \mathtt{0x7538159E}) \times \mathtt{0x120D0703}) \oplus \mathtt{0x6032C9D3}) + \mathtt{0x2EBB0085}$$
|
||||
|
||||
#### 2. Method Key (`method_key`):
|
||||
$$v_1 = (i \times \mathtt{0x31E1}) \oplus \mathtt{0x33914937}$$
|
||||
$$v_2 = ((v_1 \times \mathtt{0x2C03F17D}) \gg 23) \times \mathtt{0x540CC9F4} \gg 21$$
|
||||
$$K_{\text{method}}(i) = (v_2 \text{ as u32}) + \mathtt{0x71BC7861}$$
|
||||
|
||||
#### 3. Parameter Key (`parameter_key`):
|
||||
$$v = ((i \times \mathtt{0x072E1D74B12B} + \mathtt{0x01911D05AFF5}) \gg 11) \text{ as u32}$$
|
||||
$$K_{\text{param}}(i) = (v \times \mathtt{0x58B870A2}) + \mathtt{0x83CF7B44}$$
|
||||
|
||||
#### 4. Field Key (`field_key`):
|
||||
$$K_{\text{field}}(\text{start}, j) = \mathtt{0xAD416BB9} - (\text{start} \times \mathtt{0x2C5DCB00}) + (j \times \mathtt{0xD3A23500})$$
|
||||
|
||||
#### 5. Generic Container Key (`generic_container_key`):
|
||||
$$v_1 = (i \times \mathtt{0x3D6913E0AF40} + \mathtt{0x0A64CAD60FA052C0}) \gg 23$$
|
||||
$$v_2 = ((v_1 \times \mathtt{0x770E3FE8}) \gg 11) \times \mathtt{0x2C9A0EA3} \gg 23$$
|
||||
$$K_{\text{gen\_container}}(i) = v_2 \text{ as u32}$$
|
||||
|
||||
---
|
||||
|
||||
## 6. Type System Reconstruction & Output Generation
|
||||
|
||||
### 6.1 Dynamic Table Resolution in `GameAssembly.dll`
|
||||
In addition to metadata parsing, method RVAs require locating the compiled function pointers. In `src/layout_model/parser/loader.rs`:
|
||||
- **`il2cpp_type_table`**: Scans `GameAssembly.dll` for continuous 16-byte descriptors where `kind` $\in (0, 0x50)$ and `byref` $\in \{0, 1, 0x80, 0x81\}$.
|
||||
- **`method_pointer_table`**: Scans for continuous runs of 64-bit pointers residing within the `.text` segment ($ImageBase + 0x1000 \le VA < ImageBase + Size$).
|
||||
- **`generic_inst_table`**: Scans for runs of `argc` ($0 < argc < 30$) with 32-bit zero padding and valid pointer prefixes (`0x01`, `0x00`, `0x7F`).
|
||||
|
||||
### 6.2 Output Formats
|
||||
|
||||
#### 1. Reconstructed C# Source (`dump.cs`)
|
||||
Outputs complete class declarations with namespaces, type index comments, attributes, and field offsets:
|
||||
|
||||
```csharp
|
||||
// FirelfyShelter static asm parse
|
||||
|
||||
// Image 0: Assembly-CSharp.dll
|
||||
|
||||
// Namespace: RPG.Client
|
||||
// TypeDefIndex: 1420
|
||||
[Serializable]
|
||||
public class AutoScrollRect : ScrollRect
|
||||
{
|
||||
// Fields
|
||||
private Vector3[] KOIAJFFGPGF; // 0x180
|
||||
|
||||
// Methods
|
||||
// RVA: 0x1febdf0 VA: 0x181febdf0
|
||||
public void .ctor() { }
|
||||
|
||||
// RVA: 0x1feb700 VA: 0x181feb700
|
||||
private void MNOGNALMGPL(AnimatorButton target) { }
|
||||
|
||||
// RVA: 0x1feb9c0 VA: 0x181feb9c0
|
||||
public void SnapTo(GameObject target) { }
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. JSON Method Map (`methods.json`)
|
||||
Outputs canonical method signatures mapped to hexadecimal RVAs:
|
||||
|
||||
```json
|
||||
{
|
||||
"RPG.Client.AutoScrollRect::.ctor()": "0x1febdf0",
|
||||
"RPG.Client.AutoScrollRect::MNOGNALMGPL(RPG.Client.AnimatorButton)": "0x1feb700",
|
||||
"RPG.Client.AutoScrollRect::POFLDHKMLAF()": "0x1feb7a0",
|
||||
"RPG.Client.AutoScrollRect::SetItemSelectCallback()": "0x1feb7f0",
|
||||
"RPG.Client.AutoScrollRect::SnapTo(UnityEngine.GameObject)": "0x1feb9c0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Comparative Analysis Matrix
|
||||
|
||||
| Feature | `Firefly-Static-Parser` | `honkai-dumper` | Standard `Il2CppDumper` |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Execution Mode** | **Offline CLI** | **In-Process DLL** | **Offline CLI** |
|
||||
| **Target Files** | `GameAssembly.dll`<br>`global-metadata.dat`<br>`startup-metadata.dat` | Running `StarRail.exe` process | `GameAssembly.dll`<br>`global-metadata.dat` |
|
||||
| **`MHY\0` Container Decryption** | **Native Built-in** (Algebraic & QWORD stream cipher) | **Delegated** to game process | **Unsupported** (Fails on magic mismatch) |
|
||||
| **Anti-Cheat Exposure** | **Zero** | High (Process injection) | **Zero** |
|
||||
| **Key Extraction** | **Dynamic Semantic Discovery** (via `<Module>`, `ValueType`) | Memory query | Static pattern scanning |
|
||||
| **Method RVA Output** | Canonical typed signatures in `methods.json` | Flattened overloaded names in `methods.json` | Script files for IDA/Ghidra |
|
||||
| **Portability** | Multi-platform (Linux/Windows/macOS) | Windows-only (x86_64) | Multi-platform |
|
||||
|
||||
---
|
||||
|
||||
## 8. Integration & Protocol Engineering Synergy
|
||||
|
||||
### 8.1 Feedstock for `FireflyProtoc`
|
||||
In the Star Rail reverse-engineering toolchain:
|
||||
1. `Firefly-Static-Parser` extracts the exact class structures and obfuscated field names (e.g., `KOIAJFFGPGF` $\rightarrow$ Wire tag `1` at offset `0x180`).
|
||||
2. Engineers cross-reference these offsets with network serialization routines.
|
||||
3. The resulting proto definitions with wire masks (`[get: (id ^ 1) + 2]`) and metadata (`meta uint32 cmd_id = 143;`) are compiled using **`FireflyProtoc`**.
|
||||
4. The generated high-performance C# or Go models are directly consumed by **`March7thHoney`** or **`GaC-hkrpg-go`**.
|
||||
|
||||
### 8.2 Compliance with Repository Rules (`AGENTS.md`)
|
||||
- `Firefly-Static-Parser` operates purely as an offline analysis tool.
|
||||
- It contains no hardcoded user paths, machine IP addresses, or environment-dependent paths.
|
||||
- It eliminates the need for prohibited live heartbeat scanning scripts in game client directories (`scripts/`), providing a 100% compliant and safe research workflow.
|
||||
@@ -0,0 +1,450 @@
|
||||
# Honkai: Star Rail In-Process Dumper (`honkai-dumper`) Architectural Specification & Deep Dive
|
||||
|
||||
This technical specification provides an exhaustive, code-level analysis of **`honkai-dumper`** (by AzenKain / lanylow), an in-process, native Rust dynamic link library (`cdylib`) engineered to extract metadata, field layouts, and compiled method Relative Virtual Addresses (RVAs) from the live process memory of *Honkai: Star Rail* (`StarRail.exe`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Design Rationale
|
||||
|
||||
### 1.1 The Fundamental Problem of Offline IL2CPP Dumpers
|
||||
In conventional Unity IL2CPP games, compiled assemblies exist as two distinct artifacts:
|
||||
1. `GameAssembly.dll`: Native machine code (PE x86_64) resulting from C# $\rightarrow$ C++ $\rightarrow$ Clang compilation.
|
||||
2. `global-metadata.dat`: Binary metadata file containing string literals, type definitions, method signatures, and field offset tables.
|
||||
|
||||
In standard environments, tools like **Il2CppDumper** or **Il2CppInspector** parse `global-metadata.dat` statically on disk. However, modern live-service games protected by commercial packers/DRM implement active anti-reversing countermeasures:
|
||||
- **Encrypted Metadata**: `global-metadata.dat` is heavily obfuscated and encrypted on disk; static extractors fail immediately without proprietary decryption keys.
|
||||
- **Stripped API Exports**: `GameAssembly.dll` strips all standard IL2CPP C-API exports (e.g., `il2cpp_domain_get`, `il2cpp_class_get_methods`), rendering standard dynamic import resolution (`GetProcAddress`) ineffective.
|
||||
|
||||
### 1.2 The In-Process Dumper Solution
|
||||
`honkai-dumper` bypasses disk-level metadata encryption entirely by operating **inside the game process space** as an injected native module (`.dll`):
|
||||
- It delays execution until the game engine has completed unpacking, decrypted metadata, and instantiated the internal runtime structures in memory.
|
||||
- Instead of relying on exported symbols in `GameAssembly.dll`, it utilizes **signature pattern scanning** inside `UnityPlayer.dll` to locate the hidden global table of IL2CPP API function pointers.
|
||||
- It walks the live memory graph (`Domain` $\rightarrow$ `Assembly` $\rightarrow$ `Image` $\rightarrow$ `Class` $\rightarrow$ `Method` / `Field`), emitting reconstructed C# header definitions (`dump.cs`) and method RVA address maps (`methods.json`) ready for IDA Pro, Ghidra, or Binary Ninja.
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| Game Process Address Space |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ UnityPlayer.dll ] [ GameAssembly.dll ] |
|
||||
| +-------------------------------------+ +-------------------------------+ |
|
||||
| | Pattern: 48 8B 05 .. 48 8D 0D .. | | Native Machine Code Functions | |
|
||||
| | | | | (Compiled from C# Methods) | |
|
||||
| | v | +---------------+---------------+ |
|
||||
| | RIP-relative calculation | ^ |
|
||||
| | | | | |
|
||||
| | v | | Method Pointers |
|
||||
| | Internal Function Pointer Table | | (VAs / RVAs) |
|
||||
| | [0] = null | | |
|
||||
| | [22] = il2cpp_assembly_get_image | | |
|
||||
| | [35] = il2cpp_class_get_methods | | |
|
||||
| | [63] = il2cpp_domain_get | | |
|
||||
| +-------------------+-----------------+ | |
|
||||
| | | |
|
||||
+----------------------|----------------------------------------------------------|------------------+
|
||||
| Resolves Function Pointers |
|
||||
v |
|
||||
+---------------------------------------------------------------------------------+------------------+
|
||||
| honkai-dumper.dll (Injected Module) |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| 1. entry() [#[ctor]] -> Spawns worker thread |
|
||||
| 2. thread::sleep(10s) -> Awaits runtime initialization & metadata decryption |
|
||||
| 3. AllocConsole() -> Interactive logging |
|
||||
| 4. Traverser: Domain -> Assemblies -> Images -> Classes -> Fields & Methods |
|
||||
| 5. Serialization: |
|
||||
| * dump.cs : Reconstructed C# class headers with field offsets and method RVAs |
|
||||
| * methods.json : JSON dictionary mapping "Namespace.Class::Method" -> "0xRVA" |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Codebase Architecture & File Organization
|
||||
|
||||
The project is implemented in pure Rust (2021 Edition) configured as a Windows C dynamic library (`crate-type = ["cdylib"]`).
|
||||
|
||||
```
|
||||
honkai-dumper/
|
||||
├── Cargo.toml # cdylib target configuration, winapi, pelite, serde_json, ctor
|
||||
├── src/
|
||||
│ ├── lib.rs # Module root (base, il2cpp, outputs)
|
||||
│ ├── base.rs # DLL entry point, thread creation, 10s synchronization delay
|
||||
│ ├── il2cpp/
|
||||
│ │ ├── mod.rs # Re-exports submodules
|
||||
│ │ ├── types.rs # Raw C-ABI memory representations (MethodInfo, Il2CppClass, etc.)
|
||||
│ │ ├── constants.rs # ECMA-335 CLI metadata attribute masks and flags
|
||||
│ │ ├── module.rs # Win32 module loader & generic FunctionPtr<T> wrapper
|
||||
│ │ ├── utils.rs # PE scanner & RIP-relative displacement pattern matcher
|
||||
│ │ ├── functions.rs # Il2CppFunctions table index mapping (array offsets)
|
||||
│ │ └── api.rs # High-level safe wrapper over runtime API pointers
|
||||
│ └── outputs/
|
||||
│ ├── mod.rs # Output format definitions
|
||||
│ ├── methoddumper.rs # Method RVA JSON exporter (methods.json)
|
||||
│ └── csdumper.rs # C# AST reconstruction exporter (dump.cs)
|
||||
```
|
||||
|
||||
### 2.1 Dependencies & Their Technical Roles
|
||||
|
||||
| Crate | Version | Architectural Role |
|
||||
| :--- | :--- | :--- |
|
||||
| `ctor` | `0.2.8` | Emits `#[ctor]` constructor function into `.CRT$XCU` section for execution on DLL load. |
|
||||
| `pelite` | `0.10.0` | High-performance, zero-allocation PE64 file format parser for section headers and RVA conversion. |
|
||||
| `winapi` | `0.3.9` | Windows API bindings for `libloaderapi`, `consoleapi`, `psapi`, and `processthreadsapi`. |
|
||||
| `serde_json` | `1.0.116` | Serialization engine for generating formatted `methods.json`. |
|
||||
| `thiserror` | `1.0.59` | Type-safe error enumeration derivation. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Initialization Lifecycle & Process Synchronization
|
||||
|
||||
### 3.1 Non-Blocking DLL Attachment Pattern
|
||||
Standard Windows DLL injection executes the payload inside `DllMain` during `DLL_PROCESS_ATTACH`. Executing heavy workloads or blocking synchronization inside `DllMain` violates Windows loader lock contracts and causes deadlocks.
|
||||
|
||||
`honkai-dumper` isolates injection lifecycle via `src/base.rs`:
|
||||
|
||||
```rust
|
||||
use ctor::ctor;
|
||||
use winapi::um::consoleapi;
|
||||
use std::{error::Error, thread, time::Duration};
|
||||
use crate::outputs::{csdumper, methoddumper};
|
||||
|
||||
#[ctor]
|
||||
fn entry() {
|
||||
thread::spawn(|| {
|
||||
init().unwrap_or_else(|e| {
|
||||
panic!("honkai-dumper encountered an error: {}", e)
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
1. **`#[ctor]` Injection**: The `ctor` macro generates a C-runtime startup initializer function.
|
||||
2. **`thread::spawn`**: Immediately offloads execution to an isolated OS thread, returning control to the Windows loader instantly and averting loader lock deadlocks.
|
||||
3. **Engine Warmup Delay**:
|
||||
```rust
|
||||
fn init() -> Result<(), Box<dyn Error>> {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
unsafe { consoleapi::AllocConsole() };
|
||||
// ...
|
||||
}
|
||||
```
|
||||
A fixed sleep of 10 seconds is imposed. This provides the host game process sufficient wall-clock time to:
|
||||
- Load and decrypt secondary DLLs (`GameAssembly.dll`).
|
||||
- Run native unpackers and decrypt internal `global-metadata.dat` structures into RAM.
|
||||
- Execute IL2CPP runtime initialization routines (`il2cpp_init`) and register class domains.
|
||||
4. **Console Allocation**: Calls Win32 `AllocConsole()` to attach a dedicated Win32 terminal window to the host process for operator feedback.
|
||||
|
||||
---
|
||||
|
||||
## 4. The Signature Scanning Engine (`utils.rs` & `api.rs`)
|
||||
|
||||
### 4.1 Locating Stripped IL2CPP Function Pointers
|
||||
Because `GameAssembly.dll` exports are stripped, `honkai-dumper` leverages the structural architecture of the Unity Engine: `UnityPlayer.dll` acts as the engine host and holds a dispatch table of internal IL2CPP functions passed or registered during engine startup.
|
||||
|
||||
### 4.2 The x86_64 Machine Code Pattern
|
||||
In `src/il2cpp/utils.rs`, the tool scans `UnityPlayer.dll` on disk using the following byte pattern:
|
||||
```
|
||||
48 8B 05 ? ? ? ? 48 8D 0D ? ? ? ? FF D0
|
||||
```
|
||||
|
||||
#### Disassembly & Instruction Breakdown:
|
||||
```nasm
|
||||
48 8B 05 xx xx xx xx ; mov rax, [rip + disp32_1] <-- Loads target function table pointer
|
||||
48 8D 0D yy yy yy yy ; lea rcx, [rip + disp32_2] <-- Loads context argument into RCX
|
||||
FF D0 ; call rax <-- Indirect call to IL2CPP initialization
|
||||
```
|
||||
|
||||
### 4.3 RIP-Relative Displacement Calculation
|
||||
x86_64 uses relative instruction pointer (RIP) addressing. The displacement stored in the machine instruction is relative to the address of the **subsequent** instruction.
|
||||
|
||||
`honkai-dumper` resolves this with mathematical precision:
|
||||
|
||||
$$\text{RVA}_{\text{target}} = \text{RVA}_{\text{instruction}} + \text{Len}_{\text{instruction}} + \text{disp32}$$
|
||||
|
||||
In `utils.rs`:
|
||||
```rust
|
||||
pub fn find_qword_addr(data: &[u8], base_addr: u64, pe: &PeFile) -> Option<usize> {
|
||||
let pattern = pattern_to_bytes("48 8B 05 ? ? ? ? 48 8D 0D ? ? ? ? FF D0");
|
||||
|
||||
for i in 0..=data.len().saturating_sub(pattern.len()) {
|
||||
if match_pattern(data, &pattern, i) {
|
||||
let rva = file_offset_to_rva(i as u32, pe)?;
|
||||
let va = base_addr + rva as u64;
|
||||
|
||||
let rel_offset = i + 3; // Offset to 4-byte displacement in `mov rax, [rip + disp32]`
|
||||
let disp = i32::from_le_bytes(data[rel_offset..rel_offset + 4].try_into().unwrap());
|
||||
|
||||
let rip_after_mov = va + 7; // Length of `mov rax, [rip + disp32]` is 7 bytes
|
||||
let qword_addr = rip_after_mov.wrapping_add(disp as u64);
|
||||
|
||||
return Some((qword_addr - base_addr) as usize);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
1. **File Offset $\rightarrow$ RVA Conversion**: Iterates PE section headers via `pelite` to map raw file offset $i$ to section Virtual Address.
|
||||
2. **Displacement Extraction**: Reads 32-bit signed little-endian integer (`i32`) from offset `i + 3`.
|
||||
3. **Effective Address**: Adds displacement to $RIP_{\text{next}} = VA + 7$.
|
||||
4. **Base Address Normalization**: Subtracts image base to return the target table RVA relative to `UnityPlayer.dll`.
|
||||
|
||||
---
|
||||
|
||||
## 5. IL2CPP Function Table Jump Indices (`functions.rs`)
|
||||
|
||||
Once the table RVA is resolved, `honkai-dumper` treats the memory address inside the running `UnityPlayer.dll` as an array of raw function pointers:
|
||||
```rust
|
||||
let functions = Il2CppFunctions::new(unity_player.handle as usize + qword);
|
||||
```
|
||||
|
||||
### 5.1 Function Pointer Dereferencing Wrapper
|
||||
To safely call untyped C function pointers, `src/il2cpp/module.rs` defines a zero-cost wrapper `FunctionPtr<T>` utilizing Rust's `Deref` trait and `PhantomData`:
|
||||
|
||||
```rust
|
||||
pub struct FunctionPtr<T> {
|
||||
pub ptr: *const c_void,
|
||||
pd: PhantomData<T>
|
||||
}
|
||||
|
||||
impl<T> Deref for FunctionPtr<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &T {
|
||||
unsafe { &*(&self.ptr as *const *const _ as *const T) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 The Index Map
|
||||
`src/il2cpp/functions.rs` maps array offsets using the `index!` macro:
|
||||
```rust
|
||||
macro_rules! index {
|
||||
($start:expr, $index:expr) => {{
|
||||
let addr = unsafe { *$start.offset($index) };
|
||||
if addr.is_null() { None } else { Some(FunctionPtr::new(addr)) }
|
||||
}};
|
||||
}
|
||||
```
|
||||
|
||||
The specific indices corresponding to the global HSR Unity release:
|
||||
|
||||
| Table Index | Function Signature | Description |
|
||||
| :---: | :--- | :--- |
|
||||
| **22** | `il2cpp_assembly_get_image(*const Il2CppAssembly) -> *const Il2CppImage` | Retrieves primary PE image from assembly |
|
||||
| **31** | `il2cpp_class_get_fields(*const Il2CppClass, *const *const c_void) -> *const FieldInfo` | Iterates member fields of a class |
|
||||
| **33** | `il2cpp_class_get_interfaces(*const Il2CppClass, *const *const c_void) -> *const Il2CppClass` | Iterates implemented interfaces |
|
||||
| **35** | `il2cpp_class_get_methods(*const Il2CppClass, *const *const c_void) -> *const MethodInfo` | Iterates class methods |
|
||||
| **37** | `il2cpp_class_get_name(*const Il2CppClass) -> *const c_char` | Class name string |
|
||||
| **39** | `il2cpp_class_get_namespace(*const Il2CppClass) -> *const c_char` | Class namespace string |
|
||||
| **40** | `il2cpp_class_get_parent(*const Il2CppClass) -> *const Il2CppClass` | Base inheritance class |
|
||||
| **43** | `il2cpp_class_is_valuetype(*const Il2CppClass) -> bool` | Checks if type is struct / ValueType |
|
||||
| **45** | `il2cpp_class_get_flags(*const Il2CppClass) -> i32` | CLI TypeDef attribute flags |
|
||||
| **49** | `il2cpp_class_from_type(*const Il2CppType) -> *const Il2CppClass` | Resolves class definition from type ref |
|
||||
| **53** | `il2cpp_class_is_enum(*const Il2CppClass) -> bool` | Checks if type is Enum |
|
||||
| **63** | `il2cpp_domain_get() -> *const Il2CppDomain` | Returns root AppDomain pointer |
|
||||
| **65** | `il2cpp_domain_get_assemblies(*const Il2CppDomain, *const usize) -> *const *const Il2CppAssembly` | Returns array of loaded assemblies |
|
||||
| **72** | `il2cpp_field_get_flags(*const FieldInfo) -> i32` | CLI Field attribute flags |
|
||||
| **73** | `il2cpp_field_get_name(*const FieldInfo) -> *const c_char` | Field identifier name |
|
||||
| **75** | `il2cpp_field_get_offset(*const FieldInfo) -> usize` | Memory offset within instance/static buffer |
|
||||
| **76** | `il2cpp_field_get_type(*const FieldInfo) -> *const Il2CppType` | Field type definition |
|
||||
| **116** | `il2cpp_method_get_return_type(*const MethodInfo) -> *const Il2CppType` | Method return type |
|
||||
| **117** | `il2cpp_method_get_name(*const MethodInfo) -> *const c_char` | Method identifier name |
|
||||
| **123** | `il2cpp_method_get_param_count(*const MethodInfo) -> u32` | Parameter count |
|
||||
| **124** | `il2cpp_method_get_param(*const MethodInfo, u32) -> *const Il2CppType` | Parameter type at index |
|
||||
| **161** | `il2cpp_type_get_name(*const Il2CppType) -> *const c_char` | Full CLR type name |
|
||||
| **162** | `il2cpp_type_is_byref(*const Il2CppType) -> bool` | Checks `ref` / pointer indirection |
|
||||
| **163** | `il2cpp_type_get_attrs(*const Il2CppType) -> u32` | Param/Type attribute flags (`[In]`, `[Out]`) |
|
||||
| **168** | `il2cpp_image_get_name(*const Il2CppImage) -> *const c_char` | Assembly module name (e.g. `Assembly-CSharp.dll`) |
|
||||
| **169** | `il2cpp_image_get_class_count(*const Il2CppImage) -> usize` | Total number of classes in image |
|
||||
| **170** | `il2cpp_image_get_class(*const Il2CppImage, usize) -> *const Il2CppClass` | Retrieves class definition by index |
|
||||
|
||||
> [!WARNING] Version Fragility
|
||||
> Because these table indices represent direct slots inside an internal dispatch table in `UnityPlayer.dll`, **any major Unity version update or custom engine compilation will shift these indices**. If the game client updates its Unity minor/patch version, the array offsets must be re-verified against a disassembly of `UnityPlayer.dll`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Runtime Metamodel & Memory Layout (`types.rs` & `api.rs`)
|
||||
|
||||
### 6.1 Native `MethodInfo` Structure Representation
|
||||
In IL2CPP runtime architecture, every compiled managed method is represented by a C struct. In `src/il2cpp/types.rs`, `MethodInfo` is declared as:
|
||||
|
||||
```rust
|
||||
#[repr(C)]
|
||||
pub struct MethodInfo {
|
||||
pub klass: *const Il2CppClass,
|
||||
pub method_pointer: *const c_void,
|
||||
_pad: [u8; 0x20],
|
||||
pub flags: u16
|
||||
}
|
||||
```
|
||||
|
||||
#### Memory Layout Diagram:
|
||||
```
|
||||
Offset Field Type Description
|
||||
+0x00 klass *const Il2CppClass Pointer to declaring Il2CppClass
|
||||
+0x08 method_pointer *const c_void Absolute Virtual Address (VA) of native compiled code
|
||||
+0x10 _pad [u8; 0x20] Internal runtime fields (invoker_method, name, etc.)
|
||||
+0x30 flags u16 CLI Method Attributes (Public, Private, Static, etc.)
|
||||
```
|
||||
|
||||
### 6.2 Address Boundary Verification
|
||||
In JIT or native code hooking engines, pointers may point to trampolines, dynamically generated stubs, or external modules. `methoddumper.rs` enforces strict pointer sanity checking:
|
||||
|
||||
```rust
|
||||
fn verify_pointer(il2cpp: &Il2CppApi, pointer: usize) -> bool {
|
||||
(pointer > il2cpp.game_assembly.handle as usize) &&
|
||||
(pointer < il2cpp.game_assembly.handle as usize + il2cpp.game_assembly.size)
|
||||
}
|
||||
```
|
||||
Only method pointers strictly located within the memory boundary of `GameAssembly.dll` ($\text{Base} \le \text{Ptr} < \text{Base} + \text{SizeOfImage}$) are treated as genuine compiled game methods.
|
||||
|
||||
---
|
||||
|
||||
## 7. C# Header Reconstruction Engine (`csdumper.rs`)
|
||||
|
||||
`csdumper.rs` decompiles raw binary runtime structures into valid C# source definitions, decorating them with exact memory offsets and RVAs.
|
||||
|
||||
### 7.1 ECMA-335 Bitmask Decompilation
|
||||
The engine decodes CLI attribute bitfields from `constants.rs`:
|
||||
|
||||
#### 1. Method Visibility & Modifiers
|
||||
```rust
|
||||
let flags = unsafe { (*method).flags } as i32;
|
||||
let access = flags & METHOD_ATTRIBUTE_MEMBER_ACCESS_MASK;
|
||||
|
||||
let access_str = match access {
|
||||
METHOD_ATTRIBUTE_PRIVATE => "private ",
|
||||
METHOD_ATTRIBUTE_PUBLIC => "public ",
|
||||
METHOD_ATTRIBUTE_FAMILY => "protected ",
|
||||
METHOD_ATTRIBUTE_ASSEM | METHOD_ATTRIBUTE_FAM_AND_ASSEM => "internal ",
|
||||
METHOD_ATTRIBUTE_FAM_OR_ASSEM => "protected internal ",
|
||||
_ => ""
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. Virtual Table & Polymorphism Layout
|
||||
IL2CPP preserves method slot layout flags (`METHOD_ATTRIBUTE_VTABLE_LAYOUT_MASK`):
|
||||
- `METHOD_ATTRIBUTE_ABSTRACT`: Emits `abstract`.
|
||||
- `METHOD_ATTRIBUTE_FINAL` with `METHOD_ATTRIBUTE_REUSE_SLOT`: Emits `sealed override`.
|
||||
- `METHOD_ATTRIBUTE_VIRTUAL` with `METHOD_ATTRIBUTE_NEW_SLOT`: Emits `virtual`.
|
||||
- `METHOD_ATTRIBUTE_VIRTUAL` with `METHOD_ATTRIBUTE_REUSE_SLOT`: Emits `override`.
|
||||
|
||||
#### 3. ValueType Field Offset Normalization
|
||||
In IL2CPP's object model:
|
||||
- Reference types (`class`) carry a 16-byte (`0x10`) header (`Il2CppObject` containing `Il2CppClass*` and `MonitorData*`).
|
||||
- Value types (`struct`) on the heap are boxed with the 16-byte header, but unboxed instance structs lack this header.
|
||||
`csdumper.rs` handles this normalization:
|
||||
```rust
|
||||
if is_valuetype && !is_static && field_offset > 0 {
|
||||
field_offset -= 0x10;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 Type Name Normalization
|
||||
To produce clean, idiomatic C# source, `api.rs` maps raw CLR type names to C# language aliases:
|
||||
```rust
|
||||
const PRIMITIVE_TYPES: &[(&str, &str)] = &[
|
||||
("System.Void", "void"),
|
||||
("System.Boolean", "bool"),
|
||||
("System.Char", "char"),
|
||||
("System.SByte", "sbyte"),
|
||||
("System.Byte", "byte"),
|
||||
("System.Int16", "short"),
|
||||
("System.UInt16", "ushort"),
|
||||
("System.Int32", "int"),
|
||||
("System.UInt32", "uint"),
|
||||
("System.Int64", "long"),
|
||||
("System.UInt64", "ulong"),
|
||||
("System.Single", "float"),
|
||||
("System.Double", "double"),
|
||||
("System.String", "string"),
|
||||
("System.IntPtr", "IntPtr"),
|
||||
("System.UIntPtr", "UIntPtr"),
|
||||
("System.Object", "object"),
|
||||
("&", "")
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Method RVA Map Generation (`methoddumper.rs`)
|
||||
|
||||
### 8.1 Method Signature Flattening & Collision Resolution
|
||||
In C#, method overloading allows multiple methods to share the same identifier with different parameter signatures. To generate a flat JSON dictionary without key collisions, `methoddumper.rs` implements a collision tracker:
|
||||
|
||||
```rust
|
||||
let method_name = il2cpp.method_get_name(method_info)?;
|
||||
let description = format!("{}{}::{}", class_namespace, class_name, method_name);
|
||||
|
||||
let unique_description = if name_map.contains_key(&description) {
|
||||
let count = duplicates.entry(description.to_string()).or_insert(0);
|
||||
*count += 1;
|
||||
format!("{}_{}", description, count)
|
||||
} else {
|
||||
description
|
||||
};
|
||||
|
||||
name_map.insert(unique_description, pointer - il2cpp.game_assembly.handle as usize);
|
||||
```
|
||||
|
||||
### 8.2 JSON Output Schema (`methods.json`)
|
||||
The resulting file is formatted as a key-value mapping of fully qualified method signatures to hexadecimal RVAs:
|
||||
|
||||
```json
|
||||
{
|
||||
"RPG.Client.AutoScrollRect::.ctor": "0x1febdf0",
|
||||
"RPG.Client.AutoScrollRect::MNOGNALMGPL": "0x1feb700",
|
||||
"RPG.Client.AutoScrollRect::POFLDHKMLAF": "0x1feb7a0",
|
||||
"RPG.Client.AutoScrollRect::SetItemSelectCallback": "0x1feb7f0",
|
||||
"RPG.Client.AutoScrollRect::SnapTo": "0x1feb9c0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Comparative Technical Assessment: In-Process vs. Static Dumpers
|
||||
|
||||
| Dimension | `honkai-dumper` (In-Process) | `Il2CppDumper` (Static Disk) |
|
||||
| :--- | :--- | :--- |
|
||||
| **Execution Vector** | Native DLL injection into running process | Standalone CLI executable analyzing disk files |
|
||||
| **Metadata Protection** | **Immune**: Reads memory after unpacker decrypts metadata | **Fails**: Blocked by custom encryption on `global-metadata.dat` |
|
||||
| **Symbol Stripping** | **Immune**: Locates API table via signature scan in `UnityPlayer.dll` | Requires manual IDA script or pattern search if exports are stripped |
|
||||
| **Anti-Cheat Risk** | **High**: Process injection can trigger game integrity checks/watchdogs | **Zero**: Entirely offline |
|
||||
| **Build Stability** | Fragile: Dependent on fixed jump table index offsets in `functions.rs` | Robust: Parses standard metadata format specifications |
|
||||
| **Output Formats** | `dump.cs` (Headers) and `methods.json` (RVAs) | `dump.cs`, `script.py` (IDA), `il2cpp.h`, `stringliteral.json` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Integration & Cross-Application in Server & Protocol Engineering
|
||||
|
||||
### 10.1 Labeling Disassembly in IDA Pro / Ghidra
|
||||
When reverse engineering stripped Star Rail binaries:
|
||||
1. Open `GameAssembly.dll` in IDA Pro / Ghidra / Binary Ninja.
|
||||
2. In IDA Pro, run an import script:
|
||||
```python
|
||||
import json
|
||||
import ida_name, ida_bytes
|
||||
|
||||
with open("methods.json", "r") as f:
|
||||
methods = json.load(f)
|
||||
|
||||
base = ida_nalt.get_imagebase()
|
||||
for name, rva_str in methods.items():
|
||||
rva = int(rva_str, 16)
|
||||
ea = base + rva
|
||||
# Sanitize name for IDA symbol constraints
|
||||
clean_name = name.replace("::", "__").replace(".", "_")
|
||||
ida_name.set_name(ea, clean_name, ida_name.SN_NOWARN)
|
||||
```
|
||||
3. Instantly transforms thousands of anonymous subroutines (`sub_181FEB700`) into identifiable game logic (`RPG_Client_AutoScrollRect__MNOGNALMGPL`).
|
||||
|
||||
### 10.2 Protocol & Packet Handler Discovery
|
||||
In private server engineering (`LunarCore`, `March7thHoney`, `GaC-hkrpg-go`):
|
||||
- Network packet dispatchers often use obfuscated command handlers.
|
||||
- By searching `dump.cs` for classes implementing network interfaces or containing `CmdId` constants, engineers can pinpoint the exact RVA of packet deserialization routines.
|
||||
- Cross-referencing `methods.json` RVAs with dynamic breakpoints allows live tracing of packet encryption keys, seed exchanges, and payload masking expressions.
|
||||
|
||||
### 10.3 Compliance with Repository Rules (`AGENTS.md`)
|
||||
- `honkai-dumper` is a one-shot diagnostic utility executed in an isolated thread (`thread::spawn`) on injection, writing results and terminating.
|
||||
- In contrast to prohibited persistent runtime probes, it does **not** attach to per-frame update loops or main thread heartbeats, adhering to the principle of zero in-game performance degradation.
|
||||
- No local machine paths, static IPs, or client versions are hardcoded in the codebase. All memory resolution is performed dynamically relative to `GetModuleInformation` bases.
|
||||
@@ -0,0 +1,487 @@
|
||||
# รายงานการวิเคราะห์สถาปัตยกรรมและการทำงานของ LunarCore (Deep-Dive Technical Specification)
|
||||
|
||||
> **เอกสารอ้างอิงทางเทคนิคระดับลึกสำหรับวิศวกรซอฟต์แวร์และโมเดลปัญญาประดิษฐ์ (AI Systems)**
|
||||
> วิเคราะห์จากซอร์สโค้ดของโครงการ: [Melledy/LunarCore](https://github.com/Melledy/LunarCore)
|
||||
> ครอบคลุมสถาปัตยกรรมระบบ, Binary Network Protocol Framing, State Machine, Authoritative vs Client Simulation, การจัดการข้อมูล (Data & Persistence) และ Gameplay Subsystems ทั้งหมด
|
||||
|
||||
---
|
||||
|
||||
## สารบัญ (Table of Contents)
|
||||
1. [ภาพรวมสถาปัตยกรรมระดับสูง (High-Level Architecture)](#1-ภาพรวมสถาปัตยกรรมระดับสูง-high-level-architecture)
|
||||
2. [เลเยอร์เครือข่ายและการเข้ารหัสแพ็กเก็ต (Network & Protocol Framing)](#2-เลเยอร์เครือข่ายและการเข้ารหัสแพ็กเก็ต-network--protocol-framing)
|
||||
- 2.1 วงจร HTTP / Dispatch / Gateway Server
|
||||
- 2.2 โครงสร้าง Binary Packet Framing (Magic & Header Layout)
|
||||
- 2.3 กลไก KCP Transport และ GameSession State Machine
|
||||
3. [วงจรชีวิตของผู้เล่นและการเชื่อมต่อ (Player Lifecycle & Handshake Flow)](#3-วงจรชีวิตของผู้เล่นและการเชื่อมต่อ-player-lifecycle--handshake-flow)
|
||||
4. [โครงสร้างข้อมูลและฐานข้อมูล (Persistence & Morphia ODM)](#4-โครงสร้างข้อมูลและฐานข้อมูล-persistence--morphia-odm)
|
||||
- 4.1 Internal MongoDB vs Standalone MongoDB
|
||||
- 4.2 การแบ่งแยก Collections และ Document Schema
|
||||
5. [ระบบจัดการทรัพยากรเกมและตารางข้อมูล (Game Data & Resource Pipeline)](#5-ระบบจัดการทรัพยากรเกมและตารางข้อมูล-game-data--resource-pipeline)
|
||||
6. [ระบบฉากและ Entity ในโลกเปิด (Scene & Entity Engine)](#6-ระบบฉากและ-entity-ในโลกเปิด-scene--entity-engine)
|
||||
7. [ระบบการต่อสู้ (Battle Orchestration & Reconciliation)](#7-ระบบการต่อสู้-battle-orchestration--reconciliation)
|
||||
8. [ระบบโหมดการเล่นขั้นสูง (Simulated Universe & Challenge Modes)](#8-ระบบโหมดการเล่นขั้นสูง-simulated-universe--challenge-modes)
|
||||
9. [ระบบตัวละคร, คลังสัมภาระ และไอเทม (Avatar & Inventory Subsystem)](#9-ระบบตัวละคร-คลังสัมภาระ-และไอเทม-avatar--inventory-subsystem)
|
||||
- 9.1 Multi-Path Switching (ระบบเปลี่ยนคลาส Trailblazer)
|
||||
- 9.2 ระบบสุ่มค่า Relic (Main Affix & Sub Affix Step Generator)
|
||||
10. [ระบบ Gacha และคำสั่งเซิร์ฟเวอร์ (Gacha, Commands & Plugins)](#10-ระบบ-gacha-และคำสั่งเซิร์ฟเวอร์-gacha-commands--plugins)
|
||||
11. [สรุปข้อสังเกตและคู่มือการนำไปพัฒนาต่อสำหรับ AI](#11-สรุปข้อสังเกตและคู่มือการนำไปพัฒนาต่อสำหรับ-ai)
|
||||
|
||||
---
|
||||
|
||||
## 1. ภาพรวมสถาปัตยกรรมระดับสูง (High-Level Architecture)
|
||||
|
||||
LunarCore ถูกออกแบบเป็น Server Reimplementation ในภาษา Java (JDK 17+) สำหรับเกม *Honkai: Star Rail* โดยแบ่งบทบาทการทำงานออกเป็น **2 โดเมนหลัก** ในระบบเดียว (สามารถรันแยกกระบวนการหรือรันพร้อมกันได้ผ่านแฟล็ก `-dispatch` และ `-game`):
|
||||
|
||||
1. **Dispatch / HTTP Server (พอร์ตเริ่มต้น: TCP 80 หรือ 443 ผ่าน SSL)**:
|
||||
- ทำหน้าที่จำลองระบบ SDK ของ miHoYo / HoYoverse: การลงทะเบียน, ล็อกอิน (OAuth/Password/Cached Token), ตรวจสอบความเสี่ยง (Risk check), ส่งคืนรายการภูมิภาค (Region List) และข้อมูลเกตเวย์ (`query_gateway`)
|
||||
- ใช้เฟรมเวิร์ก **Javalin** (ครอบบน Jetty Server) มีประสิทธิภาพสูงและมี footprint ต่ำ
|
||||
|
||||
2. **Game Server (พอร์ตเริ่มต้น: UDP 23301 ผ่าน KCP Protocol)**:
|
||||
- รองรับการเชื่อมต่อแบบ State-full ด้วย **KCP protocol over UDP** (ใช้ไลบรารี `kcp.highway.Ukcp` บนพื้นฐานของ Netty buffer)
|
||||
- จัดการ State ของผู้เล่น, ฉาก (Scene), ตำแหน่งในแผนที่ (Floor/Position), คำสั่งในเกม (Commands), และระบบการต่อสู้ (Battle Setup & Result Validation)
|
||||
|
||||
3. **Core Dependencies & Libraries**:
|
||||
- **Quickbuf (`us.hebi.quickbuf`)**: เอนจิน Protobuf ประสิทธิภาพสูงพิเศษระดับ zero-allocation ทำงานเร็วกว่าและกินแรมน้อยกว่า Google Protobuf Java มาตรฐาน
|
||||
- **Morphia (`dev.morphia`)**: Object-Document Mapper (ODM) สำหรับติดต่อ MongoDB
|
||||
- **Mongo Java Server (`de.bwaldvogel`)**: เซิร์ฟเวอร์ MongoDB จำลองในตัวที่ทำงานบนหน่วยความจำหรือ H2 Database Backend ทำให้เซิร์ฟเวอร์รันได้ทันทีโดยไม่ต้องติดตั้ง MongoDB ภายนอก
|
||||
- **FastUtil (`it.unimi.dsi.fastutil`)**: ไฮสปีด Primitive Collections (`Int2ObjectMap`, `IntSet`, `IntList`) เพื่อลดภาระ Garbage Collection (GC)
|
||||
- **Reflections (`org.reflections`)**: สแกนหา Controller, Packet Handlers, และ Commands แบบอัตโนมัติ
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["Honkai: Star Rail Game Client"]
|
||||
ClientHTTP["Client Web/SDK Layer"]
|
||||
ClientKCP["Client KCP/UDP Transport"]
|
||||
end
|
||||
|
||||
subgraph LunarCoreServer["LunarCore Runtime"]
|
||||
subgraph HTTPServer["HTTP / Dispatch Server (Javalin/Jetty)"]
|
||||
AuthRoutes["/mdk/shield/api/login<br/>/combo/granter/login"]
|
||||
QueryDispatch["/query_dispatch (Region List)"]
|
||||
QueryGateway["/query_gateway (Base64 Proto)"]
|
||||
end
|
||||
|
||||
subgraph KcpGameServer["Game Server (KcpServer / Netty UDP)"]
|
||||
KcpListener["GameServerKcpListener"]
|
||||
Session["GameSession (State & Crypto)"]
|
||||
PacketRouter["GameServerPacketHandler (CmdId Dispatch)"]
|
||||
|
||||
subgraph Subsystems["Core Game Services"]
|
||||
SceneMgr["Scene & Entity Manager"]
|
||||
BattleSvc["Battle Service (Setup/Validate)"]
|
||||
RogueSvc["Simulated Universe (Rogue Engine)"]
|
||||
GachaSvc["Gacha Service (Hot-reloaded)"]
|
||||
InvSvc["Inventory & Relic Service"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph DataLayer["Persistence & Resources"]
|
||||
Morphia["Morphia ODM"]
|
||||
MongoDB[("MongoDB / Internal H2 Mongo")]
|
||||
GameData["GameData / StarRailData (JSON Excels)"]
|
||||
end
|
||||
end
|
||||
|
||||
ClientHTTP -->|"HTTP GET/POST"| HTTPServer
|
||||
ClientKCP <-->|"KCP UDP Packets"| KcpListener
|
||||
KcpListener --> Session
|
||||
Session --> PacketRouter
|
||||
PacketRouter --> Subsystems
|
||||
HTTPServer --> Morphia
|
||||
Subsystems --> Morphia
|
||||
Morphia --> MongoDB
|
||||
Subsystems --> GameData
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. เลเยอร์เครือข่ายและการเข้ารหัสแพ็กเก็ต (Network & Protocol Framing)
|
||||
|
||||
### 2.1 วงจร HTTP / Dispatch / Gateway Server
|
||||
|
||||
ลำดับการเรียกจากไคลเอนต์ก่อนเข้าเกม:
|
||||
1. `GET /query_dispatch`: ไคลเอนต์ขอรายชื่อเซิร์ฟเวอร์
|
||||
- เซิร์ฟเวอร์ตอบกลับด้วย Protobuf `DispatchRegionData` เข้ารหัสเป็น **Base64 String** บรรจุรายชื่อ Gateway, ชื่อภูมิภาค, Dispatch URL
|
||||
2. `POST /hkrpg_global/mdk/shield/api/login` หรือ `POST /hkrpg_global/mdk/shield/api/verify`:
|
||||
- ไคลเอนต์ส่งบัญชี/รหัสผ่าน หรือโทเค็นเดิม
|
||||
- เซิร์ฟเวอร์สร้าง Account (ถ้าเปิด `autoCreateAccount`) และสร้าง `sessionKey` (SHA-512 Hash)
|
||||
3. `POST /hkrpg_global/combo/granter/login/v2/login`:
|
||||
- ไคลเอนต์นำ `sessionKey` มาแลกเป็น `combo_token`
|
||||
4. `GET /query_gateway`:
|
||||
- ไคลเอนต์ส่งคำขอ Gateway Info สำหรับการเชื่อมต่อไปยังเกมเซิร์ฟเวอร์
|
||||
- เซิร์ฟเวอร์ตอบกลับด้วย Protobuf `Gateserver` เข้ารหัส **Base64 String** มีฟิลด์สำคัญ:
|
||||
- `ip`: IP ของ Game Server
|
||||
- `port`: KCP Port (ค่าตั้งต้น: 23301)
|
||||
- `asset_bundle_url`, `ex_resource_url`, `lua_url`, `ifix_url`: URL สำหรับ Hotfix ทรัพยากรเกม
|
||||
|
||||
### 2.2 โครงสร้าง Binary Packet Framing (Magic & Header Layout)
|
||||
|
||||
ทุกแพ็กเก็ตที่วิ่งผ่าน KCP ระหว่าง Client และ GameServer มีโครงสร้างแบบ Binary Framing ชัดเจนความยาวรวมขั้นต่ำ 16 ไบต์ (Header 12 ไบต์ + Tail 4 ไบต์):
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------+
|
||||
| Offset (Bytes) | Type | Field Name | Description |
|
||||
+-----------------------------------------------------------------------+
|
||||
| 0x00 - 0x03 | uint32 BE | HEADER_CONST | 0x9D74C714 (Magic) |
|
||||
| 0x04 - 0x05 | uint16 BE | cmdId / Opcode | คำสั่ง CmdId (เช่น 11) |
|
||||
| 0x06 - 0x07 | uint16 BE | head_len | Header Length (มักเป็น 0)|
|
||||
| 0x08 - 0x0B | uint32 BE | data_len | ขนาดไบต์ของ Protobuf|
|
||||
| 0x0C - ... | byte[] | proto_data | Protobuf Payload |
|
||||
| ... - End | uint32 BE | TAIL_CONST | 0xD7A152C8 (Magic) |
|
||||
+-----------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
- **Magic Header**: `0x9d74c714` (Big-Endian)
|
||||
- **Magic Tail**: `0xd7a152c8` (Big-Endian)
|
||||
- **Overhead**: คงที่ 16 ไบต์ (ไม่รวมขนาด Payload)
|
||||
|
||||
```java
|
||||
// โครงสร้างการเขียนไบนารีใน BasePacket.java
|
||||
byte[] packet = new byte[16 + protoSize];
|
||||
ProtoSink output = ProtoSink.newInstance(packet, 0, packet.length);
|
||||
this.writeUint32(output, 0x9d74c714); // Magic Header
|
||||
this.writeUint16(output, cmdId); // 2 bytes Opcode
|
||||
this.writeUint16(output, 0); // 2 bytes Head Size
|
||||
this.writeUint32(output, protoSize); // 4 bytes Body Size
|
||||
if (this.data != null) {
|
||||
this.data.writeTo(output); // Serialized Protobuf
|
||||
}
|
||||
this.writeUint32(output, 0xd7a152c8); // Magic Tail
|
||||
```
|
||||
|
||||
### 2.3 กลไก KCP Transport และ GameSession State Machine
|
||||
|
||||
KCP ได้รับการปรับแต่งค่าผ่าน `ChannelConfig` ใน `GameServer.start()`:
|
||||
- `nodelay(true, 40, 2, true)`: โหมด nodelay เต็มรูปแบบ, Interval 40ms, Resend 2, ปิด Congestion Control
|
||||
- `MTU`: 1400 ไบต์
|
||||
- `Sndwnd / Rcvwnd`: 256 / 256 (บัฟเฟอร์การส่งและรับ)
|
||||
- `Timeout`: 30 วินาที
|
||||
- `UseConvChannel`: เปิดใช้งานรหัส Connection Conversation
|
||||
- `AckNoDelay`: เปิดใช้งาน ACK ทันที
|
||||
|
||||
#### State Machine ของ GameSession:
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> WAITING_FOR_TOKEN: KCP Connection Established
|
||||
WAITING_FOR_TOKEN --> WAITING_FOR_LOGIN: Recv PlayerGetTokenCsReq (Auth OK)
|
||||
WAITING_FOR_LOGIN --> ACTIVE: Recv PlayerLoginCsReq
|
||||
ACTIVE --> ACTIVE: Normal Game Packets & Heartbeat
|
||||
ACTIVE --> INACTIVE: Disconnect / Timeout / Kicked
|
||||
WAITING_FOR_TOKEN --> INACTIVE: Bad Token / Full Server
|
||||
INACTIVE --> [*]: Cleanup & Save to DB
|
||||
```
|
||||
|
||||
1. **`WAITING_FOR_TOKEN`**: รับได้เฉพาะ `PlayerHeartBeatCsReq` และ `PlayerGetTokenCsReq` เท่านั้น หากส่งแพ็กเก็ตอื่นจะถูกทิ้งทันที
|
||||
2. **`WAITING_FOR_LOGIN`**: ตรวจสอบบัญชีเสร็จแล้ว รอรับ `PlayerLoginCsReq`
|
||||
3. **`ACTIVE`**: เซสชันพร้อมทำงานเต็มรูปแบบ สามารถประมวลผลคำสั่งในเกมได้ทั้งหมด
|
||||
4. **`INACTIVE`**: เซสชันถูกตัด หลุดจากเซิร์ฟเวอร์ จะบันทึกสถานะผู้เล่นลงฐานข้อมูลทันที
|
||||
|
||||
---
|
||||
|
||||
## 3. วงจรชีวิตของผู้เล่นและการเชื่อมต่อ (Player Lifecycle & Handshake Flow)
|
||||
|
||||
ลำดับการแลกเปลี่ยนแพ็กเก็ต (Packet Handshake) ขณะล็อกอินเข้าสู่ตัวเกม:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor Client as Game Client
|
||||
participant KCP as GameServer (KCP)
|
||||
participant Session as GameSession
|
||||
participant DB as MongoDB
|
||||
|
||||
Client->>KCP: KCP Connect Handshake
|
||||
KCP->>Session: Create Session (State = WAITING_FOR_TOKEN)
|
||||
|
||||
Client->>Session: PlayerGetTokenCsReq (AccountUid, Token)
|
||||
Session->>DB: Query Account by Uid & Validate Token
|
||||
Session->>DB: Query Player (or create new if null)
|
||||
Session->>Session: player.onLogin() (Load Avatars, Inventory, Lineup)
|
||||
Session->>Session: State = WAITING_FOR_LOGIN
|
||||
Session-->>Client: PlayerGetTokenScRsp (Uid, BlackInfo)
|
||||
|
||||
Client->>Session: PlayerLoginCsReq
|
||||
Session->>Session: State = ACTIVE
|
||||
Session-->>Client: PlayerLoginScRsp (BasicInfo, ServerTime, Stamina)
|
||||
Session-->>Client: StaminaInfoScNotify
|
||||
|
||||
Client->>Session: PlayerLoginFinishCsReq
|
||||
Session-->>Client: PlayerLoginFinishScRsp
|
||||
Session-->>Client: GetArchiveDataScRsp
|
||||
Session-->>Client: BattlePassInfoNotify
|
||||
Session-->>Client: ContentPackageGetDataScRsp
|
||||
|
||||
Client->>Session: GetCurSceneInfoCsReq
|
||||
Session-->>Client: GetCurSceneInfoScRsp (SceneInfo, Entities, Lineup)
|
||||
|
||||
Client->>Session: EnterSceneCsReq (Teleport / Map Loaded)
|
||||
Session-->>Client: EnterSceneScRsp
|
||||
Session-->>Client: SceneEntityMoveScNotify (Spawn Avatar in Scene)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. โครงสร้างข้อมูลและฐานข้อมูล (Persistence & Morphia ODM)
|
||||
|
||||
### 4.1 Internal MongoDB vs Standalone MongoDB
|
||||
LunarCore รองรับสองโหมดการทำงานของฐานข้อมูลผ่าน `Config.DatabaseInfo`:
|
||||
- **โหมดภายนอก (External MongoDB)**: ต่อไปยัง MongoDB Server ปกติ (เช่น `mongodb://localhost:27017`)
|
||||
- **โหมดภายใน (Internal MongoDB Server)**: เริ่มต้นเซิร์ฟเวอร์จำลองด้วย `de.bwaldvogel.mongo.MongoServer` โดยเลือก Backend ได้ 2 รูปแบบ:
|
||||
1. `H2Backend`: บันทึกลงดิสก์เป็นไฟล์ `database.mv` (Persist ถาวร)
|
||||
2. `MemoryBackend`: รันใน RAM เท่านั้น (ข้อมูลหายเมื่อปิดเซิร์ฟเวอร์)
|
||||
|
||||
### 4.2 การแบ่งแยก Collections และ Document Schema
|
||||
|
||||
| Collection | Entity Class | Primary Key | Key Indexes | หน้าที่และรายละเอียดฟิลด์สำคัญ |
|
||||
|---|---|---|---|---|
|
||||
| `accounts` | `Account` | `String _id` | `token`, `sessionKey` | จัดเก็บข้อมูลล็อกอิน, comboToken, permissions (`Set<String>`) |
|
||||
| `players` | `Player` | `int _id (uid)` | `accountUid` | ข้อมูลหลัก: ชื่อ, เลเวล, ค่าเงิน (`scoin`, `hcoin`, `mcoin`), Stamina, พิกัดตำแหน่งฉากล่าสุด (`planeId`, `floorId`, `pos`, `rot`) |
|
||||
| `avatars` | `GameAvatar` | `ObjectId _id` | `ownerUid` | ตัวละครที่ครอบครอง: `avatarId`, `level`, `promotion`, `rank` (Eidolon), `skills` (Tree points), `equips` |
|
||||
| `items` | `GameItem` | `ObjectId _id` | `ownerUid`, `equipAvatarId` | ไอเทมทุกประเภท: Relics (Main/Sub affixes), Light Cones (level, rank), Materials, Currencies |
|
||||
| `lineups` | `PlayerLineup` | `ObjectId _id` | `ownerUid` | จัดทีม 4 ตำแหน่ง, ผู้นำทีม (`leader`), สกิลพอยต์ (`mp`) |
|
||||
| `extra_lineups` | `PlayerExtraLineup` | `ObjectId _id` | `ownerUid` | ทีมพิเศษสำหรับ Memory of Chaos, Simulated Universe, Trial |
|
||||
| `challenges` | `ChallengeHistory` | `ObjectId _id` | `ownerUid` | บันทึกสถิติด่าน Forgotten Hall / Pure Fiction (ดาวที่ได้, รอบที่ใช้) |
|
||||
| `mail` | `Mail` | `ObjectId _id` | `ownerUid` | จดหมายในเกม: หัวข้อ, เนื้อหา, วันหมดอายุ, ของแนบ (`attachments`) |
|
||||
| `roguetalents`| `RogueTalentData` | `ObjectId _id` | `ownerUid` | อัปเกรดแผนผังพรสวรรค์ของ Simulated Universe |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **การแยกความสัมพันธ์แบบ Loose Coupling ใน Inventory & Avatar:**
|
||||
> ฟิลด์ `equips` ในคลาส `AvatarData` ถูกตั้งค่าเป็น `transient` โดยที่ตัวเก็บข้อมูลจริงคือ `GameItem` ที่มีฟิลด์ `@Indexed ObjectId equipAvatarId` เมื่อโหลดเกม ระบบจะ query ไอเทมทั้งหมดในคอลเลกชัน `items` ที่มี `ownerUid` ตรงกัน แล้วเชื่อมโยงเข้ากับอ็อบเจกต์ตัวละครในหน่วยความจำ วิธีนี้ป้องกันปัญหา Data Desync ระหว่างช่องเก็บของและตัวละครอย่างสมบูรณ์
|
||||
|
||||
---
|
||||
|
||||
## 5. ระบบจัดการทรัพยากรเกมและตารางข้อมูล (Game Data & Resource Pipeline)
|
||||
|
||||
ข้อมูลเกมทั้งหมด (Game Data) อ่านมาจากไฟล์ JSON ของเกม ซึ่งดึงมาจากคลังทรัพยากร เช่น `StarRailData` ของ Dimbreath:
|
||||
- ไดเรกทอรี: `resources/ExcelOutput/`
|
||||
- ไดเรกทอรีแผนที่: `resources/Config/LevelOutput/RuntimeFloor/`
|
||||
|
||||
### วงจรการโหลดข้อมูล (`ResourceLoader.java`):
|
||||
1. **Reflection Scan**: สแกนหาทุกคลาสที่สืบทอดมาจาก `GameResource` และมี Annotation `@ResourceType`
|
||||
2. **Priority Ordering**: จัดลำดับการโหลดตาม `loadPriority` เพื่อให้ข้อมูลพื้นฐาน (เช่น TextMap, Rarity, Stats) โหลดเสร็จก่อนข้อมูลที่ขึ้นต่อกัน (เช่น Avatars, Relics, Stages)
|
||||
3. **Custom Deserializers**: ใช้ Gson ร่วมกับ TypeAdapter เฉพาะ:
|
||||
- `LunarCoreDoubleDeserializer`: ป้องกันปัญหาความแม่นยำของเลข Floating-point
|
||||
- `LunarCoreHashDeserializer`: แปลง Hash String ใน ExcelBin เป็น Integer/Long Key
|
||||
4. **Depot Caching (`GameDepot.java`)**: สร้างแคชสำหรับค้นหาอย่างรวดเร็ว เช่น:
|
||||
- ตารางน้ำหนักการสุ่ม Main Affix ของ Relic แยกตามชิ้นส่วน
|
||||
- ตารางการสุ่ม Sub Affix และอัตราการเติบโตเมื่ออัปเกรด
|
||||
- ตาราง Drop Group และ Monster Wave Templates
|
||||
|
||||
---
|
||||
|
||||
## 6. ระบบฉากและ Entity ในโลกเปิด (Scene & Entity Engine)
|
||||
|
||||
ระบบฉากของ LunarCore ถูกควบคุมด้วยคลาส `Scene.java` โดยอิงตามแนวคิดของเกม:
|
||||
- **Plane (`MazePlaneExcel`)**: ขอบเขตของแผนที่หลัก (เช่น Space Station, Jarilo-VI, Xianzhou Luofu, Penacony)
|
||||
- **Floor (`FloorInfo`)**: ชั้นย่อยในฉากนั้นๆ (โหลดมาจาก `LevelOutput/RuntimeFloor/*.json`)
|
||||
- **Groups (`GroupInfo`)**: กลุ่มของ Entity ที่อยู่ในฉาก (จุดเกิดของมอนสเตอร์, หีบสมบัติ, เสาฟื้นฟูเลือด, ประตูมิติ)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Scene {
|
||||
-Player player
|
||||
-int planeId
|
||||
-int floorId
|
||||
-Int2ObjectMap~GameEntity~ entities
|
||||
-IntSet avatarEntityIds
|
||||
+loadGroup(groupId)
|
||||
+addEntity(entity)
|
||||
+removeEntity(entity)
|
||||
+onTick()
|
||||
}
|
||||
|
||||
class GameEntity {
|
||||
<<interface>>
|
||||
+getEntityId() int
|
||||
+getPos() Position
|
||||
+getRot() Position
|
||||
+toProto() SceneEntityInfo
|
||||
}
|
||||
|
||||
class GameAvatar {
|
||||
-int avatarId
|
||||
-int currentHp
|
||||
-int currentSp
|
||||
}
|
||||
|
||||
class EntityMonster {
|
||||
-int monsterId
|
||||
-int stageId
|
||||
-int worldLevel
|
||||
-boolean isAlive
|
||||
}
|
||||
|
||||
class EntityProp {
|
||||
-int propId
|
||||
-PropType propType
|
||||
-PropState propState
|
||||
}
|
||||
|
||||
class EntityNpc {
|
||||
-int npcId
|
||||
-int talkId
|
||||
}
|
||||
|
||||
GameEntity <|.. GameAvatar
|
||||
GameEntity <|.. EntityMonster
|
||||
GameEntity <|.. EntityProp
|
||||
GameEntity <|.. EntityNpc
|
||||
Scene o-- GameEntity
|
||||
```
|
||||
|
||||
### กลไก Entity ID และ World Sync:
|
||||
- Entity แต่ละตัวในฉากจะได้ `entityId` แบบ Dynamic ซึ่งเริ่มต้นรันเลขเพิ่มขึ้นเรื่อยๆ ต่อฉาก
|
||||
- ผู้เล่นส่งพิกัดการเคลื่อนที่ผ่านแพ็กเก็ต `SceneEntityMoveCsReq`
|
||||
- เซิร์ฟเวอร์อัปเดตพิกัด `pos` และ `rot` ของ `Player` ในหน่วยความจำ และกระจายแพ็กเก็ต `SceneEntityMoveScNotify`
|
||||
- **ระบบ Space Anchor (เสาฮีล)**: ค้นหาเสาที่ใกล้ที่สุดในฉากผ่าน `Scene.getNearestSpring()` เพื่อใช้เป็นจุดฟื้นฟู HP/MP หรือจุดวาปกลับเมื่อพ่ายแพ้ในการต่อสู้
|
||||
|
||||
---
|
||||
|
||||
## 7. ระบบการต่อสู้ (Battle Orchestration & Reconciliation)
|
||||
|
||||
จุดเด่นของสถาปัตยกรรม Honkai: Star Rail คือ **การจำลองลูปการต่อสู้เทิร์นเบสเกิดขึ้นที่ Client-side ทั้งหมด** โดยเซิร์ฟเวอร์ทำหน้าที่เป็น Authoritative Orchestrator ในการเตรียมข้อมูลด่าน และตรวจสอบผลลัพธ์หลังสู้เสร็จ
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor Client as Game Client
|
||||
participant BattleSvc as BattleService
|
||||
participant Battle as Battle Instance
|
||||
participant DropSvc as DropService
|
||||
|
||||
Note over Client,BattleSvc: 1. การเปิดฉากต่อสู้ในโลกเปิด
|
||||
Client->>BattleSvc: SceneCastSkillCsReq (ใช้วิชา Technique หรือโจมตีก่อน)
|
||||
alt เป็นมอนสเตอร์โจมตีผู้เล่น
|
||||
BattleSvc->>Battle: สร้าง Battle พร้อม Ambush Buff (1000102)
|
||||
else เป็นผู้เล่นโจมตีก่อน
|
||||
BattleSvc->>Battle: สร้าง Battle พร้อม Element Weakness Debuff ของตัวละคร
|
||||
end
|
||||
|
||||
BattleSvc->>Battle: รวบรวม Monster Waves จาก StageExcel
|
||||
BattleSvc->>Battle: รวบรวมค่าสถานะ HP/SP/Buffs ปัจจุบันของทีม
|
||||
BattleSvc-->>Client: SceneCastSkillScRsp (SceneBattleInfo Proto)
|
||||
|
||||
Note over Client: 2. ไคลเอนต์จำลองการต่อสู้แบบเทิร์นเบสในตัวเครื่อง
|
||||
|
||||
Note over Client,BattleSvc: 3. การรายงานผลลัพธ์การต่อสู้
|
||||
Client->>BattleSvc: PVEBattleResultCsReq (EndStatus, BattleStatistics)
|
||||
|
||||
alt ผู้เล่นชนะ (BATTLE_END_WIN)
|
||||
BattleSvc->>DropSvc: คำนวณรางวัลและของดรอป (Drops)
|
||||
BattleSvc->>BattleSvc: หักค่าพลังกาย Stamina (ถ้ามี)
|
||||
BattleSvc->>Client: ลบมอนสเตอร์ตัวนั้นออกจาก Scene
|
||||
else ผู้เล่นแพ้ (BATTLE_END_LOSE)
|
||||
BattleSvc->>BattleSvc: รีเซ็ต HP ตัวละครเหลือ 20%
|
||||
BattleSvc->>Client: วาปตัวละครกลับไปเสา Space Anchor ที่ใกล้ที่สุด
|
||||
else ผู้เล่นกดยอมแพ้ (BATTLE_END_QUIT)
|
||||
BattleSvc->>Client: คืนค่าสถานะเดิมและวาปกลับเสา
|
||||
end
|
||||
|
||||
BattleSvc->>BattleSvc: อัปเดต HP และ SP สุทธิของตัวละครลง DB
|
||||
BattleSvc-->>Client: PVEBattleResultScRsp (รางวัลที่ได้รับ, EXP)
|
||||
```
|
||||
|
||||
### รายละเอียดโครงสร้าง `SceneBattleInfo`:
|
||||
- `battleId`: เลขรันประจำการต่อสู้ของผู้เล่น
|
||||
- `stageId`: ID ด่านจาก `StageExcel`
|
||||
- `logicRandomSeed`: เลขสุ่ม Seed สำหรับให้ Client ใช้คำนวณ Random Roll ในเทิร์น
|
||||
- `monsterWaveList`: รายการ Wave ของมอนสเตอร์ (Level, Monster ID, Elite Affixes)
|
||||
- `battleAvatarList`: ข้อมูลตัวละคร 4 ตัวในทีม (Level, Promotion, Relics, Light Cone, ค่า HP/SP ปัจจุบัน)
|
||||
- `buffList`: บัฟที่ติดมาจากโลกเปิด เช่น ยาบัฟอาหาร, วิชา Technique ก่อนเข้าสู้
|
||||
- `battleTargetInfo`: เงื่อนไข 3 ดาว สำหรับด่าน Forgotten Hall / Pure Fiction
|
||||
|
||||
---
|
||||
|
||||
## 8. ระบบโหมดการเล่นขั้นสูง (Simulated Universe & Challenge Modes)
|
||||
|
||||
### 8.1 Simulated Universe (Rogue Engine)
|
||||
ระบบจำลองจักรวาล (Simulated Universe) ถูกแยกการทำงานไว้ในแพ็กเกจ `emu.lunarcore.game.rogue`:
|
||||
- **`RogueInstance`**: ตัวแทนของรอบการเล่นปัจจุบัน (Run State)
|
||||
- เก็บค่า `RogueRoomData`: ห้องต่อสู้, ห้อง Elite, ห้องเหตุการณ์ (Event), ห้องพัก (Respite), ห้องบอส
|
||||
- จัดการสกิลพอยต์จักรวาล Cosmic Fragments (Item ID: 31)
|
||||
- **ระบบเลือกพร (Blessings / Buffs)**:
|
||||
- ควบคุมผ่าน `RogueBuffSelectMenu`
|
||||
- เมื่อชนะห้องต่อสู้ เซิร์ฟเวอร์สุ่มพรตาม Path ที่เลือก และส่ง `AddRogueBuffScNotify`
|
||||
- **ระบบวัตถุหายาก (Curios / Miracles)**:
|
||||
- ควบคุมผ่าน `RogueMiracleSelectMenu` ให้ผู้เล่นเลือกรับ Curio
|
||||
- **ระบบเหตุการณ์โต้ตอบ (Interactive Events)**:
|
||||
- ควบคุมผ่าน `RogueEventManager` และ `RogueEventInstance`
|
||||
- ไคลเอนต์ส่ง `SelectRogueCommonDialogueOptionCsReq` เมื่อผู้เล่นเลือกตัวเลือกในเหตุการณ์ เซิร์ฟเวอร์คำนวณผลกระทบ (ได้เงิน, เสียเลือด, ได้พร) แล้วส่งผลลัพธ์กลับ
|
||||
|
||||
### 8.2 Forgotten Hall & Pure Fiction (Challenge Subsystem)
|
||||
- ควบคุมผ่าน `ChallengeManager.java` และ `ChallengeInstance.java`
|
||||
- จัดการรอบ (Rounds Limit) เช่น กำหนดรอบเหลือไม่ต่ำกว่า 20 เพื่อรับ 3 ดาว
|
||||
- โหลด Lineup แบบแยกอิสระ (ทีมครึ่งแรก และทีมครึ่งหลัง) ผ่าน `PlayerExtraLineup`
|
||||
- บันทึกประวัติและดาวที่ทำได้ลงคอลเลกชัน `challenges`
|
||||
|
||||
---
|
||||
|
||||
## 9. ระบบตัวละคร, คลังสัมภาระ และไอเทม (Avatar & Inventory Subsystem)
|
||||
|
||||
### 9.1 Multi-Path Switching (ระบบเปลี่ยนคลาส Trailblazer)
|
||||
ใน Honkai: Star Rail ตัวเอก (Trailblazer) สามารถเปลี่ยนเส้นทาง Path ได้ (ทำลายล้างกายภาพ, อนุรักษ์ไฟ, ประสานจินตภาพ, ล่าสังหารน้ำแข็ง):
|
||||
- LunarCore ออกแบบผ่านคลาส `AvatarMultiPath.java`
|
||||
- เมื่อผู้เล่นกดเปลี่ยน Path ไคลเอนต์จะส่ง `SetAvatarPathCsReq`:
|
||||
1. เซิร์ฟเวอร์ดึงข้อมูลจาก `MultiplePathAvatarExcel`
|
||||
2. โหลด `AvatarMultiPath` ที่บันทึกเลเวลสกิลของ Path นั้นๆ
|
||||
3. สลับข้อมูล `skills`, `equips`, และโมเดลเพศ (`gender`) ใน `GameAvatar`
|
||||
4. บันทึกลงตาราง `curAvatarPaths` ในเอกสาร `players`
|
||||
5. ส่งแพ็กเก็ต `PacketAvatarPathChangedNotify` และ `PacketPlayerSyncScNotify` ไปยัง Client
|
||||
|
||||
### 9.2 ระบบสุ่มค่า Relic (Main Affix & Sub Affix Step Generator)
|
||||
การสร้าง Relic ใน `GameItem.java` อิงตามหลักคณิตศาสตร์ของเกมจริง:
|
||||
|
||||
```
|
||||
[Main Affix Allocation]
|
||||
1. ตรวจสอบชิ้นส่วนของ Relic (Head, Hands, Body, Feet, Planar Sphere, Link Rope)
|
||||
2. ดึงตาราง MainAffixGroup จาก RelicExcel
|
||||
3. สุ่มค่า Stat หลักตาม Weight Distribution ใน GameDepot
|
||||
|
||||
[Sub Affix Allocation]
|
||||
1. คำนวณจำนวน Sub Affix เริ่มต้นตาม Rarity:
|
||||
Base Count = clamp(Rarity - 2, 0, 3) + randomRange(0, 1)
|
||||
(ตัวอย่างเช่น Relic 5 ดาว จะได้ Sub Affix เริ่มต้น 3 หรือ 4 แถว)
|
||||
2. สุ่มเลือก Stat รองโดยห้ามซ้ำกับ Main Affix และห้ามซ้ำกับ Sub Affix อื่นที่มีอยู่แล้ว
|
||||
3. สำหรับแต่ละ Sub Affix จะสุ่มค่า Step (Roll Quality):
|
||||
Step มีค่า 0, 1, หรือ 2 (แทนค่า Small, Medium, Large Roll)
|
||||
```
|
||||
|
||||
เมื่อมีการอัปเกรด Relic ทุกๆ 3 เลเวล (`HandlerExpUpRelicCsReq`):
|
||||
- หากจำนวน Sub Affix ยังไม่ครบ 4 แถว ระบบจะเพิ่มแถวใหม่
|
||||
- หากครบ 4 แถวแล้ว ระบบจะสุ่มเลือก 1 แถวที่มีอยู่เพื่อเพิ่ม `count` และสุ่มค่า `step` ใหม่
|
||||
|
||||
---
|
||||
|
||||
## 10. ระบบ Gacha และคำสั่งเซิร์ฟเวอร์ (Gacha, Commands & Plugins)
|
||||
|
||||
### 10.1 สถาปัตยกรรมตู้กาชา (`GachaService.java`)
|
||||
- **การ Hot-Reload ด้วย `WatchService`**: ตัวบริการลงทะเบียน File Watcher ตรวจจับไฟล์ `data/Banners.json` หากผู้ดูแลแก้ไขไฟล์ระบบจะ reload ตู้กาชาทันทีโดยไม่ต้องรีสตาร์ตเซิร์ฟเวอร์
|
||||
- **การคำนวณการันตี (Pity Mechanics)**:
|
||||
- 5-Star Soft Pity: อัตราพื้นฐาน 0.6% เมื่อเปิดถึงโรลที่ 74 อัตราจะเพิ่มขึ้นแบบเส้นตรงจนถึง 100% ที่โรล 90
|
||||
- ระบบหลุดเรท 50/50: หากเปิดได้ 5 ดาวที่ไม่ใช่หน้าตู้ ตัวแปร `isFeatured` จะถูกเซ็ตเป็น `true` สำหรับ 5 ดาวรอบถัดไป
|
||||
- การันตี 4 ดาว: ทุกๆ 10 โรล
|
||||
|
||||
### 10.2 เครื่องมือคำสั่งเซิร์ฟเวอร์ (Command Engine)
|
||||
- ทำงานผ่านทั้งคอนโซลเซิร์ฟเวอร์ และช่องทางแชตในเกม
|
||||
- ในเกมจะมีบัญชีปลอมชื่อ **"Server" (UID: 99)** ปรากฏในรายชื่อเพื่อนโดยอัตโนมัติ
|
||||
- ผู้เล่นส่งข้อความที่ขึ้นต้นด้วย `/` หรือ `!` (เช่น `/give 1001`, `/heal`, `/scene 20001 20001001`)
|
||||
- `ChatManager` จะดักจับข้อความและส่งต่อให้ `CommandManager.invoke(player, message)`
|
||||
- ตรวจสอบสิทธิ์ผ่านระบบ RBAC: `account.hasPermission(command.permission())`
|
||||
|
||||
### 10.3 ระบบปลั๊กอิน (Plugin Architecture)
|
||||
- รองรับการโหลดไฟล์ JAR จากโฟลเดอร์ `plugins/`
|
||||
- แต่ละปลั๊กอินมี `plugin.json` ระบุ Main Class, Version, Author
|
||||
- โหลดผ่าน `URLClassLoader` อิสระ
|
||||
- มี Lifecycle Hooks: `onLoad()`, `onEnable()`, `onDisable()`
|
||||
|
||||
---
|
||||
|
||||
## 11. สรุปข้อสังเกตและคู่มือการนำไปพัฒนาต่อสำหรับ AI
|
||||
|
||||
หากโมเดล AI ตัวอื่นต้องการนำข้อมูลสถาปัตยกรรมนี้ไปเขียนโค้ดต่อ หรือสร้างเซิร์ฟเวอร์ใหม่ในภาษาอื่น (เช่น Go, Rust, C#):
|
||||
|
||||
1. **Protocol Framing**: ห้ามลืมกรอบ 16 ไบต์เด็ดขาด (`0x9D74C714` นำหน้า และ `0xD7A152C8` ปิดท้าย) หากไบต์ขาดหรือผิดแฮนด์เชกจะค้าง
|
||||
2. **Gateway Encoding**: แพ็กเก็ต HTTP Gateway (`/query_dispatch` และ `/query_gateway`) ต้องแปลงเป็น Protobuf ไบนารีก่อน แล้วค่อยครอบด้วย **Base64 String** ส่งเป็นเนื้อหา HTTP Response
|
||||
3. **Turn-based Battle Paradigm**: เซิร์ฟเวอร์ไม่ต้องสร้าง AI คำนวณดาเมจต่อเทิร์น แต่ต้องเตรียม `SceneBattleInfo` ให้ครบถ้วน (ตัวละคร, มอนสเตอร์แต่ละเวฟ, บัฟ, ค่าพลังกาย) แล้วรอรับผลสรุป `PVEBattleResultCsReq` จาก Client
|
||||
4. **Data Synchronization (Syncable)**: หลังการเปลี่ยนแปลงไอเทมหรือตัวละคร ต้องส่ง `PacketPlayerSyncScNotify` เสมอ เพื่อให้หน้าต่าง UI ของเกมแสดงผลตรงกับฐานข้อมูล
|
||||
5. **Session States**: รักษา State Flow ให้เคร่งครัดตามลำดับ `WAITING_FOR_TOKEN` -> `WAITING_FOR_LOGIN` -> `ACTIVE` อย่าตอบรับคำสั่งเกมก่อนเซสชันจะอยู่ในสถานะ `ACTIVE`
|
||||
@@ -0,0 +1,358 @@
|
||||
# รายงานการวิเคราะห์สถาปัตยกรรมและการทำงานของ March7thHoney (Deep-Dive Technical Specification)
|
||||
|
||||
> **เอกสารอ้างอิงทางเทคนิคระดับลึกสำหรับวิศวกรซอฟต์แวร์และโมเดลปัญญาประดิษฐ์ (AI Systems)**
|
||||
> วิเคราะห์จากซอร์สโค้ดของโครงการ: [Mar7thLover/March7thHoney-OpenSource](https://github.com/Mar7thLover/March7thHoney-OpenSource)
|
||||
> ครอบคลุมสถาปัตยกรรม .NET 10, Roslyn Source Generators (NativeAOT), KcpSharp, SQLite/MySQL Storage Pipeline, Lua Client Injection, Calyx/FreeSR Scenario Injection, Muip GM API และโหมดเกมเวอร์ชันใหม่ (Apocalyptic Shadow, Divergent Universe, Currency War)
|
||||
|
||||
---
|
||||
|
||||
## สารบัญ (Table of Contents)
|
||||
1. [ภาพรวมสถาปัตยกรรมระดับสูง (High-Level Architecture & Tech Stack)](#1-ภาพรวมสถาปัตยกรรมระดับสูง-high-level-architecture--tech-stack)
|
||||
2. [การออกแบบ NativeAOT และ Roslyn Source Generators](#2-การออกแบบ-nativeaot-และ-roslyn-source-generators)
|
||||
3. [เลเยอร์เครือข่ายและการเข้ารหัสขั้นสูง (Network, KcpSharp & Crypto)](#3-เลเยอร์เครือข่ายและการเข้ารหัสขั้นสูง-network-kcpsharp--crypto)
|
||||
- 3.1 KcpSharp Transport และการจัดการบัฟเฟอร์ ArrayPool
|
||||
- 3.2 Binary Packet Framing & Buffer Overflow Protection
|
||||
- 3.3 Ec2b Key Derivation และ MT19937 XOR Key Pipeline
|
||||
4. [กลไกการฉีดสคริปต์เข้าไคลเอนต์ (Client Lua Script Injection Engine)](#4-กลไกการฉีดสคริปต์เข้าไคลเอนต์-client-lua-script-injection-engine)
|
||||
5. [ระบบจัดเก็บข้อมูลและการจำลองฐานข้อมูล (Persistence & Database Pipeline)](#5-ระบบจัดเก็บข้อมูลและการจำลองฐานข้อมูล-persistence--database-pipeline)
|
||||
- 5.1 Dual-Engine: SQLite & MySQL
|
||||
- 5.2 In-Memory Dirty Cache & Asynchronous Periodic Flusher
|
||||
6. [ระบบแคชทรัพยากรเกมความเร็วสูง (High-Speed Binary Resource Cache)](#6-ระบบแคชทรัพยากรเกมความเร็วสูง-high-speed-binary-resource-cache)
|
||||
7. [ระบบการต่อสู้และการแทรกแซงสถานการณ์ (Battle & Calyx Override Engine)](#7-ระบบการต่อสู้และการแทรกแซงสถานการณ์-battle--calyx-override-engine)
|
||||
- 7.1 ลูปการต่อสู้หลัก
|
||||
- 7.2 Calyx Override & FreeSR JSON Scenario Injection
|
||||
8. [ระบบโหมดการเล่นขั้นสูง (Modern End-Game Modes)](#8-ระบบโหมดการเล่นขั้นสูง-modern-end-game-modes)
|
||||
- 8.1 ChallengePeak (Apocalyptic Shadow & Anomaly Arbitration)
|
||||
- 8.2 Divergent Universe & Unknowable Domain
|
||||
- 8.3 Currency War & GridFight (Aetherium Wars)
|
||||
9. [การจัดการระยะไกลและหน้าต่างควบคุม (Muip Protocol & Admin Dashboard)](#9-การจัดการระยะไกลและหน้าต่างควบคุม-muip-protocol--admin-dashboard)
|
||||
- 9.1 Muip GM Protocol (RSA-Encrypted Remote Command Execution)
|
||||
- 9.2 Built-in Embedded Web Admin & Registration Portal
|
||||
- 9.3 Cluster Deployment via ServerExchange Secret
|
||||
10. [ตารางเปรียบเทียบเชิงลึก: LunarCore vs March7thHoney](#10-ตารางเปรียบเทียบเชิงลึก-lunarcore-vs-march7thhoney)
|
||||
11. [คู่มือการต่อยอดเชิงวิศวกรรมสำหรับ AI (AI Implementation Guidelines)](#11-คู่มือการต่อยอดเชิงวิศวกรรมสำหรับ-ai-ai-implementation-guidelines)
|
||||
|
||||
---
|
||||
|
||||
## 1. ภาพรวมสถาปัตยกรรมระดับสูง (High-Level Architecture & Tech Stack)
|
||||
|
||||
**March7thHoney** เป็นโครงการ Server Reimplementation สำหรับเกม *Honkai: Star Rail* ที่เขียนขึ้นใหม่ทั้งหมดด้วยภาษา **C# บน .NET 10** โดยมุ่งเน้นที่:
|
||||
1. **ประสิทธิภาพสูงสุด (Extreme Performance)**: ลด Allocation ของหน่วยความจำผ่าน `Span<T>`, `Memory<T>`, `ArrayPool<byte>`, และหลีกเลี่ยง Reflection
|
||||
2. **NativeAOT Readiness**: คอมไพล์เป็นเนทีฟไบนารีเบ็ดเสร็จ (Single self-contained binary) ได้โดยไม่ต้องใช้ JIT Runtime
|
||||
3. **ฟีเจอร์เวอร์ชันใหม่ (Modern Game Modes)**: รองรับคอนเทนต์เวอร์ชันใหม่ (2.x ถึง 4.x) เช่น Apocalyptic Shadow, Divergent Universe, Currency War, Penacony Clockie Dial, Critter Pick (Cat Cake Catch)
|
||||
4. **ความสามารถในการปรับแต่ง (Developer & Testing Friendly)**: สามารถแทรกแซงการต่อสู้ผ่านไฟล์สคริปต์ FreeSR JSON (`CalyxOverrideManager`) และมีระบบเว็บแอดมิน Dashboard ในตัว
|
||||
|
||||
### โครงสร้างโมดูลย่อย (Solution Structure):
|
||||
- `Program/`: จุดเริ่มต้นเซิร์ฟเวอร์ (`EntryPoint.cs`), LifeCycle Management, CLI Console, การจัดการล็อก
|
||||
- `GameServer/`: บริการเกม KCP, GameSession/Connection, ตัวจัดการเกม (50+ Game Managers), ตัวถอดรหัสแพ็กเก็ต (51 หมวดหมู่)
|
||||
- `WebServer/`: ASP.NET Core Kestrel (Dispatch, Gateway, Muip API, Web Admin, Registration, SDK Shop)
|
||||
- `Command/`: เครื่องมือคำสั่งเซิร์ฟเวอร์แบบ Compile-time Registry
|
||||
- `Common/`: ตัวแบบข้อมูลหลัก, คอนฟิก, โมเดลฐานข้อมูล, รหัสข้อผิดพลาด และเครื่องมือยูทิลิตี
|
||||
- `KcpSharp/`: ไคลเอนต์/เซิร์ฟเวอร์ KCP แบบ Pure C# Zero-allocation
|
||||
- `Proto/`: Google Protobuf C# Generated Classes
|
||||
- `SourceGen/`: C# Roslyn Incremental Source Generators สำหรับสร้างโค้ดแบบไร้ Reflection
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Client["HSR Game Client (PC / Android / iOS)"]
|
||||
HTTPClient["HTTP/HTTPS Requests"]
|
||||
KCPClient["KCP/UDP Traffic"]
|
||||
end
|
||||
|
||||
subgraph March7thHoney[".NET 10 Server Architecture"]
|
||||
subgraph WebServerModule["WebServer (ASP.NET Core Kestrel)"]
|
||||
DispatchCtrl["DispatchRoutes (/query_dispatch)"]
|
||||
GatewayCtrl["GateServerRoutes (/query_gateway)"]
|
||||
AdminCtrl["AdminRoutes (Embedded Dashboard)"]
|
||||
MuipCtrl["MuipServerRoutes (/muip/*)"]
|
||||
ClusterCtrl["ServerExchangeRoutes (Distributed Sync)"]
|
||||
end
|
||||
|
||||
subgraph KcpNetworkLayer["KCP Transport (KcpSharp)"]
|
||||
KcpListener["March7thHoneyListener"]
|
||||
Connection["Connection (ArrayPool & MT19937 Xor)"]
|
||||
HandlerMgr["HandlerManager (Generated Registry)"]
|
||||
end
|
||||
|
||||
subgraph GameplayEngine["Gameplay Engine & Services"]
|
||||
PlayerInst["PlayerInstance"]
|
||||
subgraph Managers["Player Subsystem Managers"]
|
||||
AvatarMgr["AvatarManager & MultiPath"]
|
||||
InvMgr["InventoryManager & Relic Generator"]
|
||||
BattleMgr["BattleManager & FreeSR CalyxOverride"]
|
||||
EndgameMgr["ChallengePeak (Apocalyptic Shadow)"]
|
||||
RogueMgr["Divergent Universe / ChessRogue"]
|
||||
end
|
||||
end
|
||||
|
||||
subgraph StorageLayer["Data & Persistence Layer"]
|
||||
ResCache["ResourceCache (MemoryPack + GZip)"]
|
||||
DbHelper["DatabaseHelper (In-Memory Dirty Cache)"]
|
||||
SQLite[("SQLite (.db)")]
|
||||
MySQL[("MySQL Server")]
|
||||
end
|
||||
end
|
||||
|
||||
HTTPClient <-->|"REST / JSON"| WebServerModule
|
||||
KCPClient <-->|"KCP UDP / Ec2b Framed Packets"| KcpListener
|
||||
KcpListener --> Connection
|
||||
Connection --> HandlerMgr
|
||||
HandlerMgr --> PlayerInst
|
||||
PlayerInst --> Managers
|
||||
Managers --> DbHelper
|
||||
DbHelper --> SQLite
|
||||
DbHelper --> MySQL
|
||||
Managers --> ResCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. การออกแบบ NativeAOT และ Roslyn Source Generators
|
||||
|
||||
หนึ่งในปัญหาใหญ่ของเซิร์ฟเวอร์เกมที่เขียนด้วย Java หรือ C# ทั่วไปคือการพึ่งพา Reflection อย่างหนัก (เช่น การสแกนหา `@Opcodes`, การสร้าง ORM mapping) ซึ่งทำให้:
|
||||
1. การบูตช้า
|
||||
2. กินแรมมาก
|
||||
3. **คอมไพล์ NativeAOT ไม่ได้** เพราะ Reflection รันไทม์ถูกตัดทิ้ง (Trimmed)
|
||||
|
||||
March7thHoney แก้ปัญหานี้โดยสร้างคอมไพลเลอร์เสริม **Roslyn Source Generator (`SourceGen/`)**:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SourceCode["C# Entity & Handler Classes<br/>[Handler(CmdIds.PlayerLoginCsReq)]"] --> Roslyn["Roslyn Source Generator"]
|
||||
Roslyn --> GeneratedCode["Generated C# Partial Classes<br/>- HandlerRegistry.g.cs<br/>- DatabaseStores.g.cs<br/>- CommandRegistry.g.cs"]
|
||||
GeneratedCode --> NativeCompiler[".NET 10 NativeAOT Compiler"]
|
||||
NativeCompiler --> NativeBinary["Single Native Executable<br/>(Zero Reflection / Instant Boot)"]
|
||||
```
|
||||
|
||||
1. **`HandlerRegistryGenerator.cs`**:
|
||||
- ตรวจจับคลาสที่มีแอตทริบิวต์ `[Handler(CmdIds....)]` ในขณะคอมไพล์
|
||||
- สร้างสวิตช์เคสหรืออาร์เรย์ Lookup โดยตรง (`HandlerRegistry.g.cs`) เพื่อแมป Opcode เข้ากับ Concrete Handler Instance แบบ Zero-reflection
|
||||
2. **`DatabaseStoreGenerator.cs`**:
|
||||
- สร้างคลาส `EntityStores.g.cs` เพื่อจัดการ Table Creation, Insert, Update, Select สำหรับทั้ง SQLite และ MySQL ของแต่ละ Entity โดยตรง
|
||||
3. **`CommandRegistryGenerator.cs`**:
|
||||
- ตรวจจับคำสั่งในโฟลเดอร์ `Cmd/` แล้วสร้างตารางแมปชื่อคำสั่ง (Labels & Aliases) ตอนคอมไพล์
|
||||
4. **`MemoryPackRegistrarGenerator.cs`**:
|
||||
- ลงทะเบียน Type Formatter ล่วงหน้าสำหรับ MemoryPack Binary Serializer
|
||||
|
||||
---
|
||||
|
||||
## 3. เลเยอร์เครือข่ายและการเข้ารหัสขั้นสูง (Network, KcpSharp & Crypto)
|
||||
|
||||
### 3.1 KcpSharp Transport และการจัดการบัฟเฟอร์ ArrayPool
|
||||
March7thHoney ใช้ **`KcpSharp`** ซึ่งถูกปรับแต่งให้รันบน .NET 10:
|
||||
- ในลูปรับข้อมูล `ReceiveLoop()` มีการใช้ `ArrayPool<byte>.Shared.Rent(result.BytesReceived)`
|
||||
- ข้อมูลที่ประมวลผลเสร็จแล้วจะถูก `ArrayPool<byte>.Shared.Return(buffer)` คืนสู่พูลทันทีเพื่อป้องกันการจัดสรรแรมซ้ำซ้อน (Zero-Garbage Collection Pressure)
|
||||
|
||||
### 3.2 Binary Packet Framing & Buffer Overflow Protection
|
||||
โครงสร้างแพ็กเก็ตอิงตามข้อกำหนดของตัวเกมจริง:
|
||||
|
||||
```
|
||||
+-----------------------------------------------------------------------------------+
|
||||
| Offset (Bytes) | Type | Field Name | Value / Description |
|
||||
+-----------------------------------------------------------------------------------+
|
||||
| 0x00 - 0x03 | uint32 BE | HEADER_CONST | 0x9D74C714 (Magic 1) |
|
||||
| 0x04 - 0x05 | uint16 BE | Opcode / CmdId | เลขรหัสคำสั่ง (ushort) |
|
||||
| 0x06 - 0x07 | uint16 BE | Header Length | ขนาดส่วนหัว (ushort) |
|
||||
| 0x08 - 0x0B | uint32 BE | Payload Length | ขนาด Protobuf Payload (uint) |
|
||||
| 0x0C - ... | byte[] | Header Bytes | ส่วนหัวแพ็กเก็ต |
|
||||
| ... - ... | byte[] | Payload Bytes | ข้อมูล Protobuf |
|
||||
| End - 4 | uint32 BE | TAIL_CONST | 0xD7A152C8 (Magic 2) |
|
||||
+-----------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
> **การป้องกัน Memory Exhaustion Attack ใน BinaryReader (`Connection.cs`):**
|
||||
> `BinaryReader.ReadBytes(n)` ใน .NET จะจัดสรร Memory ตามค่า `n` ที่อ่านได้ทันที หากไคลเอนต์ประสงค์ร้ายส่งค่า `payloadLength` สูงผิดปกติ (เช่น 2 GB) เซิร์ฟเวอร์จะเกิด Out-Of-Memory Crash ทันที
|
||||
> March7thHoney จึงเพิ่มระบบป้องกัน:
|
||||
> ```csharp
|
||||
> var remaining = br.BaseStream.Length - br.BaseStream.Position;
|
||||
> if (headerLength + (long)payloadLength > remaining) {
|
||||
> Logger.Error("Bad Data Package: payloadLength exceeds remaining buffer");
|
||||
> return;
|
||||
> }
|
||||
> ```
|
||||
|
||||
### 3.3 Ec2b Key Derivation และ MT19937 XOR Key Pipeline
|
||||
หากเปิดตัวเลือก `UsePacketEncryption = true` ในคอนฟิก:
|
||||
1. เซิร์ฟเวอร์จะโหลดหรือสร้างไฟล์คีย์ `ClientSecretKey.ec2b` (`HEAD_MAGIC = 1647469381` หรือ ASCII `"Ec2b"`)
|
||||
2. ตัวคีย์บรรจุ:
|
||||
- 16 ไบต์คีย์หลัก (`Key`)
|
||||
- 2048 ไบต์เวกเตอร์การถอดรหัส (`Data`)
|
||||
3. ตัวสร้างสุ่ม Mersenne Twister **MT19937 (64-bit)** ถูกนำมาใช้สร้างตาราง XOR Key ขนาด 4,096 ไบต์ (`Crypto.GenerateXorKey(seed)`)
|
||||
4. ทุกแพ็กเก็ตที่รับเข้าหรือส่งออกจะถูกทำการ XOR ด้วยตารางคีย์นี้
|
||||
|
||||
---
|
||||
|
||||
## 4. กลไกการฉีดสคริปต์เข้าไคลเอนต์ (Client Lua Script Injection Engine)
|
||||
|
||||
ฟีเจอร์เด่นของ March7thHoney คือการสื่อสารกับ Unity Client โดยตรงผ่านการส่งโค้ดภาษา Lua ผ่าน `HandshakePacket` ซึ่งไคลเอนต์เกมจะรันสคริปต์นี้บนตัวเครื่องโดยอัตโนมัติ:
|
||||
|
||||
### 1. ระบบ Watermark (`BuildWatermarkLua`):
|
||||
เซิร์ฟเวอร์ส่งโค้ด Lua ไปค้นหา GameObject ของ Unity ในหน้าต่างเกม:
|
||||
```lua
|
||||
local function modify_texts()
|
||||
local vGo = CS.UnityEngine.GameObject.Find("VersionText")
|
||||
if vGo == nil then return end
|
||||
local vt = vGo:GetComponent("Text")
|
||||
vt.supportRichText = true
|
||||
vt.text = "<color=#FEC6DF>March7thHoney</color> | UID: 10001"
|
||||
vt.fontSize = 18
|
||||
end
|
||||
pcall(modify_texts)
|
||||
```
|
||||
ทำให้หน้าจอเกมแสดงชื่อเซิร์ฟเวอร์, UID, เวอร์ชัน หรือข้อความไล่เฉดสี (Color Gradient) โดยไม่ต้องแก้ไขไฟล์ตัวเกมของไคลเอนต์
|
||||
|
||||
### 2. ระบบจำลองสถานะเครือข่าย (`NetStatusLua`):
|
||||
ส่งโค้ด Lua ไปควบคุมไอคอน Ping และแสดงค่า Latency คงที่ตามที่ตั้งไว้ในคอนฟิก
|
||||
|
||||
---
|
||||
|
||||
## 5. ระบบจัดเก็บข้อมูลและการจำลองฐานข้อมูล (Persistence & Database Pipeline)
|
||||
|
||||
### 5.1 Dual-Engine: SQLite & MySQL
|
||||
March7thHoney รองรับทั้งฐานข้อมูลเดี่ยว (SQLite สำหรับ Local/Single-player) และฐานข้อมูลเซิร์ฟเวอร์ (MySQL สำหรับ Production):
|
||||
- กำหนดใน `config.json` $\rightarrow$ `Database.DatabaseType`: `"sqlite"` หรือ `"mysql"`
|
||||
- ตารางทั้งหมดถูกสร้างขึ้นอัตโนมัติเมื่อเริ่มระบบผ่าน `EntityStores.Map.Values.EnsureTable(conn)`
|
||||
|
||||
### 5.2 In-Memory Dirty Cache & Asynchronous Periodic Flusher
|
||||
เพื่อลด I/O คอขวด March7thHoney ไม่เขียนข้อมูลลงดิสก์ทุกครั้งที่มีการเปลี่ยนไอเทม แต่ใช้สถาปัตยกรรม **In-Memory Cache ร่วมกับ Dirty Tracking**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant GameAction as Game Action (e.g. Add Item)
|
||||
participant MemoryMap as UidInstanceMap (In-Memory)
|
||||
participant DirtySet as DirtyUids (HashSet)
|
||||
participant Flusher as SaveThread (Background Task)
|
||||
participant Disk as SQLite / MySQL Database
|
||||
|
||||
GameAction->>MemoryMap: Update Entity Object in Memory
|
||||
GameAction->>DirtySet: DatabaseHelper.MarkDirty(playerUid)
|
||||
Note over MemoryMap,DirtySet: ผู้เล่นเล่นเกมต่อได้ทันที โดยไม่มี I/O Latency
|
||||
|
||||
loop ทุกๆ 1 วินาที (CalcSaveDatabase)
|
||||
Flusher->>DirtySet: ดึงรายการ UIDs ที่มีการแก้ไข (DirtyLock)
|
||||
Flusher->>MemoryMap: อ่าน Snapshot ข้อมูลของ UID นั้นๆ
|
||||
Flusher->>Disk: ทำ Batch Transaction บันทึกลงฐานข้อมูล
|
||||
Flusher->>DirtySet: ลบ UID ออกจาก DirtySet
|
||||
end
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> **NativeAOT Thread Safepoint Fix (`DatabaseHelper.cs`):**
|
||||
> ใน .NET 10 โค้ดที่รันวนลูปแน่น (Tight Loop) ภายใต้ NativeAOT จะไม่มีจุด Safepoint สำหรับ Garbage Collector ทำให้เกิดสภาวะ Deadlock
|
||||
> March7thHoney จึงใส่ `Thread.Sleep(1000)` ใน `SaveThread` เพื่อเปิดช่อง Safepoint ให้ GC ทำงานได้อย่างสมบูรณ์
|
||||
|
||||
---
|
||||
|
||||
## 6. ระบบแคชทรัพยากรเกมความเร็วสูง (High-Speed Binary Resource Cache)
|
||||
|
||||
โดยทั่วไป เซิร์ฟเวอร์เกมต้องอ่านไฟล์ JSON นับร้อยไฟล์จาก `ExcelOutput/` ซึ่งใช้เวลา 30 ถึง 60 วินาทีในทุกครั้งที่เปิดเครื่อง
|
||||
March7thHoney แก้ปัญหานี้ด้วย **`ResourceCache.cs`**:
|
||||
|
||||
1. **First Boot (สร้างแคชครั้งแรก)**:
|
||||
- อ่าน JSON จาก `Resources/ExcelOutput/` และ `Resources/ExcelOutputGameCore/`
|
||||
- รวบรวมข้อมูลลงใน `GameDataSnapshot`
|
||||
- ทำ Serialization ด้วย **`MemoryPack`** ซึ่งเป็น Zero-allocation Binary Serializer
|
||||
- บีบอัดข้อมูลด้วย **`GZipStream(CompressionLevel.Optimal)`** บันทึกเป็นไฟล์ `Config/Resource.bin` (ขนาดประมาณ 20-30 MB)
|
||||
2. **Subsequent Boots (การบูตครั้งถัดไป)**:
|
||||
- ตรวจสอบความถูกต้องของ Magic Header GZip (`0x1F 0x8B`)
|
||||
- คลายการบีบอัดและคืนค่าเข้า `GameData` ภายใน **ไม่เกิน 1 วินาที**
|
||||
3. **Operator Hot-Reload สำหรับ Banners**:
|
||||
- ไฟล์ `Banners.json` (ตู้กาชาที่ผู้ดูแลปรับแต่งได้เอง) จะไม่ถูกรวมเข้า `Resource.bin` แต่จะถูกอ่านแยกสด เพื่อให้แก้ไขและเปิด/ปิดตู้กาชาได้ทันทีโดยไม่ต้องล้างแคชเกมใหม่
|
||||
|
||||
---
|
||||
|
||||
## 7. ระบบการต่อสู้และการแทรกแซงสถานการณ์ (Battle & Calyx Override Engine)
|
||||
|
||||
### 7.1 ลูปการต่อสู้หลัก
|
||||
- เริ่มต้นจาก `BattleManager.StartBattle()` เมื่อมี Technique Cast หรือมอนสเตอร์โจมตี
|
||||
- สร้าง `BattleInstance` ซึ่งรวบรวม Wave มอนสเตอร์, เลเวลตัวละคร, MazeBuff (บัฟจุดอ่อนของธาตุที่โจมตี)
|
||||
- รองรับด่านหลากหลายรูปแบบ: Mainline, Cocoon (ฟาร์มของ), FarmElement, BattleCollege (บทเรียนการต่อสู้)
|
||||
|
||||
### 7.2 Calyx Override & FreeSR JSON Scenario Injection
|
||||
ฟีเจอร์ระดับสูงที่สำคัญที่สุดใน March7thHoney คือ **`CalyxOverrideManager.cs`** และ **`FreesrShared.cs`**:
|
||||
- ผู้พัฒนาสามารถใส่ไฟล์สถานการณ์จำลองในรูปแบบ **FreeSR JSON** ไว้ในไดเรกทอรี `Config/Json/*.json`
|
||||
- เมื่อเปิดใช้งานผ่านคำสั่ง `/calyx` หรือ `CalyxOverrideManager.Enable()`:
|
||||
- เซิร์ฟเวอร์จะดักจับการเข้าด่าน Calyx (ดอกไม้ฟาร์มเงิน/EXP) ในโลกเปิด
|
||||
- แทนที่มอนสเตอร์และด่านเดิมของ Calyx ด้วยการต่อสู้ที่ระบุในไฟล์ FreeSR JSON ทันที
|
||||
- สามารถสลับทีมตัวละคร (`TrySetLineupOverride`), ปรับแต่งเลือดมอนสเตอร์เป็นรายตัวและรายเวฟ (`TrySetMonsterHp`)
|
||||
- ทำให้สามารถทดสอบบอสใหม่, มอนสเตอร์จากแพตช์ล่วงหน้า หรือจัด Challenge พิเศษได้โดยไม่ต้องแก้เกม
|
||||
|
||||
---
|
||||
|
||||
## 8. ระบบโหมดการเล่นขั้นสูง (Modern End-Game Modes)
|
||||
|
||||
March7thHoney เป็นหนึ่งในเซิร์ฟเวอร์ไม่กี่แห่งที่รองรับโหมดการเล่นระดับ Endgame ยุคใหม่ของ Honkai: Star Rail:
|
||||
|
||||
### 8.1 ChallengePeak (Apocalyptic Shadow & Anomaly Arbitration)
|
||||
- ควบคุมผ่าน `ChallengePeakManager.cs`
|
||||
- รองรับโหมด **Apocalyptic Shadow (เงามายาแห่งกลียุค)** และ **Anomaly Arbitration**
|
||||
- จัดการบอสที่มีหลายเฟสและกลไกพิเศษ (เช่น บอสมีเกจ Toughness พิเศษ)
|
||||
- รองรับการจัดทีมแบบ Tierce (ด่าน 3 เฟส/ทีมย่อย)
|
||||
|
||||
### 8.2 Divergent Universe & Unknowable Domain
|
||||
- ควบคุมผ่าน `ChessRogue` และ `RogueTourn` (TournRoomGenerator)
|
||||
- จัดการระบบสมการ (Equations), ถ่วงน้ำหนักพร, Curio พิเศษ และวงล้อโชคชะตา
|
||||
- รองรับห้องบันทึกการสังเคราะห์ (Archive Repository)
|
||||
|
||||
### 8.3 Currency War & GridFight (Aetherium Wars)
|
||||
- **`GridFightManager`**: จำลองระบบมินิเกมกระดานและการแข่งขันมอนสเตอร์ (Aetherium Wars)
|
||||
- **`Currency War`**: จำลองเกมสงครามการเงิน มีระบบกระดาน, ซื้อ/ขายไอเทม และร้านค้าในมินิเกม
|
||||
|
||||
---
|
||||
|
||||
## 9. การจัดการระยะไกลและหน้าต่างควบคุม (Muip Protocol & Admin Dashboard)
|
||||
|
||||
### 9.1 Muip GM Protocol (RSA-Encrypted Remote Command Execution)
|
||||
March7thHoney พัฒนาระบบ Muip (Management User Interface Protocol) เพื่อให้แอปพลิเคชันภายนอก, Bot Discord หรือเว็บแอดมิน ติดต่อสั่งการเซิร์ฟเวอร์ได้อย่างปลอดภัยผ่านการเข้ารหัส **RSA Public/Private Key (PKCS#1)**:
|
||||
|
||||
1. `POST /muip/create_session`:
|
||||
- เซิร์ฟเวอร์สร้าง RSA Keypair และส่ง `rsaPublicKey` พร้อม `sessionId` กลับมา (มีอายุจำกัด)
|
||||
2. `POST /muip/auth_admin`:
|
||||
- ไคลเอนต์ส่ง `admin_key` ที่ถูกเข้ารหัสด้วย `rsaPublicKey` เพื่อยืนยันสิทธิ์ผู้ดูแล
|
||||
3. `POST /muip/exec_cmd`:
|
||||
- ส่งคำสั่งเกม (เช่น `/give 1001 x1`) ที่เข้ารหัสด้วย RSA พร้อมระบุ `TargetUid` เซิร์ฟเวอร์ถอดรหัสและรันคำสั่งทันที
|
||||
4. `GET /muip/server_information` & `/muip/player_information`:
|
||||
- ขอข้อมูลสถิติเซิร์ฟเวอร์ (RAM, ผู้เล่นออนไลน์) และข้อมูลไอดีผู้เล่น
|
||||
|
||||
### 9.2 Built-in Embedded Web Admin & Registration Portal
|
||||
- ไม่จำเป็นต้องเปิด Web Server แยกต่างหาก เพราะ WebServer ของ March7thHoney มีเว็บ Dashboard ในตัว
|
||||
- เขียนด้วย Embedded HTML/CSS/JS (`AdminPageRenderer.cs`, `AdminPageScripts.cs`)
|
||||
- มีหน้าระบบลงทะเบียนผู้เล่น (`RegistrationPage.cs`), หน้ารีเซ็ตรหัสผ่านผ่านอีเมล (`PasswordForgotPage.cs`, `AccountEmailSender.cs`), และหน้าจัดการไอดี
|
||||
|
||||
### 9.3 Cluster Deployment via ServerExchange Secret
|
||||
- รองรับการแยกรัน Dispatch Server และ Game Server บนเครื่องเซิร์ฟเวอร์คนละเครื่อง
|
||||
- สื่อสารระหว่างโหนดผ่าน `/get_account_info` และ `/validate_account_token` โดยยืนยันความปลอดภัยผ่าน HTTP Header `X-March7thHoney-Server-Secret` ตรวจสอบความเท่ากันแบบคงเวลา (`FixedTimeEquals`) เพื่อป้องกัน Timing Attack
|
||||
|
||||
---
|
||||
|
||||
## 10. ตารางเปรียบเทียบเชิงลึก: LunarCore vs March7thHoney
|
||||
|
||||
| มิติการเปรียบเทียบ | LunarCore (Melledy) | March7thHoney (Mar7thLover) |
|
||||
|---|---|---|
|
||||
| **ภาษาและรันไทม์** | Java 17+ (Gradle / JVM) | C# (.NET 10 SDK) |
|
||||
| **สถาปัตยกรรมประสิทธิภาพ** | Reflection รันไทม์, Quickbuf Proto | Roslyn Source Generators, **NativeAOT**, MemoryPack |
|
||||
| **เครือข่าย UDP/KCP** | Java KCP (Netty UDP Channel) | **KcpSharp** (Pure C# Zero-alloc + ArrayPool) |
|
||||
| **การเข้ารหัสทราฟฟิก** | XOR แบบธรรมดา (ปิดไว้เป็นค่าตั้งต้น) | รองรับ **Ec2b** Key Derivation + MT19937 XOR 4KB |
|
||||
| **การเก็บข้อมูล (Storage)** | MongoDB (ภายนอก) หรือ H2 Mongo (ภายใน) | **SQLite** และ **MySQL** (In-Memory Dirty Flusher) |
|
||||
| **การแคชทรัพยากรเกม** | โหลดไฟล์ JSON สดทุกครั้งที่เปิด (~15-30 วิ) | **Resource.bin** (MemoryPack + GZip) บูต < 1 วินาที |
|
||||
| **การฉีดโค้ดสู่ไคลเอนต์** | ไม่มี | **Server-Pushed Lua Injection** (Watermark, Ping UI) |
|
||||
| **เครื่องมือทดสอบการต่อสู้** | ผ่านคำสั่ง `/spawn` และ Stage IDs ปกติ | **Calyx Override + FreeSR JSON Scenario Injection** |
|
||||
| **โหมดเกมที่รองรับ** | พื้นฐาน: MoC, Pure Fiction, SU ปกติ | ขั้นสูง: **Apocalyptic Shadow**, **Divergent Universe**, **Currency War**, Aetherium Wars |
|
||||
| **ระบบบริหารจัดการ (GM/Web)**| คอนโซล และ แชตในเกมผ่านบอท UID 99 | **Muip RSA API**, Web Admin Dashboard, Web Registration |
|
||||
|
||||
---
|
||||
|
||||
## 11. คู่มือการต่อยอดเชิงวิศวกรรมสำหรับ AI (AI Implementation Guidelines)
|
||||
|
||||
หากโมเดล AI อื่นต้องการสร้างส่วนต่อขยาย, แก้บั๊ก หรือเขียนตัวแปลงโปรโตคอลสำหรับ March7thHoney:
|
||||
|
||||
1. **อย่าใช้ Reflection สำหรับ Handler และ Database Store**:
|
||||
ต้องใช้ระบบ Source Generator เสมอ หากเพิ่ม Entity ใหม่ ให้สร้างคลาสที่สืบทอดจาก `BaseDatabaseDataHelper` เพื่อให้ `DatabaseStoreGenerator` สร้างโค้ด SQL ตารางและการแมปให้อัตโนมัติ เพื่อรักษาคุณสมบัติ NativeAOT
|
||||
2. **การจัดการ Entity ในหน่วยความจำ**:
|
||||
เมื่อแก้ไขข้อมูล Entity ของผู้เล่น ต้องเรียก `DatabaseHelper.MarkDirty(player.Uid)` เสมอ เพื่อให้ Background Flusher รับรู้และนำไปบันทึกลง SQLite/MySQL
|
||||
3. **การทดสอบฉากต่อสู้แบบกำหนดเอง**:
|
||||
ใช้โครงสร้าง FreeSR JSON ในโฟลเดอร์ `Config/Json/` ร่วมกับ `CalyxOverrideManager` จะช่วยให้จำลองการต่อสู้กับบอสใดๆ ได้ทันทีโดยไม่ต้องรื้อ Resource Excel
|
||||
4. **ความปลอดภัยของ Buffer**:
|
||||
ในส่วนการอ่าน Binary Packets ให้คงการตรวจสอบ `remaining buffer` ไว้เสมอ เพื่อป้องกันช่องโหว่ Buffer Overflow และการจัดสรรหน่วยความจำเกินขนาด
|
||||
@@ -0,0 +1,379 @@
|
||||
# Static CmdId Dumper (`StaticCmdIdDumper`) Architectural Specification & Machine-Code Deep Dive
|
||||
|
||||
This technical specification provides an exhaustive, code-level analysis of **`StaticCmdIdDumper`** (by Hiro420), an ultra-minimalist, zero-dependency C# (.NET 8.0) static binary analysis utility. The tool is engineered to extract network packet Command IDs (`CmdId` / `Opcode`) directly from compiled IL2CPP game executables (such as `YuanShen.exe`, `StarRail.exe`, or `GameAssembly.dll`) on disk without launching the game process or relying on third-party disassemblers (such as Capstone or Iced).
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary & Architectural Purpose
|
||||
|
||||
### 1.1 The Protocol Command ID Problem
|
||||
In modern anime titles developed on Unity IL2CPP (Genshin Impact, Honkai: Star Rail, Zenless Zone Zero), client-server communication relies on an enumerated command opcode (`CmdId`) identifying each Protobuf packet.
|
||||
|
||||
Inside the game client assemblies, each packet class contains a getter method returning its unique opcode:
|
||||
```csharp
|
||||
public class GetAvatarDataCsReq : IMessage {
|
||||
public override ushort GetCmdId() {
|
||||
return 384; // 0x0180
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In compiled releases:
|
||||
1. Method names are stripped or obfuscated (e.g., `GetCmdId()` $\rightarrow$ `KOOKJNLBAMH()`).
|
||||
2. Tools like `Firefly-Static-Parser` or `honkai-dumper` can identify the **Relative Virtual Address (RVA)** of these getter methods.
|
||||
3. However, obtaining the actual integer value of the opcode traditionally required:
|
||||
- Attaching a live debugger / Frida script to invoke the method at runtime, or
|
||||
- Opening a 100MB+ binary in IDA Pro / Ghidra and manually decompiling thousands of tiny stub functions.
|
||||
|
||||
### 1.2 The `StaticCmdIdDumper` Solution
|
||||
`StaticCmdIdDumper` demonstrates an elegant, lightweight alternative:
|
||||
- It exploits the deterministic machine-code pattern emitted by C++ compilers when compiling a constant 16-bit integer getter.
|
||||
- It parses PE (Portable Executable) headers using pure, hand-written C# structs.
|
||||
- It maps the getter method's RVA directly to disk file offset, reads **exactly 4 bytes**, and extracts the embedded immediate integer via a micro-disassembler.
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| StaticCmdIdDumper Architecture |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ Inputs ] |
|
||||
| 1. Binary File (e.g. YuanShen.exe / GameAssembly.dll) |
|
||||
| 2. list.txt: Mapping of <ObfuscatedName> <RVA> |
|
||||
| Example: "KOOKJNLBAMH 0x0CCA3780" |
|
||||
| |
|
||||
| [ Pipeline: Zero External Dependencies ] |
|
||||
| |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 1. PEHeader.cs & Misc.cs | |
|
||||
| | - Parse DOS Header ('MZ') -> e_lfanew -> PE Signature ('PE\0\0') | |
|
||||
| | - Parse OptionalHeader64 -> Extract Section Table | |
|
||||
| | - Locate Target Section ('il2cpp' or '.text') | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 2. File Offset Translation | |
|
||||
| | FileOffset = Section.PointerToRawData + (MethodRVA - Section.VirtualAddress) | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 3. Targeted 4-Byte Seek & Read | |
|
||||
| | fs.Seek(FileOffset) -> fs.Read(buffer, 0, 4) | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| +----------------------------------------------------------------------------------------------+ |
|
||||
| | 4. Micro-Disassembler (Disassembler.cs) | |
|
||||
| | Matches x86-64 pattern: 66 B8 [imm16] --> "mov ax, imm16" | |
|
||||
| | Extracts Little-Endian uint16 CmdId | |
|
||||
| +----------------------------------------------+-----------------------------------------------+ |
|
||||
| | |
|
||||
| v |
|
||||
| [ Output ] |
|
||||
| PacketIds.json: |
|
||||
| { |
|
||||
| "KOOKJNLBAMH": 1234, |
|
||||
| "MGDFLHGODLH": 4567 |
|
||||
| } |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Codebase Organization & Engineering Philosophy
|
||||
|
||||
The repository consists of a single C# project targeting `.NET 8.0`:
|
||||
|
||||
```
|
||||
StaticCmdIdDumper/
|
||||
├── StaticCmdIdDumper.sln
|
||||
└── StaticCmdIdDumper/
|
||||
├── StaticCmdIdDumper.csproj # Zero external package references (<PackageReference>)
|
||||
├── PEHeader.cs # Complete Win32/PE64 data structure definitions & binary readers
|
||||
├── Misc.cs # Section search, header dumpers, and file parsing utilities
|
||||
├── Disassembler.cs # 50-line custom x86-64 machine-code instruction decoder
|
||||
└── Program.cs # CLI coordinator & JSON serializer
|
||||
```
|
||||
|
||||
### 2.1 The Zero-Dependency Design
|
||||
Modern .NET reverse-engineering tools typically pull in large third-party dependencies:
|
||||
- `AsmResolver.PE` or `PeNet` for PE parsing.
|
||||
- `Iced` or `Gee.External.Capstone` for disassembly.
|
||||
|
||||
`StaticCmdIdDumper` explicitly rejects external packages, implementing:
|
||||
1. Hand-coded binary reader routines (`Read16LE`, `Read32LE`, `Read64LE`).
|
||||
2. Hand-crafted PE structures mirroring Windows `winnt.h` (`IMAGE_DOS_HEADER`, `IMAGE_NT_HEADERS64`, `IMAGE_SECTION_HEADER`).
|
||||
3. Direct byte pattern recognition for the specific instruction opcode emitted by MSVC / Clang.
|
||||
|
||||
---
|
||||
|
||||
## 3. Machine Code Mechanics: IL2CPP Opcode Getters
|
||||
|
||||
### 3.1 C++ to Native x86-64 Compilation
|
||||
In Unity's IL2CPP output, a constant getter function in C++ is written as:
|
||||
```cpp
|
||||
uint16_t GetCmdId() {
|
||||
return 1234; // 0x04D2
|
||||
}
|
||||
```
|
||||
|
||||
When compiled by MSVC or Clang for x86-64 with standard optimizations (`/O2` or `-O3`), the compiler emits:
|
||||
```nasm
|
||||
mov ax, 0x04D2
|
||||
ret
|
||||
```
|
||||
|
||||
### 3.2 Binary Instruction Encoding
|
||||
The x86-64 instruction encoding for `mov ax, imm16` is:
|
||||
|
||||
$$\begin{array}{|c|c|c|c|}
|
||||
\hline
|
||||
\mathbf{0x66} & \mathbf{0xB8} & \mathbf{\text{Byte}_0} & \mathbf{\text{Byte}_1} \\
|
||||
\hline
|
||||
\text{Prefix (Operand Size)} & \text{Opcode (mov ax)} & \text{Imm16 (Low)} & \text{Imm16 (High)} \\
|
||||
\hline
|
||||
\end{array}$$
|
||||
|
||||
- **`0x66` (Operand-Size Override Prefix)**: The default operand size in 64-bit mode is 32-bit. The `0x66` prefix instructs the processor to decode the instruction for the 16-bit register `ax` instead of 32-bit `eax`.
|
||||
- **`0xB8`**: Opcode for `mov reg16/32, imm16/32` (specifically targeting accumulator register `ax`).
|
||||
- **`Byte 0` & `Byte 1`**: 16-bit little-endian immediate value representing the `CmdId`.
|
||||
- **`0xC3`**: The subsequent byte (at offset +4) is the `ret` opcode.
|
||||
|
||||
### 3.3 The Custom Micro-Disassembler (`Disassembler.cs`)
|
||||
Because the target functions follow this invariant structure, `Disassembler.cs` decodes the opcode in just a few lines:
|
||||
|
||||
```csharp
|
||||
public static int Disassemble(byte[] code)
|
||||
{
|
||||
int ip = 0;
|
||||
|
||||
while (ip < code.Length)
|
||||
{
|
||||
byte opcode = code[ip];
|
||||
|
||||
switch (opcode)
|
||||
{
|
||||
case 0x66: // 16-bit operand override
|
||||
{
|
||||
if (ip + 1 < code.Length && code[ip + 1] == 0xB8)
|
||||
{
|
||||
if (ip + 3 < code.Length)
|
||||
{
|
||||
// Read 16-bit unsigned little-endian integer
|
||||
ushort imm16 = BitConverter.ToUInt16(code, ip + 2);
|
||||
return imm16;
|
||||
}
|
||||
}
|
||||
ip += 1;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ip += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. PE Header Parsing & RVA-to-Offset Translation
|
||||
|
||||
### 4.1 PE64 Structural Traversal
|
||||
In `PEHeader.cs` and `Misc.cs`, the program traverses the binary structure:
|
||||
|
||||
1. **DOS Header**:
|
||||
- Reads `dosHeader.magic` (must equal `0x5A4D` / `"MZ"`).
|
||||
- Reads `dosHeader.e_lfanew` at offset `0x3C`, which points to the NT PE header.
|
||||
2. **PE Header & Optional Header**:
|
||||
- Validates `dosHeader.pe.signature == 0x00004550` (`"PE\0\0"`).
|
||||
- Determines architecture from `optionalHeader.magic`:
|
||||
- `0x10B`: 32-bit PE (`x86`)
|
||||
- `0x20B`: 64-bit PE (`x86_64`)
|
||||
- Reads `numberOfSections` and `optionalHeaderSize`.
|
||||
3. **Section Table Traversal (`ReadSections`)**:
|
||||
- Iterates through `IMAGE_SECTION_HEADER` entries (40 bytes each):
|
||||
```csharp
|
||||
dosHeader.section_table[idx].name = ReadStr(reader, 8);
|
||||
dosHeader.section_table[idx].virtualSize = Read32LE(reader);
|
||||
dosHeader.section_table[idx].virtualAddr = Read32LE(reader);
|
||||
dosHeader.section_table[idx].sizeOfRawData = Read32LE(reader);
|
||||
dosHeader.section_table[idx].ptrToRawData = Read32LE(reader);
|
||||
```
|
||||
|
||||
### 4.2 Locating the IL2CPP Section (`Misc.GetBaseAdd`)
|
||||
In certain compiled anime games (notably Genshin Impact's `YuanShen.exe`), the linker places compiled game code into a custom named section:
|
||||
```csharp
|
||||
for (int idx = 0; idx < dosHeader.pe.numberOfSections; idx++)
|
||||
{
|
||||
if (sections![idx].name != "il2cpp")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return sections[idx];
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 RVA to Disk File Offset Translation
|
||||
Memory-mapped virtual addresses (VAs and RVAs) differ from raw disk offsets due to section alignment. The conversion formula is:
|
||||
|
||||
$$\text{FileOffset} = \text{Section.PointerToRawData} + (\text{RVA}_{\text{target}} - \text{Section.VirtualAddress})$$
|
||||
|
||||
In `Program.cs`:
|
||||
```csharp
|
||||
ulong CmdIDTestAddr = Convert.ToUInt64(pair.Value.Substring(2), 16);
|
||||
var fileoffset = il2cpp_section.ptrToRawData + (CmdIDTestAddr - il2cpp_section.virtualAddr);
|
||||
|
||||
byte[] buffer = new byte[4];
|
||||
using (FileStream fs = new FileStream(args[0], FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
fs.Seek((long)fileoffset, SeekOrigin.Begin);
|
||||
fs.Read(buffer, 0, 4);
|
||||
}
|
||||
|
||||
int cmdid = Disassembler.Disassemble(buffer);
|
||||
packetIds[pair.Key] = cmdid;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Execution Workflow & Serialization
|
||||
|
||||
### 5.1 Input Format (`list.txt`)
|
||||
The tool accepts a plain whitespace-delimited text file containing pairs of function identifiers and hexadecimal RVAs:
|
||||
```text
|
||||
KOOKJNLBAMH 0x0CCA3780
|
||||
MGDFLHGODLH 0x0BE5AA60
|
||||
CGKBCOJMLPC 0x082AB170
|
||||
OOMIIJFHPFN 0x0D16A280
|
||||
```
|
||||
> [!NOTE] Origin of the Input List
|
||||
> This list is directly obtained from the output of `Firefly-Static-Parser` or `honkai-dumper`. In `methods.json`, getter methods for packet IDs appear as:
|
||||
> `"KOOKJNLBAMH": "0xCCA3780"`
|
||||
|
||||
### 5.2 Output Format (`PacketIds.json`)
|
||||
The resulting dictionary is serialized to formatted JSON:
|
||||
```json
|
||||
{
|
||||
"KOOKJNLBAMH": 101,
|
||||
"MGDFLHGODLH": 102,
|
||||
"CGKBCOJMLPC": 143,
|
||||
"OOMIIJFHPFN": 172
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. The Complete 5-Stage Protocol Reverse-Engineering Toolchain
|
||||
|
||||
By studying `LunarCore`, `March7thHoney`, `FireflyProtoc`, `honkai-dumper`, `Firefly-Static-Parser`, and `StaticCmdIdDumper`, we can now construct the entire open-source reverse-engineering pipeline:
|
||||
|
||||
```
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| THE UNIFIED PROTOCOL EXTRACTION PIPELINE |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
| |
|
||||
| [ Stage 1: Metadata Extraction ] |
|
||||
| Tool: Firefly-Static-Parser (Offline) OR honkai-dumper (In-Process) |
|
||||
| Input: GameAssembly.dll + global-metadata.dat (b"MHY\0") |
|
||||
| Output: dump.cs (class schemas) + methods.json (method RVAs) |
|
||||
| |
|
||||
| [ Stage 2: Static Opcode Extraction ] |
|
||||
| Tool: StaticCmdIdDumper |
|
||||
| Input: Executable binary + method RVAs of getter functions |
|
||||
| Process: Reads 4-byte machine code (66 B8 imm16) -> extracts uint16 CmdIds |
|
||||
| Output: PacketIds.json (Map of ObfuscatedPacketName -> Integer CmdId) |
|
||||
| |
|
||||
| [ Stage 3: Protobuf Schema Annotation ] |
|
||||
| Tool: Schema Annotator / String Literal Correlator |
|
||||
| Action: Injects metadata annotations into .proto files: |
|
||||
| meta uint32 cmd_id = 143; |
|
||||
| uint32 id = 1 [get: (id ^ 1) + 2]; |
|
||||
| |
|
||||
| [ Stage 4: Specialized Protobuf Compilation ] |
|
||||
| Tool: FireflyProtoc |
|
||||
| Action: Compiles .proto via AST pipeline (HIR -> MIR -> LIR) |
|
||||
| - ExpressionInverter: Automatically derives setter logic for wire masks |
|
||||
| - MessageFactory: Emits O(1) compile-time jump table switch(cmdId) |
|
||||
| Output: High-performance C# / Go protobuf stubs |
|
||||
| |
|
||||
| [ Stage 5: Server Protocol Routing ] |
|
||||
| Tool: March7thHoney (.NET 10) / LunarCore (Java) / GaC-hkrpg-go (Go) |
|
||||
| Action: Instant zero-reflection packet dispatching on KCP connections |
|
||||
| |
|
||||
+----------------------------------------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Technical Critique & Improvements for `GaC-hkrpg-go`
|
||||
|
||||
While `StaticCmdIdDumper` is compact and effective, an industrial-grade implementation (e.g. ported to Go for `GaC-hkrpg-go`) requires resolving several architectural limitations:
|
||||
|
||||
### 7.1 Limitation 1: Hardcoded Section Name (`"il2cpp"`)
|
||||
In `Misc.cs`:
|
||||
```csharp
|
||||
if (sections![idx].name != "il2cpp") continue;
|
||||
```
|
||||
- **Issue**: In *Honkai: Star Rail* and standard Unity builds, code is placed in the standard `.text` section, **not** a section named `il2cpp`.
|
||||
- **Solution**: Implement dynamic RVA-to-Section resolution across **all** sections:
|
||||
$$\text{Section} = \{ s \in \text{Sections} \mid s.\text{VirtualAddress} \le \text{RVA} < s.\text{VirtualAddress} + s.\text{VirtualSize} \}$$
|
||||
|
||||
### 7.2 Limitation 2: 32-Bit Register Emptiness (`mov eax, imm32`)
|
||||
- **Issue**: If the method return type is `int` (32-bit signed integer) rather than `ushort`, compilers emit `mov eax, imm32` without the `0x66` prefix:
|
||||
`B8 xx xx 00 00` (5 bytes: `0xB8` followed by 4-byte integer).
|
||||
- **Solution**: Support both instruction patterns:
|
||||
- `66 B8 [imm16]` $\rightarrow$ 16-bit opcode getter.
|
||||
- `B8 [imm32]` $\rightarrow$ 32-bit opcode getter.
|
||||
|
||||
### 7.3 Limitation 3: Stream Seeking Overhead
|
||||
- **Issue**: Opening and seeking `FileStream` individually for every single RVA creates heavy I/O overhead for thousands of packets.
|
||||
- **Solution**: Memory-map the section or load the section into a contiguous `[]byte` slice in memory for instant $O(1)$ random access.
|
||||
|
||||
### 7.4 Go Port Blueprint for `GaC-hkrpg-go`
|
||||
A native, high-performance Go implementation for extracting opcodes directly within the `GaC-hkrpg-go` toolset:
|
||||
|
||||
```go
|
||||
package pe_dumper
|
||||
|
||||
import (
|
||||
"debug/pe"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ExtractCmdIdFromRVA(peFile *pe.File, rva uint32, f *os.File) (uint16, error) {
|
||||
// 1. Locate the containing section dynamically
|
||||
for _, sec := range peFile.Sections {
|
||||
if rva >= sec.VirtualAddress && rva < sec.VirtualAddress+sec.VirtualSize {
|
||||
fileOffset := int64(sec.Offset + (rva - sec.VirtualAddress))
|
||||
buf := make([]byte, 4)
|
||||
if _, err := f.ReadAt(buf, fileOffset); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// 2. Pattern: 66 B8 [imm16]
|
||||
if buf[0] == 0x66 && buf[1] == 0xB8 {
|
||||
return binary.LittleEndian.Uint16(buf[2:4]), nil
|
||||
}
|
||||
// 3. Pattern: B8 [imm32] (32-bit fallback)
|
||||
if buf[0] == 0xB8 {
|
||||
return binary.LittleEndian.Uint16(buf[1:3]), nil
|
||||
}
|
||||
return 0, fmt.Errorf("unknown instruction pattern: % X", buf)
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("RVA 0x%X not found in any section", rva)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Compliance with Repository Rules (`AGENTS.md`)
|
||||
|
||||
- **Documentation & Protocol Integrity**: `StaticCmdIdDumper` extracts Command IDs purely from the machine code representation of the target client binary. It adheres strictly to the rule requiring handlers to resolve `CmdID` from verified client protocol tables without guessing or hardcoding multi-version aliases.
|
||||
- **No Machine-Specific Hardcoding**: All file paths and offsets are read dynamically from command-line arguments and PE headers.
|
||||
- **Zero Runtime Interference**: Operates entirely offline on the executable file, with zero memory injection, hooks, or heartbeat overhead.
|
||||
Reference in new issue
Block a user