package common import ( goproto "gac-hkrpg-go/protocol/proto" ) // This file mirrors the proto conversion impls from the Rust crate // (common/src/structs/{avatar,relic,lightcone,monster,scene}.rs). // --------------------------------------------------------------------------- // Lightcone // --------------------------------------------------------------------------- func (l *Lightcone) ToProtoEquipment() *goproto.Equipment { return &goproto.Equipment{ Level: l.Level, Promotion: l.Promotion, Rank: l.Rank, Tid: l.ItemID, UniqueId: l.UniqueID(), EquipAvatarId: l.EquipAvatar, } } func (l *Lightcone) ToBattleEquipment() *goproto.BattleEquipment { return &goproto.BattleEquipment{ Id: l.ItemID, Level: l.Level, Promotion: l.Promotion, Rank: l.Rank, } } // --------------------------------------------------------------------------- // Relic // --------------------------------------------------------------------------- func relicAffixes(subs []SubAffix) []*goproto.RelicAffix { out := make([]*goproto.RelicAffix, 0, len(subs)) for _, s := range subs { out = append(out, &goproto.RelicAffix{AffixId: s.SubAffixID, Cnt: s.Count, Step: s.Step}) } return out } func (r *Relic) ToBattleRelic() *goproto.BattleRelic { // Match PearlSR's standalone battle relic format. Inventory UniqueID is // only sent by ToProtoRelic/ToEquipRelic; battle carries the full affixes. return &goproto.BattleRelic{ Id: r.RelicID, Level: r.Level, MainAffixId: r.MainAffixID, SubAffixList: relicAffixes(r.SubAffixes), } } func (r *Relic) ToEquipRelic() *goproto.EquipRelic { return &goproto.EquipRelic{ RelicSlot: r.Slot(), RelicUniqueId: r.UniqueID(), } } func (r *Relic) ToProtoRelic() *goproto.Relic { return &goproto.Relic{ Level: r.Level, MainAffixId: r.MainAffixID, Tid: r.RelicID, UniqueId: r.UniqueID(), EquipAvatarId: r.EquipAvatar, SubAffixList: relicAffixes(r.SubAffixes), } } // --------------------------------------------------------------------------- // Avatar // --------------------------------------------------------------------------- // ToAvatarProto serializes the "base avatar" entry (8001 for MC, 1001 for March). func (a *AvatarJson) ToAvatarProto(lightcone *Lightcone, mcID, marchID uint32) *goproto.Avatar { baseAvatarID := BaseAvatarID(a.AvatarID) var curMultiPath uint32 switch { case IsMainCharacter(a.AvatarID): curMultiPath = MultiPathAvatarFromID(mcID).ID() case IsMarch7th(a.AvatarID): curMultiPath = MultiPathAvatarFromID(marchID).ID() default: curMultiPath = baseAvatarID } var equipUID uint32 if lightcone != nil { equipUID = lightcone.UniqueID() } return &goproto.Avatar{ BaseAvatarId: baseAvatarID, Level: a.Level, Promotion: a.Promotion, EquipmentUniqueId: equipUID, FirstMetTimestamp: 1712924677, CurMultiPathAvatarType: curMultiPath, HasTakenPromotionRewardList: []uint32{1, 3, 5}, } } // EnhanceAvatarID contains avatars that get enhanced mode enabled by default (March 7th Hunt, SP forms, etc.) var EnhanceAvatarID = map[uint32]bool{ 1004: true, 1005: true, 1006: true, 1102: true, 1205: true, 1212: true, 1217: true, 1306: true, 1307: true, 1310: true, } // AvatarExRankMap maps AvatarID to its corresponding Ex-Eidolon (Affinitive Eidolon) ID var AvatarExRankMap = map[uint32]uint32{ 1204: 120401, // Jing Yuan 1213: 121301, // Dan Heng • IL 1308: 130801, // Acheron } // ExRankAvatarID matches pearl-sr 4.6.53 (Jing Yuan 120401, Dan Heng IL 121301, Acheron 130801) var ExRankAvatarID = []uint32{120401, 121301, 130801} // DefaultAvatarSkinMap defines default skins if not explicitly set var DefaultAvatarSkinMap = map[uint32]uint32{ 1303: 1130301, 1001: 1100101, 1310: 1131001, 1415: 1141501, 1407: 1140701, 1501: 1150101, 1505: 1150501, 1409: 1140901, } func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic, dressedSkinID uint32) *goproto.AvatarPathData { enhancedID := derefU32(a.EnhancedID) if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] { enhancedID = 1 } if dressedSkinID == 0 { dressedSkinID = DefaultAvatarSkinMap[a.AvatarID] } var exRanks []uint32 if a.ExRankUnlocked { if exID, ok := AvatarExRankMap[a.AvatarID]; ok { exRanks = []uint32{exID} } } out := &goproto.AvatarPathData{ AvatarId: a.AvatarID, Rank: a.Data.Rank, EnhancedId: enhancedID, DressedSkinId: dressedSkinID, IGDEHGIKFJA: exRanks, } for _, r := range relics { out.EquipRelicList = append(out.EquipRelicList, r.ToEquipRelic()) } if len(a.Data.Skills) > 0 { for pointID, level := range a.Data.Skills { out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ PointId: pointID, Level: level, }) } } else if len(a.Data.SkillsByAnchorType) > 0 { res, _ := GameRes() for anchorType, level := range a.Data.SkillsByAnchorType { pointID := anchorType if res != nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { if pid, ok := res.AvatarSkillTreePoints[a.AvatarID][anchorType]; ok && pid != 0 { pointID = pid } } out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ PointId: pointID, Level: level, }) } } else { if res, err := GameRes(); err == nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { for _, pid := range res.AvatarSkillTreePoints[a.AvatarID] { out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{ PointId: pid, Level: 1, }) } } } // deterministic order (Rust iterated the HashMap; order is irrelevant to the // client, but stable output keeps packets reproducible) sortSkillTree(out.AvatarPathSkillTree) var pathEquip uint32 if lightcone != nil { pathEquip = lightcone.UniqueID() } out.PathEquipmentId = pathEquip return out } func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, relics []*Relic) (*goproto.BattleAvatar, []*goproto.BattleBuff) { enhancedID := derefU32(a.EnhancedID) if enhancedID == 0 && EnhanceAvatarID[a.AvatarID] { enhancedID = 1 } var exRanks []uint32 if a.ExRankUnlocked { if exID, ok := AvatarExRankMap[a.AvatarID]; ok { exRanks = []uint32{exID} } } ba := &goproto.BattleAvatar{ Index: index, AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, Id: a.AvatarID, Level: a.Level, Rank: a.Data.Rank, Hp: 10000, Promotion: a.Promotion, WorldLevel: 6, EnhancedId: enhancedID, IGDEHGIKFJA: exRanks, SpBar: &goproto.SpBarInfo{ CurSp: a.BattleSPCur(), MaxSp: battleRatioScale, }, } if len(a.Data.Skills) > 0 { for pointID, level := range a.Data.Skills { ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level}) } } else if len(a.Data.SkillsByAnchorType) > 0 { res, _ := GameRes() for anchorType, level := range a.Data.SkillsByAnchorType { pointID := anchorType if res != nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { if pid, ok := res.AvatarSkillTreePoints[a.AvatarID][anchorType]; ok && pid != 0 { pointID = pid } } ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level}) } } else { if res, err := GameRes(); err == nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil { for _, pid := range res.AvatarSkillTreePoints[a.AvatarID] { ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pid, Level: 1}) } } } sortAvatarSkillTree(ba.SkilltreeList) if lightcone != nil { ba.EquipmentList = append(ba.EquipmentList, lightcone.ToBattleEquipment()) } for _, r := range relics { ba.RelicList = append(ba.RelicList, r.ToBattleRelic()) } var buffs []*goproto.BattleBuff for _, buffID := range a.Techniques { buffs = append(buffs, &goproto.BattleBuff{ Id: buffID, Level: 1, OwnerId: index, WaveFlag: 0xffffffff, TargetIndexList: []uint32{0}, DynamicValues: map[string]float32{"SkillIndex": 0}, }) } return ba, buffs } func (a *AvatarJson) ToLineupAvatarProto(slot uint32) *goproto.LineupAvatar { return &goproto.LineupAvatar{ Id: a.AvatarID, Hp: 10000, Satiety: 100, AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, SpBar: &goproto.SpBarInfo{ CurSp: a.SPCur(), MaxSp: a.SPMaxVal(), }, Slot: slot, } } // ToLineupAvatars builds the overworld lineup (slots < 4, skipping empty/unknown). func ToLineupAvatars(f *FreesrData) []*goproto.LineupAvatar { known := func(id uint32) bool { _, ok := f.Avatars[id]; return ok } var out []*goproto.LineupAvatar for _, slot := range f.SortedLineupKeys() { avatarID := f.Lineups[slot] if slot >= 4 || avatarID == 0 || !known(avatarID) { continue } out = append(out, f.Avatars[avatarID].ToLineupAvatarProto(slot)) } return out } // ToLineupInfo builds LineupInfo from a lineup map (order: ascending slot). func ToLineupInfo(lineups map[uint32]uint32) *goproto.LineupInfo { maxMp := uint32(5) if _, ok := lineups[1408]; ok { maxMp = 8 } lineup := &goproto.LineupInfo{ ExtraLineupType: goproto.ExtraLineupType_ExtraLineupType_LineupNone, Name: "Squad 1", Mp: maxMp, MaxMp: maxMp, } keys := make([]uint32, 0, len(lineups)) for k := range lineups { keys = append(keys, k) } sortU32(keys) for _, slot := range keys { id := lineups[slot] if id == 0 { continue } lineup.AvatarList = append(lineup.AvatarList, &goproto.LineupAvatar{ Id: id, Hp: 10000, Satiety: 100, AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE, SpBar: &goproto.SpBarInfo{CurSp: 5000, MaxSp: 10000}, Slot: uint32(len(lineup.AvatarList)), }) } return lineup } // --------------------------------------------------------------------------- // Monster // --------------------------------------------------------------------------- func (m *Monster) ToSceneMonster() *goproto.SceneMonster { return &goproto.SceneMonster{ MonsterId: m.MonsterID, // Zero requests the stage's normal HP, as in PearlSR addMonsterWaves. MaxHp: m.MaxHP, } } func monsterWave(monsters []Monster) *goproto.SceneMonsterWave { maxLevel := uint32(0) for _, monster := range monsters { if monster.Level > maxLevel { maxLevel = monster.Level } } if maxLevel == 0 { maxLevel = 95 } wave := &goproto.SceneMonsterWave{ MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)), // Include a level so the client can validate this wave directly instead // of looking up monster data from the battle stage's static table. This // matters when a Calyx request reuses a custom battle stage ID. WaveParam: &goproto.SceneMonsterWaveParam{Level: maxLevel}, } for i := range monsters { wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster()) } return wave } func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave { out := make([]*goproto.SceneMonsterWave, 0, len(waves)) for _, wave := range waves { out = append(out, monsterWave(wave)) } return out } // --------------------------------------------------------------------------- // Position / Motion // --------------------------------------------------------------------------- func (p Position) ToMotionInfo() *goproto.MotionInfo { return &goproto.MotionInfo{ Pos: &goproto.Vector{X: p.X, Y: p.Y, Z: p.Z}, Rot: &goproto.Vector{Y: p.RotY}, } } func VectorToProto(v Vector) *goproto.Vector { return &goproto.Vector{X: v.X, Y: v.Y, Z: v.Z} } // --------------------------------------------------------------------------- // helpers // --------------------------------------------------------------------------- func derefU32(v *uint32) uint32 { if v == nil { return 0 } return *v } func sortU32(s []uint32) { for i := 1; i < len(s); i++ { for j := i; j > 0 && s[j] < s[j-1]; j-- { s[j], s[j-1] = s[j-1], s[j] } } } func sortAvatarSkillTree(s []*goproto.AvatarSkillTree) { for i := 1; i < len(s); i++ { for j := i; j > 0 && s[j].PointId < s[j-1].PointId; j-- { s[j], s[j-1] = s[j-1], s[j] } } } func sortSkillTree(s []*goproto.AvatarPathSkillTree) { for i := 1; i < len(s); i++ { for j := i; j > 0 && s[j].PointId < s[j-1].PointId; j-- { s[j], s[j-1] = s[j-1], s[j] } } }