Files
GaCy-GO/internal/gameserver/handlers_scene.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

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
}