File Details
Ars 'n Spells 2.0.0 (Forge 1.20.1)
- R
- May 29, 2026
- 276.79 KB
- 13
- 1.20.1
- Forge
File Name
ars_n_spells-2.0.0.jar
Supported Versions
- 1.20.1
Curse Maven Snippet
[2.0.0] - 2026-05-14
Audit-driven major release
2.0.0 addresses the comprehensive technical audit at ars-n-spells-2.0.0.md. Every High and Medium-High hypothesis in the audit's "Ranked Root-Cause Hypotheses" maps to a closed fix below. The major-version bump reflects two breaking changes (new C2S packet ID; clients older than 2.0.0 cannot cross-cast against a 2.0.0 server) plus the architectural reorganization of the cross-cast pipeline.
Phase 1 — Hotfix (cross-cast actually works again)
- Client-side cancellation no longer swallows the cast. CrossCastingHandler.java used to call
handleCrossCaston both sides, returntrueon the logical client, and thenevent.setCanceled(true)— so on a dedicated server the client claimed success and cancelled the use, while the server never received any cast trigger. Silent "input detected, nothing happens" failure. The client now short-circuits before any cast logic and sends a newCrossCastRequestPacketinstead; the server is the sole authority over cast execution. - Cross-cast decoupled from mana unification. The previous
BridgeManager.isUnificationEnabled()guard inCrossCastingHandler.onRightClickItem,CrossCastingHandler.onArsSpellCost, andCrossCastIronsHandler.onIronsSpellCastmade inscribed items inert whenevermana_mode=disabledorenable_mana_unification=false. The README documentsdisabledas a mana sharing mode, not as a master switch for cross-cast. Settled policy: cross-cast remains available in every mode; the multiplier still applies; indisabledmode, native upstream pools pay native costs (no SEPARATE split, no ARS_PRIMARY conversion). The unification check survives as an internal mode-branch flag inside each cost site.
Phase 2 — Stabilization (multiplayer authoritative + traceable)
- New C2S
CrossCastRequestPacket. Carries hand, action (CAST/CYCLE), client-observed index, and a client-generated attempt UUID. The server handler re-reads the held stack (no client trust), generates its own attempt UUID for trace correlation, and dispatches to the newCrossCastingHandler.serverHandleCastentry point. - New
PacketHandler.sendToServer. Helper for the new C2S direction.CrossCastRequestPacketis registered at the tail of the packet ID list so pre-existing IDs (Resonance, Affinity, Cooldown, Aura) do not shift. - New
CrossCastValidator. Single authority for cross-cast payload validity. Checks index range, payload non-emptiness, spell-type resolution (with namespace fallback), Arsars_spellnon-empty, Iron'sspell_idparseability + level ≥ 1 + registry resolution. Rejections produce a translation key the player sees viadisplayClientMessageand a structureddescriptor_rejectedtrace event. The four-arg public API is the production path; a package-private overload accepts a synthetic Iron's predicate so unit tests do not need Forge bootstrap. 16-caseCrossCastValidatorTestcovers every branch. attemptIdUUID onCrossCastContext.Entry. Threaded from the packet handler throughserverHandleCast, intoCrossCastContext.begin(...), and read back at every Ars/Iron mixin and event-handler trace point. Parallel cross-casts no longer alias their logs.- New
util/CrossCastTrace. Structured logger emitting[CrossCastTrace] attempt=<uuid> player=<name> side=<C|S> stage=<symbolic> k=v …exactly as specified in the audit's "Debug Instrumentation Plan". Gated ondebug_mode. Stages:INPUT_DETECTED,REQUEST_SENT,REQUEST_RECEIVED,DESCRIPTOR_VALIDATED,DESCRIPTOR_REJECTED,RESOURCE_CHECK,RESOURCE_SPEND,ARS_COST_APPLIED,IRON_COST_APPLIED,UPSTREAM_CAST_ENTER,UPSTREAM_CAST_EXIT,EFFECT_APPLIED,CYCLE_APPLIED. Used fromCrossCastingHandler,CrossCastIronsHandler,MixinSpellResolverPreCast, andMixinSpellResolverMana. - New
CrossCastCostResolver. Single source of truth for the cross-cast cost algorithm — captures the mode × multiplier × ring state matrix in one place. Returns aCostBreakdown(primary, secondary, primaryMode, secondaryMode, mode, unified, ringActive, multiplier)record. Three stages:ARS_PRECALC,IRON_PRECALC,IRON_POSTEVENT. For 2.0.0 this is an authoritative calculator — existing cost-mutation sites still own choreography (event mutation, mixin cancels, ring pending-cost stamping); full delegation onto the resolver is tracked for 2.0.1. - New
CapabilityResyncHandler. Single owner of bridge-capability resync acrossPlayerLoggedInEvent,PlayerRespawnEvent, andPlayerChangedDimensionEvent. Replays Affinity (one packet per non-zero school), Cooldown (one packet per active category), and Resonance (when Iron's is loaded). Aura is not duplicated here —AuraCapabilityProvideralready covers all three events from 1.10.0. The previousAffinitySyncOnLoginHandleris retired; its login coverage is subsumed.
Phase 3 — Hardening
- Sealed
SpellDescriptormodel withArsSerializedSpellDescriptorandIronsRegistrySpellDescriptor. Typed adapter between the two upstream spell models with uniformvalidate/serialize/displayName/resolve/systemType/spellId. On-disk NBT shape is unchanged so pre-2.0.0 inscribed items round-trip cleanly viaSpellDescriptor.parse(CompoundTag). Full migration of call sites off rawCompoundTagmaps onto descriptors is tracked for 2.1.0. - New
CastContextvalue record. Threads attempt UUID + player + hand + source stack + descriptor + mode + cost breakdown through the pipeline. Existing sites still pass individual parameters; full migration is tracked for 2.1.0. - GameTest scaffold.
build.gradlegains agameTestServerrun target gated on thears_n_spellsnamespace.CrossCastGameTestsships one sanity scenario confirming the scaffold is wired; the full seven-scenario suite from the audit's "Testing and Validation Strategy" lands in 2.0.1 alongside the structure NBT templates each scenario requires. - CI workflow.
.github/workflows/ci.yml— JDK 17 on Ubuntu, gradle and ForgeGradle caches,./gradlew compileJava testas the required-status job,./gradlew runGameTestServeras an advisory job (continue-on-error true while the GameTest suite is still landing).
Breaking changes
- Packet ID list grew.
CrossCastRequestPacketis appended at the tail ofPacketHandler.register(), so existing packet IDs (Resonance, Affinity, Cooldown, Aura) are unchanged. Clients older than 2.0.0 cannot cross-cast against a 2.0.0 server — they cannot send the new packet. Servers older than 2.0.0 cannot serve 2.0.0 clients — they will reject the new packet ID. Use matching client/server versions. AffinitySyncOnLoginHandlerremoved. Its login-sync responsibility is fully subsumed byCapabilityResyncHandler(which adds respawn + dimension sync). No user-facing behavior change for affinity on login.mana_mode=disablednow permits cross-cast. Previously, setting the mode to disabled (orenable_mana_unification=false) made inscribed items inert. They now cast normally with native upstream pool costs; only mana sharing is suppressed in disabled mode. Modpacks that intentionally wanted cross-cast disabled by settingmana_mode=disabledwill see cross-cast become available again — there is no separateenable_cross_castingtoggle; the feature is now always-on whenever an inscribed item is held.
Known follow-ups (deferred to 2.0.1 / 2.1.0)
- 2.0.1: full site delegation onto
CrossCastCostResolver(current 2.0.0 has the resolver but call sites still own choreography);CrossCastCostResolverTestwith the mode × ring × stage matrix (needs aBridgeManager.testSetModetest-only seam). - 2.0.1: the seven cross-cast GameTest scenarios (clean Ars cast, clean Iron cast, malformed NBT rejection, insufficient resources, dimension transition, separate-mode dual cost, ring + cross-cast) — each needs its own structure NBT template.
- 2.1.0: full migration of
CrossCastingHandler/CrossCastValidatorcall sites off rawCompoundTagmaps ontoSpellDescriptor;CastContextthreaded through the pipeline. - 2.1.0: server/client config split, datapack registries for spell schools / cooldown categories / progression rules / cross-cast rules.
Backward compatibility
- Save format unchanged. AffinityData, ProgressionData, CooldownData, AuraCapability, and
arsnspells:cross_spellsNBT shapes all preserved. Inscribed items from 1.8.9+ load and cast unchanged at 2.0.0. - Mod config unchanged. No keys added, removed, or renamed. The
mana_mode=disabledsemantics change is documented above. - Mixin injection points unchanged. All Ars
SpellResolverinjects keep their@Attargets and method signatures. Mixin-compatible with Ars 4.12.7+; no Iron's-side mixin changes.
Ring of Virtues and Ring of Curses — correctness pass
Investigation found five correctness bugs in the Sanctified Legacy / Covenant of the Seven ring integration. Symptoms ranged from "the rings silently do nothing on a C7-only modpack" to "the player pays both mana and aura on the same spell after unequipping the ring mid-cast." All are fixed in 1.10.0.
hasBothRingsAND-gate fix. SanctifiedLegacyCompat.java:222 returnedfalsewhenever either Covenant of the Seven or Enigmatic Legacy was missing. But C7 ships both rings — meaning a C7-only modpack with both rings equipped silently dropped through every ring path:isWearingCursedRingcancelled out,isWearingVirtueRingcancelled out, ring-conflict notification never fired, and the player paid mana while believing aura was being consumed. The guard is now&&instead of||.hasMatchingBlasphemywas also tightened to useisAvailable()for consistency.- Stale pending-cost can't leak across spells anymore. VirtueRingHandler and CursedRingHandler used to consume the pending aura/LP cost purely on UUID lookup at
SpellResolveEvent.Pre. If another HIGHEST-priority handler cancelled a spell betweenSpellCostCalcEventandSpellResolveEvent.Pre, the pending cost lingered for up to 5 seconds; the next spell — even with the ring unequipped — was charged aura/LP and mana (MixinSpellResolverMana only cancels mana if you're currently wearing the ring). Both handlers now re-verifyisWearingVirtueRing/isWearingCursedRingat every consumption site and drop the stale entry if state changed. - Resolve consumption moved from Pre to Post. Consuming on
SpellResolveEvent.Premeant that if another mod cancelled the cast at Pre after our HIGHEST handler ran, the resource was charged but the spell never executed ("paid but didn't cast"). Aura/LP is now charged onSpellResolveEvent.Post, which only fires for resolved (non-cancelled) spells. Validation continues to happen atcanCast(MixinSpellResolverPreCast) so impossible casts still get blocked cleanly. Pre is kept as a state-drift gate that drops the pending entry if the ring came off between cost-calc and resolve. - Iron's Spellbooks + Virtue Ring path now exists. Previously, casting any Iron's spell while wearing the Virtue Ring drained Iron's mana — only the Cursed Ring had an Iron's handler. Symmetric coverage with the new IronsAuraHandler: pre-cast validation against the aura pool, mana zeroed at
SpellOnCastEvent, aura consumed instead. Insufficient aura cancels the cast with an action-bar message (no death penalty — Virtue Ring failure is intentionally non-punitive). Iron's scrolls get the same treatment via a new aura branch in MixinScrollItem plus ScrollAuraTracker for the HEAD→RETURN commit pattern that ScrollLPTracker already uses. AuraCapabilityno longer corrupts new players. AuraCapability.java used to readAURA_MAX_DEFAULTin its constructor and fall back to 100 onIllegalStateException.AttachCapabilitiesEventcan fire beforeModConfigEvent.Loading, so any player created before config load was permanently capped at 100 aura (10% of the configured default of 1000). The capability now defers initialization until first server-side access (so config is guaranteed loaded), andloadNBTDataruns a one-shot migration: if a savedmaxAura == 100and the configured default is higher, the player is reset to the configured default and anans_v110_migratedmarker is stamped onto the NBT so the heuristic only runs once. Users who legitimately wanted a 100-aura cap should re-apply their config preference after upgrading.
Aura HUD and sync
The mana bar is hidden while the Virtue Ring is worn (since spells cost aura, not mana), but until now nothing replaced it — the player flew blind. 1.10.0 ships the missing pieces:
- New AuraSyncPacket — server→client sync of
(aura, maxAura). Sent on login, dimension change, respawn (post-clone), and whenever the server-side capability marks itself dirty and themana_sync_intervaltick window elapses. - New ClientAuraState — client-side mirror, reset on disconnect.
- New AuraBarController —
RenderGuiOverlayEvent.Postoverlay drawn just above the hotbar in aqua tint when the local player wears the Virtue Ring. Hidden whenmc.options.hideGui, when the ring isn't equipped, or when the aura system is disabled.
Ring swap mid-cast no longer leaks
The previous handler had no signal for Curios slot changes (LivingEquipmentChangeEvent does not fire for Curios), so the per-player curio cache could stay stale for up to a second and a stamped pending cost could be consumed against the wrong wearer state. The cleanup now happens at the consumption site instead: both SpellResolveEvent.Pre and SpellResolveEvent.Post in the ring handlers re-check isWearingVirtueRing / isWearingCursedRing (with the freshness provided by the 20-tick SanctifiedLegacyCompat curio cache) and drop the pending entry when state has changed. A dedicated CurioChangeEvent listener that would cut the "ring just equipped → not active yet" latency from ~1 s to instant is deferred — the event class is not on this mod's transitive compile classpath without an extra compileOnly dependency declaration.
Configuration
- New:
enable_aura_system(defaulttrue) — master toggle mirroringenable_lp_system. When false, the Virtue Ring is ignored and spells use normal mana. Useful for modpacks that want LP but not aura. - Removed:
virtue_ring_discount— the Ring of Virtue stopped being a mana discount in 1.2.0 (it converts mana to aura). The config key has been dead since then; setting it had no effect. Removed entirely in 1.10.0. Existing configs will get an "unknown key" warning on first load and can safely delete the line. - Changed default:
aura_minimum_costis now10(was5) — matchesars_lp_minimum_cost. Existing configs preserve whatever value the user set.
Commands
- New:
/ans aura— non-permissioned subcommand that prints the caller's own aura value. The existing/ans info <player>is unchanged (still op-only).