Generator¶
Namespace Nfty.Core.Generation in assembly Nfty.Core.
The generation pipeline. Per asset: roll a recipe, roll each layer's variant, apply the recipe's rules, compute the DNA, reject a duplicate, and only then render — colorize, composite, emit.
Rolling and rendering are separate deliberately: the DNA is a function of the selection alone, so a collision is discarded without paying for the pixels that would have been thrown away with it.
The roll itself lives in Ticketer. This class projects the book into a MenuBook (MenuProjection) and keeps only the render — resolving a ticket's ids to the actual decoded images, colorizing, compositing. There is exactly one definition of the order in which layers consume the RNG.
Methods¶
Generate(LoadedCookBook, GenerateOptions, IReadOnlyList<string>?, int, IProgress<GenerationProgress>?, CancellationToken)¶
public static GeneratedSet Generate(LoadedCookBook book, GenerateOptions opts, IReadOnlyList<string>? existingDnas = null, int startNumber = 1, IProgress<GenerationProgress>? progress = null, CancellationToken cancellationToken = default);
Generates the whole collection into memory. The returned set owns every asset image — dispose it when done. For large runs prefer Generator.GenerateStreaming(), which never holds more than one asset at a time.
Parameters
book(LoadedCookBook) - The cookbook to roll from; validated first. The caller keeps ownership.opts(GenerateOptions) - Count, seed and the other per-run settings.existingDnas(IReadOnlyList<string>?) - DNA of assets already produced (anextendrun), so no new asset repeats one. Null for a fresh run.startNumber(int) - The set number of the first asset produced; 1 for a fresh run.progress(IProgress<GenerationProgress>?) - Receives one report per asset, one at a time, in increasing order. Null for none.cancellationToken(CancellationToken) - Stops the run between assets.
Returns. The whole collection. The set owns every asset image; dispose it.
Exceptions
InvalidOperationException-GenerateOptions.Countis not positive, the book failsValidator(the message lists every problem), orGenerateOptions.RecipeIdnames no recipe.RuleConflictException- The recipe rules exclude every combination, so no asset can be rolled.UniqueSpaceExhaustedException- The unique DNA space is real but smaller than the count requested.OperationCanceledException-cancellationTokenwas canceled.
Remarks
Two phases, and only the second is parallel. Rolling consumes the RNG and does the duplicate check, and both depend on the exact order every asset is decided in — so the roll phase is sequential, always, and produces the identical stream of decisions this method has always produced. Rendering is then a pure function of one asset's own already-decided ticket: resolve each layer's image, colorize, composite, done. Nothing it does can observe another asset, the thread it runs on, or how many threads there are.
That is what makes this safe for reproducibility, and it is a stronger claim than "same machine": results cannot vary with core count or scheduling because no parallel step aggregates anything. There is no summing, no ordering, no shared accumulator — each asset is written to its own slot in a pre-sized array, so the collection comes out in roll order on any machine. GeneratorParallelismTests asserts this against Generator.GenerateStreaming(), which is the sequential oracle: same seed, byte-identical pixels, identical DNA, identical order.
GenerateAsync(LoadedCookBook, GenerateOptions, IReadOnlyList<string>?, int, IProgress<GenerationProgress>?, CancellationToken)¶
public static Task<GeneratedSet> GenerateAsync(LoadedCookBook book, GenerateOptions opts, IReadOnlyList<string>? existingDnas = null, int startNumber = 1, IProgress<GenerationProgress>? progress = null, CancellationToken cancellationToken = default);
Generation offloaded to a background thread. The work is CPU-bound, so this is a Run() over the sync core rather than genuine async — it exists so a UI thread stays responsive, and reports through progress.
Parameters
book(LoadedCookBook) - The cookbook to roll from; validated first. The caller keeps ownership.opts(GenerateOptions) - Count, seed and the other per-run settings.existingDnas(IReadOnlyList<string>?) - DNA of assets already produced (anextendrun), so no new asset repeats one. Null for a fresh run.startNumber(int) - The set number of the first asset produced; 1 for a fresh run.progress(IProgress<GenerationProgress>?) - Receives one report per asset, one at a time, in increasing order. Null for none.cancellationToken(CancellationToken) - Stops the run between assets.
Returns. A task for the finished collection. The set owns every asset image; dispose it.
Exceptions
InvalidOperationException-GenerateOptions.Countis not positive, the book failsValidator(the message lists every problem), orGenerateOptions.RecipeIdnames no recipe.RuleConflictException- The recipe rules exclude every combination, so no asset can be rolled.UniqueSpaceExhaustedException- The unique DNA space is real but smaller than the count requested.OperationCanceledException-cancellationTokenwas canceled.
GenerateStreaming(LoadedCookBook, GenerateOptions, IReadOnlyList<string>?, int, IProgress<GenerationProgress>?, CancellationToken)¶
public static IEnumerable<GeneratedAsset> GenerateStreaming(LoadedCookBook book, GenerateOptions opts, IReadOnlyList<string>? existingDnas = null, int startNumber = 1, IProgress<GenerationProgress>? progress = null, CancellationToken cancellationToken = default);
Yields assets one at a time so a caller can write or draw each and dispose it before the next is rolled. **The caller owns every asset yielded** — an abandoned enumeration leaves the last asset undisposed. The cookbook is validated eagerly, before enumeration starts.
Parameters
book(LoadedCookBook) - The cookbook to roll from; validated first. The caller keeps ownership.opts(GenerateOptions) - Count, seed and the other per-run settings.existingDnas(IReadOnlyList<string>?) - DNA of assets already produced (anextendrun), so no new asset repeats one. Null for a fresh run.startNumber(int) - The set number of the first asset produced; 1 for a fresh run.progress(IProgress<GenerationProgress>?) - Receives one report per asset, one at a time, in increasing order. Null for none.cancellationToken(CancellationToken) - Stops the run between assets.
Returns. The assets, rolled lazily one per step. The caller owns each asset yielded and must dispose it.
Exceptions
InvalidOperationException- Raised eagerly, before enumeration starts: the count is not positive, the book is invalid, orGenerateOptions.RecipeIdnames no recipe.RuleConflictException- Raised during enumeration when the rules exclude every combination.UniqueSpaceExhaustedException- Raised during enumeration when the unique space is smaller than the count requested.