Ars 'n Spells

Ars ’n Spells is a Forge 1.20.1 (with limited support for 1.21.1 ala Version 2.x.x) compatibility mod that bridges Ars Nouveau and Iron’s Spells ’n Spellbooks by integrating all mana-related systems.

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

Forge

implementation fg.deobf("curse.maven:ars-n-spells-1447914:8163108")
Curse Maven does not yet support mods that have disabled 3rd party sharing

Learn more about Curse Maven

[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 handleCrossCast on both sides, return true on the logical client, and then event.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 new CrossCastRequestPacket instead; the server is the sole authority over cast execution.
  • Cross-cast decoupled from mana unification. The previous BridgeManager.isUnificationEnabled() guard in CrossCastingHandler.onRightClickItem, CrossCastingHandler.onArsSpellCost, and CrossCastIronsHandler.onIronsSpellCast made inscribed items inert whenever mana_mode=disabled or enable_mana_unification=false. The README documents disabled as 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; in disabled mode, 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 new CrossCastingHandler.serverHandleCast entry point.
  • New PacketHandler.sendToServer. Helper for the new C2S direction. CrossCastRequestPacket is 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), Ars ars_spell non-empty, Iron's spell_id parseability + level ≥ 1 + registry resolution. Rejections produce a translation key the player sees via displayClientMessage and a structured descriptor_rejected trace 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-case CrossCastValidatorTest covers every branch.
  • attemptId UUID on CrossCastContext.Entry. Threaded from the packet handler through serverHandleCast, into CrossCastContext.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 on debug_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 from CrossCastingHandler, CrossCastIronsHandler, MixinSpellResolverPreCast, and MixinSpellResolverMana.
  • New CrossCastCostResolver. Single source of truth for the cross-cast cost algorithm — captures the mode × multiplier × ring state matrix in one place. Returns a CostBreakdown(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 across PlayerLoggedInEvent, PlayerRespawnEvent, and PlayerChangedDimensionEvent. 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 — AuraCapabilityProvider already covers all three events from 1.10.0. The previous AffinitySyncOnLoginHandler is retired; its login coverage is subsumed.

Phase 3 — Hardening

  • Sealed SpellDescriptor model with ArsSerializedSpellDescriptor and IronsRegistrySpellDescriptor. Typed adapter between the two upstream spell models with uniform validate/serialize/displayName/resolve/systemType/spellId. On-disk NBT shape is unchanged so pre-2.0.0 inscribed items round-trip cleanly via SpellDescriptor.parse(CompoundTag). Full migration of call sites off raw CompoundTag maps onto descriptors is tracked for 2.1.0.
  • New CastContext value 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.gradle gains a gameTestServer run target gated on the ars_n_spells namespace. CrossCastGameTests ships 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 test as the required-status job, ./gradlew runGameTestServer as an advisory job (continue-on-error true while the GameTest suite is still landing).

Breaking changes

  • Packet ID list grew. CrossCastRequestPacket is appended at the tail of PacketHandler.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.
  • AffinitySyncOnLoginHandler removed. Its login-sync responsibility is fully subsumed by CapabilityResyncHandler (which adds respawn + dimension sync). No user-facing behavior change for affinity on login.
  • mana_mode=disabled now permits cross-cast. Previously, setting the mode to disabled (or enable_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 setting mana_mode=disabled will see cross-cast become available again — there is no separate enable_cross_casting toggle; 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); CrossCastCostResolverTest with the mode × ring × stage matrix (needs a BridgeManager.testSetMode test-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 / CrossCastValidator call sites off raw CompoundTag maps onto SpellDescriptor; CastContext threaded 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_spells NBT 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=disabled semantics change is documented above.
  • Mixin injection points unchanged. All Ars SpellResolver injects keep their @At targets 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.

  • hasBothRings AND-gate fix. SanctifiedLegacyCompat.java:222 returned false whenever 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: isWearingCursedRing cancelled out, isWearingVirtueRing cancelled out, ring-conflict notification never fired, and the player paid mana while believing aura was being consumed. The guard is now && instead of ||. hasMatchingBlasphemy was also tightened to use isAvailable() 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 between SpellCostCalcEvent and SpellResolveEvent.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-verify isWearingVirtueRing / isWearingCursedRing at every consumption site and drop the stale entry if state changed.
  • Resolve consumption moved from Pre to Post. Consuming on SpellResolveEvent.Pre meant 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 on SpellResolveEvent.Post, which only fires for resolved (non-cancelled) spells. Validation continues to happen at canCast (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.
  • AuraCapability no longer corrupts new players. AuraCapability.java used to read AURA_MAX_DEFAULT in its constructor and fall back to 100 on IllegalStateException. AttachCapabilitiesEvent can fire before ModConfigEvent.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), and loadNBTData runs a one-shot migration: if a saved maxAura == 100 and the configured default is higher, the player is reset to the configured default and an ans_v110_migrated marker 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 the mana_sync_interval tick window elapses.
  • New ClientAuraState — client-side mirror, reset on disconnect.
  • New AuraBarControllerRenderGuiOverlayEvent.Post overlay drawn just above the hotbar in aqua tint when the local player wears the Virtue Ring. Hidden when mc.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 (default true) — master toggle mirroring enable_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_cost is now 10 (was 5) — matches ars_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).