//Based on plugin: https://dev-cs.ru/resources/984/ by GM-X Team
#include <amxmodx>
#include <json>
#include <sqlx>
#include <player_preferences>
new bool: DEBUG;
new const PluginName[] = "Player preferences";
new const PluginVersion[] = "1.0.8";
new const PluginAuthor[] = "GM-X Team, cpctrl";
new const PluginURL[] = "https://goldsrc.ru/members/3085/";
#define CHECK_NATIVE_ARGS_NUM(%1,%2,%3) \
if (%1 < %2) { \
DEBUG && log_error(AMX_ERR_NATIVE, "Invalid num of arguments %d. Expected %d", %1, %2); \
return %3; \
}
#define CHECK_NATIVE_PLAYER(%1,%2) \
if (!is_user_connected(%1)) { \
DEBUG && log_error(AMX_ERR_NATIVE, "Invalid player %d", %1); \
return %2; \
}
const MAX_KEY_LENGTH = 32;
const MAX_VALUE_STRING_LENGTH = 32;
enum fwdStruct {
Fwd_Initiated,
Fwd_KeyChanged
};
enum sqlx_e {
SQL_TABLE[32],
SQL_HOST[32],
SQL_USER[64],
SQL_PASS[64],
SQL_DB[32]
};
enum temp_e {
TEMP_ID,
TEMP_USERID,
TEMP_PLUGIN,
TEMP_CALLBACK[64]
};
new g_eForwards[fwdStruct];
new Handle: g_hTuple;
new g_eDbData[sqlx_e];
new Trie: g_tPlayerPreferences[MAX_PLAYERS + 1];
new JSON: g_jPlayerPreferences[MAX_PLAYERS + 1];
new LoadState: g_eLoadState[MAX_PLAYERS + 1] = { STATE_FAIL, ... };
public plugin_init() {
#if AMXX_VERSION_NUM < 200
register_plugin(PluginName, PluginVersion, PluginAuthor);
#else
register_plugin(PluginName, PluginVersion, PluginAuthor, PluginURL);
#endif
g_eForwards[Fwd_Initiated] = CreateMultiForward("pp_player_initiate", ET_IGNORE, FP_CELL, FP_CELL);
g_eForwards[Fwd_KeyChanged] = CreateMultiForward("pp_player_key_changed", ET_CONTINUE, FP_CELL, FP_STRING);
DEBUG = !!(plugin_flags() & AMX_FLAG_DEBUG);
sql_initiate();
}
public client_putinserver(id) {
if (is_user_hltv(id) || is_user_bot(id)) {
return;
}
g_tPlayerPreferences[id] = TrieCreate();
g_jPlayerPreferences[id] = json_init_object();
}
public client_disconnected(id) {
if (g_tPlayerPreferences[id] != Invalid_Trie) {
SavePreferences(id);
}
}
public plugin_end() {
DestroyForward(g_eForwards[Fwd_Initiated]);
DestroyForward(g_eForwards[Fwd_KeyChanged]);
}
public plugin_natives() {
register_native("pp_has_key", "native_has_key");
register_native("pp_get_load_state", "native_get_state");
register_native("pp_load_user", "native_load_user");
register_native("pp_get_number", "native_get_number");
register_native("pp_get_float", "native_get_float");
register_native("pp_get_bool", "native_get_bool");
register_native("pp_get_string", "native_get_string");
register_native("pp_set_number", "native_set_number");
register_native("pp_set_float", "native_set_float");
register_native("pp_set_bool", "native_set_bool");
register_native("pp_set_string", "native_set_string");
}
public bool: native_has_key(plugin, argc) {
enum {
arg_player = 1,
arg_key
};
CHECK_NATIVE_ARGS_NUM(argc, 2, false)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, false)
new key[MAX_KEY_LENGTH];
get_string(arg_key, key, MAX_KEY_LENGTH - 1);
return TrieKeyExists(g_tPlayerPreferences[id], key);
}
public LoadState: native_get_state(plugin, argc) {
enum {
arg_player = 1
};
CHECK_NATIVE_ARGS_NUM(argc, 1, STATE_FAIL)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, STATE_FAIL)
return g_eLoadState[id];
}
public bool: native_load_user(plugin, argc) {
if (g_hTuple == Empty_Handle) {
return false;
}
enum {
arg_player = 1,
arg_callback,
arg_authtype
};
CHECK_NATIVE_ARGS_NUM(argc, 2, false)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, false)
new buffer[512], len;
len = formatex(buffer, charsmax(buffer), "SELECT `data` FROM `%s` WHERE ", g_eDbData[SQL_TABLE]);
new name[MAX_NAME_LENGTH * 2], auth[MAX_AUTHID_LENGTH];
get_user_name(id, name, MAX_NAME_LENGTH - 1);
get_user_authid(id, auth, MAX_AUTHID_LENGTH - 1);
mysql_escape_string(name, charsmax(name));
switch (get_param(arg_authtype)) {
case AUTH_BY_NAME: {
len += formatex(buffer[len], charsmax(buffer) - len, "`name` = '%s';", name);
}
case AUTH_BY_STEAM: {
len += formatex(buffer[len], charsmax(buffer) - len, "`auth` = '%s';", auth);
}
case AUTH_COMBINE: {
len += formatex(buffer[len], charsmax(buffer) - len, "(`auth` = '%s' AND `name` = '%s');", auth, name);
}
}
g_eLoadState[id] = STATE_LOADING;
new temp[temp_e];
temp[TEMP_ID] = id;
temp[TEMP_USERID] = get_user_userid(id);
temp[TEMP_PLUGIN] = plugin;
new callback[64];
get_string(arg_callback, callback, charsmax(callback));
temp[TEMP_CALLBACK] = get_func_id(callback, plugin);
SQL_ThreadQuery(g_hTuple, "ThreadHandler", buffer, temp, sizeof temp);
return true;
}
public native_get_number(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_default
};
CHECK_NATIVE_ARGS_NUM(argc, 2, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH];
get_string(arg_key, key, MAX_KEY_LENGTH - 1);
if (!TrieKeyExists(g_tPlayerPreferences[id], key)) {
return argc >= arg_default ? get_param(arg_default) : 0;
}
new value;
TrieGetCell(g_tPlayerPreferences[id], key, value);
return value;
}
public Float: native_get_float(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_default
};
CHECK_NATIVE_ARGS_NUM(argc, 2, 0.0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0.0)
new key[MAX_KEY_LENGTH];
get_string(arg_key, key, MAX_KEY_LENGTH - 1);
if (!TrieKeyExists(g_tPlayerPreferences[id], key)) {
return argc >= arg_default ? get_param_f(arg_default) : 0.0;
}
new value;
TrieGetCell(g_tPlayerPreferences[id], key, value);
return float(value);
}
public bool: native_get_bool(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_default
};
CHECK_NATIVE_ARGS_NUM(argc, 2, false)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, false)
new key[MAX_KEY_LENGTH];
get_string(arg_key, key, MAX_KEY_LENGTH - 1);
if (!TrieKeyExists(g_tPlayerPreferences[id], key)) {
return bool: (argc >= arg_default ? get_param(arg_default) : 0);
}
new value;
TrieGetCell(g_tPlayerPreferences[id], key, value);
return bool: value;
}
public native_get_string(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_dest,
arg_length,
arg_default
};
CHECK_NATIVE_ARGS_NUM(argc, 2, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH], value[MAX_VALUE_STRING_LENGTH];
get_string(arg_key, key, MAX_KEY_LENGTH - 1);
if (!TrieGetString(g_tPlayerPreferences[id], key, value, MAX_VALUE_STRING_LENGTH - 1) && argc >= arg_default) {
get_string(arg_default, value, MAX_VALUE_STRING_LENGTH - 1);
}
return set_string(arg_dest, value, get_param(arg_length));
}
public native_set_number(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_value
};
CHECK_NATIVE_ARGS_NUM(argc, 3, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH], value;
get_string(arg_key, key, charsmax(key));
value = get_param(arg_value);
TrieSetCell(g_tPlayerPreferences[id], key, value);
return setValue(id, key, json_init_number(value));
}
public native_set_bool(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_value
};
CHECK_NATIVE_ARGS_NUM(argc, 3, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH], bool: value;
get_string(arg_key, key, charsmax(key));
value = bool: get_param(arg_value);
TrieSetCell(g_tPlayerPreferences[id], key, cell: value);
return setValue(id, key, json_init_bool(value));
}
public native_set_float(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_value
};
CHECK_NATIVE_ARGS_NUM(argc, 3, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH], Float: value;
get_string(arg_key, key, charsmax(key));
value = get_param_f(arg_value);
TrieSetCell(g_tPlayerPreferences[id], key, value);
return setValue(id, key, json_init_number(cell: value));
}
public native_set_string(plugin, argc) {
enum {
arg_player = 1,
arg_key,
arg_value
};
CHECK_NATIVE_ARGS_NUM(argc, 3, 0)
new id = get_param(arg_player);
CHECK_NATIVE_PLAYER(id, 0)
new key[MAX_KEY_LENGTH], value[MAX_VALUE_STRING_LENGTH];
get_string(arg_key, key, charsmax(key));
get_string(arg_value, value, charsmax(value));
TrieSetString(g_tPlayerPreferences[id], key, value);
return setValue(id, key, json_init_string(value));
}
public ThreadHandler(failstate, Handle: query, error[], errnum, data[], size, Float: queuetime) {
if (failstate) {
log_error(AMX_ERR_NATIVE, "[PP] [%d]: %s", errnum, error);
return PLUGIN_HANDLED;
}
new id = data[TEMP_ID];
CHECK_NATIVE_PLAYER(id, PLUGIN_HANDLED)
if (get_user_userid(id) != data[TEMP_USERID]) {
DEBUG && log_amx("[PP] Userid %d != Pushed userid %d", get_user_userid(id), data[TEMP_USERID]);
return PLUGIN_HANDLED;
}
if (SQL_NumResults(query)) {
new preferences[612];
SQL_ReadResult(query, SQL_FieldNameToNum(query, "data"), preferences, charsmax(preferences));
new JSON: jsonValue = json_parse(preferences);
if (jsonValue == Invalid_JSON || preferences[0] != '{' || preferences[strlen(preferences) - 1] != '}') {
json_free(jsonValue);
DEBUG && log_error(AMX_ERR_NATIVE, "[PP] Skipped load from bad format json on %d <%N>", id, id);
g_eLoadState[id] = STATE_FAIL;
return PLUGIN_HANDLED;
}
for (new i = 0, n = json_object_get_count(jsonValue), JSON: element, key[MAX_KEY_LENGTH], value[MAX_VALUE_STRING_LENGTH], num, bool: boolean; i < n; i++) {
json_object_get_name(jsonValue, i, key, charsmax(key));
element = json_object_get_value_at(jsonValue, i);
switch (json_get_type(element)) {
case JSONString: {
json_get_string(element, value, MAX_VALUE_STRING_LENGTH - 1);
TrieSetString(g_tPlayerPreferences[id], key, value);
json_object_set_string(g_jPlayerPreferences[id], key, value);
}
case JSONNumber: {
num = json_get_number(element);
TrieSetCell(g_tPlayerPreferences[id], key, num);
json_object_set_number(g_jPlayerPreferences[id], key, num);
}
case JSONBoolean: {
boolean = json_get_bool(element);
TrieSetCell(g_tPlayerPreferences[id], key, boolean ? 1 : 0);
json_object_set_bool(g_jPlayerPreferences[id], key, boolean);
}
}
json_free(element);
}
json_free(jsonValue);
g_eLoadState[id] = STATE_LOADED;
}
else {
g_eLoadState[id] = STATE_NODATA;
}
if (data[TEMP_PLUGIN] != INVALID_PLUGIN_ID && data[TEMP_CALLBACK] != INVALID_PLUGIN_ID && callfunc_begin_i(data[TEMP_CALLBACK], data[TEMP_PLUGIN]) == 1) {
callfunc_push_int(id);
callfunc_push_int(_: g_eLoadState[id]);
callfunc_end();
}
else {
ExecuteForward(g_eForwards[Fwd_Initiated], _, id, _: g_eLoadState[id]);
}
return PLUGIN_HANDLED;
}
stock setValue(const id, const key[], JSON: value) {
new fwReturn;
ExecuteForward(g_eForwards[Fwd_KeyChanged], fwReturn, id, key);
if (g_hTuple == Empty_Handle || fwReturn == PLUGIN_HANDLED) {
return -1;
}
json_object_set_value(g_jPlayerPreferences[id], key, value);
json_free(value);
return 1;
}
stock SavePreferences(const id) {
if (g_hTuple == Empty_Handle) {
ChallengeClear(id);
return;
}
if (json_serial_size(g_jPlayerPreferences[id]) < 5) {
ChallengeClear(id);
return;
}
static buffer[2042], serial[1024];
new auth[MAX_AUTHID_LENGTH], name[MAX_NAME_LENGTH];
get_user_name(id, name, MAX_NAME_LENGTH - 1);
mysql_escape_string(name, charsmax(name));
get_user_authid(id, auth, MAX_AUTHID_LENGTH - 1);
json_serial_to_string(g_jPlayerPreferences[id], serial, charsmax(serial));
mysql_escape_string(serial, charsmax(serial));
formatex(buffer, charsmax(buffer), "INSERT INTO `%s` (`name`, `auth`, `data`) VALUES ('%s', '%s', \
'%s') ON DUPLICATE KEY UPDATE `data` = '%s';", g_eDbData[SQL_TABLE], name, auth, serial, serial);
DEBUG && log_amx(buffer);
SQL_ThreadQuery(g_hTuple, "ThreadEmpty", buffer);
ChallengeClear(id);
}
public ThreadEmpty(failstate, Handle: query, error[], errnum, data[], size, Float: queuetime) {
if (failstate) {
log_error(AMX_ERR_NATIVE, "[PP] [%d]: %s", errnum, error);
}
return PLUGIN_HANDLED;
}
stock ChallengeClear(id) {
json_free(g_jPlayerPreferences[id]);
TrieDestroy(g_tPlayerPreferences[id]);
g_eLoadState[id] = STATE_FAIL;
}
stock sql_initiate() {
new filePath[PLATFORM_MAX_PATH];
get_localinfo("amxx_configsdir", filePath, PLATFORM_MAX_PATH - 1);
add(filePath, PLATFORM_MAX_PATH - 1, "/preferences.json");
if (!file_exists(filePath)) {
set_fail_state("Configuration file '%s' not found", filePath);
return;
}
new JSON: config = json_parse(filePath, true);
if (config == Invalid_JSON) {
set_fail_state("Configuration file '%s' read error", filePath);
return;
}
new temp[64];
json_object_get_string(config, "sql_table", temp, charsmax(temp));
copy(g_eDbData[SQL_TABLE], charsmax(g_eDbData[SQL_TABLE]), temp);
json_object_get_string(config, "sql_host", temp, charsmax(temp));
copy(g_eDbData[SQL_HOST], charsmax(g_eDbData[SQL_HOST]), temp);
json_object_get_string(config, "sql_user", temp, charsmax(temp));
copy(g_eDbData[SQL_USER], charsmax(g_eDbData[SQL_USER]), temp);
json_object_get_string(config, "sql_password", temp, charsmax(temp));
copy(g_eDbData[SQL_PASS], charsmax(g_eDbData[SQL_PASS]), temp);
json_object_get_string(config, "sql_db", temp, charsmax(temp));
copy(g_eDbData[SQL_DB], charsmax(g_eDbData[SQL_DB]), temp);
json_free(config);
new Handle: sConnection;
g_hTuple = SQL_MakeDbTuple(
g_eDbData[SQL_HOST],
g_eDbData[SQL_USER],
g_eDbData[SQL_PASS],
g_eDbData[SQL_DB]
);
new errCode, error[512];
sConnection = SQL_Connect(g_hTuple, errCode, error, charsmax(error));
if (sConnection == Empty_Handle) {
SQL_FreeHandle(g_hTuple);
g_hTuple = Empty_Handle;
DEBUG && log_amx("[PP] Error connecting to db '%s': #%d: %s", g_eDbData[SQL_DB], errCode, error);
return;
}
server_print("[PP] Connection to '%s' database success", g_eDbData[SQL_DB]);
SQL_FreeHandle(sConnection);
}
stock mysql_escape_string(dest[], len) {
replace_all(dest, len, "\\", "\\\\");
replace_all(dest, len, "\0", "\\0");
replace_all(dest, len, "\n", "\\n");
replace_all(dest, len, "\\r", "\\\r");
replace_all(dest, len, "\x1a", "\\Z");
replace_all(dest, len, "'", "\\'");
replace_all(dest, len, "`", "\\`");
}