| .forgejo/workflows | ||
| docs | ||
| examplemod | ||
| gradle | ||
| protocol | ||
| src | ||
| syncserver | ||
| tools | ||
| wardrobemod | ||
| .dockerignore | ||
| .gitignore | ||
| build.gradle | ||
| docker-compose.yaml | ||
| dockerfile | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE.txt | ||
| README.md | ||
| settings.gradle | ||
CosmeticsLib
A client-side Fabric library mod for Minecraft 26.1.2 and 26.3 that shows cosmetics — capes, hats, pets, skins, particle effects and more — on every player in the world.
CosmeticsLib is infrastructure, not a cosmetic pack: other mods register cosmetics through its API, and CosmeticsLib is responsible for resolving, syncing, caching and rendering them.
┌──────────────┐ registers a cosmetic ┌───────────────┐
│ your mod │ ────────────────────────► │ CosmeticsLib │
└──────────────┘ └───────┬───────┘
│ renders on every player,
│ resolves other players' sets
▼
┌───────────────┐
│ sync server │ (you host this)
└───────┬───────┘
│ verifies account ownership
▼
Mojang session server
Why users never sign up
There is no account, no password, and no OAuth application to register. The mod takes the access
token the Minecraft launcher already gave it, asks Mojang to vouch for a one-time nonce, and
hands the sync server only that nonce. The server calls Mojang's hasJoined and receives the
player's real UUID.
The access token never reaches the sync server. Account identity comes from Mojang, not from the client, so nobody can claim to be someone else. See docs/PROTOCOL.md for the full flow and the security reasoning.
For players
Use the jar matching your Minecraft version and install Fabric API for that version. These are separate builds, not a universal jar; cosmetics and the sync protocol are shared across versions.
Building for either Minecraft version
Use Java 25 to run Gradle:
.\gradlew.bat assemble # Minecraft 26.1.2 (default)
.\gradlew.bat assemble -Pminecraft_version=26.3 # Minecraft 26.3
The matching Fabric API and optional Mod Menu compile dependency are selected automatically.
Jars are named with mc26.1.2 or mc26.3 and placed in build/<version>/libs/
(and examplemod/build/<version>/libs/ / wardrobemod/build/<version>/libs/).
Launch a development client with gradlew :examplemod:runClient -Pminecraft_version=26.3;
omit the property for 26.1.2. Only the two explicitly supported releases are accepted.
Installing
Install CosmeticsLib alongside a provider mod to equip its cosmetics. Viewers only need CosmeticsLib: when the configured sync server hosts the provider's data bundles, the library downloads, verifies, caches and renders them, including animated skins/capes and static triangle pets. The provider's Java code is never downloaded or executed. Providers without portable bundles still require their rendering mod on the viewer.
Other players load on render demand, not as a server-wide join scan. Players outside Minecraft's entity render distance/frustum are not fetched automatically. Visible crowds load progressively in batches of up to 50, with one peer request in flight and at least one second between batches. Nearby new players get priority without permanently excluding farther visible players. Fresh snapshots (including empty sets), bounded ETag reuse and the disk/immutable asset caches avoid repeat work; unseen cosmetics do not trigger speculative bundle downloads. Hiding other players' cosmetics also suspends their fetching. When the configured server offers the live channel, visible peer updates and emotes are pushed while it is healthy, and the poll is only the fallback; an older server keeps the poll and disables remote emotes rather than reverting to fast polling.
On each world join, CosmeticsLib automatically clears your own equipped set if no local cosmetic provider is registered. This covers uninstalling the last provider mod, including Example Cosmetics, and also publishes an empty set when the local save is already empty or missing. Registered cosmetic definitions and local runtime/generated/resource asset sources count as providers; downloaded cosmetics do not. Viewing other players' cosmetics is unaffected. The automatic clear is saved and uses the same sync and offline-retry behavior as manual removal.
CosmeticsLib itself provides a client-side removal command, so you can always unequip cosmetics even after uninstalling the mod that supplied them:
/cosmeticslib clear cape # remove one slot, leaving other slots equipped
/cosmeticslib clear pet
/cosmeticslib clear all # remove every slot, including custom slots
/cosmeticslib clear # same as clear all
/cosmeticslib clear mymod:aura # remove a provider's custom slot
Built-in slot names are cape, hat, back, shoulder, pet, skin, wings, particle,
tail and emote; fully qualified names such as cosmeticslib:cape also work. Press Tab for
available slots, including saved custom slots from uninstalled providers. Clearing a slot removes
every cosmetic in that slot. The command works in singleplayer and on vanilla servers.
Removals are saved locally and queued for your configured sync server, even if your local set was already empty. Use the same sync server where the cosmetics were published to remove them for other players; they see the change after their next sync poll. Offline changes are republished on reconnect. Without a configured sync server, the command reports that removal is local only. Individual clears reuse the remaining cosmetics' existing immutable assets without requiring the provider to upload them again or changing their model versions.
In-game settings
With Mod Menu installed, a CosmeticsLib entry appears in the mod list; its config button opens a native settings screen covering the sync server URL, the other players' poll interval, the insecure-HTTP opt-in, own/other-player visibility and the disk cache budget. Save validates first and reports any problem in plain language; Cancel (or Escape) discards. No Cloth Config or YetAnotherConfigLib is involved, and CosmeticsLib runs unchanged when Mod Menu is absent.
The same settings live in config/cosmeticslib.json, which remains editable by hand. On a first run
with no file the defaults are:
{
"syncServerUrl": "https://mccosmeticslib.ramon.cc",
"allowInsecureHttp": false,
"pollIntervalSeconds": 60
}
That is a default only: an existing file keeps every value already in it, and an explicit
"syncServerUrl": "" still means "local cosmetics only". clientId, the bearer token, their expiry
and any namespace upload secrets are never shown or edited by the screen. Changing the server to a
different host forgets the previous server's sign-in, because a token is only valid for the origin
that issued it; a fresh sign-in happens after the next world join.
For server hosts
One Java process, one jar. The server embeds Undertow, so its HTTP API and the optional live
WebSocket channel share a single listener and port; the wire format dependency is Gson (via
:protocol).
gradle :syncserver:build # produces syncserver-<version>-all.jar
java -jar syncserver-0.1-all.jar # uses config.json in the working directory
For development, .\gradlew.bat :syncserver:run uses the repository-root config.json and
data directory (unless dataDir is overridden), not a separate store under syncserver.
Full hosting guide, reverse-proxy and systemd examples, and the security defaults are in syncserver/README.md.
For modders
Declare the custom cosmetics entrypoint — it is order-independent, so your registration cannot
race another mod's init.
{
"entrypoints": { "cosmetics": ["com.example.mymod.MyCosmetics"] },
"depends": { "cosmeticslib": ">=0.1" }
}
public final class MyCosmetics implements CosmeticsEntrypoint {
@Override
public void registerCosmetics(CosmeticRegistrar registrar) {
registrar.register(CosmeticDefinition.builder(CosmeticId.of("mymod", "flame_cape"))
.slot(CosmeticSlot.CAPE)
.attachment(AttachmentPoint.BACK)
.displayName(Component.translatable("cosmetic.mymod.flame_cape"))
.animator(CosmeticId.of("cosmeticslib", "sway"))
.build());
}
}
Slots and attachment points are open registries, not enums, so you can add your own.
What you get for free:
| Sync | Your cosmetic shows on other players, fetched and polled automatically |
| Caching | Models and textures cached on disk, offline-tolerant |
| Animation | Built-in animators, or write your own; composable via blend/sequence |
| Fallback rendering | Players without your mod still see it, via a data-only portable model |
| Skin replacement | Static or animated body skins with optional cape/elytra overrides |
| Particles | PARTICLE-slot cosmetics spawned around the player |
Full guide: docs/API.md.
Animated body skins use CosmeticSkinTextures.registerAnimatedSkin(id, frames, ticksPerFrame),
just like capes. Supply complete matching 64x64 skin atlases; two game ticks per frame means 10 fps.
Resource frames are automatically shared in format-4 portable bundles for CosmeticsLib-only
viewers. Use an updated library and sync server for animated skin delivery.
Repository layout
| Path | What it is |
|---|---|
src/main/java |
Platform-neutral public API, registries, config, caching |
src/client/java |
Minecraft-client rendering, HTTP sync, session auth and the mixins |
protocol/ |
The wire format, shared by the mod and the server so they cannot drift |
syncserver/ |
The self-hosted server: plain Java on embedded Undertow (HTTP API + optional live WebSocket on one port) |
examplemod/ |
A worked add-on with cape, hat, pet, tree, flower tail, skin, bat wings, shoulder, wings, particle and emote examples |
wardrobemod/ |
Player-facing provider with persistent history, native imports, model sizing, animated textures and a keyframe emote editor |
docs/ |
Protocol spec, modder API guide, research notes |
tools/ |
Build and verification helpers |
See it working in one command
examplemod/ is a real Fabric mod that registers eleven cosmetics through the same public
entrypoint any third-party mod would use. It exists so you can watch the whole pipeline run without
writing anything:
./gradlew :examplemod:runClient
It equips itself on join without changing your camera perspective. In first person the equipped skin is now visible on your arms/hands (and on the held map), because first-person rendering reads the texture straight from the player; the other cosmetics are body geometry and need F5 to be seen in third person. Your own particle aura is deliberately hidden in first person so it does not fill the camera, while other players' auras still show. You should see a flame cape, party hat, sussy imposter pet, flower tail, example skin, bat wings, a teal shoulder buddy and a sparkle particle aura, and in the log:
(cosmeticslib) Applying a cape cosmetic through vanilla's cape renderer (the cape will sway normally)
(examplecosmetics) Drawing examplecosmetics:party_hat -- the renderer ran and geometry was submitted
The cosmetics deliberately use different mechanisms, because choosing between them is
the main decision an add-on makes. The cape is an animated texture — one registerAnimatedCape
call plays 16 flame frames at 10 fps, with no model or renderer. Vanilla draws it, so it swings
properly and replaces whatever cape the
account has. The hat is geometry, submitted from a renderer, which is what you need for anything
vanilla has no equivalent of. The pet is an FBX mesh, drawn with material colours and simulated
independently: it pauses and wanders nearby, follows when you move away and faces its own movement.
The FBX loader supports binary 7400 static meshes, not skinning or animation. The example also
registers your textured GLB tree: /examplecosmetics tree switches the pet to the tree model.
GLB support includes static glTF 2.0 scenes, baked node transforms and embedded base-colour textures.
The flower tail uses models/flower.glb, mounted at the lower back and extending 0.75 blocks
behind the player. The skin animates the pastel rainbow colours of
textures/skin/65462827594073e4.png with 32 complete frames at 10 fps (a 3.2-second loop).
The source PNG, white stars, transparency, account cape, elytra and arm model are preserved.
These equip with the original examples on the first join; /examplecosmetics tail and
/examplecosmetics skin toggle them. The bat wings use models/wing.glb in the BACK slot;
/examplecosmetics back toggles them. The shoulder buddy and feather wings are baked cuboid
geometry declared as inline portable models, so the provider and a library-only viewer render the
same numbers; the feather wings carry wing_left/wing_right bones that the flap animator actually
moves. The sparkle aura is a metadata-only PARTICLE-slot cosmetic: the definition names a
client-owned effect, and the library auto-exports and publishes a bundle containing only that
metadata, so a provider and a library-only viewer spawn exactly the same particles (there is no
dummy cuboid and no texture). The dance emote is a real, finite player-body animation: the
definition carries a keyframe clip against the canonical player bones, which the library adds on top
of the vanilla pose. It never starts on equip; the configurable Play Emote key (default B,
in Controls → CosmeticsLib) triggers it, and pressing it again stops it. The clip is
auto-exported as a format-5 bundle, so a viewer without Example Cosmetics plays the same dance.
The cape uses vanilla's effective 64x32 atlas, and entity geometry is submitted as quads (FBX/GLB triangles repeat their last corner). The example also ships nine generated, data-only bundles: the complete animated cape, the exact hat/cuboid geometry for the party hat and shoulder buddy, the feather wings, all pet/tree/flower/wing triangles with their portable materials and textures, and all 32 animated skin atlases. The sparkle aura and the dance emote are two further syncable examples with no packaged bundle: their metadata-only and clip-only format-5 bundles are auto-exported and uploaded at runtime. The flower's source images are downsampled proportionally to the portable 512-pixel limit without editing the original GLB. The library automatically uploads missing equipped content to an upload-enabled sync host using the owner's normal Minecraft authentication; no namespace secrets or manual imports are needed. Runtime/user-supplied GLB or portable models use the same library APIs and sync flow. Immutable hashes deduplicate bytes and pin each player's exact model version without overwriting a provider's catalog. Other players need only CosmeticsLib and the same sync endpoint. See the hosting guide for host upload limits and optional offline imports.
Toggle them live with a client-side command — no restart, and it works on vanilla servers, because cosmetics never involve the server:
/examplecosmetics # what is currently equipped
/examplecosmetics cape # toggle the cape
/examplecosmetics hat # toggle the hat
/examplecosmetics pet # toggle the pet
/examplecosmetics tree # switch to the textured GLB tree companion
/examplecosmetics tail # toggle the flower tail
/examplecosmetics skin # toggle the animated rainbow skin
/examplecosmetics back # toggle the GLB bat wings in the BACK slot
/examplecosmetics shoulder # toggle the baked shoulder buddy
/examplecosmetics wings # toggle the flapping feather wings
/examplecosmetics aura # toggle the sparkle particle aura
/examplecosmetics emote # toggle the opt-in dance body emote (equip; it does not auto-play)
/examplecosmetics play # play/stop the equipped emote (same as the Play Emote key)
/examplecosmetics clear # remove every example slot (including the opt-in emote)
The mod is the worked example referenced by docs/EXAMPLE_MOD.md.
Building
For the player-facing cosmetic wardrobe, see wardrobemod/README.md.
Build it with ./gradlew :wardrobemod:build or launch its development client with
./gradlew :wardrobemod:runClient.
Open it with O or /wardrobe. Starter particle and emote imports are in
wardrobemod/examples.
./gradlew build
Produces cosmeticslib-<version>.jar, protocol-<version>.jar, and
syncserver/build/libs/syncserver-<version>-all.jar.
Requires JDK 25.
Notes for contributors
- Minecraft 26.1.2 ships unobfuscated. There is deliberately no
mappingsdeclaration inbuild.gradle; adding one fails with "Cannot use Mojang mappings in a non-obfuscated environment." Classes are referenced by their real names. - Yarn names do not exist in this version.
PlayerRenderer,ResourceLocation,PlayerEntityStateand friends are all renamed (AvatarRenderer,Identifier,AvatarRenderState) or gone entirely (Sessionno longer exists). Ignore Yarn-era tutorials. - 26.x uses a deferred render pipeline.
RenderLayer.render(PoseStack, MultiBufferSource, …)is nowsubmit(PoseStack, SubmitNodeCollector, …): layers push submit nodes rather than drawing. - The mod uses three Mixins:
AvatarRenderer.extractRenderState, which carries cosmetics and the frozen emote frame from the entity to the render state for third person;AbstractClientPlayer.getSkin, which applies the equipped skin to the local player's first-person arms/hands and held map; andPlayerModel.setupAnim, which adds a real body-emote clip on top of the vanilla pose at submit and replay. Everything else goes through Fabric'sLivingEntityRenderLayerRegistrationCallback,RenderStateDataKeyandModelLayerRegistry. tools/compile-check.ps1compiles the sources with plainjavacfor fast iteration;tools/server-smoke-test.ps1exercises the server's HTTP surface end to end..\gradlew.bat renderCheckchecks FBX face grouping and the cape/cuboid UV layout against Minecraft's actual model APIs. It also runs as part ofcheckandbuild..\gradlew.bat petMotionCheckchecks seeded wandering, follow hysteresis, speed/turn limits, obstacle avoidance, slabs/stairs, cliffs and safe catch-up. It also runs withcheckandbuild..\gradlew.bat animatedCapeCheckchecks animation timing, static-cape compatibility, skin patching and the example's 16 frame atlases. It also runs withcheckandbuild..\gradlew.bat firstPersonCheckchecks that the local player's first-person arm/map skin uses the animated cosmetic (model and cape/elytra preserved) and that own particle auras are suppressed in first person while other players' still spawn. It also runs withcheckandbuild..\gradlew.bat providerAssetsCheckresolves downloaded cape, hat and pet bundles on a classpath with no example mod, covering persistent caching and all mesh triangles..\gradlew.bat :syncserver:providerServerCheckcovers authorized hosting, anonymous retrieval, namespace rejection and administrative provider imports using isolated test data..\gradlew.bat :syncserver:emoteServerCheckcovers the transient emote channel: play throttling with stop exemption, clip expiry, nonce freshness and the actor's equipped-slot/digest checks..\gradlew.bat :syncserver:emoteHttpCheckexercises the real loopback/emotesendpoints with an isolated fake-auth token: 401/403, the publication race, idempotent nonce, 429 throttle, stop exemption, backdated retry, the UUID batch cap and 404 handling..\gradlew.bat :syncserver:realtimeWsCheckruns the real Undertow listener (HTTP parity plus Java WebSocket clients): anonymous subscribe snapshots, live publication and emote fan-out, HTTP mutation fan-out, auth/cap/pin rejection, reconnect resync and old-host upgrade refusal..\gradlew.bat realtimeClientCheckdrives the live-channel client against a real Undertow host: healthy suppression of periodic peer HTTP reads, subscription snapshots, HTTP push, live publication/emote acknowledgements, reconnect, and no emote polling on a host without the route..\gradlew.bat emoteCheckchecks clip bounds and interpolation, additive application to a real player model (including overlay parts and vanilla restoration), playback lifetime/age compensation, the portable format-5 round-trip and provider-free resolution..\gradlew.bat emoteTransportCheckchecks the transient transport's nonce de-duplication, repeat triggers, explicit stops, unequipped ids and stale/negative ages..\gradlew.bat automaticAssetsCheckchecks runtime model publication to an empty real server, deduplication and independent player model revisions without a provider on the viewer classpath..\gradlew.bat glbLoaderCheckchecks the real tree GLB, scene transforms, accessor layouts, flower GLB, bounded image downsampling, embedded textures and malformed-input handling.providerAssetsCheckalso checks textured mesh and skin delivery without the provider installed..\gradlew.bat :examplemod:exampleCosmeticsCheckchecks example registrations, tail/skin toggles, merged startup equipment and clearing all nine example slots in one update..\gradlew.bat animatedSkinCheckchecks looping body frames, account-skin restoration, cape/elytra compatibility, bounded resource/inline exports and provider-free HTTP delivery..\gradlew.bat cosmeticsCommandCheckchecks individual/all client clears without a provider, automatic no-provider cleanup, saved custom slots, durable empty choices and publication. It also runs withcheckandbuild.
Status
Version 0.1 — the core library, the sync server and the protocol are complete and verified. See the goal checklist in the repository history for what is verified versus assumed.
Licence
See LICENSE.txt.
