Files
GaCy-GO/internal/common/proto_convert.go
T
CAtlas0784 59ea97b1a6 feat: port GaC-hkrpg-ps to Go with SceneCastSkill camera-detach fix
- mihoyo-variant KCP in pure Go (28-byte conv+token header), UDP gateway
  with 0xFF/0x145/0x194 handshake, ms-precision KCP clock + retransmit ticker
- protobuf for 4.6.51 generated from the AstralOS StarRail.proto (4932
  messages) plus CmdID map from // CmdID: comments (2114 ids)
- sdkserver port: query_dispatch/query_gateway (Gateserver pb + base64),
  hotfix fetch, auth endpoints, SRTools freesr-data.json upload
- gameserver handlers: login flow, scene load/spawn with real monster
  entity tracking, lineup, avatar/skin, missions, tutorials (1249+855),
  bag/equip, challenge incl. StartChallenge 1787 layout + Tierce 8973 +
  peak 8948, all raw tierce replies
- SceneCastSkill camera bug fix: only real scene monsters (tracked per
  session) trigger battle; attacking props/NPCs/air replies retcode 0
  without battle_info so the camera stays locked on the character
- end-to-end smoke test simulating a client over real UDP: handshake,
  PlayerGetToken, and both SceneCastSkill paths
- reuses the Rust PS data files unchanged (freesr-data.json, persistent,
  res.json, teleports.json, challenge_data.json, mission_unlocks.json)
2026-10-07 00:22:23 +07:00

345 lines
9.2 KiB
Go

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 {
return &goproto.BattleRelic{
Id: r.RelicID,
Level: r.Level,
MainAffixId: r.MainAffixID,
UniqueId: r.UniqueID(),
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},
}
}
func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic, dressedSkinID uint32) *goproto.AvatarPathData {
out := &goproto.AvatarPathData{
AvatarId: a.AvatarID,
Rank: a.Data.Rank,
EnhancedId: derefU32(a.EnhancedID),
DressedSkinId: dressedSkinID,
}
for _, r := range relics {
out.EquipRelicList = append(out.EquipRelicList, r.ToEquipRelic())
}
for anchorType, level := range a.Data.SkillsByAnchorType {
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
PointId: anchorType,
Level: level,
})
}
// 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) {
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: derefU32(a.EnhancedID),
SpBar: &goproto.SpBarInfo{
CurSp: a.SPCur(),
MaxSp: a.SPMaxVal(),
},
}
for pointID, level := range a.Data.Skills {
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level})
}
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,
DynamicValues: map[string]float32{"SkillIndex": 2.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: 10000, MaxSp: 10000},
Slot: uint32(len(lineup.AvatarList)),
})
}
return lineup
}
// ---------------------------------------------------------------------------
// Monster
// ---------------------------------------------------------------------------
func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
return &goproto.SceneMonster{
MonsterId: m.MonsterID,
MaxHp: m.MaxHP,
CurHp: m.MaxHP,
}
}
func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
wave := &goproto.SceneMonsterWave{
WaveId: waveID,
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
WaveParam: &goproto.SceneMonsterWaveParam{},
}
maxLevel := uint32(95)
for i := range monsters {
if monsters[i].Level > maxLevel || wave.WaveParam.Level == 0 {
if monsters[i].Level > maxLevel {
maxLevel = monsters[i].Level
}
}
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
}
// Rust: monsters.iter().map(level).max().unwrap_or(95)
if len(monsters) > 0 {
maxLevel = 0
for i := range monsters {
if monsters[i].Level > maxLevel {
maxLevel = monsters[i].Level
}
}
}
wave.WaveParam.Level = maxLevel
return wave
}
func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave {
out := make([]*goproto.SceneMonsterWave, 0, len(waves))
for i, wave := range waves {
out = append(out, monsterWave(uint32(i+1), 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]
}
}
}