Commands are intent, events are fact.
An experimental C# command tree framework, inspired by the Chain mechanic in OCG card games like Yu-Gi-Oh!. It is a slice extracted from a larger personal framework, kept to its core design points.
In Yu-Gi-Oh!, when a card activates an effect, another card can chain onto it, and a third onto that — forming a chain. When it resolves, the chain unwinds in reverse (last-in, first-out): the final link resolves first, back to the first link.
This framework bets on one idea:
Any complex logic can be described as the growth of a command tree and the chaining of its effects. And that process can itself be observed and described.
A "transfer" is simply moving money from A to B, yet it chains a series of effects — charge a fee, send a notification, write an audit log. None of these effects live inside the transfer command; they are chained onto it. On undo, they roll back in reverse — last-in, first-out — exactly mirroring chain resolution.
Yu-Gi-Oh!'s chain is a line; here it grows into a tree — one effect can chain multiple branches, and each branch can chain further.
Conventional command patterns are flat queues: "execute one, discard one", with undo maintained as a separate stack. Not here:
Root (Cmd_Void) ← an empty root that never undoes
├── Cmd_Transfer ← transfer money
│ ├── Cmd_DeductFee ← chains: charge a fee
│ ├── Cmd_SendNotification ← chains: send a notification
│ └── Cmd_RecordAuditLog ← chains: write an audit log
└── Cmd_Transfer ← an independent transfer
- Every command node can have children — composite logic is expressed as tree hierarchy, not "one big command that does everything".
- Only commands declared
IBranchablemay hold children; the rest are leaves. Branching is an explicitly granted capability. - Undo = rolling back the whole subtree in reverse (last-in, first-out): undoing the transfer first rolls back its chained subcommands, and only then the transfer itself.
- Serialize = a pre-order walk of the whole tree: the recursive
[typeId][payload][childCount]layout makes the "process of logic" itself the save format.
This is what "the process can be observed and described" means — you don't get a black-box result, you get a tree that is readable, replayable, traceable:
- Readable:
DebugAll()prints every link of the tree; - Replayable: deserialization rebuilds the tree and replays the logic verbatim;
- Traceable: undo is not "jump back to a state", but walking every link of the tree in reverse.
This is the most distinctive part. Cmd_Transfer does one thing — move the money. It has no idea that "charge a fee" exists:
// Transfer command: focused on one thing, oblivious to what chains onto it
public struct Cmd_Transfer : ICommand<Account>, IBranchable
{
public int Amount;
public bool CanExecute(Account a) => Amount > 0 && a.Balance >= Amount;
public void Execute(Account a) => a.Balance -= Amount; // transfer out
public void Undo(Account a) => a.Balance += Amount; // transfer back
public string Log() => $"Transfer({Amount})";
}
// Trigger: an independently registered chainer, declaring "I chain onto Cmd_Transfer"
public sealed class Tri_ChargeFee : ICommandTrigger<Account, Cmd_Transfer>
{
public short Priority => 1; // multiple chains are ordered by priority
public bool Trigger(ICommandMachine<Account> machine, in Cmd_Transfer command)
=> command.Amount > 1000; // condition: fee only on large transfers
public void Insert(ICommandMachine<Account> machine, ICommandNode<Account, Cmd_Transfer> commandNode)
=> machine.Execute(commandNode, new Cmd_DeductFee { Amount = command.Amount / 100 }); // chain it on
}When an IBranchable command executes, the machine finds the triggers that chain onto it and asks each Trigger in priority order; for those returning true, Insert chains a new command onto the current node. Therefore:
- Open/closed principle: adding an effect to "transfer" (fee, notification, audit…) never requires touching
Cmd_Transfer— just register a new trigger. - Composition over hard-coding: a "place order" can chain "deduct stock", "create shipment", "charge payment" — none of them live inside
Cmd_PlaceOrder; triggers weave them into the tree dynamically. - Undo and save for free: because chained subcommands are part of the tree, they roll back in reverse and persist together with their trunk — no extra work.
Resulting properties:
- Zero GC:
unmanagedstructs +inparameters + a type-safe object pool; command execution allocates nothing on the heap. - Engine-agnostic: pure BCL, zero dependencies — works in Unity, Godot, and native .NET.
- Dual mode: by default execute-and-recycle (one-shot intent); with
IsSavingenabled, history is kept (undoable, serializable).
This is a slice of a larger personal framework, still in rapid iteration, showing only the core design points — not a stable library. The API evolves fast, may break without notice, and makes no semver promise. Feel free to play with it, take it apart, change it — and open an issue if you find problems.