582 lines
18 KiB
Go
582 lines
18 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"sync"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
// CURRENT_BATTLE mirrors battle.rs CURRENT_BATTLE (token -> SceneBattleInfo).
|
|
var CURRENT_BATTLE syncMap
|
|
|
|
type syncMap struct {
|
|
m map[uint32]*goproto.SceneBattleInfo
|
|
}
|
|
|
|
func (sm *syncMap) init() {
|
|
if sm.m == nil {
|
|
sm.m = map[uint32]*goproto.SceneBattleInfo{}
|
|
}
|
|
}
|
|
func (sm *syncMap) Store(k uint32, v *goproto.SceneBattleInfo) {
|
|
muBattle.Lock()
|
|
sm.init()
|
|
sm.m[k] = v
|
|
muBattle.Unlock()
|
|
}
|
|
func (sm *syncMap) Load(k uint32) (*goproto.SceneBattleInfo, bool) {
|
|
muBattle.Lock()
|
|
defer muBattle.Unlock()
|
|
sm.init()
|
|
v, ok := sm.m[k]
|
|
return v, ok
|
|
}
|
|
func (sm *syncMap) Delete(k uint32) { muBattle.Lock(); sm.init(); delete(sm.m, k); muBattle.Unlock() }
|
|
|
|
func randUint32() uint32 { return rand.Uint32() }
|
|
|
|
func emptyRelicPresetPlans(avatarID uint32) *goproto.RelicPresetGetPlanScRsp {
|
|
return &goproto.RelicPresetGetPlanScRsp{AvatarId: avatarID}
|
|
}
|
|
|
|
func emptyRelicSmartWearPlans(avatarID uint32) *goproto.RelicSmartWearGetPlanScRsp {
|
|
return &goproto.RelicSmartWearGetPlanScRsp{AvatarId: avatarID}
|
|
}
|
|
|
|
func emptyPinnedRelics(avatarID uint32) *goproto.RelicSmartWearGetPinRelicScRsp {
|
|
return &goproto.RelicSmartWearGetPinRelicScRsp{AvatarId: avatarID}
|
|
}
|
|
|
|
func emptyEquipmentRecommendation(avatarID uint32, kind goproto.BigDataRecommendType) goproto.GetBigDataRecommendScRsp {
|
|
return goproto.GetBigDataRecommendScRsp{EquipAvatar: avatarID, BigDataRecommendType: kind}
|
|
}
|
|
|
|
func playerArchive(s *Session) *goproto.ArchiveData {
|
|
archive := &goproto.ArchiveData{}
|
|
if player := s.JSON(); player != nil {
|
|
for id := range player.Avatars {
|
|
archive.ArchiveAvatarIdList = append(archive.ArchiveAvatarIdList, id)
|
|
}
|
|
sortU32Slice(archive.ArchiveAvatarIdList)
|
|
seen := map[uint32]bool{}
|
|
for _, equipment := range player.Lightcones {
|
|
if !seen[equipment.ItemID] {
|
|
seen[equipment.ItemID] = true
|
|
archive.ArchiveEquipmentIdList = append(archive.ArchiveEquipmentIdList, equipment.ItemID)
|
|
}
|
|
}
|
|
sortU32Slice(archive.ArchiveEquipmentIdList)
|
|
}
|
|
return archive
|
|
}
|
|
|
|
func init() {
|
|
// AdventureModule._OnGetCurBattleInfoScRsp dereferences BattleInfo even
|
|
// when nothing is running (Player.log 2026-10-24 NRE on an empty reply);
|
|
// the isolated reference answers with an empty, non-null SceneBattleInfo.
|
|
handle("GetCurBattleInfo", func(s *Session, req *goproto.GetCurBattleInfoCsReq, rsp *goproto.GetCurBattleInfoScRsp) error {
|
|
rsp.BattleInfo = &goproto.SceneBattleInfo{}
|
|
return nil
|
|
})
|
|
|
|
handle("GetBag", func(s *Session, req *goproto.GetBagCsReq, rsp *goproto.GetBagScRsp) error {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil
|
|
}
|
|
if err := ensureCatalogLightcones(player); err != nil {
|
|
return err
|
|
}
|
|
if err := ensureCatalogRelics(player); err != nil {
|
|
return err
|
|
}
|
|
for i := range player.Lightcones {
|
|
rsp.EquipmentList = append(rsp.EquipmentList, player.Lightcones[i].ToProtoEquipment())
|
|
}
|
|
relics, omitted, err := supportedProtoRelics(player.Relics)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if omitted > 0 {
|
|
Log(fmt.Sprintf("[INVENTORY] omitted %d Relic entries without a Client RelicConfig row", omitted))
|
|
}
|
|
rsp.RelicList = append(rsp.RelicList, relics...)
|
|
materials, err := bagMaterials(player)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rsp.MaterialList = materials
|
|
// Avatar IDs do not imply item IDs. The old 130000+avatar formula
|
|
// produced nonexistent materials and broke the client's inventory init.
|
|
return nil
|
|
})
|
|
|
|
handle("GetArchiveData", func(s *Session, req *goproto.GetArchiveDataCsReq, rsp *goproto.GetArchiveDataScRsp) error {
|
|
rsp.Retcode = 0
|
|
rsp.ArchiveData = playerArchive(s)
|
|
return nil
|
|
})
|
|
handle("GetUpdatedArchiveData", func(s *Session, req *goproto.GetUpdatedArchiveDataCsReq, rsp *goproto.GetUpdatedArchiveDataScRsp) error {
|
|
rsp.ArchiveData = playerArchive(s)
|
|
return nil
|
|
})
|
|
|
|
// These requests are made while the Client builds equipment/relic
|
|
// selection panels. An empty raw response is not equivalent to a valid
|
|
// empty list: the Client uses the echoed AvatarId to complete the request.
|
|
handle("RelicPresetGetPlan", func(_ *Session, req *goproto.RelicPresetGetPlanCsReq, rsp *goproto.RelicPresetGetPlanScRsp) error {
|
|
*rsp = *emptyRelicPresetPlans(req.AvatarId)
|
|
return nil
|
|
})
|
|
handle("RelicSmartWearGetPlan", func(_ *Session, req *goproto.RelicSmartWearGetPlanCsReq, rsp *goproto.RelicSmartWearGetPlanScRsp) error {
|
|
*rsp = *emptyRelicSmartWearPlans(req.AvatarId)
|
|
return nil
|
|
})
|
|
handle("RelicSmartWearGetPinRelic", func(_ *Session, req *goproto.RelicSmartWearGetPinRelicCsReq, rsp *goproto.RelicSmartWearGetPinRelicScRsp) error {
|
|
*rsp = *emptyPinnedRelics(req.AvatarId)
|
|
return nil
|
|
})
|
|
handle("GetBigDataRecommend", func(_ *Session, req *goproto.GetBigDataRecommendCsReq, rsp *goproto.GetBigDataRecommendScRsp) error {
|
|
*rsp = emptyEquipmentRecommendation(req.EquipAvatar, req.BigDataRecommendType)
|
|
return nil
|
|
})
|
|
|
|
// --- lightcone equipping (Rust: set_lightcone_equipper) ---
|
|
handle("DressAvatar", func(s *Session, req *goproto.DressAvatarCsReq, rsp *goproto.DressAvatarScRsp) error {
|
|
pkt, err := setLightconeEquipper(s, req.AvatarId, req.EquipmentUniqueId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pkt != nil {
|
|
return s.Send(pkt)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("TakeOffEquipment", func(s *Session, req *goproto.TakeOffEquipmentCsReq, rsp *goproto.TakeOffEquipmentScRsp) error {
|
|
pkt, err := setLightconeEquipper(s, req.AvatarId, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pkt != nil {
|
|
return s.Send(pkt)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("DressRelicAvatar", func(s *Session, req *goproto.DressRelicAvatarCsReq, rsp *goproto.DressRelicAvatarScRsp) error {
|
|
rsp.AvatarId = req.AvatarId
|
|
pkt, err := equipRelic(s, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pkt != nil {
|
|
return s.Send(pkt)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("TakeOffRelic", func(s *Session, req *goproto.TakeOffRelicCsReq, rsp *goproto.TakeOffRelicScRsp) error {
|
|
pkt, err := unequipRelic(s, req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pkt != nil {
|
|
return s.Send(pkt)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("RankUpAvatar", func(s *Session, req *goproto.RankUpAvatarCsReq, rsp *goproto.RankUpAvatarScRsp) error {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil
|
|
}
|
|
avatar, ok := player.Avatars[req.AvatarId]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
maximum := res.AvatarConfigs[req.AvatarId].MaxRank
|
|
if maximum == 0 || req.Rank > maximum {
|
|
rsp.Retcode = uint32(goproto.Retcode_RET_AVATAR_RANK_REACH_MAX)
|
|
return nil
|
|
}
|
|
if req.Rank != avatar.Data.Rank+1 {
|
|
rsp.Retcode = uint32(goproto.Retcode_RET_AVATAR_RANK_NOT_MATCH)
|
|
return nil
|
|
}
|
|
if _, active := CURRENT_BATTLE.Load(s.Token); active {
|
|
rsp.Retcode = uint32(goproto.Retcode_RET_AVATAR_RANK_NOT_MATCH)
|
|
return nil
|
|
}
|
|
// Ordinary Eidolons do not implicitly unlock EX, even at the final rank.
|
|
materials, err := eidolonMaterials(player)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := common.SaveAvatarRank(req.AvatarId, req.Rank); err != nil {
|
|
return err
|
|
}
|
|
avatar.Data.Rank = req.Rank
|
|
materials, err = eidolonMaterials(player)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ret := &goproto.PlayerSyncScNotify{}
|
|
buildSync(player, ret, []uint32{avatar.AvatarID}, nil, nil)
|
|
ret.MaterialList = materials
|
|
return s.Send(ret)
|
|
})
|
|
|
|
handle("GetBigDataAllRecommend", func(s *Session, req *goproto.GetBigDataAllRecommendCsReq, rsp *goproto.GetBigDataAllRecommendScRsp) error {
|
|
rsp.BigDataRecommendType = req.BigDataRecommendType
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
switch req.BigDataRecommendType {
|
|
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_RELIC_AVATAR:
|
|
inner := &goproto.BigDataRecommendRelicAvatar{}
|
|
setIDs := sortedU32Keys2(res.RelicAvatarRecommend)
|
|
for _, setID := range setIDs {
|
|
list := res.RelicAvatarRecommend[setID]
|
|
first := uint32(0)
|
|
if len(list) > 0 {
|
|
first = list[0]
|
|
}
|
|
inner.RecommendedAvatarInfoList = append(inner.RecommendedAvatarInfoList, &goproto.RecomendedAvatarInfo{
|
|
AvatarIdList: list,
|
|
RecommendAvatarId: first,
|
|
RelicSetId: setID,
|
|
})
|
|
}
|
|
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_RelicAvatar{RelicAvatar: inner}
|
|
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC:
|
|
// No AvatarRelicRecommend rows are available in the loaded resource.
|
|
// Empty placeholder rows crash the Client while it reads sub-properties.
|
|
inner := &goproto.BigDataRecommendAvatarRelic{}
|
|
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_AvatarRelic{AvatarRelic: inner}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// --- mail ---
|
|
handle("GetMail", func(s *Session, req *goproto.GetMailCsReq, rsp *goproto.GetMailScRsp) error {
|
|
rsp.IsEnd = true
|
|
rsp.MailList = append(rsp.MailList, &goproto.ClientMail{
|
|
Title: "Welcome!",
|
|
Sender: "Server",
|
|
Content: "Welcome!",
|
|
Id: 1,
|
|
Time: 1716041089,
|
|
ExpireTime: 1718633089,
|
|
Attachment: &goproto.ItemList{
|
|
ItemList: []*goproto.Item{{ItemId: 1310, Level: 80, Num: 1}},
|
|
},
|
|
})
|
|
return nil
|
|
})
|
|
|
|
// --- chat/friends (empty-but-valid) ---
|
|
handle("GetPrivateChatHistory", func(s *Session, req *goproto.GetPrivateChatHistoryCsReq, rsp *goproto.GetPrivateChatHistoryScRsp) error {
|
|
rsp.Retcode = 0
|
|
return nil
|
|
})
|
|
handle("GetFriendListInfo", func(s *Session, req *goproto.GetFriendListInfoCsReq, rsp *goproto.GetFriendListInfoScRsp) error {
|
|
rsp.Retcode = 0
|
|
return nil
|
|
})
|
|
handle("GetFriendLoginInfo", func(s *Session, req *goproto.GetFriendLoginInfoCsReq, rsp *goproto.GetFriendLoginInfoScRsp) error {
|
|
rsp.Retcode = 0
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// equip helpers (Go port of inventory.rs helper fns)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func setLightconeEquipper(s *Session, targetAvatarID, targetLCUID uint32) (*goproto.PlayerSyncScNotify, error) {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil, nil
|
|
}
|
|
targetAvatar, ok := player.Avatars[targetAvatarID]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
ret := &goproto.PlayerSyncScNotify{}
|
|
|
|
curAvatarLCIdx := -1
|
|
targetLCIdx := -1
|
|
for i := range player.Lightcones {
|
|
if player.Lightcones[i].EquipAvatar == targetAvatar.AvatarID {
|
|
curAvatarLCIdx = i
|
|
}
|
|
if player.Lightcones[i].UniqueID() == targetLCUID {
|
|
targetLCIdx = i
|
|
}
|
|
}
|
|
|
|
// undress
|
|
if targetLCUID == 0 {
|
|
if curAvatarLCIdx >= 0 {
|
|
lc := &player.Lightcones[curAvatarLCIdx]
|
|
previousOwner := lc.EquipAvatar
|
|
lc.EquipAvatar = 0
|
|
if err := common.SaveEquipmentAssignments(map[uint32]uint32{lc.InternalUID: 0}, nil); err != nil {
|
|
lc.EquipAvatar = previousOwner
|
|
return nil, err
|
|
}
|
|
buildSync(player, ret, []uint32{targetAvatar.AvatarID}, []int{curAvatarLCIdx}, nil)
|
|
return ret, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
if targetLCIdx < 0 {
|
|
return nil, nil
|
|
}
|
|
if targetLCIdx == curAvatarLCIdx {
|
|
return nil, nil
|
|
}
|
|
|
|
var avatarsSync []uint32
|
|
var lcIdxSync []int
|
|
previousOwners := make(map[uint32]uint32, 2)
|
|
remember := func(idx int) {
|
|
lc := &player.Lightcones[idx]
|
|
previousOwners[lc.InternalUID] = lc.EquipAvatar
|
|
}
|
|
|
|
// if the current avatar holds an LC, swap owners with the target LC's owner
|
|
if curAvatarLCIdx >= 0 {
|
|
remember(curAvatarLCIdx)
|
|
remember(targetLCIdx)
|
|
player.Lightcones[curAvatarLCIdx].EquipAvatar = player.Lightcones[targetLCIdx].EquipAvatar
|
|
}
|
|
avatarsSync = append(avatarsSync, player.Lightcones[targetLCIdx].EquipAvatar, targetAvatar.AvatarID)
|
|
player.Lightcones[targetLCIdx].EquipAvatar = targetAvatar.AvatarID
|
|
if curAvatarLCIdx < 0 {
|
|
remember(targetLCIdx)
|
|
}
|
|
owners := make(map[uint32]uint32, len(previousOwners))
|
|
for i := range player.Lightcones {
|
|
if _, changed := previousOwners[player.Lightcones[i].InternalUID]; changed {
|
|
owners[player.Lightcones[i].InternalUID] = player.Lightcones[i].EquipAvatar
|
|
}
|
|
}
|
|
if err := common.SaveEquipmentAssignments(owners, nil); err != nil {
|
|
for i := range player.Lightcones {
|
|
if owner, changed := previousOwners[player.Lightcones[i].InternalUID]; changed {
|
|
player.Lightcones[i].EquipAvatar = owner
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if curAvatarLCIdx >= 0 {
|
|
lcIdxSync = append(lcIdxSync, curAvatarLCIdx)
|
|
}
|
|
lcIdxSync = append(lcIdxSync, targetLCIdx)
|
|
|
|
buildSync(player, ret, avatarsSync, lcIdxSync, nil)
|
|
return ret, nil
|
|
}
|
|
|
|
func equipRelic(s *Session, req *goproto.DressRelicAvatarCsReq) (*goproto.PlayerSyncScNotify, error) {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil, nil
|
|
}
|
|
supportedRelics, err := supportedRelicIDs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetAvatar, ok := player.Avatars[req.AvatarId]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
ret := &goproto.PlayerSyncScNotify{}
|
|
var avatarIDsToSync []uint32
|
|
var relicIdxToSync []int
|
|
previousOwners := make(map[uint32]uint32)
|
|
|
|
for _, param := range req.SwitchList {
|
|
targetRelicIdx := -1
|
|
for i := range player.Relics {
|
|
if player.Relics[i].UniqueID() == param.RelicUniqueId {
|
|
targetRelicIdx = i
|
|
break
|
|
}
|
|
}
|
|
if targetRelicIdx < 0 {
|
|
continue
|
|
}
|
|
if _, supported := supportedRelics[player.Relics[targetRelicIdx].RelicID]; !supported {
|
|
continue
|
|
}
|
|
// The Client supplies both the desired slot and the inventory UID.
|
|
// Ignore malformed/stale requests rather than equipping a Relic into a
|
|
// different slot than the one represented by its configured item ID.
|
|
if player.Relics[targetRelicIdx].Slot() != param.RelicType {
|
|
continue
|
|
}
|
|
|
|
curAvatarRelicIdx := -1
|
|
for i := range player.Relics {
|
|
r := &player.Relics[i]
|
|
if r.EquipAvatar == targetAvatar.AvatarID && r.Slot() == param.RelicType {
|
|
curAvatarRelicIdx = i
|
|
break
|
|
}
|
|
}
|
|
|
|
if curAvatarRelicIdx >= 0 {
|
|
previousOwners[player.Relics[curAvatarRelicIdx].InternalUID] = player.Relics[curAvatarRelicIdx].EquipAvatar
|
|
previousOwners[player.Relics[targetRelicIdx].InternalUID] = player.Relics[targetRelicIdx].EquipAvatar
|
|
avatarIDsToSync = append(avatarIDsToSync, player.Relics[curAvatarRelicIdx].EquipAvatar)
|
|
player.Relics[curAvatarRelicIdx].EquipAvatar = player.Relics[targetRelicIdx].EquipAvatar
|
|
relicIdxToSync = append(relicIdxToSync, curAvatarRelicIdx)
|
|
}
|
|
|
|
avatarIDsToSync = append(avatarIDsToSync, player.Relics[targetRelicIdx].EquipAvatar)
|
|
if _, remembered := previousOwners[player.Relics[targetRelicIdx].InternalUID]; !remembered {
|
|
previousOwners[player.Relics[targetRelicIdx].InternalUID] = player.Relics[targetRelicIdx].EquipAvatar
|
|
}
|
|
player.Relics[targetRelicIdx].EquipAvatar = targetAvatar.AvatarID
|
|
avatarIDsToSync = append(avatarIDsToSync, player.Relics[targetRelicIdx].EquipAvatar)
|
|
relicIdxToSync = append(relicIdxToSync, targetRelicIdx)
|
|
}
|
|
if len(relicIdxToSync) == 0 {
|
|
return nil, nil
|
|
}
|
|
owners := make(map[uint32]uint32, len(previousOwners))
|
|
for i := range player.Relics {
|
|
if _, changed := previousOwners[player.Relics[i].InternalUID]; changed {
|
|
owners[player.Relics[i].InternalUID] = player.Relics[i].EquipAvatar
|
|
}
|
|
}
|
|
if err := common.SaveEquipmentAssignments(nil, owners); err != nil {
|
|
for i := range player.Relics {
|
|
if owner, changed := previousOwners[player.Relics[i].InternalUID]; changed {
|
|
player.Relics[i].EquipAvatar = owner
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
buildSync(player, ret, avatarIDsToSync, nil, relicIdxToSync)
|
|
return ret, nil
|
|
}
|
|
|
|
func unequipRelic(s *Session, req *goproto.TakeOffRelicCsReq) (*goproto.PlayerSyncScNotify, error) {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil, nil
|
|
}
|
|
if _, ok := player.Avatars[req.AvatarId]; !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
ret := &goproto.PlayerSyncScNotify{}
|
|
var relicIdxToSync []int
|
|
previousOwners := make(map[uint32]uint32)
|
|
|
|
for _, slot := range req.RelicTypeList {
|
|
for i := range player.Relics {
|
|
r := &player.Relics[i]
|
|
if r.EquipAvatar == req.AvatarId && r.Slot() == slot {
|
|
previousOwners[r.InternalUID] = r.EquipAvatar
|
|
r.EquipAvatar = 0
|
|
relicIdxToSync = append(relicIdxToSync, i)
|
|
}
|
|
}
|
|
}
|
|
if len(relicIdxToSync) == 0 {
|
|
return nil, nil
|
|
}
|
|
owners := make(map[uint32]uint32, len(previousOwners))
|
|
for i := range player.Relics {
|
|
if _, changed := previousOwners[player.Relics[i].InternalUID]; changed {
|
|
owners[player.Relics[i].InternalUID] = player.Relics[i].EquipAvatar
|
|
}
|
|
}
|
|
if err := common.SaveEquipmentAssignments(nil, owners); err != nil {
|
|
for i := range player.Relics {
|
|
if owner, changed := previousOwners[player.Relics[i].InternalUID]; changed {
|
|
player.Relics[i].EquipAvatar = owner
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
buildSync(player, ret, []uint32{req.AvatarId}, nil, relicIdxToSync)
|
|
return ret, nil
|
|
}
|
|
|
|
func buildSync(
|
|
player *common.FreesrData,
|
|
ret *goproto.PlayerSyncScNotify,
|
|
avatarIDs []uint32,
|
|
lightconeIndexes []int,
|
|
relicIndexes []int,
|
|
) {
|
|
supportedRelics, supportedErr := supportedRelicIDs()
|
|
if supportedErr != nil {
|
|
Log("[INVENTORY] cannot resolve Client RelicConfig; omitting Relic sync: " + supportedErr.Error())
|
|
supportedRelics = map[uint32]struct{}{}
|
|
}
|
|
sync := &goproto.AvatarSync{}
|
|
seenAvatars := make(map[uint32]bool, len(avatarIDs))
|
|
for _, id := range avatarIDs {
|
|
if seenAvatars[id] {
|
|
continue
|
|
}
|
|
seenAvatars[id] = true
|
|
if av, ok := player.Avatars[id]; ok {
|
|
sync.AvatarList = append(sync.AvatarList, av.ToAvatarProto(player.FindLightconeFor(id), player.MainCharacter.ID(), player.MarchType.ID()))
|
|
sync.AvatarPathDataInfoList = append(sync.AvatarPathDataInfoList,
|
|
av.ToAvatarPathDataProto(player.FindLightconeFor(id), filterSupportedRelics(player.FindRelicsFor(id), supportedRelics), player.DressedSkins[id]))
|
|
}
|
|
}
|
|
ret.AvatarSync = sync
|
|
|
|
seenRelics := make(map[int]bool, len(relicIndexes))
|
|
for _, idx := range relicIndexes {
|
|
if seenRelics[idx] {
|
|
continue
|
|
}
|
|
seenRelics[idx] = true
|
|
if idx >= 0 && idx < len(player.Relics) {
|
|
if _, supported := supportedRelics[player.Relics[idx].RelicID]; !supported {
|
|
continue
|
|
}
|
|
ret.RelicList = append(ret.RelicList, player.Relics[idx].ToProtoRelic())
|
|
}
|
|
}
|
|
seenLightcones := make(map[int]bool, len(lightconeIndexes))
|
|
for _, idx := range lightconeIndexes {
|
|
if seenLightcones[idx] {
|
|
continue
|
|
}
|
|
seenLightcones[idx] = true
|
|
if idx >= 0 && idx < len(player.Lightcones) {
|
|
ret.EquipmentList = append(ret.EquipmentList, player.Lightcones[idx].ToProtoEquipment())
|
|
}
|
|
}
|
|
}
|
|
|
|
var muBattle sync.Mutex
|