Skip to content

Commit c89d578

Browse files
otectusclaude
andcommitted
Deduct Covenant aura via Nature's Aura IAuraChunk; drop misleading message
User report after deploy 58e26a1: casting Ars spells produced "Insufficient Aura: Need 20, have 1000000" (visibly contradictory) and the bar didn't deplete. Diagnosis from latest.log + Covenant 2.2.6 bytecode trace: 1. The misleading message came from VirtueRingHandler.onSpellResolvePost, not from the pre-cast mixin (no DENIED warn in log). The post-resolve handler fires after the spell already cast and shows the message whenever SanctifiedLegacyCompat.consumeCovenantAura returns false. 2. consumeCovenantAura short-circuited to false because covenantConsumeAuraMethod is null. The startup log confirmed: [OK] Covenant aura reflection initialized — consume=false, ... 3. There IS no consume method on Covenant's ModUtils because Covenant's "aura" is not a per-player resource. ResourceSyncEvents.getPlayerAuraChunk reads ambient aura via: IAuraChunk.triangulateAuraInArea(level, playerPos, 35) The value belongs to the surrounding chunks (Nature's Aura's world-aura system), not the player. The canonical way to spend aura is to drain it from the chunks via Nature's Aura's public IAuraChunk API. Covenant's next server tick re-triangulates and ships a CurrentAuraSyncPacket, so the HUD bar moves naturally. Changes: compat/SanctifiedLegacyCompat.java - Add 4 new Method fields for IAuraChunk: getAuraChunk, getHighestSpot, triangulateAuraInArea, drainAura (instance). - New initNaturesAuraReflection() called from init() right after initCovenantAuraReflection(). Logs OK or DEGRADED at boot. - Rewrite consumeCovenantAura(Player, int): * Server-side only (ServerPlayer check). * IAuraChunk.getHighestSpot(level, playerPos, 35, playerPos) finds the highest-aura spot in the same 35-block radius Covenant samples from. * IAuraChunk.getAuraChunk(level, spot) -> chunk.drainAura(spot, cost). * Returns true iff drained > 0. - Augment getCovenantAura(Player) with a server-context fast path (triangulateAuraInArea). Now works on dedicated servers (where ClientResourceData is never populated). - Augment hasEnoughCovenantAura(Player, int) with the same fast path. - Added a tryGetInstance() helper next to the existing tryGetStatic. events/VirtueRingHandler.java - onSpellResolvePost: when consumeCovenantAura returns false, log a single WARN line and return. Do NOT show the player a chat message ("Insufficient Aura: Need X, have Y") — that text is misleading because the spell already cast and the bar IS the canonical visual cue (it won't move, telling the user the deduction failed). - Success-path message ("Consumed X aura (Y remaining)") unchanged. Build verified: ./gradlew build runs reobfJar; deployed jar 283,429 bytes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 58e26a1 commit c89d578

2 files changed

Lines changed: 151 additions & 33 deletions

File tree

src/main/java/com/otectus/arsnspells/compat/SanctifiedLegacyCompat.java

Lines changed: 142 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ private static final class CurioState {
118118
private static java.lang.reflect.Method covenantGetVirtuousFractionMethod = null;
119119
private static boolean covenantAuraReflectionResolved = false;
120120

121+
// --- Nature's Aura bridge ---
122+
// Covenant's "aura" is not a per-player resource — it's a sample of the world's
123+
// ambient aura via IAuraChunk.triangulateAuraInArea(level, playerPos, 35). To
124+
// deduct aura we must drain it from the surrounding chunks via the same public
125+
// Nature's Aura API. drainAura at the highest-aura spot is the canonical pattern
126+
// (Aura Cache, Sky Channeler, every Nature's Aura consumer item does this).
127+
private static java.lang.reflect.Method auraChunkGetAuraChunkMethod = null;
128+
private static java.lang.reflect.Method auraChunkGetHighestSpotMethod = null;
129+
private static java.lang.reflect.Method auraChunkTriangulateMethod = null;
130+
private static java.lang.reflect.Method auraChunkDrainAuraMethod = null;
131+
private static boolean naturesAuraReflectionResolved = false;
132+
121133
/**
122134
* LP Source modes for config.
123135
*/
@@ -153,6 +165,7 @@ public static void init() {
153165

154166
if (isLoaded) {
155167
initCovenantAuraReflection();
168+
initNaturesAuraReflection();
156169
}
157170

158171
// Warn if LP_SOURCE_MODE is BLOOD_MAGIC_ONLY but Blood Magic isn't installed
@@ -288,6 +301,61 @@ private static java.lang.reflect.Method tryGetStatic(Class<?> owner, String name
288301
}
289302
}
290303

304+
/** Look up an instance method by name + parameter types; return null on miss. */
305+
private static java.lang.reflect.Method tryGetInstance(Class<?> owner, String name, Class<?>... params) {
306+
try {
307+
return owner.getMethod(name, params);
308+
} catch (NoSuchMethodException e) {
309+
return null;
310+
}
311+
}
312+
313+
/**
314+
* Resolve Nature's Aura's IAuraChunk API at startup. This is the actual write path
315+
* for "consuming Covenant aura" — Covenant samples ambient aura via
316+
* IAuraChunk.triangulateAuraInArea, so any drain on the surrounding chunks shows up
317+
* in Covenant's bar on the next tick.
318+
*
319+
* <p>Required signatures (Nature's Aura 39.4, verified by javap):
320+
* <pre>
321+
* public static IAuraChunk getAuraChunk(Level, BlockPos)
322+
* public static BlockPos getHighestSpot(Level, BlockPos, int radius, BlockPos defaultPos)
323+
* public static int triangulateAuraInArea(Level, BlockPos, int radius)
324+
* public abstract int drainAura(BlockPos, int amount) // returns amount actually drained
325+
* </pre>
326+
*/
327+
private static void initNaturesAuraReflection() {
328+
try {
329+
Class<?> chunkCls = Class.forName("de.ellpeck.naturesaura.api.aura.chunk.IAuraChunk");
330+
Class<?> levelCls = Class.forName("net.minecraft.world.level.Level");
331+
Class<?> posCls = Class.forName("net.minecraft.core.BlockPos");
332+
333+
auraChunkGetAuraChunkMethod = tryGetStatic(chunkCls, "getAuraChunk", levelCls, posCls);
334+
auraChunkGetHighestSpotMethod = tryGetStatic(chunkCls, "getHighestSpot", levelCls, posCls, int.class, posCls);
335+
auraChunkTriangulateMethod = tryGetStatic(chunkCls, "triangulateAuraInArea", levelCls, posCls, int.class);
336+
auraChunkDrainAuraMethod = tryGetInstance(chunkCls, "drainAura", posCls, int.class);
337+
338+
naturesAuraReflectionResolved = true;
339+
boolean ok = auraChunkGetAuraChunkMethod != null
340+
&& auraChunkGetHighestSpotMethod != null
341+
&& auraChunkTriangulateMethod != null
342+
&& auraChunkDrainAuraMethod != null;
343+
if (ok) {
344+
LOGGER.info(" [OK] Nature's Aura reflection initialized — drain=true, triangulate=true, getHighestSpot=true, getAuraChunk=true");
345+
} else {
346+
LOGGER.error(" [DEGRADED] Nature's Aura reflection partial — drain={}, triangulate={}, getHighestSpot={}, getAuraChunk={}",
347+
auraChunkDrainAuraMethod != null,
348+
auraChunkTriangulateMethod != null,
349+
auraChunkGetHighestSpotMethod != null,
350+
auraChunkGetAuraChunkMethod != null);
351+
LOGGER.error(" [DEGRADED] Ars-spell aura deduction will silently fail; the green HUD bar won't move on Ars casts.");
352+
}
353+
} catch (Throwable t) {
354+
naturesAuraReflectionResolved = false;
355+
LOGGER.error(" [FAIL] Nature's Aura reflection failed (class missing?) — Ars-spell aura deduction disabled", t);
356+
}
357+
}
358+
291359
/**
292360
* Return true if the player has at least {@code cost} Covenant aura.
293361
*
@@ -298,12 +366,25 @@ private static java.lang.reflect.Method tryGetStatic(Class<?> owner, String name
298366
*/
299367
public static boolean hasEnoughCovenantAura(Player player, int cost) {
300368
if (player == null || cost <= 0) return true;
369+
// Server-authoritative path first — IAuraChunk.triangulateAuraInArea returns
370+
// the same number Covenant uses for the bar. Works on both single-player and
371+
// dedicated servers (ClientResourceData isn't populated on dedicated).
372+
try {
373+
if (player instanceof net.minecraft.server.level.ServerPlayer
374+
&& naturesAuraReflectionResolved
375+
&& auraChunkTriangulateMethod != null) {
376+
Object v = auraChunkTriangulateMethod.invoke(null,
377+
player.level(), player.blockPosition(), 35);
378+
if (v instanceof Number) {
379+
return ((Number) v).intValue() >= cost;
380+
}
381+
}
382+
} catch (Throwable t) {
383+
LOGGER.debug("hasEnoughCovenantAura: triangulate failed", t);
384+
}
301385
if (!covenantAuraReflectionResolved) return true; // degraded: allow
302386

303387
try {
304-
// Path 0 (preferred): ClientResourceData.getCurrentAura(). Player arg is unused
305-
// — Covenant tracks current aura per-client, not per-Player. Works for the local
306-
// player (the only one whose Covenant aura the client knows about).
307388
if (clientResourceDataGetCurrentAuraMethod != null) {
308389
Object v = clientResourceDataGetCurrentAuraMethod.invoke(null);
309390
if (v instanceof Number) {
@@ -332,27 +413,57 @@ public static boolean hasEnoughCovenantAura(Player player, int cost) {
332413
}
333414

334415
/**
335-
* Spend {@code cost} aura from the player's Covenant pool. Returns true on
336-
* successful deduction; false if reflection didn't resolve or the call
337-
* returned a "could not pay" signal.
416+
* Spend {@code cost} aura from the world around the player. Returns {@code true}
417+
* iff a positive amount was actually drained.
418+
*
419+
* <p>Covenant has no per-player consume API because its aura value is just a
420+
* sample of the ambient Nature's Aura level in the chunks around the player
421+
* (radius 35, same as Covenant's {@code triangulateAuraInArea} call). To
422+
* "consume aura" we drain it from the highest-aura spot in that area via
423+
* Nature's Aura's public {@code IAuraChunk.drainAura}. Covenant's own
424+
* {@code ResourceSyncEvents.getPlayerAuraChunk} will pick up the change on
425+
* the next server tick and ship a fresh {@code CurrentAuraSyncPacket} to the
426+
* client, which is what makes the HUD bar move.
427+
*
428+
* <p>This is the canonical Nature's Aura consume pattern used by every
429+
* official aura sink in the ecosystem (Aura Cache, Sky Channeler, etc.).
430+
*
431+
* <p>Must be called on the logical server (we touch world state). The
432+
* companion check {@code player instanceof ServerPlayer} short-circuits
433+
* otherwise — calling from a non-server context returns false silently.
338434
*/
339435
public static boolean consumeCovenantAura(Player player, int cost) {
340436
if (player == null || cost <= 0) return false;
341-
if (!covenantAuraReflectionResolved || covenantConsumeAuraMethod == null) return false;
437+
if (!(player instanceof net.minecraft.server.level.ServerPlayer)) return false;
438+
if (!naturesAuraReflectionResolved
439+
|| auraChunkGetAuraChunkMethod == null
440+
|| auraChunkGetHighestSpotMethod == null
441+
|| auraChunkDrainAuraMethod == null) {
442+
return false;
443+
}
342444
try {
343-
Object result = covenantConsumeAuraMethod.invoke(null, player, cost);
344-
if (result instanceof Boolean) {
345-
return (Boolean) result;
346-
}
347-
if (result instanceof Number) {
348-
return ((Number) result).intValue() >= cost;
445+
net.minecraft.world.level.Level level = player.level();
446+
net.minecraft.core.BlockPos playerPos = player.blockPosition();
447+
// 35 matches Covenant's triangulation radius — anywhere inside this area is
448+
// guaranteed to affect the next ambient-aura sample.
449+
Object highest = auraChunkGetHighestSpotMethod.invoke(null, level, playerPos, 35, playerPos);
450+
if (!(highest instanceof net.minecraft.core.BlockPos)) return false;
451+
net.minecraft.core.BlockPos spot = (net.minecraft.core.BlockPos) highest;
452+
Object chunkObj = auraChunkGetAuraChunkMethod.invoke(null, level, spot);
453+
if (chunkObj == null) return false;
454+
Object drainedObj = auraChunkDrainAuraMethod.invoke(chunkObj, spot, cost);
455+
if (drainedObj instanceof Number) {
456+
int drained = ((Number) drainedObj).intValue();
457+
if (drained > 0) {
458+
LOGGER.debug("Drained {} aura from chunk spot {} for {} (requested {})",
459+
drained, spot, player.getName().getString(), cost);
460+
return true;
461+
}
349462
}
350-
// void return type: assume success
351-
return true;
352463
} catch (Throwable t) {
353-
LOGGER.error("consumeCovenantAura reflection failed", t);
354-
return false;
464+
LOGGER.error("consumeCovenantAura: IAuraChunk drain failed", t);
355465
}
466+
return false;
356467
}
357468

358469
/**
@@ -361,11 +472,21 @@ public static boolean consumeCovenantAura(Player player, int cost) {
361472
* available either.
362473
*/
363474
public static int getCovenantAura(Player player) {
364-
if (!covenantAuraReflectionResolved) return 0;
365475
try {
366-
// Path 0 (preferred): ClientResourceData.getCurrentAura(). Static, no Player
367-
// arg — see the comment in hasEnoughCovenantAura. Returns the local-client
368-
// aura value, which is exactly what we need for HUD/peak tracking.
476+
// Server-authoritative path: ask Nature's Aura directly for the ambient aura
477+
// around the player. This is exactly what Covenant samples in its server-tick
478+
// sync handler, so it's the truth — and it works on dedicated servers where
479+
// ClientResourceData is never populated.
480+
if (player instanceof net.minecraft.server.level.ServerPlayer
481+
&& naturesAuraReflectionResolved
482+
&& auraChunkTriangulateMethod != null) {
483+
Object v = auraChunkTriangulateMethod.invoke(null,
484+
player.level(), player.blockPosition(), 35);
485+
if (v instanceof Number) return ((Number) v).intValue();
486+
}
487+
if (!covenantAuraReflectionResolved) return 0;
488+
// Client-context path: ClientResourceData.getCurrentAura() — what Covenant's
489+
// own HUD reads from. Player arg is unused (it's a per-client singleton).
369490
if (clientResourceDataGetCurrentAuraMethod != null) {
370491
Object v = clientResourceDataGetCurrentAuraMethod.invoke(null);
371492
if (v instanceof Number) return ((Number) v).intValue();

src/main/java/com/otectus/arsnspells/events/VirtueRingHandler.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -214,18 +214,15 @@ public static void onSpellResolvePost(SpellResolveEvent.Post event) {
214214

215215
boolean success = SanctifiedLegacyCompat.consumeCovenantAura(player, pending.auraCost);
216216
if (!success) {
217-
// Should be rare — the canCast pre-validation in MixinSpellResolverPreCast already
218-
// gated this. Log and skip. Note: in "degraded mode" (Covenant reflection unresolved),
219-
// consumeCovenantAura intentionally returns false; this is logged at startup, not here.
220-
LOGGER.debug("Aura consumption failed at Post for {} (spell already cast, no payment made)",
221-
player.getName().getString());
222-
223-
int currentAura = SanctifiedLegacyCompat.getCovenantAura(player);
224-
player.displayClientMessage(
225-
Component.translatable("message.ars_n_spells.aura.insufficient", pending.auraCost, currentAura)
226-
.withStyle(ChatFormatting.AQUA),
227-
true
228-
);
217+
// Either ambient aura was genuinely insufficient OR Nature's Aura reflection
218+
// failed to resolve at startup (degraded mode — already logged at boot). In
219+
// both cases the spell has already cast and we don't surface a chat message
220+
// because the bar IS the canonical visual cue: it either didn't move (no
221+
// aura was deducted) or moved by less than requested (partial drain). A
222+
// post-cast "Insufficient Aura" chat message would be misleading because
223+
// the player CAN see the bar value and the spell DID succeed.
224+
LOGGER.warn("Aura consumption returned false for {} (cost={}); bar will not reflect this cast",
225+
player.getName().getString(), pending.auraCost);
229226
return;
230227
}
231228

0 commit comments

Comments
 (0)