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
157 changes: 155 additions & 2 deletions gimuserver/db/MigrationManager.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#include "App.hpp"
#include "MigrationManager.hpp"

using MigrationMap = std::unordered_map<std::string, std::function<void(drogon::orm::DbClientPtr& db)>>;
using MigrationEntry = std::pair<std::string, std::function<void(drogon::orm::DbClientPtr&)>>;
using MigrationMap = std::vector<MigrationEntry>;

#define migrate(name, func) map.insert_or_assign(name, [](drogon::orm::DbClientPtr& p) func )
#define migrate(name, func) map.emplace_back(name, [](drogon::orm::DbClientPtr& p) func )

/*!
* Register all the available migrations
Expand Down Expand Up @@ -99,6 +100,67 @@ static void RegisterMigrations(MigrationMap& map)
);
});

// Extra user_units columns used by the quests-branch handlers
// (UnitMix/UnitEvo/UnitSell/UnitFavorite, GachaAction, FriendGet,
// CampaignBattleStart). Consolidates the former
// 13032025_AddStatsToUserUnitsTable + 09042026_AddSphereSlotsToUserUnits +
// 14042026_AddFavoriteFlgToUserUnits migrations onto the upstream table
// shape. Upstream columns (unit_lvl/base_rec/ext_rec/bb_*) remain the
// source of truth for upstream handlers; these serve the not-yet-ported
// quests handlers and are consolidated away as each moves to
// PacketInterface.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont understand why this exists, a lot of it seems like duplicates of user units.

migrate("02072026_ExtendUserUnitsForUnitOps", {
p->execSqlSync("ALTER TABLE user_units ADD COLUMN unit_lv INTEGER NOT NULL DEFAULT 1");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN base_heal INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN add_hp INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN add_atk INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN add_def INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN add_heal INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN ext_heal INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN limit_over_hp INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN limit_over_atk INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN limit_over_def INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN limit_over_heal INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN exp INTEGER NOT NULL DEFAULT 1");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN total_exp INTEGER NOT NULL DEFAULT 1");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN skill_id INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN skill_lv INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN extra_skill_id INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN extra_skill_lv INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN leader_skill_id INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN element TEXT NOT NULL DEFAULT 'fire'");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN fe_bp INTEGER NOT NULL DEFAULT 100");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN fe_max_usable_bp INTEGER NOT NULL DEFAULT 200");
// Sphere equipment slots (UserUnitInfo: Ge8Yo32T/0R3qTPK9, mZA7fH2v/RXfC31FA).
p->execSqlSync("ALTER TABLE user_units ADD COLUMN eqip_item_id INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN eqip_item_frame_id INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN eqip_item_id2 INTEGER NOT NULL DEFAULT 0");
p->execSqlSync("ALTER TABLE user_units ADD COLUMN eqip_item_frame_id2 INTEGER NOT NULL DEFAULT 0");
// Lock/favorite flag (UnitFavoriteRequest: req["3kcmQy7B"][0]["5JbjC3Pp"]).
p->execSqlSync("ALTER TABLE user_units ADD COLUMN favorite_flg INTEGER NOT NULL DEFAULT 0");
});

migrate("25042026_CreateUserTownTables", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_town_facilities ("
"user_id TEXT NOT NULL,"
"facility_id INTEGER NOT NULL,"
"lv INTEGER NOT NULL DEFAULT 1,"
"karma INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, facility_id)"
");"
);
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_town_locations ("
"user_id TEXT NOT NULL,"
"location_id INTEGER NOT NULL,"
"lv INTEGER NOT NULL DEFAULT 1,"
"karma INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, location_id)"
");"
);
});

migrate("03072026_CreateUserUnitDictionaryTable", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_unit_dictionary ("
Expand All @@ -110,6 +172,82 @@ static void RegisterMigrations(MigrationMap& map)
);
});

// Owned-item inventory: potions, materials, spheres. One row per stack.
// instance_id is the warehouse row id the client references (UserWarehouse
// n6E8iMf3 / legacy ItemSphereEqp wh ids). Populated naturally — the
// tutorial seeds a test potion in CreateUser, mission drops append here.
migrate("05072026_CreateUserItemsTable", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_items ("
"instance_id INTEGER PRIMARY KEY AUTOINCREMENT,"
"user_id TEXT NOT NULL,"
"item_id INTEGER NOT NULL,"
"item_num INTEGER NOT NULL DEFAULT 1,"
"favorite_flg INTEGER NOT NULL DEFAULT 0,"
"disp_order INTEGER NOT NULL DEFAULT 0,"
"UNIQUE(user_id, item_id)"
");"
);
});

// Viewed-cutscene state: one row per scenario the user has watched.
// GetScenarioPlayingInfo returns this set (sBbp47fi) so the client skips
// already-seen cutscenes; RaidUpScenarioInfo appends to it. Structural
// only — never seeded; a fresh account sees every cutscene once.
migrate("17072026_CreateUserScenariosTable", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_scenarios ("
"user_id TEXT NOT NULL,"
"scenario_id INTEGER NOT NULL,"
"viewed_at INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, scenario_id)"
");"
);
});

migrate("25042026_CreateUserCampaignTables", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_campaign_missions ("
"user_id TEXT NOT NULL,"
"mission_id TEXT NOT NULL,"
"state INTEGER NOT NULL DEFAULT 0,"
"attain_percent INTEGER NOT NULL DEFAULT 0,"
"clear_count INTEGER NOT NULL DEFAULT 0,"
"last_cleared_at INTEGER NOT NULL DEFAULT 0,"
"reward_claimed INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, mission_id)"
");"
);
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_campaign_decks ("
"user_id TEXT NOT NULL,"
"deck_num INTEGER NOT NULL,"
"member_type INTEGER NOT NULL,"
"user_unit_id INTEGER NOT NULL,"
"disporder INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, deck_num, disporder)"
");"
);
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_campaign_state ("
"user_id TEXT PRIMARY KEY,"
"active_mission_id TEXT NOT NULL DEFAULT '',"
"active_battle_seed INTEGER NOT NULL DEFAULT 0,"
"saved_state TEXT NOT NULL DEFAULT ''"
");"
);
});

migrate("13052026_CreateUserSummonTicketsV2", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_summon_tickets_v2 ("
"user_id TEXT NOT NULL,"
"ticket_id INTEGER NOT NULL,"
"count INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY (user_id, ticket_id)"
");"
);
});
}

/*!
Expand Down Expand Up @@ -143,6 +281,21 @@ void MigrationManager::RunMigrations(drogon::orm::DbClientPtr ptr)
MigrationMap migrations;
RegisterMigrations(migrations);

// Migrations are a vector so they run in declared order (a hash map ran them
// unordered). The vector doesn't dedup, so guard the uniqueness the map used
// to give us: a duplicate name would run twice / mask an intended migration.
std::vector<std::string> seenNames;
for (const auto& [name, _] : migrations)
{
if (std::find(seenNames.begin(), seenNames.end(), name) != seenNames.end())
{
LOG_ERROR << "Duplicate migration name: " << name;
drogon::app().quit();
return;
}
seenNames.push_back(name);
}

std::vector<std::string> runnedMigratons;
GetMigrationStatus(ptr, runnedMigratons);

Expand Down
93 changes: 91 additions & 2 deletions gimuserver/db/PacketInterfaceSchemas.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ PacketInterfaceFor<::UserTeamInfo>::fields()
.update = true,
.insert = true,
}),
// The client's gem HUD reads brave_coin (03UGMHxF), so dev wires the gems
// column here on purpose (the "BraveCoin" readParam setter name is a red
// herring for this client). Do NOT point this at a real brave_coin column
// — that zeroed the HUD and broke gems. Kept mapped to `gems` with
// paid_gems/free_gems so every gem field the client might read shows gems.
field<&::UserTeamInfo::brave_coin>("gems", {
.read = true,
.update = true,
Expand All @@ -96,6 +101,35 @@ PacketInterfaceFor<::UserTeamInfo>::fields()
.update = true,
.insert = true,
}),
field<&::UserTeamInfo::summon_ticket>("summon_tickets", {
.read = true,
}),
field<&::UserTeamInfo::rainbow_coin>("rainbow_coins", {
.read = true,
}),
field<&::UserTeamInfo::colosseum_ticket>("colosseum_tickets", {
.read = true,
}),
field<&::UserTeamInfo::brave_points_total>("total_brave_points", {
.read = true,
}),
field<&::UserTeamInfo::current_brave_points>("avail_brave_points", {
.read = true,
}),
field<&::UserTeamInfo::want_gift>("want_gift", {
.read = true,
}),
// Send the single `gems` column to BOTH gem fields. The HUD reads FREE
// gems (92uj7oXB) — it was being sent as 0, which is why the balance
// never updated (while zel, whose field IS read, did). The summon/paid
// path reads PAID gems (d37CaiX1) — the user could summon earlier when
// only paid was populated, so keep it set too. A captured production
// team_info has both populated (free=10000, paid=20000); mirroring one
// column to both keeps display + summon working without double-counting
// (the HUD shows free, not the sum).
field<&::UserTeamInfo::free_gems>("gems", {
.read = true,
}),
field<&::UserTeamInfo::paid_gems>("gems", {
.read = true,
}),
Expand Down Expand Up @@ -129,7 +163,7 @@ PacketInterfaceFor<::UserUnitInfo>::fields()
.update = true,
.insert = true,
}),
field<&::UserUnitInfo::unit_lvl>("unit_lvl", {
field<&::UserUnitInfo::unit_lvl>("unit_lv", {
.read = true,
.update = true,
.insert = true,
Expand All @@ -149,6 +183,9 @@ PacketInterfaceFor<::UserUnitInfo>::fields()
.update = true,
.insert = true,
}),
// base_rec is INTEGER NOT NULL with NO default, so the INSERT must keep
// providing it (INSERT OR IGNORE silently drops the row otherwise). Read
// stays on the canonical column; addUserUnit writes it.
field<&::UserUnitInfo::base_rec>("base_rec", {
.read = true,
.update = true,
Expand All @@ -169,7 +206,7 @@ PacketInterfaceFor<::UserUnitInfo>::fields()
.update = true,
.insert = true,
}),
field<&::UserUnitInfo::ext_rec>("ext_rec", {
field<&::UserUnitInfo::ext_rec>("ext_heal", {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the rename?

.read = true,
.update = true,
.insert = true,
Expand All @@ -194,6 +231,27 @@ PacketInterfaceFor<::UserUnitInfo>::fields()
.update = true,
.insert = true,
}),
// Extras the read/display path was missing — mapped to the quests mirror

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually dont agree with adding everything here - it introduces bloat

for example, do we really need to add fe_bp? or leader_skill_id?

for each of these fields, we should only add them under 2 conditiions (this applies to every single other field as well)

  1. we understand fully what it does in the client
  2. the client needs it either for game functionailty or it crashes

// columns the unit handlers write, so units keep full stats/level/element
// across a UserInfo reload (previously lost to column defaults).
field<&::UserUnitInfo::exp>("exp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::total_exp>("total_exp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::add_hp>("add_hp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::add_atk>("add_atk", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::add_def>("add_def", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::add_rec>("add_heal", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::limit_over_hp>("limit_over_hp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::limit_over_atk>("limit_over_atk", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::limit_over_def>("limit_over_def", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::limit_over_rec>("limit_over_heal", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::element>("element", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::leader_skill_id>("leader_skill_id", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::fe_bp>("fe_bp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::fe_max_usable_bp>("fe_max_usable_bp", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::equipitem_id>("eqip_item_id", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::equipitem_frame_id>("eqip_item_frame_id", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::equipitem_id2>("eqip_item_id2", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::equipitem_frame_id2>("eqip_item_frame_id2", { .read = true, .update = true, .insert = true, }),
field<&::UserUnitInfo::is_new>("new", {
.read = true,
.update = true,
Expand Down Expand Up @@ -226,6 +284,37 @@ PacketInterfaceFor<::UserUnitDictionary>::fields()
};
}

/*!
* Database mapping for owned item stacks stored in user_items.
*/
template <>
inline PacketInterfaceFor<::UserWarehouseInfo>::Fields
PacketInterfaceFor<::UserWarehouseInfo>::fields()
{
return {
field<&::UserWarehouseInfo::instance_id>("instance_id", {
.read = true,
}),
field<&::UserWarehouseInfo::item_id>("item_id", {
.read = true,
.insert = true,
}),
field<&::UserWarehouseInfo::item_num>("item_num", {
.read = true,
.update = true,
.insert = true,
}),
field<&::UserWarehouseInfo::favorite_flg>("favorite_flg", {
.read = true,
.update = true,
}),
field<&::UserWarehouseInfo::disp_order>("disp_order", {
.read = true,
.update = true,
}),
};
}

/*!
* Database mapping for party deck slots stored in user_decks.
*/
Expand Down
Loading