Skip to content

Commit

Permalink
Add support for storing game information in wesnoth's mysql database.
Browse files Browse the repository at this point in the history
This requires three more tables to be added to any database with the forum user handler enabled, the structures of which are defined in the wesnothd man page:
* `db_game_info_table` - stores information about each game.
* `db_game_player_info_table` - stores information about each player in the game.
* `db_game_modification_info_table` - stores information about any modifications that are enabled for the game.

Backport of 85151f7
  • Loading branch information
Pentarctagon authored and soliton- committed Aug 24, 2019
1 parent b6c311c commit 358e9a4
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 14 deletions.
14 changes: 11 additions & 3 deletions doc/man/wesnothd.6
Expand Up @@ -217,7 +217,7 @@ Configures the user handler. Available keys vary depending on which user handler
.B user_handler
key. If no
.B [user_handler]
section is present in the configuration the server will run without any nick registration service.
section is present in the configuration the server will run without any nick registration service. All additional tables that are needed for the forum_user_handler to function can be found in table_definitions.sql in the Wesnoth source repository.
.RS
.TP
.B db_host
Expand All @@ -236,8 +236,16 @@ section is present in the configuration the server will run without any nick reg
(for user_handler=forum) The name of the table in which your phpbb forums saves its user data. Most likely this will be <table-prefix>_users (e.g. phpbb3_users).
.TP
.B db_extra_table
(for user_handler=forum) The name of the table in which wesnothd will save its own data about users. You will have to create this table manually, e.g.:
.B CREATE TABLE <table-name>(username VARCHAR(255) PRIMARY KEY, user_lastvisit INT UNSIGNED NOT NULL DEFAULT 0, user_is_moderator TINYINT(4) NOT NULL DEFAULT 0);
(for user_handler=forum) The name of the table in which wesnothd will save its own data about users. You will have to create this table manually.
.TP
.B db_game_info_table
(for user_handler=forum) The name of the table in which wesnothd will save its own data about games.
.TP
.B db_game_player_info_table
(for user_handler=forum) The name of the table in which wesnothd will save its own data about the players in a game.
.TP
.B db_game_modification_info_table
(for user_handler=forum) The name of the table in which wesnothd will save its own data about the modifications used in a game.
.TP
.B user_expiration
(for user_handler=sample) The time after which a registered nick expires (in days).
Expand Down
58 changes: 58 additions & 0 deletions src/server/forum_user_handler.cpp
Expand Up @@ -42,8 +42,12 @@ fuh::fuh(const config& c)
, db_users_table_(c["db_users_table"].str())
, db_banlist_table_(c["db_banlist_table"].str())
, db_extra_table_(c["db_extra_table"].str())
, db_game_info_table_(c["db_game_info_table"].str())
, db_game_player_info_table_(c["db_game_player_info_table"].str())
, db_game_modification_info_table_(c["db_game_modification_info_table"].str())
, conn(mysql_init(nullptr))
{
mysql_options(conn, MYSQL_SET_CHARSET_NAME, "utf8mb4");
if(!conn || !mysql_real_connect(conn, db_host_.c_str(), db_user_.c_str(), db_password_.c_str(), db_name_.c_str(), 0, nullptr, 0)) {
ERR_UH << "Could not connect to database: " << mysql_errno(conn) << ": " << mysql_error(conn) << std::endl;
}
Expand Down Expand Up @@ -413,4 +417,58 @@ bool fuh::extra_row_exists(const std::string& name) {
}
}

std::string fuh::get_uuid(){
try {
return prepared_statement<std::string>("SELECT UUID()");
} catch (const sql_error& e) {
ERR_UH << "Could not retrieve a UUID:" << e.message << std::endl;
return "";
}
}

void fuh::db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name){
try {
prepared_statement<void>("insert into `" + db_game_info_table_ + "`(INSTANCE_UUID, GAME_ID, INSTANCE_VERSION, GAME_NAME) values(?, ?, ?, ?)",
uuid, game_id, version, name);
} catch (const sql_error& e) {
ERR_UH << "Could not insert into table `" + db_game_info_table_ + "`:" << e.message << std::endl;
}
}

void fuh::db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name){
try {
prepared_statement<void>("update `" + db_game_info_table_ + "` set START_TIME = CURRENT_TIMESTAMP, MAP_NAME = ?, ERA_NAME = ? where INSTANCE_UUID = ? and GAME_ID = ?",
map_name, era_name, uuid, game_id);
} catch (const sql_error& e) {
ERR_UH << "Could not update the game's starting information on table `" + db_game_info_table_ + "`:" << e.message << std::endl;
}
}

