Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions gimuserver/gme/handlers/DungeonEventUpdate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#include "App.hpp"
#include "Handlers.hpp"

// DungeonEventUpdate (BjAt1D6b / k5EiNe9x) — fires when the client enters
// the Grand Gaia world map after the opening cutscene. The original handler
// returns an empty JSON object; no fields are read by the client from this
// response. Without this handler the server returns "Unsupported request"
// and the client hard-crashes out of the cutscene.
HANDLEF(DungeonEventUpdate)
{
LOG_INFO << "DungeonEventUpdate: " << json;
co_return HandleResult::success("{}");
}

// GetScenarioPlayingInfo now lives in Scenario.cpp — it returns the real
// viewed-cutscene set from user_scenarios instead of the old {} stub.

// UpdatePermitPlaceInfo (1MJT6L3W / 3zip5Htw) — sent after entering an
// area to refresh the server-side permit-place allow-list. Original handler
// returns {}. Our PermitPlace is injected once in UserInfo so no update
// action is required here.
HANDLEF(UpdatePermitPlaceInfo)
{
LOG_INFO << "UpdatePermitPlaceInfo: " << json;
co_return HandleResult::success("{}");
}

// UpdateEventInfo (rCB7ZI8x / L1o4eGbi) — updates the client's view of
// active event data. Original handler returns {}.
HANDLEF(UpdateEventInfo)
{
LOG_INFO << "UpdateEventInfo: " << json;
co_return HandleResult::success("{}");
}

// Chronology (5o8ZlDGX / SNrhAG29) — Chronology timeline feature endpoint.
// Original handler returns {}.
HANDLEF(Chronology)
{
LOG_INFO << "Chronology: " << json;
co_return HandleResult::success("{}");
}
11 changes: 11 additions & 0 deletions gimuserver/gme/handlers/GmeControllerHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ static GmeHandler getHandler(std::string_view cmd)
REGISTER("ynB7X5P9", UpdateInfoLight, "7kH9NXwC");
REGISTER("cTZ3W2JG", UserInfo, "ScJx6ywWEb0A3njT");


// World-map / Grand Gaia entry sequence stubs.
REGISTER("BjAt1D6b", DungeonEventUpdate, "k5EiNe9x");
REGISTER("VRfsv4e3", GetScenarioPlayingInfo, "Bh4WqR01");
REGISTER("R38qvphm", RaidUpScenarioInfo, "72EyFbW8");
REGISTER("1MJT6L3W", UpdatePermitPlaceInfo, "3zip5Htw");
REGISTER("rCB7ZI8x", UpdateEventInfo, "L1o4eGbi");
REGISTER("5o8ZlDGX", Chronology, "SNrhAG29");

}
}

Expand Down Expand Up @@ -150,6 +159,7 @@ drogon::Task<GmeAction> 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;
Expand All @@ -159,6 +169,7 @@ drogon::Task<GmeAction> 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;
Expand Down
6 changes: 6 additions & 0 deletions gimuserver/gme/handlers/Handlers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,10 @@ namespace GmeHandlers
HANDLE(TutorialUpdate);
HANDLE(UpdateInfoLight);
HANDLE(UserInfo);
HANDLE(DungeonEventUpdate);
HANDLE(GetScenarioPlayingInfo);
HANDLE(RaidUpScenarioInfo);
HANDLE(UpdatePermitPlaceInfo);
HANDLE(UpdateEventInfo);
HANDLE(Chronology);
}
114 changes: 114 additions & 0 deletions gimuserver/gme/handlers/Scenario.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include "App.hpp"
#include "Handlers.hpp"

#include <gimuserver/gme/common/Common.hpp>

#include <chrono>

// Cutscene viewed-state persistence. The cutscene DATA is client-bundled
// (F_SCENARIO_MST: scenario id -> script file "mapN-*.txt" + BGM, reached via
// the CDN .dat pipeline — no MstResponse exists); the SERVER owns which
// scenarios this user has already watched, in the user_scenarios table.
//
// Flow: the client queries GetScenarioPlayingInfo during boot/scene entry and
// skips any cutscene whose id is in the returned sBbp47fi set; when a new
// cutscene finishes it reports the id via RaidUpScenarioInfo ("Up" = update —
// generic scenario reporter despite the Raid prefix). The legacy fork
// stubbed both with {}, which is why cutscenes replayed on every boot.

