diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastAdapter.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastAdapter.java index 5c222ca06052..584404b1a6f6 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastAdapter.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastAdapter.java @@ -6,14 +6,24 @@ 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.AbilityCastStart; +import gg.xp.xivsupport.events.state.combatstate.ActiveCastRepository; +import gg.xp.xivsupport.events.state.combatstate.CastTracker; import gg.xp.xivsupport.events.triggers.util.RepeatSuppressor; import java.time.Duration; +import java.util.Optional; @ScanMe public class NpcCastAdapter implements FeedHelperAdapter> { + private final ActiveCastRepository casts; + + public NpcCastAdapter(ActiveCastRepository casts) { + this.casts = casts; + } + @Override public Class eventType() { return AbilityCastStart.class; @@ -21,8 +31,9 @@ public Class eventType() { @Override public TypedEventHandler makeHandler(FeedHandlerChildInfo> info) { - long[] castIds = info.getAnnotation().value(); - long suppMs = info.getAnnotation().suppressMs(); + NpcCastCallout ann = info.getAnnotation(); + long[] castIds = ann.value(); + long suppMs = ann.suppressMs(); RepeatSuppressor supp; if (suppMs >= 0) { supp = new RepeatSuppressor(Duration.ofMillis(suppMs)); @@ -41,7 +52,37 @@ public void handle(EventContext context, AbilityCastStart event) { for (int i = 0; i < castIds.length; i++) { if (!event.getSource().isPc() && castIds[i] == event.getAbility().getId()) { if (supp.check(event)) { - context.accept(info.getHandlerFieldValue().getModified(event)); + RawModifiedCallout modified = info.getHandlerFieldValue().getModified(event); + if (ann.cancellable()) { + modified.addExpiryCondition(() -> { + Optional ctOpt = casts.forCast(event); + if (ctOpt.isEmpty()) { + return true; + } + CastTracker ct = ctOpt.get(); + switch (ct.getResult()) { + // Still in progress + case IN_PROGRESS -> { + return false; + } + // Wait for normal expiry delay. This acts as an "OR" so returning false + // means to defer to the existing logic. + case SUCCESS -> { + return false; + } + // Remove immediately if interrupted + case INTERRUPTED -> { + return true; + } + // ? + case UNKNOWN -> { + return false; + } + } + return false; + }); + } + context.accept(modified); } return; } diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastCallout.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastCallout.java index 9f96c0bad631..72751653d9a5 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastCallout.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/support/NpcCastCallout.java @@ -17,4 +17,9 @@ long[] value(); long suppressMs() default -1; + + /** + * @return Whether the callout should be removed if the cast stops + */ + boolean cancellable() default false; } diff --git a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/ultimate/FRU.java b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/ultimate/FRU.java index 4e50af9237da..00a6932681c6 100644 --- a/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/ultimate/FRU.java +++ b/triggers/triggers-dt/src/main/java/gg/xp/xivsupport/triggers/ultimate/FRU.java @@ -11,6 +11,7 @@ import gg.xp.xivsupport.callouts.ModifiableCallout; import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; import gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent; +import gg.xp.xivsupport.events.actlines.events.ActorControlExtraEvent; import gg.xp.xivsupport.events.actlines.events.BuffApplied; import gg.xp.xivsupport.events.actlines.events.HasAbility; import gg.xp.xivsupport.events.actlines.events.HasSourceEntity; @@ -32,6 +33,7 @@ import gg.xp.xivsupport.persistence.settings.JobSortOverrideSetting; import gg.xp.xivsupport.persistence.settings.JobSortSetting; +import java.io.Serial; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -109,8 +111,8 @@ public boolean enabled(EventContext context) { s.updateCall(powderMarkBombSoon, debuff); }); - private final ModifiableCallout utopianSkyStackInitial = ModifiableCallout.durationBasedCall("Utopian Sky: Initial (Fire)", "Stack Later"); - private final ModifiableCallout utopianSkySpreadInitial = ModifiableCallout.durationBasedCall("Utopian Sky: Initial (Lightning)", "Spread Later"); + private final ModifiableCallout utopianSkyStackInitial = ModifiableCallout.durationBasedCallWithoutDurationText("Utopian Sky: Initial (Fire)", "Stack Later"); + private final ModifiableCallout utopianSkySpreadInitial = ModifiableCallout.durationBasedCallWithoutDurationText("Utopian Sky: Initial (Lightning)", "Spread Later"); private final ModifiableCallout utopianSkyStackSafeSpot = ModifiableCallout.durationBasedCall("Utopian Sky: Safe Spot (Fire)", "Stack {safe}"); private final ModifiableCallout utopianSkySpreadSafeSpot = ModifiableCallout.durationBasedCall("Utopian Sky: Safe Spot (Lightning)", "Spread {safe}"); @@ -122,11 +124,13 @@ public boolean enabled(EventContext context) { (e1, s) -> { boolean isFire = e1.abilityIdMatches(0x9CDA); s.updateCall(isFire ? utopianSkyStackInitial : utopianSkySpreadInitial, e1); - var casts = s.waitEventsQuickSuccession(3, AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9CDE)); + // This is the "raising" animation. We can't use the casts directly since they position themselves in the middle. + // Could use cast locations but that ends up being more complicated. + var events = s.waitEventsQuickSuccession(3, ActorControlExtraEvent.class, acee -> acee.getCategory() == 0x3f && acee.getData0() == 0x4); List safe; do { List tmpSafe = new ArrayList<>(ArenaSector.all); - casts.stream().map(cast -> arenaPos.forCombatant(state.getLatestCombatantData(cast.getSource()))) + events.stream().map(acee -> arenaPos.forCombatant(state.getLatestCombatantData(acee.getTarget()))) .forEach(pos -> { tmpSafe.remove(pos); tmpSafe.remove(pos.opposite()); @@ -137,7 +141,9 @@ public boolean enabled(EventContext context) { } } while (safe.size() != 2); s.setParam("safe", safe); - s.updateCall(isFire ? utopianSkyStackSafeSpot : utopianSkySpreadSafeSpot); + // Get the actual cast so that we have something to display + var cast = s.waitEvent(AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9CDE)); + s.updateCall(isFire ? utopianSkyStackSafeSpot : utopianSkySpreadSafeSpot, cast); }); @AutoFeed @@ -158,10 +164,7 @@ public boolean enabled(EventContext context) { // Debounce s.waitMs(1_000); - var e4 = s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9CD2)); - s.updateCall(cyclonicBreakMove3, e4); - // Debounce - s.waitMs(1_000); + // The final movement is handled by turnNSSafe call below // Prior to the proteans (i.e. need another sq), there is, in one example: // Turn of the Heavens (6.7s) 9CD7 - determines safe spot? @@ -170,29 +173,46 @@ public boolean enabled(EventContext context) { // Burnout (9.4s) 9CE4 - probably KB? }); - private final ModifiableCallout turnNSSafe = ModifiableCallout.durationBasedCall("Turn of the Heavens: Dodge Lightning", "North/South Out"); - // private final ModifiableCallout turnKB = ModifiableCallout.durationBasedCall("Turn of the Heavens: Dodge Lightning", "Get Knocked to {redSafe ? 'Red' : 'Blue'}"); - private final ModifiableCallout turnKB = ModifiableCallout.durationBasedCall("Turn of the Heavens: Dodge Lightning", "Get Knocked to Safe Side"); + private final ModifiableCallout turnInitial = new ModifiableCallout<>("Turn of the Heavens: Initial", "{redSafe ? 'Red' : 'Blue'} Safe"); + private final ModifiableCallout turnNSSafe = ModifiableCallout.durationBasedCall("Turn of the Heavens: Dodge Lightning", "Move, North/South Out"); + private final ModifiableCallout turnKB = ModifiableCallout.durationBasedCall("Turn of the Heavens: Dodge Lightning", "Get Knocked {safe}"); @AutoFeed private final SequentialTrigger turnOfTheHeavensSq = SqtTemplates.sq(30_000, - AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9CD7, 0x9CD6), + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9CD6, 0x9CD7), (e1, s) -> { - // TODO: which is red vs blue safe? // How do we determine where red or blue is? -// s.updateCall(TODO); - s.waitMs(4_000); + // For the rings, they are called Halo of Flame (NPC 17821:9710) or Halo of Levin (17822:9711) + // 9CD6 is blue safe, 9CD7 is red safe + boolean redSafe = e1.abilityIdMatches(0x9CD7); + s.setParam("redSafe", redSafe); + s.updateCall(turnInitial); + s.waitMs(3_700); - s.setParam("redSafe", true); // TODO Optional lightning = casts.getActiveCastById(0x9CE3); s.updateCall(turnNSSafe, lightning.map(CastTracker::getCast).orElse(e1)); s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9CE3)); Optional burnout = casts.getActiveCastById(0x9CE4); + ArenaSector safe; + do { + // Find orbs that are east or west + // We are looking for the safe spot, so if fire is safe, look for the fire orb + List eastWest = state.npcsById(redSafe ? 17821 : 17822).stream() + // + .map(fireOrb -> arenaPos.forCombatant(state.getLatestCombatantData(fireOrb))) + .filter(pos -> pos == ArenaSector.WEST || pos == ArenaSector.EAST) + .toList(); + if (eastWest.size() == 1) { + safe = eastWest.get(0); + break; + } + else { + s.waitThenRefreshCombatants(200); + } + } while (true); + s.setParam("safe", safe); s.updateCall(turnKB, burnout.map(CastTracker::getCast).orElse(e1)); - s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9CE4)); - - }); @@ -233,6 +253,8 @@ public enum MechType { } public static class FruP1TetherEvent extends BaseEvent implements HasSourceEntity, HasTargetEntity, HasAbility { + @Serial + private static final long serialVersionUID = -9182985137525672763L; private final AbilityCastStart cast; private final XivCombatant source; private final XivPlayerCharacter target; @@ -324,8 +346,7 @@ private static ModifiableCallout makeTetherDefault(String desc s.waitMs(1_000); - // Wait for floating fetters - s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9CEB)); + // Call the first one soon, but for the rest, wait until the previous tether goes off s.updateCall(fourTetherResolving1); s.waitEvent(AbilityUsedEvent.class, aue -> aue.abilityIdMatches(0x9CEB)); s.updateCall(fourTetherResolving2); @@ -358,7 +379,7 @@ private static ModifiableCallout makeTetherDefault(String desc } ); - @NpcCastCallout(0x9CC0) + @NpcCastCallout(value = 0x9CC0, cancellable = true) private final ModifiableCallout p1enrage = ModifiableCallout.durationBasedCall("P1 Enrage", "Enrage"); /* @@ -373,6 +394,24 @@ private static ModifiableCallout makeTetherDefault(String desc private final ModifiableCallout quadrupleSlap = ModifiableCallout.durationBasedCall("Quadruple Slap", "Buster on {event.target}"); @NpcCastCallout(0x9D05) private final ModifiableCallout diamondDust = ModifiableCallout.durationBasedCall("Diamond Dust", "Raidwide"); + + @AutoFeed + private final SequentialTrigger axeScythe = SqtTemplates.sq(60_000, + AbilityCastStart.class, acs -> acs.abilityIdMatches(0x9D0A, 0x9D0B), + (e1, s) -> { + boolean isAxeKick = e1.abilityIdMatches(0x9D0A); + if (isAxeKick) { + // out + } + else { + // in + } + }); + + @NpcCastCallout(0x9D01) + private final ModifiableCallout twinStillness = ModifiableCallout.durationBasedCall("Twin Stillness", "Back to Front"); + @NpcCastCallout(0x9D02) + private final ModifiableCallout twinSilence = ModifiableCallout.durationBasedCall("Twin Silence", "Front to Back"); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java b/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java index 5e09b5274dfa..0c9a652eb914 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java @@ -28,7 +28,7 @@ public class RawModifiedCallout extends BaseEvent implements HasCalloutTracki private final @Nullable X event; private final Map arguments; private final Function guiProvider; - private final Predicate> expiry; + private Predicate> expiry; private @Nullable HasCalloutTrackingKey replaces; private @Nullable Color colorOverride; private final ModifiedCalloutHandle handle; @@ -74,6 +74,14 @@ public BooleanSupplier getExpiry() { return () -> expiry.test(this) || this.forceExpired; } + public void addExpiryPredicate(Predicate> condition) { + this.expiry = this.expiry.or(condition); + } + + public void addExpiryCondition(BooleanSupplier newExpiry) { + this.expiry = this.expiry.or(ignored -> newExpiry.getAsBoolean()); + } + public @Nullable HasCalloutTrackingKey getReplaces() { return replaces; } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/ActiveCastRepository.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/ActiveCastRepository.java index d3c25859d780..a9238a3e63bd 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/ActiveCastRepository.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/ActiveCastRepository.java @@ -1,6 +1,7 @@ package gg.xp.xivsupport.events.state.combatstate; import gg.xp.reevent.scan.Alias; +import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; import gg.xp.xivsupport.models.XivCombatant; import org.jetbrains.annotations.Nullable; @@ -29,4 +30,11 @@ default Optional getActiveCastById(long... ids) { .filter(ct -> ct.getCast().abilityIdMatches(ids)) .findFirst(); } + + default Optional forCast(AbilityCastStart event) { + return getAll() + .stream() + .filter(ct -> ct.getCast() == event) + .findFirst(); + } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/FlyingTextOverlay.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/FlyingTextOverlay.java index ec9b2400ceec..18d2e885a42c 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/FlyingTextOverlay.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/FlyingTextOverlay.java @@ -261,13 +261,16 @@ private void refreshCallouts() { } } synchronized (lock) { + outer: for (CalloutEvent callout : toAdd) { for (int i = 0; i < currentCallouts.size(); i++) { if (callout.shouldReplace(currentCallouts.get(i).event)) { + // If replacing a call, replace it in the list as-is currentCallouts.set(i, new VisualCalloutItem(callout)); - return; + continue outer; } } + // Otherwise, add it to the end of the list currentCallouts.add(new VisualCalloutItem(callout)); } currentCallouts.removeIf(visualCalloutItem -> { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java index 22d2afff2dc1..44f38decb6b3 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/StandardColumns.java @@ -224,7 +224,7 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole } }); c.setMinWidth(50); - c.setMaxWidth(50); + c.setMaxWidth(100); }); public static final CustomColumn combatantRawTypeColumn diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/persistence/settings/JobSortSetting.java b/xivsupport/src/main/java/gg/xp/xivsupport/persistence/settings/JobSortSetting.java index 4ba0fcefd468..afa9e95ce8ed 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/persistence/settings/JobSortSetting.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/persistence/settings/JobSortSetting.java @@ -156,6 +156,8 @@ public void validateJobSortOrder(List newSort) { int expectedNumberOfJobs = allValidJobs.size(); int actualNumberOfJobs = newSort.size(); if (expectedNumberOfJobs != actualNumberOfJobs) { + // TODO: this spams log on startup + // Make it more intelligent - if saved sort is a subset of the available jobs, don't throw this throw new IllegalArgumentException(String.format("New jail sort order was not the same size! %s -> %s", expectedNumberOfJobs, actualNumberOfJobs)); } EnumSet newSortAsSet = EnumSet.copyOf(newSort);