feat(uniformbuilder): generate award sprite sheets from upload folder in CI#139
feat(uniformbuilder): generate award sprite sheets from upload folder in CI#139JensGryspeert wants to merge 4 commits into
Conversation
… in CI Add a CI pipeline so contributors queue a new medal/ribbon by committing source PNG(s) to uniform-uploads/ with a manifest entry and editing the registry, instead of hand-splicing the binary sprite sheets in GIMP. generateAwardSprites.js validates the manifest against AwardRegistry.jsx (awardType-gated sheet membership), normalizes art to tile geometry with sharp, splices tiles into the ribbon/medal sheets at registry-derived rows via raw-byte concatenation (non-inserted rows stay byte-identical; appends pad to the exact row), removes consumed sources, and empties the manifest. The award_sprites workflow runs it on push to uniform-uploads/** and opens a PR for review.
SyniRon
left a comment
There was a problem hiding this comment.
Really nice work. I pulled this into a worktree and checked the parsing and splice math against the real registry and both actual sheets, and the geometry holds up: ribbon rows at awardPriority * 14, medal at (medalPriority - 2) * 120, both sheets RGBA at the sizes the code expects. The validate-then-normalize-then-write ordering is the right shape for something destructive running unattended, and the byte-concat splice genuinely keeps untouched rows identical. I verified the multi-insert interleaving and the padded-append case too. Good tests on the pure functions.
A few things I'd want sorted before this runs on real contributor pushes. Two are worth calling out up front because they're not visible in the diff:
-
The test suite never runs in CI. There's no
testscript inpackage.jsonand no workflow step runsgenerateAwardSprites.test.js. All 424 lines of it are protecting nothing right now. One line inpackage.jsonplus a step fixes it, and everything else here gets safer once it's in place. -
parseAwardcan lose a field silently and that turns into data loss. If a registry entry is found but one field doesn't parse, it returns a partial object instead of failing. A medal whosemedalPrioritydidn't parse just doesn't get placed, but its source PNG still gets deleted and the manifest still gets emptied, so the run reports success while the art is gone. Left a detailed inline note on this one.
The rest are inline: the atomicity comment promises more than the code delivers, the regex breaks on nested braces, the spritesheet-geometry.md reference points at a file that isn't in the repo, and a couple of smaller things.
One design question rather than a bug: this is insert-only. If someone wants to fix the art on an award that already has a tile, adding it to the manifest splices a duplicate and shifts everything below it. The README's "renumber when adding" instruction covers the new-award path, but nothing stops or warns about the fix-existing-art case. Worth either a guard or a loud note in the README.
- reject registry fields that are present but unparseable as a hard validation error, so a dropped priority can no longer silently delete source art while reporting success - parse the award body with a brace-balanced scan instead of a first-} regex, so a nested object can't truncate trailing fields - write both sheets to temp files and rename only after both encodes succeed, so a failure on the second sheet can't leave the first overwritten - only delete a source after its tile is actually placed; keep ignored extra sources - assert sheets are RGBA on read instead of running an untested RGB path - use MEDAL_MIN_PRIORITY for the medal row derivation - add a test script and run it in CI (award_sprites + lint workflows) - gate the auto-PR on an actual sprite-sheet change and surface generator warnings in the PR body - drop the stale spritesheet-geometry.md reference and correct the row-shift comment; document the insert-only limitation in the README
|
Thanks for the thorough review — all of it is addressed in dd72a92. The two up-front items:
The rest are answered inline. |
Add an optional "replace": true on a manifest entry. Instead of splicing a new tile in and shifting every row below, it overwrites the tile already at the award's registry-derived row in place — no shift, no height change — so re-uploading art for an existing award fixes it rather than inserting a duplicate. Replacing a row past the end of the sheet (no existing tile) fails the run; new awards still use insert. Document both modes in the upload-folder README and cover replace (in-place overwrite, no shift/growth, and the past-the-end rejection) in the test suite.
|
Follow-up on the insert-only design question (b19b567): the generator now supports in-place replace, not just docs. Set Covered by new tests (in-place overwrite with rows above/below untouched and height unchanged, plus the past-the-end rejection) and documented in |
SyniRon
left a comment
There was a problem hiding this comment.
Came back through this in a fresh worktree at b19b567. First: you actually did the work, and it shows. Every one of the ten items from last round is fixed in the code, not just in the replies. The brace-balanced parse, the present-but-unparsed hard error before any write, the temp-then-rename ordering, the consumed.add move into the placement blocks, the workflow change-gate and the warnings in the PR body. I checked each against the source and the suite runs green (48 assertions). The partial-parse data-loss path we were worried about is genuinely closed.
The problem is the feature you added on top of those fixes. The replace: true path reopens the same class of silent data loss we just spent a round closing. When a manifest mixes a replace with a lower-priority insert, the insert grows the buffer before the replace lands, so the replace overwrites the wrong award while the intended one stays broken, and both sources still get deleted. The run exits 0. I left the mechanics and a reproduction inline on buildSplicedSheet, along with the doc comment that currently claims this case is safe.
That one blocks. Either reject mixing a replace and an insert on the same sheet, or apply replaces against the original offsets before any insert grows the buffer; it needs a mixed-batch test either way.
The rest is smaller and inline: the two-rename window isn't atomic and leaves a stale sheet plus an orphan tmp file if the second throws. One leftover from last round too, the awardType "covered by new tests" note doesn't match the suite. None of those block; the mixed replace does.
A replace copies into an absolute registry-derived byte offset, but an insert grows the buffer and shifts every lower row down. Sorted after an insert, a replace landed on the shifted neighbour and overwrote the wrong award while the intended one stayed broken — run still exited 0 and both sources were deleted. Reject the mix in buildSplicedSheet (per-sheet, as it is called once per sheet) and pre-flight in validateManifest. Also harden the temp-then-rename write: wrap it in try/finally so a write failure or a partial-rename crash (e.g. EXDEV) no longer leaves an orphan *.tmp.png, and document that a partial-rename needs manual reconciliation since two destinations can't be made atomic. Add tests for the mixed replace+insert rejection (validate and splice levels, asserting the sheet is untouched) and for the distinct "no parseable awardType" validation error.
|
Thanks for catching the regression the The blocker (mixed replace + insert): rejected, not reconciled. A replace and an insert can no longer run against the same sheet in one batch — guarded in Non-blocking, also done:
Suite is at 53 assertions, Prettier-clean. Replies left inline on each thread. |
| } | ||
| if (ins.replace) { | ||
| // Overwrite the existing tile at `y` in place — no shift, no growth. | ||
| if (ins.y + ins.tileHeight > height) { |
There was a problem hiding this comment.
This looks good overall. I found one last correctness issue before approval.
The replace guard on line 301 rejects the bottom-most ribbon award. The real ribbon sheet is 769px tall (54 * 14 + 13), so its last row is only 13px high.
The European/African/Middle Eastern Campaign Medal at awardPriority: 54 starts at y = 756. The current check does 756 + 14 = 770 > 769 and throws "no existing tile to overwrite", even though the tile is there in the remaining 13px. Re-uploading that award's art therefore fails. I reproduced it with a synthetic 43x769 sheet.
Checking the start of the row should handle it:
if (ins.y >= height) {rawTile.copy already clamps to the available height, so it will copy 13px into that last row. A replace starting at or below height will still be rejected.
Please add a test with a partial final row too. The current test only covers a full last row. The medal sheet does not hit this case right now, only ribbon.
Once that's fixed, I'll approve.
What
Adds a CI pipeline so contributors can add a new medal/ribbon to the Uniform Builder without hand-splicing the binary sprite sheets in GIMP. The only human image work left is supplying the artwork and editing the registry.
The workflow:
AwardRegistry.jsxfor placement, drops source PNG(s) intouniform-uploads/, adds amanifest.jsonentry, and pushes.award_spritesCI validates the manifest against the registry, normalizes each source to its tile geometry, splices the tile into the ribbon/medal sheet(s) at the registry-derived row, removes the consumed sources, empties the manifest, and opens a PR with just the regenerated sheets + cleanup.How
generateAwardSprites.js— parsesAwardRegistry.jsxas text (never executed), validates, normalizes withsharp, and splices.awardPriority; medal sheet = {Medal, MedalTiered, MedalWithValor} withmedalPriority ≥ 2. Tabs/weapon quals/unit citations/badges (which live in other assets) are rejected..github/workflows/award_sprites.yml— triggers on push touniform-uploads/**(bot branch ignored, concurrency-guarded), runsnpm run format:checkas a self-gate, then opens the PR viapeter-evans/create-pull-requestinto the pushed branch.uniform-uploads/— tracked drop folder withmanifest.json+ a README documenting the flow.sharpas a root devDependency and asprites:generatescript.Verification
generateAwardSprites.test.js— 34 assertions over synthetic sheets (parsing, membership gating, validation errors, interleaved multi-insert, padded append, full run idempotency).npm run format:checkpasses.Notes
AwardRegistry.jsxor the.xcfGIMP sources (additions only; the human owns the registry).GITHUB_TOKENdon't trigger other workflows, which is why the workflow runsformat:checkitself before opening the PR.