pre cw with probe and a bunch script to read game behavior

This commit is contained in:
CAtlas0784 committed 2026-10-21 20:40:13 +07:00
1 parent 5120219ae2
commit da8a32f60d
310 files changed
+219693 -872

No files matched your search

+200 -42
View File
@@ -1,6 +1,7 @@
package gameserver
import (
"fmt"
"math/rand"
"sync"
@@ -37,6 +38,22 @@ func (sm *syncMap) Delete(k uint32) { muBattle.Lock(); sm.init(); delete(sm.m, k
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 {
@@ -62,32 +79,28 @@ func init() {
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())
}
for i := range player.Relics {
rsp.RelicList = append(rsp.RelicList, player.Relics[i].ToProtoRelic())
}
rsp.MaterialList = []*goproto.Material{
{Tid: 1, Num: 9999999}, // Stellar Jade
{Tid: 2, Num: 99999999}, // Credit
{Tid: 101, Num: 999999}, // Normal Pass
{Tid: 102, Num: 999999}, // Special Pass
}
materials, err := eidolonMaterials(player)
relics, omitted, err := supportedProtoRelics(player.Relics)
if err != nil {
return err
}
for _, material := range materials {
if material.Num > 0 {
rsp.MaterialList = append(rsp.MaterialList, material)
}
if omitted > 0 {
Log(fmt.Sprintf("[INVENTORY] omitted %d Relic entries without a Client RelicConfig row", omitted))
}
foods, err := techniqueMaterials(player)
rsp.RelicList = append(rsp.RelicList, relics...)
materials, err := bagMaterials(player)
if err != nil {
return err
}
rsp.MaterialList = append(rsp.MaterialList, foods...)
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
@@ -103,30 +116,67 @@ func init() {
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 {
if pkt := setLightconeEquipper(s, req.AvatarId, req.EquipmentUniqueId); pkt != nil {
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 {
if pkt := setLightconeEquipper(s, req.AvatarId, 0); pkt != nil {
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 {
if pkt := equipRelic(s, req); pkt != nil {
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 {
if pkt := unequipRelic(s, req); pkt != nil {
pkt, err := unequipRelic(s, req)
if err != nil {
return err
}
if pkt != nil {
return s.Send(pkt)
}
return nil
@@ -203,12 +253,9 @@ func init() {
}
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_RelicAvatar{RelicAvatar: inner}
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC:
// Loaded schema: recomended_relic_info_list is repeated GDFKJKDLEPM
// (obfuscated, contents unused by this PS) — reply with empty entries
// No AvatarRelicRecommend rows are available in the loaded resource.
// Empty placeholder rows crash the Client while it reads sub-properties.
inner := &goproto.BigDataRecommendAvatarRelic{}
for range BaseAvatarIDs {
inner.RecomendedRelicInfoList = append(inner.RecomendedRelicInfoList, &goproto.GDFKJKDLEPM{})
}
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_AvatarRelic{AvatarRelic: inner}
}
return nil
@@ -287,14 +334,14 @@ func init() {
// equip helpers (Go port of inventory.rs helper fns)
// ---------------------------------------------------------------------------
func setLightconeEquipper(s *Session, targetAvatarID, targetLCUID uint32) *goproto.PlayerSyncScNotify {
func setLightconeEquipper(s *Session, targetAvatarID, targetLCUID uint32) (*goproto.PlayerSyncScNotify, error) {
player := s.JSON()
if player == nil {
return nil
return nil, nil
}
targetAvatar, ok := player.Avatars[targetAvatarID]
if !ok {
return nil
return nil, nil
}
ret := &goproto.PlayerSyncScNotify{}
@@ -313,26 +360,59 @@ func setLightconeEquipper(s *Session, targetAvatarID, targetLCUID uint32) *gopro
// undress
if targetLCUID == 0 {
if curAvatarLCIdx >= 0 {
player.Lightcones[curAvatarLCIdx].EquipAvatar = 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
return ret, nil
}
return nil
return nil, nil
}
if targetLCIdx < 0 {
return nil
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)
@@ -340,22 +420,27 @@ func setLightconeEquipper(s *Session, targetAvatarID, targetLCUID uint32) *gopro
lcIdxSync = append(lcIdxSync, targetLCIdx)
buildSync(player, ret, avatarsSync, lcIdxSync, nil)
return ret
return ret, nil
}
func equipRelic(s *Session, req *goproto.DressRelicAvatarCsReq) *goproto.PlayerSyncScNotify {
func equipRelic(s *Session, req *goproto.DressRelicAvatarCsReq) (*goproto.PlayerSyncScNotify, error) {
player := s.JSON()
if player == nil {
return nil
return nil, nil
}
supportedRelics, err := supportedRelicIDs()
if err != nil {
return nil, err
}
targetAvatar, ok := player.Avatars[req.AvatarId]
if !ok {
return nil
return nil, nil
}
ret := &goproto.PlayerSyncScNotify{}
var avatarIDsToSync []uint32
var relicIdxToSync []int
previousOwners := make(map[uint32]uint32)
for _, param := range req.SwitchList {
targetRelicIdx := -1
@@ -368,6 +453,15 @@ func equipRelic(s *Session, req *goproto.DressRelicAvatarCsReq) *goproto.PlayerS
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 {
@@ -379,45 +473,86 @@ func equipRelic(s *Session, req *goproto.DressRelicAvatarCsReq) *goproto.PlayerS
}
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
return ret, nil
}
func unequipRelic(s *Session, req *goproto.TakeOffRelicCsReq) *goproto.PlayerSyncScNotify {
func unequipRelic(s *Session, req *goproto.TakeOffRelicCsReq) (*goproto.PlayerSyncScNotify, error) {
player := s.JSON()
if player == nil {
return nil
return nil, nil
}
if _, ok := player.Avatars[req.AvatarId]; !ok {
return nil
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
return ret, nil
}
func buildSync(
@@ -427,22 +562,45 @@ func buildSync(
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), player.FindRelicsFor(id), player.DressedSkins[id]))
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())
}