Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/__DEFINES/dcs/signals_atoms/signals_atom.dm
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@
#define COMSIG_ATOM_EXAMINE_TAGS "atom_examine_tags"
///from base of atom/examine_more(): (/mob, examine_list)
#define COMSIG_ATOM_EXAMINE_MORE "atom_examine_more"

/// Sent on COMPILE_OVERLAYS()
/// * Make extra, extra sure you **do not** call managed overlay procs like add_overlay() or cut_overlay()
/// when handling this signal; doing so WILL cause an infinite loop!
#define COMSIG_ATOM_COMPILED_OVERLAYS "atom-compile_overlays"
1 change: 1 addition & 0 deletions code/__DEFINES/subsystems.dm
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@
if(LAZYLEN(po)){\
A.overlays |= po;\
}\
SEND_SIGNAL(A, COMSIG_ATOM_COMPILED_OVERLAYS);\
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed a signal hook to know when to re-render

for(var/I in A.alternate_appearances){\
var/datum/atom_hud/alternate_appearance/AA = A.alternate_appearances[I];\
if(AA.transfer_overlays){\
Expand Down
64 changes: 39 additions & 25 deletions code/__HELPERS/_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,47 @@
#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
#define reverseList(L) reverseRange(L.Copy())

// binary search sorted insert
// IN: Object to be inserted
// LIST: List to insert object into
// TYPECONT: The typepath of the contents of the list
// COMPARE: The variable on the objects to compare
#define BINARY_INSERT(IN, LIST, TYPECONT, COMPARE) \
var/__BIN_CTTL = length(LIST);\
if(!__BIN_CTTL) {\
LIST += IN;\
} else {\
var/__BIN_LEFT = 1;\
var/__BIN_RIGHT = __BIN_CTTL;\
var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
var/##TYPECONT/__BIN_ITEM;\
while(__BIN_LEFT < __BIN_RIGHT) {\
__BIN_ITEM = LIST[__BIN_MID];\
if(__BIN_ITEM.##COMPARE <= IN.##COMPARE) {\
__BIN_LEFT = __BIN_MID + 1;\
} else {\
__BIN_RIGHT = __BIN_MID;\
/// Passed into BINARY_INSERT to compare keys
#define COMPARE_KEY __BIN_LIST[__BIN_MID]
/// Passed into BINARY_INSERT to compare values
#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]]

/****
* Binary search sorted insert
* Sorts low to high.
*
* * INPUT: Object to be inserted
* * LIST: List to insert object into
* * TYPECONT: The typepath of the contents of the list
* * COMPARE: The object to compare against, usualy the same as INPUT
* * COMPARISON: The variable on the objects to compare
* * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE.
Comment on lines -40 to +50
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your binary insert was out of date

*/
#define BINARY_INSERT(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \
do {\
var/list/__BIN_LIST = LIST;\
var/__BIN_CTTL = length(__BIN_LIST);\
if(!__BIN_CTTL) {\
__BIN_LIST += INPUT;\
} else {\
var/__BIN_LEFT = 1;\
var/__BIN_RIGHT = __BIN_CTTL;\
var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
var ##TYPECONT/__BIN_ITEM;\
while(__BIN_LEFT < __BIN_RIGHT) {\
__BIN_ITEM = COMPTYPE;\
if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\
__BIN_LEFT = __BIN_MID + 1;\
} else {\
__BIN_RIGHT = __BIN_MID;\
};\
__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
};\
__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\
__BIN_ITEM = COMPTYPE;\
__BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\
__BIN_LIST.Insert(__BIN_MID, INPUT);\
};\
__BIN_ITEM = LIST[__BIN_MID];\
__BIN_MID = __BIN_ITEM.##COMPARE > IN.##COMPARE ? __BIN_MID : __BIN_MID + 1;\
LIST.Insert(__BIN_MID, IN);\
}
} while(FALSE)

//Returns a list in plain english as a string
/proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" )
Expand Down
4 changes: 2 additions & 2 deletions code/controllers/subsystem/timer.dm
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ SUBSYSTEM_DEF(timer)
if(ctime_timer.flags & TIMER_LOOP)
ctime_timer.spent = 0
ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait
BINARY_INSERT(ctime_timer, clienttime_timers, datum/timedevent, timeToRun)
BINARY_INSERT(ctime_timer, clienttime_timers, /datum/timedevent, ctime_timer, timeToRun, COMPARE_KEY)
else
qdel(ctime_timer)

Expand Down Expand Up @@ -423,7 +423,7 @@ SUBSYSTEM_DEF(timer)
L = SStimer.second_queue

if(L)
BINARY_INSERT(src, L, datum/timedevent, timeToRun)
BINARY_INSERT(src, L, /datum/timedevent, src, timeToRun, COMPARE_KEY)
return

//get the list of buckets
Expand Down
29 changes: 29 additions & 0 deletions code/datums/callback.dm
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,35 @@
return call(delegate)(arglist(calling_arguments))
return call(object, delegate)(arglist(calling_arguments))

/**
* Invoke this callback and crash if it sleeps.
*
* * Use when a callback should never sleep, as call() cannot be verified by static analysis.
* * Do not use in performance critical code. This wraps calls more aggressively than InvokeAsync().
* * `null` is returned if the call sleeps.
*
* The specific use case here is an async call where:
*
* * It's always invalid behavior for a callback to sleep.
* * The caller should be protected (caller shouldn't be interrupted by the sleep).
*
* This allows enforcement of the above invariants by loudly runtiming and bringing attention to the issue,
* as opposed to the usual way of compile checking it (which we can't because this is a reflection-based call to an arbitrary proc).
*/
/datum/callback/proc/invoke_no_sleep(...)
#define CALLBACK_SLEEP_SENTINEL "---!!SOMEBULLSHITSTRINGYOUDEFINITELYWONTSEEANYWHEREELSE----!!!"
. = CALLBACK_SLEEP_SENTINEL
. = invoke_no_sleep_call(arglist(args))
if(. == CALLBACK_SLEEP_SENTINEL)
. = null
CRASH("Callback [src] slept on a no-sleeping invoke.")
#undef CALLBACK_SLEEP_SENTINEL

/datum/callback/proc/invoke_no_sleep_call(...)
PRIVATE_PROC(TRUE)
set waitfor = FALSE
Comment on lines +179 to +190
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protective sentinel call to prevent sleeping; if the hooks used in the component slept it would literally detonate SSoverlays, you do not want this

. = Invoke(arglist(args))

/**
Helper datum for the select callbacks proc
*/
Expand Down
126 changes: 126 additions & 0 deletions code/datums/components/mobs/self_image_override.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@

/**
* This mildly unhinged-sounding components allows arbitrarily having a somewhat-automatically-
* updating 'override' image for one's self. This solves the issue of BYOND only allowing
* one override image per atom per client, but, there's a catch.
*
* * The API for this isn't laggy, per se, but isn't high-perforamnce either. This is
* a lazy component to fulfil a purpose. If you are reading this in the future and
* this component is visible on profiler's CPU rankings and it's a problem,
* look into proper managed rendering via planes / vis contents / byond intrinsics /
* refactoring this component to be update-event based, not rebuild based.
* * The reason this is inefficient is because all callbacks are invoked every time
* we need an update. This is not great; there's no way to selective-update.
* * The API to use this is LoadComponent(). Yeah, this isn't amazing.
* * The hook API uses string keys. This is because I don't really trust featurecoders
* with handling raw callback references. Sorry! Strings, at the least, are pooled by
* BYOND for you.
* * One override per atom per client still applies; this can be trampled by misbehaving code
* elsewhere, like using alternate appearances that show to self while this is active.
* There's nothing I can do about that; having an actually generate alternate-appearance-override
* system is the realm of a *massive* atom HUD refactor, which I am not able to do in just 24 hours.
*/
/datum/component/self_image_override
/// associative list key to /datum/component_self_image_override_entry; lower priority is applied first
var/list/alter_entries
/// our rendering image
var/image/renderer

/datum/component/self_image_override/Initialize()
if(!ismob(parent))
return COMPONENT_INCOMPATIBLE
. = ..()
if(. == COMPONENT_INCOMPATIBLE)
return

/datum/component/self_image_override/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_COMPILED_OVERLAYS, PROC_REF(on_overlay_update))
renderer = new
renderer.loc = parent
ensure_image_is_on_client()
update()

/datum/component/self_image_override/UnregisterFromParent()
UnregisterSignal(parent, COMSIG_ATOM_COMPILED_OVERLAYS)
if(renderer)
var/mob/mob_parent = parent
if(mob_parent.client)
mob_parent.client -= renderer
renderer.loc = null
// try not to harddel; byond scans active procs faster.
var/image/unreferencing = renderer
renderer = null
qdel(renderer)

/**
* Adds an alteration hook with a given priority.
* * This hook is ran on every update.
* * This does **not** trigger an immediate update.
* * If the key already exists, the old hook will be overwritten.
* * You are responsible for ensuring the callback is valid for the duration of the alteration's
* lifetime on this component. If the callback is invalidated or its delegate is, runtimes
* will occur every update and this is very, very bad.
* * Callback hooks **cannot sleep under any circumstances.** Doing so will blow things up, like
* for example the overlay subsystem. This is enforced with an `invoke_no_sleep()`.
*
* @params
* * key - string key to register hook under
* * hook - the callback hook, which will be called with (mob/host_mob, mutable_appearance/modifying)
* * priority - priority to register under; lower runs first.
*/
/datum/component/self_image_override/proc/add_alteration_hook(key, datum/callback/hook, priority)
ASSERT(isnum(priority))
ASSERT(istype(hook))
if(alter_entries[key])
remove_alteration_hook(key)
var/datum/component_self_image_override_entry/entry = new(key, hook, priority)
BINARY_INSERT(entry, alter_entries, /datum/component_self_image_override_entry, entry, priority, COMPARE_KEY)

/datum/component/self_image_override/proc/remove_alteration_hook(key)
alter_entries -= key
if(!length(alter_entries))
addtimer(CALLBACK(src, PROC_REF(auto_gc_if_empty)), 0)

/datum/component/self_image_override/proc/auto_gc_if_empty()
if(!length(alter_entries))
qdel(src)

/datum/component/self_image_override/proc/on_overlay_update(datum/source)
PRIVATE_PROC(TRUE)
SIGNAL_HANDLER
update()

/datum/component/self_image_override/proc/update()
if(!renderer)
return
var/mob/our_parent_mob = parent
var/mutable_appearance/mutating = new(our_parent_mob)
for(var/datum/component_self_image_override_entry/entry as anything in alter_entries)
// just because i expect featurecoders to use this code this contains a sanity check
// to make sure the callback's target object
var/datum/callback/entry_cb = entry.callback
if(entry_cb.object != GLOBAL_PROC && QDELETED(entry_cb.object))
alter_entries -= entry
// if you are seeing this this is **always** incorrect behavior.
stack_trace("self image override on mob [REF(our_parent_mob)] contained an alter hook callback with key [entry.key] with a callback targeting a qdeleted object [entry_cb.object]")
continue
entry_cb.invoke_no_sleep(our_parent_mob, mutating)
renderer.appearance = mutating

/datum/component/self_image_override/proc/ensure_image_is_on_client()
// it should not be necessary to trigger this proc too much but most codebases
// are lazy and just clear images all the time, so, just call this lol
var/mob/our_parent_mob = parent
var/client/maybe_client = our_parent_mob.client
maybe_client.images |= renderer

/// quite a mouthful
/datum/component_self_image_override_entry
var/key
var/datum/callback/callback
var/priority

/datum/component_self_image_override_entry/New(key, datum/callback/callback, priority)
src.key = key
src.callback = callback
src.priority = priority
27 changes: 24 additions & 3 deletions code/datums/components/seethrough_mob.dm
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,42 @@
button_icon_state = "smallqueen"
background_icon_state = "bg_alien"
var/small = FALSE
//var/image/small_icon

// TODO: this shouldn't be a global proc, I just didn't want to validate the action lifetime
// and make sure it always cleans up after itself.
/proc/sizecode_impl_self_downsize_hook(mob/host_mob, mutable_appearance/modifying)
var/matrix/inverse_transform = modifying.transform
// TODO: put logic for determining what transform is applied here. i just reset it right now.
inverse_transform = matrix()
modifying.transform = inverse_transform

/datum/action/sizecode_smallsprite/Remove(mob/M)
if(small && owner)
var/datum/component/self_image_override/image_override_handler = owner.GetComponent(/datum/component/self_image_override)
if(image_override_handler)
image_override_handler.remove_alteration_hook("sizecode_smallsprite")
image_override_handler.update()
return ..()

/datum/action/sizecode_smallsprite/Trigger(trigger_flags)
. = ..()
if(!owner)
return

if(!small)
var/datum/component/self_image_override/image_override_handler = owner.LoadComponent(/datum/component/self_image_override)
// execute very early; TODO: define the priority
image_override_handler.add_alteration_hook("sizecode_smallsprite", CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(sizecode_impl_self_downsize_hook)), -100000)
image_override_handler.update()
var/image/I = image(icon = owner.icon, icon_state = owner.icon_state, loc = owner, layer = owner.layer, pixel_x = owner.pixel_x, pixel_y = owner.pixel_y)
I.override = TRUE
I.overlays += owner.overlays
owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic, "smallsprite_sizecode", I)
//small_icon = I
else
owner.remove_alt_appearance("smallsprite_sizecode")
var/datum/component/self_image_override/image_override_handler = owner.GetComponent(/datum/component/self_image_override)
if(image_override_handler)
image_override_handler.remove_alteration_hook("sizecode_smallsprite")
image_override_handler.update()

