Files

301 lines
8.3 KiB
Plaintext

//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2023 Citadel Station developers. *//
/**
* ? Character Data System
*
* This is both used for persistence, as well as potentially admin backend in the future.
* Please exercise caution when editing the subsystem and the root of /datum/character_data.
* Use and abuse subtypes to do what you need to do for persistence features.
*
* todo: this should go on SScharacters
*/
/datum/controller/subsystem/characters
/// loaded characters - "[id]" = /datum/character_data instance
var/list/character_cache = list()
/**
* flushes a character to db
* a new character is valid to save this way.
*
* @params
* * char - character datum
* * persisting - update persistence info if mob is given
*/
/datum/controller/subsystem/characters/proc/save_character(datum/character_data/char, mob/persisting)
// pause admin proccall guard
var/__oldusr = usr
usr = null
// section below can never be allowed to runtime
// if we have id, we're updating
if(char.character_id)
// last played is not updated by this proc
// everything else can though!
var/datum/db_query/update_query = SSdbcore.NewQuery(
"UPDATE [DB_PREFIX_TABLE_NAME("character")] \
SET[persisting? " last_persisted = NOW()," : ""] canonical_name = :name, persist_data = :data, \
playerid = :pid \
WHERE id = :id",
list(
"id" = char.character_id,
"pid" = char.player_id,
"name" = ckey(char.canonical_name),
"data" = persisting? json_encode(char.make_persist_data(persisting)) : json_encode(list()),
)
)
update_query.Execute(async = FALSE)
qdel(update_query)
else
var/datum/db_query/insert_query = SSdbcore.NewQuery(
"INSERT INTO [DB_PREFIX_TABLE_NAME("character")] \
(`created`, `last_played`, `last_persisted`, `playerid`, `canonical_name`, \
`persist_data`, `character_type`) \
VALUES (NOW(), NULL, [persisting? "NOW" : "NULL"], :pid, :name, :data, :type)",
list(
"pid" = char.player_id,
"name" = ckey(char.canonical_name),
"data" = persisting? json_encode(char.make_persist_data(persisting)) : json_encode(list()),
"type" = char.character_type,
),
)
insert_query.Execute(async = FALSE)
char.character_id = isnum(insert_query.last_insert_id)? insert_query.last_insert_id : text2num(insert_query.last_insert_id)
qdel(insert_query)
// fetch our ID immediately to repopulate
fetch_character(char.character_id, TRUE)
// resume admin proccall guard
usr = __oldusr
/**
* fetches or makes a character datum of the given canonical name, player id, and character type
*/
/datum/controller/subsystem/characters/proc/load_character(name, playerid, character_type)
var/path = character_type_to_datum_path(character_type)
ASSERT(!isnull(path))
ASSERT(isnum(playerid))
name = ckey(name)
ASSERT(length(name))
// pause admin proccall guard
var/__oldusr = usr
usr = null
// section below can never be allowed to runtime
var/datum/db_query/load_query = SSdbcore.NewQuery(
"SELECT `id` FROM \
[DB_PREFIX_TABLE_NAME("character")] WHERE playerid = :id AND canonical_name = :name AND character_type = :type",
list(
"id" = playerid,
"canonical_name" = name,
"type" = character_type,
)
)
load_query.Execute(async = FALSE)
// resume admin proccall guard
usr = __oldusr
if(!load_query.NextRow())
// need a new one
var/datum/character_data/character = new path
character.player_id = playerid
character.canonical_name = ckey(name)
character.character_type = character_type
// immediately save
save_character(character)
return character
// was found, load it
return fetch_character(text2num(load_query.item[1]))
/**
* fetches a character datum
* you should not hold references to it yourself
* refetch when you need it!
*
* @params
* * id - character id
* * force - reload from sql if it's in cache
*/
/datum/controller/subsystem/characters/proc/fetch_character(id, force = FALSE)
ASSERT(isnum(id))
// pause admin proccall guard
var/__oldusr = usr
usr = null
// section below can never be allowed to runtime
var/datum/character_data/loaded = character_cache[num2text(id, 16)]
if(!force && loaded)
return loaded
var/datum/db_query/load_query = SSdbcore.NewQuery(
"SELECT `created`, `last_played`, `last_persisted`, `playerid`, `canonical_name`, `persist_data`, `character_type` FROM \
[DB_PREFIX_TABLE_NAME("character")] WHERE id = :id",
list(
"id" = id,
)
)
load_query.Execute(async = FALSE)
if(!load_query.NextRow())
character_cache -= num2text(id, 16)
qdel(load_query)
return null
var/char_type = load_query.item[7]
var/datum_type = character_type_to_datum_path(char_type)
if(!datum_type)
. = null
CRASH("unexpected char_type: [char_type]")
// one's there, make one if it isn't already there
if(!istype(loaded, datum_type))
character_cache[num2text(id, 16)] = (loaded = new datum_type)
loaded.character_id = id
loaded.player_id = text2num(load_query.item[4])
loaded.created_at = load_query.item[1]
loaded.played_at = load_query.item[2]
loaded.persisted_at = load_query.item[3]
loaded.canonical_name = load_query.item[5]
loaded.read_persist_data(load_query.item[6]? safe_json_decode(load_query.item[6], list()) : list())
. = loaded
qdel(load_query)
// resume admin proccall guard
usr = __oldusr
/**
* returns a list of character ids for a player
*
* @params
* * playerid - player id
* * fetch - fetch the character datums in the process
* * force - forcefully fetch the character even if it's cached
*/
/datum/controller/subsystem/characters/proc/query_characters(playerid, fetch = FALSE, force = FALSE)
ASSERT(isnum(playerid))
// pause admin proccall guard
var/__oldusr = usr
usr = null
// section below can never be allowed to runtime
. = list()
var/datum/db_query/iteration_query = SSdbcore.ExecuteQuery(
"SELECT id FROM [DB_PREFIX_TABLE_NAME("character")] WHERE playerid = :id",
list(
"id" = playerid
)
)
while(iteration_query.NextRow())
. += text2num(iteration_query.item[1])
if(!length(.))
return
// resume admin proccall guard
usr = __oldusr
// fetch if needed
if(fetch)
for(var/id in .)
fetch_character(id, force)
/**
* mark a character as having played
*
* @params
* * id - character id
*/
/datum/controller/subsystem/characters/proc/character_played(id)
ASSERT(isnum(id))
// pause admin proccall guard
var/__oldusr = usr
usr = null
// section below can never be allowed to runtime
var/datum/db_query/mark_query = SSdbcore.ExecuteQuery(
"UPDATE [DB_PREFIX_TABLE_NAME("character")] SET last_played = NOW() WHERE id = :id",
list(
"id" = id
)
)
. = !!mark_query.affected
// resume admin proccall guard
usr = __oldusr
/**
* hardcoded switch: what character type string corrosponds to what /datum/character_data
*/
/datum/controller/subsystem/characters/proc/character_type_to_datum_path(what)
switch(what)
if(OBJECT_PERSISTENCE_CHARACTER_TYPE_HUMAN)
return /datum/character_data/human
/datum/character_data
abstract_type = /datum/character_data
/// our character id, if we're already in the sql database
var/character_id
/// character type - should never change
var/character_type
/// our player id
var/player_id
/// our ckey(name) - used to avoid spaces/whatever getting in the way
var/canonical_name
/// created - SQL datetime - not modifiable
var/created_at
/// last played SQL datetime - not modifiable directly, use proc
var/played_at
/// last persisted SQL datetime - not modifiable directly
var/persisted_at
/**
* reads from a given mob
*
* this *is* allowed to touch the mob's /datum/mind!
*/
/datum/character_data/proc/read_from(mob/M)
return
/**
* writes to a given mob
*
* this *is* allowed to touch the mob's /datum/mind!
*/
/datum/character_data/proc/write_to(mob/M)
return
/**
* gets data to persist
*
* @return list of k-v entries
*/
/datum/character_data/proc/make_persist_data(mob/M)
return list()
/**
* loads fields from persisting data
*/
/datum/character_data/proc/read_persist_data(list/data)
return
/**
* changes the name of this character
* only ever change names this way, this'll handle the necessary updates, within the subsystem and on /datum/player's.
*/
/datum/character_data/proc/immediate_rename(new_name)
canonical_name = ckey(new_name)
SScharacters.save_character(src)
/datum/character_data/human
character_type = OBJECT_PERSISTENCE_CHARACTER_TYPE_HUMAN