Skip to content

Latest commit

 

History

History
1234 lines (1028 loc) · 43.4 KB

File metadata and controls

1234 lines (1028 loc) · 43.4 KB

Examples: Real-World Patterns

Production-ready workflows demonstrating common game patterns.

Overview

Example Pattern Key Techniques
Buff Timed effects Loop + select, signal payloads, stacking
Countdown Periodic updates Loop + timer, mutable state
Conditional Branching If/else with await
Parent-Child Hierarchical For loop + spawn
Patrol Looping behavior Waypoint iteration, threat response
Production Economic cycle State machine, pause/resume
Combat Turn-based Action timeout, select with break/continue
Respawn Uninterruptible Simple timer, no signals
Day/Night Global singleton Infinite loop, explicit stop, correlation_id
Auction Timed bidding Bid signals, buyout, timeout
Trade Multi-party Both parties accept, timeout
Quest Objectives Progress tracking, abandon/fail

Buff Workflow

Use case: Timed status effect with stacking and early termination.

State Machine

    ┌───────┐
    │ START │
    └───┬───┘
        │
        ▼
    ┌────────┐   expire timer    ┌─────────┐
    │ ACTIVE │──────────────────▶│ EXPIRED │──▶ Complete
    └───┬────┘                   └─────────┘
        │
        │ stack signal (continue loop)
        │ dispel signal (break)
        ▼
    ┌────────┐
    │ ACTIVE │ (timer restarted)
    └────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum BuffTimer { Expire }

