package gameserver import ( "encoding/json" "os" "testing" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) func TestLightconeSwapAndUnequipPersistAssignments(t *testing.T) { t.Chdir(t.TempDir()) writeEquipmentFixture(t, `{"lightcones":[{"item_id":23001,"internal_uid":1,"equip_avatar":1201},{"item_id":23002,"internal_uid":2,"equip_avatar":1202}],"relics":[]}`) player := &common.FreesrData{ Avatars: map[uint32]*common.AvatarJson{ 1201: {AvatarID: 1201}, 1202: {AvatarID: 1202}, }, Lightcones: []common.Lightcone{ {ItemID: 23001, InternalUID: 1, EquipAvatar: 1201}, {ItemID: 23002, InternalUID: 2, EquipAvatar: 1202}, }, } session := &Session{json: player} update, err := setLightconeEquipper(session, 1201, 3002) if err != nil { t.Fatalf("equip Light Cone: %v", err) } if player.Lightcones[0].EquipAvatar != 1202 || player.Lightcones[1].EquipAvatar != 1201 { t.Fatalf("Light Cone swap failed: %+v", player.Lightcones) } if len(update.EquipmentList) != 2 || len(update.AvatarSync.AvatarList) != 2 { t.Fatalf("swap did not sync both Light Cones and avatars: %+v", update) } assertSavedEquipmentOwners(t, "lightcones", map[uint32]uint32{1: 1202, 2: 1201}) update, err = setLightconeEquipper(session, 1201, 0) if err != nil { t.Fatalf("unequip Light Cone: %v", err) } if player.Lightcones[1].EquipAvatar != 0 || len(update.EquipmentList) != 1 { t.Fatalf("Light Cone unequip failed: items=%+v update=%+v", player.Lightcones, update) } assertSavedEquipmentOwners(t, "lightcones", map[uint32]uint32{1: 1202, 2: 0}) } func TestRelicSwapUnequipAndSlotValidationPersistAssignments(t *testing.T) { if _, err := supportedRelicIDs(); err != nil { t.Fatalf("preload inventory items: %v", err) } t.Chdir(t.TempDir()) writeEquipmentFixture(t, `{"lightcones":[],"relics":[{"relic_id":31012,"internal_uid":11,"equip_avatar":1201},{"relic_id":31022,"internal_uid":12,"equip_avatar":1202}]}`) player := &common.FreesrData{ Avatars: map[uint32]*common.AvatarJson{ 1201: {AvatarID: 1201}, 1202: {AvatarID: 1202}, }, Relics: []common.Relic{ {RelicID: 31012, InternalUID: 11, EquipAvatar: 1201}, {RelicID: 31022, InternalUID: 12, EquipAvatar: 1202}, }, } session := &Session{json: player} wrongSlot, err := equipRelic(session, &goproto.DressRelicAvatarCsReq{ AvatarId: 1201, SwitchList: []*goproto.DressRelicParam{{RelicType: 1, RelicUniqueId: 12}}, }) if err != nil || wrongSlot != nil { t.Fatalf("mismatched Relic slot should be ignored: update=%+v err=%v", wrongSlot, err) } if player.Relics[0].EquipAvatar != 1201 || player.Relics[1].EquipAvatar != 1202 { t.Fatalf("mismatched Relic slot changed ownership: %+v", player.Relics) } update, err := equipRelic(session, &goproto.DressRelicAvatarCsReq{ AvatarId: 1201, SwitchList: []*goproto.DressRelicParam{{RelicType: 2, RelicUniqueId: 12}}, }) if err != nil { t.Fatalf("equip Relic: %v", err) } if player.Relics[0].EquipAvatar != 1202 || player.Relics[1].EquipAvatar != 1201 { t.Fatalf("Relic swap failed: %+v", player.Relics) } if len(update.RelicList) != 2 || len(update.AvatarSync.AvatarList) != 2 { t.Fatalf("swap did not sync both Relics and avatars: %+v", update) } assertSavedEquipmentOwners(t, "relics", map[uint32]uint32{11: 1202, 12: 1201}) update, err = unequipRelic(session, &goproto.TakeOffRelicCsReq{ AvatarId: 1201, RelicTypeList: []uint32{2}, }) if err != nil { t.Fatalf("unequip Relic: %v", err) } if player.Relics[1].EquipAvatar != 0 || len(update.RelicList) != 1 { t.Fatalf("Relic unequip failed: items=%+v update=%+v", player.Relics, update) } assertSavedEquipmentOwners(t, "relics", map[uint32]uint32{11: 1202, 12: 0}) } func TestEquipmentPickerAuxiliaryRequestsHaveTypedReplies(t *testing.T) { for _, name := range []string{ "RelicPresetGetPlanCsReq", "RelicSmartWearGetPlanCsReq", "RelicSmartWearGetPinRelicCsReq", "GetBigDataRecommendCsReq", } { t.Run(name, func(t *testing.T) { id, ok := goproto.CmdIDByName[name] if !ok { t.Fatalf("protocol table is missing %s", name) } if typedHandlers[uint16(id)] == nil { t.Fatalf("%s is not registered as a typed handler", name) } }) } avatarID := uint32(1201) recommendation := emptyEquipmentRecommendation(avatarID, goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC) responses := []proto.Message{ emptyRelicPresetPlans(avatarID), emptyRelicSmartWearPlans(avatarID), emptyPinnedRelics(avatarID), &recommendation, } for _, response := range responses { encoded, err := proto.Marshal(response) if err != nil { t.Fatalf("marshal %T: %v", response, err) } if len(encoded) == 0 { t.Fatalf("%T encoded as an empty response", response) } } if recommendation.EquipAvatar != avatarID || recommendation.BigDataRecommendType != goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC { t.Fatalf("recommendation response lost request context: %+v", &recommendation) } } func writeEquipmentFixture(t *testing.T, contents string) { t.Helper() if err := os.WriteFile("freesr-data.json", []byte(contents), 0600); err != nil { t.Fatalf("write fixture: %v", err) } } func assertSavedEquipmentOwners(t *testing.T, collection string, expected map[uint32]uint32) { t.Helper() contents, err := os.ReadFile("freesr-data.json") if err != nil { t.Fatalf("read persisted equipment: %v", err) } var root map[string][]struct { InternalUID uint32 `json:"internal_uid"` EquipAvatar uint32 `json:"equip_avatar"` } if err := json.Unmarshal(contents, &root); err != nil { t.Fatalf("decode persisted equipment: %v", err) } owners := make(map[uint32]uint32, len(root[collection])) for _, entry := range root[collection] { owners[entry.InternalUID] = entry.EquipAvatar } for uid, want := range expected { if got, ok := owners[uid]; !ok || got != want { t.Errorf("%s UID %d owner = %d (present %v), want %d", collection, uid, got, ok, want) } } }