Ariadne runs gameplay scripts (missions, quests, tutorials) that can be saved midflight and still loaded after the script itself has been edited (that is, the scripts can be versioned). A script is an Arc subclass that overrides Run: plain C#, where each pause is a labeled loop ending in Yield(): build_generator: while (!HasGenerator(world)) { Show("Build a generator"); Yield(); } A pause can also be a bare Yield() after a label, including inside blocks, ifs, switches and while, do and for loops. Anywhere else, such as inside a foreach, lambda or try, is an error. A goto works as it does in C#: it can leave a block to reach a label in an enclosing one, and the label can be on a pause or on an ordinary statement. Sibling blocks can each declare a label of the same name. Their save keys count up in source order ("step", then "step#1"), so swapping the blocks swaps which saved position belongs to which. Any method that Run can call that has Yield() in its call stack is a Passage. The path of passage names and labels the script is paused at (eg: "ConnectYourOwnGenerator.build_generator") gets saved out. Loading jumps straight to that path rather than replaying the script, so code before it never runs again. An old save naming a label that no longer exists resumes at the start of the nearest enclosing passage instead. Locals and passage parameters still needed after a pause are saved, too. A passage must be an instance method. It can't be static, async, unsafe, an iterator, or an override of a method that isn't a passage. Each of those is an error. A passage can be generic, as in `Wait(…)`, with locals of those types kept across its pauses. Each call site keeps a frame for it, typed with the type arguments of that call, and saves its locals in a table for the call. A generic passage can call other passages, generic or not, and passages of other arcs. The locals of an ordinary passage that it calls stay where they are, with the arc's own. A generic passage can also be called on another arc, as in `greenhouse.Grow(…)`, and can take ref and out parameters of its type parameters. A passage can call itself, directly or through other passages, and each level keeps its own locals. The passages in such a cycle each keep their locals in a frame of their own, and a call keeps the frame of the level below it, so the frames form a chain as long as the calls go. Each is made the first time its call is reached and reused after that. A save holds a table for each level inside the one above, so its keys grow with the depth and the whole save grows with the square of it. A cycle that never ends is stopped with an exception when the stack runs low, rather than overflowing it. Loading a save checks the stack in the same way. Run can be part of a cycle too. It runs again from the top with a frame of its own, and its locals for the first lap sit at the top of the save. A generic passage in a cycle can pass its type parameters on as they are, swapped, or replaced by types that don't use them, and its frames are then a few fixed types. It can also build a type argument from its type parameters, as in `Wrap>` inside `Wrap`, so every level is a frame of a new type. Such a call keeps its callee's frame as a plain `Frame`, and the save names the type arguments in the call's table (`types`), so a load can build the frame again with `MakeGenericType`. The names can only spell types the save library has registered, and names that don't fit the passage fail the load. The locals of such a level can't be listed before the save is read, so the save library lists them from the closed frame type the names make, as it walks down the save. This needs the runtime to build types it didn't see when it was built. A runtime that compiles ahead of time can't do that for every type, so a game that targets one should keep polymorphic recursion to types it has used. The interpreter and the .NET Framework can. A passage can live in a base class, with or without a Run of its own, and the scripts that derive from it call it like one of their own. It runs on the derived arc, and its locals are saved with that arc's under the passage's own keys, so a passage of a derived class can't share its name with one it inherits, unless it overrides it. The base class can be built into another assembly, as long as that assembly ran Ariadne.Generator too. A Run that overrides its base class's can call it, as in `base.Run(world);`, from Run or from any other passage. The base's Run keeps its locals in a frame of the base class's type, made when the call is reached and reused after that. Its locals therefore stay apart from the caller's, even when both use the same names. A save holds them in a table inside the caller's locals, named for the call, as in locals["Run.Run/"]["Run.count"]. The base class can be built into another assembly. Every virtual or abstract method that a script class declares is a passage, whether or not its own body pauses, since an override might. So is every override of one. A virtual method with an expression body is not, since a pause needs a labeled block, and an override of one can't pause (ARI0025 says to give the base a block body). A call to it runs the most derived override on the arc, as in C#, and a call through base, as in `base.Greet(world);`, runs the version above on a frame of its own, as base.Run does. An override keeps its locals under its class's name, as in locals["Loud.Greet.volume"], so a local it shares a name with in the passage it overrides can be another type. An abstract passage has nothing to run, and an override of one is written like any other. The override of a method of Arc itself, or of any class that isn't a script, stays an ordinary method. A passage is called as a statement, so a virtual helper used inside a bigger expression is spilled like any other, and a virtual method can't be named without being called. A virtual passage keeps its locals in the arc's frame, which holds one run of it at a time. One that is generic, or that calls itself, is an error (ARI0038). Put the recursion or the type parameter in a passage that isn't virtual, and make only the step it calls at each level virtual. A call that goes round through an override the base can't see is caught when it happens: starting the passage again before it has finished throws, and the arc is quarantined as for any throw. A call to Run by name always runs the Run of the class that makes it, because it isn't dispatched on the arc's type. C# would run the override. So a derived class that calls base.Run, or an inherited passage, which reaches a call to Run by name in the base is ARI0036. Put the shared work in a passage with another name and call that from both. The generator can't read the bodies of a class built into another assembly, so it marks the copy of each passage that reaches such a call (ReachesRunByName) and reads the mark there. A library built before the mark existed has none, so it is unchecked until it is rebuilt. The base's Run therefore never keeps a frame of its own that a derived class's base.Run call has to find. A passage can only be called. Naming it without calling it, as in `Action later = Act;`, is ARI0037, since a delegate carries no frame or cursor for it and can't be saved. A passage can also be called on another arc, as in `plant.Update(world);`, if that arc's class is a script in the same project or in another assembly that ran Ariadne.Generator. The generator tells a passage there from any other method by the resumable copy it wrote beside it, so an assembly built without the generator, or before a passage was added, has nothing to call and its methods are called like any object's. The passage runs on that arc, so whatever it changes lands there. Only what it needs to resume is kept by the caller: the path it is paused at, and its locals, which are saved in a table inside the caller's locals named for the call, as in locals["Run.update/"]["Update.waited"]. The arc to run it on is worked out once, when the call begins, and is saved in that table too, as "receiver". Such a call can return a value and sit inside a bigger expression, like a call to a passage of the arc itself, as in `Log("total " + (before + plant.Measure(world)))`. A passage can return a value, as in `int choice = AskPlayer(world);`. The value is handed back the moment the passage finishes, so it is never saved. The call can also sit inside a bigger expression, such as `Log(prefix + AskPlayer(world))`. The generator then stores what the expression read before the call, in C#'s order, so the pause doesn't change the answer. The exception is a name or a chain of fields and properties that holds an object, like `world.Player`. Those are read again after the pause, since a copy kept across it would be a different object than the world's own. A call on the right of `&&`, `||`, `??` or `?.`, or in either branch of `?:`, only runs when that side is reached. The conditions of ifs and loops, a for's lone initializer and step, and a switch's governing expression take such a call too. So do patterns, switch expression arms, several declarators, and assignments such as `total += Ask(world)`, `slots[i] = Ask(world)` and `var (low, high) = Range(world)`. A passage can have out parameters. The caller sees each one when the passage ends, not before, and `out int choice` and `out var choice` both work. A ref parameter changes the caller's variable in place, and a ref readonly parameter reads it live. Those kinds need the caller to name a plain variable or field, since the argument is worked out again each time the passage resumes. An in parameter is a copy, taken once when the passage is called. Every other argument is worked out once, on the way in, never again when the passage resumes. That includes the ones a params parameter takes, which are made into a single array on the way in. Resuming builds nothing. A passage's params parameter has to be an array, not one of C# 13's other params collections. Ariadne.Generator compiles each script into the code that actually runs, and places a copy of it beside the script.cs file as Script.CodeBehind.cs. Breakpoints, errors and stack traces all stay on the original script's own lines. To use it, a project needs: with the Import after Microsoft.CSharp.targets. Every assembly with arcs in it needs the generator, including one holding only a base class for scripts, since it writes the code that saves each arc class's fields and the constructor that loads it from a save. So every arc class must be partial, and so must any type it is declared inside. A script class can have type parameters, and a passage can be called on an arc of a constructed class like Plant. Two scripts in different folders can share a file name. The loading constructor takes an IArcReader and is private, or protected on a class others derive from. A class can write its own to adjust what loaded, chaining to the generated one with : this(reader, default(AutoLoadEveryField)). Loading never runs a script's constructors, so a field the save lacks keeps only its initializer. One without an initializer makes the load throw an InvalidDataException naming the field, rather than come back as a null or zero. A loading constructor can't rescue it, since the generated one throws first. A value that is in the save but can't be read is different. That is recorded in the session's sandbox, and the field keeps its initial value. Ariadne itself doesn't know how scripts are written to disk. Ariadne.Vellum is the bridge that saves them with Vellum. Its ArcConverter is a converter family for every arc class, so an arc is a value like any other: it can be a field of any saved object, and a save holding many arcs and the rest of a game's data keeps every object they share as one object when it's loaded. A project that uses it needs Vellum.Generator as an analyzer too. The host runs an arc with Tick(context, tick). The TickContext holds the ExceptionSandbox, which needs Ariadne as one of its anchors, and two times, DeltaTime and GameTime. They are Amphora Durations, which a TimeSpan converts to implicitly, and they must be finite. The host advances the sandbox's clock, once per frame. GameTime can stop while the game is paused, which the sandbox's UI time never does. A passage reads the times as DeltaTime and GameTime, which the generator turns into a hidden parameter every passage copy is handed, so a passage run on another arc sees the caller's times and no arc stores them. Nothing else can read them, and ARI0030 reports a method that isn't a passage doing so: hand the time to it as an argument. Neither time is saved, so a script that needs one later keeps it in a local, as in `Duration deadline = GameTime + Duration.FromSeconds(1);`. Wait(span) pauses for that much game time, so a paused game doesn't count down. Like Yield() it is a statement of its own after a label, and it pauses in the same places. The generator turns it into a deadline, GameTime plus the span, saved under the passage's key ("Run.__wait1") and tested on each tick, so a save in the middle of a wait keeps the time left exactly. The span is an Amphora Duration, which a TimeSpan converts to implicitly. A span of zero or less doesn't pause. Duration.Forever, and Timeout.InfiniteTimeSpan which converts to it, never ends, so `Wait(Duration.Forever)` is how to park an arc for good. A span too long to add to the game time is the longest Duration there is, about 29,000 years, which is as good as Forever. Nothing throws, and nothing wraps. The span is worked out once, on the way in. It can call a passage of this arc, as in `Wait(Ask(world))` or `Wait(other.Ask(world))`: the call runs first, for as long as it pauses, and the deadline counts from when it finishes. A Trigger is an event that arcs wait on. `Wait(trigger)` pauses the arc until the trigger is raised, `WaitAny(a, b)` until either is, and `WaitAll(a, b)` until both have been. They pause in the same places as Wait(span). WaitAny and WaitAll return the Trigger whose raise ended the wait, which is the first one raised for WaitAny and the last for WaitAll, and the call can be a statement, a declaration, an assignment or a return. The result is never saved, since it only exists as the wait ends. A trigger wait always pauses, even if the trigger was raised a moment before, since a raise is an event and not a state. Raise() marks every arc waiting at that moment, in the order the arcs were added to their scheduler, and they run on their next tick. A script that raises a trigger for another to wait on can therefore hand work along no faster than a frame at a time. Raise() doesn't need a TickContext, so it can be called outside a tick. Waiting needs an ArcScheduler to hold the arc, and ticking the arc without one throws. The first parameter can be `timeout:`, as in `Wait(timeout: Duration.FromSeconds(5), trigger: door)`, and it comes first so that WaitAny and WaitAll can still take a params list. The wait then returns null when the time runs out. A trigger raised on the tick the deadline falls on wins, and a timeout of zero or less doesn't pause. A timeout of Forever, or Timeout.InfiniteTimeSpan, waits on the triggers alone. The deadline is saved as a Wait(span) deadline is, so a save in the middle of the wait keeps the time left. A wait that timed out is ended, so a later raise of its triggers doesn't wake an arc that has moved on. A save keeps the triggers a wait holds and which of them were raised, and a load subscribes the arc to them again. A Latch is a condition that stays set until it is cleared, for a fact that lasts, such as a door being open. `Wait(latch)` pauses until it is set, and `Wait(!latch)` until it is clear. Unlike a trigger wait, one whose condition already holds doesn't pause. The pause asks the latch each time the arc resumes, so a latch cleared between the wake and the arc's next tick sends the arc back to waiting, and the arc never sees a condition that isn't true. Set() on a set latch and Clear() on a clear one do nothing. `Wait(latch, timeout: span)` returns the latch, or null when the time runs out. A latch is part of the world, so its state and the arcs waiting on it are saved with it, and a wait on a latch saves which latches it holds. `!latch` is a read-only inverse the latch makes once, so waiting on it allocates nothing. `WaitAny(a, !b)` and `WaitAll(a, !b)` wait on a list of latches or inverses, with `timeout:` first as for triggers. WaitAny returns the first listed latch that is set, which isn't always the first to be set, and WaitAll returns the last listed. Either returns null when the time runs out, so a result that isn't null tells the script it can go on. A list holds triggers or latches, not both, and the generator reports a mixed list. A WaitAll that lists a latch and its inverse could never end, so it throws and the sandbox quarantines the arc. A WaitAny of the two is always met. An ArcScheduler ticks a set of arcs in the order they were added, and spends nothing on the ones that are waiting. It is only a faster way to do what ticking every arc each frame in that order would do, so scripts can't tell the difference. An arc paused in a Wait is put in a priority queue by its deadline, and ticked again on the frame the game time reaches it. Each frame costs one look at the head of the queue, plus the work of the arcs that run. A Wait reports its deadline through a WakeSlot the scheduler hands the arc, which a passage run on another arc shares, so that wait puts the arc that was added to sleep. A wait on triggers or latches suspends the arc through the same slot, so it costs nothing until a Raise() or a Set() wakes it, and a timeout also reports its deadline, as a Wait(span) does. An arc that overrides TickSelf never sleeps, since it may be ticking other arcs. An arc that waits fewer than MinimumSleepFrames frames, 8 unless it is set, stays awake, since a trip through the queue costs more than the ticks it would skip. An arc added during a tick first runs on the next one. Remove, Wake and Restart take an arc out, wake it early, or restart it so that it doesn't sleep through the restart. A restart also ends any wait on triggers or latches the arc was in, so they no longer wake it. An arc whose script throws stays in the scheduler, and is skipped until the sandbox releases it, as it would be without one. The queue is Amphora's IndexedPriorityQueue. An exception a script throws is recorded in the sandbox and swallowed, and the arc is quarantined where it paused. A step isn't safe to run twice, so the arc isn't ticked again until the host calls sandbox.Release(AriadneSites.Tick(arc)). Its state still saves and loads as usual. The sandbox raises SiteQuarantined on its next Advance, which is where a host can let go of whatever the script was holding. An arc can override AddFaultNotes to say what it was doing, such as the area or the character it was talking to. A script can't tick another arc, since it never sees the TickContext. An arc that holds other arcs overrides TickSelf instead. Tick calls it inside the arc's own fault boundary. The default runs the script, and returns true once the arc is done. An override can tick its children before or after calling base.TickSelf, or never call it, and it returns whether the arc is done. Each child's Tick is its own boundary, so a child that throws is quarantined alone. A throw from the override quarantines the arc that owns it. Ticking an arc that is already being ticked, itself included, throws. The script decides when a child starts and what it waits for, as in "while (!child.Finished) Yield();". Children are fields, so a save keeps them. A script that needs random numbers holds a Rand from Amphora in a field, as in `public readonly Rand Rand;`, and rolls with `Rand.NextInt(1, 7)`. It is an ordinary call, so nothing in Ariadne rewrites it. The host decides what the Rand is: a new one seeded from the clock, a Spawn() of its own, or the same Rand it gives other arcs. A save keeps the Rand's stream, so a loaded arc rolls what it would have rolled, and arcs given one Rand load still sharing it. A passage run on another arc rolls on that arc's own Rand, since it is that arc's field. Hand both arcs the same Rand to make them one stream. To wait for something that happens at random, as in "a raid comes about every 12 days", pass the draw to Wait: `Wait(Rand.NextEvent(Duration.FromDays(12)))`. The wait is drawn once, when the Wait first runs, and the deadline is saved like any other, so a loaded arc wakes when the unsaved one would have and draws nothing more. A mean of Forever draws a wait of Forever, so an event that can't happen never wakes the arc. Rand.NextEventWithin(mean, span) gives when in the span an event happened, given that one did, and Rand.EventCount(mean, span) how many happened.