Files

108 lines
4.3 KiB
Plaintext

//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2024 Citadel Station Developers *//
/**
* Arbitrary holder for an in-progress combo.
*
* This matches via chaining, rather than tail match. For tail match, use
* the simplified helper on `/datum/combo_set`
*
* todo: unit test this shit
*/
/datum/combo_tracker
/// current stored key sequence, first to last
/// * this is not a lazy list
var/list/stored_keys = list()
/// current comboset evaluating against
/// * while we can techncially not store this, as remaining combos list
/// is enough to run an existing combo chain, this is stored to more
/// easily detect errors
var/datum/combo_set/combo_active
/// current possible combos left
var/list/datum/combo/combo_possible
/// current position in a potential combo
var/combo_position
/datum/combo_tracker/proc/reset()
// do not cut, make a new list; subtypes might be referencing this before reset!
stored_keys = list()
combo_active = combo_possible = combo_position = null
/**
* ## algorithm
*
* * 'stored' is the current set of keys that still can trigger a combo. invalid keys will never be added to stored.
* * after a combo is fully completed, the stored buffer is cleared.
* * introspecting our internal state to see where we're at is valid
*
* ## caveats
*
* * cannot check multiple combo sets at once as this tracks info about the active combo set (via weakref).
* if the combo set being checked switches, the combo is dumped.
* * current combos must be terminated to start a new one. a longer combo will always suppress immediately
* executing a shorter one if the keys at that point match.
* * only supports, as of now, executing on an inbound key, instead of working in-place.
*
* ## invalid behavior
*
* certain invalid behavior listed occurs due to ambiguous combos and is intentionally unchecked.
* * if two combos have the same length and keys neither will complete as the algoirthm will see there's
* more than one combo left and will keep going, skipping past the end of both.
* * if a combo is a prefix of another combo, it will never be invoked as the algorithm will see there's
* more than one combo left and keep going instead of checking for completion.
*
* @params
* * inbound - inbound key
* * combo_set - combo set to evaluate against
* * tail_match - allow tail matching. this means we will immediately evaluate all stored keys
* against the combo set, allowing a trigger of a combo mid-set. this can lead
* to unexpected behaviors, so be careful.
*
* @return /datum/combo successfully ran, or null
*/
/datum/combo_tracker/proc/process_inbound(inbound, datum/combo_set/combo_set, tail_match)
SHOULD_NOT_SLEEP(TRUE)
// increment stored
stored_keys += inbound
// if tail match requested, run tail match immediately and ask questions later
if(tail_match)
var/datum/combo/tail_matched = combo_set.simple_tail_match(stored_keys)
if(tail_matched)
return tail_matched
// trim stored
if(length(stored_keys) > combo_set.computed_max_sequence_length)
stored_keys.len = combo_set.computed_max_sequence_length
// reset if we changed combos
if(combo_set != combo_active)
combo_active = combo_set
combo_possible = null
// repopulate and reset if no current combo can still run empty
if(!length(combo_possible))
combo_possible = combo_set.combos.Copy()
combo_position = 1
// see what is still valid, eject invalid ones, etc
for(var/datum/combo/combo as anything in combo_possible)
// if combo_position went past last, it's no longer valid
if(length(combo.keys) < combo_position)
combo_possible -= combo
continue
// if current combo_position in combo keys is not inbound key it's no longer valid
if(combo.keys[combo_position] != inbound)
combo_possible -= combo
continue
// if we're out of possible ones, reset and return
if(!length(combo_possible))
combo_active = combo_position = combo_possible = null
return
var/datum/combo/one_left = length(combo_possible) == 1 ? combo_possible[1] : null
var/finished = one_left ? combo_position == length(one_left.keys) : FALSE
if(finished)
// if finished, wipe possible and it'll trigger a reset on next iteration
combo_active = combo_position = combo_possible = null
. = one_left
else
// advance combo_position otherwise
++combo_position