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:
commit
59ea97b1a6
45 files changed
+1963868
No files matched your search
@@ -0,0 +1,260 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tierce lineup store + challenge scene loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
tierceLineups = map[uint32][][]uint32{}
|
||||
tierceLineupsMu sync.Mutex
|
||||
)
|
||||
|
||||
// loadChallengeScene is the Go port of scene.rs load_challenge_scene.
|
||||
func loadChallengeScene(s *Session, entryID, targetGroupID, monsterID, eventID uint32, avatarIDs []uint32) (*goproto.SceneInfo, *goproto.MotionInfo, error) {
|
||||
player := s.JSON()
|
||||
if player == nil {
|
||||
return nil, nil, errors.New("data is not set!")
|
||||
}
|
||||
res, err := common.GameRes()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
name, scene, ok := firstSceneOfEntry(res, entryID)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("Challenge Map Entrance Not Found %d", entryID)
|
||||
}
|
||||
planeID, floorID, err := parsePlaneFloor(name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 1. locate the boss monster position from scene config
|
||||
var monsPos common.Position
|
||||
var instID uint32 = 1
|
||||
fallbackPos := common.Position{X: -61000, Y: -2141, Z: -170700, RotY: 90000}
|
||||
if grp, okg := scene.Scenes[targetGroupID]; okg && len(grp.Monsters) > 0 {
|
||||
m := grp.Monsters[0]
|
||||
monsPos = common.Position{X: m.Pos.X, Y: m.Pos.Y, Z: m.Pos.Z, RotY: m.Rot.Y}
|
||||
instID = m.InstID
|
||||
} else {
|
||||
found := false
|
||||
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
||||
g := scene.Scenes[gid]
|
||||
if len(g.Monsters) > 0 {
|
||||
m := g.Monsters[0]
|
||||
monsPos = common.Position{X: m.Pos.X, Y: m.Pos.Y, Z: m.Pos.Z, RotY: m.Rot.Y}
|
||||
instID = m.InstID
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
monsPos = fallbackPos
|
||||
}
|
||||
}
|
||||
|
||||
// 2. position the player safely between boss and arena center
|
||||
centerVX := float64(-monsPos.X)
|
||||
centerVZ := float64(-monsPos.Z)
|
||||
distToCenter := math.Sqrt(centerVX*centerVX + centerVZ*centerVZ)
|
||||
|
||||
var spawnX, spawnY, spawnZ, spawnRotY, bossRotY int32
|
||||
if distToCenter > 1000.0 {
|
||||
nx := centerVX / distToCenter
|
||||
nz := centerVZ / distToCenter
|
||||
offset := math.Min(6000.0, distToCenter*0.6)
|
||||
spawnX = monsPos.X + int32(nx*offset)
|
||||
spawnZ = monsPos.Z + int32(nz*offset)
|
||||
|
||||
pDeg := int32(math.Round(math.Atan2(-nx, -nz) * 180.0 / math.Pi))
|
||||
if pDeg < 0 {
|
||||
pDeg += 360
|
||||
}
|
||||
bDeg := int32(math.Round(math.Atan2(nx, nz) * 180.0 / math.Pi))
|
||||
if bDeg < 0 {
|
||||
bDeg += 360
|
||||
}
|
||||
spawnY = monsPos.Y
|
||||
spawnRotY = pDeg * 1000
|
||||
bossRotY = bDeg * 1000
|
||||
} else {
|
||||
spawnX, spawnY, spawnZ, spawnRotY, bossRotY = 0, monsPos.Y, 6000, 180000, 0
|
||||
}
|
||||
monsPos.RotY = bossRotY
|
||||
|
||||
playerMotion := &goproto.MotionInfo{
|
||||
Pos: &goproto.Vector{X: spawnX, Y: spawnY, Z: spawnZ},
|
||||
Rot: &goproto.Vector{Y: spawnRotY},
|
||||
}
|
||||
|
||||
player.Scene.EntryID = entryID
|
||||
player.Scene.FloorID = floorID
|
||||
player.Scene.PlaneID = planeID
|
||||
|
||||
sceneInfo := &goproto.SceneInfo{
|
||||
FloorId: floorID,
|
||||
PlaneId: planeID,
|
||||
EntryId: entryID,
|
||||
GameModeType: scene.PlaneType,
|
||||
LeaderEntityId: 1,
|
||||
WorldId: scene.WorldID,
|
||||
SceneIdentifier: &goproto.SceneIdentifier{FloorId: floorID},
|
||||
SceneMissionInfo: &goproto.MissionStatusBySceneInfo{},
|
||||
}
|
||||
if sceneInfo.WorldId == 100 {
|
||||
sceneInfo.WorldId = 501
|
||||
}
|
||||
sceneInfo.LightenSectionList = scene.Sections
|
||||
|
||||
// load props for group 1, target group, and common prop groups (7/8)
|
||||
propEntityID := uint32(1000)
|
||||
s.PropEntities = map[uint32]PropEntry{}
|
||||
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
||||
if gid != 1 && gid != targetGroupID && gid != 7 && gid != 8 {
|
||||
continue
|
||||
}
|
||||
group := scene.Scenes[gid]
|
||||
groupInfo := &goproto.SceneEntityGroupInfo{GroupId: gid}
|
||||
for i := range group.Props {
|
||||
prop := &group.Props[i]
|
||||
propEntityID++
|
||||
s.PropEntities[propEntityID] = PropEntry{PropID: prop.PropID, InstID: prop.InstID, State: prop.PropState}
|
||||
groupInfo.EntityList = append(groupInfo.EntityList, &goproto.SceneEntityInfo{
|
||||
InstId: prop.InstID,
|
||||
GroupId: prop.GroupID,
|
||||
EntityId: propEntityID,
|
||||
Motion: (&common.Position{X: prop.Pos.X, Y: prop.Pos.Y, Z: prop.Pos.Z, RotY: prop.Rot.Y}).ToMotionInfo(),
|
||||
Entity: &goproto.SceneEntityInfo_Prop{
|
||||
Prop: &goproto.ScenePropInfo{PropState: prop.PropState, PropId: prop.PropID},
|
||||
},
|
||||
})
|
||||
}
|
||||
if len(groupInfo.EntityList) > 0 {
|
||||
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, groupInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// single boss monster (entity 30001, registered for SceneCastSkill battles)
|
||||
monsterGroup := &goproto.SceneEntityGroupInfo{GroupId: targetGroupID}
|
||||
s.MonsterEntities = map[uint32]uint32{30001: eventID}
|
||||
monsterGroup.EntityList = append(monsterGroup.EntityList, &goproto.SceneEntityInfo{
|
||||
InstId: instID,
|
||||
GroupId: targetGroupID,
|
||||
EntityId: 30001,
|
||||
Motion: monsPos.ToMotionInfo(),
|
||||
Entity: &goproto.SceneEntityInfo_NpcMonster{
|
||||
NpcMonster: &goproto.SceneNpcMonsterInfo{
|
||||
MonsterId: monsterID,
|
||||
EventId: eventID,
|
||||
WorldLevel: 6,
|
||||
},
|
||||
},
|
||||
})
|
||||
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, monsterGroup)
|
||||
|
||||
// player team actors
|
||||
avatarsToUse := avatarIDs
|
||||
if len(avatarsToUse) == 0 {
|
||||
for _, slot := range player.SortedLineupKeys() {
|
||||
if v := player.Lineups[slot]; v != 0 {
|
||||
avatarsToUse = append(avatarsToUse, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playerGroup := &goproto.SceneEntityGroupInfo{State: 0, GroupId: 0}
|
||||
for slot, aid := range avatarsToUse {
|
||||
playerGroup.EntityList = append(playerGroup.EntityList, &goproto.SceneEntityInfo{
|
||||
EntityId: uint32(slot) + 1,
|
||||
Motion: playerMotion,
|
||||
Entity: &goproto.SceneEntityInfo_Actor{
|
||||
Actor: &goproto.SceneActorInfo{
|
||||
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
||||
BaseAvatarId: aid,
|
||||
MapLayer: 0,
|
||||
Uid: 1337,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, playerGroup)
|
||||
|
||||
// NOTE: do not overwrite the saved scene so the player returns to the
|
||||
// open world on logout/battle end
|
||||
return sceneInfo, playerMotion, nil
|
||||
}
|
||||
|
||||
// handleStartChallengePeak ports on_start_challenge_peak_cs_req (cmd 8948):
|
||||
// StartChallengePeakScRsp has no scene field, so the arena is sent via
|
||||
// EnterSceneByServerScNotify (cmd 1427).
|
||||
func handleStartChallengePeak(s *Session, req *goproto.StartChallengePeakCsReq, rsp *goproto.StartChallengePeakScRsp) error {
|
||||
rsp.Retcode = 0
|
||||
|
||||
peakID := req.PeakId
|
||||
stage, ok := getChallengeData().Peak[itoa(int(peakID))]
|
||||
if !ok {
|
||||
Log("[PEAK] peak_id=" + itoa(int(peakID)) + " not found in challenge_data!")
|
||||
return nil
|
||||
}
|
||||
eventID := stage.EventID
|
||||
monsterID := stage.MonsterID
|
||||
buff := stage.DefaultBuff
|
||||
if req.BossBuffId != 0 {
|
||||
buff = req.BossBuffId
|
||||
}
|
||||
Log("[PEAK] Start: peak_id=" + itoa(int(peakID)) + " event=" + itoa(int(eventID)) + " monster=" + itoa(int(monsterID)))
|
||||
|
||||
avatars := dedupeTruncate4(req.PeakAvatarIdList)
|
||||
if len(avatars) == 0 {
|
||||
avatars = challengeAvatarsFallback(s)
|
||||
}
|
||||
|
||||
if player := s.JSON(); player != nil {
|
||||
customLineup := map[uint32]uint32{}
|
||||
for i, aid := range avatars {
|
||||
customLineup[uint32(i)] = aid
|
||||
}
|
||||
player.BattleConfig.CustomBattleLineup = customLineup
|
||||
player.BattleConfig.StageID = eventID
|
||||
player.BattleConfig.CycleCount = 30
|
||||
player.BattleConfig.BattleType = common.BattleTypeDefault
|
||||
player.BattleConfig.Monsters = [][]common.Monster{{{Level: 68, MonsterID: monsterID}}}
|
||||
if buff != 0 {
|
||||
player.BattleConfig.Blessings = []common.BattleBuffJson{{ID: buff, Level: 1}}
|
||||
}
|
||||
player.SavePersistent()
|
||||
}
|
||||
|
||||
// standard arena 3000101 (group 2 = boss position)
|
||||
sceneInfo, _, err := loadChallengeScene(s, 3000101, 2, monsterID, eventID, avatars)
|
||||
if err != nil {
|
||||
Log("[PEAK] Failed to load peak arena scene!")
|
||||
return nil
|
||||
}
|
||||
|
||||
custom := map[uint32]uint32{}
|
||||
for i, aid := range avatars {
|
||||
custom[uint32(i)] = aid
|
||||
}
|
||||
lineupInfo := common.ToLineupInfo(custom)
|
||||
|
||||
return s.Send(&goproto.EnterSceneByServerScNotify{
|
||||
Scene: sceneInfo,
|
||||
Lineup: lineupInfo,
|
||||
})
|
||||
}
|
||||
|
||||
var _ = proto.Marshal
|
||||
Reference in new issue
Block a user