server-1.12/plugins/cflogger/cflogger.c

749 lines
24 KiB
C

/*****************************************************************************/
/* Logger plugin version 1.0 alpha. */
/* Contact: */
/*****************************************************************************/
/* That code is placed under the GNU General Public Licence (GPL) */
/* (C)2007 by Weeger Nicolas (Feel free to deliver your complaints) */
/*****************************************************************************/
/* CrossFire, A Multiplayer game for X-windows */
/* */
/* Copyright (C) 2000 Mark Wedel */
/* Copyright (C) 1992 Frank Tore Johansen */
/* */
/* This program is free software; you can redistribute it and/or modify */
/* it under the terms of the GNU General Public License as published by */
/* the Free Software Foundation; either version 2 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU General Public License for more details. */
/* */
/* You should have received a copy of the GNU General Public License */
/* along with this program; if not, write to the Free Software */
/* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */
/* */
/*****************************************************************************/
/**
* @file cflogger.c
* This plugin will log events to an sqlite3 database named cflogger.db in the
* var directory.
*
* Log includes:
* @li players join/leave/creation/quit
* @li map load/unload/reset/enter/leave
* @li kills, whenever a player is concerned
* @li ingame/real time links
*
* @warning
* The plugin will not check the database size, which can grow a lot.
*
* @note
* Thanks to sqlite's locking, it's possible to access the database through the command
* line even while the server is running.
*/
#include <cflogger.h>
#ifndef __CEXTRACT__
#include <cflogger_proto.h>
#endif
/*#include <stdarg.h>*/
#include <sqlite3.h>
/** Current database format */
#define CFLOGGER_CURRENT_FORMAT 3
/** Pointer to the logging database. */
static sqlite3 *database;
/** To keep track of stored ingame/real time matching. */
static int last_stored_day = -1;
/**
* Simple callback to get an integer from a query.
*
* @param param
* user-supplied data.
* @param argc
* number of items.
* @param argv
* values.
* @param azColName
* column names.
*
* @return
* always returns 0 to continue the execution.
*/
static int check_tables_callback(void *param, int argc, char **argv, char **azColName) {
int *format = (int *)param;
*format = atoi(argv[0]);
return 0;
}
/**
* Helper function to run a SQL query.
*
* Will LOG() an error if the query fails.
*
* @param sql
* query to run.
*
* @return
* SQLITE_OK if no error, other value if error.
*
* @note
* There is most likely no need to check return value unless you need to
* rollback a transaction or similar.
*/
static int do_sql(const char *sql) {
int err;
char *msg;
if (!database)
return -1;
err = sqlite3_exec(database, sql, NULL, NULL, &msg);
if (err != SQLITE_OK) {
cf_log(llevError, " [%s] error: %d [%s] for sql = %s\n", PLUGIN_NAME, err, msg, sql);
sqlite3_free(msg);
}
return err;
}
/**
* Updates a table to a new schema, used for when ALTER TABLE doesn't work.
* (Such as when changing column constraints.)
*
* @param table
* Name of table.
* @param newschema
* This is the new table format. Will be inserted into the parantheses of
* "create table table_name()".
* @param select_columns
* This is inserted into "INSERT INTO table_name SELECT _ FROM ..." to allow
* changing order of columns, or skipping some. Normally it should be "*".
*
* @warning
* This function should only be used in check_tables() below.
*
* No error checking is done. Also it is expected that appending an _old
* on the table name won't collide with anything.
*
* Note that columns are expected to have same (or compatible) type, and be in
* the same order. Further both tables should have the same number of columns.
*
* @return
* SQLITE_OK if no error, non-zero if error. This SHOULD be rollback any
* transaction this function is called in.
*/
static int update_table_format(const char *table, const char *newschema,
const char *select_columns) {
char *sql;
int err;
sql = sqlite3_mprintf("ALTER TABLE %s RENAME TO %s_old;", table, table);
err = do_sql(sql);
sqlite3_free(sql);
if (err != SQLITE_OK)
return err;
sql = sqlite3_mprintf("CREATE TABLE %s(%s);", table, newschema);
err = do_sql(sql);
sqlite3_free(sql);
if (err != SQLITE_OK)
return err;
sql = sqlite3_mprintf("INSERT INTO %s SELECT %s FROM %s_old;",
table, select_columns, table);
err = do_sql(sql);
sqlite3_free(sql);
if (err != SQLITE_OK)
return err;
sql = sqlite3_mprintf("DROP TABLE %s_old;", table, table);
err = do_sql(sql);
sqlite3_free(sql);
/* Final return. */
return err;
}
/**
* Helper macros for rolling back and returning if query failed.
* Used in check_tables().
*
* Yes they are quite messy. The alternatives seemed worse.
*/
#define DO_OR_ROLLBACK(sqlstring) \
if (do_sql(sqlstring) != SQLITE_OK) { \
do_sql("rollback transaction;"); \
cf_log(llevError, " [%s] Logger database format update failed! Couldn't upgrade from format %d to fromat %d!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);\
sqlite3_close(database); \
database = NULL; \
return; \
}
#define UPDATE_OR_ROLLBACK(tbl, newschema, select_columns) \
if (update_table_format((tbl), (newschema), (select_columns)) != SQLITE_OK) { \
do_sql("rollback transaction;"); \
cf_log(llevError, " [%s] Logger database format update failed! Couldn't upgrade from format %d to fromat %d!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);\
sqlite3_close(database); \
database = NULL; \
return; \
}
/**
* Checks the database format, and applies changes if old version.
*/
static void check_tables(void) {
int format;
int err;
format = 0;
err = sqlite3_exec(database, "select param_value from parameters where param_name = 'version';", check_tables_callback, &format, NULL);
/* Safety check. */
if (format > CFLOGGER_CURRENT_FORMAT) {
cf_log(llevError, " [%s] Logger database format (%d) is newer than supported (%d) by this binary!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);
/* This will disable using the db since do_sql() checks if database is
* NULL.
*/
sqlite3_close(database);
database = NULL;
}
/* Check if we need to upgrade/create database. */
if (format < 1) {
cf_log(llevDebug, " [%s] Creating logger database schema (format 1).\n", PLUGIN_NAME);
if (do_sql("BEGIN EXCLUSIVE TRANSACTION;") != SQLITE_OK) {
cf_log(llevError, " [%s] Logger database format update failed! Couldn't acquire exclusive lock to database when upgrading from format %d to fromat %d!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);
sqlite3_close(database);
database = NULL;
return;
}
DO_OR_ROLLBACK("create table living(liv_id integer primary key autoincrement, liv_name text, liv_is_player integer, liv_level integer);");
DO_OR_ROLLBACK("create table region(reg_id integer primary key autoincrement, reg_name text);");
DO_OR_ROLLBACK("create table map(map_id integer primary key autoincrement, map_path text, map_reg_id integer);");
DO_OR_ROLLBACK("create table time(time_real integer, time_ingame text);");
DO_OR_ROLLBACK("create table living_event(le_liv_id integer, le_time integer, le_code integer, le_map_id integer);");
DO_OR_ROLLBACK("create table map_event(me_map_id integer, me_time integer, me_code integer, me_living_id integer);");
DO_OR_ROLLBACK("create table kill_event(ke_time integer, ke_victim_id integer, ke_victim_level integer, ke_map_id integer , ke_killer_id integer, ke_killer_level integer);");
DO_OR_ROLLBACK("create table parameters(param_name text, param_value text);");
DO_OR_ROLLBACK("insert into parameters values( 'version', '1' );");
do_sql("COMMIT TRANSACTION;");
}
/* Must be able to handle update from format 1. If we are creating a new
* database, format 1 is still created first, then updated.
*
* This way is simpler than having to create two ways to make a format 2 db.
*/
if (format < 2) {
cf_log(llevDebug, " [%s] Upgrading logger database schema (to format 2).\n", PLUGIN_NAME);
if (do_sql("BEGIN EXCLUSIVE TRANSACTION;") != SQLITE_OK) {
cf_log(llevError, " [%s] Logger database format update failed! Couldn't acquire exclusive lock to database when upgrading from format %d to fromat %d!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);
sqlite3_close(database);
database = NULL;
return;
}
/* Update schema for various tables. Why so complex? Because ALTER TABLE
* can't add the "primary key" bit or other constraints...
*/
UPDATE_OR_ROLLBACK("living", "liv_id INTEGER PRIMARY KEY AUTOINCREMENT, liv_name TEXT NOT NULL, liv_is_player INTEGER NOT NULL, liv_level INTEGER NOT NULL", "*");
UPDATE_OR_ROLLBACK("region", "reg_id INTEGER PRIMARY KEY AUTOINCREMENT, reg_name TEXT UNIQUE NOT NULL", "*");
UPDATE_OR_ROLLBACK("map", "map_id INTEGER PRIMARY KEY AUTOINCREMENT, map_path TEXT NOT NULL, map_reg_id INTEGER NOT NULL, CONSTRAINT map_path_reg_id UNIQUE(map_path, map_reg_id)", "*");
#if 0
/* Turned out this was incorrect. And version 1 -> 3 directly works for this. */
UPDATE_OR_ROLLBACK("time", "time_real INTEGER PRIMARY KEY, time_ingame TEXT UNIQUE NOT NULL");
#endif
UPDATE_OR_ROLLBACK("living_event", "le_liv_id INTEGER NOT NULL, le_time INTEGER NOT NULL, le_code INTEGER NOT NULL, le_map_id INTEGER NOT NULL", "*");
UPDATE_OR_ROLLBACK("map_event", "me_map_id INTEGER NOT NULL, me_time INTEGER NOT NULL, me_code INTEGER NOT NULL, me_living_id INTEGER NOT NULL", "*");
UPDATE_OR_ROLLBACK("kill_event", "ke_time INTEGER NOT NULL, ke_victim_id INTEGER NOT NULL, ke_victim_level INTEGER NOT NULL, ke_map_id INTEGER NOT NULL, ke_killer_id INTEGER NOT NULL, ke_killer_level INTEGER NOT NULL", "*");
/* Handle changed parameters table format: */
/* Due to backward compatiblity "primary key" in SQLite doesn't imply
* "not null" (http://www.sqlite.org/lang_createtable.html), unless it
* is "integer primary key".
*
* We don't need to save anything stored in this in format 1, it was
* only used for storing what format was used.
*/
DO_OR_ROLLBACK("DROP TABLE parameters;");
DO_OR_ROLLBACK("CREATE TABLE parameters(param_name TEXT NOT NULL PRIMARY KEY, param_value TEXT);");
DO_OR_ROLLBACK("INSERT INTO parameters (param_name, param_value) VALUES( 'version', '2' );");
/* Create various indexes. */
DO_OR_ROLLBACK("CREATE INDEX living_name_player_level ON living(liv_name,liv_is_player,liv_level);");
/* Newspaper module could make use of some indexes too: */
DO_OR_ROLLBACK("CREATE INDEX kill_event_time ON kill_event(ke_time);");
DO_OR_ROLLBACK("CREATE INDEX map_reg_id ON map(map_reg_id);");
/* Finally commit the transaction. */
do_sql("COMMIT TRANSACTION;");
}
if (format < 3) {
cf_log(llevDebug, " [%s] Upgrading logger database schema (to format 3).\n", PLUGIN_NAME);
if (do_sql("BEGIN EXCLUSIVE TRANSACTION;") != SQLITE_OK) {
cf_log(llevError, " [%s] Logger database format update failed! Couldn't acquire exclusive lock to database when upgrading from format %d to fromat %d!. Won't log.\n", PLUGIN_NAME, format, CFLOGGER_CURRENT_FORMAT);
sqlite3_close(database);
database = NULL;
return;
}
UPDATE_OR_ROLLBACK("time", "time_ingame TEXT NOT NULL PRIMARY KEY, time_real INTEGER NOT NULL", "time_ingame, time_real");
DO_OR_ROLLBACK("UPDATE parameters SET param_value = '3' WHERE param_name = 'version';");
do_sql("COMMIT TRANSACTION;");
/* After all these changes better vacuum... The tables could have been
* huge, and since we recreated several of them above there could be a
* lot of wasted space.
*/
do_sql("VACUUM;");
}
}
/**
* Returns a unique identifier for specified object.
*
* Will insert an item in the table if required.
*
* If the object is a player, only name is taken into account to generate an id.
*
* Else, the object's level is taken into account, to distinguish monsters with
* the same name and different levels (special monsters, and such).
*
* @param living
* object to get identifier for.
* @return
* unique identifier in the 'living' table.
*/
static int get_living_id(object *living) {
char **line;
char *sql;
int nrow, ncolumn, id;
if (living->type == PLAYER)
sql = sqlite3_mprintf("select liv_id from living where liv_name='%q' and liv_is_player = 1", living->name);
else
sql = sqlite3_mprintf("select liv_id from living where liv_name='%q' and liv_is_player = 0 and liv_level = %d", living->name, living->level);
sqlite3_get_table(database, sql, &line, &nrow, &ncolumn, NULL);
if (nrow > 0)
id = atoi(line[ncolumn]);
else {
sqlite3_free(sql);
sql = sqlite3_mprintf("insert into living(liv_name, liv_is_player, liv_level) values('%q', %d, %d)", living->name, living->type == PLAYER ? 1 : 0, living->level);
do_sql(sql);
id = sqlite3_last_insert_rowid(database);
}
sqlite3_free(sql);
sqlite3_free_table(line);
return id;
}
/**
* Gets the unique identifier for a region.
*
* Will generate one if required.
*
* @param reg
* region for which an id is wanted
* @return
* unique region identifier, or 0 if reg is NULL.
*/
static int get_region_id(region *reg) {
char **line;
char *sql;
int nrow, ncolumn, id;
if (!reg)
return 0;
sql = sqlite3_mprintf("select reg_id from region where reg_name='%q'", reg->name);
sqlite3_get_table(database, sql, &line, &nrow, &ncolumn, NULL);
if (nrow > 0)
id = atoi(line[ncolumn]);
else {
sqlite3_free(sql);
sql = sqlite3_mprintf("insert into region(reg_name) values( '%q' )", reg->name);
do_sql(sql);
id = sqlite3_last_insert_rowid(database);
}
sqlite3_free(sql);
sqlite3_free_table(line);
return id;
}
/**
* Gets the unique identifier for a map.
*
* Will generate one if required.
*
* Maps starting with '/random/' will all share the same identifier for the same region.
*
* @param map
* map for which an id is wanted. Must not be NULL.
* @return
* unique map identifier.
*/
static int get_map_id(mapstruct *map) {
char **line;
char *sql;
int nrow, ncolumn, id, reg_id;
const char *path = map->path;
if (strncmp(path, "/random/", 7) == 0)
path = "/random/";
reg_id = get_region_id(map->region);
sql = sqlite3_mprintf("select map_id from map where map_path='%q' and map_reg_id = %d", path, reg_id);
sqlite3_get_table(database, sql, &line, &nrow, &ncolumn, NULL);
if (nrow > 0)
id = atoi(line[ncolumn]);
else {
sqlite3_free(sql);
sql = sqlite3_mprintf("insert into map(map_path, map_reg_id) values( '%q', %d)", path, reg_id);
do_sql(sql);
id = sqlite3_last_insert_rowid(database);
}
sqlite3_free(sql);
sqlite3_free_table(line);
return id;
}
/**
* Stores a line to match current ingame and real time.
*
* @return
* 1 if a line was inserted, 0 if the current ingame time was already logged.
*/
static int store_time(void) {
char **line;
char *sql;
int nrow, ncolumn;
char date[50];
time_t now;
timeofday_t tod;
cf_get_time(&tod);
now = time(NULL);
if (tod.day == last_stored_day)
return 0;
last_stored_day = tod.day;
snprintf(date, 50, "%10d-%2d-%2d %2d:%2d", tod.year, tod.month, tod.day, tod.hour, tod.minute);
sql = sqlite3_mprintf("select * from time where time_ingame='%q'", date);
sqlite3_get_table(database, sql, &line, &nrow, &ncolumn, NULL);
sqlite3_free(sql);
sqlite3_free_table(line);
if (nrow > 0)
return 0;
sql = sqlite3_mprintf("insert into time (time_ingame, time_real) values( '%s', %d )", date, now);
do_sql(sql);
sqlite3_free(sql);
return 1;
}
/**
* Logs an event for a living object.
*
* @param pl
* object for which to log an event.
* @param event_code
* arbitrary event code.
*/
static void add_player_event(object *pl, int event_code) {
int id = get_living_id(pl);
int map_id = 0;
char *sql;
if (pl->map)
map_id = get_map_id(pl->map);
sql = sqlite3_mprintf("insert into living_event values( %d, %d, %d, %d)", id, time(NULL), event_code, map_id);
do_sql(sql);
sqlite3_free(sql);
}
/**
* Logs an event for a map.
*
* @param map
* map for which to log an event.
* @param event_code
* arbitrary event code.
* @param pl
* object causing the event. Can be NULL.
*/
static void add_map_event(mapstruct *map, int event_code, object *pl) {
int mapid;
int playerid = 0;
char *sql;
if (pl && pl->type == PLAYER)
playerid = get_living_id(pl);
mapid = get_map_id(map);
sql = sqlite3_mprintf("insert into map_event values( %d, %d, %d, %d)", mapid, time(NULL), event_code, playerid);
do_sql(sql);
sqlite3_free(sql);
}
/**
* Logs a death.
*
* If either of the parameters is NULL, or if neither is a PLAYER, nothing is logged.
*
* @param victim
* who died.
* @param killer
* who killed.
*/
static void add_death(object *victim, object *killer) {
int vid, kid, map_id;
char *sql;
if (!victim || !killer)
return;
if (victim->type != PLAYER && killer->type != PLAYER) {
/* Killer might be a bullet, which might be owned by the player. */
object *owner = cf_object_get_object_property(killer, CFAPI_OBJECT_PROP_OWNER);
if (owner != NULL && owner->type == PLAYER)
killer = owner;
else
return;
}
vid = get_living_id(victim);
kid = get_living_id(killer);
map_id = get_map_id(victim->map);
sql = sqlite3_mprintf("insert into kill_event values( %d, %d, %d, %d, %d, %d)", time(NULL), vid, victim->level, map_id, kid, killer->level);
do_sql(sql);
sqlite3_free(sql);
}
/**
* Main plugin entry point.
*
* @param iversion
* server version.
* @param gethooksptr
* function to get hooks from.
* @return
* always 0.
*/
CF_PLUGIN int initPlugin(const char *iversion, f_plug_api gethooksptr) {
cf_init_plugin(gethooksptr);
cf_log(llevInfo, "%s init\n", PLUGIN_VERSION);
return 0;
}
/**
* Gets a plugin property.
*
* @param type
* ignored.
* @return
* @li the name, if asked for 'Identification'.
* @li the version, if asked for 'FullName'.
* @li NULL else.
*/
CF_PLUGIN void *getPluginProperty(int *type, ...) {
va_list args;
const char *propname;
char *buf;
int size;
va_start(args, type);
propname = va_arg(args, const char *);
if (!strcmp(propname, "Identification")) {
buf = va_arg(args, char *);
size = va_arg(args, int);
va_end(args);
snprintf(buf, size, PLUGIN_NAME);
return NULL;
} else if (!strcmp(propname, "FullName")) {
buf = va_arg(args, char *);
size = va_arg(args, int);
va_end(args);
snprintf(buf, size, PLUGIN_VERSION);
return NULL;
}
va_end(args);
return NULL;
}
/**
* Runs a plugin command. Doesn't do anything.
*
* @param op
* ignored.
* @param params
* ignored.
* @return
* -1.
*/
CF_PLUGIN int cflogger_runPluginCommand(object *op, char *params) {
return -1;
}
/**
* Handles an object-related event. Doesn't do anything.
*
* @param type
* ignored.
* @return
* pointer to an int containing 0.
*/
void *eventListener(int *type, ...) {
static int rv = 0;
return &rv;
}
/**
* Handles a global event.
*
* @param type
* ignored.
* @return
* pointer to an int containing 0.
*/
CF_PLUGIN void *cflogger_globalEventListener(int *type, ...) {
va_list args;
static int rv = 0;
player *pl;
object *op;
int event_code;
mapstruct *map;
va_start(args, type);
event_code = va_arg(args, int);
switch (event_code) {
case EVENT_BORN:
case EVENT_PLAYER_DEATH:
case EVENT_REMOVE:
case EVENT_MUZZLE:
case EVENT_KICK:
op = va_arg(args, object *);
add_player_event(op, event_code);
break;
case EVENT_LOGIN:
case EVENT_LOGOUT:
pl = va_arg(args, player *);
add_player_event(pl->ob, event_code);
break;
case EVENT_MAPENTER:
case EVENT_MAPLEAVE:
op = va_arg(args, object *);
map = va_arg(args, mapstruct *);
add_map_event(map, event_code, op);
break;
case EVENT_MAPLOAD:
case EVENT_MAPUNLOAD:
case EVENT_MAPRESET:
map = va_arg(args, mapstruct *);
add_map_event(map, event_code, NULL);
break;
case EVENT_GKILL: {
object *killer;
op = va_arg(args, object *);
killer = va_arg(args, object *);
add_death(op, killer);
}
break;
case EVENT_CLOCK:
store_time();
break;
}
va_end(args);
return &rv;
}
/**
* Plugin was initialized, now to finish.
*
* Registers events, initializes the database.
*
* @return
* 0.
*/
CF_PLUGIN int postInitPlugin(void) {
char path[500];
const char *dir;
cf_log(llevInfo, "%s post init\n", PLUGIN_VERSION);
dir = cf_get_directory(4);
snprintf(path, sizeof(path), "%s/cflogger.db", dir);
cf_log(llevDebug, " [%s] database file: %s\n", PLUGIN_NAME, path);
if (sqlite3_open(path, &database) != SQLITE_OK) {
cf_log(llevError, " [%s] database error!\n", PLUGIN_NAME);
sqlite3_close(database);
database = NULL;
return 0;
}
check_tables();
store_time();
cf_system_register_global_event(EVENT_BORN, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_REMOVE, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_GKILL, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_LOGIN, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_LOGOUT, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_PLAYER_DEATH, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MAPENTER, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MAPLEAVE, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MAPRESET, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MAPLOAD, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MAPUNLOAD, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_MUZZLE, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_KICK, PLUGIN_NAME, cflogger_globalEventListener);
cf_system_register_global_event(EVENT_CLOCK, PLUGIN_NAME, cflogger_globalEventListener);
return 0;
}
/**
* Close the plugin.
*
* Closes the sqlite database.
*
* @return
* 0.
*/
CF_PLUGIN int closePlugin(void) {
cf_log(llevInfo, "%s closing.\n", PLUGIN_VERSION);
if (database) {
sqlite3_close(database);
database = NULL;
}
return 0;
}