#[derive(Signal)]
pub enum BuffSignal {
    Dispel,
    Stack(u32),  // Payload auto-deserialized
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuffInit {
    pub effect_type: String,
    pub magnitude: i32,
    pub duration_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuffResult {
    pub effect_type: String,
    pub was_dispelled: bool,
    pub final_stacks: u32,
}

#[workflow]
fn buff(init: BuffInit) -> Result<BuffResult> {
    let mut stacks: u32 = 1;

    loop {
        select! {
            timer!(BuffTimer::Expire, init.duration_secs.secs()) => break,
            signal!(BuffSignal::Dispel) => break,
            signal!(BuffSignal::Stack(n)) => {
                stacks += n;
                continue
            },
        }.await;
    }

    Ok(BuffResult {
        effect_type: init.effect_type.clone(),
        was_dispelled: false,
        final_stacks: stacks,
    })
}

Signals

Signal Payload Effect
dispel End immediately
stack amount (u32) Add stacks, continue

Countdown Workflow

Use case: Timer that ticks down, can be reset or cancelled.

State Machine

    ┌───────┐
    │ START │
    └───┬───┘
        │ count = init.count
        ▼
    ┌─────────────┐
    │   TICKING   │◀─────────────────┐
    └──────┬──────┘                  │
           │                         │
    ┌──────┴──────┬─────────────┐   │
    │ tick timer  │ reset signal │   │
    ▼             ▼              │   │
 count -= 1    count = new     continue
    │                               │
    └───────────────────────────────┘
           │
    count == 0 ?
           │
           ▼
    ┌───────────┐
    │ COMPLETED │
    └───────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum CountdownTimer { Tick }

#[derive(Signal)]
pub enum CountdownSignal {
    Cancel,
    Reset(u32),  // New count value
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CountdownInit { pub count: u32 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CountdownResult {
    pub completed: bool,
    pub final_count: u32,
}

#[workflow]
fn countdown(init: CountdownInit) -> Result<CountdownResult> {
    let mut count: u32 = init.count;

    loop {
        select! {
            timer!(CountdownTimer::Tick, 1.secs()) => {
                count = count.saturating_sub(1);
                if count == 0 { break }
                continue
            },
            signal!(CountdownSignal::Cancel) => break,
            signal!(CountdownSignal::Reset(new_count)) => {
                count = new_count;
                continue
            },
        }.await;
    }

    Ok(CountdownResult {
        completed: count == 0,
        final_count: count,
    })
}

Signals

Signal Payload Effect
cancel Stop immediately
reset count (u32) Reset to new value

Conditional Workflow

Use case: Different behavior based on initialization data.

State Machine

    ┌───────────────┐
    │     START     │
    └───────┬───────┘
            │
    ┌───────┴───────┐
    │ use_short?    │
    ▼               ▼
┌───────┐      ┌───────┐
│ SHORT │      │ LONG  │
│ timer │      │ timer │
│ (1s)  │      │ (2s)  │
└───┬───┘      └───┬───┘
    │              │
    │ value += 10  │ value += 20
    │              │
    └──────┬───────┘
           ▼
    ┌───────────┐
    │ COMPLETED │
    └───────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer};

#[derive(Timer)]
pub enum ConditionalTimer {
    Short,
    Long,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalInit {
    pub use_short_timer: bool,
    pub value: i32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalResult {
    pub used_short: bool,
    pub final_value: i32,
}

#[workflow]
fn conditional_test(init: ConditionalInit) -> Result<ConditionalResult> {
    let mut value: i32 = init.value;

    // Conditional with await - each branch waits for a different timer
    if init.use_short_timer {
        timer!(ConditionalTimer::Short, 1.secs()).await;
        value += 10;
    } else {
        timer!(ConditionalTimer::Long, 2.secs()).await;
        value += 20;
    }

    Ok(ConditionalResult {
        used_short: init.use_short_timer,
        final_value: value,
    })
}

Parent-Child Workflow

Use case: Workflow that spawns multiple child workflows and aggregates results.

State Machine

    ┌─────────────┐
    │   PARENT    │
    └──────┬──────┘
           │
    for i in 0..n {
           │
           ▼
    ┌─────────────┐
    │ spawn child │
    │ wait result │
    └──────┬──────┘
           │
    completed_count += 1
           │
    }      │
           ▼
    ┌─────────────┐
    │  COMPLETED  │
    │ all_completed │
    └─────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::workflow;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParentInit { pub num_children: u32 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParentResult {
    pub children_spawned: u32,
    pub all_completed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildInit { pub delay_secs: u64 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildResult { pub completed: bool }

#[workflow]
fn parent_workflow(init: ParentInit) -> Result<ParentResult> {
    let mut completed_count: u32 = 0;

    // For loop with await - spawns children sequentially
    for _i in 0..init.num_children as usize {
        let child_init = ChildInit { delay_secs: 1 };
        let child_result: ChildResult = spawn!("child", child_init).await;

        if child_result.completed {
            completed_count += 1;
        }
    }

    Ok(ParentResult {
        children_spawned: init.num_children,
        all_completed: completed_count == init.num_children,
    })
}

Patrol Workflow

Use case: NPC walks between waypoints, responds to threats.

State Machine

        ┌──────────────────────────────────────────────────────────┐
        │                                                          │
        ▼                                                          │
    ┌───────┐                                                      │
    │ IDLE  │                                                      │
    └───┬───┘                                                      │
        │ start timer                                              │
        ▼                                                          │
    ┌─────────┐   arrival timer    ┌─────────────┐                 │
    │ WALKING │──────────────────▶│ AT_WAYPOINT │                 │
    └────┬────┘                    └──────┬──────┘                 │
         │                                │ wait timer             │
         │ threat_detected               ▼                        │
         │ signal              ┌─────────────────┐                │
         │                     │ next waypoint   │────────────────┘
         ▼                     └─────────────────┘
    ┌─────────┐
    │ COMBAT  │───▶ spawns CombatWorkflow
    └────┬────┘
         │ child completes
         ▼
    ┌───────────────┐
    │ RESUME PATROL │
    └───────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum PatrolTimer {
    StartPatrol,
    Arrival,
    Wait,
}

#[derive(Signal)]
pub enum PatrolSignal {
    ThreatDetected(u64),  // enemy_id
    StandDown,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatrolInit {
    pub waypoint_count: u32,
    pub walk_time_secs: u64,
    pub wait_time_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatrolResult {
    pub waypoints_visited: u32,
    pub threats_engaged: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatInit { pub enemy_id: u64 }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatResult { pub victory: bool }

#[workflow]
fn patrol(init: PatrolInit) -> Result<PatrolResult> {
    let mut waypoints_visited: u32 = 0;
    let mut threats_engaged: u32 = 0;

    // Initial delay before starting patrol
    timer!(PatrolTimer::StartPatrol, 2.secs()).await;

    loop {
        // Walk to next waypoint
        timer!(PatrolTimer::Arrival, init.walk_time_secs.secs()).await;
        waypoints_visited += 1;

        // Wait at waypoint, but respond to threats
        select! {
            timer!(PatrolTimer::Wait, init.wait_time_secs.secs()) => {
                // Check if we've completed the patrol
                if waypoints_visited >= init.waypoint_count {
                    break
                }
                continue
            },
            signal!(PatrolSignal::ThreatDetected(enemy_id)) => {
                // Spawn combat workflow and wait for result
                let combat_init = CombatInit { enemy_id };
                let _result: CombatResult = spawn!("combat", combat_init).await;
                threats_engaged += 1;
                // Resume patrol after combat
                continue
            },
            signal!(PatrolSignal::StandDown) => break,
        }.await;
    }

    Ok(PatrolResult {
        waypoints_visited,
        threats_engaged,
    })
}

Signals

Signal Payload Effect
threat_detected enemy ID (u64) Spawn combat, then resume
stand_down Return home, end patrol

Production Workflow

Use case: Factory that converts inputs to outputs on a cycle.

State Machine

    ┌─────────────────┐
    │ AWAITING_INPUTS │◀──────────────────────────┐
    └────────┬────────┘                           │
             │ inputs available                   │
             ▼                                    │
    ┌────────────────┐                           │
    │   PRODUCING    │                           │
    └────────┬───────┘                           │
             │ cycle_complete timer              │
             ▼                                    │
    ┌─────────────────┐                          │
    │ deposit output  │──────────────────────────┘
    │ cycles += 1     │
    └─────────────────┘

    Any state + "halt" signal ──▶ HALTED
    HALTED + "resume" signal ──▶ previous state

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum ProductionTimer { CycleComplete }

#[derive(Signal)]
pub enum ProductionSignal {
    Halt,
    Resume,
    Stop,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionInit {
    pub blueprint_id: String,
    pub cycle_duration_secs: u64,
    pub max_cycles: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionResult {
    pub cycles_completed: u32,
    pub was_halted: bool,
}

#[workflow]
fn production(init: ProductionInit) -> Result<ProductionResult> {
    let mut cycles_completed: u32 = 0;
    let mut halted: bool = false;

    loop {
        // Check for halt before each cycle
        if halted {
            select! {
                signal!(ProductionSignal::Resume) => {
                    halted = false;
                    continue
                },
                signal!(ProductionSignal::Stop) => break,
            }.await;
        }

        // Production cycle
        select! {
            timer!(ProductionTimer::CycleComplete, init.cycle_duration_secs.secs()) => {
                cycles_completed += 1;

                // Check if we've reached max cycles
                if cycles_completed >= init.max_cycles {
                    break
                }
                continue
            },
            signal!(ProductionSignal::Halt) => {
                halted = true;
                continue
            },
            signal!(ProductionSignal::Stop) => break,
        }.await;
    }

    Ok(ProductionResult {
        cycles_completed,
        was_halted: halted,
    })
}

Signals

Signal Payload Effect
halt Pause production
resume Continue production
stop End workflow

Combat Workflow

Use case: Turn-based combat with action submission and timeout.

State Machine

    ┌─────────────────────────────────────────────────────────────┐
    │                    COMBAT WORKFLOW                          │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌───────┐                                                  │
    │  │ START │  Initialize round counter                        │
    │  └───┬───┘                                                  │
    │      │                                                      │
    │      ▼                                                      │
    │  ┌─────────────────┐                                        │
    │  │ AWAITING_ACTION │◀─────────────────────┐                 │
    │  └────────┬────────┘                      │                 │
    │           │                               │                 │
    │    ┌──────┼──────────────┐                │                 │
    │    │      │              │                │                 │
    │ timeout submit_action  flee               │                 │
    │    │      │              │                │                 │
    │    ▼      ▼              ▼                │                 │
    │  round++ round++    ┌──────────┐          │                 │
    │    │      │         │ COMPLETE │          │                 │
    │    │      │         │ (fled)   │          │                 │
    │    │      │         └──────────┘          │                 │
    │    │      │                               │                 │
    │    └──────┴───────────────────────────────┘                 │
    │    (if round < max_rounds)                                  │
    │                                                             │
    │    (if round >= max_rounds)                                 │
    │           │                                                 │
    │           ▼                                                 │
    │  ┌─────────────────┐                                        │
    │  │    COMPLETE     │                                        │
    │  └─────────────────┘                                        │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum CombatTimer {
    ActionTimeout,
}

#[derive(Signal)]
pub enum CombatSignal {
    SubmitAction(CombatAction),
    Flee,  // Unit variant - no payload
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatAction {
    pub actor_id: u64,
    pub action_type: String,
    pub target_id: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatInit {
    pub attacker_id: u64,
    pub defender_id: u64,
    pub action_timeout_secs: u64,
    pub max_rounds: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatResult {
    pub winner_id: Option<u64>,
    pub rounds_fought: u32,
    pub ended_by_flee: bool,
}

#[workflow]
fn combat(init: CombatInit) -> Result<CombatResult> {
    let mut round: u32 = 0;
    let mut ended_by_flee: bool = false;

    loop {
        round += 1;

        select! {
            timer!(CombatTimer::ActionTimeout, init.action_timeout_secs.secs()) => {
                if round >= init.max_rounds {
                    break
                }
                continue
            },
            signal!(CombatSignal::SubmitAction(_action)) => {
                if round >= init.max_rounds {
                    break
                }
                continue
            },
            signal!(CombatSignal::Flee) => {
                ended_by_flee = true;
                break
            },
        }.await;
    }

    Ok(CombatResult {
        winner_id: if ended_by_flee { Some(init.defender_id) } else { None },
        rounds_fought: round,
        ended_by_flee,
    })
}

Signals

Signal Payload Effect
submit_action CombatAction Record action, continue to next round
flee End combat immediately

Respawn Workflow

Use case: Simple uninterruptible timer to respawn an entity after death.

State Machine

    ┌────────┐                ┌─────────┐                ┌──────────┐
    │ START  │───schedule────▶│ WAITING │───timer:spawn─▶│ COMPLETE │
    └────────┘                └─────────┘                └────┬─────┘
                                                              │
                                           ┌──────────────────┘
                                           │ Side effects:
                                           │ • Create entity
                                           │ • Start AI workflow
                                           │ • Initialize inventory
                                           └────────────────────────

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer};

#[derive(Timer)]
pub enum RespawnTimer { Spawn }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespawnInit {
    pub entity_template: String,
    pub spawn_location_id: u64,
    pub respawn_delay_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespawnResult {
    pub spawned_entity_id: u64,
}

// Simple uninterruptible workflow - no signals, just wait and spawn
#[workflow]
fn respawn(init: RespawnInit) -> Result<RespawnResult> {
    // Wait for respawn delay
    timer!(RespawnTimer::Spawn, init.respawn_delay_secs.secs()).await;

    // Spawn the entity via reducer (returns new entity ID via table)
    reducer!(spawn_entity(
        init.entity_template.clone(),
        init.spawn_location_id
    ));

    // In real code, you'd look up the spawned entity ID
    // For demo, using placeholder
    Ok(RespawnResult {
        spawned_entity_id: 0,
    })
}

Key pattern: No select!, no signals - purely timer-driven. The entity will respawn no matter what. This is the simplest workflow pattern.


Day/Night Cycle Workflow

Use case: Global world state that cycles between time-of-day phases.

State Machine

    ┌─────────────────────────────────────────────────────────────┐
    │                  DAY/NIGHT CYCLE                            │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌──────┐   ┌─────┐   ┌──────┐   ┌───────┐                  │
    │  │ DAWN │──▶│ DAY │──▶│ DUSK │──▶│ NIGHT │──┐               │
    │  └──────┘   └─────┘   └──────┘   └───────┘  │               │
    │      ▲                                      │               │
    │      └──────────────────────────────────────┘               │
    │                                                             │
    │  Transitions: timer:phase_end OR signal:skip_phase          │
    │  Exit: signal:stop                                          │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum DayNightTimer {
    PhaseEnd,
}

#[derive(Signal)]
pub enum DayNightSignal {
    SkipPhase,  // Jump to next phase immediately
    Stop,       // End the cycle
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TimeOfDay { Dawn, Day, Dusk, Night }

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DayNightInit {
    pub phase_duration_secs: u64,
    pub starting_phase: TimeOfDay,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DayNightResult {
    pub cycles_completed: u32,
    pub final_phase: TimeOfDay,
    pub stopped_by_signal: bool,
}

#[workflow]
fn day_night(init: DayNightInit) -> Result<DayNightResult> {
    let mut current_phase: u8 = match init.starting_phase {
        TimeOfDay::Dawn => 0,
        TimeOfDay::Day => 1,
        TimeOfDay::Dusk => 2,
        TimeOfDay::Night => 3,
    };
    let mut cycles_completed: u32 = 0;
    let mut stopped: bool = false;

    loop {
        select! {
            timer!(DayNightTimer::PhaseEnd, init.phase_duration_secs.secs()) => {
                current_phase = (current_phase + 1) % 4;
                if current_phase == 0 {
                    cycles_completed += 1;
                }
                continue
            },
            signal!(DayNightSignal::SkipPhase) => {
                current_phase = (current_phase + 1) % 4;
                if current_phase == 0 {
                    cycles_completed += 1;
                }
                continue
            },
            signal!(DayNightSignal::Stop) => {
                stopped = true;
                break
            },
        }.await;
    }

    Ok(DayNightResult {
        cycles_completed,
        final_phase: match current_phase {
            0 => TimeOfDay::Dawn,
            1 => TimeOfDay::Day,
            2 => TimeOfDay::Dusk,
            _ => TimeOfDay::Night,
        },
        stopped_by_signal: stopped,
    })
}

Signals

Signal Payload Effect
skip_phase Jump to next phase immediately
stop End the cycle

Key pattern: Infinite loop with explicit stop signal. Use correlation_id to create a singleton ("global_day_night") so you can find it later. Good for world-state workflows that run for the lifetime of the server.


Auction Workflow

Use case: Timed auction with bidding and optional instant buyout.

State Machine

    ┌─────────────────────────────────────────────────────────────┐
    │                      AUCTION                                │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌───────┐                                                  │
    │  │ START │  Set starting price                              │
    │  └───┬───┘                                                  │
    │      │                                                      │
    │      ▼                                                      │
    │  ┌─────────────────┐                                        │
    │  │ AWAITING_BIDS   │◀─────────────────────┐                 │
    │  └────────┬────────┘                      │                 │
    │           │                               │                 │
    │    ┌──────┼──────────────┬───────┐        │                 │
    │    │      │              │       │        │                 │
    │ timeout  bid           buyout  cancel     │                 │
    │    │      │              │       │        │                 │
    │    ▼      ▼              ▼       ▼        │                 │
    │  end   update bid    ┌────────┐  end      │                 │
    │    │   (if higher)   │ SOLD   │ (no bids) │                 │
    │    │      │          └────────┘           │                 │
    │    │      └───────────────────────────────┘                 │
    │    │                                                        │
    │    ▼                                                        │
    │  ┌─────────────────┐                                        │
    │  │    COMPLETE     │  (sold if had bids)                    │
    │  └─────────────────┘                                        │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum AuctionTimer { End }

#[derive(Signal)]
pub enum AuctionSignal {
    Bid(BidData),
    Buyout(u64),  // bidder_id
    Cancel,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BidData {
    pub bidder_id: u64,
    pub amount: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuctionInit {
    pub item_id: u64,
    pub seller_id: u64,
    pub starting_price: u64,
    pub buyout_price: Option<u64>,
    pub duration_secs: u64,
}

#[workflow]
fn auction(init: AuctionInit) -> Result<AuctionResult> {
    let mut current_bid: u64 = init.starting_price;
    let mut high_bidder: u64 = 0;
    let mut sold: bool = false;

    loop {
        select! {
            timer!(AuctionTimer::End, init.duration_secs.secs()) => {
                if high_bidder > 0 { sold = true; }
                break
            },
            signal!(AuctionSignal::Bid(bid)) => {
                if bid.amount > current_bid {
                    current_bid = bid.amount;
                    high_bidder = bid.bidder_id;
                }
                continue
            },
            signal!(AuctionSignal::Buyout(bidder_id)) => {
                if let Some(price) = init.buyout_price {
                    current_bid = price;
                    high_bidder = bidder_id;
                    sold = true;
                    break
                }
                continue
            },
            signal!(AuctionSignal::Cancel) => {
                if high_bidder == 0 { break }
                continue
            },
        }.await;
    }

    Ok(AuctionResult { sold, final_price: current_bid, winner_id: high_bidder })
}

Signals

Signal Payload Effect
bid BidData Update high bid if higher
buyout bidder_id End auction immediately at buyout price
cancel Cancel if no bids yet

Trade Workflow

Use case: Two-player item exchange requiring both parties to accept.

State Machine

    ┌─────────────────────────────────────────────────────────────┐
    │                        TRADE                                │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌───────┐                                                  │
    │  │ START │  Both players not accepted                       │
    │  └───┬───┘                                                  │
    │      │                                                      │
    │      ▼                                                      │
    │  ┌─────────────────┐                                        │
    │  │ AWAITING_ACCEPT │◀─────────────────────┐                 │
    │  └────────┬────────┘                      │                 │
    │           │                               │                 │
    │    ┌──────┼──────────────┬───────┐        │                 │
    │    │      │              │       │        │                 │
    │ timeout accept(p1/p2) cancel  modify      │                 │
    │    │      │              │       │        │                 │
    │    ▼      ▼              ▼       ▼        │                 │
    │  expire  track who   ┌────────┐ reset     │                 │
    │    │     accepted    │COMPLETE│ accepts   │                 │
    │    │      │          │(cancel)│     │     │                 │
    │    │      │          └────────┘     │     │                 │
    │    │      │                         │     │                 │
    │    │    both? ──No──────────────────┴─────┘                 │
    │    │      │                                                 │
    │    │     Yes                                                │
    │    │      │                                                 │
    │    ▼      ▼                                                 │
    │  ┌─────────────────┐                                        │
    │  │    COMPLETE     │                                        │
    │  └─────────────────┘                                        │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum TradeTimer { Expire }

#[derive(Signal)]
pub enum TradeSignal {
    Accept(u64),       // player_id
    Cancel(u64),       // player_id
    ModifyOffer(u64),  // player_id - resets acceptances
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeInit {
    pub player1_id: u64,
    pub player2_id: u64,
    pub player1_offers: Vec<u64>,
    pub player2_offers: Vec<u64>,
    pub timeout_secs: u64,
}

#[workflow]
fn trade(init: TradeInit) -> Result<TradeResult> {
    let mut player1_accepted: bool = false;
    let mut player2_accepted: bool = false;

    loop {
        select! {
            timer!(TradeTimer::Expire, init.timeout_secs.secs()) => break,
            signal!(TradeSignal::Accept(player_id)) => {
                if player_id == init.player1_id { player1_accepted = true; }
                else if player_id == init.player2_id { player2_accepted = true; }
                if player1_accepted && player2_accepted { break }
                continue
            },
            signal!(TradeSignal::Cancel(_player_id)) => break,
            signal!(TradeSignal::ModifyOffer(_player_id)) => {
                player1_accepted = false;
                player2_accepted = false;
                continue
            },
        }.await;
    }

    Ok(TradeResult { completed: player1_accepted && player2_accepted })
}

Signals

Signal Payload Effect
accept player_id Mark player as accepted
cancel player_id End trade immediately
modify_offer player_id Reset both acceptances

Key pattern: Multi-party coordination. Both parties must accept before completion. Modifying the offer resets both acceptances.


Quest Workflow

Use case: Multi-objective quest with progress tracking.

State Machine

    ┌─────────────────────────────────────────────────────────────┐
    │                        QUEST                                │
    ├─────────────────────────────────────────────────────────────┤
    │                                                             │
    │  ┌───────┐                                                  │
    │  │ START │  0 objectives complete                           │
    │  └───┬───┘                                                  │
    │      │                                                      │
    │      ▼                                                      │
    │  ┌─────────────────┐                                        │
    │  │ IN_PROGRESS     │◀─────────────────────┐                 │
    │  └────────┬────────┘                      │                 │
    │           │                               │                 │
    │    ┌──────┼──────────────┬───────┐        │                 │
    │    │      │              │       │        │                 │
    │ timeout objective     abandon  fail       │                 │
    │    │      │              │       │        │                 │
    │    ▼      ▼              ▼       ▼        │                 │
    │  expire  done++      ┌────────────────┐   │                 │
    │    │      │          │    COMPLETE    │   │                 │
    │    │      │          │(abandon/fail)  │   │                 │
    │    │      │          └────────────────┘   │                 │
    │    │      │                               │                 │
    │    │   all done? ──No────────────────────┘                  │
    │    │      │                                                 │
    │    │     Yes                                                │
    │    │      │                                                 │
    │    ▼      ▼                                                 │
    │  ┌─────────────────┐                                        │
    │  │   COMPLETE      │                                        │
    │  └─────────────────┘                                        │
    │                                                             │
    └─────────────────────────────────────────────────────────────┘

Code

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum QuestTimer { Expire }

#[derive(Signal)]
pub enum QuestSignal {
    ObjectiveComplete(u32),  // objective_index
    Abandon,
    Fail,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuestInit {
    pub quest_id: String,
    pub player_id: u64,
    pub objective_count: u32,
    pub time_limit_secs: Option<u64>,
}

#[workflow]
fn quest(init: QuestInit) -> Result<QuestResult> {
    let mut objectives_done: u32 = 0;
    let mut abandoned: bool = false;
    let mut failed: bool = false;
    let timeout = init.time_limit_secs.unwrap_or(86400);

    loop {
        select! {
            timer!(QuestTimer::Expire, timeout.secs()) => {
                if init.time_limit_secs.is_some() { failed = true; }
                break
            },
            signal!(QuestSignal::ObjectiveComplete(index)) => {
                if index < init.objective_count { objectives_done += 1; }
                if objectives_done >= init.objective_count { break }
                continue
            },
            signal!(QuestSignal::Abandon) => { abandoned = true; break },
            signal!(QuestSignal::Fail) => { failed = true; break },
        }.await;
    }

    Ok(QuestResult {
        completed: objectives_done >= init.objective_count && !abandoned && !failed,
        objectives_done,
        abandoned,
        failed,
    })
}

Signals

Signal Payload Effect
objective_complete index Increment progress
abandon Player gave up
fail Quest failed (e.g., NPC died)

Key pattern: Progress tracking with multiple exit conditions. Use entity_id to attach to player for easy querying.


Common Patterns Summary

Pattern Example Technique
Looping Patrol waypoints loop { select! { ... }.await }
Interruptible Buff dispel signal!() arm with break
Uninterruptible Respawn No signal arms, only timer
Timeout Combat actions Timer arm with default action
Child spawn Patrol → Combat spawn!("workflow", init).await
Pause/Resume Production halt State flag + conditional select
Counter loop Parent-child for i in 0..n { spawn!().await }
Conditional Feature flags if cond { timer!().await }
Payload binding Stack amount signal!(Signal::Stack(n))

Next Steps

API Reference — Complete API documentation → Deployment — Production setup