From c16c95db945e2354eb81a6d2d01e06bcc7afa6bc Mon Sep 17 00:00:00 2001 From: capyb2222 Date: Sat, 12 Sep 2026 20:30:03 -0700 Subject: [PATCH] fix(server/game): replace the old session on a new login --- src/game/login_handlers.cpp | 3 +++ src/net/gateway.cpp | 14 ++++++++++++++ src/net/gateway.h | 3 +++ src/net/session.cpp | 2 ++ src/net/session.h | 3 +++ tests/test_flow.cpp | 13 +++++++++++++ 6 files changed, 38 insertions(+) diff --git a/src/game/login_handlers.cpp b/src/game/login_handlers.cpp index cd2776f..455839a 100644 --- a/src/game/login_handlers.cpp +++ b/src/game/login_handlers.cpp @@ -32,6 +32,9 @@ void fillBasicInfo(proto::PlayerBasicInfo& info, const Player& player) { void onGetToken(net::Session& session, const proto::PlayerGetTokenCsReq& req) { uint32_t uid = core::Config::get().player.uid; + // Before the save is read: a session this login replaces saves its newest state on the + // way out, and cannot write a stale copy over this one's later. + session.dropOtherLogins(uid); auto player = std::make_shared(uid); player->setMainCharacter(core::Config::get().gameplay.mainCharacter); player->setMarchType(core::Config::get().gameplay.marchType); diff --git a/src/net/gateway.cpp b/src/net/gateway.cpp index f5835c8..c54c96d 100644 --- a/src/net/gateway.cpp +++ b/src/net/gateway.cpp @@ -5,6 +5,7 @@ #include "core/logger.h" #include "core/util.h" +#include "game/player.h" namespace net { namespace { @@ -102,6 +103,19 @@ void Gateway::drop(uint32_t conv, const char* why) { logging::info("game", "conv {} from {} disconnected ({})", conv, session->remote().str(), why); } +void Gateway::dropOthers(const Session& keep, uint32_t uid) { + std::vector stale; + { + std::lock_guard lock(mutex_); + for (const auto& [conv, session] : sessions_) { + if (session.get() == &keep) continue; + const game::Player* player = session->player(); + if (player != nullptr && player->uid() == uid) stale.push_back(conv); + } + } + for (uint32_t conv : stale) drop(conv, "the account logged in again"); +} + void Gateway::receiveLoop() { std::vector buffer(kMaxDatagram); while (running_) { diff --git a/src/net/gateway.h b/src/net/gateway.h index c9c8823..75d5dda 100644 --- a/src/net/gateway.h +++ b/src/net/gateway.h @@ -24,6 +24,9 @@ public: // `why` is logged: knowing whether the client hung up, timed out or handshook // again is most of the diagnosis when a session ends unexpectedly. void drop(uint32_t conv, const char* why); + // A restarted client comes from a new port, so its old session outlives it; this drops + // that one before the new login reads the save it would otherwise overwrite later. + void dropOthers(const Session& keep, uint32_t uid); std::shared_ptr find(uint32_t conv); // Every session still connected, in conv order. The admin routes act on these. std::vector> sessions(); diff --git a/src/net/session.cpp b/src/net/session.cpp index e2cc6cf..2b824b9 100644 --- a/src/net/session.cpp +++ b/src/net/session.cpp @@ -143,6 +143,8 @@ void Session::sendRaw(uint16_t cmdId, std::string_view body) { void Session::sendEmpty(uint16_t cmdId) { sendRaw(cmdId, {}); } +void Session::dropOtherLogins(uint32_t uid) { gateway_.dropOthers(*this, uid); } + void Session::flushOutbound() { if (!kcp_) return; // ikcp_flush stamps every segment from kcp->current, and only ikcp_update ever diff --git a/src/net/session.h b/src/net/session.h index dc969d8..1a6ba69 100644 --- a/src/net/session.h +++ b/src/net/session.h @@ -54,6 +54,9 @@ public: void sendRaw(uint16_t cmdId, std::string_view body); void sendEmpty(uint16_t cmdId); + // Drops every other session logged in as `uid`; each saves on its way out. + void dropOtherLogins(uint32_t uid); + template void send(uint16_t cmdId, const T& message) { sendRaw(cmdId, message.serialize()); diff --git a/tests/test_flow.cpp b/tests/test_flow.cpp index a98532a..ae34401 100644 --- a/tests/test_flow.cpp +++ b/tests/test_flow.cpp @@ -1073,5 +1073,18 @@ void runFlowTests() { check(!client.await(cmd::ChessRogueQueryScRsp, packet, 600), "a silenced module gets no reply"); + // The same account logging in from a new port replaces the old session, instead of + // leaving it to write its stale copy over the new login's save when it times out. + { + TestClient second; + check(second.connect(kTestPort), "a second client connects"); + proto::PlayerGetTokenCsReq again; + again.account_uid = "capybara"; + second.send(cmd::PlayerGetTokenCsReq, again); + net::Packet secondPacket; + check(second.await(cmd::PlayerGetTokenScRsp, secondPacket), "and logs in"); + check(gateway.sessions().size() == 1, "leaving one session for the account"); + } + gateway.stop(); }