Skip to content

Commit

Permalink
API: Spawn pools, a distributed loot manager. (#27199)
Browse files Browse the repository at this point in the history
* api: Spawn pools, a distributed loot manager.

* meh

* documentation and cleanups

* how do numbers work

* word wrapping

* fixes found from prototyping
  • Loading branch information
warriorstar-orion authored Nov 22, 2024
1 parent cb1e9df commit c4e4487
Show file tree
Hide file tree
Showing 8 changed files with 420 additions and 9 deletions.
2 changes: 2 additions & 0 deletions code/controllers/subsystem/non_firing/SSlate_mapping.dm
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ SUBSYSTEM_DEF(late_mapping)
QDEL_LIST_CONTENTS(maze_generators)
var/duration = stop_watch(watch)
log_startup_progress("Generated [mgcount] mazes in [duration]s")

GLOB.spawn_pool_manager.process_pools()
104 changes: 104 additions & 0 deletions code/game/objects/effects/spawners/random/pool/pool_spawner.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/// A random spawner managed by a [/datum/spawn_pool].
/obj/effect/spawner/random/pool
icon = 'icons/effects/random_spawners.dmi'
icon_state = "loot"

/// How much this spawner will subtract from the available budget if it
/// spawns. A value of `INFINITY` (i.e., not setting the value on a subtype)
/// does not attempt to subtract from the budget. This is useful for
/// spawners which themselves spawn other spawners.
var/point_value = INFINITY
/// Whether non-spawner items should be removed from the shared loot pool
/// after spawning.
var/unique_picks = FALSE
/// Guaranteed spawners will always proc, and always proc first.
var/guaranteed = FALSE
/// The ID of the spawn pool. Must match the pool's [/datum/spawn_pool/var/id].
var/spawn_pool_id

/obj/effect/spawner/random/pool/Initialize(mapload)
// short-circuit atom init machinery since we won't be around long
if(initialized)
stack_trace("Warning: [src]([type]) initialized multiple times!")
initialized = TRUE

if(!spawn_pool_id)
stack_trace("No spawn pool ID provided to [src]([type])")

if(GLOB.spawn_pool_manager.finalized)
// We've already gotten through SSlate_mapping, so someone probably spawned this manually.
// Skip all the shit and just spawn it.
spawn_loot()
qdel(src)
return

var/datum/spawn_pool/pool = GLOB.spawn_pool_manager.get(spawn_pool_id)
if(!pool)
stack_trace("Could not find spawn pool with ID [spawn_pool_id]")

if(unique_picks && !(type in pool.unique_spawners))
pool.unique_spawners[type] = loot.Copy()

if(guaranteed)
pool.guaranteed_spawners |= src
else
pool.known_spawners |= src

/obj/effect/spawner/random/pool/generate_loot_list()
var/datum/spawn_pool/pool = GLOB.spawn_pool_manager.get(spawn_pool_id)
if(!pool)
stack_trace("Could not find spawn pool with ID [spawn_pool_id]")

if(unique_picks)
var/list/unique_loot = pool.unique_spawners[type]
return unique_loot.Copy()

return ..()

/obj/effect/spawner/random/pool/check_safe(type_path_to_make)
// TODO: Spawners with `spawn_all_loot` set will subtract the
// point value for each item spawned. This needs to change so
// that the budget is only checked once initially, and then
// all of the loot is spawned after.
if(!..())
return FALSE

var/is_safe = FALSE
var/deduct_points = TRUE
var/datum/spawn_pool/pool = GLOB.spawn_pool_manager.get(spawn_pool_id)
if(!pool)
stack_trace("Could not find spawn pool with ID [spawn_pool_id]")

if(ispath(type_path_to_make, /obj/effect/spawner/random/pool))
return TRUE

// If we're past SSlate_mapping, we're safe and don't have a pool
// to deduct points from
if(GLOB.spawn_pool_manager.finalized)
is_safe = TRUE
deduct_points = FALSE

// If we don't have a sane point value, don't deduct points
if(point_value == INFINITY)
deduct_points = FALSE

// If we deduct points, we need to check affordability
if(deduct_points)
if(pool.can_afford(point_value))
is_safe = TRUE
else
is_safe = TRUE

// Early breakout if we're not safe
if(!is_safe)
return FALSE

if(deduct_points)
pool.consume(point_value)

if(pool && unique_picks)
// We may have multiple instances of a given type so just remove the first instance we find
var/list/unique_spawners = pool.unique_spawners[type]
unique_spawners.Remove(type_path_to_make)

return TRUE
59 changes: 59 additions & 0 deletions code/game/objects/effects/spawners/random/pool/spawn_pool.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// Keeps track of the available points for a given pool, as well as any
/// spawners that need to keep track globally of the number of any specific item
/// that they spawn.
/datum/spawn_pool
/// The ID of the spawn pool. All spawners registered to this pool must use this ID.
var/id
/// The number of points left for the spawner to use. Starts at its initial value.
var/available_points = 0
/// A list of all spawners registered to this pool.
var/list/known_spawners = list()
/// A key-value list of spawners with TRUE `unique_picks` to a shared copy of their
/// loot pool. When items from one of these spawners are spawned, it is removed
/// from the shared loot pool so it never spawns again.
var/list/unique_spawners = list()
/// A list of spawners whose `guaranteed` is `TRUE`. These spawners will
/// always spawn, and always before anything else,
var/list/guaranteed_spawners = list()

/datum/spawn_pool/proc/can_afford(points)
if(available_points >= points)
return TRUE

return FALSE

/datum/spawn_pool/proc/consume(points)
available_points -= points

/datum/spawn_pool/proc/process_guaranteed_spawners()
while(length(guaranteed_spawners))
var/obj/effect/spawner/random/pool/spawner = guaranteed_spawners[length(guaranteed_spawners)]
guaranteed_spawners.len--
spawner.spawn_loot()
qdel(spawner)

QDEL_LIST_CONTENTS(guaranteed_spawners)

/datum/spawn_pool/proc/process_spawners()
process_guaranteed_spawners()

shuffle_inplace(known_spawners)
while(length(known_spawners))
if(available_points <= 0)
break

var/obj/effect/spawner/random/pool/spawner = known_spawners[length(known_spawners)]
known_spawners.len--
if(spawner.point_value != INFINITY && available_points < spawner.point_value)
qdel(spawner)
continue

spawner.spawn_loot()
if(length(guaranteed_spawners))
WARNING("non-guaranteed spawner [spawner.type] spawned a guaranteed spawner, this should be avoided")
process_guaranteed_spawners()

qdel(spawner)


QDEL_LIST_CONTENTS(known_spawners)
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
GLOBAL_DATUM_INIT(spawn_pool_manager, /datum/spawn_pool_manager, new)

/// The singleton which keeps track of all spawn pools.
/// All known [/datum/spawn_pool] subtypes are registered
/// to it and are processed in SSlate_mapping.
/datum/spawn_pool_manager
var/list/spawn_pools = list()
var/finalized = FALSE

/datum/spawn_pool_manager/New()
for(var/spawn_pool_type in subtypesof(/datum/spawn_pool))
var/datum/spawn_pool/pool = new spawn_pool_type
spawn_pools[pool.id] = pool

/datum/spawn_pool_manager/proc/get(id)
return spawn_pools[id]

/datum/spawn_pool_manager/proc/process_pools()
for(var/pool_id in spawn_pools)
var/datum/spawn_pool/pool = spawn_pools[pool_id]
pool.process_spawners()

finalized = TRUE

/datum/spawn_pool_manager/Destroy()
QDEL_LIST_CONTENTS(spawn_pools)
return ..()
38 changes: 29 additions & 9 deletions code/game/objects/effects/spawners/random/random_spawner.dm
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@
spawn_loot()
return INITIALIZE_HINT_QDEL

/obj/effect/spawner/random/proc/generate_loot_list()
if(loot_type_path)
loot += typesof(loot_type_path)

if(loot_subtype_path)
loot += subtypesof(loot_subtype_path)

return loot

/obj/effect/spawner/random/proc/check_safe(type_path_to_make)
return TRUE

///If the spawner has any loot defined, randomly picks some and spawns it. Does not cleanup the spawner.
/obj/effect/spawner/random/proc/spawn_loot(lootcount_override)
if(!prob(spawn_loot_chance))
Expand All @@ -66,21 +78,22 @@
spawn_loot_count = INFINITY
spawn_loot_double = FALSE

if(loot_type_path)
loot += typesof(loot_type_path)

if(loot_subtype_path)
loot += subtypesof(loot_subtype_path)
var/list/loot_list = generate_loot_list()
var/safe_failure_count = 0

if(length(loot))
if(length(loot_list))
var/loot_spawned = 0
var/pixel_divider = FLOOR(spawn_random_offset_max_pixels / spawn_loot_split_pixel_offsets, 1)
while((spawn_loot_count-loot_spawned) && length(loot))
while((spawn_loot_count-loot_spawned) && length(loot_list) && safe_failure_count <= 10)
loot_spawned++
var/lootspawn = pick_weight_recursive(loot_list)

if(!check_safe(lootspawn))
safe_failure_count++
continue

var/lootspawn = pick_weight_recursive(loot)
if(!spawn_loot_double)
loot.Remove(lootspawn)
loot_list.Remove(lootspawn)
if(lootspawn)
var/turf/spawn_loc = loc
if(spawn_scatter_radius > 0 && length(spawn_locations))
Expand All @@ -91,6 +104,13 @@
continue

var/atom/movable/spawned_loot = make_item(spawn_loc, lootspawn)

// If we make something that then makes something else and gets itself
// qdel'd, we'll have a null result here. This doesn't necessarily mean
// that nothing's been spawned, so it's not necessarily a failure.
if(!spawned_loot)
continue

spawned_loot.setDir(dir)

if(!spawn_loot_split && !spawn_random_offset)
Expand Down
Loading

0 comments on commit c4e4487

Please sign in to comment.