small = !small
return TRUE
Expand Down
3 changes: 3 additions & 0 deletions code/modules/mob/login.dm
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
client.screen = list() //remove hud items just in case
client.images = list()

var/datum/component/self_image_override/maybe_self_image_override_component = mob.GetComponent(/datum/component/self_image_override)
maybe_self_image_override_component?.ensure_image_is_on_client()

if(!hud_used)
create_mob_hud()
if(hud_used && client && client.prefs)
Expand Down
26 changes: 13 additions & 13 deletions code/modules/mob/vision_cone.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/client
var/list/hidden_atoms = list()
var/list/hidden_mobs = list()
// seemingly a list of images to relay things you should always see above the vision cone.
var/list/hidden_images = list()

/mob
Expand Down Expand Up @@ -82,29 +83,21 @@
hud_used.fov_blocker.dir = src.dir
START_PROCESSING(SSincone, client)

/client/proc/update_cone() //This is where the sprite decides to return to the same size, some utter bullshit
/client/proc/update_cone()
if(mob)
var/mob/user = usr
mob.update_cone()



/mob/proc/__cb_lift_fovcone_image_plane(mob/host_mob, mutable_appearance/modifying)
modifying.plane = GAME_PLANE_UPPER

/mob/living/update_cone()
for(var/hidden_hud in client.hidden_images)
client.images -= hidden_hud
client.hidden_images -= hidden_hud
if(hud_used?.fov)
if(hud_used.fov.alpha == 0)
return
var/image/I = image(src, src)
I.override = 1
I.plane = GAME_PLANE_UPPER
I.layer = layer
I.pixel_x = 0
I.pixel_y = 0
client.images += I
client.hidden_images += I
I.appearance_flags = RESET_TRANSFORM|KEEP_TOGETHER|PIXEL_SCALE
if(buckled)
var/image/IB = image(buckled, buckled)
IB.override = 1
Expand Down Expand Up @@ -269,8 +262,10 @@
return hide_cone()
return show_cone()


/mob/proc/update_fov_angles()
var/datum/component/self_image_override/self_image_handler = LoadComponent(/datum/component/self_image_override)
self_image_handler.remove_alteration_hook("fov-plane-lift")

fovangle = initial(fovangle)
if(ishuman(src) && fovangle)
var/mob/living/carbon/human/H = src
Expand All @@ -291,6 +286,11 @@
return
if(!hud_used.fov_blocker)
return

// at this point we know they are going to have fov rendering so let's put in a hook to lift
// themselves up
self_image_handler.add_alteration_hook("fov-plane-lift", CALLBACK(src, PROC_REF(__cb_lift_fovcone_image_plane)), 100000)

if(fovangle & FOV_DEFAULT)
if(fovangle & FOV_RIGHT)
if(fovangle & FOV_LEFT)
Expand Down
Loading
Loading