package gameserver import ( "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "sort" ) func init() { handle("SetAvatarPath", func(s *Session, req *goproto.SetAvatarPathCsReq, rsp *goproto.SetAvatarPathScRsp) error { player := s.JSON() if player == nil { return nil } supportedRelics, err := supportedRelicIDs() if err != nil { return err } id := uint32(req.AvatarId) path := common.MultiPathAvatarFromID(id) if player.Avatars[id] == nil || (!common.IsMainCharacter(id) && !common.IsMarch7th(id)) || (common.IsMainCharacter(id) && path.Gender() != player.MainCharacter.Gender()) { rsp.Retcode = uint32(goproto.Retcode_RET_AVATAR_PATH_NOT_MATCH) return nil } if _, active := CURRENT_BATTLE.Load(s.Token); active { rsp.Retcode = uint32(goproto.Retcode_RET_LINEUP_NOT_ALLOW_EDIT) return nil } previousMC, previousMarch := player.MainCharacter, player.MarchType if common.IsMainCharacter(id) { player.MainCharacter = path } else { player.MarchType = path } if err := player.SavePersistentChecked(); err != nil { player.MainCharacter, player.MarchType = previousMC, previousMarch return err } rsp.AvatarId = req.AvatarId // Match services/avatar.zig at CommandSourceRevision: path change, // changed avatar data, refreshed lineup, then the single response. if err := s.Send(&goproto.AvatarPathChangedNotify{BaseAvatarId: common.BaseAvatarID(id), CurMultiPathAvatarType: req.AvatarId}); err != nil { return err } avatarSync := &goproto.AvatarSync{} for _, baseID := range BaseAvatarIDs { if av := player.Avatars[baseID]; av != nil { avatarSync.AvatarList = append(avatarSync.AvatarList, av.ToAvatarProto(player.FindLightconeFor(baseID), player.MainCharacter.ID(), player.MarchType.ID())) } } for _, pathID := range sortedU32Keys2(player.Avatars) { av := player.Avatars[pathID] avatarSync.AvatarPathDataInfoList = append(avatarSync.AvatarPathDataInfoList, av.ToAvatarPathDataProto(player.FindLightconeFor(pathID), filterSupportedRelics(player.FindRelicsFor(pathID), supportedRelics), player.DressedSkins[pathID])) } if err := s.Send(&goproto.PlayerSyncScNotify{AvatarSync: avatarSync}); err != nil { return err } return refreshLineup(s) }) handle("GetAvatarData", func(s *Session, req *goproto.GetAvatarDataCsReq, rsp *goproto.GetAvatarDataScRsp) error { player := s.JSON() if player == nil { Log("data is not set!") return nil } supportedRelics, err := supportedRelicIDs() if err != nil { return err } rsp.IsGetAll = req.IsGetAll for _, id := range BaseAvatarIDs { if av, ok := player.Avatars[id]; ok { rsp.AvatarList = append(rsp.AvatarList, av.ToAvatarProto(player.FindLightconeFor(id), player.MainCharacter.ID(), player.MarchType.ID())) } else { // avatar without save data: report a level-80 default rsp.AvatarList = append(rsp.AvatarList, &goproto.Avatar{ BaseAvatarId: id, Level: 80, Promotion: 6, FirstMetTimestamp: 1712924677, HasTakenPromotionRewardList: []uint32{1, 3, 5}, }) } } for _, av := range player.Avatars { rsp.AvatarPathDataInfoList = append(rsp.AvatarPathDataInfoList, av.ToAvatarPathDataProto(player.FindLightconeFor(av.AvatarID), filterSupportedRelics(player.FindRelicsFor(av.AvatarID), supportedRelics), player.DressedSkins[av.AvatarID])) } // unlocked skins: res.json skinIds + currently dressed ones seen := map[uint32]bool{} for _, id := range player.DressedSkins { if id != 0 && !seen[id] { seen[id] = true rsp.SkinList = append(rsp.SkinList, id) } } if res, err := common.GameRes(); err == nil { for _, id := range res.SkinIDs { if id != 0 && !seen[id] { seen[id] = true rsp.SkinList = append(rsp.SkinList, id) } } } cosmetics, err := common.LoadPlayerCosmetics() if err != nil { return err } for _, skin := range cosmetics.Skins { id := skin.ID if !seen[id] { seen[id] = true rsp.SkinList = append(rsp.SkinList, id) } } sort.Slice(rsp.SkinList, func(i, j int) bool { return rsp.SkinList[i] < rsp.SkinList[j] }) rsp.OutfitList = &goproto.OutfitInfo{Outfits: append([]uint32{}, player.PlayerOutfits...)} return nil }) handle("DressAvatarSkin", func(s *Session, req *goproto.DressAvatarSkinCsReq, rsp *goproto.DressAvatarSkinScRsp) error { player := s.JSON() if player == nil { return nil } player.DressedSkins[req.AvatarId] = req.AvatarSkinId player.SavePersistent() return nil }) handle("TakeOffAvatarSkin", func(s *Session, req *goproto.TakeOffAvatarSkinCsReq, rsp *goproto.TakeOffAvatarSkinScRsp) error { player := s.JSON() if player == nil { return nil } delete(player.DressedSkins, req.AvatarId) player.SavePersistent() return nil }) handle("SetPlayerOutfit", func(s *Session, req *goproto.SetPlayerOutfitCsReq, rsp *goproto.SetPlayerOutfitScRsp) error { player := s.JSON() if player == nil || req.OutfitList == nil { return nil } // Outfit IDs are supplied by the Client's outfit selector. They are // distinct from ownership item IDs; do not invent an ID conversion. // PearlSR 005d2c12 services/avatar.zig echoes the same wire structure. previous := player.PlayerOutfits player.PlayerOutfits = append([]uint32{}, req.OutfitList.Outfits...) if err := player.SavePersistentChecked(); err != nil { player.PlayerOutfits = previous return err } if err := s.Send(rsp); err != nil { return err } if err := s.Send(&goproto.PlayerSyncScNotify{OutfitList: &goproto.OutfitInfo{Outfits: player.PlayerOutfits}}); err != nil { return err } return errResponseAlreadySent }) handle("TakePromotionReward", func(s *Session, req *goproto.TakePromotionRewardCsReq, rsp *goproto.TakePromotionRewardScRsp) error { return nil }) handle("SetAvatarEnhancedId", func(s *Session, req *goproto.SetAvatarEnhancedIdCsReq, rsp *goproto.SetAvatarEnhancedIdScRsp) error { return nil }) }