fix(server/game): unstick without a client connected

This commit is contained in:
capyb2222 committed 2026-09-11 20:01:27 -07:00
1 parent 9dd37f8087
commit 9030af1c4d
4 files changed
+91 -2

No files matched your search

+55
View File
@@ -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<int64_t>(anchor.pos.x) - at.x;
int64_t dz = static_cast<int64_t>(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() {
+23
View File
@@ -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
+5
View File
@@ -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
+8 -2
View File
@@ -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 {