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
8 changes: 8 additions & 0 deletions gimuserver/gme/handlers/GmeControllerHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,19 @@ static GmeHandler getHandler(std::string_view cmd)
REGISTER("NiYWKdzs", HomeInfo, "f6uOewOD");
REGISTER("9TvyNR5H", MissionEnd, "oINq0rfUFPx5MgmT");
REGISTER("jE6Sp0q4", MissionStart, "csiVLDKkxEwBfR70");
REGISTER("ruoB7bD8", ItemEdit, "DHEfRexCu0q5TAQm");
REGISTER("0IXGiC9t", ItemSphereEqp, "CZE56XAY");
REGISTER("I8il6EiI", ItemFavorite, "aRoIftRy");
REGISTER("qDQerU74", ItemSell, "73aFNjPu");
REGISTER("4P5GELTF", ItemMix, "AFqKIJ8Z4mHPB9xg");
REGISTER("TA4MnZX8", NgwordCheck, "r4Smw5TX");
REGISTER("uV6yH5MX", CreateUser, "4agnATy2DrJsWzQk");
REGISTER("d36DaiJl", TutorialSkip, "p3qD61db");
REGISTER("T1nCVvx4", TutorialUpdate, "7hqzmR3T");
REGISTER("ynB7X5P9", UpdateInfoLight, "7kH9NXwC");
REGISTER("cTZ3W2JG", UserInfo, "ScJx6ywWEb0A3njT");


}
}

Expand Down Expand Up @@ -150,6 +156,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 +166,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
5 changes: 5 additions & 0 deletions gimuserver/gme/handlers/Handlers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ namespace GmeHandlers
HANDLE(HomeInfo);
HANDLE(MissionEnd);
HANDLE(MissionStart);
HANDLE(ItemEdit);
HANDLE(ItemSphereEqp);
HANDLE(ItemFavorite);
HANDLE(ItemSell);
HANDLE(ItemMix);
HANDLE(NgwordCheck);
HANDLE(CreateUser);
HANDLE(TutorialSkip);
Expand Down
27 changes: 27 additions & 0 deletions gimuserver/gme/handlers/ItemEdit.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#include "App.hpp"
#include "Handlers.hpp"

// ItemEdit (ruoB7bD8) — fired during mission prep when the player sets the
// battle-item loadout (potions/spheres carried into the fight), alongside the
// party/reinforcement select. The request body (ItemEditRequest::createBody,
// bfdata/createbody/ItemEditRequest.txt) is UserEquipItemInfo + bonus-item
// lists; the client tracks the loadout itself and MissionEnd reports what was
// consumed, so no response body is required.
//
// The legacy server (BF-WorkingDir ItemEditRequestHandler) simply acked with an
// empty OK — matched here. Without this handler the dispatcher returns
// "Unsupported request: ruoB7bD8", which the client renders as a Close dialog
// back to home on quest entry.
//
// GroupId = "ruoB7bD8", AES key = "DHEfRexCu0q5TAQm" (from the legacy
// ItemEditRequestHandler GetGroupId/GetAesKey).
//
// TODO (optional): persist the selected loadout to user_items equip state so it
// survives a reload; the client re-sends it each mission, so an ack suffices for
// now.
HANDLEF(ItemEdit)
{
(void)session;
LOG_INFO << "ItemEdit: " << json;
co_return HandleResult::success("{}");
}
69 changes: 69 additions & 0 deletions gimuserver/gme/handlers/ItemFavorite.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#include "App.hpp"
#include "Handlers.hpp"

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

// ItemFavorite (I8il6EiI) — favorite/lock warehouse item stacks. The client
// sends the COMPLETE current favorite set on every call (only flag=1 entries;
// ItemFavoriteRequest::createBody skips rows whose getFavoFlg() != 1), so the
// server resets favorite_flg for the user and re-marks the listed instance
// ids. An empty list is therefore valid — it means "nothing is favorited".
//
// Request : {"VSRPkdId":[{"n6E8iMf3":"<instance_id>","5JbjC3Pp":"1"},...]}
// Response: same shape echoed back (legacy ItemFavoriteResponse serialised
// an array of instance ids under VSRPkdId; the client readParam
// reads n6E8iMf3 + 5JbjC3Pp per node).
//
// The legacy fork acked without persisting; this port writes user_items.
// favorite_flg so locks survive a reload (UserInfo reports them back through
// UserInfoResp.item_favorite).
//
// GroupId = "I8il6EiI", AES key = "aRoIftRy" (legacy ItemFavoriteRequestHandler).
HANDLEF(ItemFavorite)
{
(void)session;
LOG_INFO << "ItemFavorite: " << json;

ItemFavoriteReq 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 ItemFavorite 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;

try
{
// The request is the full favorite set: clear-then-set.
co_await theDb()->execSqlCoro(
"UPDATE user_items SET favorite_flg = 0 WHERE user_id = $1;", userId);

for (const auto& e : req.entries)
{
co_await theDb()->execSqlCoro(
"UPDATE user_items SET favorite_flg = $1 "
"WHERE user_id = $2 AND instance_id = $3;",
e.favorite, userId, e.instance_id);
}
}
catch (const drogon::orm::DrogonDbException& ex)
{
LOG_ERROR << "ItemFavorite: DB error: " << ex.base().what();
}

ItemFavoriteResp resp = {};
resp.entries = req.entries;

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

co_return HandleResult::success(buffer);
}
26 changes: 26 additions & 0 deletions gimuserver/gme/handlers/ItemMix.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#include "App.hpp"
#include "Handlers.hpp"

