Next iteration of "wait for completion" framework #1582
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
See #1494 for the first part of this discussion.
This PR makes a number of large leaps allowing certain general effects in the game engine to "wait" for other effects to complete before proceeding with their remaining logic. Normally this just happens naturally with function calls, but when functions require user input to complete (via prompts) they return immediately before the prompt is actually dealt with. #1494 solved this problem for a few select Corp Operations; this PR improves the framework and extends it to many other effects, notably damage routines, traces, psi games, and a few run events.
There are a lot of changes, and I'm not 100% confident that there are no breaking changes here. All the tests pass, but we have imperfect coverage.
Fixes #1092, fixes #1533, fixes #1453, fixes #1429, fixes #1208, fixes #1153.
eids
If we want the ability to wait for an effect to complete before resuming other code, then we need to identify every effect that occurs in the engine. So we assign integer identifiers ("
eid
s") to every ability that gets invoked viaresolve-ability
, plus a few other core functions. When these effects complete, they signal their completion to any "listeners", where a listener is a closure over a function body that wants to resume once the subscribed effect is completed.Technically an
eid
is a map with a single key:eid
mapped to a unique integer. The state assigns neweid
s viamake-eid
.The
effect
/req
macros of implementing card abilities have been expanded to take a fifth parameter (it is in the third position, afterstate side
) calledeid
, representing the eid assigned to the resolution of the ability. Some core engine abilities also now take aneid
parameter; in general, these functions are the ones I have upgraded to support this framework.Waiting for an effect to complete
If you want to wait for an effect to complete, you generally use the
when-completed
macro.when-completed
takes two arguments: a single function to execute "asynchronously" (let's call this "the async") and a single function to execute once the first has completed (let's call this "the await"). The async function must be a function that takesstate side eid
as its first three arguments.when-completed
will generate aneid
(unless the async already has a third parameter that is an eid), register a closure around the await function that will listen for the completion of the async's eid, then return.If you have programmed in C# or F#, you may recognize this pattern as what Microsoft calls "async/await".
Examples:
Suppose
ability
is some card ability we want to resolve, and once that ability is fully completed we want to print a system message.resolve-ability
has been upgraded to take aneid
parameter, so this works. Once the selected ability has resolved, we will execute thesystem-msg
. (Example: Accelerated Diagnostics will wait for its first operation to fully resolve before giving a prompt to choose the next.)Suppose we want to invoke the
:successful-run
event, wait for all handlers to complete, and then proceed with the run/access code.trigger-event-sync
will trigger an event, but wait for each handler ability to complete before invoking the next, and the whole event will only complete once the last handler completes. Thus, we will only move todo-access
once all event handlers for:successful-run
have finished. (Example: Bernice Mai's trace will fully resolve before moving to the access phase.)These cover the two most common uses: waiting for a single ability, and waiting for all event listeners. There are a few other functions that can be awaited, but they are mostly just to implement certain other end-results.
play-instant
(when the card's effect is complete)corp-install
(when the install-effect of the card is complete [currently no such effects?])damage
(when the entire damage/prevention sequence completes)register-successful-run
(when the event handlers for:successful-run
have completed)When is an effect "completed"?
An effect is completed when its
eid
is passed to(effect-completed)
. When this happens depends a little on context:resolve-ability
will immediately calleffect-completed
after invoking the:effect
of the ability, UNLESS the ability is marked with:delayed-completion true
, OR the ability is a psi, trace, optional, or prompt (choices) ability AND is not marked with:delayed-completion false
. Thus, simple abilities do not need to change anything about how they work: Hedge Fund will "complete" after granting its credits.:delayed-completion true
, it is responsible for invokingeffect-completed
itself at an appropriate time. There are two shortcuts:continue-ability
(see below) can resolve a sub-ability using the same eid, and if that sub-ability immediately resolves as above, that will in effect complete the main ability. Or you can usefinal-effect
instead ofeffect
to automatically insert theeffect-completed
call.damage
will complete once the damage is applied (post-prevention effects)Current uses
I am currently using the framework for these fixes/effects:
Damage: each instance of damage will fully resolve before moving onto the next, including prevention opportunities. This cleans up issues with Deus X preventing multiple sources of damage. It also helps Subcontract or AccDiag into double Scorched Earth, by not allowing the selection of the second Scorched until the first fully resolves.
This also cleans up Chronos Protocol, by allowing its damage-replacement effect to fully resolve before moving on to any other sources of damage.
On-access effects: if a Corp card has an
:access
effect, that effect will be resolved before the runner is allowed to trash/steal/continue accessing other cards. Thus, Fetal AI will do its damage routine before Personal Evolution will apply its damage from the steal. Also, the psi game and damage from Psychic Field will complete before PF is trashed and damage from Hostile Infrastructure might be applied.Successful-run effects: the runner will not move to the access phase until all
:successful-run
triggers have resolved, so Bernice Mai's trace will complete prior to access, and if the trace fails she will be trashed and not a valid access target.Other details
when-completed
will assign aneid
to the async target automatically, but you can assign one yourself by giving aneid
as the third parameter to the async function. This can be useful when you want to be specific about what eid the async gets, because you know someone in particular is waiting for that eid.when-completed
will also work withapply
.continue-ability
is a new macro that works exactly the same asresolve-ability
, except it passes the currenteid
of whoever calledcontinue-ability
as the eid of the ability to be resolved. This is useful when a card's effect callsresolve-ability
to "finish" the effect; by usingcontinue-ability
instead, the resolved sub-ability keeps the sameeid
as the "main" ability, and so the sub-ability's completion signifies the completion of the main ability.