mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-16 05:56:37 +01:00
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may not be viewable. --> <!-- You can view Contributing.MD for a detailed description of the pull request process. --> ## About The Pull Request update time related procs & glob too (i was originally targeting that lmao). also **stop** using byondtime for sql stuff, we have `NOW()`!! <!-- Describe The Pull Request. Please be sure every change is documented or this can delay review and even discourage maintainers from merging your PR! --> ## Why It's Good For The Game <!-- Argue for the merits of your changes and how they benefit the game, especially if they are controversial and/or far reaching. If you can't actually explain WHY what you are doing will improve the game, then it probably isn't good for the game in the first place. --> ## Changelog <!-- If your PR modifies aspects of the game that can be concretely observed by players or admins you should add a changelog. If your change does NOT meet this description, remove this section. Please note that maintainers freely reserve the right to remove and add tags should they deem it appropriate. You can attempt to finagle the system all you want, but it's best to shoot for clear communication right off the bat. --> <!-- Both 🆑's are required for the changelog to work! You can put your name to the right of the first 🆑 if you want to overwrite your GitHub username as author ingame. --> <!-- You can use multiple of the same prefix (they're only used for the icon ingame) and delete the unneeded ones. Despite some of the tags, changelogs should generally represent how a player might be affected by the changes rather than a summary of the PR's contents. -->
268 lines
6.5 KiB
Plaintext
268 lines
6.5 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2023 Citadel Station developers. *//
|
|
|
|
GLOBAL_LIST_EMPTY(player_data)
|
|
|
|
/proc/resolve_player_data(ckey, key)
|
|
ckey = ckey(ckey) // just in case
|
|
if(!islist(GLOB.player_data))
|
|
// we CANNOT RUNTIME
|
|
GLOB.player_data = list()
|
|
if(!istype(GLOB.player_data[ckey], /datum/player_data))
|
|
GLOB.player_data[ckey] = new /datum/player_data(ckey, key)
|
|
return GLOB.player_data[ckey]
|
|
|
|
/**
|
|
* holds db-related data
|
|
* loaded every connect
|
|
*/
|
|
/datum/player_data
|
|
//! intrinsics
|
|
/// our ckey
|
|
var/ckey
|
|
/// our key
|
|
var/key
|
|
/// available: null if don't know yet, FALSE if no dbcon, TRUE if loaded
|
|
var/available
|
|
/// is guest
|
|
var/is_guest
|
|
/// loading?
|
|
var/loading = FALSE
|
|
/// saving?
|
|
var/saving = FALSE
|
|
/// re-save queued?
|
|
/// used so we only have one blocking save if we get modified during a save
|
|
var/resave_queued = FALSE
|
|
|
|
//! loaded data
|
|
/// player id
|
|
var/player_id
|
|
/// player flags
|
|
var/player_flags = NONE
|
|
/// player age
|
|
var/player_age
|
|
/// join date
|
|
var/player_first_seen
|
|
/// arbitrary JSON-serializable key-value data
|
|
/// you must handle errors yourself! we won't handle it for you.
|
|
var/list/player_misc
|
|
|
|
/datum/player_data/New(ckey, key)
|
|
src.ckey = ckey
|
|
if(!src.ckey)
|
|
return
|
|
src.key = key
|
|
is_guest = IsGuestKey(key)
|
|
load()
|
|
|
|
/**
|
|
* async
|
|
*/
|
|
/datum/player_data/proc/load()
|
|
if(!SSdbcore.Connect())
|
|
if(isnull(available))
|
|
available = FALSE
|
|
return FALSE
|
|
// make separate call chain; do not mess with client init
|
|
spawn(0)
|
|
load_blocking()
|
|
return TRUE
|
|
|
|
/datum/player_data/proc/load_blocking()
|
|
// allow admin proccalls - there's no args here.
|
|
var/was_proccall = !!IsAdminAdvancedProcCall()
|
|
var/old_usr = usr
|
|
usr = null
|
|
_load_lock(was_proccall)
|
|
usr = old_usr
|
|
|
|
/datum/player_data/proc/_load_lock(was_proccall)
|
|
if(IsAdminAdvancedProcCall())
|
|
return
|
|
if(loading)
|
|
if(!was_proccall)
|
|
CRASH("loading is still locked; why are we loading this fast?")
|
|
return
|
|
loading = TRUE
|
|
_load()
|
|
loading = FALSE
|
|
|
|
/datum/player_data/proc/_load()
|
|
if(IsAdminAdvancedProcCall())
|
|
return
|
|
if(IsGuestKey(key))
|
|
return
|
|
var/datum/db_query/lookup
|
|
lookup = SSdbcore.ExecuteQuery(
|
|
"SELECT id, playerid, firstseen FROM [DB_PREFIX_TABLE_NAME("player_lookup")] WHERE ckey = :ckey",
|
|
list(
|
|
"ckey" = ckey
|
|
)
|
|
)
|
|
if(!lookup.NextRow())
|
|
CRASH("failed to load lookup data")
|
|
var/lookup_id = lookup.item[1]
|
|
var/lookup_pid = lookup.item[2]
|
|
var/lookup_firstseen = lookup.item[3]
|
|
if(lookup_pid)
|
|
if(istext(lookup_id))
|
|
lookup_id = text2num(lookup_id)
|
|
if(istext(lookup_pid))
|
|
lookup_pid = text2num(lookup_pid)
|
|
qdel(lookup)
|
|
lookup = SSdbcore.ExecuteQuery(
|
|
"SELECT id, flags, datediff(Now(), firstseen), firstseen, misc FROM [DB_PREFIX_TABLE_NAME("player")] WHERE id = :id",
|
|
list(
|
|
"id" = lookup_pid
|
|
)
|
|
)
|
|
if(lookup.NextRow())
|
|
// found!
|
|
var/lookup_flags = lookup.item[2]
|
|
var/lookup_age = lookup.item[3]
|
|
if(istext(lookup_flags))
|
|
lookup_flags = text2num(lookup_flags)
|
|
if(istext(lookup_age))
|
|
lookup_age = text2num(lookup_age)
|
|
player_id = lookup_pid
|
|
player_flags = lookup_flags
|
|
player_first_seen = lookup.item[4]
|
|
player_age = lookup_age
|
|
player_misc = safe_json_decode(lookup.item[5])
|
|
qdel(lookup)
|
|
available = TRUE
|
|
else
|
|
CRASH("failed to lookup player row on id [lookup_pid] even though id was present in player_lookup.")
|
|
else
|
|
qdel(lookup)
|
|
register_new_player(lookup_firstseen, lookup_id)
|
|
|
|
/datum/player_data/proc/register_new_player(migrate_firstseen, lookup_id)
|
|
if(IsAdminAdvancedProcCall())
|
|
return
|
|
// new person!
|
|
player_age = 0
|
|
player_flags = NONE
|
|
player_misc = list()
|
|
var/datum/db_query/insert
|
|
if(migrate_firstseen)
|
|
insert = SSdbcore.ExecuteQuery({"
|
|
INSERT INTO [DB_PREFIX_TABLE_NAME("player")] (`flags`, `firstseen`, `lastseen`, `misc`)
|
|
VALUES (:flags, :fs, NOW(), :misc)
|
|
"}, list(
|
|
"flags" = player_flags,
|
|
"fs" = migrate_firstseen,
|
|
"misc" = safe_json_encode(player_misc),
|
|
)
|
|
)
|
|
player_first_seen = migrate_firstseen
|
|
else
|
|
insert = SSdbcore.ExecuteQuery({"
|
|
INSERT INTO [DB_PREFIX_TABLE_NAME("player")] (`flags`, `firstseen`, `lastseen`, `misc`)
|
|
VALUES (:flags, NOW(), NOW(), :misc)
|
|
"}, list(
|
|
"flags" = player_flags,
|
|
"misc" = safe_json_encode(player_misc),
|
|
)
|
|
)
|
|
player_first_seen = ISOtime() // pretend i am Now()
|
|
var/insert_id = insert.last_insert_id
|
|
if(istext(insert_id))
|
|
insert_id = text2num(insert_id)
|
|
if(!isnum(insert_id))
|
|
CRASH("invalid insert id??")
|
|
player_id = insert_id
|
|
qdel(insert)
|
|
// now update lookup
|
|
insert = SSdbcore.ExecuteQuery(
|
|
"UPDATE [DB_PREFIX_TABLE_NAME("player_lookup")] SET playerid = :pid WHERE id = :id",
|
|
list(
|
|
"id" = lookup_id,
|
|
"pid" = insert_id
|
|
)
|
|
)
|
|
qdel(insert)
|
|
available = TRUE
|
|
// todo: lookup DATEDIFF so player age is set for the first connect after migrating a ckey.
|
|
|
|
/**
|
|
* async
|
|
*/
|
|
/datum/player_data/proc/save()
|
|
set waitfor = FALSE
|
|
// why are we in here if we're write locked?
|
|
if(saving)
|
|
if(resave_queued)
|
|
return
|
|
resave_queued = TRUE
|
|
UNTIL(!saving && !loading)
|
|
resave_queued = FALSE
|
|
// don't fuck with things if we're read-locked
|
|
UNTIL(!loading)
|
|
// check again
|
|
if(saving)
|
|
CRASH("race condition in save")
|
|
// allow admin proccalls - there's no args here.
|
|
var/old_usr = usr
|
|
usr = null
|
|
// don't lock; if something is spamming our flags they probably shouldn't be
|
|
saving = TRUE
|
|
_save()
|
|
saving = FALSE
|
|
usr = old_usr
|
|
|
|
/datum/player_data/proc/_save()
|
|
qdel(SSdbcore.ExecuteQuery(
|
|
"UPDATE [DB_PREFIX_TABLE_NAME("player")] SET flags = :flags, misc = :misc WHERE id = :id",
|
|
list(
|
|
"flags" = player_flags,
|
|
"id" = player_id,
|
|
"misc" = safe_json_encode(player_misc, "{}"),
|
|
)
|
|
))
|
|
|
|
/**
|
|
* async
|
|
*/
|
|
/datum/player_data/proc/log_connect()
|
|
// make separate call chain; do not mess with client init
|
|
spawn(0)
|
|
update_last_seen()
|
|
|
|
/datum/player_data/proc/update_last_seen()
|
|
// don't interrupt
|
|
if(!block_on_available())
|
|
return FALSE
|
|
qdel(SSdbcore.ExecuteQuery(
|
|
"UPDATE [DB_PREFIX_TABLE_NAME("player")] SET lastseen = Now() WHERE id = :id",
|
|
list(
|
|
"id" = player_id,
|
|
)
|
|
))
|
|
return TRUE
|
|
|
|
/**
|
|
* sync
|
|
*/
|
|
/datum/player_data/proc/player_age()
|
|
UNTIL(!loading)
|
|
return player_age
|
|
|
|
/**
|
|
* block until we know if we're available
|
|
* then return if we are
|
|
*
|
|
* WARNING: without database, or if this is for a guest key, we will never be available.
|
|
*/
|
|
/datum/player_data/proc/block_on_available(timeout = 10 SECONDS)
|
|
var/timed_out = world.time + timeout
|
|
UNTIL(!isnull(available) || world.time > timed_out)
|
|
return available
|
|
|
|
/**
|
|
* returns if we're available
|
|
* if we don't know yet, return false
|
|
*/
|
|
/datum/player_data/proc/immediately_available()
|
|
return !!available
|