diff --git a/code/__DEFINES/dcs/signals_atoms/signals_atom.dm b/code/__DEFINES/dcs/signals_atoms/signals_atom.dm index 79d6ee06..bb6aa605 100644 --- a/code/__DEFINES/dcs/signals_atoms/signals_atom.dm +++ b/code/__DEFINES/dcs/signals_atoms/signals_atom.dm @@ -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" diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 0ae3c61b..692bb6b0 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -214,6 +214,7 @@ if(LAZYLEN(po)){\ A.overlays |= po;\ }\ + SEND_SIGNAL(A, COMSIG_ATOM_COMPILED_OVERLAYS);\ for(var/I in A.alternate_appearances){\ var/datum/atom_hud/alternate_appearance/AA = A.alternate_appearances[I];\ if(AA.transfer_overlays){\ diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 6a13a450..4cc4af99 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -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. + */ +#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 = "" ) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index d4e03ce9..5e2e5f44 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -133,8 +133,7 @@ SUBSYSTEM_DEF(ticker) if(isemptylist(music)) music = world.file2list(ROUND_START_MUSIC_LIST, "\n") - login_music = pick(music) - else + if(length(music)) login_music = "[global.config.directory]/title_music/sounds/[pick(music)]" login_music = pick('sound/music/title.ogg','sound/music/title2.ogg') diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index c5e9abee..1e07fae2 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -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) @@ -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 diff --git a/code/datums/callback.dm b/code/datums/callback.dm index 7ef4b536..46a9191a 100644 --- a/code/datums/callback.dm +++ b/code/datums/callback.dm @@ -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 + . = Invoke(arglist(args)) + /** Helper datum for the select callbacks proc */ diff --git a/code/datums/components/mobs/self_image_override.dm b/code/datums/components/mobs/self_image_override.dm new file mode 100644 index 00000000..411cca48 --- /dev/null +++ b/code/datums/components/mobs/self_image_override.dm @@ -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 = list() + /// 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(unreferencing) + +/** + * 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 diff --git a/code/datums/components/seethrough_mob.dm b/code/datums/components/seethrough_mob.dm index 897ee324..cc0aa509 100644 --- a/code/datums/components/seethrough_mob.dm +++ b/code/datums/components/seethrough_mob.dm @@ -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 diff --git a/code/modules/mob/login.dm b/code/modules/mob/login.dm index 04cd1dfb..b9d4b7e6 100644 --- a/code/modules/mob/login.dm +++ b/code/modules/mob/login.dm @@ -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 = 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) diff --git a/code/modules/mob/vision_cone.dm b/code/modules/mob/vision_cone.dm index 86babc64..e19f83f1 100644 --- a/code/modules/mob/vision_cone.dm +++ b/code/modules/mob/vision_cone.dm @@ -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 @@ -82,13 +83,12 @@ 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 - if(mob) - var/mob/user = usr - mob.update_cone() - - +/client/proc/update_cone() + 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 @@ -96,15 +96,6 @@ 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 @@ -269,8 +260,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 @@ -291,6 +284,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) diff --git a/roguetown.dme b/roguetown.dme index d9e816e5..bd311724 100644 --- a/roguetown.dme +++ b/roguetown.dme @@ -600,6 +600,7 @@ #include "code\datums\components\crafting\crafting.dm" #include "code\datums\components\crafting\recipes.dm" #include "code\datums\components\decals\blood.dm" +#include "code\datums\components\mobs\self_image_override.dm" #include "code\datums\components\storage\storage.dm" #include "code\datums\components\storage\storage_types.dm" #include "code\datums\components\storage\concrete\_concrete.dm"