mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-21 01:46:35 +01:00
301 lines
8.3 KiB
Plaintext
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
|