Files

337 lines
9.5 KiB
Plaintext

//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* action datums
*
* holder for movable/snappable hud action buttons.
* binds to a single target datum; clicks are sent to it
* or processed on `trigger` --> `on_trigger`.
*
* it is allowable to manually re-cast the target
* arg on trigger and on_trigger *only* if you use target_compatible
* to verify the target is valid.
*
* keep in mind that you have to manually delete actions
* when gcing its target! this is to encourage best practice;
* the action button will not automatically listen to qdel's.
*
*/
/datum/action
/// action name
var/name = "Generic Action"
/// description
var/desc = "An action."
//* Checks *//
/// required mobility flags
var/check_mobility_flags = NONE
/// custom check callback called before invocation with (actor)
///
/// * using this is generally bad practice as it's exceedingly easy to block GC via usage of this.
var/datum/callback/check_callback
//* Target / Delegate *//
/// callback to invoke with (datum/action/action, datum/event_args/actor/actor) on trigger at base of /invoke().
///
/// * return a truthy value from the callback to halt propagation
var/datum/callback/invoke_callback
/// target; it will receive ui_action_click(datum/action/action, datum/event_args/actor/actor)
///
/// * happens after [invoke_callback]
/// * return a truthy value from ui_action_click() to halt propagation
var/datum/target
/// expected target type
var/target_type
//* Ownership *//
/// the holders we're in
var/list/datum/action_holder/holders
//* Button(s) *//
/// all buttons that are on us right now
var/list/atom/movable/screen/movable/action_button/buttons
/// do not update buttons; something else manages them
var/rendering_externally_managed = FALSE
/// where the button's background icon is from
var/background_icon = 'icons/screen/actions/backgrounds.dmi'
/// what the action's background state should be
var/background_icon_state = "default"
/// custom background overlay to add; this goes below button sprite / overlays!
var/background_additional_overlay
/// the icon of the button's actual internal sprite, overlaid on the background
var/button_icon = 'icons/screen/actions/actions.dmi'
/// the icon_state of the button's actual internal sprite, overlaid on the background
var/button_icon_state = "default"
/// only overlay [button_additional_overlay]
var/button_additional_only = FALSE
/// custom overlay to add to all buttons; this is arbitrary, and can be a reference to an atom
var/button_additional_overlay
/// set availability to; it must be 0 to 1, inclusive.
var/button_availability = 1
/// default handling for availability should be invoked
var/button_availability_automatic = TRUE
/// are we active?
var/button_active = FALSE
/// overlay to add to background if active
var/button_active_overlay = "active-1"
/datum/action/New(datum/target)
if(!target_compatible(target))
qdel(src)
CRASH("invalid target for [src] - [target]")
src.target = target
/datum/action/Destroy()
target = null
invoke_callback = null
check_callback = null
for(var/datum/action_holder/holder in holders)
holder.remove_action(src)
if(length(buttons))
stack_trace("still had buttons after Destroy")
QDEL_LIST(buttons)
// clear refs to overlays; they might be objects / not belonging to us, do not delete it
button_additional_overlay = null
background_additional_overlay = null
return ..()
/**
* checks if a datum is a valid target for us
*/
/datum/action/proc/target_compatible(datum/target)
return isnull(target_type) || istype(target, target_type)
//* Button *//
/**
* set button availability
*/
/datum/action/proc/push_button_availability(availability, update = TRUE)
button_availability = availability
if(update)
update_buttons(TRUE)
/**
* set button active-ness
*/
/datum/action/proc/set_button_active(active, defer_update)
button_active = active
if(!defer_update)
update_buttons(TRUE)
/**
* updates if availability changed
*/
/datum/action/proc/update_button_availability()
if(!button_availability_automatic)
return
var/calculated = calculate_availability()
if(calculated == button_availability)
return
button_availability = calculated
update_buttons(TRUE)
/**
* called pre-render
*
* use this to automatically update overlays and whatnot
*
* * called once for all buttons, not once per button!
*/
/datum/action/proc/pre_render_hook()
return
/**
* update all button appearances / states
*/
/datum/action/proc/update_buttons(no_state_calculations)
if(rendering_externally_managed)
return
if(!no_state_calculations)
if(button_availability_automatic)
button_availability = calculate_availability()
pre_render_hook()
var/appearance/direct_appearance = render_button_appearance()
for(var/atom/movable/screen/movable/action_button/button as anything in buttons)
update_button(button, direct_appearance)
/**
* updates a button's appearance / state
*/
/datum/action/proc/update_button(atom/movable/screen/movable/action_button/button, appearance/use_direct)
if(rendering_externally_managed)
return
if(isnull(use_direct))
pre_render_hook()
use_direct = render_button_appearance()
button.appearance = use_direct
/**
* gets our button appearance
*/
/datum/action/proc/render_button_appearance()
var/image/generating = new
generating.name = name
generating.desc = desc
generating.icon = background_icon
generating.icon_state = background_icon_state
generating.plane = HUD_PLANE
generating.layer = HUD_LAYER_BASE
if(button_active && button_active_overlay)
generating.overlays += button_active_overlay
if(background_additional_overlay)
generating.overlays += background_additional_overlay
var/image/button
if(button_additional_only)
if(button_additional_overlay)
button = button_additional_overlay
else
button = new
button.icon = button_icon
button.icon_state = button_icon_state
if(button_additional_overlay)
button.overlays += button_additional_overlay
generating.overlays += button
if(button_availability < 1)
generating.color = rgb(128, 0, 0, 128)
else
generating.color = rgb(255, 255, 255, 255)
return generating
/**
* calculates current automatic availability
*/
/datum/action/proc/calculate_availability()
return 1
/**
* create a button for a specific action drawer
*/
/datum/action/proc/create_button(datum/action_drawer/drawer, datum/action_holder/holder)
RETURN_TYPE(/atom/movable/screen/movable/action_button)
var/atom/movable/screen/movable/action_button/creating = new(null, holder, src)
pre_render_hook()
creating.appearance = render_button_appearance()
LAZYADD(buttons, creating)
return creating
/**
* asks us to gc a button used by a drawer that no longer has us
*/
/datum/action/proc/destroy_button(atom/movable/screen/movable/action_button/button, datum/action_drawer/drawer)
qdel(button)
//* Invocation *//
/**
* called when someone tries to invoke us
*
* * this is the **only** place where user input should go
* *add a force parameter if we at some point need a way to ignore check_invoke
* * we require this because logging goes through here!
*
* @return TRUE / FALSE
*/
/datum/action/proc/try_invoke(datum/event_args/actor/actor)
SHOULD_CALL_PARENT(TRUE)
SHOULD_NOT_OVERRIDE(TRUE) // logging happens here, do not fuck around and find out!
if(!check_invoke(actor))
log_click_action(actor, src, "failed-invoke")
return FALSE
log_click_action(actor, src, "invoke")
invoke(actor)
return TRUE
/**
* runs invocation checks
*
* todo: maybe tell them why it isn't working, yeah, that'd probably be a good idea!
*/
/datum/action/proc/check_invoke(datum/event_args/actor/actor, silent)
var/mob/performer = actor.performer
if((performer.mobility_flags & check_mobility_flags) != check_mobility_flags)
return FALSE
return TRUE
/**
* called when we are invoked
*
* * **do not directly call this proc**, this doesn't log!
* * please return immediately if ..() returns a truthy value!
*
* @return TRUE to stop propagation
*/
/datum/action/proc/invoke(datum/event_args/actor/actor)
PROTECTED_PROC(TRUE) // you thought i was joking??? do not directly call this goddamn proc.
SHOULD_NOT_OVERRIDE(TRUE)
if(invoke_callback?.Invoke(src, actor))
return TRUE
if(invoke_target(target, actor))
return TRUE
return FALSE
/**
* called when we get triggered by someone
*
* it is valid to typecast target accordingly **if and only if**
* target_type has been used to type-filter for certainty.
*
* @params
* * target - the target; provided to allow for typecasting
* * actor - the invoker
*
* @return TRUE to stop invoke() propagation
*/
/datum/action/proc/invoke_target(datum/target, datum/event_args/actor/actor)
return target?.ui_action_click(src, actor)
//* Ownership *//
/**
* grant us to an action holder
*/
/datum/action/proc/grant(datum/action_holder/holder)
if(src in holder.actions)
return
LAZYADD(holders, holder)
LAZYADD(holder.actions, src)
holder.on_action_add(src)
/**
* remove us from an action holder
*/
/datum/action/proc/revoke(datum/action_holder/holder)
if(!(src in holder.actions))
return
LAZYREMOVE(holders, holder)
LAZYREMOVE(holder.actions, src)
holder.on_action_remove(src)
//* /datum implementation & hooks *//
/**
* called when someone clicks an action bound to us as the target.
*
* * only called if the action datum didn't handle it itself.
*
* @params
* * action - action datuam
* * actor - the person clicking
*
* @return TRUE if handled
*/
/datum/proc/ui_action_click(datum/action/action, datum/event_args/actor/actor)
return FALSE