void fuh::db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location){
try {
prepared_statement<void>("update `" + db_game_info_table_ + "` set END_TIME = CURRENT_TIMESTAMP, REPLAY_NAME = ? where INSTANCE_UUID = ? and GAME_ID = ?",
replay_location, uuid, game_id);
} catch (const sql_error& e) {
ERR_UH << "Could not update the game's ending information on table `" + db_game_info_table_ + "`:" << e.message << std::endl;
}
}

void fuh::db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, const std::string& is_host, const std::string& faction){
try {
prepared_statement<void>("insert into `" + db_game_player_info_table_ + "`(INSTANCE_UUID, GAME_ID, USER_ID, SIDE_NUMBER, IS_HOST, FACTION) values(?, ?, IFNULL((select user_id from `"+db_users_table_+"` where username = ?), -1), ?, ?, ?)",
uuid, game_id, username, side_number, is_host, faction);
} catch (const sql_error& e) {
ERR_UH << "Could not insert the game's player information on table `" + db_game_player_info_table_ + "`:" << e.message << std::endl;
}
}

void fuh::db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name){
try {
prepared_statement<void>("insert into `" + db_game_modification_info_table_ + "`(INSTANCE_UUID, GAME_ID, MODIFICATION_NAME) values(?, ?, ?)",
uuid, game_id, modification_name);
} catch (const sql_error& e) {
ERR_UH << "Could not insert the game's modification information on table `" + db_game_modification_info_table_ + "`:" << e.message << std::endl;
}
}

#endif //HAVE_MYSQLPP
9 changes: 8 additions & 1 deletion src/server/forum_user_handler.hpp
Expand Up @@ -80,6 +80,13 @@ class fuh : public user_handler {

bool use_phpbb_encryption() const { return true; }

std::string get_uuid();
void db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name);
void db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name);
void db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location);
void db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, const std::string& is_host, const std::string& faction);
void db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name);

private:
std::string get_hash(const std::string& user);
std::string get_mail(const std::string& user);
Expand All @@ -97,7 +104,7 @@ class fuh : public user_handler {
std::time_t retrieve_ban_duration_internal(const std::string& col, const std::string& detail);
std::time_t retrieve_ban_duration_internal(const std::string& col, unsigned int detail);

std::string db_name_, db_host_, db_user_, db_password_, db_users_table_, db_banlist_table_, db_extra_table_;
std::string db_name_, db_host_, db_user_, db_password_, db_users_table_, db_banlist_table_, db_extra_table_, db_game_info_table_, db_game_player_info_table_, db_game_modification_info_table_;

MYSQL *conn;

Expand Down
21 changes: 11 additions & 10 deletions src/server/game.cpp
Expand Up @@ -1831,6 +1831,16 @@ static bool is_invalid_filename_char(char c)
);
}

std::string game::get_replay_filename()
{
std::stringstream name;
name << (*starting_pos(level_.root()))["name"] << " Turn " << current_turn() << " (" << id_ << ").bz2";
std::string filename(name.str());
std::replace(filename.begin(), filename.end(), ' ', '_');
filename.erase(std::remove_if(filename.begin(), filename.end(), is_invalid_filename_char), filename.end());
return filename;
}

