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)
This commit is contained in:
CAtlas0784 committed 2026-10-07 00:22:23 +07:00
commit 59ea97b1a6
45 files changed
+1963868

No files matched your search

+479
View File
@@ -0,0 +1,479 @@
// Package common ports the Rust `common` crate: the JSON data model shared by
// the sdkserver (srtools upload), the file watcher and the gameserver. JSON
// field names match the original FreeSR-format files byte-for-byte so the
// existing freesr-data.json / persistent / res.json / teleports.json keep
// working without conversion.
package common
import (
"encoding/json"
"fmt"
"os"
"sort"
)
// ---------------------------------------------------------------------------
// MultiPathAvatar — string-serialized C-like enum in JSON, numeric in logic.
// ---------------------------------------------------------------------------
type MultiPathAvatar uint32
const (
MultiPathUnk MultiPathAvatar = 0
MultiPathMarchPreservation MultiPathAvatar = 1001
MultiPathMarchHunt MultiPathAvatar = 1224
MultiPathMalePhysical MultiPathAvatar = 8001
MultiPathFemalePhysical MultiPathAvatar = 8002
MultiPathMalePreservation MultiPathAvatar = 8003
MultiPathFemalePreservation MultiPathAvatar = 8004
MultiPathMaleHarmony MultiPathAvatar = 8005
MultiPathFemaleHarmony MultiPathAvatar = 8006
MultiPathMaleRemembrance MultiPathAvatar = 8007
MultiPathFemaleRemembrance MultiPathAvatar = 8008
MultiPathMaleElation MultiPathAvatar = 8009
MultiPathFemaleElation MultiPathAvatar = 8010
)
var multiPathByName = map[string]MultiPathAvatar{
"MalePyhsical": MultiPathMalePhysical, // typo kept from the Rust enum
"FemalePhysical": MultiPathFemalePhysical,
"MalePreservation": MultiPathMalePreservation,
"FemalePreservation": MultiPathFemalePreservation,
"MaleHarmony": MultiPathMaleHarmony,
"FemaleHarmony": MultiPathFemaleHarmony,
"MaleRemembrance": MultiPathMaleRemembrance,
"FemaleRemembrance": MultiPathFemaleRemembrance,
"MaleElation": MultiPathMaleElation,
"FemaleElation": MultiPathFemaleElation,
"MarchHunt": MultiPathMarchHunt,
"MarchPreservation": MultiPathMarchPreservation,
"Unk": MultiPathUnk,
}
var multiPathByID = map[uint32]MultiPathAvatar{
8001: MultiPathMalePhysical,
8002: MultiPathFemalePhysical,
8003: MultiPathMalePreservation,
8004: MultiPathFemalePreservation,
8005: MultiPathMaleHarmony,
8006: MultiPathFemaleHarmony,
8007: MultiPathMaleRemembrance,
8008: MultiPathFemaleRemembrance,
8009: MultiPathMaleElation,
8010: MultiPathFemaleElation,
1224: MultiPathMarchHunt,
1001: MultiPathMarchPreservation,
}
// MultiPathAvatarFromID mirrors Rust From<u32> (unknown -> Unk).
func MultiPathAvatarFromID(v uint32) MultiPathAvatar {
if m, ok := multiPathByID[v]; ok {
return m
}
return MultiPathUnk
}
// ID mirrors Rust From<MultiPathAvatar> for u32 (Unk -> 8006).
func (m MultiPathAvatar) ID() uint32 {
if m == MultiPathUnk {
return 8006
}
return uint32(m)
}
func (m MultiPathAvatar) String() string {
for name, v := range multiPathByName {
if v == m {
return name
}
}
return "Unk"
}
func (m MultiPathAvatar) MarshalJSON() ([]byte, error) { return json.Marshal(m.String()) }
func (m *MultiPathAvatar) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
// numeric fallback
var n uint32
if err2 := json.Unmarshal(data, &n); err2 == nil {
*m = MultiPathAvatarFromID(n)
return nil
}
return err
}
v, ok := multiPathByName[s]
if !ok {
v = MultiPathUnk
}
*m = v
return nil
}
// MultiPathAvatarType in the 4.6.51 proto uses the avatar id itself as the
// enum value (Mar7thKnightType=1001, BoyWarriorType=8001, ...), so the proto
// value is simply the numeric id (0 for Unk).
func (m MultiPathAvatar) ProtoType() uint32 { return uint32(m) }
func IsMainCharacter(avatarID uint32) bool { return avatarID >= 8001 && avatarID <= 8010 }
func IsMarch7th(avatarID uint32) bool { return avatarID == 1001 || avatarID == 1224 }
func BaseAvatarID(avatarID uint32) uint32 {
if IsMainCharacter(avatarID) {
return 8001
}
if IsMarch7th(avatarID) {
return 1001
}
return avatarID
}
// ---------------------------------------------------------------------------
// Item structs (freesr-data.json)
// ---------------------------------------------------------------------------
type Lightcone struct {
Level uint32 `json:"level"`
ItemID uint32 `json:"item_id"`
EquipAvatar uint32 `json:"equip_avatar"`
Rank uint32 `json:"rank"`
Promotion uint32 `json:"promotion"`
InternalUID uint32 `json:"internal_uid"`
}
type SubAffix struct {
SubAffixID uint32 `json:"sub_affix_id"`
Count uint32 `json:"count"`
Step uint32 `json:"step"`
}
type Relic struct {
Level uint32 `json:"level"`
RelicID uint32 `json:"relic_id"`
MainAffixID uint32 `json:"main_affix_id"`
SubAffixes []SubAffix `json:"sub_affixes"`
InternalUID uint32 `json:"internal_uid"`
EquipAvatar uint32 `json:"equip_avatar"`
}
func (r *Relic) Slot() uint32 { return r.RelicID % 10 }
func (r *Relic) UniqueID() uint32 { return r.InternalUID }
func (l *Lightcone) UniqueID() uint32 { return 3000 + l.InternalUID }
// ---------------------------------------------------------------------------
// Avatar (freesr-data.json)
// ---------------------------------------------------------------------------
type AvatarData struct {
Rank uint32 `json:"rank"`
Skills map[uint32]uint32 `json:"skills"`
SkillsByAnchorType map[uint32]uint32 `json:"skills_by_anchor_type"`
}
type AvatarJson struct {
AvatarID uint32 `json:"avatar_id"`
Data AvatarData `json:"data"`
Level uint32 `json:"level"`
Promotion uint32 `json:"promotion"`
Techniques []uint32 `json:"techniques"`
SPValue *uint32 `json:"sp_value"`
SPMax *uint32 `json:"sp_max"`
EnhancedID *uint32 `json:"enhanced_id"`
}
func (a *AvatarJson) SPCur() uint32 {
if a.SPValue != nil {
return *a.SPValue
}
return 10000
}
func (a *AvatarJson) SPMaxVal() uint32 {
if a.SPMax != nil {
return *a.SPMax
}
return 10000
}
// ---------------------------------------------------------------------------
// Battle config (freesr-data.json)
// ---------------------------------------------------------------------------
type BattleType string
const (
BattleTypeDefault BattleType = "Default"
BattleTypeMOC BattleType = "MOC"
BattleTypePF BattleType = "PF"
BattleTypeSU BattleType = "SU"
BattleTypeAS BattleType = "AS"
BattleTypeAA BattleType = "AA"
)
type Monster struct {
Level uint32 `json:"level"`
MonsterID uint32 `json:"monster_id"`
MaxHP uint32 `json:"max_hp"`
}
type DynamicKey struct {
Key string `json:"key"`
Value uint32 `json:"value"`
}
type BattleBuffJson struct {
Level uint32 `json:"level"`
ID uint32 `json:"id"`
DynamicKey *DynamicKey `json:"dynamic_key"`
DynamicValues []DynamicKey `json:"dynamic_values"`
}
type RogueMagicComponentType uint32
const (
RogueMagicPassive RogueMagicComponentType = 3
RogueMagicActive RogueMagicComponentType = 4
RogueMagicAttach RogueMagicComponentType = 5
)
type RogueMagicComponent struct {
ID uint32 `json:"id"`
Level uint32 `json:"level"`
ComponentType RogueMagicComponentType `json:"component_type"`
}
type RogueMagicScepter struct {
Level uint32 `json:"level"`
ID uint32 `json:"id"`
Components []RogueMagicComponent `json:"components"`
}
type BattleConfig struct {
BattleType BattleType `json:"battle_type"`
Monsters [][]Monster `json:"monsters"`
Blessings []BattleBuffJson `json:"blessings"`
StageID uint32 `json:"stage_id"`
CycleCount uint32 `json:"cycle_count"`
PathResonanceID uint32 `json:"path_resonance_id"`
CustomStats []SubAffix `json:"custom_stats"`
Scepters []RogueMagicScepter `json:"scepters"`
CustomBattleLineup map[uint32]uint32 `json:"custom_battle_lineup"`
}
func DefaultBattleConfig() BattleConfig {
return BattleConfig{
BattleType: BattleTypeDefault,
Monsters: [][]Monster{{{Level: 60, MonsterID: 3014022}}},
StageID: 201012311,
}
}
// ---------------------------------------------------------------------------
// Persistent (file: "persistent")
// ---------------------------------------------------------------------------
type Position struct {
X int32 `json:"x"`
Y int32 `json:"y"`
Z int32 `json:"z"`
RotY int32 `json:"rot_y"`
}
func DefaultPosition() Position {
return Position{X: -26968, Y: 78953, Z: 14457, RotY: 11858}
}
type Scene struct {
PlaneID uint32 `json:"plane_id"`
FloorID uint32 `json:"floor_id"`
EntryID uint32 `json:"entry_id"`
}
func DefaultScene() Scene {
return Scene{PlaneID: 20411, FloorID: 20411001, EntryID: 2041101}
}
type Persistent struct {
Lineups map[uint32]uint32 `json:"lineups"`
Position Position `json:"position"`
Scene Scene `json:"scene"`
MainCharacter MultiPathAvatar `json:"main_character"`
MarchType MultiPathAvatar `json:"march_type"`
EnableSWGlobal *bool `json:"enable_sw_global"`
EnableCastoriceGlobal *bool `json:"enable_castorice_global"`
DressedSkins map[uint32]uint32 `json:"dressed_skins"`
}
// DefaultPersistent mirrors the Rust Default impl.
func DefaultPersistent() Persistent {
sw := true
cg := true
return Persistent{
Lineups: map[uint32]uint32{0: 1313, 1: 1006, 2: 8001, 3: 1405},
Position: DefaultPosition(),
MainCharacter: MultiPathFemaleRemembrance,
Scene: DefaultScene(),
MarchType: MultiPathMarchHunt,
EnableSWGlobal: &sw,
EnableCastoriceGlobal: &cg,
DressedSkins: map[uint32]uint32{},
}
}
// ---------------------------------------------------------------------------
// FreesrData
// ---------------------------------------------------------------------------
type FreesrData struct {
Lightcones []Lightcone `json:"lightcones"`
Relics []Relic `json:"relics"`
Avatars map[uint32]*AvatarJson `json:"avatars"`
BattleConfig BattleConfig `json:"battle_config"`
// Fields stored in "persistent", merged on load (not serialized here).
Lineups map[uint32]uint32 `json:"-"`
Position Position `json:"-"`
Scene Scene `json:"-"`
MainCharacter MultiPathAvatar `json:"-"`
MarchType MultiPathAvatar `json:"-"`
EnableSWGlobal *bool `json:"-"`
EnableCastoriceGlobal *bool `json:"-"`
DressedSkins map[uint32]uint32 `json:"-"`
}
// LoadFreesrData reads freesr-data.json + persistent and merges them,
// mirroring FreesrData::load() from the Rust crate.
func LoadFreesrData() (*FreesrData, error) {
raw, err := os.ReadFile("freesr-data.json")
if err != nil {
return nil, fmt.Errorf("failed to read freesr-data.json: %w", err)
}
var data FreesrData
if err := json.Unmarshal(raw, &data); err != nil {
return nil, fmt.Errorf("freesr-data.json is broken: %w, pls redownload", err)
}
if data.Avatars == nil {
data.Avatars = map[uint32]*AvatarJson{}
}
if data.BattleConfig.Monsters == nil && data.BattleConfig.StageID == 0 {
data.BattleConfig = DefaultBattleConfig()
}
// persistent is optional
pers, err := os.ReadFile("persistent")
if err == nil {
var p Persistent
if err := json.Unmarshal(pers, &p); err == nil {
data.Lineups = p.Lineups
data.Position = p.Position
data.Scene = p.Scene
data.MainCharacter = p.MainCharacter
data.MarchType = p.MarchType
data.EnableSWGlobal = p.EnableSWGlobal
data.EnableCastoriceGlobal = p.EnableCastoriceGlobal
data.DressedSkins = p.DressedSkins
}
}
if data.DressedSkins == nil {
data.DressedSkins = map[uint32]uint32{}
}
// remove unequipped lightcones / relics (keeps huge FreeSR dumps usable)
if len(data.Lightcones) > 1500 {
kept := data.Lightcones[:0]
for _, v := range data.Lightcones {
if v.EquipAvatar != 0 {
kept = append(kept, v)
}
}
data.Lightcones = kept
}
if len(data.Relics) > 3000 {
kept := data.Relics[:0]
for _, v := range data.Relics {
if v.EquipAvatar != 0 {
kept = append(kept, v)
}
}
data.Relics = kept
}
data.VerifyLineup()
if data.MarchType.ID() > 8000 {
data.MarchType = MultiPathMarchHunt
}
return &data, nil
}
// SortedLineupKeys returns lineup slots in ascending order (BTreeMap order).
func (f *FreesrData) SortedLineupKeys() []uint32 {
keys := make([]uint32, 0, len(f.Lineups))
for k := range f.Lineups {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
return keys
}
// VerifyLineup pads the lineup to 4 slots, defaulting to {0:8001}, then saves.
func (f *FreesrData) VerifyLineup() {
if len(f.Lineups) == 0 {
f.Lineups = map[uint32]uint32{0: 8001, 1: 0, 2: 0, 3: 0}
} else if len(f.Lineups) < 4 {
for i := uint32(len(f.Lineups)); i < 4; i++ {
if _, ok := f.Lineups[i]; !ok {
f.Lineups[i] = 0
}
}
}
f.SavePersistent()
}
// SavePersistent writes the persistent subset back to the "persistent" file.
func (f *FreesrData) SavePersistent() {
p := Persistent{
Lineups: f.Lineups,
Position: f.Position,
Scene: f.Scene,
MainCharacter: f.MainCharacter,
MarchType: f.MarchType,
EnableSWGlobal: f.EnableSWGlobal,
EnableCastoriceGlobal: f.EnableCastoriceGlobal,
DressedSkins: f.DressedSkins,
}
out, err := json.MarshalIndent(p, "", " ")
if err != nil {
return
}
if err := os.WriteFile("persistent", out, 0o644); err == nil {
LogInfo("persistent saved")
}
}
// FindLightconeFor returns the lightcone equipped by the given avatar (first match).
func (f *FreesrData) FindLightconeFor(avatarID uint32) *Lightcone {
for i := range f.Lightcones {
if f.Lightcones[i].EquipAvatar == avatarID {
return &f.Lightcones[i]
}
}
return nil
}
// FindRelicsFor returns the relics equipped by the given avatar.
func (f *FreesrData) FindRelicsFor(avatarID uint32) []*Relic {
var out []*Relic
for i := range f.Relics {
if f.Relics[i].EquipAvatar == avatarID {
out = append(out, &f.Relics[i])
}
}
return out
}
+344
View File
@@ -0,0 +1,344 @@
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]
}
}
}
+134
View File
@@ -0,0 +1,134 @@
package common
import (
"encoding/json"
"fmt"
"os"
"sync"
"time"
)
// LogInfo prints a timestamped info line (kept dependency-free).
func LogInfo(format string, args ...any) {
fmt.Printf("%s %s\n", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprintf(format, args...))
}
// ---------------------------------------------------------------------------
// res.json (camelCase, produced by the AstralOS dump pipeline)
// ---------------------------------------------------------------------------
type Vector struct {
X int32 `json:"x"`
Y int32 `json:"y"`
Z int32 `json:"z"`
}
type SceneMonsterInfo struct {
Pos Vector `json:"pos"`
Rot Vector `json:"rot"`
GroupID uint32 `json:"groupId"`
InstID uint32 `json:"instId"`
MonsterID uint32 `json:"monsterId"`
EventID uint32 `json:"eventId"`
}
type SceneNpcInfo struct {
Pos Vector `json:"pos"`
Rot Vector `json:"rot"`
GroupID uint32 `json:"groupId"`
InstID uint32 `json:"instId"`
NpcID uint32 `json:"npcId"`
}
type ScenePropInfo struct {
Pos Vector `json:"pos"`
Rot Vector `json:"rot"`
GroupID uint32 `json:"groupId"`
InstID uint32 `json:"instId"`
PropState uint32 `json:"propState"`
PropID uint32 `json:"propId"`
}
type TeleportInfo struct {
Pos Vector `json:"pos"`
Rot Vector `json:"rot"`
}
type SceneData struct {
Npcs []SceneNpcInfo `json:"npcs"`
Props []ScenePropInfo `json:"props"`
Monsters []SceneMonsterInfo `json:"monsters"`
Teleports map[uint32]TeleportInfo `json:"teleports"`
FinishedSubMissions []uint32 `json:"finishedSubMissions"`
FinishedMainMissions []uint32 `json:"finishedMainMissions"`
Chests []uint32 `json:"chests"`
}
type LevelOutputConfig struct {
IsEnteredSceneInfo bool `json:"isEnteredSceneInfo"`
Scenes map[uint32]SceneData `json:"scenes"`
PlaneType uint32 `json:"planeType"`
WorldID uint32 `json:"worldId"`
Sections []uint32 `json:"sections"`
SavedValues map[string]int32 `json:"savedValues"`
}
type AvatarConfig struct {
WeaknessBuffID uint32 `json:"weaknessBuffId"`
}
type JsonConfig struct {
// entryId -> "P<plane>_F<floor>" -> config
LevelOutputConfigs map[uint32]map[string]*LevelOutputConfig `json:"levelOutputConfigs"`
AvatarConfigs map[uint32]AvatarConfig `json:"avatarConfigs"`
MapDefaultEntranceMap map[uint32]uint32 `json:"mapDefaultEntranceMap"`
RelicAvatarRecommend map[uint32][]uint32 `json:"relicAvatarRecommend"`
SkinIDs []uint32 `json:"skinIds"`
}
var (
gameResOnce sync.Once
gameRes *JsonConfig
gameResErr error
teleportOnce sync.Once
teleportDB map[uint32]map[uint32]TeleportInfo
)
// GameRes lazily parses res.json (13MB; parsed once).
func GameRes() (*JsonConfig, error) {
gameResOnce.Do(func() {
gameRes = &JsonConfig{}
raw, err := os.ReadFile("res.json")
if err != nil {
gameResErr = err
return
}
if err := json.Unmarshal(raw, gameRes); err != nil {
gameResErr = fmt.Errorf("res.json is malformed: %w", err)
return
}
if gameRes.LevelOutputConfigs == nil {
gameRes.LevelOutputConfigs = map[uint32]map[string]*LevelOutputConfig{}
}
if gameRes.AvatarConfigs == nil {
gameRes.AvatarConfigs = map[uint32]AvatarConfig{}
}
if gameRes.MapDefaultEntranceMap == nil {
gameRes.MapDefaultEntranceMap = map[uint32]uint32{}
}
})
return gameRes, gameResErr
}
// Teleports lazily parses teleports.json ({floor_id: {teleport_id: info}}).
func Teleports() map[uint32]map[uint32]TeleportInfo {
teleportOnce.Do(func() {
teleportDB = map[uint32]map[uint32]TeleportInfo{}
raw, err := os.ReadFile("teleports.json")
if err != nil {
return
}
_ = json.Unmarshal(raw, &teleportDB)
})
return teleportDB
}