mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-21 10:26:54 +01:00
- separates the composition of cyborgs into modules, iconsets, chassis, upgrades - modules / chassis atm can provide items, so can upgrades; in the future we will want to probably generalize the item API a bit so components can work - chassis now can (unimplemented) easily be made to give different stats - reorganizes all the cyborg sprites - adds generalized item mounts with redirection support and tears out legacy cyborg items - entirely rebalances modules & writes some new lawsets in for future use --------- Co-authored-by: silicons <silicons@silicons.dev>
453 lines
13 KiB
Plaintext
453 lines
13 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2024 Citadel Station Developers *//
|
|
|
|
/**
|
|
* Repository controllers.
|
|
*
|
|
* Storage for prototypes.
|
|
*
|
|
* Allows:
|
|
*
|
|
* * Looking instances up via ID
|
|
* * Looking instances up via typepath (if hardcoded)
|
|
*
|
|
* Should be:
|
|
*
|
|
* * Init order independent. This means that repositories should function
|
|
* if the database is up even if it hasn't technically initialized yet.
|
|
* This is because repositories init in an undetermined order (on purpose),
|
|
* and many repositories may / will depend on others.
|
|
* As an example, design datums require resolution of material datums.
|
|
*/
|
|
/datum/controller/repository
|
|
abstract_type = /datum/controller/repository
|
|
name = "REPOSITORY OF SOME KIND"
|
|
|
|
/// expected type of prototype
|
|
var/expected_type
|
|
|
|
/// database key; this is immutable.
|
|
/// * persistence is disabled if this is not set
|
|
var/database_key
|
|
/// store version
|
|
/// * persistence is disabled if this is not set
|
|
/// * migration is triggered if this doesn't match a loaded entry
|
|
/// * this should only ever be incremented.
|
|
var/store_version
|
|
/// store enabled? Updated by config reloads.
|
|
var/store_enabled = FALSE
|
|
|
|
/// by-id lookup
|
|
var/list/id_lookup
|
|
/// by-type lookup
|
|
var/list/type_lookup
|
|
|
|
/// fetched subtype lists
|
|
var/tmp/list/subtype_lists
|
|
|
|
/// 'doesn't exist' cache for DB loads
|
|
var/tmp/list/doesnt_exist_cache
|
|
var/const/doesnt_exist_cache_trim_at = 1000
|
|
var/const/doesnt_exist_cache_trim_to = 500
|
|
|
|
/// temporary id to path lookup used during init
|
|
// todo: figure out a way to not do this, this is bad
|
|
var/tmp/list/init_reverse_lookup_shim
|
|
|
|
/datum/controller/repository/proc/Create()
|
|
id_lookup = list()
|
|
type_lookup = list()
|
|
subtype_lists = list()
|
|
init_reverse_lookup_shim = list()
|
|
doesnt_exist_cache = list()
|
|
for(var/datum/prototype/casted as anything in subtypesof(expected_type))
|
|
if(initial(casted.abstract_type) == casted)
|
|
continue
|
|
var/casted_id = initial(casted.id)
|
|
if(!casted_id)
|
|
continue
|
|
init_reverse_lookup_shim[casted_id] = casted
|
|
return TRUE
|
|
|
|
/datum/controller/repository/Initialize()
|
|
generate()
|
|
init_reverse_lookup_shim = null
|
|
return ..()
|
|
|
|
/datum/controller/repository/vv_edit_var(var_name, var_value, mass_edit, raw_edit)
|
|
switch(var_name)
|
|
if(NAMEOF(src, store_version), NAMEOF(src, database_key), NAMEOF(src, store_enabled))
|
|
return FALSE
|
|
if(NAMEOF(src, expected_type))
|
|
return FALSE
|
|
return ..()
|
|
|
|
/**
|
|
* Repository Recover()
|
|
*
|
|
* The old instance is passed in.
|
|
* You can, and should, cast it to the type you're defining this on, as it'll always be the same type.
|
|
*/
|
|
/datum/controller/repository/Recover(datum/controller/repository/old_instance)
|
|
// todo: redo recover logic; maybe /datum/controller as a whole should be brushed up
|
|
. = ..()
|
|
if(!istype(old_instance))
|
|
src.type_lookup = list()
|
|
src.id_lookup = list()
|
|
src.subtype_lists = list()
|
|
generate()
|
|
return FALSE
|
|
src.type_lookup = old_instance.type_lookup
|
|
if(!islist(src.type_lookup))
|
|
src.type_lookup = list()
|
|
. = FALSE
|
|
src.id_lookup = old_instance.id_lookup
|
|
if(!islist(src.id_lookup))
|
|
src.id_lookup = list()
|
|
. = FALSE
|
|
src.subtype_lists = list()
|
|
|
|
/**
|
|
* Called when config is reloaded.
|
|
*/
|
|
/datum/controller/repository/proc/on_config_loaded()
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
store_enabled = Configuration.get_entry(/datum/toml_config_entry/backend/repository/persistence) && database_key && store_version
|
|
|
|
/**
|
|
* regenerates entries, kicking out anything that's in the way
|
|
*/
|
|
/datum/controller/repository/proc/generate()
|
|
for(var/datum/prototype/instance as anything in subtypesof(expected_type))
|
|
if(initial(instance.abstract_type) == instance)
|
|
continue
|
|
if(initial(instance.lazy))
|
|
continue
|
|
// are we already loaded?
|
|
if(type_lookup[instance])
|
|
continue
|
|
instance = new instance
|
|
instance.hardcoded = TRUE
|
|
load(instance)
|
|
|
|
//* Public API *//
|
|
|
|
/**
|
|
* Fetches a prototype by type or ID.
|
|
*
|
|
* * Allows passing in a prototype instance which will be returned as itself.
|
|
* Useful for procs that should accept types, IDs, *and* instances.
|
|
* * Unlike fetch local / fetch or defer, this **can** sleep!
|
|
*
|
|
* prototypes returned should never, ever be modified
|
|
*
|
|
* @return prototype instance or null
|
|
*/
|
|
/datum/controller/repository/proc/fetch(datum/prototype/type_or_id) as /datum/prototype
|
|
RETURN_TYPE(/datum/prototype)
|
|
// todo: optimize
|
|
if(isnull(type_or_id))
|
|
return
|
|
else if(istext(type_or_id))
|
|
if(init_reverse_lookup_shim)
|
|
var/potential_path = init_reverse_lookup_shim[type_or_id]
|
|
return fetch(potential_path)
|
|
. = id_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(!store_enabled)
|
|
return
|
|
if(doesnt_exist_cache[type_or_id])
|
|
return
|
|
return handle_db_load(type_or_id)
|
|
else if(ispath(type_or_id))
|
|
. = type_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(initial(type_or_id.abstract_type) == type_or_id)
|
|
CRASH("tried to fetch an abstract prototype")
|
|
var/datum/prototype/loading = new type_or_id
|
|
loading.hardcoded = TRUE
|
|
load(loading)
|
|
return loading
|
|
else if(istype(type_or_id))
|
|
return type_or_id
|
|
else
|
|
CRASH("what?")
|
|
|
|
/**
|
|
* Fetches a prototype by type or ID.
|
|
*
|
|
* * Allows passing in a prototype instance which will be returned as itself.
|
|
* Useful for procs that should accept types, IDs, *and* instances.
|
|
* * If something doesn't exist and we don't know if it exists in the database, we throw a runtime error.
|
|
* * If fetching a hardcoded path, this should generally be used as it never sleeps.
|
|
*
|
|
* prototypes returned should never, ever be modified
|
|
*
|
|
* @return prototype instance or null
|
|
*/
|
|
/datum/controller/repository/proc/fetch_local_or_throw(datum/prototype/type_or_id) as /datum/prototype
|
|
RETURN_TYPE(/datum/prototype)
|
|
// todo: optimize
|
|
if(isnull(type_or_id))
|
|
// null is valid
|
|
return
|
|
else if(istext(type_or_id))
|
|
if(init_reverse_lookup_shim)
|
|
var/potential_path = init_reverse_lookup_shim[type_or_id]
|
|
return fetch_local_or_throw(potential_path)
|
|
. = id_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(!store_enabled)
|
|
return
|
|
if(doesnt_exist_cache[type_or_id])
|
|
return
|
|
CRASH("fetch_local_or_throw of [type_or_id] couldn't determine if id existed without a fetch.")
|
|
else if(ispath(type_or_id))
|
|
. = type_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(initial(type_or_id.abstract_type) == type_or_id)
|
|
CRASH("tried to fetch an abstract prototype")
|
|
var/datum/prototype/loading = new type_or_id
|
|
loading.hardcoded = TRUE
|
|
load(loading)
|
|
return loading
|
|
else if(istype(type_or_id))
|
|
return type_or_id
|
|
else
|
|
CRASH("what?")
|
|
|
|
/**
|
|
* Fetches a prototype by type or ID.
|
|
*
|
|
* * Allows passing in a prototype instance which will be returned as itself.
|
|
* Useful for procs that should accept types, IDs, *and* instances.
|
|
* * If something doesn't exist and we don't know if it exists in the database, we return
|
|
* REPOSITORY_FETCH_DEFER. The caller should invoke normal fetch() at a time when sleeping is allowed.
|
|
*
|
|
* prototypes returned should never, ever be modified
|
|
*
|
|
* @return prototype instance, null, or REPOSITORY_FETCH_DEFER
|
|
*/
|
|
/datum/controller/repository/proc/fetch_or_defer(datum/prototype/type_or_id) as /datum/prototype
|
|
RETURN_TYPE(/datum/prototype)
|
|
// todo: optimize
|
|
if(isnull(type_or_id))
|
|
return
|
|
else if(istext(type_or_id))
|
|
if(init_reverse_lookup_shim)
|
|
var/potential_path = init_reverse_lookup_shim[type_or_id]
|
|
return fetch_or_defer(potential_path)
|
|
. = id_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(!store_enabled)
|
|
return
|
|
if(doesnt_exist_cache[type_or_id])
|
|
return
|
|
return REPOSITORY_FETCH_DEFER
|
|
else if(ispath(type_or_id))
|
|
. = type_lookup[type_or_id]
|
|
if(.)
|
|
return
|
|
if(initial(type_or_id.abstract_type) == type_or_id)
|
|
CRASH("tried to fetch an abstract prototype")
|
|
var/datum/prototype/loading = new type_or_id
|
|
loading.hardcoded = TRUE
|
|
load(loading)
|
|
return loading
|
|
else if(istype(type_or_id))
|
|
return type_or_id
|
|
else
|
|
CRASH("what?")
|
|
|
|
/**
|
|
* Fetches a list of prototypes by type or ID.
|
|
*
|
|
* * Allows passing in prototype instances which will be returned as itself.
|
|
* Useful for procs that should accept types, IDs, *and* instances.
|
|
*
|
|
* prototypes returned should never, ever be modified
|
|
*
|
|
* @return list() of instances
|
|
*/
|
|
/datum/controller/repository/proc/fetch_multi(list/datum/prototype/types_or_ids)
|
|
// todo: optimize
|
|
. = list()
|
|
for(var/datum/prototype/casted as anything in types_or_ids)
|
|
. += fetch(casted)
|
|
|
|
/**
|
|
* lists returned should never, ever be modified.
|
|
* this fetches subtypes, not the first type on purpose.
|
|
*/
|
|
/datum/controller/repository/proc/fetch_subtypes_immutable(path) as /list
|
|
RETURN_TYPE(/list)
|
|
ASSERT(ispath(path, /datum/prototype))
|
|
if(subtype_lists[path])
|
|
return subtype_lists[path]
|
|
var/list/generating = list()
|
|
subtype_lists[path] = generating
|
|
for(var/datum/prototype/casted as anything in subtypesof(path))
|
|
if(initial(casted.abstract_type) == casted)
|
|
continue
|
|
var/datum/prototype/instance = fetch_local_or_throw(casted)
|
|
generating += instance
|
|
return generating
|
|
|
|
/**
|
|
* lists returned may be modified
|
|
*/
|
|
/datum/controller/repository/proc/fetch_subtypes_mutable(path) as /list
|
|
RETURN_TYPE(/list)
|
|
return fetch_subtypes_immutable(path).Copy()
|
|
|
|
/**
|
|
* Registers a prototype created midround.
|
|
*
|
|
* * This can immediately save it to the database.
|
|
* * After calling this, **you must release any cached references to the instance from the calling proc.**
|
|
* After this call, the repository now owns the instance, not whichever system created it.
|
|
*/
|
|
/datum/controller/repository/proc/register(datum/prototype/instance)
|
|
. = load(instance)
|
|
if(!.)
|
|
return
|
|
if(store_enabled)
|
|
handle_db_store(instance)
|
|
|
|
//* Private API *//
|
|
|
|
/**
|
|
* Registers a prototype with the subsystem.
|
|
*
|
|
* * This is for internal use.
|
|
*/
|
|
/datum/controller/repository/proc/load(datum/prototype/instance)
|
|
PROTECTED_PROC(TRUE)
|
|
if(id_lookup[instance])
|
|
. = FALSE
|
|
CRASH("attempted to load an instance that collides with a currently loaded instance on ID.")
|
|
if(instance.hardcoded && type_lookup[instance.type])
|
|
. = FALSE
|
|
CRASH("attempted to load an instance that collides with a currently loaded instance on type.")
|
|
if(!instance.register())
|
|
. = FALSE
|
|
CRASH("instance failed to register. this is undefined behavior.")
|
|
id_lookup[instance.id] = instance
|
|
if(instance.hardcoded)
|
|
// invalidate cache
|
|
// todo: smarter way to do this
|
|
subtype_lists = list()
|
|
type_lookup[instance.type] = instance
|
|
return TRUE
|
|
|
|
/**
|
|
* Unregister a prototype.
|
|
*
|
|
* * This does not delete it from existence, this just unloads it from the subsystem.
|
|
* * This is for internal use.
|
|
*/
|
|
/datum/controller/repository/proc/unload(datum/prototype/instance)
|
|
PROTECTED_PROC(TRUE)
|
|
if(!instance.unregister())
|
|
. = FALSE
|
|
CRASH("instance failed to unregister. this is undefined behavior.")
|
|
id_lookup -= instance.id
|
|
if(instance.hardcoded)
|
|
// invalidate cache
|
|
// todo: smarter way to do this
|
|
subtype_lists = list()
|
|
type_lookup -= instance.type
|
|
return TRUE
|
|
|
|
/**
|
|
* Perform migration on a data-list from the database.
|
|
*
|
|
* * Edit the passed in list directly.
|
|
* * This should update to latest.
|
|
*
|
|
* todo: proc to auto-migrate everything.
|
|
*/
|
|
/datum/controller/repository/proc/migrate(list/modifying, from_version)
|
|
PROTECTED_PROC(TRUE)
|
|
|
|
/datum/controller/repository/proc/handle_db_store(datum/prototype/instance)
|
|
doesnt_exist_cache -= instance.id
|
|
|
|
// intentionally allow admin proccalls to bypass checks in NewQuery()
|
|
var/old_usr = usr
|
|
usr = null
|
|
|
|
var/datum/db_query/store_query = SSdbcore.NewQuery(
|
|
"INSERT INTO " + DB_PREFIX_TABLE_NAME("backend_repository") + "(repository, id, version, data) VALUES \
|
|
(:repo, :id, :version, :data) ON DUPLICATE KEY UPDATE data = :data, modifiedTime = Now(), version = :version",
|
|
list(
|
|
"repo" = database_key,
|
|
"id" = instance.id,
|
|
"version" = store_version,
|
|
"data" = json_encode(instance.serialize()),
|
|
),
|
|
)
|
|
|
|
usr = old_usr
|
|
|
|
store_query.Execute(TRUE)
|
|
qdel(store_query)
|
|
|
|
/datum/controller/repository/proc/handle_db_load(instance_id)
|
|
if(doesnt_exist_cache[instance_id])
|
|
return
|
|
|
|
// intentionally allow admin proccalls to bypass checks in NewQuery()
|
|
var/old_usr = usr
|
|
usr = null
|
|
|
|
var/datum/db_query/load_query = SSdbcore.NewQuery(
|
|
"SELECT version, data FROM " + DB_PREFIX_TABLE_NAME("backend_repository") + " WHERE repository = :repo, id = :id",
|
|
list(
|
|
"repo" = database_key,
|
|
"id" = instance_id,
|
|
),
|
|
)
|
|
|
|
usr = old_usr
|
|
|
|
load_query.Execute(TRUE)
|
|
|
|
if(!length(load_query.item))
|
|
mark_doesnt_exist(instance_id)
|
|
else
|
|
var/list/fetched = load_query.item[1]
|
|
var/version = fetched[1]
|
|
var/encoded_data = fetched[2]
|
|
var/list/decoded_data = json_decode(encoded_data)
|
|
var/migrated = FALSE
|
|
|
|
if(version < store_version)
|
|
migrate(decoded_data, version)
|
|
migrated = TRUE
|
|
else if(version == store_version)
|
|
mark_doesnt_exist(instance_id)
|
|
CRASH("[version] was not less or eq to [store_version]. something's very wrong!")
|
|
|
|
var/datum/prototype/loaded_instance = new expected_type
|
|
loaded_instance.deserialize(decoded_data)
|
|
if(!load(loaded_instance))
|
|
mark_doesnt_exist(instance_id)
|
|
CRASH("[instance_id] failed to load into the repository during database load!")
|
|
. = loaded_instance
|
|
|
|
if(migrated)
|
|
handle_db_store(loaded_instance)
|
|
|
|
qdel(load_query)
|
|
|
|
/datum/controller/repository/proc/mark_doesnt_exist(instance_id)
|
|
doesnt_exist_cache[instance_id] = TRUE
|
|
if(length(doesnt_exist_cache) > doesnt_exist_cache_trim_at)
|
|
doesnt_exist_cache.len = doesnt_exist_cache_trim_to
|