Embed nfty in your program¶
Use nfty from your own code: a game server that decides what a drop contains, an engine that draws it, a tool that cooks collections. There are three libraries. Pick the smallest one that does the job; Three ways to use nfty explains the split.
| You want to | Reference | Gets you |
|---|---|---|
| Decide what an asset is, on a server or in an engine, and draw it yourself | Nfty.Ticket |
The roll: .mnu reading, rolling, tickets, plating. No packages. |
Cook collections, read .cbk, render pixels, write Sets, export |
Nfty.Core |
Everything above, plus archives, validation, generation, rendering, Sets, sealing. Brings ImageSharp. |
| Make 3D models | Nfty.Model |
Voxel meshes and glTF. Brings Nfty.Core. |
The libraries are not on NuGet. Reference the projects (ProjectReference) or build them and
reference the DLLs. Both samples below are in the repository under samples/, build with the
solution, and are the source of every code block on this page, so what you copy is what compiles.
Get a Menu¶
A Menu (.mnu) is a CookBook flattened for runtime: weights, rules,
colorization and art, no .cbk needed. Write one with the command line:
or from code with MenuWriter.Write (needs Nfty.Core, see Cook from code).
Roll and draw with Nfty.Ticket alone¶
samples/Nfty.Ticket.Sample/Nfty.Ticket.Sample.csproj has exactly one reference, to
Nfty.Ticket. Building it is the proof that the roll needs nothing else.
These namespaces come from Nfty.Ticket.dll. Some begin with Nfty.Core because those types began in
the engine and kept their names when the roll moved out:
using Nfty.Core.Generation; // SplitMix64Rng, SeedHash (they live in the Ticket assembly)
using Nfty.Core.Imaging; // ColorConvert, ColorSpec, RgbColor
using Nfty.Core.Model; // LayerKind
using Nfty.Ticket; // MenuArchive, Ticketer, AssetTicket, Plating
Open the Menu and prepare it¶
MenuArchive.Open reads the manifest and decodes no image. Ticketer.Prepare resolves the draw
order and weight totals once. Hold on to the result and reuse it for every roll; it is the
expensive part, and it is read-only, so several threads may roll from one.
using var menu = MenuArchive.Open(args[0]); // reads the manifest; decodes no image
PreparedMenu prepared = Ticketer.Prepare(menu.Book); // once; reuse it for every roll
Roll¶
var rng = new SplitMix64Rng(SeedHash.ToUlong(seed)); // same seed, same drops, any machine
AssetTicket? ticket = null;
for (int attempt = 0; attempt < 1000 && ticket is null; attempt++)
ticket = Ticketer.TryRoll(prepared, rng); // null = a rule rejected this roll
if (ticket is null)
throw new InvalidOperationException("The rules rejected every roll.");
string wire = ticket.ToWire(); // store this, or send it
- The seed goes through
SeedHash.ToUlonginto aSplitMix64Rng. The same Menu and seed give the same tickets, in the same order, on any machine and CPU. TryRollreturnsnullwhen the Recipe's incompatibility rules reject the roll. That is not an error: roll again from the same generator. Bound your loop, as above.Generatorallows 10,000 attempts per asset by default.- One generator is one stream. Do not share an
IRngbetween threads; give each worker its own. AssetTicket.ToWire()is one line of text, a few hundred bytes: the recipe, one variant per present layer, each colored layer's rolled hue and saturation, and the DNA. Store it, send it, put it in a token's metadata. It names a selection; it holds no pixels.
Draw a ticket¶
AssetTicket back = AssetTicket.Parse(wire); // recomputes and checks the DNA
foreach (Draw draw in Plating.Plate(menu, back)) // bottom-first: paint in this order
{
byte[] png = menu.ArtBytes(draw.ImageKey); // atlas mode: the whole atlas PNG
MenuCell cell = draw.Source; // crop this rectangle out of it
if (draw.Kind == LayerKind.Custom)
{
// Full-color art: composite the cropped cell as it is.
Console.WriteLine($" custom {cell.Key} at ({cell.X},{cell.Y}) {cell.Width}x{cell.Height}");
}
else
{
// Gray value-map: every pixel becomes HsvToRgb(hue, sat, gray / 255), alpha untouched.
// (draw.Model says whether the layer is HSV or HSL; this sample only shows the idea.)
RgbColor mid = ColorConvert.HsvToRgb(draw.Hue, draw.Sat, 128 / 255.0);
Console.WriteLine($" tinted {cell.Key} hue {draw.Hue:0.#} sat {draw.Sat:0.##}"
+ $" -> mid-gray becomes {ColorSpec.Format(mid)}");
}
_ = png; // a real renderer decodes this with its own PNG library and crops `cell`
}
AssetTicket.Parserecomputes the DNA from the contents and throwsFormatExceptionif it does not match, or if the wire is another version.Plating.Platereturns theDrawlist bottom-first, in the book'slayerOrder. A layer left out by its absent chance is simply not in the list. It takes each layer's kind and color model from the Menu, never from the ticket, and refuses a ticket whose quantize steps differ from the Menu's, so a tampered ticket cannot keep its identity while drawing something else.MenuArchive.ArtBytes(key)gives PNG bytes. In atlas mode that is the whole atlas, anddraw.Sourceis the rectangle to crop; in loose mode the rectangle is the whole image. One cropping routine serves both. Decoding is yours: use whatever PNG library your engine has.- Custom layers composite as they are. Dynamic and Static layers are gray value-maps:
each pixel becomes
HsvToRgb(hue, sat, gray / 255)with its alpha untouched (orHslToRgbwhendraw.Modelsays HSL).ColorConvertinNfty.Tickethas both conversions. - Depth for 3D comes from
MenuArchive.Depth(key), which isnullfor a variant that has none.
Cook from code¶
samples/Nfty.Embedding.Sample references Nfty.Core and Nfty.Model, and shows the three things
you would do with the full engine.
using Nfty.Core.Formats; // CookBookArchive, LoadedCookBook
using Nfty.Core.Generation; // Generator, GenerateOptions, GeneratedSet
using Nfty.Core.Publish; // MenuRenderer
using Nfty.Model; // Riser, ModelWriter, StackDepth, RiseOptions
using Nfty.Ticket; // MenuArchive, AssetTicket
using SixLabors.ImageSharp; // SaveAsPng - arrives through Nfty.Core
Read a book and generate¶
// Reading decodes every variant PNG, so the book owns images: dispose it.
using LoadedCookBook book = CookBookArchive.Read(args[0]);
// Generate validates the book itself and throws on any problem, so there is nothing to pre-check.
using GeneratedSet set = Generator.Generate(book, new GenerateOptions(Count: 3, Seed: "embed-demo"));
foreach (GeneratedAsset asset in set.Assets) // the set owns the asset images too
{
asset.Image.SaveAsPng(Path.Combine(outDir, $"{asset.SetNumber:0000}.png"));
Console.WriteLine($"#{asset.SetNumber} {asset.RecipeName} {asset.Dna[..12]}");
}
Callers own images. Reading a .cbk decodes every variant PNG into memory, so LoadedCookBook
(and the recipes and ingredients inside it) is IDisposable: using var frees the whole tree.
GeneratedSet and GeneratedAsset are disposable too, and disposing a set disposes every asset
image. Generator.Generate holds the whole collection. For a collection too big for memory, use
Generator.GenerateStreaming, which yields one asset at a time and hands you its ownership.
Abandoning the enumeration leaves the last asset undisposed.
Generate validates the book itself and throws on any problem, with a message listing every one, so
nothing needs pre-checking. It rolls sequentially and renders in parallel; pass
GenerateOptions.Parallelism to choose the width. The output is byte-identical at any width. Async
twins exist (GenerateAsync, CookBookArchive.ReadAsync, SetWriter.WriteAsync) and report
progress and honor cancellation. SetWriter.Write(set, dir, pack) writes the Set to disk.
Render a ticket to pixels¶
// A ticket rolled elsewhere (a server, nfty serve) renders to the identical pixels. Note
// MenuRenderer needs Nfty.Core; the Nfty.Ticket sample draws the same ticket without it.
using var menu = MenuArchive.Open(args[1]);
AssetTicket ticket = Ticketer.TryRoll(Ticketer.Prepare(menu.Book),
new SplitMix64Rng(7))
?? throw new InvalidOperationException("The rules rejected the roll; roll again.");
using (var image = MenuRenderer.Render(menu, ticket)) // you own the returned image
image.SaveAsPng(Path.Combine(outDir, "ticket.png"));
MenuRenderer.Render uses the same colorizer and compositor generate uses, so a client rendering a
server's ticket gets identical pixels. To go the other way, from a .cbk to a .mnu:
MenuWriter.Write(book, path, new MenuWriteOptions { Art = MenuArtMode.Atlas }). It runs the
validator first and throws, writing nothing, if the book has problems; an atlas past the pixel
ceiling throws AtlasTooLargeException, and MenuArtMode.Loose avoids it.
Make a 3D model¶
// Rise the first cooked asset into a 3D model, the way a Set written with --models does.
GeneratedAsset first = set.Assets[0];
var options = new RiseOptions { Format = ModelFormat.Glb, Pivot = Pivot.BottomCenter, VoxelSize = 1f };
using StackRise? rise = StackDepth.ForAsset(book.Recipe(first.RecipeId), first, "asset #1");
Mesh? mesh = Riser.TryBuild(first.Image, rise?.Depth, options, "asset #1", rise?.Faces);
if (mesh is null)
Console.WriteLine("Nothing fully opaque: no model."); // not an error; a batch skips and names it
else
foreach (ModelFile file in ModelWriter.Write(mesh, first.Image, options, "0001", rise?.Faces))
{
File.WriteAllBytes(Path.Combine(outDir, file.FileName), file.Bytes);
Console.WriteLine($"model {file.FileName} {file.Bytes.Length:N0} bytes");
}
Riser.TryBuildmakes the mesh from finished art plus an optionalDepthMap. It returnsnullwhen nothing is fully opaque, and throwsPartialAlphaExceptionnaming the picture when a pixel is partly transparent: a voxel is either there or not.StackDepth.ForAssetis the rule for a layered asset: per pixel the front is the largest front over the layers, the back the largest back. Pass theFacesit returns so the sides take the right layer's color. For one flat picture, leave bothnull.ModelWriter.WritereturnsModelFiles (name and bytes) and writes nothing; you choose where they go. glTF with a.binreturns two files.- To model a whole Set as you write it, give
SetWriter.WriteanIAssetModeler;Nfty.Model.SetModeleris the implementation the command line uses. Core knows no model library. GltfReaderreads back nfty's own glTF subset only, and reports anything else asUnsupportedModelException.
Run the samples¶
dotnet run --project samples/Nfty.Ticket.Sample -- ./demo/chest.mnu player-42
dotnet run --project samples/Nfty.Embedding.Sample -- ./demo/ChestDemo.cbk ./demo/chest.mnu ./out
The first prints the seed's ticket in wire form and each layer's draw. The second writes three PNGs,
the rendered ticket, and one .glb into ./out.
Things that will surprise you¶
- Same seed, same book, same bytes. That holds across machines and CPU architectures. It breaks if the book changes: reordering layers moves which RNG draw reaches each layer, so the same seed gives a different collection. See Why a seed reproduces a collection.
- A sealed
.tincannot be opened without its passphrase, andSetExporterrefuses to export from one. The seal is a policy marker backed by real encryption, not a wall against someone who holds the passphrase: see What sealing does and does not do. - Old archives are refused, not misread. An archive written by a newer build than yours throws
UnsupportedSchemaVersionException, and so does the retired v1. - Sort ordinally. If you add code that sorts anything reaching an output, use
StringComparer.Ordinal. The default sorts by culture and silently breaks determinism.
See also¶
- API reference: every public type, with signatures
- Ticket and Menu API: the roll at a glance
- Roll assets at runtime from a Menu: the command-line route
- Three ways to use nfty