
Overgrown's Apoli
A data-driven power system that's build for custom abilities, mechanics, and entire origin systems from JSON alone.
- Загрузки
- 775
- Подписчики
- 4
- Обновлён
- 12 августа 2026 г.
- Лицензия
- A-O-License
Опубликован 26 июня 2026 г.
Apoli is a mod for building custom powers, unique abilities, and entirely new game mechanics. All through data packs that are themed around modular design. It's the engine behind Overgrown's Origins.
To get started, create a power file under data/<namespace>/powers/ in your data pack.
The mod features:
- A power system with 100+ built-in ability types, grantable to any entity. Think of it as a toolkit for superpowers (No Java Required).
- A typed action system spanning entity, bi-entity, block, item, and meta actions.
- A comprehensive condition system across 7 context types.
- Data-driven skill trees for progression paths and unlockable abilities.
- A physics-based rope and grapple system (swing, leap, and reel).
- An entity disguise system for transforming mobs and players.
- Custom entities: projectiles, minions, and player clones.
- Data-driven keybindings, defined entirely through data packs.
- Custom recipes and loot functions that grant powers.
- A built-in expression engine for math-driven power values.
- Optional integration with Trinkets, Curios, Accessories, Figura, Icarus, and Hardcore Revival.
- Built for real servers: indexed power lookups, chunked network sync for large packs, and a versioned protocol that fails safely instead of desyncing.
Available for Fabric and NeoForge, on 1.20.1 and 1.21.1.
Questions or bug reports? Join the Discord or open an issue on GitHub. If you'd like to support development, Ko-fi donations are appreciated.
Ченджлог
1.34.0Бета1.20.1 · 12 августа 2026 г.
Fixes
apoli:change_slotsilently did nothing whenever either slot was an armour slot. Vanilla wraps every equipment slot except the head, mainhand and offhand in a filter that rejects any item whose natural equipment slot is not that one, so an attempt to swap something like a blaze rod intoarmor.feetwas refused and the whole swap rolled back without any error. The action now bypasses that accessor for the six equipment slots and writes to them directly, which is what a data pack asking for an arbitrary item in an arbitrary slot actually means.apoli:if_else_listran through every branch in a single tick instead of only the branches that genuinely matched. Each branch's condition was being tested immediately before that branch's action ran, so an action near the top of the list could change the very state that a condition further down was about to read, and the whole list would cascade. All conditions are now evaluated before any action runs, so each branch is judged against the state as it was when the list started. The list still fires every branch whose condition holds, which is the intended behaviour; only the cascade is gone. Adding a delay to work around this is no longer necessary.apoli:action_on_landfired late, and on long falls often appeared not to fire at all. It had been polling the entity's on-ground flag at the end of each server tick and reconstructing the fall distance from a value it wrote into the power's auxiliary data on every airborne tick. It now hooks the exact moment vanilla processes a landing, which fires precisely once per landing and has the real fall distance available. As a side effect the power no longer writes auxiliary data and sends a power sync packet on every tick of a fall.Powers stopped working entirely for a player after going through a portal. The tick registry drops any entity that is flagged as removed, which happens during a dimension transfer, and the only path that added an entity back required its power container to have been empty beforehand, so a player who already had powers was never re-registered. Dimension changes and respawns now explicitly resume ticking and resend the player's powers to the client.
Powers from a removed source could survive their removal. When a power was fully removed, the sweep that drops anything granted underneath it only ran if the power's own definition happened to be missing; when the definition was present the code trusted the power's own cleanup hook to cascade. The unconditional sweep that the full-removal path already performed is now used in both cases. This is the mechanism behind old powers lingering after an origin switch.
apoli:action_over_timenever re-ran itsrising_actionafter the holder died. The flag recording whether the power was already active lives in persisted container data and survives a respawn, so if the condition was still true after respawning there was no rising edge to detect and the action never ran again. That flag is now cleared on respawn.A
conditiononapoli:effect_immunityhad no visible effect. Immunity was only ever consulted at the moment an effect was applied, so an effect picked up while the condition was false simply stayed on the entity forever once the condition became true. While the power is active it now also strips any effect it covers that the entity already has.The
lockoption onapoli:conjure_equipmentwas only half enforced. It correctly stopped the item being moved in the inventory screen, but the drop key bypassed it completely, and the cleanup that deletes a conjured item once it reaches the ground only ran on an item entity's very first tick, so anything that slipped through stayed in the world permanently. Dropping a locked conjured item is now refused outright, conjured items are never spawned as item entities in the first place, and the cleanup no longer depends on the item entity being brand new.An
apoli:inventorypower used withinventory_type: "power"allowed items to be duplicated. Opening the inventory screen created one container instance backed by the stored data, while actions such asdrop_inventory,inventory_actionandchange_slotcreated a second, independent instance from that same stored data. An action could therefore empty its copy and save it while the open screen still held the old contents, and the next change in the screen wrote those stale contents straight back over the save — leaving the items both on the ground and still in the inventory. There is now exactly one live container per player and power, shared by the screen and by every action that touches it.A misspelled or unrecognized field on
apoli:damageturned the action into a guaranteed kill. Unknown keys in JSON are ignored, and an absentamountis meaningful — it tells the action to use the target's max health as the base value, which is how you write "damage a fraction of max health" using only modifiers. The combination meant that writing, for example,damageinstead ofamountproduced an action that always dealt exactly the target's max health, with nothing in the log. A present but unparseableamountis now a load error that names the power instead of being silently discarded, and both damage actions acceptdamageas an alias foramountso the most common version of this mistake simply works.
Additions and changes
The food component now supports a per-effect
chance. Vanilla has always supported a probability on each food effect and the documentation already described the field, but it was hardcoded to always apply. Botheffectandeffectsnow take an optionalchancebetween0.0and1.0, defaulting to1.0. Existing food components are unaffected.The bi-entity form of
apoli:damagenow accepts an expression foramountrather than only a plain number, matching the entity form of the same action.The bi-entity form of
apoli:add_velocitynow accepts expressions forx,yandzrather than only plain numbers, matching the entity form of the same action. They are evaluated against the entity being pushed.Both damage actions accept
damageas an alias foramount.
1.34.0Бета1.21.1 · 12 августа 2026 г.
Fixes
apoli:change_slotsilently did nothing whenever either slot was an armour slot. Vanilla wraps every equipment slot except the head, mainhand and offhand in a filter that rejects any item whose natural equipment slot is not that one, so an attempt to swap something like a blaze rod intoarmor.feetwas refused and the whole swap rolled back without any error. The action now bypasses that accessor for the six equipment slots and writes to them directly, which is what a data pack asking for an arbitrary item in an arbitrary slot actually means.apoli:if_else_listran through every branch in a single tick instead of only the branches that genuinely matched. Each branch's condition was being tested immediately before that branch's action ran, so an action near the top of the list could change the very state that a condition further down was about to read, and the whole list would cascade. All conditions are now evaluated before any action runs, so each branch is judged against the state as it was when the list started. The list still fires every branch whose condition holds, which is the intended behaviour; only the cascade is gone. Adding a delay to work around this is no longer necessary.apoli:action_on_landfired late, and on long falls often appeared not to fire at all. It had been polling the entity's on-ground flag at the end of each server tick and reconstructing the fall distance from a value it wrote into the power's auxiliary data on every airborne tick. It now hooks the exact moment vanilla processes a landing, which fires precisely once per landing and has the real fall distance available. As a side effect the power no longer writes auxiliary data and sends a power sync packet on every tick of a fall.Powers stopped working entirely for a player after going through a portal. The tick registry drops any entity that is flagged as removed, which happens during a dimension transfer, and the only path that added an entity back required its power container to have been empty beforehand, so a player who already had powers was never re-registered. Dimension changes and respawns now explicitly resume ticking and resend the player's powers to the client.
Powers from a removed source could survive their removal. When a power was fully removed, the sweep that drops anything granted underneath it only ran if the power's own definition happened to be missing; when the definition was present the code trusted the power's own cleanup hook to cascade. The unconditional sweep that the full-removal path already performed is now used in both cases. This is the mechanism behind old powers lingering after an origin switch.
apoli:action_over_timenever re-ran itsrising_actionafter the holder died. The flag recording whether the power was already active lives in persisted container data and survives a respawn, so if the condition was still true after respawning there was no rising edge to detect and the action never ran again. That flag is now cleared on respawn.A
conditiononapoli:effect_immunityhad no visible effect. Immunity was only ever consulted at the moment an effect was applied, so an effect picked up while the condition was false simply stayed on the entity forever once the condition became true. While the power is active it now also strips any effect it covers that the entity already has.The
lockoption onapoli:conjure_equipmentwas only half enforced. It correctly stopped the item being moved in the inventory screen, but the drop key bypassed it completely, and the cleanup that deletes a conjured item once it reaches the ground only ran on an item entity's very first tick, so anything that slipped through stayed in the world permanently. Dropping a locked conjured item is now refused outright, conjured items are never spawned as item entities in the first place, and the cleanup no longer depends on the item entity being brand new.An
apoli:inventorypower used withinventory_type: "power"allowed items to be duplicated. Opening the inventory screen created one container instance backed by the stored data, while actions such asdrop_inventory,inventory_actionandchange_slotcreated a second, independent instance from that same stored data. An action could therefore empty its copy and save it while the open screen still held the old contents, and the next change in the screen wrote those stale contents straight back over the save — leaving the items both on the ground and still in the inventory. There is now exactly one live container per player and power, shared by the screen and by every action that touches it.A misspelled or unrecognized field on
apoli:damageturned the action into a guaranteed kill. Unknown keys in JSON are ignored, and an absentamountis meaningful — it tells the action to use the target's max health as the base value, which is how you write "damage a fraction of max health" using only modifiers. The combination meant that writing, for example,damageinstead ofamountproduced an action that always dealt exactly the target's max health, with nothing in the log. A present but unparseableamountis now a load error that names the power instead of being silently discarded, and both damage actions acceptdamageas an alias foramountso the most common version of this mistake simply works.
Additions and changes
The food component now supports a per-effect
chance. Vanilla has always supported a probability on each food effect and the documentation already described the field, but it was hardcoded to always apply. Botheffectandeffectsnow take an optionalchancebetween0.0and1.0, defaulting to1.0. Existing food components are unaffected.The bi-entity form of
apoli:damagenow accepts an expression foramountrather than only a plain number, matching the entity form of the same action.The bi-entity form of
apoli:add_velocitynow accepts expressions forx,yandzrather than only plain numbers, matching the entity form of the same action. They are evaluated against the entity being pushed.Both damage actions accept
damageas an alias foramount.
1.34.0Бета1.21.1 · 12 августа 2026 г.
Fixes
apoli:change_slotsilently did nothing whenever either slot was an armour slot. Vanilla wraps every equipment slot except the head, mainhand and offhand in a filter that rejects any item whose natural equipment slot is not that one, so an attempt to swap something like a blaze rod intoarmor.feetwas refused and the whole swap rolled back without any error. The action now bypasses that accessor for the six equipment slots and writes to them directly, which is what a data pack asking for an arbitrary item in an arbitrary slot actually means.apoli:if_else_listran through every branch in a single tick instead of only the branches that genuinely matched. Each branch's condition was being tested immediately before that branch's action ran, so an action near the top of the list could change the very state that a condition further down was about to read, and the whole list would cascade. All conditions are now evaluated before any action runs, so each branch is judged against the state as it was when the list started. The list still fires every branch whose condition holds, which is the intended behaviour; only the cascade is gone. Adding a delay to work around this is no longer necessary.apoli:action_on_landfired late, and on long falls often appeared not to fire at all. It had been polling the entity's on-ground flag at the end of each server tick and reconstructing the fall distance from a value it wrote into the power's auxiliary data on every airborne tick. It now hooks the exact moment vanilla processes a landing, which fires precisely once per landing and has the real fall distance available. As a side effect the power no longer writes auxiliary data and sends a power sync packet on every tick of a fall.Powers stopped working entirely for a player after going through a portal. The tick registry drops any entity that is flagged as removed, which happens during a dimension transfer, and the only path that added an entity back required its power container to have been empty beforehand, so a player who already had powers was never re-registered. Dimension changes and respawns now explicitly resume ticking and resend the player's powers to the client.
Powers from a removed source could survive their removal. When a power was fully removed, the sweep that drops anything granted underneath it only ran if the power's own definition happened to be missing; when the definition was present the code trusted the power's own cleanup hook to cascade. The unconditional sweep that the full-removal path already performed is now used in both cases. This is the mechanism behind old powers lingering after an origin switch.
apoli:action_over_timenever re-ran itsrising_actionafter the holder died. The flag recording whether the power was already active lives in persisted container data and survives a respawn, so if the condition was still true after respawning there was no rising edge to detect and the action never ran again. That flag is now cleared on respawn.A
conditiononapoli:effect_immunityhad no visible effect. Immunity was only ever consulted at the moment an effect was applied, so an effect picked up while the condition was false simply stayed on the entity forever once the condition became true. While the power is active it now also strips any effect it covers that the entity already has.The
lockoption onapoli:conjure_equipmentwas only half enforced. It correctly stopped the item being moved in the inventory screen, but the drop key bypassed it completely, and the cleanup that deletes a conjured item once it reaches the ground only ran on an item entity's very first tick, so anything that slipped through stayed in the world permanently. Dropping a locked conjured item is now refused outright, conjured items are never spawned as item entities in the first place, and the cleanup no longer depends on the item entity being brand new.An
apoli:inventorypower used withinventory_type: "power"allowed items to be duplicated. Opening the inventory screen created one container instance backed by the stored data, while actions such asdrop_inventory,inventory_actionandchange_slotcreated a second, independent instance from that same stored data. An action could therefore empty its copy and save it while the open screen still held the old contents, and the next change in the screen wrote those stale contents straight back over the save — leaving the items both on the ground and still in the inventory. There is now exactly one live container per player and power, shared by the screen and by every action that touches it.A misspelled or unrecognized field on
apoli:damageturned the action into a guaranteed kill. Unknown keys in JSON are ignored, and an absentamountis meaningful — it tells the action to use the target's max health as the base value, which is how you write "damage a fraction of max health" using only modifiers. The combination meant that writing, for example,damageinstead ofamountproduced an action that always dealt exactly the target's max health, with nothing in the log. A present but unparseableamountis now a load error that names the power instead of being silently discarded, and both damage actions acceptdamageas an alias foramountso the most common version of this mistake simply works.
Additions and changes
The food component now supports a per-effect
chance. Vanilla has always supported a probability on each food effect and the documentation already described the field, but it was hardcoded to always apply. Botheffectandeffectsnow take an optionalchancebetween0.0and1.0, defaulting to1.0. Existing food components are unaffected.The bi-entity form of
apoli:damagenow accepts an expression foramountrather than only a plain number, matching the entity form of the same action.The bi-entity form of
apoli:add_velocitynow accepts expressions forx,yandzrather than only plain numbers, matching the entity form of the same action. They are evaluated against the entity being pushed.Both damage actions accept
damageas an alias foramount.
1.22.0Бета1.20.1 · 4 августа 2026 г.
[1.22.0] — 2026-08-04
Added
apoli:custom_model_rendergeometry mode honoursshow_first_person. The field existed but only ever did anything in texture mode, so a Blockbench model bound to the arms was invisible on your own hands. It now draws the model'sright_arm/left_armbones — and everything nested under them — onto the vanilla first-person arm, posed from the same bones the third-person model uses.body_partsstill applies, so a power that only shows aheadbone draws nothing in first person, and the vanilla arm is still underneath: hide it withapoli:modify_model_partsif the model is meant to replace it.apoli:show_both_armsgains ghost arms —ghost_arms(0–16),ghost_spacingandghost_alpha. Each ghost is a translucent copy of the same arm frozen at an earlier point of the swing, so one punch trails a fan behind it and a fast stream of swings reads as a flurry. They are drawn only while an arm is swinging, and only on arms that are drawn at all. They go through the samerenderPlayerArmpath as the real arm, socustom_model_render,model_color,modify_player_model,modify_model_partsand disguise skins all apply to them for free.apoli:attach_ropegainsstart_length— the length the rope is created at, clamped tomin_length–max_length.
Fixed
apoli:creative_flightignored itscondition. The power granted flight on add and then re-granted it every tick without ever testing the condition, so a conditional flight power was permanent flight. It now re-evaluates every tick: while the condition fails (or the power is suppressed) flight is taken away and the holder drops, and it comes back when the condition passes. Creative and spectator mode are never touched.apoli:custom_model_render— a rig's second layer detached from its limb. Only the first bone naming a body part bound to it, so a model with bothright_armandright_sleeve(orheadandhat_layer, orbodyandjacket) animated one and froze the other at its rest pose. Standing still it looked right; sneaking, blocking with a shield or winding up a trident moved the bound bone and left the other behind, which read as the model coming apart. Every bone that names a body part now binds to it.apoli:custom_model_render— a body-part bone under a rotated group animated twice. Body-part bones are posed in the player's frame, so they are now always lifted out of their parent group, not just when no ancestor carried a rotation. A bone that loses a parent rotation this way is named in the load log.apoli:custom_model_render— slim skins were half a pixel out. The rest pose the animation is measured against was baked from the slim player layer, whose arms sit aty = 2.5; vanilla normalises them to the widey = 2.0every frame, so every arm-bound bone on a slim player was offset by 0.5. The rest pose is now always the wide bake, which is the basis vanilla animates from for both models.apoli:attach_rope—max_lengthdid nothing on a rope that can't be reeled. Every rope was created at whatever distance its two ends happened to be apart, and only the reel keys could change that. Acontrollablegrapple wants to start taut, so that is still its default — but a tether is not controllable and has no way to pay itself out, so"max_length": 20on one meant "stuck at however far apart you were", not "may roam 20 blocks". Ropes that are notcontrollablenow start atmax_length, andstart_lengthoverrides either default.
[1.21.2] — 2026-08-03
Fixed
apoli:custom_model_renderis drawn in the inventory preview while the camera is in first person. Both modes suppressed themselves whenever the holder was the local player and the camera was first person. That guard was aimed at the first-person hand view, but the hand is rendered byPlayerRenderer.renderHand, which draws the arm parts directly and never runs render layers — so it never had anything to suppress there. What it did suppress was every other whole-body render of the local player, all of which happen with the camera still in first person: the inventory preview and any screen using it, and your own body while sleeping. The model showed in third person and vanished in first person for the same screen. The guard is gone; vanilla already declines to render the camera entity's body during the first-person world pass, so there is nothing left to guard against.apoli:disguise_ashas the same fix. The disguise renderer hooksEntityRenderDispatcher.render, which is exactly what the inventory preview calls, and carried the same first-person guard — so a disguised player saw their own undisguised body in the preview while everyone else saw the disguise. The preview now shows the disguise.
[1.21.1] — 2026-08-03
Follow-up to 1.21.0. Bedrock models now match Blockbench's preview exactly — verified per cube corner and per UV rect against Blockbench's own import and export code, not by eye.
Fixed
apoli:custom_model_render— box UV is laid out on floored cube sizes. Blockbench rounds each cube dimension down before packing the six box-UV faces; only its Java-entity format keeps the fractional size. Apoli was using the raw float size, so every cube with a fractional dimension sampled the wrong part of the texture — up to 2.65 UV pixels out on a 32×32 sheet. Faces that overran their island's edge into empty texture also read as gaps between cubes, so this fixes two separate-looking symptoms. Cubes with whole-number sizes are unaffected. The cube's geometry still uses the exact fractional size.apoli:custom_model_render— every bone is bindable, not just root bones. A rig built inside a Blockbench wrapper group (the normal case) never picked up the player's animation, because only top-level bones were indexed by name.apoli:custom_model_render— a body-part bone nested under another body-part bone no longer animates twice. Java's player parts are siblings in root space —bodynever carriesright_arm's swing — so aheadbone parented to abodybone was picking up the body's animation and its own. Such bones are now re-parented to the model root with a root-relative bind offset, which is exactly bind-pose-preserving. Skipped when an ancestor carries its own rotation, where nesting is what the author meant.
Added
apoli:custom_model_render— body-part names are matched loosely. Case, spaces,_and-are ignored, and alias spellings bind to the seven vanilla parts:headwear/hat_layer/head_layer→hat;torso/waist/jacket/body_layer→body;arm_right/right_sleeve/right_arm_layer→right_arm, and the left and leg equivalents includingright_pants/left_pants.
Changed
body_partsin geometry mode now names bones in your own model, not just the seven vanilla parts. Behaviour change: a non-emptybody_partsnow hides every bone it does not name (hiding a bone hides everything nested under it). Leavebody_partsout to draw the whole model, as before.
[1.21.0] — 2026-08-03
apoli:custom_model_render geometry mode was correct in Blockbench and "exploded" in game. Two independent causes, both fixed.
Fixed
- Per-cube
rotationandpivotare now applied. Bedrock rotates individual cubes; Java'sModelPart.Cubecannot. Each rotated cube now becomes a synthetic child bone<bone>_r<n>carrying that cube's pivot and rotation — the same transformation Blockbench's own Java exporter performs. Previously every rotated cube was drawn unrotated, which flattened angled parts and pulled the model apart at the joints. - Body-part binding no longer teleports bones. Binding copied the vanilla
part's position along with its rotation, so a bone named
headsnapped to the vanilla head pivot,bodyto the model origin, and the arms to±5. Binding is now the vanilla part's delta from its rest pose, applied on top of the pose you authored — a bone rotates about its own pivot and stays where Blockbench put it. Models built on the vanilla skeleton behave exactly as before.
Added
- Per-face UV.
"uv": { "north": { "uv": […], "uv_size": […] }, … }is now read per face, including cubes that mix per-face and box UV within one model. Previously the north face's UV was applied to all six sides. Bedrock face names map onto Java's aseast↔west,up↔down,north/southunchanged.
Known limitations
uv_rotation(Bedrock format 1.21.0+) is not supported; a model using it logs a warning and draws that face unrotated.- Only
minecraft:geometry[0]is read from a.geo.json. - A cube with a zero-size axis is a flat plane, and both of its faces land on
the same plane — the game z-fights it, as Blockbench does. Give the cube a
small
inflateor a non-zero thickness. - Geometry mode renders in third person only.
1.22.0Бета1.21.1 · 4 августа 2026 г.
[1.22.0] — 2026-08-04
Added
apoli:custom_model_rendergeometry mode honoursshow_first_person. The field existed but only ever did anything in texture mode, so a Blockbench model bound to the arms was invisible on your own hands. It now draws the model'sright_arm/left_armbones — and everything nested under them — onto the vanilla first-person arm, posed from the same bones the third-person model uses.body_partsstill applies, so a power that only shows aheadbone draws nothing in first person, and the vanilla arm is still underneath: hide it withapoli:modify_model_partsif the model is meant to replace it.apoli:show_both_armsgains ghost arms —ghost_arms(0–16),ghost_spacingandghost_alpha. Each ghost is a translucent copy of the same arm frozen at an earlier point of the swing, so one punch trails a fan behind it and a fast stream of swings reads as a flurry. They are drawn only while an arm is swinging, and only on arms that are drawn at all. They go through the samerenderPlayerArmpath as the real arm, socustom_model_render,model_color,modify_player_model,modify_model_partsand disguise skins all apply to them for free.apoli:attach_ropegainsstart_length— the length the rope is created at, clamped tomin_length–max_length.
Fixed
apoli:creative_flightignored itscondition. The power granted flight on add and then re-granted it every tick without ever testing the condition, so a conditional flight power was permanent flight. It now re-evaluates every tick: while the condition fails (or the power is suppressed) flight is taken away and the holder drops, and it comes back when the condition passes. Creative and spectator mode are never touched.apoli:custom_model_render— a rig's second layer detached from its limb. Only the first bone naming a body part bound to it, so a model with bothright_armandright_sleeve(orheadandhat_layer, orbodyandjacket) animated one and froze the other at its rest pose. Standing still it looked right; sneaking, blocking with a shield or winding up a trident moved the bound bone and left the other behind, which read as the model coming apart. Every bone that names a body part now binds to it.apoli:custom_model_render— a body-part bone under a rotated group animated twice. Body-part bones are posed in the player's frame, so they are now always lifted out of their parent group, not just when no ancestor carried a rotation. A bone that loses a parent rotation this way is named in the load log.apoli:custom_model_render— slim skins were half a pixel out. The rest pose the animation is measured against was baked from the slim player layer, whose arms sit aty = 2.5; vanilla normalises them to the widey = 2.0every frame, so every arm-bound bone on a slim player was offset by 0.5. The rest pose is now always the wide bake, which is the basis vanilla animates from for both models.apoli:attach_rope—max_lengthdid nothing on a rope that can't be reeled. Every rope was created at whatever distance its two ends happened to be apart, and only the reel keys could change that. Acontrollablegrapple wants to start taut, so that is still its default — but a tether is not controllable and has no way to pay itself out, so"max_length": 20on one meant "stuck at however far apart you were", not "may roam 20 blocks". Ropes that are notcontrollablenow start atmax_length, andstart_lengthoverrides either default.
[1.21.2] — 2026-08-03
Fixed
apoli:custom_model_renderis drawn in the inventory preview while the camera is in first person. Both modes suppressed themselves whenever the holder was the local player and the camera was first person. That guard was aimed at the first-person hand view, but the hand is rendered byPlayerRenderer.renderHand, which draws the arm parts directly and never runs render layers — so it never had anything to suppress there. What it did suppress was every other whole-body render of the local player, all of which happen with the camera still in first person: the inventory preview and any screen using it, and your own body while sleeping. The model showed in third person and vanished in first person for the same screen. The guard is gone; vanilla already declines to render the camera entity's body during the first-person world pass, so there is nothing left to guard against.apoli:disguise_ashas the same fix. The disguise renderer hooksEntityRenderDispatcher.render, which is exactly what the inventory preview calls, and carried the same first-person guard — so a disguised player saw their own undisguised body in the preview while everyone else saw the disguise. The preview now shows the disguise.
[1.21.1] — 2026-08-03
Follow-up to 1.21.0. Bedrock models now match Blockbench's preview exactly — verified per cube corner and per UV rect against Blockbench's own import and export code, not by eye.
Fixed
apoli:custom_model_render— box UV is laid out on floored cube sizes. Blockbench rounds each cube dimension down before packing the six box-UV faces; only its Java-entity format keeps the fractional size. Apoli was using the raw float size, so every cube with a fractional dimension sampled the wrong part of the texture — up to 2.65 UV pixels out on a 32×32 sheet. Faces that overran their island's edge into empty texture also read as gaps between cubes, so this fixes two separate-looking symptoms. Cubes with whole-number sizes are unaffected. The cube's geometry still uses the exact fractional size.apoli:custom_model_render— every bone is bindable, not just root bones. A rig built inside a Blockbench wrapper group (the normal case) never picked up the player's animation, because only top-level bones were indexed by name.apoli:custom_model_render— a body-part bone nested under another body-part bone no longer animates twice. Java's player parts are siblings in root space —bodynever carriesright_arm's swing — so aheadbone parented to abodybone was picking up the body's animation and its own. Such bones are now re-parented to the model root with a root-relative bind offset, which is exactly bind-pose-preserving. Skipped when an ancestor carries its own rotation, where nesting is what the author meant.
Added
apoli:custom_model_render— body-part names are matched loosely. Case, spaces,_and-are ignored, and alias spellings bind to the seven vanilla parts:headwear/hat_layer/head_layer→hat;torso/waist/jacket/body_layer→body;arm_right/right_sleeve/right_arm_layer→right_arm, and the left and leg equivalents includingright_pants/left_pants.
Changed
body_partsin geometry mode now names bones in your own model, not just the seven vanilla parts. Behaviour change: a non-emptybody_partsnow hides every bone it does not name (hiding a bone hides everything nested under it). Leavebody_partsout to draw the whole model, as before.
[1.21.0] — 2026-08-03
apoli:custom_model_render geometry mode was correct in Blockbench and "exploded" in game. Two independent causes, both fixed.
Fixed
- Per-cube
rotationandpivotare now applied. Bedrock rotates individual cubes; Java'sModelPart.Cubecannot. Each rotated cube now becomes a synthetic child bone<bone>_r<n>carrying that cube's pivot and rotation — the same transformation Blockbench's own Java exporter performs. Previously every rotated cube was drawn unrotated, which flattened angled parts and pulled the model apart at the joints. - Body-part binding no longer teleports bones. Binding copied the vanilla
part's position along with its rotation, so a bone named
headsnapped to the vanilla head pivot,bodyto the model origin, and the arms to±5. Binding is now the vanilla part's delta from its rest pose, applied on top of the pose you authored — a bone rotates about its own pivot and stays where Blockbench put it. Models built on the vanilla skeleton behave exactly as before.
Added
- Per-face UV.
"uv": { "north": { "uv": […], "uv_size": […] }, … }is now read per face, including cubes that mix per-face and box UV within one model. Previously the north face's UV was applied to all six sides. Bedrock face names map onto Java's aseast↔west,up↔down,north/southunchanged.
Known limitations
uv_rotation(Bedrock format 1.21.0+) is not supported; a model using it logs a warning and draws that face unrotated.- Only
minecraft:geometry[0]is read from a.geo.json. - A cube with a zero-size axis is a flat plane, and both of its faces land on
the same plane — the game z-fights it, as Blockbench does. Give the cube a
small
inflateor a non-zero thickness. - Geometry mode renders in third person only.
1.22.0Бета1.21.1 · 4 августа 2026 г.
[1.22.0] — 2026-08-04
Added
apoli:custom_model_rendergeometry mode honoursshow_first_person. The field existed but only ever did anything in texture mode, so a Blockbench model bound to the arms was invisible on your own hands. It now draws the model'sright_arm/left_armbones — and everything nested under them — onto the vanilla first-person arm, posed from the same bones the third-person model uses.body_partsstill applies, so a power that only shows aheadbone draws nothing in first person, and the vanilla arm is still underneath: hide it withapoli:modify_model_partsif the model is meant to replace it.apoli:show_both_armsgains ghost arms —ghost_arms(0–16),ghost_spacingandghost_alpha. Each ghost is a translucent copy of the same arm frozen at an earlier point of the swing, so one punch trails a fan behind it and a fast stream of swings reads as a flurry. They are drawn only while an arm is swinging, and only on arms that are drawn at all. They go through the samerenderPlayerArmpath as the real arm, socustom_model_render,model_color,modify_player_model,modify_model_partsand disguise skins all apply to them for free.apoli:attach_ropegainsstart_length— the length the rope is created at, clamped tomin_length–max_length.
Fixed
apoli:creative_flightignored itscondition. The power granted flight on add and then re-granted it every tick without ever testing the condition, so a conditional flight power was permanent flight. It now re-evaluates every tick: while the condition fails (or the power is suppressed) flight is taken away and the holder drops, and it comes back when the condition passes. Creative and spectator mode are never touched.apoli:custom_model_render— a rig's second layer detached from its limb. Only the first bone naming a body part bound to it, so a model with bothright_armandright_sleeve(orheadandhat_layer, orbodyandjacket) animated one and froze the other at its rest pose. Standing still it looked right; sneaking, blocking with a shield or winding up a trident moved the bound bone and left the other behind, which read as the model coming apart. Every bone that names a body part now binds to it.apoli:custom_model_render— a body-part bone under a rotated group animated twice. Body-part bones are posed in the player's frame, so they are now always lifted out of their parent group, not just when no ancestor carried a rotation. A bone that loses a parent rotation this way is named in the load log.apoli:custom_model_render— slim skins were half a pixel out. The rest pose the animation is measured against was baked from the slim player layer, whose arms sit aty = 2.5; vanilla normalises them to the widey = 2.0every frame, so every arm-bound bone on a slim player was offset by 0.5. The rest pose is now always the wide bake, which is the basis vanilla animates from for both models.apoli:attach_rope—max_lengthdid nothing on a rope that can't be reeled. Every rope was created at whatever distance its two ends happened to be apart, and only the reel keys could change that. Acontrollablegrapple wants to start taut, so that is still its default — but a tether is not controllable and has no way to pay itself out, so"max_length": 20on one meant "stuck at however far apart you were", not "may roam 20 blocks". Ropes that are notcontrollablenow start atmax_length, andstart_lengthoverrides either default.
[1.21.2] — 2026-08-03
Fixed
apoli:custom_model_renderis drawn in the inventory preview while the camera is in first person. Both modes suppressed themselves whenever the holder was the local player and the camera was first person. That guard was aimed at the first-person hand view, but the hand is rendered byPlayerRenderer.renderHand, which draws the arm parts directly and never runs render layers — so it never had anything to suppress there. What it did suppress was every other whole-body render of the local player, all of which happen with the camera still in first person: the inventory preview and any screen using it, and your own body while sleeping. The model showed in third person and vanished in first person for the same screen. The guard is gone; vanilla already declines to render the camera entity's body during the first-person world pass, so there is nothing left to guard against.apoli:disguise_ashas the same fix. The disguise renderer hooksEntityRenderDispatcher.render, which is exactly what the inventory preview calls, and carried the same first-person guard — so a disguised player saw their own undisguised body in the preview while everyone else saw the disguise. The preview now shows the disguise.
[1.21.1] — 2026-08-03
Follow-up to 1.21.0. Bedrock models now match Blockbench's preview exactly — verified per cube corner and per UV rect against Blockbench's own import and export code, not by eye.
Fixed
apoli:custom_model_render— box UV is laid out on floored cube sizes. Blockbench rounds each cube dimension down before packing the six box-UV faces; only its Java-entity format keeps the fractional size. Apoli was using the raw float size, so every cube with a fractional dimension sampled the wrong part of the texture — up to 2.65 UV pixels out on a 32×32 sheet. Faces that overran their island's edge into empty texture also read as gaps between cubes, so this fixes two separate-looking symptoms. Cubes with whole-number sizes are unaffected. The cube's geometry still uses the exact fractional size.apoli:custom_model_render— every bone is bindable, not just root bones. A rig built inside a Blockbench wrapper group (the normal case) never picked up the player's animation, because only top-level bones were indexed by name.apoli:custom_model_render— a body-part bone nested under another body-part bone no longer animates twice. Java's player parts are siblings in root space —bodynever carriesright_arm's swing — so aheadbone parented to abodybone was picking up the body's animation and its own. Such bones are now re-parented to the model root with a root-relative bind offset, which is exactly bind-pose-preserving. Skipped when an ancestor carries its own rotation, where nesting is what the author meant.
Added
apoli:custom_model_render— body-part names are matched loosely. Case, spaces,_and-are ignored, and alias spellings bind to the seven vanilla parts:headwear/hat_layer/head_layer→hat;torso/waist/jacket/body_layer→body;arm_right/right_sleeve/right_arm_layer→right_arm, and the left and leg equivalents includingright_pants/left_pants.
Changed
body_partsin geometry mode now names bones in your own model, not just the seven vanilla parts. Behaviour change: a non-emptybody_partsnow hides every bone it does not name (hiding a bone hides everything nested under it). Leavebody_partsout to draw the whole model, as before.
[1.21.0] — 2026-08-03
apoli:custom_model_render geometry mode was correct in Blockbench and "exploded" in game. Two independent causes, both fixed.
Fixed
- Per-cube
rotationandpivotare now applied. Bedrock rotates individual cubes; Java'sModelPart.Cubecannot. Each rotated cube now becomes a synthetic child bone<bone>_r<n>carrying that cube's pivot and rotation — the same transformation Blockbench's own Java exporter performs. Previously every rotated cube was drawn unrotated, which flattened angled parts and pulled the model apart at the joints. - Body-part binding no longer teleports bones. Binding copied the vanilla
part's position along with its rotation, so a bone named
headsnapped to the vanilla head pivot,bodyto the model origin, and the arms to±5. Binding is now the vanilla part's delta from its rest pose, applied on top of the pose you authored — a bone rotates about its own pivot and stays where Blockbench put it. Models built on the vanilla skeleton behave exactly as before.
Added
- Per-face UV.
"uv": { "north": { "uv": […], "uv_size": […] }, … }is now read per face, including cubes that mix per-face and box UV within one model. Previously the north face's UV was applied to all six sides. Bedrock face names map onto Java's aseast↔west,up↔down,north/southunchanged.
Known limitations
uv_rotation(Bedrock format 1.21.0+) is not supported; a model using it logs a warning and draws that face unrotated.- Only
minecraft:geometry[0]is read from a.geo.json. - A cube with a zero-size axis is a flat plane, and both of its faces land on
the same plane — the game z-fights it, as Blockbench does. Give the cube a
small
inflateor a non-zero thickness. - Geometry mode renders in third person only.
1.21.0Бета1.21.1 · 3 августа 2026 г.
Added
- Per-face UV, including models that mix box UV and per-face UV cube by cube. Previously every face of a per-face cube was given the north face's UV, so anything not authored with Box UV checked was mis-textured.
- Warnings instead of silence for duplicate bone names, unknown
parentreferences, bone parent cycles, anduv_rotation(Bedrock format 1.21.0+), which is still unsupported.
If you built a model against the old behaviour: to attach geometry to a body part, build it around that part's pivot in Bedrock coordinates —
head/hat/bodyat[0, 24, 0], arms at[±5, 22, 0], legs at[±1.9, 12, 0]. Everything else is now placed exactly where Blockbench shows it, relative to the player's feet.
apoli:suppress_powerandapoli:unsuppress_powertake arrays.powerandsourceeach accept a single identifier or a list, so one action replaces a stack of them inside anapoli:and. The plural spellingspowers/sourceswork as aliases. Existing single-string packs parse unchanged.
Suppression is source-counted — a power stays off until every source that suppressed it is lifted — which is what makes multi-source arrays safe to combine.
Changed
Fabric builds moved onto the refreshed templates; only the Fabric API version had drifted.
Version alignment with Origins 1.9.2. No Apoli behaviour change.
NeoForge toolchain updated to ModDevGradle 2.0.143 and NeoForge 21.1.248.
Fixed
Per-cube rotation was discarded. Bedrock lets each cube carry its own
pivotandrotationindependently of its bone. Neither was read, so every rotated cube drew axis-aligned — a part modelled at 45° came out flat. Models built with rotated detail cubes looked like they had exploded.Bones named after body parts were teleported onto the vanilla skeleton. A bone called
headwas moved to the vanilla head pivot,bodyto the model origin, the arms to(±5, 2, 0)— so a model authored standing on the Blockbench floor had its parts flung apart, and its height never matched the player. The vanilla animation is now applied on top of the pose you authored: a bone rotates about its own pivot and stays where you put it. A model built on the vanilla skeleton is unaffected.Bone names are matched case-insensitively. Blockbench's default capitalisation (
Head,RightArm) matched nothing, so on a typical model both pose-following andbody_partssilently did nothing at all.Bone-level
mirrorandinflateare inherited by cubes that don't set their own.Suppression did nothing on
apoli:multiplepowers — the reason suppress/unsuppress looked completely broken. Amultiplehas no behaviour of its own; it all lives in the sub-powers the loader generates. Suppressing the id you actually know the name of therefore switched off nothing. Suppression is now effective: suppressing a power also suppresses everything it granted, at any depth. The actions, the commands, the codecs and the sync were all fine.Suppressed powers left lasting effects behind. A suppressed power stops ticking, so anything it installed outside the container stayed applied forever:
apoli:attributemodifiers, creative flight, and game-event listeners. Those three now undo and redo their state when suppression flips.apoli:edible_itemdid nothing. The power type was registered and parsed, but nothing ever read it. It now works end to end: hunger and saturation, status effects,entity_actionanditem_action,result_stack,consume_sound, the eating animation and its duration — and it takes priority over vanilla food, so it can replace an existing food's behaviour rather than only adding food to non-food items. An omitteditem_conditionstill means every stack, matching upstream.apoli:raycast'scommand_at_hitonly fired when the ray ended on a block. Any entity in front of the block silently swallowed it, and a ray with"entity": true, "block": falsecould never fire it at all. It now runs on the nearest hit of either kind.command_hit_offset(default0) pushes back along the block's face for a block hit and back along the ray for an entity hit.Speech-to-action works on NeoForge again, without reintroducing the 1.17.4 launch crash. JNA and Vosk now ship as plain resources, are unpacked on first use, and load in an isolated class loader, so they never become JPMS modules and can't collide with another mod's copy. Verified to still work with a foreign JNA already on the classpath (the ImmediatelyFast case).
apoli:player_model_typetreated every unreported player as wide, so"model_type": "slim"could never pass and a wide/slim power pair only ever fired the wide half. The model type is now resolved in order from the client's own report, then the player's signed skin metadata, then vanilla's default-skin rule for their UUID. Vanilla clients, older Apoli clients, and players whose skin hadn't loaded yet are all handled.A malformed optional field was dropped in complete silence. A typo in a nested field name, or broken escaping inside an SNBT
tag, made the field vanish with no error — so a power ran unconditionally, or with no NBT filter, and the log said nothing. Apoli now logs a warning naming both the power and the field. It is a warning, not a hard failure, so one bad field can't drop a whole power on a live server.SNBT parse errors now report the actual problem (
Expected '}' at position 81: …) instead of the uselessExpected NBT compound, got STRING.Legacy
origins:-namespaced action ids skipped their field defaults. The type resolved fine, which is why nothing showed in the log, butorigins:replace_inventorydecoded asoperation: modify— a silent no-op.origins:drop_inventoryand the three*_teamaliases had the same problem;origins:set_no_gravityandorigins:set_gravityfailed to parse outright.inventory_actionwithoperation: replacecould not fill an empty slot, which is the common use — putting an item into an empty offhand.modifyanddropstill skip empty slots.
1.21.0Бета1.20.1 · 3 августа 2026 г.
Added
- Per-face UV, including models that mix box UV and per-face UV cube by cube. Previously every face of a per-face cube was given the north face's UV, so anything not authored with Box UV checked was mis-textured.
- Warnings instead of silence for duplicate bone names, unknown
parentreferences, bone parent cycles, anduv_rotation(Bedrock format 1.21.0+), which is still unsupported.
If you built a model against the old behaviour: to attach geometry to a body part, build it around that part's pivot in Bedrock coordinates —
head/hat/bodyat[0, 24, 0], arms at[±5, 22, 0], legs at[±1.9, 12, 0]. Everything else is now placed exactly where Blockbench shows it, relative to the player's feet.
apoli:suppress_powerandapoli:unsuppress_powertake arrays.powerandsourceeach accept a single identifier or a list, so one action replaces a stack of them inside anapoli:and. The plural spellingspowers/sourceswork as aliases. Existing single-string packs parse unchanged.
Suppression is source-counted — a power stays off until every source that suppressed it is lifted — which is what makes multi-source arrays safe to combine.
Changed
Fabric builds moved onto the refreshed templates; only the Fabric API version had drifted.
Version alignment with Origins 1.9.2. No Apoli behaviour change.
NeoForge toolchain updated to ModDevGradle 2.0.143 and NeoForge 21.1.248.
Fixed
Per-cube rotation was discarded. Bedrock lets each cube carry its own
pivotandrotationindependently of its bone. Neither was read, so every rotated cube drew axis-aligned — a part modelled at 45° came out flat. Models built with rotated detail cubes looked like they had exploded.Bones named after body parts were teleported onto the vanilla skeleton. A bone called
headwas moved to the vanilla head pivot,bodyto the model origin, the arms to(±5, 2, 0)— so a model authored standing on the Blockbench floor had its parts flung apart, and its height never matched the player. The vanilla animation is now applied on top of the pose you authored: a bone rotates about its own pivot and stays where you put it. A model built on the vanilla skeleton is unaffected.Bone names are matched case-insensitively. Blockbench's default capitalisation (
Head,RightArm) matched nothing, so on a typical model both pose-following andbody_partssilently did nothing at all.Bone-level
mirrorandinflateare inherited by cubes that don't set their own.Suppression did nothing on
apoli:multiplepowers — the reason suppress/unsuppress looked completely broken. Amultiplehas no behaviour of its own; it all lives in the sub-powers the loader generates. Suppressing the id you actually know the name of therefore switched off nothing. Suppression is now effective: suppressing a power also suppresses everything it granted, at any depth. The actions, the commands, the codecs and the sync were all fine.Suppressed powers left lasting effects behind. A suppressed power stops ticking, so anything it installed outside the container stayed applied forever:
apoli:attributemodifiers, creative flight, and game-event listeners. Those three now undo and redo their state when suppression flips.apoli:edible_itemdid nothing. The power type was registered and parsed, but nothing ever read it. It now works end to end: hunger and saturation, status effects,entity_actionanditem_action,result_stack,consume_sound, the eating animation and its duration — and it takes priority over vanilla food, so it can replace an existing food's behaviour rather than only adding food to non-food items. An omitteditem_conditionstill means every stack, matching upstream.apoli:raycast'scommand_at_hitonly fired when the ray ended on a block. Any entity in front of the block silently swallowed it, and a ray with"entity": true, "block": falsecould never fire it at all. It now runs on the nearest hit of either kind.command_hit_offset(default0) pushes back along the block's face for a block hit and back along the ray for an entity hit.Speech-to-action works on NeoForge again, without reintroducing the 1.17.4 launch crash. JNA and Vosk now ship as plain resources, are unpacked on first use, and load in an isolated class loader, so they never become JPMS modules and can't collide with another mod's copy. Verified to still work with a foreign JNA already on the classpath (the ImmediatelyFast case).
apoli:player_model_typetreated every unreported player as wide, so"model_type": "slim"could never pass and a wide/slim power pair only ever fired the wide half. The model type is now resolved in order from the client's own report, then the player's signed skin metadata, then vanilla's default-skin rule for their UUID. Vanilla clients, older Apoli clients, and players whose skin hadn't loaded yet are all handled.A malformed optional field was dropped in complete silence. A typo in a nested field name, or broken escaping inside an SNBT
tag, made the field vanish with no error — so a power ran unconditionally, or with no NBT filter, and the log said nothing. Apoli now logs a warning naming both the power and the field. It is a warning, not a hard failure, so one bad field can't drop a whole power on a live server.SNBT parse errors now report the actual problem (
Expected '}' at position 81: …) instead of the uselessExpected NBT compound, got STRING.Legacy
origins:-namespaced action ids skipped their field defaults. The type resolved fine, which is why nothing showed in the log, butorigins:replace_inventorydecoded asoperation: modify— a silent no-op.origins:drop_inventoryand the three*_teamaliases had the same problem;origins:set_no_gravityandorigins:set_gravityfailed to parse outright.inventory_actionwithoperation: replacecould not fill an empty slot, which is the common use — putting an item into an empty offhand.modifyanddropstill skip empty slots.
Комментарии
Загружаем…