mirror of
https://github.com/Citadel-Station-13/Citadel-Station-13-RP.git
synced 2026-08-20 17:56:37 +01:00
467 lines
16 KiB
Plaintext
467 lines
16 KiB
Plaintext
//* This file is explicitly licensed under the MIT license. *//
|
|
//* Copyright (c) 2024 Citadel Station Developers *//
|
|
|
|
/**
|
|
* The movement manager for turf movements
|
|
*
|
|
* This is named after SS14's grids.
|
|
* This is a subsystem for OOP/encapsulation reasons.
|
|
*/
|
|
SUBSYSTEM_DEF(grids)
|
|
name = "Grids"
|
|
subsystem_flags = SS_NO_FIRE | SS_NO_INIT
|
|
|
|
/// global motion mutex
|
|
var/translation_mutex = FALSE
|
|
|
|
/**
|
|
* gets ordered turfs for operation
|
|
*
|
|
* x2 must be the high value
|
|
* y2 must be the high value
|
|
*
|
|
* We always sweep left to right, top to bottom, from the relative perspective of
|
|
* looking towards the front of the shuttle's direction from the back.
|
|
*
|
|
* e.g. when NORTH, it's left to right, one line at a time from front to back.
|
|
*
|
|
* * If we are out of bounds of a level, we will end up emitting a list with nulls where there are no turfs!
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/get_ordered_turfs(x1, x2, y1, y2, z, dir)
|
|
ASSERT(x2 >= x1)
|
|
ASSERT(y2 >= y1)
|
|
. = list()
|
|
switch(dir)
|
|
if(NORTH)
|
|
for(var/y in y2 to y1 step -1)
|
|
for(var/x in x1 to x2 step 1)
|
|
. += locate(x, y, z)
|
|
if(SOUTH)
|
|
for(var/y in y1 to y2 step 1)
|
|
for(var/x in x2 to x1 step -1)
|
|
. += locate(x, y, z)
|
|
if(EAST)
|
|
for(var/x in x2 to x1 step -1)
|
|
for(var/y in y2 to y1 step -1)
|
|
. += locate(x, y, z)
|
|
if(WEST)
|
|
for(var/x in x1 to x2 step 1)
|
|
for(var/y in y1 to y2 step 1)
|
|
. += locate(x, y, z)
|
|
|
|
/datum/controller/subsystem/grids/proc/rotation_angle(from_dir, to_dir)
|
|
return (angle_to_dir(to_dir) - angle_to_dir(from_dir))
|
|
|
|
/datum/controller/subsystem/grids/proc/angle_to_dir(dir)
|
|
switch(dir)
|
|
if(NORTH)
|
|
return 0
|
|
if(SOUTH)
|
|
return 180
|
|
if(EAST)
|
|
return 270
|
|
if(WEST)
|
|
return 90
|
|
else
|
|
CRASH("non-cardinal")
|
|
|
|
/**
|
|
* Taking ordered 'from' and 'to' lists, nulls out any entries that shouldn't be moved.
|
|
*
|
|
* * turfs can contain nulls
|
|
* * input turf lists are edited
|
|
* * area_cache must have truthy associations.
|
|
* * the same index in from_turfs that are nulled are nulled in to_turfs
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/null_filter_translation_ordered_turfs_in_place_via_area(list/area/area_cache, list/turf/from_turfs, list/turf/to_turfs)
|
|
ASSERT(length(from_turfs) == length(to_turfs))
|
|
for(var/i in 1 to length(from_turfs))
|
|
var/turf/T = from_turfs[i]
|
|
if(area_cache[T.loc])
|
|
continue
|
|
from_turfs[i] = null
|
|
to_turfs[i] = null
|
|
|
|
/**
|
|
* Taking ordered turfs as a list, nulls out any entries that isn't in the areas given
|
|
*
|
|
* * turfs can contain nulls
|
|
* * input turf list is edited
|
|
* * area_cache must have truthy associations.
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/null_filter_ordered_turfs_in_place_via_area(list/area/area_cache, list/turf/ordered_turfs)
|
|
for(var/i in 1 to length(ordered_turfs))
|
|
var/turf/T = ordered_turfs[i]
|
|
if(area_cache[T.loc])
|
|
continue
|
|
ordered_turfs[i] = null
|
|
|
|
/**
|
|
* Taking ordered 'from' and 'to' lists, erases any entries that shouldn't be moved.
|
|
* preserves relative order
|
|
*
|
|
* * turfs can contain nulls
|
|
* * input turf lists are edited
|
|
* * area_cache must have truthy associations.
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/filter_ordered_turfs_via_area(list/area/area_cache, list/turf/ordered_turfs)
|
|
. = list()
|
|
for(var/turf/T in ordered_turfs)
|
|
if(!area_cache[T.loc])
|
|
continue
|
|
. += T
|
|
|
|
/**
|
|
* performs turf translation
|
|
*
|
|
* baseturf boundary is important if you do not want things being ripped out of the ground.
|
|
* without boundary set, turfs will be completely scraped down to their bottom baseturfs,
|
|
* and destination turfs will have their baseturf stacks trampled by this.
|
|
*
|
|
* * from_turfs and to_turfs can have nulls, as long as they're in the same order.
|
|
* * said null behavior is intentional, so that shuttles can easily perform turf filtering.
|
|
*
|
|
* @params
|
|
* * from_turfs - get_ordered_turfs return list
|
|
* * from_dir - dir of from_turfs
|
|
* * to_turfs - get_ordered_turfs return list
|
|
* * to_dir - dir of to_turfs
|
|
* * grid_flags - flags to pass during move to motion procs
|
|
* * baseturf_boundary - if set, turfs move down to this baseturf boundary. if it's not there, the turf is automatically skipped.
|
|
* * leave_area - the area instance to leave behind. if not set, this defaults to world.area. this can be a typepath if the typepath is an unique area.
|
|
* * emit_motion_flags - use this to extract ordered motion flags
|
|
* * emit_moved_atoms - use this to extract what movables got moved
|
|
* * turf_overlap_handler - bound proc that's fired for things in the way with (from_turf, to_turf); things: turfs. ATOM_ABSTRACT and ATOM_NONWORLD are ignored. Called before `movable_overlap_handler`.
|
|
* * movable_overlap_handler - bound proc that's fired for things in the way with (thing, from_turf, to_turf); things: objs, mobs. ATOM_ABSTRACT and ATOM_NONWORLD are ignored.
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/translate(list/from_turfs, list/to_turfs, from_dir, to_dir, grid_flags, baseturf_boundary, area/leave_area, list/emit_motion_flags = list(), list/emit_moved_atoms = list(), datum/bound_proc/turf_overlap_handler, datum/bound_proc/movable_overlap_handler)
|
|
UNTIL(!translation_mutex)
|
|
translation_mutex = TRUE
|
|
. = do_translate(arglist(args))
|
|
translation_mutex = FALSE
|
|
|
|
/datum/controller/subsystem/grids/proc/do_translate(list/from_turfs, list/to_turfs, from_dir, to_dir, grid_flags, baseturf_boundary, area/leave_area, list/emit_motion_flags, list/emit_moved_atoms, datum/bound_proc/turf_overlap_handler, datum/bound_proc/movable_overlap_handler)
|
|
PRIVATE_PROC(TRUE)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
// While based on /tg/'s movement system, we do a few things differently.
|
|
// First, limitations:
|
|
// * base-areas aren't a thing. Areas are flat out trampled on move. On takeoff, areas are reset.
|
|
// * Turfs are assumed to be entirely described by baseturfs. So, flooring's just trampled too.
|
|
// * Areas may be left behind if a bounding box doesn't completely envelop an area. There's not many ways to fix this, so make sure your bounding boxes do.
|
|
// The actual process:
|
|
// * Collect turfs, with order of opinions being area -> turf -> movable
|
|
// * Move areas to their new turfs all at once
|
|
// * Move turfs one by one, calling turf --> movable overlap handlers on that turf if needed.
|
|
// * Move movables to their new turfs
|
|
// * Proc grid_after() on all areas -> turfs -> movables in these stages
|
|
// * Cleanup areas from their old turfs all at once
|
|
// * Cleanup turfs one by onew
|
|
// * Proc grid_finished() on all registered movables
|
|
// Caveats / Pointers:
|
|
// * rotation_angle is in turn() angles.
|
|
// * none of these are tick checked right now. do not sleep,
|
|
// * do not do anything expensive, async your calls or kick them elsewhere if they're expensive!
|
|
|
|
. = FALSE
|
|
|
|
ASSERT(length(from_turfs) == length(to_turfs))
|
|
|
|
/// motion flags corrosponding to ordered turfs. this is ordered. null turf --> null.
|
|
var/list/ordered_motion_flags = emit_motion_flags
|
|
ordered_motion_flags.len = length(from_turfs)
|
|
/// list of area instances associated to turfs being moved from
|
|
var/list/source_turfs_by_area = list()
|
|
/// list of area instances associated to turfs being moved to
|
|
var/list/destination_turfs_by_area = list()
|
|
/// things moved
|
|
var/list/atom/movable/moved = emit_moved_atoms
|
|
/// for things that need a late / extra stage. these get grid_finished() called on it after everything
|
|
var/list/atom/movable/late_callers = list()
|
|
/// calculate rotation angle
|
|
var/rotation_angle = rotation_angle(from_dir, to_dir)
|
|
|
|
//* Collect
|
|
for(var/i in 1 to length(from_turfs))
|
|
var/turf/source = from_turfs[i]
|
|
if(isnull(source))
|
|
continue
|
|
// if not null, we assume to_turfs is there.
|
|
var/turf/destination = to_turfs[i]
|
|
// we assume all turfs have areas
|
|
var/area/source_area = source.loc
|
|
// ask area -> turf -> movable
|
|
// movable have the capability to 'insist' on moving something that is otherwise not supposed to move
|
|
var/motion_flags = source_area.grid_collect(grid_flags, source, destination, baseturf_boundary)
|
|
motion_flags = source.grid_collect(grid_flags, destination, baseturf_boundary, motion_flags)
|
|
for(var/atom/movable/AM as anything in source)
|
|
// no abstract check - abstract atoms can impact the collect cycle
|
|
motion_flags = AM.grid_collect(grid_flags, destination, motion_flags)
|
|
// add to ordered list
|
|
ordered_motion_flags[i] = motion_flags
|
|
// if moving area, add to turfs_by_area
|
|
if(motion_flags & GRID_MOVE_AREA)
|
|
if(isnull(source_turfs_by_area[source_area]))
|
|
source_turfs_by_area[source_area] = list(source)
|
|
destination_turfs_by_area[source_area] = list(destination)
|
|
else
|
|
source_turfs_by_area[source_area] += source
|
|
destination_turfs_by_area[source_area] += destination
|
|
|
|
//* Transfer areas
|
|
for(var/area/A as anything in source_turfs_by_area)
|
|
A.grid_transfer(grid_flags, source_turfs_by_area[A], destination_turfs_by_area[A], baseturf_boundary)
|
|
|
|
//* Transfer turfs
|
|
for(var/i in 1 to length(from_turfs))
|
|
var/turf/source = from_turfs[i]
|
|
if(isnull(source))
|
|
continue
|
|
if(!(ordered_motion_flags[i] & GRID_MOVE_TURF))
|
|
continue
|
|
var/turf/destination = to_turfs[i]
|
|
// -- fire overlap handlers --
|
|
turf_overlap_handler?.invoke_async(source, destination)
|
|
for(var/atom/movable/AM as anything in destination)
|
|
if(AM.atom_flags & (ATOM_NONWORLD | ATOM_ABSTRACT))
|
|
continue
|
|
if(AM.handle_grid_overlap(grid_flags))
|
|
continue
|
|
movable_overlap_handler?.invoke_async(AM, source, destination)
|
|
// -- end --
|
|
source.grid_transfer(grid_flags, destination, baseturf_boundary)
|
|
|
|
//* Move movables
|
|
for(var/i in 1 to length(from_turfs))
|
|
var/turf/source = from_turfs[i]
|
|
if(isnull(source))
|
|
continue
|
|
if(!(ordered_motion_flags[i] & GRID_MOVE_MOVABLES))
|
|
continue
|
|
var/turf/destination = to_turfs[i]
|
|
for(var/atom/movable/AM as anything in source)
|
|
if(AM.atom_flags & ATOM_ABSTRACT) // don't move
|
|
continue
|
|
if(AM.loc != source) // multi tile object check
|
|
continue
|
|
AM.grid_move(grid_flags, destination)
|
|
moved += AM
|
|
|
|
//* Moved - areas
|
|
for(var/area/A as anything in source_turfs_by_area)
|
|
A.grid_after(grid_flags, source_turfs_by_area[A], destination_turfs_by_area[A], baseturf_boundary)
|
|
|
|
//* Moved - turfs
|
|
for(var/i in 1 to length(to_turfs))
|
|
var/turf/destination = to_turfs[i]
|
|
if(isnull(destination))
|
|
continue
|
|
if(!(ordered_motion_flags[i] & GRID_MOVE_TURF))
|
|
continue
|
|
destination.grid_after(grid_flags, rotation_angle)
|
|
|
|
//* Moved - movables
|
|
for(var/atom/movable/AM as anything in moved)
|
|
if(QDELETED(AM))
|
|
continue
|
|
AM.grid_after(grid_flags, rotation_angle, late_callers)
|
|
|
|
//* Late - movables
|
|
for(var/atom/movable/AM as anything in late_callers)
|
|
if(QDELETED(AM))
|
|
continue
|
|
AM.grid_finished(grid_flags, rotation_angle)
|
|
|
|
//* Cleanup areas
|
|
for(var/area/A as anything in source_turfs_by_area)
|
|
A.grid_clean(grid_flags, source_turfs_by_area[A], destination_turfs_by_area[A], baseturf_boundary, leave_area)
|
|
|
|
//* Cleanup turfs
|
|
for(var/i in 1 to length(from_turfs))
|
|
var/turf/source = from_turfs[i]
|
|
if(isnull(source))
|
|
continue
|
|
if(!(ordered_motion_flags[i] & GRID_MOVE_TURF))
|
|
continue
|
|
source.grid_clean(grid_flags, baseturf_boundary)
|
|
|
|
return TRUE
|
|
|
|
//* Debug Functions *//
|
|
|
|
/**
|
|
* source/target specifiers can be turfs or lists
|
|
*/
|
|
/datum/controller/subsystem/grids/proc/debug_yeet_a_chunk(source_lower_left, source_top_right, target_lower_left, target_top_right, turn_angle = 0)
|
|
var/list/S_LL
|
|
var/list/S_TR
|
|
var/list/T_LL
|
|
var/list/T_TR
|
|
|
|
if(isturf(source_lower_left))
|
|
S_LL = list(source_lower_left:x, source_lower_left:y, source_lower_left:z)
|
|
else
|
|
S_LL = source_lower_left
|
|
if(isturf(source_top_right))
|
|
S_TR = list(source_top_right:x, source_top_right:y, source_top_right:z)
|
|
else
|
|
S_TR = source_top_right
|
|
if(isturf(target_lower_left))
|
|
T_LL = list(target_lower_left:x, target_lower_left:y, target_lower_left:z)
|
|
else
|
|
T_LL = target_lower_left
|
|
if(isturf(target_top_right))
|
|
T_TR = list(target_top_right:x, target_top_right:y, target_top_right:z)
|
|
else
|
|
T_TR = target_top_right
|
|
|
|
var/list/from_turfs = get_ordered_turfs(S_LL[1], S_TR[1], S_LL[2], S_TR[2], S_LL[3], SOUTH)
|
|
var/list/to_turfs = get_ordered_turfs(T_LL[1], T_TR[1], T_LL[2], T_TR[2], T_LL[3], turn(SOUTH, turn_angle))
|
|
|
|
ASSERT(length(from_turfs) == length(to_turfs))
|
|
|
|
translate(from_turfs, to_turfs, SOUTH, turn(SOUTH, turn_angle))
|
|
|
|
//* Areas
|
|
|
|
/**
|
|
* Called when collecting filtered turfs to move
|
|
*
|
|
* @return motion flags
|
|
*/
|
|
/area/proc/grid_collect(grid_flags, turf/old_turf, turf/new_turf, baseturf_boundary)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
return GRID_MOVE_AREA
|
|
|
|
/**
|
|
* Called when copying area to new turfs
|
|
*/
|
|
/area/proc/grid_transfer(grid_flags, list/turf/old_turfs, list/turf/new_turfs, baseturf_boundary)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
take_turfs(new_turfs)
|
|
|
|
/**
|
|
* Called when cleaning up after transfer
|
|
*/
|
|
/area/proc/grid_clean(grid_flags, list/turf/old_turfs, list/turf/new_turfs, baseturf_boundary, area/leave_area)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
// contents -= old_turfs
|
|
if(ispath(leave_area))
|
|
leave_area = dynamic_area_of_type(leave_area)
|
|
else if(istype(leave_area))
|
|
else
|
|
leave_area = dynamic_area_of_type(world.area)
|
|
if(isnull(leave_area))
|
|
leave_area = new /area/grid_orphaned
|
|
leave_area.take_turfs(old_turfs)
|
|
|
|
/**
|
|
* Called after everything is moved
|
|
*/
|
|
/area/proc/grid_after(grid_flags, list/turf/old_turfs, list/turf/new_turfs, baseturf_boundary)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
return
|
|
|
|
//* Turfs
|
|
|
|
/**
|
|
* Called when collecting filtered turfs to move
|
|
*
|
|
* @return motion flags
|
|
*/
|
|
/turf/proc/grid_collect(grid_flags, turf/new_turf, baseturf_boundary, area_opinion)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(isnull(baseturf_boundary))
|
|
return area_opinion | GRID_MOVE_TURF | GRID_MOVE_MOVABLES
|
|
if(baseturf_boundary in baseturfs)
|
|
return area_opinion | GRID_MOVE_TURF | GRID_MOVE_MOVABLES
|
|
return area_opinion
|
|
|
|
/**
|
|
* Called when copying over to new turf
|
|
* Only called if moved
|
|
*/
|
|
/turf/proc/grid_transfer(grid_flags, turf/new_turf, baseturf_boundary)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(isnull(baseturf_boundary))
|
|
new_turf.CopyOnTop(src, null, null, CHANGETURF_INHERIT_AIR)
|
|
else
|
|
new_turf.CopyOnTop(src, 1, length(baseturfs) - baseturfs.Find(baseturf_boundary) + 1, CHANGETURF_INHERIT_AIR)
|
|
|
|
/**
|
|
* Called when cleaning up after transfer
|
|
* Only called if moved
|
|
*/
|
|
/turf/proc/grid_clean(grid_flags, baseturf_boundary)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(isnull(baseturf_boundary))
|
|
// tear to the bottom
|
|
ChangeTurf(baseturf_bottom(), /turf/baseturf_bottom)
|
|
else
|
|
// tear down to below boundary
|
|
var/tear_to_index = baseturfs.Find(baseturf_boundary)
|
|
ScrapeAway(length(baseturfs) - tear_to_index + 1)
|
|
|
|
/**
|
|
* Called after everything settles
|
|
*/
|
|
/turf/proc/grid_after(grid_flags, rotation_angle)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(rotation_angle != 0)
|
|
setDir(turn(dir, rotation_angle))
|
|
|
|
//* Movables
|
|
|
|
/**
|
|
* Called when collecting filtered turfs to move
|
|
*
|
|
* @return motion flags
|
|
*/
|
|
/atom/movable/proc/grid_collect(grid_flags, turf/new_turf, loc_opinion)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
return loc_opinion
|
|
|
|
/**
|
|
* Called when moving to new position
|
|
* Only called if moved
|
|
*/
|
|
/atom/movable/proc/grid_move(grid_flags, turf/new_turf)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
abstract_move(new_turf)
|
|
|
|
/**
|
|
* Called after everything settles, after area/turf moved
|
|
* Only called if moved
|
|
*/
|
|
/atom/movable/proc/grid_after(grid_flags, rotation_angle, list/late_call_hooks)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
if(rotation_angle != 0)
|
|
setDir(turn(dir, rotation_angle))
|
|
|
|
/**
|
|
* Called if we got added to late_call_hooks in grid_after.
|
|
*/
|
|
/atom/movable/proc/grid_finished(grid_flags, rotation_angle)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
return
|
|
|
|
/**
|
|
* called when we get overlapped by a grid
|
|
*
|
|
* we can be overlapped if non-ABSTRACT.
|
|
*
|
|
* @return TRUE to override overlap_handler callback.
|
|
*/
|
|
/atom/movable/proc/handle_grid_overlap(grid_flags)
|
|
SHOULD_NOT_SLEEP(TRUE)
|
|
return FALSE
|
|
|
|
//* grid area left behind if a grid move is not given an area to leave *//
|
|
|
|
/area/grid_orphaned
|
|
name = "orphaned grid area"
|
|
desc = "someone fucked up"
|
|
icon = 'icons/turf/area/debug.dmi'
|
|
icon_state = "grid-orphan"
|
|
plane = DEBUG_PLANE
|
|
layer = DEBUG_LAYER_AREA_OVERLAYS
|