Retrofits the correct CustomModelData (CMD) onto existing Slimefun items on the AddstarMC Survival server so they render with the shared Nexo resource pack and stack with newly-created (CMD-enabled) items.
Survival's existing Slimefun items carry a stable Slimefun id in their NBT but no CMD, so on modern clients they render as plain vanilla items. This plugin reads that id and stamps the matching CMD — the same scheme already used on Skygrid.
- Idempotent upgrade: for each item, read its Slimefun id
(PDC key
slimefun:slimefun_item, read directly), resolve the target CMD, and set it only if missing/wrong. Running it twice is a no-op. Non-Slimefun items and ids with no assigned CMD are left untouched. Shulker-box contents are migrated recursively. - Cheap by design (safe to run for weeks): each item does one item-meta read; cheap rejects (air / no-meta / non-Slimefun) exit before any lookup, and an already-correct item early-exits at the CMD compare. There is no per-item registry lookup. Once an area is converted, re-scanning it costs almost nothing.
- Lazy migration + sweeper: the event hooks (player join, inventory open, click, held-item change, item frames) convert everything a player touches — this is the primary mechanism and carries the load. The throttled background sweeper over already-loaded chunks (never force-loads cold chunks) is a low-priority safety net for items no player interacts with: automation-moved (hopper/cargo) legacy items, and areas you pre-load yourself. It can be disabled to run hook-only.
- QuickShop-safe: the passive hooks/sweeper always skip shop containers, because
mutating shop stock alone desyncs QuickShop's recorded item (chest no longer
matches → "out of stock"). Shops are migrated instead by the dedicated
/sfskin shopscommand, which updates the recorded item and the chest stock together (see below). - Fail-safe: if Slimefun or QuickShop is absent, or an id has no CMD, the plugin no-ops rather than corrupting items.
Base command: /slimefunskinner (alias /sfskin). Requires slimefunskinner.admin.
| Command | Description |
|---|---|
/sfskin |
Show current mode and CMD source. |
/sfskin stats |
Cumulative counts: scanned, changed / would-change, and skipped by reason (non-SF, already-correct, no-CMD, QuickShop, blacklisted). Flags ids seen with no CMD. Also shows the claim sweep's live/last-run status (RUNNING/done, elapsed, claims & chunks processed, containers, frames, changed, errors) so you can confirm it's progressing. |
/sfskin audit |
Pre-flight report: compares the file vs api CMD sources per shared id and lists mismatches, plus ids seen on items with no CMD. Run this before switching file → api at go-live. |
/sfskin mode <dry-run|active> |
Toggle mutation mode at runtime. |
/sfskin source <file|api> |
Switch the CMD value source at runtime (the go-live flip). |
/sfskin sweep <on|off> |
Start/stop the background sweeper. With no argument, reports its state. |
/sfskin shops |
One-time bulk migration of all QuickShop-Hikari shops: updates each shop's recorded item and its chest stock together. Throttled across ticks (force-loads market chunks in bounded batches). Honours mode (dry-run reports only). Notifies on completion. |
/sfskin claims [days] |
Sweep GriefPrevention player claims whose owner has joined within days (default claim-sweep.recent-days; 0 = all claims). Expands each claim to chunks (deduped), then force-loads and sweeps them in throttled batches, releasing chunks it loaded. Honours mode, world-blacklist, and skips QuickShop containers. Notifies on completion. |
/sfskin reload |
Re-read config.yml from disk into the live config (throttles, blacklist, mode, source, sweeper on/off). No restart needed. |
| Node | Default | Gates |
|---|---|---|
slimefunskinner.admin |
op | All /sfskin subcommands. |
| Key | Default | Meaning |
|---|---|---|
mode |
dry-run |
dry-run = observe/log only, never mutate; active = apply CMDs. |
cmd-source |
file |
Where CMD values come from. file (pre-live, bundled Skygrid map) or api (live Slimefun CustomTextureService). Values only — both can run in active. |
cmd-source-file |
'' |
Path to an item-models.yml for the file source. Empty = the bundled Skygrid map. |
world-blacklist |
[survivalspawn] |
Worlds whose items are never migrated (location-based). |
skip-quickshop |
true |
Passive hooks/sweeper always skip shop containers. Leave on. |
item-frames |
true |
Migrate item-frame contents on chunk/entity load. |
chunk-sweep.enabled |
true |
Enable the background loaded-chunk sweeper. |
chunk-sweep.per-tick-budget |
50 |
Approx chunks' worth of work per sweeper run (see config comments). |
chunk-sweep.per-tick-time-cap-ms |
5 |
Hard wall-clock cap per run (yields and resumes next run). |
chunk-sweep.interval-ticks |
20 |
Ticks between sweeper wake-ups. |
quickshop-migration.shops-per-tick |
5 |
Shops processed per tick by /sfskin shops (each may force-load a chunk). |
quickshop-migration.per-tick-time-cap-ms |
10 |
Hard wall-clock cap per shop-migration tick. |
quickshop-migration.interval-ticks |
2 |
Ticks between shop-migration batches. |
claim-sweep.enabled |
true |
false = /sfskin claims is refused. Independent of chunk-sweep.enabled (the background loaded-chunk sweeper). |
claim-sweep.recent-days |
30 |
/sfskin claims sweeps only claims of owners seen within this many days (0 = all). Overridable per run: /sfskin claims <days>. |
claim-sweep.chunks-per-tick |
4 |
Chunks force-loaded/swept per batch by /sfskin claims. |
claim-sweep.per-tick-time-cap-ms |
10 |
Hard wall-clock cap per claim-sweep batch. |
claim-sweep.interval-ticks |
20 |
Ticks between claim-sweep batches. |
claim-sweep.batch-rest-ticks |
60 |
Extra ticks to idle after a batch that force-loaded chunks, letting the server settle between load/unload waves (0 = none). |
logging.summary-interval-seconds |
60 |
Periodic summary log interval (0 = off). |
The blacklist is location-based. A player's own inventory is only skipped while they stand in a blacklisted world; it migrates once they leave. This matches the intent — protecting a curated world's placed chests/frames/containers, which the location checks fully cover. It is not a per-player exclusion.
- Now (pre-live): deploy with
mode: dry-run,cmd-source: file. Join and open containers holding legacy Slimefun items; check/sfskin statsand/sfskin audit. Nothing is mutated. - D-day (single coordinated cutover): load the populated
item-models.ymlon Survival and enable Slimefun's CMD config for new items. Run/sfskin auditto confirmfileandapiagree, then flip/sfskin source apiand/sfskin mode activetogether. - Migration proceeds lazily (hooks + sweeper) as players play. Cold, never-loaded containers migrate on first access.
- QuickShop shops (one-time): once
mode: active, run/sfskin shopsto migrate the market. It updates each shop's recorded item and chest stock together. Because it force-loads market chunks in throttled batches, run it during a quiet period; re-run if it reports errors. Dry-run first (mode: dry-run) to preview.
A shop's identity is a recorded item stored by QuickShop; the chest's physical stock must byte-match it or the shop reads "out of stock". So migrating only one side (as the passive hooks would) breaks the shop — which is why hooks skip shop containers entirely.
/sfskin shops handles both sides atomically per shop: it reads the recorded item,
upgrades its CMD, writes it back (Shop.setItem + Shop.update()), and migrates the
container's existing stock in the same step. Shops are enumerated cheaply from
QuickShop's DB, but reaching each container force-loads its chunk — so the command
runs as a throttled, time-capped job (quickshop-migration.*) to avoid an I/O
storm on the market world. It is a one-time operation: once migrated, a shop
never regresses, because any item a player later inserts has already been CMD-migrated
via their inventory. Progress is reported on completion; mode: dry-run previews
without changing anything.
/sfskin claims [days] accelerates migration by sweeping the areas players actually
build in: their GriefPrevention claims. It enumerates claims from GriefPrevention's
in-memory store (cheap), keeps player claims (admin claims are skipped) whose
owner has joined within days (via Bukkit OfflinePlayer.getLastSeen() — no
player.dat parsing), and expands each claim's footprint into chunk coordinates,
deduped across overlapping/adjacent claims so no chunk is loaded twice.
Because mass chunk loading is heavy and loaded chunks linger, the sweep is bounded on
four axes: chunks-per-tick, a per-tick-time-cap-ms wall-clock cap, interval-ticks
between batches, and an extra batch-rest-ticks pause after any batch that
force-loaded chunks — so the server can settle between load/unload waves. Chunks it
force-loads (that were cold) are async-loaded (getChunkAtAsync), swept, and
released so they age out normally; chunks already loaded (a player is nearby) are
swept in place and left alone. It reuses the same idempotent upgrader, honours
mode: dry-run and world-blacklist, and skips QuickShop containers. Requires
GriefPrevention; no-ops with a message if absent. Run it during a quiet period and
tune the throttle live with /sfskin reload.
gradle build
Requires Java 21. Targets Paper 1.21.10. The Slimefun fork jar
(libs/Slimefun-5.0.4.jar) and libs/GriefPrevention.jar are local compileOnly
dependencies; QuickShop-Hikari and Paper are resolved from their repos. Output jar is
under build/libs/.