Files
Paradise/code/datums/components/orbiter.dm
warriorstar-orion 79bad427c8 Movement cross/uncross implementation. (#26762)
* refactor: Movement cross/uncross implementation.

* wrong var name

* fix unit tests dropping PDAs into nowhere

* Add documentation.

* remove unused constants

* say which procs are off limits

* fix simpleanimal z change runtime

* helps not to leave merge conflicts

* kill me

* fix typecast

* fix projectile/table collision

* treadmills don't cause MC to crash anymore

* connect_loc is appropriate here

* fix windoors and teleporters

* fix bonfires and clarify docs

* fix proximity sensors

Tested with sensors in crates, sensors in modsuits
Tested new proximity component with firing projectiles at singularity
Tested new proximity component with portable flashes
Tested new proximity component with facehuggers

* lint

* fix: polarized access helper false positives

* Revert "fix: polarized access helper false positives"

This reverts commit 9814f98cf6.

* hopefully the right change for mindflayer steam

* Changes following cameras

* fix glass table collision

* appears to fix doorspam

* fix ore bags not picking up ore

* fix signatures of /Exited

* remove debug log

* remove duplicate signal registrar

* fix emptying bags into locations

* I don't trust these nested Move calls

* use connect_loc for upgraded resonator fields

* use moveToNullspace

* fix spiderweb crossing

* fix pass checking for windows from a tile off

* fix bluespace closet/transparency issues

* fix mechs not interacting with doors and probably other things

* fix debug

* fix telepete

* add some docs

* stop trying to shoehorn prox monitor into cards

* I should make sure things build

* kill override signal warning

* undef signal

* not many prox monitors survive going off like this

* small fixes to storage

* make moving wormholes respect signals

* use correct signals for pulse demon

* fix pulse heart too

* fix smoke signals

* may have fucked singulo projectile swerve

* fix singulo projectile arcing

* remove duplicate define

* just look at it

* hopefully last cleanups of incorrect signal usage

* fix squeaking

* may god have mercy on my soul

* Apply suggestions from code review

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

* lewc review

* Apply suggestions from code review

Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

* burza review

* fix bad args for grenade assemblies

* Update code/__DEFINES/is_helpers.dm

Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

---------

Signed-off-by: warriorstar-orion <orion@snowfrost.garden>
Co-authored-by: DGamerL <daan.lyklema@gmail.com>
Co-authored-by: Luc <89928798+lewcc@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
2024-12-21 08:07:44 +00:00

477 lines
18 KiB
Plaintext

/**
* Code to handle atoms orbiting other atoms, following them as they move.
* The basic logic is simple. We register a signal, COMSIG_MOVABLE_MOVED onto orbited atoms.
* When the orbited atom moves, any ghosts orbiting them are moved to their turf.
* We also register a MOVED signal onto the ghosts to cancel their orbit if they move themselves.
* Complexities come in for items within other items (such as the NAD in a box in a backpack on an assistant pretending to be the captain),
* as items in containers do **not** fire COMSIG_MOVABLE_MOVED when their container moves.
*
* The signal logic for items in containers is as follows:
* Assume 1 is some item (for example, the NAD) and 2 is a box.
* When 1 is added to 2, we register the typical orbit COMSIG_MOVABLE_MOVED onto 2.
* This in essence makes 2 the "leader", the atom that ghosts follow in movement.
* As 2 is moved around (say, dragged on the floor) ghosts will follow it.
* We also register a new intermediate COMSIG_MOVABLE_MOVED signal onto 1 that tracks if 1 is moved.
* Remember, this will only fire if 1 is moved around containers, since it's impossible for it to actually move on its own.
* If 1 is moved out of 2, this signal makes 1 the new leader.
* Lastly, we add a COMSIG_ATOM_EXITED to 2, which tracks if 1 is removed from 2.
* This EXITED signal cleans up any orbiting signals on and above 2.
* If 2 is added to another item, (say a backpack, 3)
* 3 becomes the new leader
* 2 becomes an intermediate
* 1 is unchanged (but still carries the orbiter datum)
*
*
* You may be asking yourself: is this overengineered?
* In part, yes. However, MOVED signals don't get fired for items in containers, so this is
* really the next best way.
* Also, is this really optimal?
* As much as it can be, I believe. This signal-shuffling will not happen for any item that is just moving from turf to turf,
* which should apply to 95% of cases (read: ghosts orbiting mobs).
*/
#define ORBIT_LOCK_IN (1<<0)
#define ORBIT_FORCE_MOVE (1<<1)
/datum/component/orbiter
can_transfer = TRUE
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// List of observers orbiting the parent
var/list/orbiter_list
/// Cached transforms from before the orbiter started orbiting, to be restored on stopping their orbit
var/list/orbit_data
/// See atom/movable/proc/orbit for parameter definitions
/datum/component/orbiter/Initialize(atom/movable/orbiter, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE, lock_in_orbit = FALSE, force_move = FALSE, orbit_layer = FLY_LAYER)
if(!istype(orbiter) || !isatom(parent) || isarea(parent))
return COMPONENT_INCOMPATIBLE
orbiter_list = list()
orbit_data = list()
begin_orbit(orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, lock_in_orbit, force_move, orbit_layer)
/datum/component/orbiter/RegisterWithParent()
var/atom/target = parent
register_signals(target)
/datum/component/orbiter/UnregisterFromParent()
var/atom/target = parent
remove_signals(target)
/datum/component/orbiter/Destroy()
remove_signals(parent)
for(var/i in orbiter_list)
end_orbit(i)
orbiter_list = null
orbit_data = null
return ..()
/// See atom/movable/proc/orbit for parameter definitions
/datum/component/orbiter/InheritComponent(datum/component/orbiter/new_comp, original, atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, lock_in_orbit, force_move, orbit_layer)
// No transfer happening
if(!new_comp)
begin_orbit(arglist(args.Copy(3)))
return
orbiter_list += new_comp.orbiter_list
orbit_data += new_comp.orbit_data
new_comp.orbiter_list = list()
new_comp.orbit_data = list()
QDEL_NULL(new_comp)
/datum/component/orbiter/PostTransfer()
if(!isatom(parent) || isarea(parent) || !get_turf(parent))
return COMPONENT_INCOMPATIBLE
/// See atom/movable/proc/orbit for parameter definitions
/datum/component/orbiter/proc/begin_orbit(atom/movable/orbiter, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, lock_in_orbit, force_move, orbit_layer)
if(!istype(orbiter))
return
var/was_refreshing = FALSE
var/atom/currently_orbiting = locateUID(orbiter.orbiting_uid)
if(currently_orbiting)
if(orbiter in orbiter_list)
was_refreshing = TRUE
end_orbit(orbiter, TRUE)
else
var/datum/component/orbiter/orbit_comp = currently_orbiting.GetComponent(/datum/component/orbiter)
orbit_comp.end_orbit(orbiter)
orbiter_list += orbiter
// make sure orbits get cleaned up nicely if the parent qdels
RegisterSignal(orbiter, COMSIG_PARENT_QDELETING, PROC_REF(end_orbit))
var/orbit_flags = 0
if(lock_in_orbit)
orbit_flags |= ORBIT_LOCK_IN
if(force_move)
orbit_flags |= ORBIT_FORCE_MOVE
orbiter.orbiting_uid = parent.UID()
store_orbit_data(orbiter, orbit_flags)
if(!lock_in_orbit)
RegisterSignal(orbiter, COMSIG_MOVABLE_MOVED, PROC_REF(orbiter_move_react))
// Head first!
if(pre_rotation)
var/matrix/M = matrix(orbiter.transform)
var/pre_rot = 90
if(!clockwise)
pre_rot = -90
M.Turn(pre_rot)
orbiter.transform = M
var/matrix/shift = matrix(orbiter.transform)
shift.Translate(0,radius)
orbiter.transform = shift
orbiter.layer = orbit_layer
SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_BEGIN, orbiter)
// If we changed orbits, we didn't stop our rotation, and don't need to start it up again
if(!was_refreshing)
orbiter.SpinAnimation(rotation_speed, -1, clockwise, rotation_segments, parallel = FALSE)
var/target_loc = get_turf(parent)
var/current_loc = orbiter.loc
if(force_move)
orbiter.forceMove(target_loc)
else
orbiter.loc = target_loc
// Setting loc directly doesn't fire COMSIG_MOVABLE_MOVED, so we need to do it ourselves
SEND_SIGNAL(orbiter, COMSIG_MOVABLE_MOVED, current_loc, target_loc, null)
orbiter.animate_movement = SYNC_STEPS
/**
* End the orbit and clean up our transformation.
* If this removes the last atom orbiting us, then qdel ourselves.
* Howver, if refreshing == TRUE, src will not be qdeleted if this leaves us with 0 orbiters.
*/
/datum/component/orbiter/proc/end_orbit(atom/movable/orbiter, refreshing = FALSE)
SIGNAL_HANDLER
if(!(orbiter in orbiter_list))
return
if(orbiter)
orbiter.animate_movement = SLIDE_STEPS
if(!QDELETED(parent))
SEND_SIGNAL(parent, COMSIG_ATOM_ORBIT_STOP, orbiter)
SEND_SIGNAL(orbiter, COMSIG_ATOM_ORBITER_STOP, parent)
orbiter.transform = get_cached_transform(orbiter)
orbiter.layer = get_orbiter_layer(orbiter)
UnregisterSignal(orbiter, COMSIG_MOVABLE_MOVED)
UnregisterSignal(orbiter, COMSIG_PARENT_QDELETING)
orbiter.orbiting_uid = null
if(!refreshing)
orbiter.SpinAnimation(0, 0, parallel = FALSE)
// If it's null, still remove it from the list
orbiter_list -= orbiter
orbit_data -= orbiter
if(!length(orbiter_list) && !QDELING(src) && !refreshing)
qdel(src)
/**
* The actual implementation function of the move react.
* **if you're trying to call this from a signal, call parent_move_react instead.**
* This implementation is separate so the orbited atom's old location and new location can be passed in separately.
*/
/datum/component/orbiter/proc/handle_parent_move(atom/movable/orbited, atom/old_loc, atom/new_loc, direction)
if(new_loc == old_loc)
return
var/turf/new_turf = get_turf(new_loc)
if(!new_turf)
// don't follow someone to nullspace
qdel(src)
var/atom/cur_loc = new_loc
// If something's only moving between turfs, don't bother changing signals, just move ghosts.
// Honestly, that should apply to 95% of orbiting cases, which should be a nice optimization.
if(!(isturf(old_loc) && isturf(new_loc)))
// Clear any signals that may still exist upstream of where this object used to be
remove_signals(old_loc)
// ...and create a new signal hierarchy upstream of where the object is now.
// cur_loc is the current "leader" atom
cur_loc = register_signals(orbited)
var/orbit_params
var/orbiter_turf
new_turf = get_turf(cur_loc)
for(var/atom/movable/movable_orbiter in orbiter_list)
orbiter_turf = get_turf(movable_orbiter)
if(QDELETED(movable_orbiter) || orbiter_turf == new_turf)
continue
orbit_params = get_orbit_params(movable_orbiter)
if(orbit_params & ORBIT_FORCE_MOVE)
movable_orbiter.forceMove(new_turf)
else
var/orbiter_loc = movable_orbiter.loc
movable_orbiter.loc = new_turf
SEND_SIGNAL(movable_orbiter, COMSIG_MOVABLE_MOVED, orbiter_loc, new_turf, null)
if(CHECK_TICK && new_turf != get_turf(movable_orbiter))
// We moved again during the checktick, cancel current operation
break
/**
* Signal handler for COMSIG_MOVABLE_MOVED. Special wrapper to handle the arguments that come from the signal.
* If you want to call this directly, just call handle_parent_move.
*/
/datum/component/orbiter/proc/parent_move_react(atom/movable/orbited, atom/old_loc, direction)
set waitfor = FALSE // Transfer calls this directly and it doesnt care if the ghosts arent done moving
handle_parent_move(orbited, old_loc, orbited.loc, direction)
/**
* Called when the orbiter themselves moves.
*/
/datum/component/orbiter/proc/orbiter_move_react(atom/movable/orbiter, atom/oldloc, direction)
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
if(get_turf(orbiter) == get_turf(parent) || get_turf(orbiter) == get_turf(oldloc) || get_turf(orbiter) == oldloc || orbiter.loc == oldloc)
return
if(orbiter in orbiter_list)
// Only end the spin animation when we're actually ending an orbit, not just changing targets
orbiter.SpinAnimation(0, 0, parallel = FALSE)
end_orbit(orbiter)
/**
* Remove all orbit-related signals in the object hierarchy above start.
*/
/datum/component/orbiter/proc/remove_signals(atom/start)
var/atom/cur_atom = start
while(cur_atom && !isturf(cur_atom) && !(cur_atom in orbiter_list) && cur_atom != parent)
UnregisterSignal(cur_atom, COMSIG_ATOM_EXITED)
UnregisterSignal(cur_atom, COMSIG_MOVABLE_MOVED)
cur_atom = cur_atom.loc
/**
* Register signals up the hierarchy, adding them to each parent (and their parent, and so on) up to the turf they're on.
* The last atom in the hierarchy (the one whose .loc is the turf) becomes the leader, the atom ghosts will follow.
* Registers on_intermediate_move for every non-leader atom so that if they move (removing them from the hierarchy), they will be set as the new leader.
* Also registers on_remove_child to remove signals up the hierarchy when a child gets removed.
* start: the first atom to register signals on. If start isn't inside of anything (or if its .loc is a turf), then it will become the leader.
* Returns the new "leader", the atom that ghosts will follow.
*/
/datum/component/orbiter/proc/register_signals(atom/start)
if(isturf(start))
return
var/atom/cur_atom = start
while(cur_atom.loc && !isturf(cur_atom.loc) && !(cur_atom.loc in orbiter_list))
RegisterSignal(cur_atom, COMSIG_MOVABLE_MOVED, PROC_REF(on_intermediate_move), TRUE)
RegisterSignal(cur_atom, COMSIG_ATOM_EXITED, PROC_REF(on_remove_child), TRUE)
cur_atom = cur_atom.loc
// Set the topmost atom (right before the turf) to be our new leader
RegisterSignal(cur_atom, COMSIG_MOVABLE_MOVED, PROC_REF(parent_move_react), TRUE)
RegisterSignal(cur_atom, COMSIG_ATOM_EXITED, PROC_REF(on_remove_child), TRUE)
return cur_atom
/**
* Callback fired when an item is removed from a tracked atom.
* Removes all orbit-related signals up its hierarchy and moves orbiters to the current child.
* As this will never be called by a turf, this should not conflict with parent_move_react.
*/
/datum/component/orbiter/proc/on_remove_child(datum/source, atom/movable/exiting, direction)
SIGNAL_HANDLER // COMSIG_ATOM_EXITED
// ensure the child is actually connected to the orbited atom
if(!is_in_hierarchy(exiting) || (exiting in orbiter_list))
return
// Remove all signals upwards of the child and re-register them as the new parent
remove_signals(exiting)
RegisterSignal(exiting, COMSIG_MOVABLE_MOVED, PROC_REF(parent_move_react), TRUE)
RegisterSignal(exiting, COMSIG_ATOM_EXITED, PROC_REF(on_remove_child), TRUE)
var/new_loc = get_step(exiting, direction)
INVOKE_ASYNC(src, PROC_REF(handle_parent_move), exiting, exiting.loc, new_loc)
/**
* Called when an intermediate (somewhere between the topmost and the orbited) atom moves.
* This atom will now become the leader.
*/
/datum/component/orbiter/proc/on_intermediate_move(atom/movable/tracked, atom/old_loc)
SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
// Make sure we don't trigger off an orbiter following!
if(!is_in_hierarchy(tracked) || (tracked in orbiter_list))
return
remove_signals(old_loc) // TODO this doesn't work if something's removed from hand
RegisterSignal(tracked, COMSIG_MOVABLE_MOVED, PROC_REF(parent_move_react), TRUE)
RegisterSignal(tracked, COMSIG_ATOM_EXITED, PROC_REF(on_remove_child), TRUE)
INVOKE_ASYNC(src, PROC_REF(handle_parent_move), tracked, old_loc, tracked.loc)
/**
* Returns TRUE if atom_to_find is transitively a parent of src.
*/
/datum/component/orbiter/proc/is_in_hierarchy(atom/movable/atom_to_find)
var/atom/check = parent
while(check)
if(check == atom_to_find)
return TRUE
check = check.loc
return FALSE
///////////////////////////////////
/// orbit data helper functions
///////////////////////////////////
/**
* Store a collection of data for an orbiter.
* orbiter: orbiter atom itself. The orbiter's transform and layer at this point will be captured and cached.
* orbit_flags: bitfield consisting of flags describing the orbit.
*/
/datum/component/orbiter/proc/store_orbit_data(atom/movable/orbiter, orbit_flags)
var/list/new_orbit_data = list(
orbiter.transform, // cached transform
orbit_flags, // params about the orbit
orbiter.layer // cached layer from the orbiter
)
orbit_data[orbiter] = new_orbit_data
return new_orbit_data
/**
* Get cached transform of the given orbiter.
*/
/datum/component/orbiter/proc/get_cached_transform(atom/movable/orbiter)
if(orbiter)
var/list/orbit_params = orbit_data[orbiter]
if(orbit_params)
return orbit_params[1]
/**
* Get the given orbiter's orbit parameters bitfield
*/
/datum/component/orbiter/proc/get_orbit_params(atom/movable/orbiter)
if(orbiter)
var/list/orbit_params = orbit_data[orbiter]
if(orbit_params)
return orbit_params[2]
/**
* Get the layer the given orbiter was on before they started orbiting
*/
/datum/component/orbiter/proc/get_orbiter_layer(atom/movable/orbiter)
if(orbiter)
var/list/orbit_params = orbit_data[orbiter]
if(orbit_params)
return orbit_params[3]
///////////////////////////////////
// Atom procs/vars
///////////////////////////////////
/**
* Set an atom to orbit around another one. This atom will follow the base atom's movement and rotate around it.
*
* orbiter: atom which will be doing the orbiting
* radius: range to orbit at, radius of the circle formed by orbiting
* clockwise: whether you orbit clockwise or anti clockwise
* rotation_speed: how fast to rotate
* rotation_segments: the resolution of the orbit circle, less = a more block circle, this can be used to produce hexagons (6 segments) triangles (3 segments), and so on, 36 is the best default.
* pre_rotation: Chooses to rotate src 90 degress towards the orbit dir (clockwise/anticlockwise), useful for things to go "head first" like ghosts
* lock_in_orbit: Forces src to always be on A's turf, otherwise the orbit cancels when src gets too far away (eg: ghosts)
* force_move: If true, ghosts will be ForceMoved instead of having their .loc updated directly.
* orbit_layer: layer that the orbiter should be on. The original layer will be restored on orbit end.
*/
/atom/movable/proc/orbit(atom/A, radius = 10, clockwise = FALSE, rotation_speed = 20, rotation_segments = 36, pre_rotation = TRUE, lock_in_orbit = FALSE, force_move = FALSE, orbit_layer = FLY_LAYER)
if(!istype(A) || !get_turf(A) || A == src)
return
// Adding a new component every time works as our dupe type will make us just inherit the new orbiter
return A.AddComponent(/datum/component/orbiter, src, radius, clockwise, rotation_speed, rotation_segments, pre_rotation, lock_in_orbit, force_move, orbit_layer)
/**
* Stop this atom from orbiting whatever it's orbiting.
*/
/atom/movable/proc/stop_orbit()
var/atom/orbited = locateUID(orbiting_uid)
if(!orbited)
return
var/datum/component/orbiter/C = orbited.GetComponent(/datum/component/orbiter)
if(!C)
return
C.end_orbit(src)
/**
* Simple helper proc to get a list of everything directly orbiting the current atom, without checking contents, or null if nothing is.
*/
/atom/proc/get_orbiters()
var/datum/component/orbiter/C = GetComponent(/datum/component/orbiter)
if(C && C.orbiter_list)
return C.orbiter_list
else
return null
/**
* Remove an orbiter from the atom it's orbiting.
*/
/atom/proc/remove_orbiter(atom/movable/orbiter)
var/datum/component/orbiter/C = GetComponent(/datum/component/orbiter)
if(C && C.orbiter_list && (orbiter in C.orbiter_list))
C.end_orbit(orbiter)
/**
* Recursive getter method to return a list of all ghosts transitively orbiting this atom.
* This will find orbiters either directly orbiting the followed atom, or any orbiters orbiting them (and so on).
*
* This shouldn't be passed arugments.
*/
/atom/proc/get_orbiters_recursive(list/processed, source = TRUE)
var/list/output = list()
if(!processed)
processed = list()
if(src in processed)
return output
processed += src
// Make sure we don't shadow outer orbiters
for(var/atom/movable/atom_orbiter in get_orbiters())
if(isobserver(atom_orbiter))
output |= atom_orbiter
output += atom_orbiter.get_orbiters_recursive(processed, source = FALSE)
return output
/**
* Check every object in the hierarchy above ourselves for orbiters, and return the full list of them.
* If an object is being held in a backpack, returns orbiters of the backpack, the person
* If recursive == TRUE, this will also check recursively through any ghosts seen to make sure we find *everything* upstream
*/
/atom/proc/get_orbiters_up_hierarchy(list/processed, source = TRUE, recursive = FALSE)
var/list/output = list()
if(!processed)
processed = list()
if((src in processed) || isturf(src))
return output
processed += src
for(var/atom/movable/atom_orbiter in get_orbiters())
if(isobserver(atom_orbiter))
if(recursive)
output += atom_orbiter.get_orbiters_recursive()
output += atom_orbiter
if(loc)
output += loc.get_orbiters_up_hierarchy(processed, source = FALSE)
return output
#undef ORBIT_LOCK_IN
#undef ORBIT_FORCE_MOVE