
Overgrown's Apoli
A data-driven power system that's build for custom abilities, mechanics, and entire origin systems from JSON alone.
Список изменений
Fixed
-
A bi-entity
apoli:damagewith an expression foramountalways dealt its constant term, and no amount of rewriting the formula helped. The reported case was"amount": "5 + ((max_health - health) * 0.35)", meant to hit harder the more health the attacker was missing, which dealt a flat 5 every time. The cause was not the arithmetic but which entity the variables were read from:amountwas evaluated against the target, somax_health - healthdescribed the victim, and against a healthy victim that is zero. There was no way at all to reach the actor from a bi-entity expression, so the intended formula could not be written. Bare variables in a bi-entity action now read the actor — the entity whose power fired the action, which is what every other expression field in Apoli already meant — and the newtarget_prefix reads the other side. The original JSON now behaves as it reads. -
*wildcards were only expanded when they began a string, so they silently failed inside expressions."*:example / 10"worked because it starts with*:, but"5 + *:example"did not, and the result was a load error naming a variable that looked perfectly correct. Wildcards are now expanded anywhere in a string where the*:is not part of a longer word, so an expression can reference several of its own file's resources in one formula. -
The server tick loop crashed whenever an
apoli:predicatecondition ran on 1.21.1.PredicateConditionbuilt its loot context withcreate(null), which was correct on 1.20.1 where the parameter is a nullable identifier. On 1.21.1 that parameter became anOptional, andnullstill compiles — so the mistake survived the port and only surfaced at runtime as aNullPointerExceptiondeep inside vanilla'sLootContext.Builder. Any pack usingapoli:predicateinside a ticking power hit it within a second of the power activating. It now passesOptional.empty(). This was the cause of both crash reports filed against the Origins-Vampire pack; WoodWalkers was not involved. -
Clones could disguise as entities but never as players. Non-player disguises are drawn by swapping in a dummy entity, which works for any disguised entity including a clone. Player disguises take a different path — they replace the skin on the player renderer — and a clone is not rendered by the player renderer, so the disguise was simply ignored.
CloneRenderernow resolves a player disguise directly, using both the disguised player's skin texture and their slim/wide model, so a clone disguised as a player looks like that player rather than like its owner. -
apoli:force_key_presseddid nothing unless the target was a player. The action only ever sent a packet to a client, so firing it at a clone, a minion or any other entity was a no-op — which made it impossible for a pack author to trigger a power on a summon. Forced key presses are now tracked server-side per entity, soapoli:key_pressedandapoli:action_on_key_sequencesee them, and a new dispatcher firesapoli:action_on_key_press,apoli:fire_projectile,apoli:toggleandapoli:inventoryfor non-players. Players keep the existing client round-trip, so nothing about their behaviour changes. Forced keys are cleared when a summon is removed rather than being left behind. -
A minion's
invulnerableflag looked broken because vanilla exempts creative players. The flag was always being set correctly. Minecraft's own invulnerability check ignores the flag when the damage comes from a player in creative mode, which is exactly the situation a pack author is in while testing — so an "invulnerable" minion died to a single creative punch and the field looked dead.MinionEntitynow overrides that check: while the flag is set, the only damage that gets through is damage tagged as bypassing invulnerability, which still leaves/killand the void working. -
A minion's model never faced the direction it was actually looking, so anything it fired came out sideways. Two separate problems: the minion set its head and body yaw but never its body rotation, and Minecraft's body-rotation controller only drags the body toward the head slowly and clamps it to a maximum angle, so the model lagged behind and could never turn more than part of the way. On top of that, pitch was never applied to the model at all, because the renderer only rotates a model by its body yaw. The minion now snaps its body rotation to where it is looking every tick, and the model applies pitch, so
apoli:fire_projectileand anything else that uses the minion's look direction now visually lines up with where the projectile goes. The model's pivot was moved to the centre of its cube so it rotates about itself rather than swinging around its feet. -
apoli:model_colordid not tint the first-person hand on NeoForge. The hook was attached to the two public per-hand render methods and looked its model parts up by name from the renderer's current model. That model can be swapped byapoli:modify_player_model, in which case the parts being drawn are not the parts the lookup returned, and the tint silently applies to nothing. The hook now attaches to the single private method both hands funnel through and takes the model parts straight from its arguments, so there is no lookup to get wrong and no dependency on which of the two public wrappers a loader happens to call. This should be verified in game — the two loaders' source was identical, so the fault could not be reproduced by reading the code alone. -
apoli:damage_over_timeignored a pre-1.19.4damage_sourceobject and dealt generic damage instead. Packs written before damage types existed describe the damage with an object carrying anameand flags such asbypasses_armor. Apoli only ever read the newerdamage_typefield, so those packs got unarmoured generic damage with no warning. Thenameis now mapped to the matching vanilla damage type, so the Origins-Vampire pack's starvation damage resolves tominecraft:starveand bypasses armour as it was meant to. -
Disguises and forced key presses leaked for summons. Both are keyed by entity UUID and were only cleared when a player disconnected, so every clone and minion that was ever disguised left an entry behind for the lifetime of the server. Both are now cleared when the summon is removed.
-
apoli:mount's offset was ignored by every vehicle a player actually rides. The offset hookedEntity.getPassengerAttachmentPoint, the method that decides where a passenger sits — butAbstractHorse,Camel,Llama,Strider,Slime,BoatandAbstractMinecartall override it with their own seat maths, so the hook never ran for any of them. A mob riding a player worked, becausePlayerdoes not override it; a player riding a horse, pig, boat or minecart did nothing. Moving to the overriding classes would not have helped either —Boat,ChickenandAbstractHorsealso override the two-argumentpositionRider. The offset now applies fromEntity.positionRider(Entity), which isfinal, is whatEntity.rideTickcalls every tick for every passenger on both sides, and runs after whatever seat maths the vehicle used. Verified on a cow (plain mob), a saddled pig and a horse: with"y": 3.0the rider ends up exactly three blocks above the vehicle's own seat position in all three cases. -
apoli:mountdid nothing at all when the actor was already riding the target. It bailed out onstartRidingreturningfalse, which is what vanilla returns when the entity is already on that vehicle — so the offset was never stored. Firing it at a rider who climbed on by ordinary means now applies (or replaces) the offset without re-mounting, which is what makes "sit them on my shoulder" work on a mount the player got on themselves. -
apoli:modify_block_stuck_speedcould not make a block stickier, and quietly ignored any per-axis value below the block's own. The power is documented as supplying the multiplier used instead of the block's, but the merge folded the block's own value into the comparison — so against cobweb's0.25, amultiplierof0.02came out as0.25and did nothing. The block's multiplier is now replaced outright; when several matching powers apply, the largest value on each axis still wins, so the least restrictive power decides. The "1.0 on every axis skips the mechanic entirely" behaviour is unchanged, and the stuck-in-block hook now does one lookup per call instead of two. -
apoli:exposed_to_sunwas really justapoli:exposed_to_sky. It read the raw sky-light value at the entity's feet, which is 15 outdoors at midnight and in a thunderstorm alike, so it returned true whenever the sky was visible at all. It now requires all four of the things the name implies: it is daytime in that dimension, the sky is visible from the entity's eyes, it is not raining on the entity, and the light level at the eyes is above 0.5 — the same test vanilla uses to decide whether an undead mob burns. -
apoli:daytimeandapoli:exposed_to_sunwere frozen client-side. Minecraft only computes a client world's ambient darkness once, when the world is created, so any condition derived from it answered with whatever the sky looked like at join time for the rest of the session. Both conditions now refresh it, at most once per tick, before reading it. -
apoli:modify_healingdid nothing at all. The type parsed and registered but had no runtime hook, so a power using it was silently inert. It is now applied to every source that routes through the game's healing path — natural regeneration, Regeneration and Instant Health, golden apples, feeding a tamed animal, and theapoli:healaction. The result is clamped at zero, so{"operation": "multiply_total", "value": -1}stops healing entirely rather than turning it into unblockable damage. -
apoli:self_glowevaluatedbientity_conditionwith the actor and target the wrong way round.apoli:actor_conditioninside a self-glow tested the viewer rather than the entity holding the power, which reads exactly like the condition being ignored — a glow gated on the holder's inventory appeared for anyone. In both glow typesactoris now always the power holder andtargetis the other party: forapoli:entity_glowthat is the viewer and the entity that would glow, forapoli:self_glowthe glowing holder and the viewer deciding whether they can see it.apoli:entity_glowwas already correct and is unchanged. -
An
apoli:inventorypower's contents were invisible to client-side conditions. Glow and render powers evaluate their conditions on the client, where a power container simply read as empty — so anapoli:inventorycheck inside anapoli:entity_glowcondition could never pass, no matter what was in the container. Your own power containers are now mirrored to your client and kept in step as their contents change, so an inventory check inside a client-evaluated condition works. Other players' containers are still not sent, andapoli:command,apoli:advancement,apoli:scoreboard,apoli:statandapoli:predicateremain server-only. -
Ropes could fling an endpoint hundreds of blocks when their physics were tuned past 1.
radial_dampingabove1.0multiplied outward speed every tick instead of bleeding it off, andstiffnessorspring_scalingabove1.0made each correction overshoot further than the last — both are exponential runaways, which is why a rope felt fine while it was slack and exploded the moment it was stretched, and why the problem only appeared after a reload or a re-cast (physics settings are captured when the rope is created, so editing them changes nothing for ropes that already exist).stiffness,radial_dampingandspring_scalingare now clamped to0.0–1.0andswing_boostto0.0–2.0at load time, with a warning naming the field and the value it was clamped to. As a backstop the constraint will never apply more than 4 blocks/tick of correction, so no combination of settings can launch an endpoint across the world. -
A resource's
min_actionormax_actionfailed silently when it did not parse. The optional-field decoder treats "absent" and "present but malformed" as the same thing, so a mistyped action was discarded without a single line in the log — the resource loaded, the boundary action just never existed. Every optional action field in Apoli now logs a warning naming the power and the field when it drops one, with the underlying parse error attached. (The usual cause is a legacy damage source:apoli:damagetakesdamage_type, not the pre-1.19.4sourceobject.) -
A malformed nested condition silently removed the gate instead of failing. The same lenient decoder applied to every
entity_condition,bientity_condition,block_conditionand friend, so a typo inside one dropped the whole condition and left the power, action or condition running unrestricted — anapoli:entity_glowwhosebientity_conditiondid not parse lit up every entity in range, forever, with nothing in the log. Dropping a gate fails in the dangerous direction, so every condition field is now strict: an unparseable condition fails the power it belongs to, and the log names the field, the power and the reason. This is the rule Apoli 1.29.1 already applied to a power's own top-levelcondition, extended to the nested ones.
Added
-
apoli:custom_model_rendergeometry models can play Blockbench animations. A newanimationsfield takes the same shapehud_renderdoes — one object, or an array read top to bottom where the first entry whoseconditionpasses is the one that plays. Each entry names an animation file (example:wingsresolves toassets/example/animations/wings.animation.json, and a plain.jsonextension works too), optionally the animation inside it by name, and optionallyspeedand aloopoverride. That is the whole feature: a data pack drives a model's state machine — flapping while gliding, idle otherwise — without writing a line of Java. Theposition,rotationandscalechannels are read with linear interpolation between keyframes, honouring a keyframe'spre/postpair so Blockbench's stepped keyframes survive the trip. Playback time restarts whenever the selected entry changes, so a non-looping animation replays every time its condition flips back on. -
apoli:fluid_vision, a power that replaces the fog while the camera is inside a fluid. It isapoli:lava_visiongeneralised: afluidfield pickswater,lava,powder_snowornone,startandendset the fog distances, an optionalfog_coloroverrides the tint, andrender_overlay: falseremoves the full-screen fluid texture. Water vision — the thing packs have been asking for and faking with potions — is now three lines of JSON.apoli:lava_visionis kept as a type alias that defaultsfluidtolava, and itssandvfields still decode asstartandend, so no existing pack has to change. -
apoli:damage_would_kill, an entity condition and a bi-entity condition. It simulates a hit of a givendamage_typeandamountand answers whether it would actually kill. The simulation is the real pipeline, not a health comparison:apoli:modify_damageandapoli:modify_projectile_damagepreviews, invulnerability, shield blocking, the hurt cooldown and its rememberedlastHurt, armour with the attacker's weapon taken into account, Resistance, Protection and absorption — and it returns false when anapoli:prevent_deathpower would catch the entity anyway. The bi-entity form builds the damage source with the actor attached, so attacker-side damage modifiers and shield facing count. It reads the world and never changes it: no armour is damaged and none of the modify-damage powers' actions fire. -
apoli:critical, a damage condition that passes on a vanilla critical hit. The flag is taken from the attacking player's own critical-hit decision at the moment the hit is dealt, so it is true for exactly the hits vanilla calls critical and false for everything else, crit arrows included. -
Voice loudness, and the rest of the voice state, are readable from expressions. Five new variables:
voice_loudnesson the same 0–100 scale theapoli:voice_loudnesscondition has always used,voice_loudness_normalizedon a 0–1 scale for when the number is going straight into an arithmetic formula, andvoice_speaking,voice_whisperingandvoice_disabledas 0 or 1. Both loudness scales exist deliberately — the 0–100 one so that a threshold picked by testing against the condition means the same thing in an expression, and the 0–1 one because a shout scaling a damage number wants a small multiplier rather than something that adds a hundred hearts. So"amount": "5 + (1 + voice_loudness_normalized)"is the form you want for damage, and"5 + (1 + voice_loudness)"is the form you want when 100 really is the intended magnitude. All five take theactor_andtarget_prefixes like any other variable, so a bi-entity action can read how loudly the other entity is talking. -
apoli:modify_hearing_range, a power that changes how far away its holder can hear. It covers both ordinary game sounds and Simple Voice Chat proximity audio, and it is listener-side: it changes what its holder hears, never what other people hear coming from them. Modifiers apply to whatever the sound would normally carry, so a single power covers everything at once — for game sounds that is the sound event's own range, which is 16 blocks for most things and much further for loud ones like a lightning strike, and for voice chat it is the server'smax_voice_distance, orwhisper_distancewhen the speaker is whispering. A result of zero or less means the holder hears nothing at all on that channel, and there is no upper bound, so a high enough number lets them hear the entire dimension. Two boolean fields,soundsandvoice, both defaulting to true, let a power take only one of the two channels. The modifier's value is an expression, so hearing can be driven by a resource. -
apoli:voice_whispering, an entity condition that passes while the entity is talking with Simple Voice Chat's whisper key held. This is real whisper state read from the voice packets themselves rather than something reconstructed from volume, and it is false whenever the entity is not speaking at all, so it already impliesapoli:voice_speaking. -
apoli:execute_command, a bi-entity action. It runs a command on the server as the actor, at permission level 4 with output suppressed, after substituting the actor's and target's UUIDs into the command string. The placeholders are%aand%tby default and can be renamed per action withactor_selectorandtarget_selector, or disabled by setting either to an empty string. Because the command runs as the actor,@sinside it is the actor and moving execution to the target is done the ordinary way, withexecute at %t run .... -
apoli:resourcecan store more than one value. A newsizefield turns a resource power into a table of that many independently addressable slots, each with the same bounds and starting value as before, and each addressed by a zero-basedposition. This is the answer to needing to remember a handful of related numbers without either fifty separate powers holding a 0 or a 1, or a detour through scoreboards and command blocks. Slot 0 is the resource's ordinary scalar value, so the HUD bar, an unindexed expression reference and everything else that reads a resource without asking for a position behave exactly as they always did — the feature is additive and no existing pack sees any change. The field also answers topositionsandslots, and there is no ceiling on it: slots are only allocated when they are written, so a declared size of a million costs nothing until you write to a second slot, and a slot that has never been written reads back as the resource's starting value. What a table costs is decided by the highest slot you actually write, not by the number you declare.sizeis also the guard on a computedposition— a write at or above it is refused — so declaring one far larger than you need gives that guard nothing to do. A declared size above 65536 logs one warning naming the power, as a check against a typo. -
positionis accepted everywhere a resource is read or written. On theapoli:resourcecondition it selects a slot, and leaving it out asks a different and equally useful question — whether any slot satisfies the comparison, which is how you ask "is this value in the table at all". Onapoli:modify_resourceit selects the slot to change, and leaving it out applies the modification to every slot at once, which is how a table gets filled or cleared in one action. It is also accepted on an attribute modifier that reads its value from a resource, and on the/apoli:resourcecommand, which gained alistsub-command that prints a whole table at once. In every casepositionis a full expression rather than a plain number, so the slot being read can itself be computed — one resource can act as a cursor into another. -
Resources can be copied, whole tables at a time.
apoli:modify_resourcegainedfromandfrom_position. Given onlyfrom, it copies the source resource into the destination slot for slot, which saves writing one action per slot for a table of any size. Given a position on either side, it copies a single slot. The modifier's operation still applies, so copying withadd_base_earlyaccumulates the source into the destination rather than overwriting it, and the default is a straight copy. A single-slot copy was already expressible as an expression, and still is. -
minandmaxonapoli:resourceare now optional, and an absent bound means no bound. A running total, a score or a counter that should never stop climbing no longer needs a made-up ceiling picked large enough to never be reached. -
A
maxfield on Hud Render, for bars that need a scale. A bar is a fraction between empty and full, so an uncapped resource has nothing to fill up to. Giving thehud_renderits ownmaxlets the resource stay uncapped while the bar still fills toward a value you name, and because it is an expression that value can move — a score bar can fill toward the next milestone and reset as the milestone rises. Without one, a bar on an uncapped resource is not drawn and Apoli logs one warning naming the power, rather than showing a bar frozen at one end. The same field works on a cooldown bar if you want it to fill over something other than the full cooldown. -
Expressions can index a table and ask questions about it.
example:table[2]reads a slot, and the index inside the brackets is a full expression, soexample:table[example:cursor + 1]is valid.example:table_sizereports how many slots a resource has, alongside the existing_minand_maxsuffixes. There are also functions that search a table rather than indexing it:resource_contains(id, value)answers whether any slot holds a value, andresource_index_of(id, value)returns the first slot that does, or -1. -
Expressions gained functions that take an id. These are written
resource(example:mana),has_power(example:some_power),has_resource(example:mana)and so on, with the identifier passed literally rather than being read as a variable. Between them they make explicit what the barenamespace:pathform does implicitly, and they add the two questions that had no expression form at all — whether the entity holds a given power, and whether a resource exists on it rather than what its value is. -
A bi-entity action's expressions can now reach both entities.
actor_andtarget_prefixes work on any of the ordinary variables, sotarget_health,actor_max_health,target_foodand the rest all resolve, andtarget_resource(id)andtarget_has_power(id)read across as well. The binding covers the whole action, including anything nested inside it, so anapoli:actor_actionwrapping aapoli:modify_resourcecan still read the target. Outside a bi-entity context both prefixes fall back to the single entity in scope, so an expression written with them stays valid wherever it is reused. -
apoli:raycastexposes what it hit. Insidehit_action,miss_action,bientity_actionandblock_action, and anything nested in them, the expression variablesdistance,hit_x,hit_y,hit_z,countandindexdescribe the hit. Per-entity hooks get that entity's own hit point and its position along the ray; the block hooks get the block's hit point; the hit and miss hooks get wherever the ray finally stopped. Storing the hit distance in a resource is now a singleapoli:modify_resourcewith"value": "distance", where previously it needed a longapoli:if_else_listof hard-coded distance bands. -
apoli:area_of_effectcan count.countandindexare bound as expression variables while it runs,bientity_actionbecame optional so the sweep can be used purely to produce a number, and a newafter_actionruns once on the actor afterwards with the final count available. "How many mobs are near me" is now a value you can store and compare rather than something approximated with nested conditions. -
apoli:entity_in_radiusandapoli:block_in_radiustake an expression forcompare_to, so the threshold can be read from a resource or computed rather than being fixed in the JSON. -
apoli:modify_model_partstransformations take expressions. Both a transformation'svalueand each keyframe'svalueaccept a formula instead of a plain number, so a part can be driven directly by a resource —"value": "example:charge / 10"scales a limb with a charge meter, with no intermediate power. This is evaluated as the model renders, on the client, so it can read anything the client knows about: resources are synced, including individual table slots, which are carried by a new network message added for the purpose. -
A
power_countexpression variable reports how many powers the entity currently holds. -
Twenty-one power, action and condition types from the original Apoli that this rewrite had never implemented. Data packs written for Apace's Apoli were silently failing on any of them. The full list: the
apoli:damage_over_time,apoli:burn,apoli:modify_air_speed,apoli:modify_attribute,apoli:modify_status_effect_amplifier,apoli:modify_status_effect_duration,apoli:night_vision,apoli:lava_vision, andapoli:modify_camera_submersionpower types; theapoli:has_command_tagentity condition; theapoli:meatandapoli:relative_item_cooldownitem conditions; theapoli:bypasses_armor,apoli:unblockable,apoli:out_of_world,apoli:explosive,apoli:fireandapoli:from_fallingdamage conditions; and theapoli:custom_data,apoli:damageable,apoli:fire_resistant,apoli:conditioned_restrict_armor,apoli:fire_immunity,apoli:dummyandapoli:toggle_night_visionaliases for types that already existed under a different name. Between them these close the gap that was breaking the MobestiaryWave2 and Origins-Vampire packs; both now resolve every type id they use. -
onset_delayonapoli:action_over_time. It waits a number of ticks after the condition first becomes true before the action starts running, whilerising_actionstill fires immediately — so a power can warn you the moment a condition trips and only start hurting later. It takes an expression as well as a plain number, so the delay can be driven by a resource:"onset_delay": "20 + mypack:insulation * 26"waits longer the more insulation the holder has. -
An
apoli:difficultyentity condition and a matchingdifficultyexpression variable. The condition takes one difficulty or a list of them; the variable reads 0 for peaceful through 3 for hard. This is what makes difficulty-dependent damage expressible without a bespoke field on one power type. -
The Shappoli power-to-power messaging system.
apoli:send_action(entity, bi-entity and item flavours) andapoli:send_condition(the same three) hand off to anapoli:receive_actionorapoli:receive_conditionpower named on the same entity, which lets one power ask another to do something or answer a question instead of duplicating the logic in both. Also ported:apoli:self_bientity_action, which runs a bi-entity action with the entity as both actor and target, and theapoli:is_blockandapoli:holder_conditionitem conditions. Every one of them also answers to itsshappoli:id, so packs written against Shappoli load unchanged. -
WoodWalkers compatibility, as twelve shape types. Conditions for whether a shape has an ability, whether that ability is usable right now, how long its cooldown has left, and a bi-entity condition between the player and their shape; actions to change shape, to adjust the ability cooldown, to trigger the ability and to run a bi-entity action against the shape; and powers that fire when the shape or the ability is used, or that prevent either. All twelve are registration-gated, so they do not exist without WoodWalkers installed, and all of them answer to their
shappoli:ids. Apoli talks to WoodWalkers through its published API resolved once at load, so a WoodWalkers update that changes that API logs one warning and turns the types inert rather than crashing. Apoli's own disguise system and a WoodWalkers shape now agree with each other:apoli:disguisedpasses for a shape as well as for a disguise, and an explicit Apoli disguise takes precedence in rendering. -
Scripting, through KubeJS.
apoli:scriptexists as an action and a condition in all four flavours, plus a power type for reacting to a power being granted or revoked. There are two ways to write one. From a KubeJS script you register a handler under an id —ApoliEvents.entityAction('mypack:zap', event => { ... })— and reference that id from JSON. From a data pack you ship a.jsfile atdata/<namespace>/apoli/scripts/<path>.jsand reference it by that path, in which case the file body is the action andctxandparamsare already in scope. AnApolibinding exposes granting, revoking and querying powers. KubeJS has no Fabric build for 1.21, so on that one branch the types load, log once and do nothing rather than failing the pack. -
A sandbox for data pack scripts, off by default. A
.jsfile inside a data pack is content that travels with a world, so it is not trusted the way an operator's own KubeJS script is. Scripts run server-side only and are never sent to a client. They resolve through the resource manager by namespaced id, so no filesystem path is ever built from pack-supplied text and a crafted id cannot escape the data pack. The script scope is built from Rhino's safe standard objects, which means noPackages, nojava, noJavaAdapter, noimportClass, and no reachablegetClass()on anything Apoli hands over. On top of that, any member whose type touches file, network, reflection, class-loading, process or server-internal APIs is filtered out of the object model. Every invocation runs against an instruction budget so a runaway loop aborts the script instead of hanging the server. The whole file-based path is disabled until an operator setsallow_data_pack_scriptsinconfig/apoli-scripts.json. -
A new
apoli:if_casemeta action. It takes the same listapoli:if_else_listdoes and runs every case whose condition holds, rather than stopping at the first. All conditions are evaluated up front, against the state as it was when the action started, so an earlier case cannot switch a later one on or off.apoli:if_else_listkeeps its sequential-escape behaviour and is unchanged — useif_else_listfor a ladder ("pick the first tier that fits") andif_casewhen the cases are independent ("apply each effect that currently applies"). The list is calledcases, butactionsis accepted as a legacy spelling, so converting a block between the two types means changing only thetype. -
apoli:mounttakes an offset. Newx,yandzfields shift the rider away from the vehicle's usual seat, andspacedecides how they are read —worldfor an absolute offset,localorlocal_horizontalto rotate the offset with the vehicle so a shoulder stays a shoulder when the vehicle turns. The offset is remembered per rider until it dismounts and is applied on both sides, so hit detection, rendering and the rider's own camera all agree. A newforcefield exposes the "mount even if the target would normally refuse" flag that was previously hard-coded on. -
The reach attributes exist on 1.20.1.
minecraft:player.block_interaction_rangeandminecraft:player.entity_interaction_rangewere added to Minecraft in 1.20.5, so on 1.20.1 anapoli:attributepower naming them modified an attribute that did not exist and did nothing — silently, because an unknown attribute is skipped rather than reported. The 1.20.1 build now registers both itself, with vanilla's4.5and3.0defaults, and answers to theminecraft:names as aliases, so the same power JSON works unchanged on every branch. It drives the same places vanilla 1.21 does: the client's block raycast and entity pick range, and the server's block-use, block-break and entity-interaction distance checks. With no modifier applied the server limits work out to vanilla's exact numbers. One 1.20.1-only limit: the client looks for entities inside the block raycast, so entity reach is capped by block reach — raise both together, as Origins' Enderian does.
Changed
-
Voice loudness is measured on a better-calibrated scale. The value is still approximated from the size of the encoded voice packet rather than by decoding any audio, which is what keeps it cheap, but the reference it was being divided by assumed a frame roughly four to eight times larger than real speech actually produces. In practice that meant a player shouting rarely got past about 20 out of a possible 100, and the top three-quarters of the scale were unreachable. The measurement now runs from a silence floor of 16 bytes to a loud-speech reference of 160, and it is smoothed with a fast-attack, slow-decay envelope so that the number behaves like a loudness meter instead of flickering between frames. Anything comparing against the default of
>= 1is unaffected, since any speech at all still clears it. Anything comparing against a specific number tuned by testing will now read considerably higher than it used to and is worth re-checking. -
Voice state is discarded when a player disconnects rather than being kept until the server stops. It was a small leak of a handful of map entries per player per session, and it also meant a returning player could briefly be seen as still speaking.
-
Bare variables in a bi-entity action now describe the actor rather than the target. This is the behaviour change behind the damage fix above, and it applies to
apoli:add_velocityas well, wherex,yandzpreviously read the entity being pushed.apoli:mountalready read the actor and is unaffected. Both actions gained thetarget_prefix, so a pack that genuinely wanted the old meaning can restore it by adding that prefix to the variables in question. Bi-entity conditions deliberately do not take part in this: they are evaluated once per candidate entity inside things likeapoli:entity_in_radius, which makes them a hot path, and no bi-entity condition takes an expression field today. -
The
[character in an expression is no longer unconditionally a random generator. Brackets whose contents are one of the generator names —[Uni],[Nat3],[Int]and the rest — still work exactly as before. Anything else in brackets is now read as a table index, and is only allowed directly after an identifier containing a colon, so a mistake there is a load error naming the position rather than a confusing "unknown random generator". -
modifieronapoli:modify_resourcebecame optional, defaulting to setting the value to zero. This is what makes the copying form above readable:{"resource": "a", "from": "b"}with no modifier is a plain copy. -
apoli:resourceskips its per-tick clamp entirely whenenforce_limitsis off, rather than running a clamp that could not change anything. No behaviour change, marginally less work per tick per resource. -
Four power types were merged into the general types they were shorthand for.
apoli:damage_over_time,apoli:burnandapoli:exhaustare now aliases ofapoli:action_over_timewith the relevant action filled in, andapoli:modify_attributeis an alias ofapoli:attributewith the attribute folded into each modifier.apoli:fire_immunityhad already become an alias ofapoli:invulnerabilitywith a fire damage-type tag — the same JSON Origins already writes for Blazeborn. Every one of these ids still loads and existing JSON still parses; what changed is that there is now one implementation behind each of them instead of four separate ones with their own tick loops and state. The reshaping happens once when the data pack loads, so there is no runtime cost, and because the ids resolve to the canonical type, indexing, suppression, syncing and profiling all report the canonical name. -
apoli:exhaustwas already redundant before this release and was caught by the same audit; it is the same power asapoli:action_over_timewith anapoli:exhaustaction. -
apoli:script's power type lost itstickandis_activefields. Both were duplicates the day they were written: ticking isapoli:action_over_timewith anapoli:scriptaction, which additionally gives youinterval,onset_delay,rising_actionandfalling_action, and gating is theconditionevery power already has holding anapoli:scriptcondition. The power type now covers only the thing that cannot be composed — running a script when the power is granted or revoked. The matchingpowerTickandpowerActiveKubeJS events were removed with them rather than being left as hooks that never fire. -
apoli:damage_over_timelostprotection_enchantmentandprotection_effectiveness. They stretched the onset delay along a fixed curve based on how much of one enchantment the holder was wearing — a very specific answer to a general question, and one that only ever worked for enchantments. The replacement is the expression form ofonset_delay: track whatever you want to call protection in anapoli:resourceand write the curve yourself. A pack that still sets the old fields gets one warning naming the replacement and otherwise keeps its plain delay. Three files in the MobestiaryWave2 pack use these fields and will start damaging without the delay. -
Everything that reads or reshapes data now goes through codecs and
Dynamic, not Gson. The power-loading pipeline — wildcard substitution, the legacy shape rewrites,apoli:multipleexpansion, alias field renames and alias defaults — used to walk Gson trees by hand, which pinned all of it to JSON. It is now written againstDynamicOps, so the same logic works unchanged on any data format. Alias defaults are no longer hand-built JSON objects but real values with their codec, encoded into whatever format the power is being read from. The Bedrock model parser, the recipe power, the particle data type, the skill-tree and global-power loaders, the client key watcher, the skin-metadata reader and both config files were converted the same way. What remains of Gson is only the places Minecraft itself uses it: the constructor of vanilla's JSON reload listener, the tree type thatJsonOpsoperates on, and parsing a string into a tree. On 1.20.1 two loot functions and the recipe hand-off also stay on Gson, because codecs for those parts of Minecraft only arrived in 1.20.5. -
RecipePower's config exposes the recipe as aDynamicrather than a Gson object. This is a source-level break for anything compiled against Apoli's Java API; Origins has been updated to match. ArecipeAsJson()helper is provided on 1.20.1 where vanilla's recipe parser still requires a Gson tree. -
The network protocol version moved from 7 to 8. Two new server-to-client messages were added (a power container's contents, and a rider's mount offset), so a 1.38.0 client and a 1.37.0 server are not compatible.
