Skip to content

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:

nfty demo ./demo
nfty menu ./demo/ChestDemo.cbk --out ./demo/chest.mnu

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.

<ItemGroup>
  <ProjectReference Include="..\..\src\Nfty.Ticket\Nfty.Ticket.csproj" />
</ItemGroup>

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.ToUlong into a SplitMix64Rng. The same Menu and seed give the same tickets, in the same order, on any machine and CPU.
  • TryRoll returns null when the Recipe's incompatibility rules reject the roll. That is not an error: roll again from the same generator. Bound your loop, as above. Generator allows 10,000 attempts per asset by default.
  • One generator is one stream. Do not share an IRng between 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.Parse recomputes the DNA from the contents and throws FormatException if it does not match, or if the wire is another version.
  • Plating.Plate returns the Draw list bottom-first, in the book's layerOrder. 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, and draw.Source is 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 (or HslToRgb when draw.Model says HSL). ColorConvert in Nfty.Ticket has both conversions.
  • Depth for 3D comes from MenuArchive.Depth(key), which is null for 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.TryBuild makes the mesh from finished art plus an optional DepthMap. It returns null when nothing is fully opaque, and throws PartialAlphaException naming the picture when a pixel is partly transparent: a voxel is either there or not.
  • StackDepth.ForAsset is the rule for a layered asset: per pixel the front is the largest front over the layers, the back the largest back. Pass the Faces it returns so the sides take the right layer's color. For one flat picture, leave both null.
  • ModelWriter.Write returns ModelFiles (name and bytes) and writes nothing; you choose where they go. glTF with a .bin returns two files.
  • To model a whole Set as you write it, give SetWriter.Write an IAssetModeler; Nfty.Model.SetModeler is the implementation the command line uses. Core knows no model library.
  • GltfReader reads back nfty's own glTF subset only, and reports anything else as UnsupportedModelException.

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 .tin cannot be opened without its passphrase, and SetExporter refuses 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