Apoli (1.90.0)
Curse Maven Snippet
What's new
1.90.0
Added — the apoli:print entity action
Writes a line to the server log, and optionally to the entity's chat. A debugging tool: drop it into an action list to see whether that branch ran and what a value came out as. Also answers to apoli:log and apoli:logger.
| Field | Type | Default | What it does |
|---|---|---|---|
text |
Text Component | optional | What to print. A plain string works; so does a full component with colour, translate or keybind. |
number |
Expression | optional | A number to print, evaluated against the entity every time the action runs. |
message_id |
String | apoli/print |
The logger name the line is written under, so your prints can be filtered out of the rest of the log. |
show_in_chat |
Boolean | false |
Also send the line to the entity. Only players can see chat. |
With both text and number the line reads Text: <text> Number: <number>. With one, it is just that value. With neither it prints the message_id, so a bare {"type": "apoli:print"} is a "did this run?" marker.
Fixed — apoli:print crashed on anything that was not a player
The action cast the acting entity to a player before doing anything else, so running it on a mob threw a ClassCastException out of the middle of whatever action list it sat in. It works on any entity now.
Fixed — apoli:print crashed on an expression, and on an empty config
A number written as an expression rather than a literal threw instead of printing — the action asked for the expression's constant value, which a real expression does not have. That is the only reason the field is an expression at all. A config with neither text nor number threw as well.
Fixed — apoli:print truncated decimals
"number": 2.5 printed 2. Numbers keep their decimals now, and a whole number prints as 5 rather than 5.0.
Fixed — a particle that could not be read failed silently
apoli:spawn_particles, apoli:particle and a raycast's particles all skipped a particle they could not build without a word, so a misspelled id — or a form the version does not accept — looked exactly like a power that was not running. Apoli names the particle in the log now, once, the first time it tries to build it.
That turned up a real trap. The params short form is 1.20.1 only:
{"type": "minecraft:dust", "params": "0.96 0.95 0.76 1.5"}
1.20.1 reads it as /particle command arguments. 1.21.1 reads it as SNBT, cannot, and spawns nothing. Write the fields out instead — this is read identically on every version:
{"type": "minecraft:dust", "color": [0.96, 0.95, 0.76], "scale": 1.5}
Fixed — inline badges on an apoli:multiple power logged a false warning
Origins reads a power's inline badges straight out of its file, but the apoli:multiple expansion did not recognise the field and warned on every load that it was "being ignored". The badges were never ignored; the warning was.
Fixed — Fabric 1.20.1 did not compile
apoli:freeze's action class imported an unrelated Fabric event class instead of Entity and Optional, on that tree only, so 1.89.0 never built there.
Performance — apoli:block_in_radius stops as soon as the answer is decided
The condition used to count every matching block in the radius before comparing, and built a list of every position in the shape before it started. A ">=" 1 check over a radius of 5 walked 1331 positions and allocated about a thousand short-lived objects, even when the first block it looked at already settled the question. It walks the shape in place now and returns the moment the count passes the threshold — the answer is identical for every comparison, because any count above the threshold compares the same way.
> It still has to look at the whole radius to prove a block is absent, so this is still the expensive kind of condition to put on a power that is evaluated every tick — and apoli:shader re-resolves its condition once per player, every tick. When a per-tick power needs an expensive condition, put the condition on an apoli:action_over_time with an interval, have its rising_action and falling_action write a resource, and test that resource instead. There is a worked example on the apoli:action_over_time page.
Performance — smaller things
apoli:printresolves its logger once, when the power is read, instead of asking the logging framework for it on every call — that lookup walks the call stack. It also returns before evaluatingnumberwhen nobody is listening, i.e.show_in_chatoff and the logger's level excludingINFO.apoli:action_on_key_pressno longer allocates a lookup key per power, per tick, when no cooldown is running. An origin with twenty keyed sub-powers was allocating twenty short-lived objects every tick, per holder, to find an empty map.
1.89.0
Removed — the apoli:freeze power type
The apoli:freeze and apoli:unfreeze entity actions do everything it did and take a duration, so the power was a second way to say the same thing.
{"type": "apoli:freeze"}as a power keeps working. It is read as anapoli:action_over_timerunning theapoli:freezeaction every tick, so existing packs behave exactly as before — including theircondition.- New packs should write the action. It can be fired from anything, takes a
duration, and sits in anactionslist next to other steps.
Fixed — freezing never actually dealt freeze damage
Vanilla drains the freeze counter by 2 every tick before it checks whether you are fully frozen, so a counter pinned at exactly the threshold is always 1 below it when the damage check runs. The old power pinned it there, which is why it showed full frost and never hurt anyone. apoli:freeze with no duration now sets the counter far enough past the threshold to survive the drain, so "frozen solid" means what it says.
Fixed — apoli:freeze evaluated its duration twice
A duration written as an expression was evaluated once to test its sign and again to apply it, so anything involving random(…) froze you for a different length than it checked. duration is also optional now.
Fixed — apoli:modify_fog powers fought over fields they did not set
Every active fog power used to claim every field. Two powers at the same priority meant the second one did nothing at all, even for fields the first one left alone — so {"s": 0, "v": 6} plus {"r": 0.4, "g": 0, "b": 0.5} drew thick fog in the wrong colour. Each field is now decided on its own, and a power only enters the contest for a field it actually sets.
Fixed — apoli:modify_fog fade-out could flash black
A fog power ending with no recorded vanilla colour faded towards black instead of towards the colour the game was about to draw.
Performance
- Resolving fog no longer allocates a map and seven
Optionals per tick; it folds into a reused buffer, and the power id is interned instead of rebuilt per call. - Fog interpolation returns the target itself once a fade has finished, so a settled fog allocates nothing per frame.
1.88.0
Added — reflective projectiles
apoli:fire_projectile, as a power type and as an entity action, takesreflective,max_bounces(default4,-1for no limit) andbounce_speed(default1.0). A reflective projectile mirrors its velocity through the block face it hits and carries on flying instead of stopping.- Every bounce runs
block_action_on_hitand the newbientity_action_on_bounce.bientity_action_on_missis held back until the projectile actually stops, so "missed" still means missed. - Works on Apoli's own
texture_locationprojectiles and on any vanilla or modded projectile fired throughentity_type.
Added — apoli:modify_resource_change
A power type that scales the size of every change made to the holder's resources and cooldowns, rather than setting a value.
modify(gain,drain,both),resources,cooldownsand an optionalresourcepick what it touches.- Cooldowns are read as remaining ticks, so their natural countdown of one tick per tick is a drain —
"modify": "drain"withmultiply_total: 1empties every cooldown twice as fast. Fractional rates are carried between ticks rather than rounded away. - Holders without the power skip the whole path; the resource-write hook only costs anything while some loaded pack uses it.
Added — apoli:source damage condition
Checks which power dealt the damage, and which source granted that power. With no fields it matches anything a power caused and nothing a sword, a fall or a creeper did. source matches the grant source, so "origins:layer/origin" means "anything the player's origin gave them". Attribution follows the power through delayed and nested actions.
Added — advancement triggers
apoli:powerfires when a power is granted, with optionalpowerandsource.apoli:resourcefires when a resource or cooldown the player holds is written to, with optionalresourceand avalueint range.
Added — voice chat
apoli:modify_speaking_range, the speaker-side counterpart toapoli:modify_hearing_range: it changes how far the holder's own voice carries.normalandwhisperpick which channel. Speaking range resolves first and each listener's hearing range applies on top, so the two compose.apoli:modify_hearing_rangetakes abientity_condition, evaluated per speaker with the holder as the actor and the speaker as the target — "I hear my own team twice as far, everyone else normally". Pairwise ranges are resolved once a tick and only for players who are actually talking. A power that sets it is skipped for game sounds, which have no speaker to test.- Compat verified against Simple Voice Chat 2.6.24. Every API member Apoli uses is unchanged; the build still compiles against the 2.6.0 API so one jar works across the whole 2.6.x line.
Added — apoli:toggle turn-on / turn-off conditions
turn_on_condition and turn_off_condition gate the key, separately per direction — a state you can only enter while sneaking and only leave while standing. Neither gates apoli:toggle the entity action, and retain_state: false still forces the toggle off when the power's own condition stops holding, so a state can always be taken away from a holder who could not have let go of it themselves.
1.87.1
Fixed
apoli:custom_model_renderingeometrymode rendered upside down and mirrored on custom projectiles, hanging below the projectile. Entity models are authored in a flipped space that a living entity's renderer sets up for free; the projectile renderer was missing it. The same.geo.jsonnow looks identical on a player and on a projectile.
1.87.0
The scale system from 1.86.0 stored its values correctly but almost nothing acted on them. This release makes it work.
Fixed — the scale system did nothing visible
- Hitboxes never scaled. The hitbox hook sat on a method that every player and mob overrides, so it never ran for either.
- Models never scaled on players. The model hook sat on a method the player renderer overrides. Scaling now happens above every renderer, so it covers players, mobs, armour stands, item frames and modded entities alike. The entity's shadow scales with it.
apoli:scalepowers did nothing on non-living entities, despite the power ticking them.
Fixed — held items
- Held items were only scaled in first person, which is why
apoli:held_itemandapoli:baseinteracted so strangely. Both views now go through one path. apoli:held_itemis no longer built fromapoli:base. In third person the item is drawn inside the entity's model and already follows it, sobasewas applying twice.apoli:held_itemis the extra multiplier on top, in both views. This matches Pehkui.
Fixed — multiplayer
- Scales are sent to every player tracking the entity, and re-sent on join, respawn and dimension change.
- A scale now survives death — respawning keeps the size you had.
- A
ticksanimation is sent once as its start, end and easing rather than as a value per tick, so a four-second shrink costs one packet and every client eases through it in step.
Performance
- An entity whose size is settled does no per-tick work at all. The tick hook returns immediately unless an animation is in flight.
- A scaled entity with no
apoli:scalepower keeps its resolved values until something actually changes them — granting the power, revoking it, suppressing it, or a/reload. Only expression-driven scales are recomputed every tick. - Render frames share one cached resolution instead of recomputing per frame.
Added
apoli:action_over_timeaccepts an Expression forinterval, on the power and on each entry ofactions. The gap is measured from the last firing and recomputed when the action runs, so it can follow a resource, an enchantment level, anything an expression can read.{ "type": "apoli:action_over_time", "interval": "20 + min(enchantment[origins:water_protection, armor, sum], 8) * 6", "condition": { "type": "apoli:in_rain" }, "entity_action": { "type": "apoli:damage", "amount": 2, "damage_type": "origins:hurt_by_water" } }An expression interval also fires on the tick the condition becomes true instead of up to one interval later. The trade: it checks the condition every tick, where a plain number checks once per interval. A plain number costs exactly what it did before.
scale_typeis accepted as a spelling ofscale_typeson theapoli:scalepower and entity action.
Changed
- A malformed
intervaloronset_delayonapoli:action_over_timeis now logged. It used to be swallowed and silently replaced with the default, so a typo in an expression turned into "every 20 ticks" with no trace.
1.86.0
Added — the scale system
Size changing, built into Apoli. No Pehkui required.
- Nineteen scale types, each one axis of an entity's size, all starting at
1.apoli:baseis the master dial;apoli:widthandapoli:heighthang off it;apoli:hitbox_*andapoli:model_*hang off those — so something can look enormous and still fit through a door, or the reverse. The non-geometry dials are built from the geometry ones:apoli:fallingis one divided byapoli:motion, so a bigger entity takes more fall damage and a smaller one less, with nothing to wire up. apoli:scalepower — size for exactly as long as the power is held. Takes an Expression, so an entity can shrink as it is hurt and grow back as it heals with no resource plumbing.apoli:scaleentity action — size that sticks. Stored on the entity, survives relogs and dimension changes, and eases into place over a number of ticks with a choice of fourteen easings.apoli:scaleentity condition to read one back./apoli:scale—get,set,add,subtract,multiply,divide,power,reset,list. Works on any entity, not just players.- Pehkui compatibility: when Pehkui is installed, Apoli hands its values to Pehkui rather than applying them itself, so the two mods agree on one size instead of multiplying each other.
Added
apoli:solid_hitbox— makes an entity's hitbox solid the way a boat or a shulker is, so you can stand on it. Takes abientity_condition, so it can be solid to some entities and not others.
Fixed
- Expressions inside
modify_damage_dealtread the wrong entity."amount": "nbt[Health]"read the victim's health instead of the attacker's, andresource(...)read the victim's resources. Every modifier now evaluates against the entity that holds the power, withactor_andtarget_bound correctly.modify_projectile_damagegot the same fix;modify_damage_takenwas already correct. apoli:prevent_elytra_flightdid nothing. It was registered with a codec and had no runtime hook at all. It now blocks take-off and also stops an elytra mid-glide when its condition turns true.
1.85.0
Added — body parts
apoli:body_partdamage condition. Use it insideapoli:modify_damageto react to where a hit landed. It casts the attacker's look, a mob's swing or a projectile's path against the posed limbs, falls back to the#apoli:body_part/*damage type tags, and then to a random limb for damage with no direction.- Part groups that move as one rigid piece —
arms,legs,upper,lowerandwhole, each about the joint that holds it on, with an optionalpivot. Plus the regionshands,feet,chest,backandachilles_heelfor anchors and hits. - One registry now backs every part name in the mod, so a name that works in one field works in all of them.
Added
apoli:energy_swirl— the charged-creeper swirl as a power, preconfigured, with ascroll_speed.- Expressions read enchantments and NBT:
enchantment[id, slot, sum|max]andnbt[source, path, aggregator](aliasdata[...]), both withtarget_andactor_forms. NBT can come from the entity, a target or actor, an item slot, a block entity or command storage.
Changed
- Texture overlays render on any living entity, not only players.
1.84.0
Four features that parsed but did nothing, finished.
Fixed
apoli:prevent_entity_collisionwas codec-only. It now works, and it covers both the soft shove and the solid hitboxes of boats and shulkers. Either side's power counts.apoli:keep_inventorywas codec-only. It now keeps the configured slots through death. Default slots are 0–35 and 100–103 (no offhand), matching Apace.apoli:phasingwithrender_type: blindnesslooked like it did nothing. Only the fog half existed, and the sky is drawn at a fixed distance where terrain fog never reaches it — so you got black fog under a bright sky. The sky is now covered too, and the in-wall test uses all eight corners of the hitbox instead of a single eye-block check, so standing half in a wall counts.apoli:teleport_to_spawnwithplayer_spawn: truecould drop you into a broken or obstructed bed. It now validates a respawn point the same way dying does and lands on the stand-up position. It only ever showed up before your first death, because dying clears an unusable respawn point.
Changed
- A summon that runs out of
max_life_ticksnow dies properly instead of quietly vanishing.apoli:action_on_deathnever fired on a timeout, so a pack that hands out summon slots and takes them back on death leaked one every time.
Added
actor_resource(id)andactor_has_power(id)expression functions — the missing counterpart oftarget_resource. Insideapoli:target_action, a bare resource id reads the target; these read the entity that ran the action.
This mod has no additional files

