package protocol import ( "bytes" "encoding/binary" "encoding/hex" "encoding/json" "go/ast" "go/parser" "go/token" "io" "os" "path/filepath" "strconv" "strings" "testing" ) func TestActiveMethodIDsMatchLegacyContracts(t *testing.T) { data, err := os.ReadFile("testdata/legacy_method_ids.json") if err != nil { t.Fatal(err) } var expected map[string]uint32 if err := json.Unmarshal(data, &expected); err != nil { t.Fatal(err) } aliases := map[string]string{"GateLogin": "Login", "AskEnterWorld": "LoginGame", "SyncSwitchSpiritInSitu": "SyncPlayerSwitchSpiritInSitu"} f, err := parser.ParseFile(token.NewFileSet(), "method_id.go", nil, 0) if err != nil { t.Fatal(err) } ast.Inspect(f, func(n ast.Node) bool { v, ok := n.(*ast.ValueSpec) if !ok { return true } for i, id := range v.Names { name := strings.TrimPrefix(id.Name, "Method") if alias, ok := aliases[name]; ok { name = alias } lit, ok := v.Values[i].(*ast.BasicLit) if !ok { t.Fatalf("nonliteral method %s", name) } value, err := strconv.ParseUint(lit.Value, 10, 32) want, exists := expected[name] if err != nil || !exists || uint32(value) != want { t.Errorf("%s = %d, legacy = %d (exists=%v)", name, value, want, exists) } } return true }) } func TestDefaultReturnContracts(t *testing.T) { if len(defaultReturnBodies) != 2356 { t.Fatalf("missing return contracts: %d", len(defaultReturnBodies)) } // Independent examples from the original Lua-reader catalog, not round trips. for _, tc := range []struct { id uint32 hex string }{ {34700853, ""}, {34022227, "ff01"}, {13224129, "00000000"}, {12279476, "01"}, {34383517, "0000000000000000"}, {34601301, "00000000ff01"}, } { body, ok := DefaultReturnBody(tc.id) if !ok || hex.EncodeToString(body) != tc.hex { t.Errorf("%d: %x, exists=%v", tc.id, body, ok) } } if _, ok := DefaultReturnBody(0xffffffff); ok { t.Fatal("unknown contract accepted") } a, _ := DefaultReturnBody(34022227) a[0] = 0 b, _ := DefaultReturnBody(34022227) if b[0] != 0xff { t.Fatal("shared body mutated") } } func TestUnknownMethodCannotUseGuessedID(t *testing.T) { if id := MethodGetMethodIDDefault("InventedMethod", 68030312); id != 0 { t.Fatalf("guessed id accepted: %d", id) } if id := MethodGetMethodIDDefault("SyncEnterScene", 1); id != 64406364 { t.Fatalf("wrong canonical id: %d", id) } } func TestEnterSceneWireLayout(t *testing.T) { b := EncodeEnterScene(100, 23300888, 20001222, 76000888, 9, 15020967, 1, 2, 3, 4) // Offsets derived from C# EnterSceneInfo, SpiritInitData and ServerSimpleGridInfo. // LoadingTypeInfo uses TypeMark=1, byte Type and an Int32 member count. if len(b) != 146 { t.Fatalf("EnterSceneInfo size = %d, want 146", len(b)) } if got := hex.EncodeToString(b[103:111]); got != "0100ff0000000000" { t.Fatalf("LoadingTypeInfo: %s", got) } // LinkSimpleInfo: mark, ulong id, three bytes, uint event, uint time, ulong host. if got := hex.EncodeToString(b[111:139]); got != "01000000000000000000010000000000000000000000000000000000" { t.Fatalf("LinkSimpleInfo: %s", got) } if got := hex.EncodeToString(b[139:]); got != "ff01000000ff01" { t.Fatalf("scene tail: %s", got) } } func TestPlayerInfoPatchesVariableLengthLogin(t *testing.T) { tail := []byte{10, 0, 0, 0, 1, 0xff, 0, 0, 0, 0, 0xff} // Nonempty config buffer exercises dynamic offsets rather than fixed offsets 4/8. original := NewUxWriter().U8(0xff).U8(0xff).Int7(3).Raw([]byte{7, 8, 9}).U8(0xff). I32(1).U64(2).UxString("old").UxString("old name").Raw(tail).Bytes() path := filepath.Join(t.TempDir(), "player.bin") if err := os.WriteFile(path, original, 0600); err != nil { t.Fatal(err) } b, err := EncodePlayerInfo(path, 5944, 100, "longer-account", "辰宿") if err != nil { t.Fatal(err) } r := NewUxReader(b) _, _ = r.Bytes(7) aid, _ := r.I32() pid, _ := r.U64() account, _ := r.UxString() name, _ := r.UxString() if aid != 5944 || pid != 100 || account != "longer-account" || name != "辰宿" { t.Fatalf("login fields: %d %d %s %s", aid, pid, account, name) } if !bytes.Equal(b[r.Position():], tail) { t.Fatal("unrelated player data changed") } for _, bad := range [][]byte{nil, {0}, {0xff, 0xff, 1, 0xff, 0}} { if err := os.WriteFile(path, bad, 0600); err != nil { t.Fatal(err) } if _, err := EncodePlayerInfo(path, 1, 2, "", ""); err == nil { t.Fatalf("accepted malformed template: %x", bad) } } } type shortWriter struct { bytes.Buffer limit int } func (w *shortWriter) Write(b []byte) (int, error) { if len(b) > w.limit { b = b[:w.limit] } return w.Buffer.Write(b) } type zeroWriter struct{} func (zeroWriter) Write([]byte) (int, error) { return 0, nil } func TestWriteFrameHandlesShortWrites(t *testing.T) { w := &shortWriter{limit: 2} if err := WriteFrame(w, 9, []byte{1, 2, 3}, nil); err != nil { t.Fatal(err) } if hex.EncodeToString(w.Bytes()) != "0300000009010203" { t.Fatalf("truncated frame: %x", w.Bytes()) } if err := WriteFrame(zeroWriter{}, 9, nil, nil); err != io.ErrShortWrite { t.Fatalf("zero writer: %v", err) } } func TestVarIntRejectsOverflow(t *testing.T) { for _, b := range [][]byte{{0x80}, {0xff, 0xff, 0xff, 0xff, 0x08}, {0x80, 0x80, 0x80, 0x80, 0x80, 0}} { if _, err := NewUxReader(b).UxString(); err == nil { t.Fatalf("overflow accepted: %x", b) } } if n, err := NewUxReader([]byte{0xff, 0xff, 0xff, 0xff, 0x07}).Raw7(); err != nil || n != 2147483647 { t.Fatalf("max Int32: %d, %v", n, err) } } func TestGroundDataUsesByteFields(t *testing.T) { b := EncodeRaidBattleUnitSpirit(9, 15020967, 1, 2, 3, 4, 100) // GroundData is the last field: marker + byte + ulong + byte + Vector3. if len(b) < 23 || b[len(b)-23] != 0xff { t.Fatalf("invalid GroundData tail: %x", b) } if binary.LittleEndian.Uint64(b[len(b)-21:]) != 0 { t.Fatal("nonzero ground id") } }