diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java index 96074321fd2e..e495ee341e46 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusAdapter.java @@ -6,11 +6,23 @@ import gg.xp.reevent.scan.FeedHelperAdapter; import gg.xp.reevent.scan.ScanMe; import gg.xp.xivsupport.callouts.ModifiableCallout; +import gg.xp.xivsupport.callouts.RawModifiedCallout; import gg.xp.xivsupport.events.actlines.events.BuffApplied; +import gg.xp.xivsupport.events.state.combatstate.CastTracker; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectCurrentStatus; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; + +import java.util.Optional; @ScanMe public class PlayerStatusAdapter implements FeedHelperAdapter> { + private final StatusEffectRepository buffs; + + public PlayerStatusAdapter(StatusEffectRepository buffs) { + this.buffs = buffs; + } + @Override public Class eventType() { return BuffApplied.class; @@ -19,6 +31,7 @@ public Class eventType() { @Override public TypedEventHandler makeHandler(FeedHandlerChildInfo> info) { long[] castIds = info.getAnnotation().value(); + PlayerStatusCallout ann = info.getAnnotation(); return new TypedEventHandler<>() { @Override public Class getType() { @@ -28,7 +41,14 @@ public Class getType() { @Override public void handle(EventContext context, BuffApplied event) { if (event.getTarget().isThePlayer() && event.buffIdMatches(castIds)) { - context.accept(info.getHandlerFieldValue().getModified(event)); + RawModifiedCallout modified = info.getHandlerFieldValue().getModified(event); + if (ann.cancellable()) { + modified.addExpiryCondition(() -> { + StatusEffectCurrentStatus cs = buffs.statusOf(event); + return cs != StatusEffectCurrentStatus.ACTIVE; + }); + } + context.accept(modified); } } }; diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java index 99a073ea4e88..2e820e8d93a9 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/PlayerStatusCallout.java @@ -15,4 +15,11 @@ * @return Which status IDs to trigger on */ long[] value(); + + /** + * @return Whether the callout should be removed if the buff is removed. A buff being replaced/refreshed + * counts as being removed. + */ + boolean cancellable() default false; + } diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java new file mode 100644 index 000000000000..963e5d9a2e0d --- /dev/null +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCar.java @@ -0,0 +1,444 @@ +package gg.xp.xivsupport.triggers.car; + +import gg.xp.reevent.events.BaseEvent; +import gg.xp.reevent.events.EventContext; +import gg.xp.reevent.scan.AutoChildEventHandler; +import gg.xp.reevent.scan.AutoFeed; +import gg.xp.reevent.scan.FilteredEventHandler; +import gg.xp.reevent.scan.HandleEvents; +import gg.xp.xivdata.data.duties.*; +import gg.xp.xivsupport.callouts.CalloutRepo; +import gg.xp.xivsupport.callouts.ModifiableCallout; +import gg.xp.xivsupport.callouts.RawModifiedCallout; +import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; +import gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent; +import gg.xp.xivsupport.events.actlines.events.BuffApplied; +import gg.xp.xivsupport.events.actlines.events.HeadMarkerEvent; +import gg.xp.xivsupport.events.misc.pulls.PullStartedEvent; +import gg.xp.xivsupport.events.state.XivState; +import gg.xp.xivsupport.events.state.combatstate.ActiveCastRepository; +import gg.xp.xivsupport.events.state.combatstate.CastTracker; +import gg.xp.xivsupport.events.triggers.seq.SequentialTrigger; +import gg.xp.xivsupport.events.triggers.seq.SqtTemplates; +import gg.xp.xivsupport.events.triggers.support.NpcCastCallout; +import gg.xp.xivsupport.events.triggers.support.PlayerHeadmarker; +import gg.xp.xivsupport.events.triggers.support.PlayerStatusCallout; +import gg.xp.xivsupport.models.ArenaSector; +import gg.xp.xivsupport.models.XivCombatant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.List; + +@CalloutRepo(name = "CoD (CAR) Triggers", duty = KnownDuty.CodCar) +public class CodCar extends AutoChildEventHandler implements FilteredEventHandler { + + private static final Logger log = LoggerFactory.getLogger(CodCar.class); + private final XivState state; + private ActiveCastRepository casts; + + public CodCar(XivState state, ActiveCastRepository casts) { + this.state = state; + this.casts = casts; + } + + private CodCarSection mySection; + + @HandleEvents + public void reset(PullStartedEvent pse) { + mySection = null; + } + + @HandleEvents + public void innerOuterDarkness(BuffApplied ba) { + if (!ba.getTarget().isThePlayer()) { + return; + } + if (ba.buffIdMatches(0x1051)) { + mySection = CodCarSection.INSIDE; + log.info("My area: INSIDE"); + } + else if (ba.buffIdMatches(0x1052)) { + mySection = CodCarSection.forPos(ba.getTarget().getPos()); + log.info("My area: {}", mySection); + } + } + + @Override + public boolean enabled(EventContext context) { + return state.dutyIs(KnownDuty.CodCar); + } + + // Intro + @NpcCastCallout(0x9DFC) + private final ModifiableCallout bladeOfDarknessRight = ModifiableCallout.durationBasedCall("Blade of Darkness: Right Safe", "Right"); + @NpcCastCallout(0x9DFE) + private final ModifiableCallout bladeOfDarknessLeft = ModifiableCallout.durationBasedCall("Blade of Darkness: Left Safe", "Left"); + @NpcCastCallout(0x9E00) + private final ModifiableCallout bladeOfDarknessOut = ModifiableCallout.durationBasedCall("Blade of Darkness: Out", "Out"); + + // P1 + + private final ModifiableCallout grimEmbraceFrontInitial = new ModifiableCallout("Grim Embrace: Initial Front Dodge", "Later: Dodge Forwards").autoIcon(); + private final ModifiableCallout grimEmbraceRearInitial = new ModifiableCallout("Grim Embrace: Initial Rear Dodge", "Later: Dodge Backwards").autoIcon(); + private final ModifiableCallout grimEmbraceFrontSoon = new ModifiableCallout("Grim Embrace: Front Dodge Soon", "Dodge Forwards").autoIcon(); + private final ModifiableCallout grimEmbraceRearSoon = new ModifiableCallout("Grim Embrace: Rear Dodge Soon", "Dodge Backwards").autoIcon(); + private final ModifiableCallout grimEmbraceFrontMove = new ModifiableCallout<>("Grim Embrace: Rear Dodge Now", "Move"); + private final ModifiableCallout grimEmbraceRearMove = new ModifiableCallout<>("Grim Embrace: Rear Dodge Now", "Move"); + + @AutoFeed + private final SequentialTrigger grimEmbrace = SqtTemplates.sq(180_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E39, 0x9E3A), + (e1, s) -> { + boolean isFront = e1.abilityIdMatches(0x9E3A); + List buffs = s.waitEventsQuickSuccession(12, BuffApplied.class, ba -> ba.buffIdMatches(0x1055)); + buffs.stream().filter(ba -> ba.getTarget().isThePlayer()) + .findAny() + .ifPresent(b -> { + s.updateCall(isFront ? grimEmbraceFrontInitial : grimEmbraceRearInitial, b); + s.waitDuration(b.remainingDurationPlus(Duration.ofSeconds(-5))); + s.updateCall(isFront ? grimEmbraceFrontSoon : grimEmbraceRearSoon, b); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E3C)); + s.updateCall(isFront ? grimEmbraceFrontMove : grimEmbraceRearMove); + }); + }); + + // Real death IV: + // Boss casts 0x9E43 (5.3s, no aoe) BEFORE the others + // Ball of Naught casts 0x9E46 (5.7s, donut) + // Fake boss casts 0x9E45 (3.7s, circle) + // Fake boss casts 0x9E44 (vortex, 1.7s, pull-in) + // KB seems to be scripted? + // Endeath IV: + // Boss casts 9E53 beforehand, giving the 0x1056 status + // Ball casts 9E49 (4.7s, donut) + // Fake boss casts 9E48 (2.7s, circle) + // Fake boss casts 9E47 (vortex, 0.7s, pull-in) + + @NpcCastCallout(0x9E4C) + private final ModifiableCallout aeroIV = new ModifiableCallout<>("Aero IV", "Knockback"); + + private final ModifiableCallout deathIV = new ModifiableCallout<>("Death IV", "Out of Middle"); + private final ModifiableCallout deathIVin = ModifiableCallout.durationBasedCall("Death IV: Inside", "In"); + + @AutoFeed + private final SequentialTrigger deathIVsq = SqtTemplates.sq(15_000, + // Start on the real cast + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E43), + (e1, s) -> { + s.updateCall(deathIV); + // Wait for inner circle to pop + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9E45)); + // Use the donut cast as the time basis + var e2 = s.findOrWaitForCast(casts, acs -> acs.abilityIdMatches(0x9E46), false); + s.updateCall(deathIVin, e2); + }); + + private static final long[] ALL_BLADE_IDS = {0x9DFC, 0x9DFE, 0x9E00}; + + private final ModifiableCallout enaeroInitial = ModifiableCallout.durationBasedCall("Enaero: Initial", "Stocking Aero"); + private final ModifiableCallout endeathInitial = ModifiableCallout.durationBasedCall("Endeath: Initial", "Stocking Death"); + + @AutoFeed + private final SequentialTrigger enaeroSq = SqtTemplates.sq(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E54), + (e1, s) -> { + s.updateCall(enaeroInitial, e1); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(ALL_BLADE_IDS)); + // Don't talk over the other call + s.waitMs(2_000); + s.updateCall(aeroIV); + }); + + @AutoFeed + private final SequentialTrigger endeathSq = SqtTemplates.sq(120_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E53), + (e1, s) -> { + s.updateCall(endeathInitial, e1); + s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(ALL_BLADE_IDS)); + // Don't talk over the other call + s.waitMs(2_000); + s.updateCall(deathIV); + // Wait for inner circle to pop + s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9E48)); + // Use the donut cast as the time basis + var e2 = s.findOrWaitForCast(casts, acs -> acs.abilityIdMatches(0x9E49), false); + s.updateCall(deathIVin, e2); + }); + + @NpcCastCallout(0x9E3E) + private final ModifiableCallout floodOfDarkness = ModifiableCallout.durationBasedCall("Flood of Darkness", "Raidwide"); + + @NpcCastCallout(0x9E0D) + private final ModifiableCallout wildCharge = ModifiableCallout.durationBasedCall("Wild Charge", "Stacks, Tanks in Front"); + + private final ModifiableCallout flare = ModifiableCallout.durationBasedCallWithOffset("Flare", "Flare", Duration.ofMillis(9_300)); + private final ModifiableCallout flareOnYou = ModifiableCallout.durationBasedCallWithOffset("Flare on You", "Out", Duration.ofMillis(9_300)); + private final ModifiableCallout flareNotOnYou = ModifiableCallout.durationBasedCallWithOffset("Flare on You", "Middle", Duration.ofMillis(9_300)); + + @AutoFeed + private final SequentialTrigger flareSq = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E58), + (e1, s) -> { + s.updateCall(flare, e1); + // This fight does not have consistent marker offsets + s.waitCastFinished(casts, e1); + List headmarkers = s.waitEventsQuickSuccession(3, HeadMarkerEvent.class, hme -> true); + headmarkers.stream().filter(hm -> hm.getTarget().isThePlayer()).findAny() + .ifPresentOrElse(h -> s.updateCall(flareOnYou, e1), + () -> s.updateCall(flareNotOnYou, e1)); + }); + + @NpcCastCallout(0xA12D) + private final ModifiableCallout unholyDarkness = ModifiableCallout.durationBasedCallWithOffset("Unholy Darkness", "Light Party Stacks", Duration.ofMillis(9_100)); + @NpcCastCallout(0x9E40) + private final ModifiableCallout rapidSequence = ModifiableCallout.durationBasedCall("Rapid-sequence Particule Beam", "Wild Charges"); + + @NpcCastCallout(0x9E50) + private final ModifiableCallout breakIV = ModifiableCallout.durationBasedCall("Break IV", "Look Away"); + + @NpcCastCallout(value = 0x9E3F, suppressMs = 30_000) + private final ModifiableCallout razingVolley = ModifiableCallout.durationBasedCall("Razing Volley", "Dodge Lasers"); + + + // P2 + + @NpcCastCallout(0x9E10) + private final ModifiableCallout diffusive = ModifiableCallout.durationBasedCall("Diffusive-force Particle Beam", "Spread"); + + @NpcCastCallout(0x9E0B) + private final ModifiableCallout ghastlyGloomDonut = ModifiableCallout.durationBasedCall("Ghastly Gloom (Huge Donut)", "In"); + + @NpcCastCallout(0x9E09) + private final ModifiableCallout ghastlyGloomCross = ModifiableCallout.durationBasedCall("Ghastly Gloom (Cross)", "Corners"); + + private final ModifiableCallout curseOfDarkness = ModifiableCallout.durationBasedCall("Curse of Darkness", "Look Out"); + + @NpcCastCallout({0x9E01, 0x9E3D}) + private final ModifiableCallout deluge = ModifiableCallout.durationBasedCall("Deluge of Darkness", "Raidwide with Bleed"); + + + @AutoFeed + private final SequentialTrigger curseOfDarknessSq = SqtTemplates.callWhenDurationIs( + BuffApplied.class, + ba -> ba.buffIdMatches(0x953) && ba.getTarget().isThePlayer(), + curseOfDarkness, + Duration.ofSeconds(3)); + + @NpcCastCallout(value = 0xA2C9, suppressMs = 1000) + private final ModifiableCallout loomingChaos = ModifiableCallout.durationBasedCall("Looming Chaos", "Prepare for Swaps"); + + @NpcCastCallout(0x9E08) + private final ModifiableCallout darkDominion = ModifiableCallout.durationBasedCall("Dark Dominion", "Raidwide"); + + + // TODO: wild charge + + /* + * Third Art of Darkness (cleaves, buddies/spread) + * Two initial casts: 9E20 and 9E23 + * Headmarkers happen during the cast + * + * Example 1: + * 9E20 -> F0 F1 F0, east boss, facing east + * North Buddies North + * + * Example 2: + * 9E23 -> EF F1 EF, west boss, facing west + * North Buddies North + * + * Example 3: + * 9E20 -> F0 F1 EF, west boss, facing west + * South Buddies North + * + * Example 4: + * 9E23 -> EF F0 F1, east boss, facing east + * South North Buddies + * + * Exampel 5: + * 9E23 -> EF F0 F2, east boss, facing east + * South North Spread + * + * Seems to be: + * EF: Cleaving left + * F0: Cleaving right + * + */ + + private enum ArtOfDarknessMech { + CLEAVE_LEFT, + CLEAVE_RIGHT, + BUDDIES, + PROTEANS; + + private static ArtOfDarknessMech forHm(HeadMarkerEvent hme) { + return switch ((int) hme.getMarkerId()) { + case 0xEF -> CLEAVE_LEFT; + case 0xF0 -> CLEAVE_RIGHT; + case 0xF1 -> BUDDIES; + case 0xF2 -> PROTEANS; + default -> null; + }; + } + } + + private final ModifiableCallout artOfDarknessWestCleaveLeft = new ModifiableCallout<>("North"); + private final ModifiableCallout artOfDarknessWestCleaveRight = new ModifiableCallout<>("South"); + private final ModifiableCallout artOfDarknessEastCleaveLeft = new ModifiableCallout<>("South"); + private final ModifiableCallout artOfDarknessEastCleaveRight = new ModifiableCallout<>("North"); + private final ModifiableCallout artOfDarknessBuddies = new ModifiableCallout<>("Buddies"); + private final ModifiableCallout artOfDarknessProteans = new ModifiableCallout<>("Proteans"); + + private ModifiableCallout calloutForMech(ArtOfDarknessMech artOfDarknessMech, boolean east) { + return switch (artOfDarknessMech) { + case CLEAVE_LEFT -> east ? artOfDarknessEastCleaveLeft : artOfDarknessWestCleaveLeft; + case CLEAVE_RIGHT -> east ? artOfDarknessEastCleaveRight : artOfDarknessWestCleaveRight; + case BUDDIES -> artOfDarknessBuddies; + case PROTEANS -> artOfDarknessProteans; + }; + } + + private static final long[] ALL_ART_IDS = {0x9E22, 0x9E25, 0x9E26, 0x9E28}; + + private boolean doWeCareAboutStygian(XivCombatant combatant) { + // Check that this one is the one that the player is near + // TODO: what if someone is walking on outer ring? + boolean isEast = combatant.getPos().x() > 0; + if (isEast) { + return mySection == CodCarSection.EAST_OUTSIDE; + } + else { + return mySection == CodCarSection.WEST_OUTSIDE; + } + + } + + @AutoFeed + private final SequentialTrigger sq = SqtTemplates.sq(30_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E20, 0x9E23), + (e1, s) -> { + XivCombatant npc = e1.getSource(); + boolean isEast = npc.getPos().x() > 0; + if (!doWeCareAboutStygian(npc)) { + return; + } + + var mech1 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc1 = calloutForMech(mech1, isEast); + var call1 = s.call(mc1); + + var mech2 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc2 = calloutForMech(mech2, isEast); + var call2 = s.call(mc2); + + var mech3 = ArtOfDarknessMech.forHm(s.waitEvent(HeadMarkerEvent.class, hme -> hme.getTarget().equals(npc))); + ModifiableCallout mc3 = calloutForMech(mech3, isEast); + var call3 = s.call(mc3); + + // Wait so we don't talk over the call + s.waitMs(1_500); + + RawModifiedCallout mc1a = s.call(mc1); + mc1a.setReplaces(call1); + + s.waitEventsQuickSuccession(2, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(ALL_ART_IDS)); + s.waitMs(800); + + mc1a.forceExpire(); + RawModifiedCallout mc2a = s.call(mc2); + mc2a.setReplaces(call2); + + s.waitEventsQuickSuccession(2, AbilityUsedEvent.class, aue -> aue.abilityIdMatches(ALL_ART_IDS)); + s.waitMs(800); + + mc2a.forceExpire(); + RawModifiedCallout mc3a = s.call(mc3); + mc3a.setReplaces(call3); + + // TODO: tower call? + + }); + + private final ModifiableCallout evilSeedOnYou = new ModifiableCallout<>("Evil Seed: On You", "Drop Bramble"); + private final ModifiableCallout evilSeedNotOnYou = new ModifiableCallout<>("Evil Seed: Not On You", "Avoid Brambles"); + + @AutoFeed + private final SequentialTrigger evilSeed = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E2A), + (e1, s) -> { + s.waitEventsQuickSuccession(8, HeadMarkerEvent.class, hme -> hme.getMarkerId() == 0x227) + .stream() + .filter(hme -> hme.getTarget().isThePlayer()) + .findAny() + .ifPresentOrElse(h -> { + s.updateCall(evilSeedOnYou, h); + }, () -> s.updateCall(evilSeedNotOnYou)); + }); + + @PlayerStatusCallout(value = 0x1BD, cancellable = true) + private final ModifiableCallout thornyVine = ModifiableCallout.durationBasedCall("Thorny Vine", "Break Tether"); + + private final ModifiableCallout lateralCore = ModifiableCallout.durationBasedCall("Lateral-Core Phaser", "Sides then In"); + private final ModifiableCallout coreLateral = ModifiableCallout.durationBasedCall("Core-Lateral Phaser", "In then Sides"); + private final ModifiableCallout coreWithTower = new ModifiableCallout<>("Lateral-Core: Follow-Up with Tower", "In then Tower"); + private final ModifiableCallout lateralWithTower = new ModifiableCallout<>("Core-Lateral: Follow-Up with Tower", "Out and Tower"); + private final ModifiableCallout coreWithPivot = new ModifiableCallout<>("Lateral-Core: Follow-Up with Pivot", "In then {rotationSafe}"); + private final ModifiableCallout lateralWithPivot = new ModifiableCallout<>("Core-Lateral: Follow-Up with Pivot", "Out and {rotationSafe}"); + + @AutoFeed + private final SequentialTrigger phaser = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E2F, 0x9E30), + (e1, s) -> { + if (!doWeCareAboutStygian(e1.getSource())) { + return; + } + boolean sidesFirst = e1.abilityIdMatches(0x9E2F); + AbilityCastStart pivotCast = casts.getActiveCastById(0x9E13, 0x9E15).stream().map(CastTracker::getCast).findFirst() + .orElse(null); + if (pivotCast == null) { + s.updateCall(sidesFirst ? lateralCore : coreLateral, e1); + s.waitCastFinished(casts, e1); + s.updateCall(sidesFirst ? coreWithTower : lateralWithTower); + } + else { + boolean cw = pivotCast.abilityIdMatches(0x9E13); + if (mySection == CodCarSection.EAST_OUTSIDE) { + s.setParam("rotationSafe", cw ? ArenaSector.NORTHEAST : ArenaSector.SOUTHEAST); + } + else if (mySection == CodCarSection.WEST_OUTSIDE) { + s.setParam("rotationSafe", cw ? ArenaSector.SOUTHWEST : ArenaSector.NORTHWEST); + } + else { + throw new IllegalStateException("How?"); + } + s.updateCall(sidesFirst ? lateralCore : coreLateral, e1); + s.waitCastFinished(casts, e1); + s.updateCall(sidesFirst ? coreWithPivot : lateralWithPivot); + } + }); + + private final ModifiableCallout pivotCW = ModifiableCallout.durationBasedCall("Pivot: Clockwise", "Clockwise"); + private final ModifiableCallout pivotCCW = ModifiableCallout.durationBasedCall("Pivot: Counter-Clockwise", "Counter-Clockwise"); + + @AutoFeed + private final SequentialTrigger pivot = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E13, 0x9E15), + (e1, s) -> { + if (mySection == CodCarSection.INSIDE) { + s.updateCall(e1.abilityIdMatches(0x9E13) ? pivotCW : pivotCCW); + } + }); + + private final ModifiableCallout excruciate = ModifiableCallout.durationBasedCall("Excruciate on You", "Tank Buster"); + + @AutoFeed + private final SequentialTrigger excuciateSq = SqtTemplates.sq(10_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9E36) && acs.getTarget().isThePlayer(), + (e1, s) -> { + s.updateCall(excruciate, e1); + }); + + @PlayerHeadmarker(0xC5) + private final ModifiableCallout chaser = new ModifiableCallout<>("Feint Particle Beam", "Chasing AoE"); +} diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java new file mode 100644 index 000000000000..b6f06ff6e876 --- /dev/null +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/car/CodCarSection.java @@ -0,0 +1,27 @@ +package gg.xp.xivsupport.triggers.car; + +import gg.xp.xivsupport.models.ArenaPos; +import gg.xp.xivsupport.models.ArenaSector; +import gg.xp.xivsupport.models.Position; + +public enum CodCarSection { + INSIDE, + WEST_OUTSIDE, + EAST_OUTSIDE; + + private static final ArenaPos ap = new ArenaPos(100, 100, 18.5, 10); + + public static CodCarSection forPos(Position pos) { + double dist = pos.distanceFrom2D(Position.of2d(100, 100)); + // Outer ring + if (dist > 33) { + return pos.x() < 100 ? WEST_OUTSIDE : EAST_OUTSIDE; + } + ArenaSector sect = ap.forPosition(pos); + return switch (sect) { + case WEST -> WEST_OUTSIDE; + case EAST -> EAST_OUTSIDE; + default -> INSIDE; + }; + } +} diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java b/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java index 8f63612424dd..fee5d0ee17ee 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/XivMap.java @@ -51,7 +51,7 @@ public XivMap(int offsetX, int offsetY, int scaleFactor, @Nullable String filena if (parts.length == 2) { String stub = parts[0]; String index = parts[1]; - String urlStr = String.format("https://xivapi.com/m/%s/%s.%s.jpg", stub, stub, index); + String urlStr = String.format("https://beta.xivapi.com/api/1/asset/map/%s/%s", stub, index); URL url; try { url = new URL(urlStr); diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java b/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java index 7fad9ac83b64..1396f4a5e231 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/duties/DutyType.java @@ -11,7 +11,8 @@ public enum DutyType { HUNT("Hunt"), SOLO_INSTANCE("Solo Instance"), OPEN_WORLD("Eureka-like"), - ALLIANCE_RAID("Alliance Raid"); + ALLIANCE_RAID("Alliance Raid"), + CAR("Chaotic AR"); private final String name; diff --git a/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java b/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java index f5621e22b067..43bafec8ee36 100644 --- a/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java +++ b/xivdata/src/main/java/gg/xp/xivdata/data/duties/KnownDuty.java @@ -67,6 +67,7 @@ public enum KnownDuty implements Duty { M4S("M4S", 1232,Expansion.DT, DutyType.SAVAGE_RAID), FRU("FRU", 1238, Expansion.DT, DutyType.ULTIMATE), + CodCar("CoD CAR", 1241, Expansion.DT, DutyType.CAR), ; private final String name; diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java index 603176c38260..1d8636596ff1 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/FieldMapper.java @@ -310,4 +310,5 @@ public List getCombatantsToUpdate() { public void flushStateOverrides() { state.flushProvidedValues(); } + } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java index fb5fab163454..672f4c62d2ca 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line261Parser.java @@ -81,7 +81,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim // of them changes does not work on an 'add' line, and the 'add' line will not write 'default' // values. So if there is a Z or heading of 0 (very likely), those will not be written. // To work around this, we just assume those are 0. - state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.getOrDefault(PosKeys.PosZ, 0.0), pos.getOrDefault(PosKeys.Heading, 0.0))); + state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.getOrDefault(PosKeys.PosZ, 0.0), pos.getOrDefault(PosKeys.Heading, 0.0)), true); } else { log.trace("Incomplete position info for 0x{}", Long.toString(existing.getId(), 16)); @@ -89,7 +89,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim } } else { - state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.get(PosKeys.PosZ), pos.get(PosKeys.Heading))); + state.provideCombatantPos(existing, new Position(pos.get(PosKeys.PosX), pos.get(PosKeys.PosY), pos.get(PosKeys.PosZ), pos.get(PosKeys.Heading)), true); } } else { @@ -98,7 +98,7 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim pos.getOrDefault(PosKeys.PosY, existingPos.y()), pos.getOrDefault(PosKeys.PosZ, existingPos.z()), pos.getOrDefault(PosKeys.Heading, existingPos.heading()) - )); + ), true); } } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java index e0b8f7d87d82..3ec1641a7abe 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line264Parser.java @@ -71,7 +71,6 @@ protected Event convert(FieldMapper fields, int lineNumber, ZonedDateTim // Animation Target Id // TODO: backwards compat? make sure this doesn't catch the hash XivCombatant animationTarget = fields.getOptionalEntity(Fields.animationTargetId); - LoggerFactory.getLogger(Line264Parser.class).info("animation target: {}", animationTarget); // Cast location/angle // First, check if the line indicates that such data is actually present. diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java index b78d32036891..ae277dbef0f4 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/actlines/parsers/Line271Parser.java @@ -32,7 +32,7 @@ enum Fields { fields.getDouble(Fields.x), fields.getDouble(Fields.y), fields.getDouble(Fields.z), - fields.getDouble(Fields.heading))); + fields.getDouble(Fields.heading)), true); return null; } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java index bf3b46dbdfaa..70020d458943 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivState.java @@ -72,7 +72,11 @@ default boolean dutyIs(Duty duty) { void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoints); - void provideCombatantPos(XivCombatant target, Position newPos); + default void provideCombatantPos(XivCombatant target, Position newPos) { + provideCombatantPos(target, newPos, false); + } + + void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted); void provideActFallbackCombatant(XivCombatant cbt); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java index b99edea411e7..2a0a5da706a2 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateDummy.java @@ -116,7 +116,7 @@ public void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoin } @Override - public void provideCombatantPos(XivCombatant target, Position newPos) { + public void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted) { throw new UnsupportedOperationException("not supported"); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java index 8c3c1fdb2dba..f6a52fa7c794 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/XivStateImpl.java @@ -444,8 +444,8 @@ public void provideCombatantMP(XivCombatant target, @NotNull ManaPoints manaPoin } @Override - public void provideCombatantPos(XivCombatant target, Position newPos) { - getOrCreateData(target.getId()).setPosOverride(newPos); + public void provideCombatantPos(XivCombatant target, Position newPos, boolean trusted) { + getOrCreateData(target.getId()).setPosOverride(newPos, trusted); dirtyOverrides = true; } @@ -628,6 +628,7 @@ private final class CombatantData { private final long id; private @Nullable RawXivCombatantInfo raw; private @Nullable Position posOverride; + private @Nullable Position posOverrideTrusted; private @Nullable HitPoints hpOverride; private @Nullable ManaPoints mpOverride; private @Nullable XivCombatant fromOtherActLine; @@ -684,10 +685,18 @@ public void setFromPartyInfo(RawXivPartyInfo fromPartyInfo) { dirty = true; } - public void setPosOverride(@Nullable Position posOverride) { - if (!Objects.equals(this.posOverride, posOverride)) { - this.posOverride = posOverride; - dirty = true; + public void setPosOverride(@Nullable Position posOverride, boolean trusted) { + if (trusted) { + if (!Objects.equals(this.posOverrideTrusted, posOverride)) { + this.posOverrideTrusted = posOverride; + dirty = true; + } + } + else { + if (!Objects.equals(this.posOverride, posOverride)) { + this.posOverride = posOverride; + dirty = true; + } } } @@ -784,7 +793,8 @@ private synchronized void recompute() { // HP prefers trusted ACT hp lines HitPoints hp = hpOverride != null ? hpOverride : raw != null ? raw.getHP() : null; ManaPoints mp = mpOverride != null ? mpOverride : raw != null ? raw.getMP() : null; - Position pos = posOverride != null ? posOverride : raw != null ? raw.getPos() : fromOther != null ? fromOther.getPos() : null; + // TODO: should there be a way to "un-trust" these if they get stale? + Position pos = posOverrideTrusted != null ? posOverrideTrusted : posOverride != null ? posOverride : raw != null ? raw.getPos() : fromOther != null ? fromOther.getPos() : null; XivCombatant computed; long bnpcId = raw != null ? raw.getBnpcId() : 0; diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java index 7f7e63b77dbe..e552d48bcc07 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/groovy/GroovyColumns.java @@ -1,6 +1,5 @@ package gg.xp.xivsupport.gui.tables.groovy; -import gg.xp.xivsupport.gui.groovy.GroovyPanel; import gg.xp.xivsupport.gui.tables.CustomColumn; import gg.xp.xivsupport.gui.tables.CustomTableModel; import gg.xp.xivsupport.gui.tables.TableWithFilterAndDetails; @@ -12,7 +11,6 @@ import javax.swing.*; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; import java.awt.*; import java.lang.reflect.Array; import java.util.ArrayList; @@ -52,7 +50,14 @@ public static List getValues(Object obj) { else { return DefaultGroovyMethods.getMetaPropertyValues(obj) .stream() - .filter(GroovyColumns::isReadable) + .filter(pv1 -> { + try { + return isReadable(pv1); + } + catch (Throwable t) { + return false; + } + }) .filter(pv -> !"serialVersionUID".equals(pv.getName())) .toList(); } diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 7697294d58c1..d6725003ea1d 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -1,5 +1,10 @@ +

December 27, 2024

+
    +
  • CoD Chaotic Alliance Raid initial triggers added.
  • +
  • Various behind-the-scenes improvements.
  • +

December 21, 2024

  • Game data updates for 7.15.
  • diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java new file mode 100644 index 000000000000..2e2a857cdb33 --- /dev/null +++ b/xivsupport/src/test/java/gg/xp/xivsupport/events/actlines/Line271Test.java @@ -0,0 +1,36 @@ +package gg.xp.xivsupport.events.actlines; + +import gg.xp.reevent.events.EventDistributor; +import gg.xp.reevent.events.TestEventCollector; +import gg.xp.xivsupport.events.ACTLogLineEvent; +import gg.xp.xivsupport.events.state.XivState; +import gg.xp.xivsupport.models.Position; +import gg.xp.xivsupport.models.XivCombatant; +import gg.xp.xivsupport.sys.XivMain; +import org.picocontainer.MutablePicoContainer; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class Line271Test { + @Test + void testPos() { + String line = "271|2024-12-26T13:15:21.8910000-08:00|4000249E|-1.5745|00|00|73.5000|100.0000|0.0000|4843ca62b169bc5a"; + String dummyLine = "21|2024-12-26T13:15:21.9360000-08:00|4000249E|Cloud of Darkness|9E22|_rsv_40482_-1_1_0_0_SE2DC5B04_EE2DC5B04|E0000000||0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|||||||||||44|44|0|10000|||100.00|100.00|0.00|2.76|0000B1C5|0|0|00||01|9E22|9E22|1.100|3FD9|47c31102dc03ad85"; + MutablePicoContainer container = XivMain.testingMasterInit(); + TestEventCollector coll = new TestEventCollector(); + EventDistributor dist = container.getComponent(EventDistributor.class); + dist.registerHandler(coll); + dist.acceptEvent(new ACTLogLineEvent(line)); + XivState state = container.getComponent(XivState.class); + { + XivCombatant cbt = state.getCombatant(0x4000249E); + Assert.assertEquals(cbt.getPos(), new Position(73.5000, 100.0000, 0.0000, -1.5745)); + } + // Now submit a 21-line with a stale position - it should not do anything + dist.acceptEvent(new ACTLogLineEvent(dummyLine)); + { + XivCombatant cbt = state.getCombatant(0x4000249E); + Assert.assertEquals(cbt.getPos(), new Position(73.5000, 100.0000, 0.0000, -1.5745)); + } + } +}