Structed.Inkwell 0.1.1

dotnet add package Structed.Inkwell --version 0.1.1
                    
NuGet\Install-Package Structed.Inkwell -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Structed.Inkwell" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Structed.Inkwell" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Structed.Inkwell" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Structed.Inkwell --version 0.1.1
                    
#r "nuget: Structed.Inkwell, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Structed.Inkwell@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Structed.Inkwell&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Structed.Inkwell&version=0.1.1
                    
Install as a Cake Tool

Inkwell

A seeded generator engine for table-driven tabletop tools, in .NET.

Inkwell is the machinery underneath a generator — the randomness, the data loading, the drawing, the sharing — with none of the game. It does not know what a settlement is, what a mouse is, or which edition you are playing. You bring the tables; it makes rolling them reproducible, re-rollable, translatable and shareable.

Package
Structed.Inkwell The engine.
Structed.Inkwell.FantasiaArchive Optional. Writes and reads Fantasia Archive projects.
dotnet add package Structed.Inkwell

What it does

Randomness you can put in a URL. PCG32 rather than System.Random, whose seeded output is not stable across .NET versions — using it means an SDK upgrade silently invalidates every link anyone ever shared. Pcg32 is pinned against the reference vectors.

A separate stream per field. Each field draws from SplitMix64(rootSeed ^ FNV1a(fieldPath)), so one value can be re-rolled without shifting any other. That is what makes "lock this, re-roll the rest" possible at all: without it, changing the innkeeper's name changes everything after her.

Locks that survive a language change. A pin stores a table position, not the words on the page. Pin a row in German, switch to English, and you get the English wording of the same row rather than one stranded German line.

Translation as an overlay. A locale's JSON is deep-merged onto the canonical file before it is deserialised, so the loader, the validator and every generator go on seeing a single well-formed data file and never learn that languages exist.

Provenance that travels with the data. A data file records where it came from and under what licence, including whether it is derived and what notices a site has to show — so attribution is a property of the data rather than something a UI is trusted to remember.

Maps, drawn rather than plotted. Roads are grown first and buildings placed along them, inside a silhouette you name, rendered as hand-inked SVG through a wobble pen that is itself seeded — so the same seed produces the same drawing, down to the last shaky line.

A versioned envelope for what came out. Format id, version and payload, with the format and version checked on the way back in, so a file from another tool is refused plainly instead of half-read.

A worked shape

A generator derives its own plan from RollPlan, so its with helpers return its own type while the pin and re-roll bookkeeping stays shared:

public sealed record KeepPlan : RollPlan
{
    public int Size { get; init; } = 3;
}

Then every value is addressed by a path, and each path gets its own stream:

KeepPlan plan = new() { Seed = 0x0C179000, Size = 6 };
RollContext roll = new(plan);

// Honours a pin on this path, and records the row it landed on so locking it
// stores the position rather than the words.
string ruler = roll.Text("keep/ruler", rulers);
string mood = roll.Text("keep/mood", moods);

// Dice notation comes out of your data file rather than out of the code.
int garrison = DiceExpression.Roll(roll.Dice("keep/garrison"), "2d6+3");

The map is seeded from the plan rather than from the raw seed, so it can be redrawn on its own without re-rolling the keep. Its silhouettes are deliberately abstract — linear, vessel, boxy, warren, sprawl — because the engine has no opinion about whether a vessel is a boot, a teapot or a beached hull. That mapping is your game's:

uint mapSeed = MapGenerator.SeedFor(plan);

PlaceMap map = MapGenerator.Generate(new MapBrief
{
    Shape = "boxy",              // your ruined keep; somebody else's shipping container
    Scale = plan.Size,
    Subject = "Blackwater Hold",
    HasWater = true,
    Keys = [new MapKeySubject("The Broken Sword", "tavern")],
}, mapSeed);

string svg = SvgMapRenderer.Render(map, mapSeed, new MapRenderOptions
{
    AriaLabel = "A map of Blackwater Hold",
    CssClass = "keep-map",
});

Nothing above mentions a game. Swap the tables and the silhouette and the same engine draws a different one.

A field path is a load-bearing string

The single thing most worth knowing before building on this.

A field path such as place/name is an input to the generator — it is hashed into the seed of that field's stream — and it is the key a lock is filed under in every link and every export you have ever written. So renaming one does two things at once: it changes what a given seed rolls, and it orphans every lock already saved against the old name.

Treat field paths, format ids, version numbers and exported JSON property names as a public interface with users on the other side of it. Pin them in a test that compares whole generated output against a committed fixture, and never regenerate that fixture to make a failing build pass — that is the one move that defeats the entire safeguard.

Fantasia Archive

Structed.Inkwell.FantasiaArchive is a separate package because it is an integration with somebody else's application rather than part of generating anything, and a tool that does not want it should not have to carry it.

It writes and reads Fantasia Archive v1 projects: the newline-delimited PouchDB dumps, the polymorphic document and field model, paired relationships, reproducible document identities, and the ZIP a page in a browser has to hand over in place of a folder. It also carries a hidden state payload, so a generated thing can be exported, edited in someone else's app, and brought back still re-rollable.

Fantasia Archive validates nothing on import — no version, no checksum, no schema — so the tests in this repository stand in for the validator that does not exist. Two things in particular import "successfully" and quietly produce a broken project: a document without a revision is dropped by the loader, and a relationship written from only one end never gets the other side filled in.

Fantasia Archive is GPL-3.0. Only the identifiers needed to write a file it accepts are re-derived here; no blueprint source, tooltip or value list is copied.

Building

Requires the .NET 10 SDK.

dotnet test                  # the whole suite
dotnet pack -c Release       # both packages

Releasing

Releasing is tagging. v0.2.1 builds, tests, packs and pushes 0.2.1; the tag is the version, so there is no second place to forget to update.

git tag v0.2.1 && git push origin v0.2.1

Publishing uses NuGet trusted publishing: nuget.org trades a signed GitHub OIDC token for an API key valid for one hour, so this repository holds no publishing secret at all. The policy on nuget.org is pinned to these values, and changing any of them in publish.yml means editing the policy to match:

Policy field Value
Repository Owner Structed
Repository inkwell
Workflow File publish.yml
Environment nuget

The user: given to NuGet/login is the nuget.org profile name of the policy's creator — Structed.me — not an email address and not the GitHub handle. They are easy to confuse: the bare structed on nuget.org is a different account, and using it fails the token exchange with a 401 saying no matching trust policy was found.

Where this came from

Inkwell was extracted from structed/mausritter-tools, where it grew as the guts of a Mausritter settlement generator before there was any reason to name it. It was lifted out one layer at a time, each step checked against a golden baseline of four fully generated settlements, so that a tool already in use by players kept producing byte-for-byte the same settlements, maps, SVGs and exports throughout.

This repository starts from 8335d1597737d334a33e1372632efda0a50182bf in that repository, which is the commit the extraction finished at. The history before that point is over there.

Licence

MIT. See LICENSE.

Inkwell ships no game content: no tables, no names, no prose. What you roll on is yours, and its licence is yours to get right.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Structed.Inkwell:

Package Downloads
Structed.Inkwell.FantasiaArchive

Writes and reads Fantasia Archive v1 projects: PouchDB database dumps, the polymorphic document and field model, paired relationships, reproducible document identities, and the ZIP an in-browser tool has to hand over in place of a folder. Separate from Structed.Inkwell because it is an integration with somebody else's application rather than part of generating anything.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 189 9/14/2026
0.1.0 82 9/14/2026