// GetScenarioPlayingInfo (VRfsv4e3 / Bh4WqR01) — bare request (identity tags
// only); returns the viewed-scenario set.
HANDLEF(GetScenarioPlayingInfo)
{
(void)session;
LOG_INFO << "GetScenarioPlayingInfo: " << json;

// Identity-only request (no body beyond the login-info tag).
GetScenarioPlayingInfoReq req = {};
{
glz::context ctx{};
if (const auto& ec = glz::read<glz::opts{ .error_on_unknown_keys = false }>(req, json, ctx); ec)
LOG_WARN << "GetScenarioPlayingInfo: parse error: " << glz::format_error(ec, json);
}

const auto identity = (co_await gme::getUserIdentity(theDb(), req.login_info)).nonEmpty();
const std::string userId = identity.userId;

GetScenarioPlayingInfoResp resp = {};
{
const auto rows = co_await theDb()->execSqlCoro(
"SELECT scenario_id FROM user_scenarios WHERE user_id = $1;", userId);
for (const auto& row : rows)
{
resp.scenarios.push_back(::UserScenarioInfo{
.scenario_id = row["scenario_id"].as<uint32_t>(),
});
}
}

std::string buffer{};
if (const auto& ec = glz::write_json(resp, buffer); ec)
{
const auto& glze = glz::format_error(ec, buffer);
LOG_DEBUG << "Gme GetScenarioPlayingInfo Error during JSON writing: " << glze;
co_return HandleResult::error("Serialization error", glze);
}

co_return HandleResult::success(buffer);
}

// RaidUpScenarioInfo (R38qvphm / 72EyFbW8) — stores a viewed scenario id.
// Request: {"1ry6BKoe":[{"N4XVE1uA":"<scenario>"}]}. The N4XVE1uA payload
// format is UNVERIFIED (expected: bare scenario id, quoted) — the INFO log
// below is the capture; non-numeric payloads are logged and skipped rather
// than guessed at (handbook §3.4 anti-pattern).
HANDLEF(RaidUpScenarioInfo)
{
(void)session;
LOG_INFO << "RaidUpScenarioInfo (CAPTURE — N4XVE1uA format unverified): " << json;

RaidUpScenarioInfoReq req = {};
if (const auto& ec = glz::read<glz::opts{ .error_on_unknown_keys = false }>(req, json); ec)
{
const auto& fmte = glz::format_error(ec, json);
LOG_DEBUG << "Gme RaidUpScenarioInfo Error during JSON read: " << fmte;
co_return HandleResult::error("Deserialization error", fmte);
}

const auto identity = (co_await gme::getUserIdentity(theDb(), req.login_info)).nonEmpty();
const std::string userId = identity.userId;

const auto now = static_cast<int64_t>(std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count());

for (const auto& node : req.nodes)
{
if (node.scenario_info.empty())
continue;

uint32_t scenarioId = 0;
try { scenarioId = static_cast<uint32_t>(std::stoul(node.scenario_info)); }
catch (...)
{
LOG_WARN << "RaidUpScenarioInfo: non-numeric N4XVE1uA payload (composite "
"format? capture above): " << node.scenario_info;
continue;
}

try
{
co_await theDb()->execSqlCoro(
"INSERT OR IGNORE INTO user_scenarios (user_id, scenario_id, viewed_at) "
"VALUES ($1, $2, $3);",
userId, scenarioId, now);
LOG_INFO << "RaidUpScenarioInfo: stored viewed scenario " << scenarioId;
}
catch (const drogon::orm::DrogonDbException& ex)
{
LOG_ERROR << "RaidUpScenarioInfo: store failed for " << scenarioId
<< ": " << ex.base().what();
}
}

co_return HandleResult::success("{}");
}