▶️ ЗАБЕРИ СВОИ 8 ПОДАРКОВ 🎁 ПРИ СОЗДАНИИ СВОЕГО МАЙНКРАФТ СЕРВЕРА
Моды/Overgrown's Apoli
Overgrown's Apoli

Overgrown's Apoli

A data-driven power system that's build for custom abilities, mechanics, and entire origin systems from JSON alone.

Оцените первым
826
4
Все версииApoli (1.42.1)

Apoli (1.42.1)

Beta23.08.2026

Список изменений

Fixed

  • A bi-entity apoli:damage with an expression for amount always 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: amount was evaluated against the target, so max_health - health described 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 new target_ 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:predicate condition ran on 1.21.1. PredicateCondition built its loot context with create(null), which was correct on 1.20.1 where the parameter is a nullable identifier. On 1.21.1 that parameter became an Optional, and null still compiles — so the mistake survived the port and only surfaced at runtime as a NullPointerException deep inside vanilla's LootContext.Builder. Any pack using apoli:predicate inside a ticking power hit it within a second of the power activating. It now passes Optional.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. CloneRenderer now 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_pressed did 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, so apoli:key_pressed and apoli:action_on_key_sequence see them, and a new dispatcher fires apoli:action_on_key_press, apoli:fire_projectile, apoli:toggle and apoli:inventory for 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 invulnerable flag 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. MinionEntity now overrides that check: while the flag is set, the only damage that gets through is damage tagged as bypassing invulnerability, which still leaves /kill and 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_projectile and 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_color did 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 by apoli: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_time ignored a pre-1.19.4 damage_source object and dealt generic damage instead. Packs written before damage types existed describe the damage with an object carrying a name and flags such as bypasses_armor. Apoli only ever read the newer damage_type field, so those packs got unarmoured generic damage with no warning. The name is now mapped to the matching vanilla damage type, so the Origins-Vampire pack's starvation damage resolves to minecraft:starve and 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 hooked Entity.getPassengerAttachmentPoint, the method that decides where a passenger sits — but AbstractHorse, Camel, Llama, Strider, Slime, Boat and AbstractMinecart all override it with their own seat maths, so the hook never ran for any of them. A mob riding a player worked, because Player does 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, Chicken and AbstractHorse also override the two-argument positionRider. The offset now applies from Entity.positionRider(Entity), which is final, is what Entity.rideTick calls 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.0 the rider ends up exactly three blocks above the vehicle's own seat position in all three cases.

  • apoli:mount did nothing at all when the actor was already riding the target. It bailed out on startRiding returning false, 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_speed could 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's 0.25, a multiplier of 0.02 came out as 0.25 and 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_sun was really just apoli: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:daytime and apoli:exposed_to_sun were 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_healing did 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 the apoli:heal action. The result is clamped at zero, so {"operation": "multiply_total", "value": -1} stops healing entirely rather than turning it into unblockable damage.

  • apoli:self_glow evaluated bientity_condition with the actor and target the wrong way round. apoli:actor_condition inside 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 types actor is now always the power holder and target is the other party: for apoli:entity_glow that is the viewer and the entity that would glow, for apoli:self_glow the glowing holder and the viewer deciding whether they can see it. apoli:entity_glow was already correct and is unchanged.

  • An apoli:inventory power'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 an apoli:inventory check inside an apoli:entity_glow condition 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, and apoli:command, apoli:advancement, apoli:scoreboard, apoli:stat and apoli:predicate remain server-only.

  • Ropes could fling an endpoint hundreds of blocks when their physics were tuned past 1. radial_damping above 1.0 multiplied outward speed every tick instead of bleeding it off, and stiffness or spring_scaling above 1.0 made 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_damping and spring_scaling are now clamped to 0.01.0 and swing_boost to 0.02.0 at 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_action or max_action failed 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:damage takes damage_type, not the pre-1.19.4 source object.)

  • A malformed nested condition silently removed the gate instead of failing. The same lenient decoder applied to every entity_condition, bientity_condition, block_condition and friend, so a typo inside one dropped the whole condition and left the power, action or condition running unrestricted — an apoli:entity_glow whose bientity_condition did 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-level condition, extended to the nested ones.

