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("{}"); +}