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) { 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_F". 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, 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 } 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: common.ToLineupInfo(player.Lineups), }); 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) if entryID := findEntryForFloor(res, 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 } } } // 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_F" 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 } 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 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{State: 1, 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, }, }, }) } 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) 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 { // 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: common.ToLineupInfo(player.Lineups), }); 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 }