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
11 changes: 11 additions & 0 deletions deploy/archive/unit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"unit_id": 50253,
"lord_stats": {
"hp": 4500,
"atk": 1340,
"def": 1340,
"rec": 1680
}
}
]
1 change: 1 addition & 0 deletions deploy/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"game_version": 21900, // game version (must be this one or it might cry about an update)
"notice_url": "http://ios21900.bfww.gumi.sg/pages/versioninfo", // notice url (this points to a drogon orm page)
"mst_root": "./system", // MST cache directory
"archive_root": "./archive", // Server-owned curated archive data directory
"initial_level": 1,
"initial_free_gems": 5,
"initial_zel": 10000000,
Expand Down
35 changes: 16 additions & 19 deletions generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@
import os
from pathlib import Path

base_dir = "packet-generator/assets"
output_base = "gimuserver/packets"
kdl_file = "packet-generator/assets/all.kdl"
targets = [
("packet-generator/assets/all.kdl", "gimuserver/packets"),
("packet-generator/assets/archive.kdl", "gimuserver/archive"),
]

p_kdl_file = Path(kdl_file)
kdl_dir = p_kdl_file.parent
relative_dir = os.path.relpath(kdl_dir, base_dir)
kdl_file_real = os.path.relpath(kdl_file, "packet-generator")
target_dir = os.path.join(output_base, relative_dir)
for kdl_file, target_dir in targets:
kdl_file_real = os.path.relpath(kdl_file, "packet-generator")
os.makedirs(target_dir, exist_ok=True)

os.makedirs(target_dir, exist_ok=True)

try:
subprocess.run([
"cargo", "run", "--",
"generate", "--cxx", "--glaze",
"-i", kdl_file_real,
"-o", f"../{target_dir}"
], cwd="packet-generator", check=True)
except subprocess.CalledProcessError:
pass
try:
subprocess.run([
"cargo", "run", "--",
"generate", "--cxx", "--glaze",
"-i", kdl_file_real,
"-o", f"../{target_dir}"
], cwd="packet-generator", check=True)
except subprocess.CalledProcessError:
pass
1 change: 1 addition & 0 deletions gimuserver/archive/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
archive.hpp
83 changes: 83 additions & 0 deletions gimuserver/archive/UnitArchiver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#include "UnitArchiver.hpp"

#include <gimuserver/utils/JsonFile.hpp>

#include <drogon/drogon.h>

#include <cassert>
#include <exception>
#include <string>
#include <utility>
#include <vector>

UnitArchiver& UnitArchiver::Instance()
{
static UnitArchiver instance;
return instance;
}

void UnitArchiver::Setup(const Json::Value& serverObj)
{
LOG_INFO << "Setting up unit archiver cache. "
"If you see this after initialization, it is a bug.";

const auto archiveRoot = serverObj["archive_root"].asString();

std::vector<UnitRecord> units;
try
{
units = LoadJson<std::vector<UnitRecord>>(archiveRoot, "unit.json");
}
catch (const std::exception& ex)
{
LOG_ERROR << "Unable to set up unit archiver cache: " << ex.what();
return;
}

cache_.clear();
cache_.reserve(units.size());
for (auto& unit : units)
{
cache_.insert_or_assign(unit.unit_id, std::move(unit));
}
}

bool UnitArchiver::Lookup(UnitRecord& record)
{
// Callers should populate this key since we use it for lookups.
assert (record.unit_id > 0);

const auto it = cache_.find(record.unit_id);
if (it == cache_.end())
{
return false;
}

record = it->second;
return true;
}

bool UnitArchiver::Lookup(UserUnitInfo& unit)
{
// Callers should populate these keys since we use them for lookups.
assert (unit.unit_id > 0);
assert (unit.unit_type_id > 0);

UnitRecord record = {
.unit_id = unit.unit_id,
};

if (!Lookup(record))
{
return false;
}

// Grab the stats for the unit type.
const auto& stats = UnitTypeStats(record, unit.unit_type_id);
unit.base_hp = stats.hp;
unit.base_atk = stats.atk;
unit.base_def = stats.def;
unit.base_rec = stats.rec;

return true;
}
86 changes: 86 additions & 0 deletions gimuserver/archive/UnitArchiver.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#pragma once

#include <gimuserver/archive/archive.hpp>
#include <gimuserver/packets/all.hpp>

#include <json/value.h>

#include <cstdint>
#include <stdexcept>
#include <unordered_map>

