- 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)
694 lines
20 KiB
Go
694 lines
20 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
goproto "gac-hkrpg-go/protocol/proto"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Scene loading (Go port of handlers/scene.rs)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// firstSceneOfEntry returns the (configName, config) pair for an entry.
|
|
// Rust iterated a HashMap arbitrarily; we pick the lexicographically first
|
|
// name for determinism.
|
|
func firstSceneOfEntry(res *common.JsonConfig, entryID uint32) (string, *common.LevelOutputConfig, bool) {
|
|
m, ok := res.LevelOutputConfigs[entryID]
|
|
if !ok || len(m) == 0 {
|
|
return "", nil, false
|
|
}
|
|
names := make([]string, 0, len(m))
|
|
for k := range m {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return names[0], m[names[0]], true
|
|
}
|
|
|
|
// parsePlaneFloor splits "P<plane>_F<floor>".
|
|
func parsePlaneFloor(name string) (uint32, uint32, error) {
|
|
if len(name) < 2 || name[0] != 'P' {
|
|
return 0, 0, fmt.Errorf("bad scene config name %q", name)
|
|
}
|
|
split := -1
|
|
for i := 1; i < len(name); i++ {
|
|
if name[i] == '_' && i+1 < len(name) && name[i+1] == 'F' {
|
|
split = i
|
|
break
|
|
}
|
|
}
|
|
if split < 0 {
|
|
return 0, 0, fmt.Errorf("bad scene config name %q", name)
|
|
}
|
|
var plane, floor uint32
|
|
if _, err := fmt.Sscanf(name[1:split], "%d", &plane); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
if _, err := fmt.Sscanf(name[split+2:], "%d", &floor); err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return plane, floor, nil
|
|
}
|
|
|
|
func init() {
|
|
handle("GetCurSceneInfo", func(s *Session, req *goproto.GetCurSceneInfoCsReq, rsp *goproto.GetCurSceneInfoScRsp) error {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
Log("data is not set!")
|
|
return nil
|
|
}
|
|
|
|
// recover from a stale challenge arena back to the Parlor Car
|
|
entryID := player.Scene.EntryID
|
|
if entryID >= 3000000 && entryID < 4000000 {
|
|
entryID = 100000104
|
|
}
|
|
|
|
defaultScene := &goproto.SceneInfo{
|
|
GameModeType: 1,
|
|
EntryId: entryID,
|
|
}
|
|
if entryID == 100000104 {
|
|
defaultScene.PlaneId = 10000
|
|
defaultScene.FloorId = 10000000
|
|
} else {
|
|
defaultScene.PlaneId = player.Scene.PlaneID
|
|
defaultScene.FloorId = player.Scene.FloorID
|
|
}
|
|
|
|
scene, err := loadScene(s, entryID, false, nil, nil)
|
|
rsp.Retcode = 0
|
|
if err == nil {
|
|
rsp.Scene = scene
|
|
} else {
|
|
rsp.Scene = defaultScene
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("EnterScene", func(s *Session, req *goproto.EnterSceneCsReq, rsp *goproto.EnterSceneScRsp) error {
|
|
Log(fmt.Sprintf("on_enter_scene_cs_req: entry_id=%d, teleport_id=%d, interact_id=%d", req.EntryId, req.TeleportId, req.InteractId))
|
|
SceneDebugLog("ENTER_SCENE entry_id=%d teleport_id=%d interact_id=%d", req.EntryId, req.TeleportId, req.InteractId)
|
|
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
player := s.JSON()
|
|
currentEntry := func() uint32 {
|
|
if player != nil {
|
|
return player.Scene.EntryID
|
|
}
|
|
return 100000104
|
|
}
|
|
|
|
interactID := uint32(req.InteractId)
|
|
var sceneIdent *goproto.SceneIdentifier
|
|
if req.SceneIdentifier != nil {
|
|
sceneIdent = proto.Clone(req.SceneIdentifier).(*goproto.SceneIdentifier)
|
|
}
|
|
|
|
// resolve target entry (mirrors the Rust resolution chain)
|
|
resolvedEntry := uint32(0)
|
|
switch {
|
|
case req.EntryId != 0 && mapHasEntry(res, req.EntryId):
|
|
resolvedEntry = req.EntryId
|
|
case sceneIdent != nil && res.MapDefaultEntranceMap[sceneIdent.FloorId] != 0:
|
|
resolvedEntry = res.MapDefaultEntranceMap[sceneIdent.FloorId]
|
|
case res.MapDefaultEntranceMap[req.EntryId] != 0:
|
|
resolvedEntry = res.MapDefaultEntranceMap[req.EntryId]
|
|
case req.TeleportId != 0:
|
|
resolvedEntry = findEntryByTeleport(res, req.TeleportId, currentEntry())
|
|
case interactID != 0:
|
|
resolvedEntry = findEntryByInteract(res, interactID, currentEntry())
|
|
default:
|
|
resolvedEntry = currentEntry()
|
|
}
|
|
|
|
// resolve teleport id
|
|
var teleportID *uint32
|
|
switch {
|
|
case req.TeleportId != 0:
|
|
t := req.TeleportId
|
|
teleportID = &t
|
|
case sceneIdent != nil && sceneIdent.TeleportNigger != nil:
|
|
if t, okc := sceneIdent.TeleportNigger.(*goproto.SceneIdentifier_MDAIDPPKOPO); okc && t.MDAIDPPKOPO != nil && t.MDAIDPPKOPO.PLIDNBMCIJH != 0 {
|
|
v := t.MDAIDPPKOPO.PLIDNBMCIJH
|
|
teleportID = &v
|
|
}
|
|
case interactID != 0:
|
|
if m, ok := res.LevelOutputConfigs[resolvedEntry]; ok {
|
|
for _, sc := range m {
|
|
for _, grp := range sc.Scenes {
|
|
hit := false
|
|
for _, p := range grp.Props {
|
|
if p.InstID == interactID || p.PropID == interactID {
|
|
hit = true
|
|
break
|
|
}
|
|
}
|
|
if hit && len(grp.Teleports) > 0 {
|
|
keys := sortedU32Keys(grp.Teleports)
|
|
teleportID = &keys[0]
|
|
break
|
|
}
|
|
}
|
|
if teleportID != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
case req.EntryId != 0 && !mapHasEntry(res, req.EntryId):
|
|
v := req.EntryId
|
|
teleportID = &v
|
|
case (resolvedEntry == 100000104 || resolvedEntry == 1000001) && sceneIdent != nil && sceneIdent.ContentId != 0:
|
|
v := uint32(2206)
|
|
teleportID = &v
|
|
}
|
|
|
|
sceneInfo, err := loadScene(s, resolvedEntry, true, teleportID, sceneIdent)
|
|
if err != nil {
|
|
SceneDebugLog("ENTER_SCENE_FAILED requested_entry=%d resolved_entry=%d err=%v", req.EntryId, resolvedEntry, err)
|
|
Log(fmt.Sprintf("Failed to enter scene %d: %v", resolvedEntry, err))
|
|
rsp.Retcode = 2605
|
|
return nil
|
|
}
|
|
|
|
SceneDebugLog("ENTER_SCENE_RESOLVED requested_entry=%d resolved_entry=%d", req.EntryId, resolvedEntry)
|
|
rsp.Retcode = 0
|
|
rsp.SceneIdentifier = sceneInfo.SceneIdentifier
|
|
rsp.IsCloseMap = req.IsCloseMap
|
|
return nil
|
|
})
|
|
|
|
handle("InteractProp", func(s *Session, req *goproto.InteractPropCsReq, rsp *goproto.InteractPropScRsp) error {
|
|
SceneDebugLog("INTERACT_PROP prop_entity_id=%d interact_id=%d interact_id2=%d",
|
|
req.PropEntityId, req.InteractId, req.InteractId2)
|
|
|
|
// answer with the real prop state recorded at scene load; replying with
|
|
// a wrong state (like 1100) breaks interactions and UI entry points
|
|
state := uint32(1)
|
|
if p, ok := s.PropEntities[req.PropEntityId]; ok {
|
|
state = p.State
|
|
}
|
|
|
|
SceneDebugLog("INTERACT_PROP_RESOLVED entity_id=%d state=%d", req.PropEntityId, state)
|
|
|
|
rsp.Retcode = 0
|
|
rsp.PropEntityId = req.PropEntityId
|
|
rsp.PropState = state
|
|
return nil
|
|
})
|
|
|
|
handle("GetNpcTakenReward", func(s *Session, req *goproto.GetNpcTakenRewardCsReq, rsp *goproto.GetNpcTakenRewardScRsp) error {
|
|
// must be answered or NPC dialogs freeze with no options
|
|
SceneDebugLog("GET_NPC_TAKEN_REWARD npc_id=%d", req.NpcId)
|
|
rsp.Retcode = 0
|
|
rsp.NpcId = req.NpcId
|
|
return nil
|
|
})
|
|
|
|
handle("GetSceneMapInfo", func(s *Session, req *goproto.GetSceneMapInfoCsReq, rsp *goproto.GetSceneMapInfoScRsp) error {
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, si := range req.SceneIdentifiers {
|
|
floorID := si.FloorId
|
|
mapInfo := &goproto.SceneMapInfo{
|
|
FloorId: floorID,
|
|
SceneIdentifier: proto.Clone(si).(*goproto.SceneIdentifier),
|
|
}
|
|
mapInfo.ChestList = append(mapInfo.ChestList,
|
|
&goproto.ChestInfo{ChestType: goproto.ChestType_MAP_INFO_CHEST_TYPE_NORMAL},
|
|
&goproto.ChestInfo{ChestType: goproto.ChestType_MAP_INFO_CHEST_TYPE_CHALLENGE},
|
|
&goproto.ChestInfo{ChestType: goproto.ChestType_MAP_INFO_CHEST_TYPE_PUZZLE},
|
|
)
|
|
|
|
if entryID := res.MapDefaultEntranceMap[floorID]; entryID != 0 {
|
|
if _, cfg, ok := firstSceneOfEntry(res, entryID); ok {
|
|
groupIDs := sortedU32Keys2(cfg.Scenes)
|
|
for _, groupID := range groupIDs {
|
|
group := cfg.Scenes[groupID]
|
|
mapInfo.GroupList = append(mapInfo.GroupList, &goproto.MapInfoGroup{GroupId: groupID})
|
|
|
|
for tp := range group.Teleports {
|
|
mapInfo.UnlockedTeleportList = append(mapInfo.UnlockedTeleportList, tp)
|
|
}
|
|
for _, prop := range group.Props {
|
|
mapInfo.MapInfoPropList = append(mapInfo.MapInfoPropList, &goproto.MazePropState{
|
|
GroupId: prop.GroupID,
|
|
State: prop.PropState,
|
|
ConfigId: prop.InstID,
|
|
})
|
|
}
|
|
}
|
|
mapInfo.LightenSectionList = cfg.Sections
|
|
if cfg.SavedValues != nil {
|
|
mapInfo.FloorSavedData = cfg.SavedValues
|
|
}
|
|
}
|
|
}
|
|
|
|
rsp.SceneMapInfo = append(rsp.SceneMapInfo, mapInfo)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
handle("SceneEntityMove", func(s *Session, req *goproto.SceneEntityMoveCsReq, rsp *goproto.SceneEntityMoveScRsp) error {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil
|
|
}
|
|
|
|
// don't save positions inside challenge arenas
|
|
if player.Scene.EntryID >= 3000000 && player.Scene.EntryID < 4000000 {
|
|
return nil
|
|
}
|
|
nowMs := time.Now().UnixMilli()
|
|
if nowMs <= s.nextSceneSave {
|
|
return nil
|
|
}
|
|
s.nextSceneSave = nowMs + 5000 // save every 5s
|
|
|
|
for _, entity := range req.EntityMotionList {
|
|
if entity.EntityId != 0 {
|
|
continue
|
|
}
|
|
if motion := entity.Motion; motion != nil {
|
|
if pos := motion.Pos; pos != nil {
|
|
if pos.Y < -5000 {
|
|
return nil
|
|
}
|
|
player.Position.X = pos.X
|
|
player.Position.Y = pos.Y
|
|
player.Position.Z = pos.Z
|
|
}
|
|
if rot := motion.Rot; rot != nil {
|
|
player.Position.RotY = rot.Y
|
|
}
|
|
}
|
|
}
|
|
player.SavePersistent()
|
|
return nil
|
|
})
|
|
|
|
handle("GetEnteredScene", func(s *Session, req *goproto.GetEnteredSceneCsReq, rsp *goproto.GetEnteredSceneScRsp) error {
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entryIDs := make([]uint32, 0, len(res.LevelOutputConfigs))
|
|
for k := range res.LevelOutputConfigs {
|
|
entryIDs = append(entryIDs, k)
|
|
}
|
|
sort.Slice(entryIDs, func(i, j int) bool { return entryIDs[i] < entryIDs[j] })
|
|
|
|
for _, eid := range entryIDs {
|
|
m := res.LevelOutputConfigs[eid]
|
|
names := make([]string, 0, len(m))
|
|
for k := range m {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
if !m[name].IsEnteredSceneInfo {
|
|
continue
|
|
}
|
|
plane, floor, err := parsePlaneFloor(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
rsp.EnteredSceneInfoList = append(rsp.EnteredSceneInfoList, &goproto.EnteredSceneInfo{
|
|
FloorId: floor,
|
|
PlaneId: plane,
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// cmd 1432 -> 1421: unlock every teleport in the game
|
|
handle("GetUnlockTeleport", func(s *Session, req *goproto.GetUnlockTeleportCsReq, rsp *goproto.GetUnlockTeleportScRsp) error {
|
|
rsp.Retcode = 0
|
|
unlocks := getMissionUnlocks()
|
|
seen := map[uint32]bool{}
|
|
var tps []uint32
|
|
for _, id := range unlocks.TeleportIDs {
|
|
if id != 0 && !seen[id] {
|
|
seen[id] = true
|
|
tps = append(tps, id)
|
|
}
|
|
}
|
|
if len(tps) == 0 {
|
|
for _, m := range common.Teleports() {
|
|
for id := range m {
|
|
if id != 0 && !seen[id] {
|
|
seen[id] = true
|
|
tps = append(tps, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(tps, func(i, j int) bool { return tps[i] < tps[j] })
|
|
rsp.UnlockedTeleportList = tps
|
|
|
|
SceneDebugLog("GET_UNLOCK_TELEPORT req_entries=%d teleports_sent=%d", len(req.EntryIdList), len(rsp.UnlockedTeleportList))
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func mapHasEntry(res *common.JsonConfig, entryID uint32) bool {
|
|
_, ok := res.LevelOutputConfigs[entryID]
|
|
return ok
|
|
}
|
|
|
|
func findEntryByTeleport(res *common.JsonConfig, teleportID uint32, fallback uint32) uint32 {
|
|
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
|
|
for _, eid := range entryIDs {
|
|
names := make([]string, 0, len(res.LevelOutputConfigs[eid]))
|
|
for k := range res.LevelOutputConfigs[eid] {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
sc := res.LevelOutputConfigs[eid][name]
|
|
groupIDs := sortedU32Keys2(sc.Scenes)
|
|
for _, gid := range groupIDs {
|
|
if _, ok := sc.Scenes[gid].Teleports[teleportID]; ok {
|
|
return eid
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func findEntryByInteract(res *common.JsonConfig, interactID uint32, fallback uint32) uint32 {
|
|
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
|
|
for _, eid := range entryIDs {
|
|
names := make([]string, 0, len(res.LevelOutputConfigs[eid]))
|
|
for k := range res.LevelOutputConfigs[eid] {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
sc := res.LevelOutputConfigs[eid][name]
|
|
groupIDs := sortedU32Keys2(sc.Scenes)
|
|
for _, gid := range groupIDs {
|
|
for _, p := range sc.Scenes[gid].Props {
|
|
if p.InstID == interactID || p.PropID == interactID {
|
|
return eid
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func sortedU32Keys2[V any](m map[uint32]V) []uint32 {
|
|
keys := make([]uint32, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
|
|
return keys
|
|
}
|
|
|
|
func sortedU32Keys[V any](m map[uint32]V) []uint32 { return sortedU32Keys2(m) }
|
|
|
|
// loadScene is the Go port of load_scene. When isEnterScene is true it also
|
|
// sends EnterSceneByServerScNotify and persists the new position.
|
|
func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32, reqSceneIdent *goproto.SceneIdentifier) (*goproto.SceneInfo, error) {
|
|
player := s.JSON()
|
|
if player == nil {
|
|
return nil, fmt.Errorf("data is not set!")
|
|
}
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
name, scene, ok := firstSceneOfEntry(res, entryID)
|
|
if !ok {
|
|
Log(fmt.Sprintf("Map Entrance Not Found %d", entryID))
|
|
return nil, fmt.Errorf("Map Entrance Not Found %d", entryID)
|
|
}
|
|
planeID, floorID, err := parsePlaneFloor(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jsonPos := player.Position
|
|
if teleportID != nil {
|
|
found := false
|
|
groupIDs := sortedU32Keys2(scene.Scenes)
|
|
for _, gid := range groupIDs {
|
|
if tp, ok := scene.Scenes[gid].Teleports[*teleportID]; ok {
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = tp.Pos.X, tp.Pos.Y, tp.Pos.Z, tp.Rot.Y
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found && len(groupIDs) > 0 {
|
|
// first teleport of the first group that has one
|
|
for _, gid := range groupIDs {
|
|
tpIDs := sortedU32Keys2(scene.Scenes[gid].Teleports)
|
|
if len(tpIDs) > 0 {
|
|
tp := scene.Scenes[gid].Teleports[tpIDs[0]]
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = tp.Pos.X, tp.Pos.Y, tp.Pos.Z, tp.Rot.Y
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else if isEnterScene || jsonPos.Y < -5000 {
|
|
// first teleport in the scene, else the teleport DB, else spawn near the first prop
|
|
var firstTP *common.TeleportInfo
|
|
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
|
tpIDs := sortedU32Keys2(scene.Scenes[gid].Teleports)
|
|
if len(tpIDs) > 0 {
|
|
tp := scene.Scenes[gid].Teleports[tpIDs[0]]
|
|
firstTP = &tp
|
|
break
|
|
}
|
|
}
|
|
if firstTP == nil {
|
|
if m := common.Teleports()[floorID]; m != nil {
|
|
for _, tp := range m {
|
|
t := tp
|
|
firstTP = &t
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if firstTP != nil {
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = firstTP.Pos.X, firstTP.Pos.Y, firstTP.Pos.Z, firstTP.Rot.Y
|
|
} else {
|
|
var firstProp *common.ScenePropInfo
|
|
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
|
if len(scene.Scenes[gid].Props) > 0 {
|
|
firstProp = &scene.Scenes[gid].Props[0]
|
|
break
|
|
}
|
|
}
|
|
if firstProp != nil {
|
|
// Endgame/arena floors have no teleports: spawn near the first
|
|
// prop (the interactable entrance) facing it
|
|
dx := float64(-firstProp.Pos.X)
|
|
dz := float64(-firstProp.Pos.Z)
|
|
dist := math.Sqrt(dx*dx + dz*dz)
|
|
nx, nz := 0.0, 1.0
|
|
if dist > 1000.0 {
|
|
nx, nz = dx/dist, dz/dist
|
|
}
|
|
offset := math.Min(2500.0, math.Max(dist, 1000.0))
|
|
jsonPos.X = firstProp.Pos.X + int32(nx*offset)
|
|
jsonPos.Y = firstProp.Pos.Y
|
|
jsonPos.Z = firstProp.Pos.Z + int32(nz*offset)
|
|
|
|
deg := int32(math.Round(math.Atan2(-nx, -nz) * 180.0 / math.Pi))
|
|
if deg < 0 {
|
|
deg += 360
|
|
}
|
|
jsonPos.RotY = deg * 1000
|
|
}
|
|
}
|
|
}
|
|
|
|
sceneIdent := reqSceneIdent
|
|
if sceneIdent != nil {
|
|
sceneIdent = proto.Clone(sceneIdent).(*goproto.SceneIdentifier)
|
|
if sceneIdent.FloorId == 0 {
|
|
sceneIdent.FloorId = floorID
|
|
}
|
|
} else {
|
|
sceneIdent = &goproto.SceneIdentifier{FloorId: floorID}
|
|
}
|
|
|
|
sceneInfo := &goproto.SceneInfo{
|
|
FloorId: floorID,
|
|
PlaneId: planeID,
|
|
EntryId: entryID,
|
|
GameModeType: scene.PlaneType,
|
|
LeaderEntityId: 1,
|
|
WorldId: scene.WorldID,
|
|
SceneIdentifier: sceneIdent,
|
|
FloorSavedData: scene.SavedValues,
|
|
}
|
|
if sceneInfo.WorldId == 100 {
|
|
sceneInfo.WorldId = 501
|
|
}
|
|
sceneInfo.LightenSectionList = scene.Sections
|
|
|
|
missionInfo := &goproto.MissionStatusBySceneInfo{}
|
|
var openedChests []uint32
|
|
for _, gid := range sortedU32Keys2(scene.Scenes) {
|
|
openedChests = append(openedChests, scene.Scenes[gid].Chests...)
|
|
missionInfo.FinishedMainMissionIdList = append(missionInfo.FinishedMainMissionIdList, scene.Scenes[gid].FinishedMainMissions...)
|
|
for _, sm := range scene.Scenes[gid].FinishedSubMissions {
|
|
missionInfo.SubMissionStatusList = append(missionInfo.SubMissionStatusList, &goproto.Mission{
|
|
Id: sm,
|
|
Status: goproto.MissionStatus_MISSION_FINISH,
|
|
Progress: 0,
|
|
})
|
|
}
|
|
}
|
|
sceneInfo.OpenedChestsList = openedChests
|
|
sceneInfo.SceneMissionInfo = missionInfo
|
|
|
|
playerPos := &goproto.MotionInfo{
|
|
Pos: &goproto.Vector{X: jsonPos.X, Y: jsonPos.Y, Z: jsonPos.Z},
|
|
Rot: &goproto.Vector{Y: jsonPos.RotY},
|
|
}
|
|
|
|
loadedNpc := map[uint32]bool{}
|
|
propEntityID := uint32(1000)
|
|
npcEntityID := uint32(20000)
|
|
monsterEntityID := uint32(30000)
|
|
|
|
s.PropEntities = map[uint32]PropEntry{}
|
|
s.MonsterEntities = map[uint32]uint32{}
|
|
|
|
for _, groupID := range sortedU32Keys2(scene.Scenes) {
|
|
group := scene.Scenes[groupID]
|
|
groupInfo := &goproto.SceneEntityGroupInfo{GroupId: groupID}
|
|
|
|
// props
|
|
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,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
// npcs
|
|
for i := range group.Npcs {
|
|
npc := &group.Npcs[i]
|
|
if loadedNpc[npc.NpcID] || hasAvatar(player, npc.NpcID) {
|
|
continue
|
|
}
|
|
loadedNpc[npc.NpcID] = true
|
|
npcEntityID++
|
|
|
|
groupInfo.EntityList = append(groupInfo.EntityList, &goproto.SceneEntityInfo{
|
|
InstId: npc.InstID,
|
|
GroupId: npc.GroupID,
|
|
EntityId: npcEntityID,
|
|
Motion: (&common.Position{X: npc.Pos.X, Y: npc.Pos.Y, Z: npc.Pos.Z, RotY: npc.Rot.Y}).ToMotionInfo(),
|
|
Entity: &goproto.SceneEntityInfo_Npc{
|
|
Npc: &goproto.SceneNpcInfo{NpcId: npc.NpcID},
|
|
},
|
|
})
|
|
}
|
|
|
|
// monsters
|
|
for i := range group.Monsters {
|
|
monster := &group.Monsters[i]
|
|
monsterEntityID++
|
|
s.MonsterEntities[monsterEntityID] = monster.EventID
|
|
|
|
groupInfo.EntityList = append(groupInfo.EntityList, &goproto.SceneEntityInfo{
|
|
InstId: monster.InstID,
|
|
GroupId: monster.GroupID,
|
|
EntityId: monsterEntityID,
|
|
Motion: (&common.Position{X: monster.Pos.X, Y: monster.Pos.Y, Z: monster.Pos.Z, RotY: monster.Rot.Y}).ToMotionInfo(),
|
|
Entity: &goproto.SceneEntityInfo_NpcMonster{
|
|
NpcMonster: &goproto.SceneNpcMonsterInfo{
|
|
MonsterId: monster.MonsterID,
|
|
EventId: monster.EventID,
|
|
WorldLevel: 6,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
if len(group.Props) == 0 && len(group.Npcs) == 0 && len(group.Monsters) == 0 {
|
|
continue
|
|
}
|
|
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, groupInfo)
|
|
}
|
|
|
|
// player entities (lineup slots 0..3 -> entity ids 1..4)
|
|
playerGroup := &goproto.SceneEntityGroupInfo{State: 0, GroupId: 0}
|
|
for _, slot := range player.SortedLineupKeys() {
|
|
avatarID := player.Lineups[slot]
|
|
playerGroup.EntityList = append(playerGroup.EntityList, &goproto.SceneEntityInfo{
|
|
EntityId: slot + 1,
|
|
Motion: playerPos,
|
|
Entity: &goproto.SceneEntityInfo_Actor{
|
|
Actor: &goproto.SceneActorInfo{
|
|
AvatarType: goproto.AvatarType_AVATAR_FORMAL_TYPE,
|
|
BaseAvatarId: avatarID,
|
|
MapLayer: 0,
|
|
Uid: 25,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, playerGroup)
|
|
|
|
if isEnterScene {
|
|
player.Scene.EntryID = entryID
|
|
player.Scene.FloorID = floorID
|
|
player.Scene.PlaneID = planeID
|
|
player.Position = jsonPos
|
|
player.SavePersistent()
|
|
|
|
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
|
Scene: sceneInfo,
|
|
Lineup: common.ToLineupInfo(player.Lineups),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return sceneInfo, nil
|
|
}
|
|
|
|
func hasAvatar(player *common.FreesrData, id uint32) bool {
|
|
_, ok := player.Avatars[id]
|
|
return ok
|
|
}
|