Added

  • apoli:custom_model_render geometry models can play Blockbench animations. A new animations field takes the same shape hud_render does — one object, or an array read top to bottom where the first entry whose condition passes is the one that plays. Each entry names an animation file (example:wings resolves to assets/example/animations/wings.animation.json, and a plain .json extension works too), optionally the animation inside it by name, and optionally speed and a loop override. 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. The position, rotation and scale channels are read with linear interpolation between keyframes, honouring a keyframe's pre/post pair 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 is apoli:lava_vision generalised: a fluid field picks water, lava, powder_snow or none, start and end set the fog distances, an optional fog_color overrides the tint, and render_overlay: false removes 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_vision is kept as a type alias that defaults fluid to lava, and its s and v fields still decode as start and end, 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 given damage_type and amount and answers whether it would actually kill. The simulation is the real pipeline, not a health comparison: apoli:modify_damage and apoli:modify_projectile_damage previews, invulnerability, shield blocking, the hurt cooldown and its remembered lastHurt, armour with the attacker's weapon taken into account, Resistance, Protection and absorption — and it returns false when an apoli:prevent_death power 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_loudness on the same 0–100 scale the apoli:voice_loudness condition has always used, voice_loudness_normalized on a 0–1 scale for when the number is going straight into an arithmetic formula, and voice_speaking, voice_whispering and voice_disabled as 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 the actor_ and target_ 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's max_voice_distance, or whisper_distance when 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, sounds and voice, 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 implies apoli: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 %a and %t by default and can be renamed per action with actor_selector and target_selector, or disabled by setting either to an empty string. Because the command runs as the actor, @s inside it is the actor and moving execution to the target is done the ordinary way, with execute at %t run ....

  • apoli:resource can store more than one value. A new size field 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-based position. 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 to positions and slots, 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. size is also the guard on a computed position — 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.

  • position is accepted everywhere a resource is read or written. On the apoli:resource condition 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". On apoli:modify_resource it 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:resource command, which gained a list sub-command that prints a whole table at once. In every case position is 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_resource gained from and from_position. Given only from, 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 with add_base_early accumulates 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.

  • min and max on apoli:resource are 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 max field 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 the hud_render its own max lets 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, so example:table[example:cursor + 1] is valid. example:table_size reports how many slots a resource has, alongside the existing _min and _max suffixes. There are also functions that search a table rather than indexing it: resource_contains(id, value) answers whether any slot holds a value, and resource_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 bare namespace:path form 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_ and target_ prefixes work on any of the ordinary variables, so target_health, actor_max_health, target_food and the rest all resolve, and target_resource(id) and target_has_power(id) read across as well. The binding covers the whole action, including anything nested inside it, so an apoli:actor_action wrapping a apoli:modify_resource can 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:raycast exposes what it hit. Inside hit_action, miss_action, bientity_action and block_action, and anything nested in them, the expression variables distance, hit_x, hit_y, hit_z, count and index describe 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 single apoli:modify_resource with "value": "distance", where previously it needed a long apoli:if_else_list of hard-coded distance bands.

  • apoli:area_of_effect can count. count and index are bound as expression variables while it runs, bientity_action became optional so the sweep can be used purely to produce a number, and a new after_action runs 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_radius and apoli:block_in_radius take an expression for compare_to, so the threshold can be read from a resource or computed rather than being fixed in the JSON.

  • apoli:modify_model_parts transformations take expressions. Both a transformation's value and each keyframe's value accept 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_count expression 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, and apoli:modify_camera_submersion power types; the apoli:has_command_tag entity condition; the apoli:meat and apoli:relative_item_cooldown item conditions; the apoli:bypasses_armor, apoli:unblockable, apoli:out_of_world, apoli:explosive, apoli:fire and apoli:from_falling damage conditions; and the apoli:custom_data, apoli:damageable, apoli:fire_resistant, apoli:conditioned_restrict_armor, apoli:fire_immunity, apoli:dummy and apoli:toggle_night_vision aliases 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_delay on apoli:action_over_time. It waits a number of ticks after the condition first becomes true before the action starts running, while rising_action still 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:difficulty entity condition and a matching difficulty expression 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) and apoli:send_condition (the same three) hand off to an apoli:receive_action or apoli:receive_condition power 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 the apoli:is_block and apoli:holder_condition item conditions. Every one of them also answers to its shappoli: 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:disguised passes for a shape as well as for a disguise, and an explicit Apoli disguise takes precedence in rendering.

  • Scripting, through KubeJS. apoli:script exists 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 .js file at data/<namespace>/apoli/scripts/<path>.js and reference it by that path, in which case the file body is the action and ctx and params are already in scope. An Apoli binding 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 .js file 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 no Packages, no java, no JavaAdapter, no importClass, and no reachable getClass() 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 sets allow_data_pack_scripts in config/apoli-scripts.json.

  • A new apoli:if_case meta action. It takes the same list apoli:if_else_list does 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_list keeps its sequential-escape behaviour and is unchanged — use if_else_list for a ladder ("pick the first tier that fits") and if_case when the cases are independent ("apply each effect that currently applies"). The list is called cases, but actions is accepted as a legacy spelling, so converting a block between the two types means changing only the type.

  • apoli:mount takes an offset. New x, y and z fields shift the rider away from the vehicle's usual seat, and space decides how they are read — world for an absolute offset, local or local_horizontal to 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 new force field 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_range and minecraft:player.entity_interaction_range were added to Minecraft in 1.20.5, so on 1.20.1 an apoli:attribute power 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's 4.5 and 3.0 defaults, and answers to the minecraft: 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 >= 1 is 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_velocity as well, where x, y and z previously read the entity being pushed. apoli:mount already read the actor and is unaffected. Both actions gained the target_ 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 like apoli: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".

  • modifier on apoli:modify_resource became 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:resource skips its per-tick clamp entirely when enforce_limits is 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:burn and apoli:exhaust are now aliases of apoli:action_over_time with the relevant action filled in, and apoli:modify_attribute is an alias of apoli:attribute with the attribute folded into each modifier. apoli:fire_immunity had already become an alias of apoli:invulnerability with 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:exhaust was already redundant before this release and was caught by the same audit; it is the same power as apoli:action_over_time with an apoli:exhaust action.

  • apoli:script's power type lost its tick and is_active fields. Both were duplicates the day they were written: ticking is apoli:action_over_time with an apoli:script action, which additionally gives you interval, onset_delay, rising_action and falling_action, and gating is the condition every power already has holding an apoli:script condition. The power type now covers only the thing that cannot be composed — running a script when the power is granted or revoked. The matching powerTick and powerActive KubeJS events were removed with them rather than being left as hooks that never fire.

  • apoli:damage_over_time lost protection_enchantment and protection_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 of onset_delay: track whatever you want to call protection in an apoli:resource and 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:multiple expansion, alias field renames and alias defaults — used to walk Gson trees by hand, which pinned all of it to JSON. It is now written against DynamicOps, 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 that JsonOps operates 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 a Dynamic rather than a Gson object. This is a source-level break for anything compiled against Apoli's Java API; Origins has been updated to match. A recipeAsJson() 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.

Файлы

apoli-1.42.1.jar(28.74 MiB)
Основной
Скачать

Метаданные

Канал релиза

Beta

Номер версии

1.42.1

Загрузчики

NeoForge

Версии игры

1.21.1

Загрузок

4

Дата публикации

23.08.2026

Загрузил

ID версии

Главная