diff --git a/src/game/player.cpp b/src/game/player.cpp index 83b7740..754b966 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -1,8 +1,10 @@ #include "game/player.h" #include "core/config.h" +#include "core/logger.h" #include "core/util.h" #include "data/excel.h" +#include "data/scene_res.h" #include "game/player_store.h" #include "game/srtools.h" @@ -11,6 +13,57 @@ namespace { constexpr uint64_t kSaveIntervalMs = 2000; +// MazePlane's Challenge type, as the scene dump numbers it, and the Express floor to +// fall back to. +constexpr uint32_t kChallengePlaneType = 4; +constexpr uint32_t kStartEntry = 1000002; + +// A run that never got to finish -- a server restart mid-floor, a crash -- leaves the +// saved spot inside a challenge arena with nothing left to rebuild it from. The client +// then gets a floor with no ground and no party under it: a black screen it can only +// look around in. Start on the Express instead. +void leaveStrandedArena(Player& player) { + const data::SceneRes& res = data::SceneRes::get(); + const data::ResFloor* floor = res.byEntry(player.location().entryId); + if (floor == nullptr || floor->planeType != kChallengePlaneType) return; + + const data::ResFloor* start = res.byEntry(kStartEntry); + if (start == nullptr) return; + logging::info("player", "uid {} was saved inside arena {}; starting at {} instead", + player.uid(), player.location().entryId, kStartEntry); + player.location().entryId = kStartEntry; + player.location().planeId = start->planeId; + player.location().floorId = start->floorId; + player.position() = Position{}; +} + +// The last saved spot can be mid-jump or mid-fall, and coming back to it leaves the +// player hanging in the air. Logging in lands on the floor's nearest anchor instead. +void landOnAnchor(Player& player) { + const data::ResFloor* floor = data::SceneRes::get().byEntry(player.location().entryId); + if (floor == nullptr) return; + + Position& at = player.position(); + const data::ResTeleport* nearest = nullptr; + int64_t best = 0; + for (const data::ResGroup& group : floor->groups) { + for (const data::ResTeleport& anchor : group.teleports) { + int64_t dx = static_cast(anchor.pos.x) - at.x; + int64_t dz = static_cast(anchor.pos.z) - at.z; + int64_t distance = dx * dx + dz * dz; + if (nearest == nullptr || distance < best) { + nearest = &anchor; + best = distance; + } + } + } + if (nearest == nullptr) return; + at.x = nearest->pos.x; + at.y = nearest->pos.y; + at.z = nearest->pos.z; + at.rotY = nearest->rot.y; +} + // A squad that at least exists, so a fresh account is not stranded with no party. void seedDefaultSquad(LineupBook& book) { if (book.cur().count() != 0) return; @@ -60,6 +113,8 @@ void Player::load() { loadPlayerState(*this); seedDefaultSquad(lineups_); lineups_.clampMp(); + leaveStrandedArena(*this); + landOnAnchor(*this); } void Player::save() { diff --git a/src/game/rescue.cpp b/src/game/rescue.cpp index ea812c5..9becb8f 100644 --- a/src/game/rescue.cpp +++ b/src/game/rescue.cpp @@ -1,6 +1,8 @@ #include "game/rescue.h" +#include "core/config.h" #include "core/logger.h" +#include "data/scene_res.h" #include "game/challenge.h" #include "game/peak.h" #include "game/player.h" @@ -85,5 +87,26 @@ std::string unstick(net::Session& session, uint32_t entryId) { return did; } +std::string relocate(uint32_t entryId) { + if (data::SceneRes::get().byEntry(entryId) == nullptr) { + return "entry " + std::to_string(entryId) + " is not in the scene dump"; + } + + Player player(core::Config::get().player.uid); + player.load(); + // scene::load only re-anchors when the floor changes, and the saved spot may already + // be on this one. Clearing the floor forces the entrance anchor -- keeping old + // coordinates, or worse (0, 0, 0), drops the player outside the geometry. + player.location().floorId = 0; + proto::SceneInfo scene; + if (!scene::load(player, entryId, 0, true, scene)) { + return "entry " + std::to_string(entryId) + " could not be loaded"; + } + player.saveNow(); + + logging::info("rescue", "no client connected; the save now starts at entry {}", entryId); + return "saved spot moved to entry " + std::to_string(entryId); +} + } // namespace rescue } // namespace game diff --git a/src/game/rescue.h b/src/game/rescue.h index bbea726..e79837d 100644 --- a/src/game/rescue.h +++ b/src/game/rescue.h @@ -23,5 +23,10 @@ namespace rescue { // Returns a one-line summary of what was done, for the log and the http reply. std::string unstick(net::Session& session, uint32_t entryId); +// The same escape with nobody connected: rewrites the saved spot so the next login +// starts somewhere else. For the floor the client cannot finish loading at all, where +// there is no session left to push anything to. +std::string relocate(uint32_t entryId); + } // namespace rescue } // namespace game diff --git a/src/sdk/admin.cpp b/src/sdk/admin.cpp index 8214577..db88bcd 100644 --- a/src/sdk/admin.cpp +++ b/src/sdk/admin.cpp @@ -18,7 +18,8 @@ namespace sdk { void registerAdminRoutes(http::Server& server, net::Gateway& gateway) { // GET /unstick -- push the current scene again - // GET /unstick?entry=1000002 -- and move to the Astral Express while doing it + // GET /unstick?entry=1000002 -- and move to the Astral Express while doing it, + // or move the save there when nothing is connected // // The client has no timeout on a transition it started, so a scene or a battle the // server never finished leaves it looking at nothing forever. Pushing a scene is @@ -34,7 +35,12 @@ void registerAdminRoutes(http::Server& server, net::Gateway& gateway) { if (session == nullptr) continue; out["sessions"].push_back(game::rescue::unstick(*session, entryId)); } - if (out["sessions"].empty()) { + if (out["sessions"].empty() && entryId != 0) { + // Nobody is connected, so move the saved spot instead. A client that cannot + // finish loading the floor it was left on has no session to rescue, and this + // is the only way out of it short of editing the save by hand. + out["message"] = game::rescue::relocate(entryId); + } else if (out["sessions"].empty()) { logging::warn("rescue", "nothing to unstick -- no client is connected"); out["message"] = "no client is connected"; } else {