/*!
* Loads and serves server-owned curated unit archive records.
*
* The archive is intentionally smaller than the official MST data. It contains
* only fields the server understands well enough to use when creating
* player-owned units.
*/
class UnitArchiver final
{
public:
using UnitId = uint32_t;
using UnitType = uint32_t;

/*!
* Gets the process-wide unit archiver.
*
* Setup() must be called during server initialization before request handlers
* depend on archive data.
*/
static UnitArchiver& Instance();

/*!
* Loads unit archive records from archive_root/unit.json.
* @param serverObj Server configuration object from the Drogon plugin config.
*/
void Setup(const Json::Value& serverObj);

/*!
* Looks up a unit archive record.
*
* The caller must set record.unit_id before calling. When found, the
* matching archive record replaces record.
* @param record Input lookup key and output archive record.
* @return True if the archive contains the unit, false otherwise.
*/
bool Lookup(UnitRecord& record);

/*!
* Looks up a unit archive record and converts it to the DB-backed subset of
* UserUnitInfo.
*
* The caller must set unit.unit_id and unit.unit_type_id before calling.
* Ownership fields such as user_id and user_unit_id are not modified.
* @param unit Input lookup/type keys and output user unit info.
* @return True if the archive contains the unit, false otherwise.
*/
bool Lookup(UserUnitInfo& unit);

/*!
* Gets the stat block for a unit type.
*
* Currently only unit_type_id 1 is understood and maps to lord_stats.
* @throws std::runtime_error when the unit type is not supported.
*/
static const UnitRecordStats& UnitTypeStats(const UnitRecord& unitRecord, UnitType unit_type_id)
{
switch (unit_type_id)
{
case 1:
return unitRecord.lord_stats;
default:
throw std::runtime_error("Unsupported unit type " + std::to_string(unit_type_id));
}
}

UnitArchiver(const UnitArchiver&) = delete;
UnitArchiver& operator=(const UnitArchiver&) = delete;

private:
using UnitRecordCache = std::unordered_map<UnitId, UnitRecord>;

UnitArchiver() = default;

UnitRecordCache cache_;
};
17 changes: 12 additions & 5 deletions gimuserver/db/MigrationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,24 @@ static void RegisterMigrations(MigrationMap& map)
);
});

#if 0
migrate("08032025_CreateUserUnitsTable", {
p->execSqlSync(
"CREATE TABLE IF NOT EXISTS user_units ("
"id INTEGER PRIMARY KEY AUTOINCREMENT," // Add: Auto-incrementing primary key as per PR comment
"user_id TEXT NOT NULL," // Keep: Links unit to a user
"unit_id TEXT NOT NULL" // Keep: Stores the unit identifier
"user_unit_id INTEGER PRIMARY KEY AUTOINCREMENT,"
"user_id TEXT NOT NULL,"
"unit_id INTEGER NOT NULL,"
"unit_type_id INTEGER NOT NULL,"
"base_hp INTEGER NOT NULL,"
"base_atk INTEGER NOT NULL,"
"base_def INTEGER NOT NULL,"
"base_rec INTEGER NOT NULL,"
"ext_hp INTEGER NOT NULL DEFAULT 0,"
"ext_atk INTEGER NOT NULL DEFAULT 0,"
"ext_def INTEGER NOT NULL DEFAULT 0,"
"ext_rec INTEGER NOT NULL DEFAULT 0"
");"
);
});
#endif
}

/*!
Expand Down
107 changes: 107 additions & 0 deletions gimuserver/db/UserUnitService.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "UserUnitService.hpp"

#include <string>

drogon::Task<bool> UserUnitService::AddUnit(Database db, std::string_view user_id, const UserUnitInfo& unit)
{
if (!db)
{
co_return false;
}

try
{
const auto result = co_await db->execSqlCoro(
"INSERT INTO user_units ("
"user_id, "
"unit_id, "
"unit_type_id, "
"base_hp, "
"base_atk, "
"base_def, "
"base_rec, "
"ext_hp, "
"ext_atk, "
"ext_def, "
"ext_rec"
") VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);",
user_id,
unit.unit_id,
unit.unit_type_id,
unit.base_hp,
unit.base_atk,
unit.base_def,
unit.base_rec,
unit.ext_hp,
unit.ext_atk,
unit.ext_def,
unit.ext_rec);

co_return result.affectedRows() == 1;
}
catch (const Exception&)
{
co_return false;
}
}

drogon::Task<bool> UserUnitService::getUnits(
Database db,
std::string_view user_id,
std::vector<UserUnitInfo>& units)
{
if (!db)
{
co_return false;
}

try
{
const auto result = co_await db->execSqlCoro(
"SELECT "
"user_unit_id, "
"user_id, "
"unit_id, "
"unit_type_id, "
"base_hp, "
"base_atk, "
"base_def, "
"base_rec, "
"ext_hp, "
"ext_atk, "
"ext_def, "
"ext_rec "
"FROM user_units "
"WHERE user_id = $1 "
"ORDER BY user_unit_id ASC;",
user_id);

units.clear();
units.reserve(result.size());

for (const auto& row : result)
{
// Keep this mapping in lockstep with the selected DB-backed fields.
UserUnitInfo unit = {};
unit.user_unit_id = row["user_unit_id"].as<uint32_t>();
unit.user_id = row["user_id"].as<std::string>();
unit.unit_id = row["unit_id"].as<uint32_t>();
unit.unit_type_id = row["unit_type_id"].as<uint32_t>();
unit.base_hp = row["base_hp"].as<uint32_t>();
unit.base_atk = row["base_atk"].as<uint32_t>();
unit.base_def = row["base_def"].as<uint32_t>();
unit.base_rec = row["base_rec"].as<uint32_t>();
unit.ext_hp = row["ext_hp"].as<uint32_t>();
unit.ext_atk = row["ext_atk"].as<uint32_t>();
unit.ext_def = row["ext_def"].as<uint32_t>();
unit.ext_rec = row["ext_rec"].as<uint32_t>();
units.emplace_back(std::move(unit));
}

co_return true;
}
catch (const Exception&)
{
co_return false;
}
}
Loading