void game::save_replay()
{
if(!save_replays_ || !started_ || history_.empty()) {
Expand All @@ -1848,9 +1858,6 @@ void game::save_replay()

history_.clear();

std::stringstream name;
name << (*starting_pos(level_.root()))["name"] << " Turn " << current_turn();

std::stringstream replay_data;
try {
// level_.set_attr_dup("label", name.str().c_str());
Expand All @@ -1873,16 +1880,10 @@ void game::save_replay()
<< (has_old_replay ? "" : "\t[command]\n\t\t[start]\n\t\t[/start]\n\t[/command]\n")
<< replay_commands << "[/replay]\n";

name << " (" << id_ << ").bz2";

std::string replay_data_str = replay_data.str();
simple_wml::document replay(replay_data_str.c_str(), simple_wml::INIT_STATIC);

std::string filename(name.str());

std::replace(filename.begin(), filename.end(), ' ', '_');
filename.erase(std::remove_if(filename.begin(), filename.end(), is_invalid_filename_char), filename.end());

std::string filename = get_replay_filename();
DBG_GAME << "saving replay: " << filename << std::endl;

filesystem::scoped_ostream os(filesystem::ostream_file(replay_save_path_ + filename));
Expand Down
2 changes: 2 additions & 0 deletions src/server/game.hpp
Expand Up @@ -139,6 +139,8 @@ class game
current_turn_ = turn;
}

std::string get_replay_filename();

void mute_all_observers();

/**
Expand Down
20 changes: 20 additions & 0 deletions src/server/sample_user_handler.cpp
Expand Up @@ -233,3 +233,23 @@ std::string suh::user_info(const std::string& name) {
}
return info.str();
}

std::string suh::get_uuid(){
return "";
}

void suh::db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name){
std::cout << uuid << " - " << game_id << " - " << version << " - " << name << std::endl;
}
void suh::db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name){
std::cout << uuid << " - " << game_id << " - " << map_name << " - " << era_name << std::endl;
}
void suh::db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location){
std::cout << uuid << " - " << game_id << " - " << replay_location << std::endl;
}
void suh::db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, const std::string& is_host, const std::string& faction){
std::cout << uuid << " - " << game_id << " - " << username << " - " << side_number << " - " << is_host << " - " << faction << std::endl;
}
void suh::db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name){
std::cout << uuid << " - " << game_id << " - " << modification_name << std::endl;
}
7 changes: 7 additions & 0 deletions src/server/sample_user_handler.hpp
Expand Up @@ -67,6 +67,13 @@ class suh : public user_handler {
std::string extract_salt(const std::string&) { return ""; }
bool use_phpbb_encryption() const { return false; }

std::string get_uuid();
void db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name);
void db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name);
void db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location);
void db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, const std::string& is_host, const std::string& faction);
void db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name);

private:
std::string get_mail(const std::string& user);
std::string get_password(const std::string& user);
Expand Down
28 changes: 28 additions & 0 deletions src/server/server.cpp
Expand Up @@ -230,6 +230,7 @@ server::server(int port,
#ifndef _WIN32
, input_path_()
#endif
, uuid_("")
, config_file_(config_file)
, cfg_(read_config())
, accepted_versions_()
Expand Down Expand Up @@ -536,6 +537,7 @@ void server::load_config()
// from the config file
if(user_handler_) {
user_handler_->init_mailer(cfg_.child("mail"));
uuid_ = user_handler_->get_uuid();
}
}
}
Expand Down Expand Up @@ -1405,12 +1407,20 @@ void server::create_game(player_record& host_record, simple_wml::node& create_ga
}

create_game.copy_into(g.level().root());

if(user_handler_) {
user_handler_->db_insert_game_info(uuid_, g.id(), game_config::wesnoth_version.str(), g.name());
}
}

void server::cleanup_game(game* game_ptr)
{
metrics_.game_terminated(game_ptr->termination_reason());

if(user_handler_){
user_handler_->db_update_game_end(uuid_, game_ptr->id(), game_ptr->get_replay_filename());
}

simple_wml::node* const gamelist = games_and_users_list_.child("gamelist");
assert(gamelist != nullptr);

Expand Down Expand Up @@ -1722,6 +1732,24 @@ void server::handle_player_in_game(socket_ptr socket, std::shared_ptr<simple_wml
g.send_data(data, socket);
g.start_game(socket);

if(user_handler_) {
const simple_wml::node& multiplayer = *g.level().root().child("multiplayer");
user_handler_->db_update_game_start(uuid_, g.id(), multiplayer["mp_scenario"].to_string(), multiplayer["mp_era"].to_string());

const simple_wml::node::child_list& sides = g.get_sides_list();
for(unsigned side_index = 0; side_index < sides.size(); ++side_index) {
const simple_wml::node& side = *sides[side_index];
user_handler_->db_insert_game_player_info(uuid_, g.id(), side["player_id"].to_string(), side["side"].to_int(), side["is_host"].to_string(), side["faction"].to_string());
}

const std::string mods = multiplayer["active_mods"].to_string();
if(mods != "") {
for(const std::string mod : utils::split(mods, ',')){
user_handler_->db_insert_modification_info(uuid_, g.id(), mod);
}
}
}

// update the game having changed in the lobby
update_game_in_lobby(g);
return;
Expand Down
2 changes: 2 additions & 0 deletions src/server/server.hpp
Expand Up @@ -124,6 +124,8 @@ class server : public server_base
std::string input_path_;
#endif

std::string uuid_;

const std::string config_file_;
config cfg_;

Expand Down
7 changes: 7 additions & 0 deletions src/server/user_handler.hpp
Expand Up @@ -171,6 +171,13 @@ class user_handler {
*/
virtual bool use_phpbb_encryption() const =0;

virtual std::string get_uuid() =0;
virtual void db_insert_game_info(const std::string& uuid, int game_id, const std::string& version, const std::string& name) =0;
virtual void db_update_game_start(const std::string& uuid, int game_id, const std::string& map_name, const std::string& era_name) =0;
virtual void db_update_game_end(const std::string& uuid, int game_id, const std::string& replay_location) =0;
virtual void db_insert_game_player_info(const std::string& uuid, int game_id, const std::string& username, int side_number, const std::string& is_host, const std::string& faction) =0;
virtual void db_insert_modification_info(const std::string& uuid, int game_id, const std::string& modification_name) =0;

protected:

/**
Expand Down
88 changes: 88 additions & 0 deletions utils/mp-server/table_definitions.sql
@@ -0,0 +1,88 @@
-- a minimal users table, if not using a phpbb3 installation
-- CREATE TABLE users
-- (
-- user_id int(10) unsigned NOT NULL AUTO_INCREMENT,
-- user_type tinyint(2) NOT NULL DEFAULT '0',
-- username varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
-- user_password varchar(255) COLLATE utf8_bin NOT NULL DEFAULT '',
-- user_email varchar(100) COLLATE utf8_bin NOT NULL DEFAULT '',
-- PRIMARY KEY (user_id),
-- KEY user_type (user_type)
-- ) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

-- table which the forum inserts bans into, which wesnothd checks during login
-- CREATE TABLE ban
-- (
-- ban_userid varchar(100) NOT NULL,
-- ban_end int(10) unsigned NOT NULL DEFAULT '0',
-- ban_ip varchar(100) DEFAULT NULL,
-- ban_email varchar(100) DEFAULT NULL,
-- ban_exclude int(10) unsigned NOT NULL DEFAULT '0',
-- PRIMARY KEY (ban_userid)
-- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

------

-- extra information as necessary per user
-- user_lastvisit is used by the phpbb extension displaying the last time the user logged in to the MP server
-- user_is_moderator determines people who have the abilities granted to MP Moderators
CREATE TABLE extra
(
username varchar(100) NOT NULL,
user_lastvisit int(10) unsigned NOT NULL DEFAULT '0',
user_is_moderator tinyint(4) NOT NULL DEFAULT '0',
PRIMARY KEY (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- information about a single game
-- INSTANCE_UUID: retrieved from the UUID() function on wesnothd start up
-- GAME_ID: a sequential id wesnoth generates, resets on restart
-- INSTANCE_VERSION: the version of the server
-- GAME_NAME: the game's displayed title in the lobby
-- CREATE_TIME: when the game is made available in the lobby
-- START_TIME: when the players enter the game and begin playing
-- END_TIME: when the game ends, for any particular reason
-- MAP_NAME: the mp_scenario attribute value
-- ERA_NAME: the mp_era attribute value
-- REPLAY_NAME: the file name of the replay create when the game is ended
create table game_info
(
INSTANCE_UUID CHAR(36) NOT NULL,
GAME_ID INT UNSIGNED NOT NULL,
INSTANCE_VERSION VARCHAR(255) NOT NULL,
GAME_NAME VARCHAR(255) NOT NULL,
CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
START_TIME TIMESTAMP NULL DEFAULT NULL,
END_TIME TIMESTAMP NULL DEFAULT NULL,
MAP_NAME VARCHAR(255),
ERA_NAME VARCHAR(255),
REPLAY_NAME VARCHAR(255),
primary key (INSTANCE_UUID, GAME_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- information about the players in a particular game present in game_info
-- this is accurate at the start of the game, but is not currently updated if a side changes owners, someone disconnects, etc
-- USER_ID: the ID of the player, taken from the USERS table
-- SIDE_NUMBER: the side controlled by USER_ID
-- IS_HOST: if USER_ID is the game's host
-- FACTION: the faction being played by this side
-- STATUS: the status of the side, currently only updated at game end
create table game_player_info
(
INSTANCE_UUID CHAR(36) NOT NULL,
GAME_ID INT UNSIGNED NOT NULL,
USER_ID INT NOT NULL,
SIDE_NUMBER SMALLINT UNSIGNED NOT NULL,
IS_HOST VARCHAR(255) NOT NULL,
FACTION VARCHAR(255) NOT NULL,
primary key (INSTANCE_UUID, GAME_ID, SIDE_NUMBER)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- information about any modifications that the game present in game_info has enabled
create table game_modification_info
(
INSTANCE_UUID CHAR(36) NOT NULL,
GAME_ID INT UNSIGNED NOT NULL,
MODIFICATION_NAME VARCHAR(255) NOT NULL,
primary key (INSTANCE_UUID, GAME_ID, MODIFICATION_NAME)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

0 comments on commit 358e9a4

Please sign in to comment.