Skip to content

nfty 1.7.0 — spritesheets, a debug number, and a space that outgrew a long

Choose a tag to compare

@corderro-artz corderro-artz released this 16 Sep 01:02
· 12 commits to main since this release

Spritesheets, a debug number on every sprite, and a DNA space that stopped fitting in a long.
2,286 tests, 0 failures.

Two tags in one release: v1.6.0 is the large-integer audit, v1.7.0 the features on top of it.

Export as spritesheet

Every asset stitched into one image, left to right then top to bottom in the Set's own
numbering
— so cell n is asset n+1, and an engine indexing frames agrees with a reader
counting rows without anything being recorded. You give it columns and rows; both boxes open
already filled with the squarest grid that holds the collection, and the pixel size sits beside
them so the two numbers explain themselves.

A short last row leaves transparent cells, because that is what a spritesheet looks like and
closing the gap would renumber every frame after it. A missing asset leaves a hole rather than
failing the sheet — a gap among drawn cells is unmistakable, which is the point.

There is a real ceiling, and it refuses with the numbers rather than dying: a sheet is one
contiguous raster, so 500 assets at 1000px in a 23×23 grid is 529 megapixels and about 2 GB of
RGBA. A refusal an author can act on beats an OutOfMemoryException half a minute in.

A set number stamped on every sprite

Loose sprites lose their filenames the moment they are dragged into an engine or a chat window,
and a folder of five hundred near-identical characters is unsortable. Tick the box, pick a corner,
and every exported asset carries its set number — so a loose sprite cross-references
nfty/NNNN.json with nothing else written down.

The digits are a 3×5 bitmap font, not a typeface. A real font would mean a new dependency
beside an ImageSharp pinned on purpose, and would put grey anti-aliased ink on art that has no
grey anywhere in it. White over a one-pixel black halo, so it reads on any ground; it declines to
draw rather than clip, because a "12" cropped to "1" is a wrong answer where a missing stamp is
merely a missing one.

It never touches your Set. The export renders a stamped copy and ships that. Stamping runs
before stitching, so the sheet carries the same numbers its sprites do.

Progress that means something

Opening a Set ran on the UI thread. That unpacks a .set into a temporary directory and reads a
file per asset, so a real collection froze the window for seconds with nothing on screen saying
why. The async path had always existed — nothing was calling it.

There is one small card for every job that has no screen of its own, and the export's bar is a
real fraction now instead of spinning whatever it was doing: a barber pole for minutes is
indistinguishable from a hang. Indeterminate survives only where the work genuinely cannot count.

The export card is three pages — CONTENTS, IMAGES, SEAL — with the manifest and the footer
outside the tabs, because what is going and what is stopping it are the two things you should
never have to go looking for. It also stopped stretching, which was costing about 230px of empty
panel on every page.

The DNA space outgrew a long

Six layers of ten variants at a 10°/10% quantize is about 2.2e21 distinct assets, against a
long's 9.22e18 — so that book's headline figure read more than 9,223,372,036,854,775,807: a
constant, printed where an author is trying to read a count. The totals are BigInteger now and
the ceiling is gone rather than raised again, along with the saturating arithmetic that
existed to keep a number inside a type too small to hold it.

The figure prints in full whenever it fits, measured against the cell at the window you
actually have — so a maximised window shows what the smallest one abbreviates — and the exact
digits are always on the tooltip. Past the named magnitudes it becomes an exponent, because there
is no name above "quintillion" that a reader knows.

Seven smaller things went with it, each probed by reverting the fix:

  • A NaN colorization range validated clean. Every check in CheckRange is false for NaN,
    so the inverted-range test and both axis tests all waved it through.
  • Describe claimed "at most 1 in a trillion" on a bounded figure, which is a tighter bound
    than the arithmetic supports. That branch had only ever been tested unbounded.
  • A target over an AtLeast space warned that the supply would not fit. That total is a
    floor, so it proves nothing — and it is exactly the count a big book produces.
  • A Set past ten thousand assets loaded in the wrong order: the reader sorted by filename, and
    the stem pads to four, so 10000.json sorts before 9999.json.
  • A non-positive enumeration budget was laundered into an answer instead of refused.
  • One unchecked multiply in the odds walk, safe only at the default budget.
  • Rare traits all printed 0% — the same string a trait nothing carries gets — and sorted
    arbitrarily, because a data source was rounding for display. Two of the three surfaces that
    print a share were also culture-sensitive, under comments promising otherwise.

Downloads

Size .NET 10 needed
Portable 84 MB no — unzip anywhere and run. Start here if you are unsure.
Single file 76 MB no — one .exe. Unpacks itself on first run, so that launch is slower.
Single file, .NET 14 MB yes — one .exe, runtime left out.
Framework-dependent 14 MB yes — the same, as a folder.

The two .NET builds need the .NET 10 desktop
runtime
; the other two carry everything.