1151 lines
36 KiB
Go
1151 lines
36 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)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Protocol scene modes from PearlSR 005d2c12, manager/scene_mgr.zig and
|
|
// services/challenge.zig. MazePlane.PlaneType uses a different enumeration.
|
|
const (
|
|
sceneModeAdventure uint32 = 1
|
|
sceneModeChallenge uint32 = 4
|
|
)
|
|
|
|
// 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) {
|
|
if m, ok := res.LevelOutputConfigs[entryID]; ok && len(m) > 0 {
|
|
names := make([]string, 0, len(m))
|
|
for k := range m {
|
|
names = append(names, k)
|
|
}
|
|
sort.Strings(names)
|
|
return names[0], m[names[0]], true
|
|
}
|
|
if entryID != 0 {
|
|
name := fmt.Sprintf("P%d_F%d", entryID/100, entryID*10+1)
|
|
cfg := &common.LevelOutputConfig{
|
|
IsEnteredSceneInfo: true,
|
|
Scenes: map[uint32]common.SceneData{
|
|
1: {
|
|
Teleports: map[uint32]common.TeleportInfo{
|
|
entryID: {Pos: common.Vector{X: 0, Y: 0, Z: 0}},
|
|
1: {Pos: common.Vector{X: 0, Y: 0, Z: 0}},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if res.LevelOutputConfigs == nil {
|
|
res.LevelOutputConfigs = map[uint32]map[string]*common.LevelOutputConfig{}
|
|
}
|
|
res.LevelOutputConfigs[entryID] = map[string]*common.LevelOutputConfig{
|
|
name: cfg,
|
|
}
|
|
return name, cfg, true
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
// sceneOfEntryForTeleport selects the floor config that actually owns the
|
|
// requested teleport. Some resource sets put several floors under one entry;
|
|
// choosing the lexicographically first config can then send a valid scene
|
|
// transition with the wrong floor and spawn position.
|
|
func sceneOfEntryForTeleport(res *common.JsonConfig, entryID, floorID, teleportID uint32) (string, *common.LevelOutputConfig, bool) {
|
|
configs := res.LevelOutputConfigs[entryID]
|
|
names := make([]string, 0, len(configs))
|
|
for name := range configs {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
if floorID != 0 {
|
|
want := fmt.Sprintf("_F%d", floorID)
|
|
for _, name := range names {
|
|
cfg := configs[name]
|
|
if len(name) > len(want) && name[len(name)-len(want):] == want && sceneHasTeleport(cfg, teleportID) {
|
|
return name, cfg, true
|
|
}
|
|
}
|
|
}
|
|
if teleportID != 0 {
|
|
for _, name := range names {
|
|
cfg := configs[name]
|
|
if sceneHasTeleport(cfg, teleportID) {
|
|
return name, cfg, true
|
|
}
|
|
}
|
|
}
|
|
if floorID != 0 {
|
|
want := fmt.Sprintf("_F%d", floorID)
|
|
for _, name := range names {
|
|
if len(name) > len(want) && name[len(name)-len(want):] == want {
|
|
return name, configs[name], true
|
|
}
|
|
}
|
|
}
|
|
return firstSceneOfEntry(res, entryID)
|
|
}
|
|
|
|
func sceneHasTeleport(cfg *common.LevelOutputConfig, teleportID uint32) bool {
|
|
if cfg == nil || teleportID == 0 {
|
|
return false
|
|
}
|
|
for _, group := range cfg.Scenes {
|
|
if _, ok := group.Teleports[teleportID]; ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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("SetTrainWorldId", func(s *Session, req *goproto.SetTrainWorldIdCsReq, rsp *goproto.SetTrainWorldIdScRsp) error {
|
|
// The default unknown-request path sends an empty response. The Client
|
|
// expects the selected train world ID in this response before it enters
|
|
// Train-plane scenes.
|
|
rsp.Retcode = 0
|
|
rsp.TrainWorldId = req.TrainWorldId
|
|
SceneDebugLog("SET_TRAIN_WORLD_ID interact_id=%d world_id=%d", req.InteractId, req.TrainWorldId)
|
|
return nil
|
|
})
|
|
|
|
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, broken map, or void coordinate back to Belobog Admin District
|
|
entryID := player.Scene.EntryID
|
|
if (entryID >= 3000000 && entryID < 4000000) || entryID == 100000104 || entryID == 100000304 || entryID == 1030402 || entryID == 0 || player.Position.Y < -50000 {
|
|
entryID = 102020107
|
|
player.Scene.EntryID = 102020107
|
|
player.Scene.PlaneID = 10202
|
|
player.Scene.FloorID = 10202001
|
|
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
|
|
player.SavePersistent()
|
|
}
|
|
|
|
defaultScene := &goproto.SceneInfo{
|
|
GameModeType: 1,
|
|
EntryId: entryID,
|
|
}
|
|
if entryID == 102020107 {
|
|
defaultScene.PlaneId = 10202
|
|
defaultScene.FloorId = 10202001
|
|
} else {
|
|
defaultScene.PlaneId = player.Scene.PlaneID
|
|
defaultScene.FloorId = player.Scene.FloorID
|
|
}
|
|
|
|
scene, err := loadScene(s, entryID, false, 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 scene_identifier=%v", req.EntryId, req.TeleportId, req.InteractId, req.SceneIdentifier)
|
|
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
player := s.JSON()
|
|
currentEntry := func() uint32 {
|
|
if player != nil && player.Scene.EntryID != 0 && player.Scene.EntryID != 100000104 && player.Scene.EntryID != 100000304 {
|
|
return player.Scene.EntryID
|
|
}
|
|
return 102020107
|
|
}
|
|
|
|
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, with cascading fallbacks)
|
|
resolvedEntry := uint32(0)
|
|
if req.EntryId != 0 && mapHasEntry(res, req.EntryId) {
|
|
resolvedEntry = req.EntryId
|
|
} else if sceneIdent != nil && sceneIdent.FloorId != 0 {
|
|
resolvedEntry = findEntryForFloor(res, sceneIdent.FloorId)
|
|
}
|
|
if resolvedEntry == 0 && req.EntryId != 0 {
|
|
resolvedEntry = findEntryForFloor(res, req.EntryId)
|
|
}
|
|
if resolvedEntry == 0 && req.TeleportId != 0 {
|
|
floorID := uint32(0)
|
|
if sceneIdent != nil {
|
|
floorID = sceneIdent.FloorId
|
|
}
|
|
if entry := findEntryByTeleportInFloor(res, req.TeleportId, floorID); entry != 0 {
|
|
resolvedEntry = entry
|
|
} else {
|
|
resolvedEntry = findEntryByTeleport(res, req.TeleportId, 0)
|
|
}
|
|
}
|
|
if resolvedEntry == 0 && sceneIdent != nil && sceneIdent.TeleportNigger != nil {
|
|
if t, okc := sceneIdent.TeleportNigger.(*goproto.SceneIdentifier_MDAIDPPKOPO); okc && t.MDAIDPPKOPO != nil && t.MDAIDPPKOPO.PLIDNBMCIJH != 0 {
|
|
tpID := t.MDAIDPPKOPO.PLIDNBMCIJH
|
|
if entry := findEntryByTeleportInFloor(res, tpID, sceneIdent.FloorId); entry != 0 {
|
|
resolvedEntry = entry
|
|
} else {
|
|
resolvedEntry = findEntryByTeleport(res, tpID, 0)
|
|
}
|
|
}
|
|
}
|
|
if resolvedEntry == 0 && interactID != 0 {
|
|
resolvedEntry = findEntryByInteract(res, interactID, 0)
|
|
}
|
|
if resolvedEntry == 0 && req.EntryId != 0 {
|
|
resolvedEntry = req.EntryId
|
|
}
|
|
if resolvedEntry == 0 {
|
|
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
|
|
}
|
|
|
|
// Map checkpoints may name the Mission entrance even when a Town
|
|
// entrance exists on the same floor. Route only checkpoint teleports;
|
|
// direct story entries and scripted prop interactions keep their entry.
|
|
if teleportID != nil && *teleportID != 0 && interactID == 0 && !mapHasEntry(res, req.EntryId) {
|
|
floorID := res.MapEntrances[resolvedEntry].FloorID
|
|
if floorID == 0 && sceneIdent != nil {
|
|
floorID = sceneIdent.FloorId
|
|
}
|
|
if target := res.MapTeleportEntrances[floorID]; target != 0 {
|
|
SceneDebugLog("MAP_TELEPORT_ENTRY requested=%d resolved=%d floor=%d teleport=%d", resolvedEntry, target, floorID, *teleportID)
|
|
resolvedEntry = target
|
|
}
|
|
}
|
|
|
|
sceneInfo, err := loadScene(s, resolvedEntry, true, false, 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 floor_id=%d plane_id=%d teleport_id=%d", req.EntryId, resolvedEntry, sceneInfo.FloorId, sceneInfo.PlaneId, teleportIDValue(teleportID))
|
|
rsp.Retcode = 0
|
|
rsp.SceneIdentifier = sceneInfo.SceneIdentifier
|
|
rsp.IsOverMap = true
|
|
rsp.IsCloseMap = req.IsCloseMap
|
|
if err := s.Send(rsp); err != nil {
|
|
return err
|
|
}
|
|
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
|
Scene: sceneInfo,
|
|
Lineup: s.techniqueLineup(),
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
return errResponseAlreadySent
|
|
})
|
|
|
|
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 {
|
|
if p.State == 0 {
|
|
p.State = 1
|
|
s.PropEntities[req.PropEntityId] = p
|
|
}
|
|
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
|
|
})
|
|
|
|
// decodeSceneIdentifiersLenient: some Client scene-map payloads can
|
|
// fail strict proto parsing (field layout drift between builds) — walk the
|
|
// wire format manually and pull floor_id/content_id out of field 2.
|
|
// GetSceneMapInfo as a raw handler: some Client scene-map payloads
|
|
// can fail strict proto parsing (layout drift), so fall back to a manual
|
|
// wire walk instead of dropping the request (the map UI stalls without
|
|
// this reply - no pillars, "navigation loading").
|
|
rawNamed("GetSceneMapInfoCsReq", func(s *Session, payload []byte) error {
|
|
res, err := common.GameRes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rsp := &goproto.GetSceneMapInfoScRsp{}
|
|
|
|
var identifiers []*goproto.SceneIdentifier
|
|
var strictReq goproto.GetSceneMapInfoCsReq
|
|
if err := proto.Unmarshal(payload, &strictReq); err == nil {
|
|
identifiers = strictReq.SceneIdentifiers
|
|
}
|
|
if len(identifiers) == 0 {
|
|
identifiers = decodeSceneIdentifiersLenient(payload)
|
|
if len(identifiers) > 0 {
|
|
PacketLog("[SCENE] GetSceneMapInfo: used lenient decode (strict parse failed)")
|
|
}
|
|
}
|
|
|
|
for _, si := range identifiers {
|
|
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},
|
|
)
|
|
|
|
// resolve the floor's entry directly (mapDefaultEntranceMap covers
|
|
// only 115/371 floors - new-region maps were left with no unlocked
|
|
// teleports, which locked the pillars)
|
|
entryID := findEntryForFloor(res, floorID)
|
|
if player := s.JSON(); player != nil {
|
|
current := res.MapEntrances[player.Scene.EntryID]
|
|
if current.FloorID == floorID && mapHasEntry(res, player.Scene.EntryID) {
|
|
entryID = player.Scene.EntryID
|
|
}
|
|
}
|
|
if 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, PropertyMap: s.sceneGroupProperties(floorID, groupID, cfg.GroupProperties[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
|
|
// Client-owned groups still have saved properties used by navmap
|
|
// conditions. Include data without spawning their entities.
|
|
for _, groupID := range sortedU32Keys2(cfg.GroupProperties) {
|
|
if _, exists := cfg.Scenes[groupID]; exists {
|
|
continue
|
|
}
|
|
mapInfo.GroupList = append(mapInfo.GroupList, &goproto.MapInfoGroup{GroupId: groupID, PropertyMap: s.sceneGroupProperties(floorID, groupID, cfg.GroupProperties[groupID])})
|
|
}
|
|
if cfg.SavedValues != nil {
|
|
mapInfo.FloorSavedData = cfg.SavedValues
|
|
}
|
|
}
|
|
}
|
|
|
|
// teleports.json covers every map (incl. floors whose res config
|
|
// has no inline teleports)
|
|
if m := common.Teleports()[floorID]; m != nil {
|
|
seen := map[uint32]bool{}
|
|
for _, t := range mapInfo.UnlockedTeleportList {
|
|
seen[t] = true
|
|
}
|
|
for id := range m {
|
|
if id != 0 && !seen[id] {
|
|
mapInfo.UnlockedTeleportList = append(mapInfo.UnlockedTeleportList, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
rsp.SceneMapInfo = append(rsp.SceneMapInfo, mapInfo)
|
|
}
|
|
|
|
return s.Send(rsp)
|
|
})
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
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
|
|
seen := map[uint32]bool{}
|
|
var tps []uint32
|
|
|
|
// 1. All IDs from mission_unlocks.json
|
|
unlocks := getMissionUnlocks()
|
|
for _, id := range unlocks.TeleportIDs {
|
|
if id != 0 && !seen[id] {
|
|
seen[id] = true
|
|
tps = append(tps, id)
|
|
}
|
|
}
|
|
|
|
// 2. All IDs from teleports DB across all floors
|
|
for _, m := range common.Teleports() {
|
|
for id := range m {
|
|
if id != 0 && !seen[id] {
|
|
seen[id] = true
|
|
tps = append(tps, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. All teleport IDs from LevelOutputConfigs
|
|
if res, err := common.GameRes(); err == nil {
|
|
for _, m := range res.LevelOutputConfigs {
|
|
for _, cfg := range m {
|
|
for _, sc := range cfg.Scenes {
|
|
for tid := range sc.Teleports {
|
|
if tid != 0 && !seen[tid] {
|
|
seen[tid] = true
|
|
tps = append(tps, tid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Any entry IDs requested by client or in EntranceIDs
|
|
for _, eid := range req.EntryIdList {
|
|
if eid != 0 && !seen[eid] {
|
|
seen[eid] = true
|
|
tps = append(tps, eid)
|
|
}
|
|
}
|
|
for _, id := range unlocks.EntranceIDs {
|
|
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
|
|
})
|
|
|
|
// cmd 1438 -> 1409: handle entity teleport within scene
|
|
handle("SceneEntityTeleport", func(s *Session, req *goproto.SceneEntityTeleportCsReq, rsp *goproto.SceneEntityTeleportScRsp) error {
|
|
rsp.Retcode = 0
|
|
rsp.EntityMotion = req.EntityMotion
|
|
if req.EntityMotion != nil && req.EntityMotion.Motion != nil {
|
|
motion := req.EntityMotion.Motion
|
|
player := s.JSON()
|
|
if player != nil {
|
|
if motion.Pos != nil {
|
|
player.Position.X = motion.Pos.X
|
|
player.Position.Y = motion.Pos.Y
|
|
player.Position.Z = motion.Pos.Z
|
|
}
|
|
if motion.Rot != nil {
|
|
player.Position.RotY = motion.Rot.Y
|
|
}
|
|
player.SavePersistent()
|
|
}
|
|
}
|
|
SceneDebugLog("SCENE_ENTITY_TELEPORT entry_id=%d", req.EntryId)
|
|
return nil
|
|
})
|
|
|
|
// cmd 1267 -> 1263: teleport to mission reset point
|
|
handle("TeleportToMissionResetPoint", func(s *Session, req *goproto.TeleportToMissionResetPointCsReq, rsp *goproto.TeleportToMissionResetPointScRsp) error {
|
|
rsp.Retcode = 0
|
|
player := s.JSON()
|
|
if player != nil {
|
|
rsp.Motion = &goproto.MotionInfo{
|
|
Pos: &goproto.Vector{
|
|
X: player.Position.X,
|
|
Y: player.Position.Y,
|
|
Z: player.Position.Z,
|
|
},
|
|
Rot: &goproto.Vector{
|
|
Y: player.Position.RotY,
|
|
},
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// findEntryForFloor resolves an entry id for a floor WITHOUT relying on
|
|
// mapDefaultEntranceMap (which misses 287/371 floors): search the config
|
|
// names "P<plane>_F<floor>" directly.
|
|
func findEntryForFloor(res *common.JsonConfig, floorID uint32) uint32 {
|
|
if entryID := res.MapDefaultEntranceMap[floorID]; entryID != 0 {
|
|
return entryID
|
|
}
|
|
suffix := fmt.Sprintf("_F%d", floorID)
|
|
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
|
|
for _, eid := range entryIDs {
|
|
for name := range res.LevelOutputConfigs[eid] {
|
|
if len(name) > len(suffix) && name[len(name)-len(suffix):] == suffix {
|
|
return eid
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func mapHasEntry(res *common.JsonConfig, entryID uint32) bool {
|
|
_, ok := res.LevelOutputConfigs[entryID]
|
|
return ok
|
|
}
|
|
|
|
// findEntryByTeleportInFloor resolves a teleport inside a KNOWN floor
|
|
// (map-teleport clicks always carry the floor in scene_identifier).
|
|
func findEntryByTeleportInFloor(res *common.JsonConfig, teleportID, floorID uint32) uint32 {
|
|
if floorID != 0 {
|
|
suffix := fmt.Sprintf("_F%d", floorID)
|
|
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
|
|
for _, eid := range entryIDs {
|
|
for name, cfg := range res.LevelOutputConfigs[eid] {
|
|
if len(name) <= len(suffix) || name[len(name)-len(suffix):] != suffix {
|
|
continue
|
|
}
|
|
for _, sc := range cfg.Scenes {
|
|
if _, ok := sc.Teleports[teleportID]; ok {
|
|
return eid
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// floor known but teleport not in its res config (teleports.json-only
|
|
// floors): the entry owning the floor is still the right destination
|
|
return findEntryForFloor(res, floorID)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func findEntryByTeleport(res *common.JsonConfig, teleportID uint32, fallback uint32) uint32 {
|
|
// NOTE: never scan teleports.json globally by id alone - teleport ids
|
|
// (1001, 1002, ...) repeat on every floor, which sent players to a random
|
|
// region ("AA teleport landed in AP").
|
|
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
|
|
}
|
|
|
|
// decodeSceneIdentifiersLenient walks the raw wire format of a
|
|
// GetSceneMapInfoCsReq payload: field 2 = repeated SceneIdentifier;
|
|
// inside each: floor_id = 13 (varint), content_id = 7 (varint).
|
|
func decodeSceneIdentifiersLenient(payload []byte) []*goproto.SceneIdentifier {
|
|
var out []*goproto.SceneIdentifier
|
|
buf := payload
|
|
for len(buf) > 0 {
|
|
tag, n := readUvarint(buf)
|
|
if n == 0 {
|
|
break
|
|
}
|
|
buf = buf[n:]
|
|
fieldNumber := uint32(tag >> 3)
|
|
wireType := uint32(tag & 0x7)
|
|
|
|
if fieldNumber == 2 && wireType == 2 {
|
|
l, n2 := readUvarint(buf)
|
|
if n2 == 0 || int(l) > len(buf[n2:]) {
|
|
break
|
|
}
|
|
sub := buf[n2 : n2+int(l)]
|
|
buf = buf[n2+int(l):]
|
|
|
|
si := &goproto.SceneIdentifier{}
|
|
for len(sub) > 0 {
|
|
stag, n3 := readUvarint(sub)
|
|
if n3 == 0 {
|
|
break
|
|
}
|
|
sfn := uint32(stag >> 3)
|
|
swt := uint32(stag & 0x7)
|
|
sub2 := sub[n3:]
|
|
if swt == 0 && sfn == 13 {
|
|
v, n4 := readUvarint(sub2)
|
|
if n4 == 0 {
|
|
break
|
|
}
|
|
sub = sub2[n4:]
|
|
si.FloorId = uint32(v)
|
|
} else if swt == 0 && sfn == 7 {
|
|
v, n4 := readUvarint(sub2)
|
|
if n4 == 0 {
|
|
break
|
|
}
|
|
sub = sub2[n4:]
|
|
si.ContentId = uint32(v)
|
|
} else {
|
|
skipWireField(swt, &sub)
|
|
}
|
|
}
|
|
out = append(out, si)
|
|
} else {
|
|
skipWireField(wireType, &buf)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
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 updates
|
|
// the player's scene; sendNotify controls whether it also emits the notify.
|
|
func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify 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
|
|
}
|
|
|
|
floorHint := uint32(0)
|
|
if reqSceneIdent != nil {
|
|
floorHint = reqSceneIdent.FloorId
|
|
}
|
|
requestedTeleport := uint32(0)
|
|
if teleportID != nil {
|
|
requestedTeleport = *teleportID
|
|
}
|
|
name, scene, ok := sceneOfEntryForTeleport(res, entryID, floorHint, requestedTeleport)
|
|
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
|
|
}
|
|
|
|
var cwEntrance *common.CurrencyWarsEntrance
|
|
cwCatalog, err := common.LoadCurrencyWarsCatalog()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cwCatalog.Entrance.EntryID == entryID {
|
|
catalog := cwCatalog
|
|
if catalog.Entrance.PlaneID != planeID || catalog.Entrance.FloorID != floorID {
|
|
return nil, fmt.Errorf("CW entrance scene differs from captured start anchor")
|
|
}
|
|
if isEnterScene && teleportID == nil {
|
|
cwEntrance = &catalog.Entrance
|
|
}
|
|
scene, err = currencyWarsEntranceScene(scene, catalog.Entrance)
|
|
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 {
|
|
// exact teleport from teleports.json (covers floors whose res
|
|
// config has no inline teleports)
|
|
if m := common.Teleports()[floorID]; m != nil {
|
|
if tp, ok := m[*teleportID]; ok {
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = tp.Pos.X, tp.Pos.Y, tp.Pos.Z, tp.Rot.Y
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
SceneDebugLog("LOAD_SCENE_TELEPORT entry_id=%d config=%s floor_id=%d teleport_id=%d matched=%t pos=(%d,%d,%d)", entryID, name, floorID, *teleportID, found, jsonPos.X, jsonPos.Y, jsonPos.Z)
|
|
} else if cwEntrance != nil {
|
|
anchor := cwEntrance.Anchor
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = anchor.Pos.X, anchor.Pos.Y, anchor.Pos.Z, anchor.Rot.Y
|
|
SceneDebugLog("CW_ENTRANCE_SPAWN entry=%d floor=%d group=%d anchor=%d pos=(%d,%d,%d)", entryID, floorID, cwEntrance.StartGroupID, anchor.ID, anchor.Pos.X, anchor.Pos.Y, anchor.Pos.Z)
|
|
} else if anchor, ok := sceneEntranceAnchor(res, entryID, floorID); isEnterScene && ok {
|
|
jsonPos.X, jsonPos.Y, jsonPos.Z, jsonPos.RotY = anchor.Pos.X, anchor.Pos.Y, anchor.Pos.Z, anchor.Rot.Y
|
|
SceneDebugLog("ENTRANCE_ANCHOR_SPAWN entry=%d floor=%d group=%d anchor=%d pos=(%d,%d,%d)", entryID, floorID, res.MapEntrances[entryID].StartGroupID, anchor.ID, anchor.Pos.X, anchor.Pos.Y, anchor.Pos.Z)
|
|
} 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{
|
|
DimensionId: scene.DimensionID,
|
|
FloorId: floorID,
|
|
PlaneId: planeID,
|
|
EntryId: entryID,
|
|
GameModeType: sceneModeAdventure,
|
|
LeaderEntityId: 1,
|
|
WorldId: scene.WorldID,
|
|
SceneIdentifier: sceneIdent,
|
|
FloorSavedData: scene.SavedValues,
|
|
}
|
|
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.CurrentSceneFloor = floorID
|
|
s.MonsterEntities = map[uint32]uint32{}
|
|
s.SceneMonsterIdentities = map[uint32]sceneMonsterIdentity{}
|
|
s.ChallengeStageID = 0
|
|
s.ChallengeBattleConfig = nil
|
|
s.ChallengeRun = nil
|
|
|
|
for _, groupID := range sortedU32Keys2(scene.Scenes) {
|
|
group := scene.Scenes[groupID]
|
|
groupInfo := &goproto.SceneEntityGroupInfo{State: 1, GroupId: groupID, PropertyMap: s.sceneGroupProperties(floorID, groupID, scene.GroupProperties[groupID])}
|
|
|
|
// props
|
|
for i := range group.Props {
|
|
prop := &group.Props[i]
|
|
propEntityID++
|
|
s.PropEntities[propEntityID] = PropEntry{GroupID: prop.GroupID, 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
|
|
s.SceneMonsterIdentities[monsterEntityID] = sceneMonsterIdentity{MonsterID: monster.MonsterID, GroupID: monster.GroupID}
|
|
|
|
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: playerWorldLevel,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
sceneInfo.EntityList = append(sceneInfo.EntityList, groupInfo.EntityList...)
|
|
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)
|
|
s.partyActorSlots = map[uint32]bool{}
|
|
s.leaderSlot = player.TeamPreset(player.ActiveTeamPreset).LeaderSlot
|
|
playerGroup := &goproto.SceneEntityGroupInfo{State: 0, GroupId: 0}
|
|
for _, slot := range player.SortedLineupKeys() {
|
|
avatarID := player.Lineups[slot]
|
|
if avatarID == 0 {
|
|
continue
|
|
}
|
|
s.partyActorSlots[slot] = true
|
|
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: playerUID,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
if player.Lineups[s.leaderSlot] == 0 {
|
|
for _, slot := range player.SortedLineupKeys() {
|
|
if player.Lineups[slot] != 0 {
|
|
s.leaderSlot = slot
|
|
break
|
|
}
|
|
}
|
|
}
|
|
sceneInfo.LeaderEntityId = s.leaderSlot + 1
|
|
sceneInfo.EntityGroupList = append(sceneInfo.EntityGroupList, playerGroup)
|
|
SceneDebugLog("[SCENE] entry=%d floor=%d plane=%d world=%d mode=%d savedValues=%d groups=%v", entryID, floorID, planeID, sceneInfo.WorldId, sceneInfo.GameModeType, len(sceneInfo.FloorSavedData), sortedU32Keys2(scene.Scenes))
|
|
if res.MapTeleportEntrances[floorID] != 0 {
|
|
SceneDebugLog("SCENE_SAVED_VALUES entry=%d floor=%d values=%v", entryID, floorID, sceneInfo.FloorSavedData)
|
|
}
|
|
|
|
if isEnterScene {
|
|
// in-memory only: the scene is persisted by SceneEntityMove once the
|
|
// client actually moves. Saving here would brick the save when the
|
|
// client cannot load the destination (e.g. entry 100000304) - every
|
|
// login would then NRE in AdventurePhase on the same broken scene.
|
|
player.Scene.EntryID = entryID
|
|
player.Scene.FloorID = floorID
|
|
player.Scene.PlaneID = planeID
|
|
player.Position = jsonPos
|
|
|
|
if sendNotify {
|
|
if err := s.Send(&goproto.EnterSceneByServerScNotify{
|
|
Scene: sceneInfo,
|
|
Lineup: s.techniqueLineup(),
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return sceneInfo, nil
|
|
}
|
|
|
|
func teleportIDValue(id *uint32) uint32 {
|
|
if id == nil {
|
|
return 0
|
|
}
|
|
return *id
|
|
}
|
|
|
|
func hasAvatar(player *common.FreesrData, id uint32) bool {
|
|
_, ok := player.Avatars[id]
|
|
return ok
|
|
}
|