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
47 changes: 47 additions & 0 deletions deploy/archive/unit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"unit_id": 10011,
"lord_stats": {
"hp": 1948,
"atk": 700,
"def": 574,
"rec": 448
}
},
{
"unit_id": 20011,
"lord_stats": {
"hp": 1830,
"atk": 706,
"def": 468,
"rec": 693
}
},
{
"unit_id": 30011,
"lord_stats": {
"hp": 1876,
"atk": 720,
"def": 700,
"rec": 248
}
},
{
"unit_id": 40011,
"lord_stats": {
"hp": 2070,
"atk": 812,
"def": 420,
"rec": 210
}
},
{
"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
101 changes: 101 additions & 0 deletions gimuserver/archive/UnitArchiver.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#include "UnitArchiver.hpp"

#include <gimuserver/utils/JsonFile.hpp>

#include <drogon/drogon.h>

#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();
if (archiveRoot.empty())
{
LOG_ERROR << "Unable to set up unit archiver cache: archive_root is empty";
return;
}

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));
}
}

std::optional<UnitRecord> UnitArchiver::lookup(UnitId unit_id) const
{
if (unit_id == 0)
{
LOG_ERROR << "Invalid unit archive lookup: unit_id is 0";
return std::nullopt;
}

const auto it = cache_.find(unit_id);
if (it == cache_.end())
{
LOG_ERROR << "Unable to find unit archive record " << unit_id;
return std::nullopt;
}

return it->second;
}

bool UnitArchiver::populatePacket(
const UnitRecord& unitRecord,
UnitType unit_type_id,
UserUnitInfo& unit)
{
// Grab the stats for the given unit type.
const auto stats = unitTypeStats(unitRecord, unit_type_id);
if (!stats)
{
return false;
}

unit.unit_id = unitRecord.unit_id;
unit.unit_type_id = 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;
}

std::optional<UnitRecordStats> UnitArchiver::unitTypeStats(
const UnitRecord& unitRecord,
UnitType unit_type_id)
{
switch (unit_type_id)
{
case 1:
return unitRecord.lord_stats;
default:
LOG_ERROR << "Unsupported unit type " << unit_type_id
<< " for unit archive record " << unitRecord.unit_id;
return std::nullopt;
}
}
80 changes: 80 additions & 0 deletions gimuserver/archive/UnitArchiver.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#pragma once

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

#include <json/value.h>

#include <cstdint>
#include <optional>
#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.
*
* @param unit_id Master unit ID from the unit archive.
* @return Matching immutable archive record, or std::nullopt when not found.
*/
std::optional<UnitRecord> lookup(UnitId unit_id) const;

/*!
* Converts archive data to a subset of UserUnitInfo.
*
* Ownership fields such as user_id and user_unit_id are not modified.
* @param unitRecord Archive record to read from.
* @param unit_type_id Unit type to select from the archive stats.
* @param unit Packet/database object to populate.
* @return True if the packet was populated, false if the unit type is unsupported.
*/
static bool populatePacket(
const UnitRecord& unitRecord,
UnitType unit_type_id,
UserUnitInfo& unit);

/*!
* Gets the stat block for a unit type.
*
* Currently only unit_type_id 1 is understood and maps to lord_stats.
* @return Matching stat block, or std::nullopt when the unit type is unsupported.
*/
static std::optional<UnitRecordStats> unitTypeStats(
const UnitRecord& unitRecord,
UnitType unit_type_id);

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

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

UnitArchiver() = default;

UnitRecordCache cache_;
};
73 changes: 40 additions & 33 deletions gimuserver/controller/AccountController.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#include "App.hpp"
#include "AccountController.hpp"

#include <gimuserver/db/UserInfoService.hpp>
#include <gimuserver/gme/common.hpp>
#include <gimuserver/utils/Random.hpp>

#include <stdexcept>

using namespace drogon;
using namespace drogon::orm;

Expand All @@ -18,46 +24,47 @@ Task<> AccountController::HandleGuest(HttpRequestPtr rq, std::function<void(cons

try
{
// Note: something real should be used here if you want multi-user
login.user_id = "00000001";
// For the login flow, this is the first server endpoint the client hits.
// The client sends device/login identifiers such as vid, altvid, ak, and
// identifiers, but not a local Gumi Live user ID. This local server currently
// behaves as a single-user system: it reuses the first Gumi Live user in the
// database, or creates one if none exists, and returns that ID as game_user_id.
//
// Note: this only creates an entry in the gumi_live_users table. The actual
// game user row in userinfo is created later, after the client sends the
// CreateUser request.
std::string currentGumiUser;
co_await UserInfoService::fetchCurrentGumiUser(theDb(), currentGumiUser);

try
// We found a Gumi Live user in the database, so we can just return that one.
if (!currentGumiUser.empty())
{
// Check if user exists in users table
const auto& result = co_await theDb()->execSqlCoro("SELECT id FROM gumi_live_users WHERE id = $1", login.user_id);
if (result.empty()) {
/*
How should it work in an actual system that uses an external API for login?

1. Game client tries to login or make a new account "GuestLogin"
2. Based from ak and other identifiers a userid and token is computed
3. Token is passed back to the game
4. The game will try to initialize with the gumi user token
5. Once the token is validated, the initialize request should make a new user

Here there is only the user creation step.
*/

// NOTE: This should be de-hardcoded if someone plans to make a multiuser or multiprofile system.

// User does not exist, add him to the table
co_await theDb()->execSqlCoro("INSERT INTO gumi_live_users(id) VALUES ($1);", login.user_id);
LOG_DEBUG << "AccountController: new user " << login.user_id;
}

login.status = StatusEnum::Success;
login.token = "test_token"; // Note: Should we use a proper token with drogon session?
login.status_number = 0;

login.user_id = currentGumiUser;
}
catch (drogon::orm::DrogonDbException ex)
else
{
LOG_ERROR << "AccountController: Query execution failed: " << ex.base().what();
// No Gumi Live user exists yet, so we create one with a random ID and return that.
login.user_id = RandomId();

// Add him as the only local Gumi Live user.
co_await gme::nonEmpty(UserInfoService::addGumiUser(
theDb(),
login.user_id));

LOG_DEBUG << "AccountController: new user " << login.user_id;
}

login.status = StatusEnum::Success;
login.token = "test_token"; // Note: Should we use a proper token with drogon session?
login.status_number = 0;
}
catch (const DrogonDbException& ex)
{
LOG_ERROR << "AccountController: login failed " << ex.base().what();
}
catch (std::invalid_argument ex)
catch (const std::exception& ex)
{
LOG_DEBUG << "AccountController: invalid parameters " << ex.what();
LOG_ERROR << "AccountController: login failed " << ex.what();
}

auto resp = HttpResponse::newHttpResponse();
Expand Down
Loading