// ItemMix (4P5GELTF) — item synthesis at the town Synthesis facility. The
// request body (ItemMixRequest::createBody, bfdata/createbody/ItemMixRequest.txt)
// is a single node under "JTf2jY5o" carrying ItemMixRequest::itemCsvData —
// an opaque CSV whose split format is UNVERIFIED (no capture yet; per
// handbook §3.4 we do not guess wire semantics).
//
// The legacy fork acked with an empty OK and the client applies the craft
// locally, so an ack keeps the synthesis menu from erroring. Materials are
// NOT consumed and the crafted item is NOT credited server-side until the
// CSV format is decoded — the INFO log below records the exact body, so the
// first real synthesis attempt in the client produces the capture needed to
// finish this handler (deploy/log/http_log_4P5GELTF_*.log).
//
// TODO(capture): decode itemCsvData → consume recipe materials from
// user_items via recipe_mst, credit the crafted item via gme::addUserItem.
//
// GroupId = "4P5GELTF", AES key = "AFqKIJ8Z4mHPB9xg" (legacy ItemMixRequestHandler).
HANDLEF(ItemMix)
{
(void)session;
LOG_INFO << "ItemMix (CAPTURE — itemCsvData format unverified): " << json;
co_return HandleResult::success("{}");
}
104 changes: 104 additions & 0 deletions gimuserver/gme/handlers/ItemSell.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#include "App.hpp"
#include "Handlers.hpp"

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

#include <algorithm>

// ItemSell (qDQerU74) — sell warehouse item stacks for zel. Request
// (ItemSellRequest::createBody, bfdata/createbody/ItemSellRequest.txt):
//
// "M73i1c5U": [{"n6E8iMf3":"<instance_id>","wgV86x1q":"<count>"}, ...]
//
// Zel credit per unit is ItemMst.sell_price (eKtE6k0n, legacy sellPrice).
// Stacks are decremented but the row is kept at 0 so instance ids stay stable
// for the client's local warehouse model; UserInfo filters zero stacks off
// the wire.
//
// Response: empty OK — the legacy fork acked without a body and the client
// applies the zel/count changes from its own state.
//
// GroupId = "qDQerU74", AES key = "73aFNjPu" (legacy ItemSellRequestHandler).
HANDLEF(ItemSell)
{
(void)session;
LOG_INFO << "ItemSell: " << json;

ItemSellReq 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 ItemSell 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& itemMst = theServer()->cache().itemMst();
const auto sellPrice = [&itemMst](uint32_t itemId) -> int64_t {
for (const auto& m : itemMst)
{
if (m.id == static_cast<int32_t>(itemId))
return m.sell_price;
}
LOG_WARN << "ItemSell: item " << itemId << " not in ItemMst — sell price defaults to 0";
return 0;
};

int64_t zelCredit = 0;
for (const auto& e : req.entries)
{
try
{
const auto rows = co_await theDb()->execSqlCoro(
"SELECT item_id, item_num FROM user_items "
"WHERE user_id = $1 AND instance_id = $2;",
userId, e.instance_id);
if (rows.size() == 0)
{
LOG_WARN << "ItemSell: instance " << e.instance_id << " not found";
continue;
}

const uint32_t itemId = rows[0]["item_id"].as<uint32_t>();
const uint32_t held = rows[0]["item_num"].as<uint32_t>();
const uint32_t sold = std::min<uint32_t>(held, e.item_num);
if (sold == 0)
continue;

co_await theDb()->execSqlCoro(
"UPDATE user_items SET item_num = item_num - $1 "
"WHERE user_id = $2 AND instance_id = $3;",
sold, userId, e.instance_id);

zelCredit += sellPrice(itemId) * sold;
LOG_INFO << "ItemSell: sold " << sold << "x item " << itemId
<< " (instance " << e.instance_id << ")";
}
catch (const drogon::orm::DrogonDbException& ex)
{
LOG_ERROR << "ItemSell: DB error for instance " << e.instance_id
<< ": " << ex.base().what();
}
}

if (zelCredit > 0)
{
try
{
// Same display-overflow cap the debug CLI enforces: the client
// wraps zel above 99'999'999 back to 0, which breaks cost checks.
co_await theDb()->execSqlCoro(
"UPDATE user_info SET zel = MIN(zel + $1, 99999999) WHERE id = $2;",
zelCredit, userId);
LOG_INFO << "ItemSell: credited " << zelCredit << " zel";
}
catch (const drogon::orm::DrogonDbException& ex)
{
LOG_ERROR << "ItemSell: zel credit failed: " << ex.base().what();
}
}

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