Files
Letter NandGitHub 37c6a7d1fc time, is it really that time (#7482)
<!-- 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. -->
2026-01-13 22:06:30 -05:00

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