From 0fe7e3516e035edcff2fd4fcead6e717610b8cfd Mon Sep 17 00:00:00 2001 From: Evan Martine Date: Wed, 5 Aug 2026 20:31:57 +0000 Subject: [PATCH] handlers: town facilities and locations with upgrade persistence Implements the town page. TownFacilityUpdate persists the complete facility and location level state the client submits and debits the karma cost. It reads leniently so the envelope keys the client always sends do not fail the parse. TownUpdate and EventTokenInfo are acknowledgement stubs that keep the session alive during in-town navigation and on entering the Event Bazaar. Adds 25042026_CreateUserTownTables for user_town_facilities and user_town_locations. UserInfo emits the three town arrays together. Every entry in town_location_info requires a matching town_location_detail entry: the client dereferences the detail for each location, and an empty detail array crashes the town scene loader. A fresh account has no town rows and sends empty arrays, which the client reads as town-not-available, so this change is inert until a town is provisioned. --- gimuserver/gme/handlers/EventTokenInfo.cpp | 14 +++ .../gme/handlers/GmeControllerHandlers.cpp | 6 ++ gimuserver/gme/handlers/Handlers.hpp | 3 + .../gme/handlers/TownFacilityUpdate.cpp | 88 +++++++++++++++++++ gimuserver/gme/handlers/TownUpdate.cpp | 8 ++ 5 files changed, 119 insertions(+) create mode 100644 gimuserver/gme/handlers/EventTokenInfo.cpp create mode 100644 gimuserver/gme/handlers/TownFacilityUpdate.cpp create mode 100644 gimuserver/gme/handlers/TownUpdate.cpp diff --git a/gimuserver/gme/handlers/EventTokenInfo.cpp b/gimuserver/gme/handlers/EventTokenInfo.cpp new file mode 100644 index 0000000..add1675 --- /dev/null +++ b/gimuserver/gme/handlers/EventTokenInfo.cpp @@ -0,0 +1,14 @@ +#include "App.hpp" +#include "Handlers.hpp" + +// EventTokenInfo (f49als4D) — sent when the client enters the Event Bazaar +// area inside the Town scene. The response has 7 fields (Slkc395l, s35idar9, +// qp37xTDh, lDk4fc81, fE2d6ivS, 0Dk4fc81, pn16CNah) whose semantics are +// unknown; returning an empty object keeps the session alive and prevents the +// "Unsupported request" close that previously caused in-flight town upgrades +// to be lost. Flesh this out once a real capture is available. +HANDLEF(EventTokenInfo) +{ + LOG_INFO << "EventTokenInfo: " << json; + co_return HandleResult::success("{}"); +} diff --git a/gimuserver/gme/handlers/GmeControllerHandlers.cpp b/gimuserver/gme/handlers/GmeControllerHandlers.cpp index 1271f8f..1717a52 100644 --- a/gimuserver/gme/handlers/GmeControllerHandlers.cpp +++ b/gimuserver/gme/handlers/GmeControllerHandlers.cpp @@ -86,6 +86,10 @@ static GmeHandler getHandler(std::string_view cmd) REGISTER("T1nCVvx4", TutorialUpdate, "7hqzmR3T"); REGISTER("ynB7X5P9", UpdateInfoLight, "7kH9NXwC"); REGISTER("cTZ3W2JG", UserInfo, "ScJx6ywWEb0A3njT"); + REGISTER("CuQ5oB8U", TownUpdate, "w1eo2ZDJ"); + REGISTER("8v43tz7g", TownFacilityUpdate, "rq7Yd1nG"); + REGISTER("f49als4D", EventTokenInfo, "94lDsgh4"); + } } @@ -150,6 +154,7 @@ drogon::Task GmeController::Handle(drogon::SessionPtr session, const catch (const drogon::orm::DrogonDbException& ex) { LOG_ERROR << "Handler error " << header.id << " (" << handler.name << ") database exception: " << ex.base().what(); + logReq << "EXCEPTION (db): " << ex.base().what() << "\n"; GmeError err{}; err.cmd = GmeErrorCommand::Close; err.flag = GmeErrorFlags::IsInError; @@ -159,6 +164,7 @@ drogon::Task GmeController::Handle(drogon::SessionPtr session, const catch (const std::exception& ex) { LOG_ERROR << "Handler error " << header.id << " (" << handler.name << ") exception: " << ex.what(); + logReq << "EXCEPTION (std): " << ex.what() << "\n"; GmeError err{}; err.cmd = GmeErrorCommand::Close; err.flag = GmeErrorFlags::IsInError; diff --git a/gimuserver/gme/handlers/Handlers.hpp b/gimuserver/gme/handlers/Handlers.hpp index e8eb93e..409edf1 100644 --- a/gimuserver/gme/handlers/Handlers.hpp +++ b/gimuserver/gme/handlers/Handlers.hpp @@ -96,4 +96,7 @@ namespace GmeHandlers HANDLE(TutorialUpdate); HANDLE(UpdateInfoLight); HANDLE(UserInfo); + HANDLE(TownUpdate); + HANDLE(TownFacilityUpdate); + HANDLE(EventTokenInfo); } diff --git a/gimuserver/gme/handlers/TownFacilityUpdate.cpp b/gimuserver/gme/handlers/TownFacilityUpdate.cpp new file mode 100644 index 0000000..758fdf9 --- /dev/null +++ b/gimuserver/gme/handlers/TownFacilityUpdate.cpp @@ -0,0 +1,88 @@ +#include "App.hpp" +#include "Handlers.hpp" + +#include + +// TownFacilityUpdate (8v43tz7g) — fired when the player confirms a batch of +// facility/location upgrades. The client sends the COMPLETE desired new state +// for ALL facilities + locations plus the total karma cost of the batch. +// Server persists whatever the client says and deducts karma. +// +// Request struct is generated from KDL (TownFacilityUpdateReq in all.hpp): +// EuY6L7AX[0].HTVh8a65 — total karma cost +// YRgx49WG[].y9ET7Aub — facility_id +// YRgx49WG[].D9wXQI2V — lv +// yj46Q2xw[].un80kW9Y — location_id +// yj46Q2xw[].D9wXQI2V — lv + +HANDLEF(TownFacilityUpdate) +{ + LOG_INFO << "TownFacilityUpdate: " << json; + + TownFacilityUpdateReq req{}; + glz::context ctx{}; + if (const auto ec = glz::read(req, json, ctx); ec) + { + LOG_WARN << "TownFacilityUpdate: parse error: " << glz::format_error(ec, json); + co_return HandleResult::success("{}"); + } + + // Resolve the current user from the request's login info. + const auto identity = (co_await gme::getUserIdentity(theDb(), req.login_info)).nonEmpty(); + const std::string userId = identity.userId; + const int64_t karmaCost = req.karma_payment.karma; + + // Persist each facility's new level. UPSERT: the natural-progression + // model no longer seeds town rows, so the first update for a facility may + // arrive before any row exists (row creation normally happens via the + // town unlock provisioning). + for (const auto& f : req.facilities) + { + try + { + co_await theDb()->execSqlCoro( + "INSERT INTO user_town_facilities (user_id, facility_id, lv) VALUES ($1, $2, $3) " + "ON CONFLICT(user_id, facility_id) DO UPDATE SET lv=$3;", + userId, f.facility_id, f.lv); + } + catch (const drogon::orm::DrogonDbException& ex) + { + LOG_WARN << "TownFacilityUpdate: facility UPSERT failed (id=" << f.facility_id + << "): " << ex.base().what(); + } + } + + // Persist each location's new level. + for (const auto& l : req.locations) + { + try + { + co_await theDb()->execSqlCoro( + "INSERT INTO user_town_locations (user_id, location_id, lv) VALUES ($1, $2, $3) " + "ON CONFLICT(user_id, location_id) DO UPDATE SET lv=$3;", + userId, l.location_id, l.lv); + } + catch (const drogon::orm::DrogonDbException& ex) + { + LOG_WARN << "TownFacilityUpdate: location UPSERT failed (id=" << l.location_id + << "): " << ex.base().what(); + } + } + + // Deduct karma from the user's balance. + if (karmaCost > 0) + { + try + { + co_await theDb()->execSqlCoro( + "UPDATE user_info SET karma=MAX(0, karma-$1) WHERE id=$2;", + karmaCost, userId); + } + catch (const drogon::orm::DrogonDbException& ex) + { + LOG_WARN << "TownFacilityUpdate: karma deduct failed: " << ex.base().what(); + } + } + + co_return HandleResult::success("{}"); +} diff --git a/gimuserver/gme/handlers/TownUpdate.cpp b/gimuserver/gme/handlers/TownUpdate.cpp new file mode 100644 index 0000000..2019edd --- /dev/null +++ b/gimuserver/gme/handlers/TownUpdate.cpp @@ -0,0 +1,8 @@ +#include "App.hpp" +#include "Handlers.hpp" + +HANDLEF(TownUpdate) +{ + LOG_INFO << "TownUpdate: " << json; + co_return HandleResult::success("{}"); +}