Experiments
This page is different from Implementation status: that page tracks features that are part of the mod's actual design. This one tracks prototypes — things built fast, usually behind a debug command, to answer a "is this even possible?" question before committing to a real design. Nothing here should be assumed stable, and nothing here has necessarily been seen running in a live game yet. When a prototype graduates into a real feature with its own tuning and UI, it moves out of this page and into Implementation status.
Seasonal NPC costumes
Question: can an NPC's appearance be swapped for a seasonal outfit (Christmas, Halloween, birthdays, ...) without redesigning the whole model/cosmetic system?
Answer: yes. Hytale's own shipped assets already do exactly this for other creatures — see
Server/Models/Christmas/Trork_Christmas.json and the Kweebec Sapling Christmas variants in the
base game. A ModelAsset can declare a Parent (inheriting that asset's full attachment list) and
its own DefaultAttachments. Correction (05/10): they do not add to the parent's list, they replace it — see "Fixed: costumed NPCs without eyes, mouth and hair" below. That is
enough to bolt a hat onto an existing NPC model with no engine changes and no new attachment API —
just a new JSON file per costumed variant.
SimTale already had the runtime piece needed to use this: SimNPCFactory.applyModel(store, ref, modelAssetId, scale, attachments), the same method already used to turn a dying NPC into the Grim
Reaper's Necromancer_Void model. Swapping a costume in is just calling it again with a different
asset id, and swapping it back is calling it a third time with the original id.
What was built
- Six new
ModelAssetfiles undersrc/main/resources/Server/Models/Events/, one per base/event combination:SimTale_Human_Male_Christmas.json,SimTale_Human_Female_Christmas.json,SimTale_Human_Child_Christmas.json, and the same three for_Halloween. Each hasParentset to the matching base (SimTale_Human_Male/Female/Child) and a single extra attachment: a Santa hat (Colored_Cotton/Red, the exact combination copied fromTrork_Christmas.json) for Christmas, a straw/witch hat texture for Halloween. - A debug command,
/simtale costume <christmas|halloween|off>, added toSimTaleCommand.java. It finds the nearest NPC to the player, remembers that NPC's current model id in an in-memory map (COSTUME_BACKUP_MODEL), and callsSimNPCFactory.applyModelwith the costumed asset id (picking the male/female/child base automatically viaInteractionManager.isNpcAChildand the NPC'sGender).offlooks up the backup and restores it.
This was built quickly, under time pressure, to have something runnable before the session ended, so it leans entirely on values already proven by the base game's own assets rather than anything newly invented — the goal was a working sketch, not a finished feature.
What's confirmed vs. what still needs a play session
| ✅ | Confirmed working in a live game |
| 🔧 | Implemented, believed correct, not yet confirmed in-game |
| 🐛 | Known issue / limitation, still open |
| ⬜ | Not implemented |
| Item | Status | Notes |
|---|---|---|
| Feasibility (can a costume be added at all without engine changes) | ✅ | Confirmed by reading the base game's own shipped assets — not a guess. |
/simtale costume command registers and runs without crashing | 🔧 | Never run in-game — built in the same session it's being documented in. |
Asset id resolves correctly from the Events/ subfolder | 🔧 | Every reference asset (Trork_Christmas.json etc.) lives directly under Server/Models/<Category>/; whether ModelAsset.getAssetMap() keys purely by basename or cares about the subfolder path has not been checked against the loader code. This is the single biggest risk in the whole prototype — if it fails, the fix is likely just renaming/relocating the files, not a logic change. |
| Santa hat renders in a sane position on a human NPC | 🔧 | Only ever confirmed on the Trork and Kweebec Sapling models in the base game. SimTale's human rig may attach differently. |
Witch hat renders correctly (no GradientSet/GradientId set) | 🔧 | Slightly less verified than the Christmas hat — a recursive asset search for a confirmed human-compatible witch hat timed out, so this uses a texture-only attachment on the assumption (seen elsewhere in SimTale's own base JSONs) that a gradient isn't required. |
christmas → off round trip restores the original appearance exactly | 🔧 | Logic looks right (putIfAbsent + remove on the backup map) but never run. |
| Works correctly on child NPCs | 🔧 | SimTale_Human_Child_* variants exist and route through InteractionManager.isNpcAChild, but children use a single non-gendered base — not cross-checked against every existing child model variant. |
| NPC keeps its own individual look (hair/face/etc.) while costumed | 🔧 | Fixed by generating a per-NPC costume asset instead of using the generic base — see "Investigated: the 'swaps the whole model' limitation" below. Not yet confirmed in a live game. |
| Costume backup survives a server restart | 🐛 | Known limitation, not a bug to "fix" so much as a design gap: COSTUME_BACKUP_MODEL is an in-memory Map, not persisted. An NPC costumed and then left costumed across a restart would have no backup to restore from if off is used afterward. |
| Automatic seasonal trigger (calendar-based, not a manual command) | 🔧 | Built (13/09): SeasonalCostumeHelper.tick(), wired into SimTaleTickSystem, checks WorldTimeResource.getGameDateTime() at most once every 1,200 ticks (~1 real minute) and reconciles every active NPC's costume against the current date. Not yet confirmed running in a live game. |
| Child costume hat fits the head correctly (no exposed/transparent-looking faces) | 🔧 | Preventive fix built (13/09) after investigating an NPC bug report that turned out to be unrelated (a false alarm — see "Fixed (unrelated to the reported bug): child NPC costume hat clipping" below). Still a real, needed fix for when a hat costume is actually used on a child; just not the cause of that specific report. Not yet confirmed in a live game. |
| Slothian / Trork NPC coverage | ⬜ | Only the three human bases (male/female/child) have costume variants so far. |
Research: automatic calendar trigger (13/09, verified against source)
Next step 3 below ("wire a calendar/date trigger instead of a debug command") turned out to already
have everything it needs in the engine — this is written down so nobody has to re-derive it from
scratch. Verified by reading WorldTimeResource.java directly in the engine source, not just
taken on faith from a web search.
The engine exposes com.hypixel.hytale.server.core.modules.time.WorldTimeResource, and it is a
real Gregorian-style calendar under the hood: getGameDateTime() returns a plain
java.time.LocalDateTime, ticking forward from ZERO_YEAR (0001-01-01T00:00:00Z) using the
JVM's own calendar math (leap years, real month lengths, all of it — this is not a custom fantasy
calendar). DAYS_PER_YEAR is a fixed 365 (ChronoUnit.YEARS.getDuration().toDays(), not
world-configurable). Also available: getGameTime() (Instant), getCurrentHour(),
getDayProgress() (0.0–1.0), and getMoonPhase().
SimTale already reads this resource — no new access pattern to invent. See
NPCSleepHelper.java and InteractionManager.java:
WorldTimeResource time = world.getEntityStore().getStore()
.getResource(WorldTimeResource.getResourceType());
LocalDateTime date = time.getGameDateTime();
From there, a costume trigger is just:
if (date.getMonthValue() == 12 && date.getDayOfMonth() == 25) {
// apply the Christmas costume
}
Two gotchas found while verifying, worth knowing before building on this:
getMoonPhase()is not a named phase. It returns a plainintindex from0togetTotalMoonPhases() - 1(that total is configurable per world). There is no "waxing"/"full" enum anywhere in this class — mapping the index to a human label is on us if we ever want one.isYearWithinRange(min, max)looks like exactly the helper we'd want for "is today inside this date range", but its body is commented out behind a// TODO: Implementand it unconditionallyreturn false. Do not call it expecting it to work — comparegetDayOfYear()/getMonthValue()/getDayOfMonth()manually instead, as in the snippet above.
This only covers reading the date to decide when to fire; it does not by itself solve "don't reapply every tick" (needs a small per-NPC or per-world "last checked day" cache) or the model-swap limitation described below.
Investigated: the "swaps the whole model" limitation (13/09)
This is the real cost of the current approach, and it is worse than it first looked. The costume
assets built above (SimTale_Human_Male_Christmas.json etc.) set "Parent": "SimTale_Human_Male"
— the generic base asset, not the specific NPC's own appearance. Reading the base asset
confirms it carries its own hardcoded look (a "Morning" haircut, "BrownDark" hair, etc.), and
reading one of the 804 per-NPC Generated/*.json files (e.g. SimTale_Human_Male_95.json)
confirms that file is what actually carries the specific NPC's own 9 attachments (haircut, face,
eyes, pants, shirt, shoes, mouth, ears, eyebrows) — inheriting from the generic base itself. So
today, every costumed NPC of the same gender/age currently renders identically: the base's generic
look plus a hat, not their look plus a hat. That is the "swapping the whole model is a pain"
problem in concrete terms — confirmed by reading the actual asset files, not assumed.
A runtime workaround was investigated and rejected. ModelAsset's fields are protected
rather than private — the engine itself builds one ad-hoc instance this way
(ModelAsset.DEBUG = new ModelAsset() {{ id = "Debug"; model = ...; }}), and
Model.createScaledModel(asset, scale, attachments) takes a ModelAsset object directly, not
just an id — so building a synthetic in-memory ModelAsset that copies an NPC's real attachments
and adds a hat is technically possible for rendering. But PersistentModel only ever saves a
plain string id (ModelReference.toReference() → {Id, Scale, RandomAttachments, Static}), and
ModelReference.toModel() resolves that id by calling ModelAsset.getAssetMap().getAsset(id) on
every reload — falling back to ModelAsset.DEBUG (a visible placeholder box) if the id isn't
found. DefaultAssetMap's only write methods (putAll, remove) are protected, with no public
API for a mod to register a new asset at runtime. So an unregistered synthetic asset would render
correctly right up until the next chunk reload or server restart, then break visibly. Not worth
building on.
The fix that actually works, confirmed against the real files: point each costume asset's
Parent at the specific NPC's own Generated/*.json id instead of the generic base. There are
exactly 804 of these today (202 male + 202 female adults, 200 male + 200 female children, counted
directly in Generated/), and each already inherits everything else it needs from the generic
base on its own — a costume file only has to add Parent + the hat attachment, identically to how
the current 6 files are built. This means:
- A one-time codegen script that lists every id in
Generated/, and for each one writes<id>_Christmas.json/<id>_Halloween.jsonnext to the existing costume assets, each just{"Parent": "<that id>", "DefaultAttachments": [<the same hat as today>]}. ~804 × 2 ≈ 1,608 tiny files (~150 bytes each, ~240 KB total) — mechanical, no engine changes, no new asset-loading code. - The command's costume-id logic gets simpler, not more complex:
costumeId = currentId + "_" + eventdirectly, with no more branching on gender/child to pick a base —currentIdalready comes frompm.getModelReference().getModelAssetId()in the existing code. - Needs to be re-run if the pool of
Generated/*.jsonvariants ever grows.
Update (13/09, same session): built. scripts/generate_costume_assets.py was written
(follows the project's existing scripts/generate_*.py conventions) and run once. It found 820
ids in Generated/ (804 of the expected human male/female/child variants, plus 10 legacy
ungendered SimTale_Human_Child_N variants and 10 Doll_N chibi-doll models picked up for free —
harmless, since the costume command only ever looks up ids that come from a live NPC's own
PersistentModel) and wrote 1,640 files (2 events × 820 ids) into
Server/Models/Events/Generated/, each just {"Parent": "<npc id>", "DefaultAttachments": [<hat>]}.
The script is idempotent — safe to re-run after new Generated/ variants are added, it only fills
in what's missing. CostumeSubCommand in SimTaleCommand.java was updated to match: it now
computes costumeId = currentId + "_" + suffix directly, and the old gender/child branching (plus
the InteractionManager/Gender imports it needed) was deleted since it's no longer used anywhere
in that file. Not yet confirmed running in a live game — see the checklist above.
Fixed: costumed NPCs without eyes, mouth and hair (05/10)
Report: changing an NPC's clothes with /simtale costume works, but the face breaks: empty eyes, no
mouth. Cause: in a ModelAsset, DefaultAttachments replaces the parent's. The codec's
inheritance only copies the parent's array when the child does not declare the field
(child.defaultAttachments = parent.defaultAttachments, checked in the ModelAsset bytecode); there
is no merge. All 1,646 costume files listed only [<hat>], so a costumed NPC lost her hair, face,
eyes, mouth, ears, eyebrows and clothes and was left as the base body with a hat.
Fix: scripts/generate_costume_assets.py now writes each costume as the NPC's full attachment list
(resolved up the Parent chain) followed by the hat, and always rewrites every file (the 1,640
per-NPC ones and the 6 generic ones). Parent is still the NPC's id, to inherit the rest (model,
skin colour, animations).
SeasonalCostumeHelper also reads the original model back from the costume id (<id>_<event>):
after a server restart the backup map was empty, so an NPC saved in costume could never be
undressed, and switching events looked for <id>_Christmas_Halloween.
Fixed (unrelated to the reported bug): child NPC costume hat clipping (13/09)
Reported with a screenshot: a child NPC's head looked wrong — the side of the head appeared transparent, and the back looked "badly fitted" (user's words, translated: "provavelmente foi a UV", i.e. "probably the UV"). Correction, same day: the NPC in the screenshot wasn't wearing a costume at all — it was a perfectly normal NPC, and the odd look was just its red hair color. False alarm; unrelated to the costume system. While chasing this report, though, a real (separate) bug was found and is worth keeping fixed:
Root cause, confirmed by reading the actual files: every head cosmetic in this project
(haircuts, etc.) that's authored for an adult skeleton has to be run through a node-scaling
transform before it's usable on a child skeleton — see docs/assets/cosmeticos-node-scales.md and
scripts/generate_child_variants.py's NODE_SCALES table. In short: any node named exactly
"Head" (a label meaning "attaches to the head", not a literal skeleton bone) gets a uniform
1.2× scale, and everything nested under it inherits that same scale (confirmed empirically by
diffing a real adult/child haircut pair, CutePart.blockymodel vs CutePart_Child.blockymodel).
The Christmas/Halloween hat models added earlier this session
(Cosmetics/Head/SantaHat.blockymodel, StrawHat.blockymodel) were the one exception — the
codegen script that builds costume assets (scripts/generate_costume_assets.py) used the same
unscaled, adult-proportioned hat for both adult and child variants.
Reading SantaHat.blockymodel in full confirms why that's visible as a glitch rather than just
"slightly wrong size": several of its outer nested boxes are missing textureLayout entries for
faces that are normally always hidden flush inside the next box (e.g. bottom, and one back) —
a totally reasonable thing to skip for a properly-nested adult-sized hat. Once the same unscaled hat
is forced onto a smaller child head socket, the nesting no longer lines up, those never-textured
faces become visible, and they render as the transparent/misplaced geometry in the report.
Fix: scripts/generate_child_event_hats.py (new file) reuses the exact same
NODE_SCALES/FACE_ATTACHMENT_NAMES/scaling logic as generate_child_variants.py, applied to
SantaHat.blockymodel and StrawHat.blockymodel, producing
NPC/Player_Child/Cosmetics/Head/SantaHat_Child.blockymodel and StrawHat_Child.blockymodel.
Verified after running: the scale factors on every node match the same 1.2×-compounding pattern
seen in the haircut comparison. The two generic child costume assets
(SimTale_Human_Child_Christmas.json / _Halloween.json) and all 820 individually-generated child
costume files under Events/Generated/ were repatched to reference the _Child model instead of
the adult one (only Model changes — Texture/GradientSet/GradientId stay the same, which
is how every other child cosmetic in this project already works: the scaled .blockymodel's
textureLayout still maps to the same texture pixels). generate_costume_assets.py itself now
has a separate EVENTS_CHILD dict, selected whenever npc_id starts with
"SimTale_Human_Child", so re-running it in the future won't regenerate the bug.
Not yet confirmed visually in game — the fix was verified by reading/diffing JSON and geometry (the scale factors match the expected pattern), but nobody has looked at a costumed child NPC in a live session since the fix. That's the next thing to check — but again, this fix does not address the original screenshot report, which was a false alarm (see the correction above).
Where the real bug turned out to be (13/09, later still): the user clarified further — it
was a red-haired boy with short hair, no costume at all. The actual bug was a corrupted node in
Short_Child.blockymodel itself (a pivot node merged with its child box, losing the child's own
position offset) — one of 23 child haircut models found with the same class of defect out of 112
checked. See docs/assets/cosmeticos-node-scales.md and testing_checklist.md for the full
writeup and fix; unrelated to the costume system documented on this page.
Next steps, if this graduates into a real feature
- Confirm the risk items above in a live game (asset resolution first — it gates everything else, including the newly-generated per-NPC costume ids).
- Persist
COSTUME_BACKUP_MODEL(or avoid needing it at all, e.g. by deriving the "off" asset id from the NPC's existing gender/child data instead of caching it). - Adjust the date windows in
SeasonalCostumeHelper.resolveEvent()if all of December / Oct 25-31 isn't the desired window — it's a two-lineif, deliberately simple to edit. - Extend coverage to Slothian and Trork NPCs.
- Once confirmed, move this section into Implementation status and delete it from here.
Source: this page tracks fast, deliberately rough prototypes. Unlike Implementation status, an item here having no ✅ is the expected default, not a red flag — that's what makes it an experiment.