From e672f1eed1a509e6179cdbe305ef37224178daca Mon Sep 17 00:00:00 2001 From: XP Date: Thu, 27 Apr 2023 15:52:58 -0700 Subject: [PATCH 01/15] Refactor of how classes are instantiated --- .../easytriggers/conditions/IdType.java | 4 +- .../reevent/events/AutoEventDistributor.java | 4 +- .../gg/xp/reevent/events/EventMaster.java | 2 + .../main/java/gg/xp/reevent/scan/Alias.java | 11 + .../main/java/gg/xp/reevent/scan/Aliases.java | 9 + .../java/gg/xp/reevent/scan/AutoScan.java | 195 +++++++ .../xp/reevent/topology/TopologyProvider.java | 7 + .../events/triggers/seq/SqtTemplates.java | 2 +- .../events/triggers/duties/Dragonsong.java | 490 +----------------- .../java/gg/xp/compmonitor/CompListener.java | 8 + .../java/gg/xp/compmonitor/CompMonitor.java | 55 ++ .../xp/compmonitor/FilteredCompListener.java | 31 ++ .../gg/xp/compmonitor/InstantiatedItem.java | 4 + .../events/MonitoringEventDistributor.java | 115 ++++ .../gg/xp/services/ServiceSelectorGui.java | 2 +- .../cdsupport/CustomCooldownManager.java | 3 - .../xp/xivsupport/events/state/XivState.java | 3 + .../combatstate/ActiveCastRepository.java | 2 + .../combatstate/StatusEffectRepository.java | 3 + .../xp/xivsupport/groovy/GroovyManager.java | 46 +- .../java/gg/xp/xivsupport/gui/map/MapTab.java | 2 + .../xivsupport/gui/nav/GlobalUiRegistry.java | 2 +- .../xivsupport/gui/overlay/RefreshLoop.java | 2 +- .../xivsupport/gui/tables/CustomColumn.java | 2 +- .../gui/tables/CustomTableModel.java | 6 +- .../gui/tables/TableWithFilterAndDetails.java | 2 +- .../gui/tables/filters/GroovyFilter.java | 2 +- .../gui/tables/filters/TextBasedFilter.java | 8 +- .../gui/tables/filters/VisualFilter.java | 2 +- .../gui/tabs/PluginTopologyPanel.java | 4 +- .../gui/tree/TopologyTreeModel.java | 4 +- .../gui/tree/TopologyTreeRenderer.java | 2 +- .../gg/xp/xivsupport/models/ArenaSector.java | 6 +- .../gg/xp/xivsupport/models/XivCombatant.java | 2 +- .../java/gg/xp/xivsupport/sys/XivMain.java | 17 +- .../xivsupport/groovy/GroovyAliasesTest.java | 34 ++ 36 files changed, 553 insertions(+), 540 deletions(-) create mode 100644 reevent/src/main/java/gg/xp/reevent/scan/Alias.java create mode 100644 reevent/src/main/java/gg/xp/reevent/scan/Aliases.java create mode 100644 reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java create mode 100644 reevent/src/main/java/gg/xp/reevent/topology/TopologyProvider.java create mode 100644 xivsupport/src/main/java/gg/xp/compmonitor/CompListener.java create mode 100644 xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java create mode 100644 xivsupport/src/main/java/gg/xp/compmonitor/FilteredCompListener.java create mode 100644 xivsupport/src/main/java/gg/xp/compmonitor/InstantiatedItem.java create mode 100644 xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java create mode 100644 xivsupport/src/test/java/gg/xp/xivsupport/groovy/GroovyAliasesTest.java diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/conditions/IdType.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/conditions/IdType.java index 95801a191b87..c3c38a390ea9 100644 --- a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/conditions/IdType.java +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/conditions/IdType.java @@ -7,12 +7,12 @@ @Retention(RetentionPolicy.RUNTIME) public @interface IdType { /** - * @return The class of item that the ID corresponds to. + * @return The class of instance that the ID corresponds to. */ Class value(); /** - * @return True if a mapping from the given ID to a concrete item is required. False if you + * @return True if a mapping from the given ID to a concrete instance is required. False if you * want to accept non-matched items. */ boolean matchRequired() default true; diff --git a/reevent/src/main/java/gg/xp/reevent/events/AutoEventDistributor.java b/reevent/src/main/java/gg/xp/reevent/events/AutoEventDistributor.java index 0187ac54f9e7..88827a947270 100644 --- a/reevent/src/main/java/gg/xp/reevent/events/AutoEventDistributor.java +++ b/reevent/src/main/java/gg/xp/reevent/events/AutoEventDistributor.java @@ -5,6 +5,7 @@ import gg.xp.reevent.scan.AutoHandlerScan; import gg.xp.reevent.topology.Topology; import gg.xp.reevent.topology.TopologyInfo; +import gg.xp.reevent.topology.TopologyProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +17,7 @@ import java.util.Map; import java.util.stream.Collectors; -public class AutoEventDistributor extends BasicEventDistributor { +public class AutoEventDistributor extends BasicEventDistributor implements TopologyProvider { private static final Logger log = LoggerFactory.getLogger(AutoEventDistributor.class); private final AutoHandlerScan scanner; private final TopologyInfo topoInfo; @@ -65,6 +66,7 @@ protected List> getHandlersForEvent(Event event) { }).sorted(Comparator.comparing(EventHandler::getOrder)).toList()); } + @Override public Topology getTopology() { return topology; } diff --git a/reevent/src/main/java/gg/xp/reevent/events/EventMaster.java b/reevent/src/main/java/gg/xp/reevent/events/EventMaster.java index 5539ad26ed09..32ad66cbde9a 100644 --- a/reevent/src/main/java/gg/xp/reevent/events/EventMaster.java +++ b/reevent/src/main/java/gg/xp/reevent/events/EventMaster.java @@ -1,5 +1,6 @@ package gg.xp.reevent.events; +import gg.xp.reevent.scan.Alias; import gg.xp.reevent.state.QueueState; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.slf4j.Logger; @@ -8,6 +9,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadFactory; +@Alias("master") public class EventMaster { private static final Logger log = LoggerFactory.getLogger(EventMaster.class); diff --git a/reevent/src/main/java/gg/xp/reevent/scan/Alias.java b/reevent/src/main/java/gg/xp/reevent/scan/Alias.java new file mode 100644 index 000000000000..53cb2a2ec8df --- /dev/null +++ b/reevent/src/main/java/gg/xp/reevent/scan/Alias.java @@ -0,0 +1,11 @@ +package gg.xp.reevent.scan; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(Aliases.class) +public @interface Alias { + String value(); +} diff --git a/reevent/src/main/java/gg/xp/reevent/scan/Aliases.java b/reevent/src/main/java/gg/xp/reevent/scan/Aliases.java new file mode 100644 index 000000000000..bc9943443f2a --- /dev/null +++ b/reevent/src/main/java/gg/xp/reevent/scan/Aliases.java @@ -0,0 +1,9 @@ +package gg.xp.reevent.scan; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Aliases { + Alias[] value(); +} diff --git a/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java b/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java new file mode 100644 index 000000000000..51434d5b76d6 --- /dev/null +++ b/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java @@ -0,0 +1,195 @@ +package gg.xp.reevent.scan; + +import org.jetbrains.annotations.Nullable; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.reflections.scanners.Scanners.MethodsAnnotated; +import static org.reflections.scanners.Scanners.SubTypes; + +public class AutoScan { + + private static final Logger log = LoggerFactory.getLogger(AutoScan.class); + private final AutoHandlerInstanceProvider instanceProvider; + private final AutoHandlerConfig config; + private static final Pattern jarFileName = Pattern.compile("([a-zA-Z0-9\\-.]+)\\.jar"); + private static final List scanBlacklist = List.of( + "annotations", + "caffeine", + "commons", + "flatlaf", + "groovy", + "http", + "jackson", + "Java-WebSocket", + "javaassist", + "jna", + "jsr", + "nonexistent", + "logback", + "opencsv", + "picocontainer", + "reflections", + "rsyntaxtextarea", + "sfl4j", + "xivdata" + ); + private boolean scanned; + + public AutoScan(AutoHandlerInstanceProvider instanceProvider, AutoHandlerConfig config) { + this.instanceProvider = instanceProvider; + this.config = config; + } + + private List findAddonJars() { + return config.getAddonJars(); + } + + public void doScanIfNeeded() { + if (!scanned) { + doScan(); + } + } + + public void doScan() { + log.info("Scanning packages"); + List out = new ArrayList<>(); +// ClassLoader loader = new ForceReloadClassLoader(); +// ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); + //noinspection EmptyFinallyBlock + // TODO: Reload of existing classes is broken because reloading basic classes such as 'Event' + // in a new classloader causes the JVM to no longer see it as the same class, causing signature + // mismatches. + // This will become significantly less of an issue when stuff is built into separate JARs, but for + // now, only hot add/remove will be supported, no hot modify. +// Thread.currentThread().setContextClassLoader(loader); + Collection urls = ClasspathHelper.forJavaClassPath(); + // TODO: make package blacklist a setting + List addonUrls = findAddonJars(); + urls = Stream.concat(urls.stream(), addonUrls.stream()).filter(u -> { + String jarName = getJarName(u.toString()); + if (jarName == null) { + return true; + } + else { + return scanBlacklist.stream().noneMatch(jarName::startsWith); + } + }).collect(Collectors.toList()); + log.info("URLs: {}", urls); + // TODO: make this public so that we aren't doing as much re-scanning + URLClassLoader newClassLoader = new URLClassLoader(addonUrls.toArray(new URL[]{})); + ClassLoader[] loaders = {Thread.currentThread().getContextClassLoader(), newClassLoader}; + final Set annotatedMethods = ConcurrentHashMap.newKeySet(); + final Set> annotatedClasses = ConcurrentHashMap.newKeySet(); + final Map failedModules = new ConcurrentHashMap<>(); + // TODO: make these changes in the Groovy side too + urls.parallelStream().forEach(url -> { + log.info("URL: '{}'", url); + Reflections reflections = new Reflections( + new ConfigurationBuilder() + .setUrls(Collections.singletonList(url)) + .setParallel(true) + .setScanners(Scanners.TypesAnnotated, MethodsAnnotated, SubTypes)); + try { + annotatedMethods.addAll(reflections.get(MethodsAnnotated.with(HandleEvents.class).as(Method.class, loaders))); + annotatedClasses.addAll(reflections.get(Scanners.TypesAnnotated.with(ScanMe.class).asClass(loaders))); + } + catch (Throwable t) { + String jarName = getJarName(url.toString()); + failedModules.put(jarName, new RuntimeException("Module " + jarName + " failed to load: " + t, t)); + } + }); + if (!failedModules.isEmpty()) { + StringBuilder sb = new StringBuilder("One or more modules failed to load. If this problem persists, try deleting them: \n"); + failedModules.keySet().forEach(jarName -> sb.append(" - ").append(jarName)); + RuntimeException combined = new RuntimeException(sb.toString()); + failedModules.values().forEach(combined::addSuppressed); + throw combined; + } + log.info("Scan done, setting up topology now"); + Reflections reflections = new Reflections( + new ConfigurationBuilder() + .setUrls(urls) + .setParallel(true) + .setScanners(Scanners.TypesAnnotated, Scanners.MethodsAnnotated, Scanners.SubTypes)); + + Map, List> classMethodMap = new LinkedHashMap<>(); + for (Class annotatedClass : annotatedClasses) { + if (isClassInstantiable(annotatedClass)) { + classMethodMap.computeIfAbsent(annotatedClass, unused -> new ArrayList<>()); + } + else { + log.warn("Not adding @ScanMe class {} because it is not instantiable", annotatedClass); + } + + } + int methodCount = 0; + for (Method method : annotatedMethods) { + if (method.isBridge() || method.isSynthetic()) { + // If you both annotate the method, and implement EventHandler yourself, you'll get an extra bridge+synthetic + // method lying around. Safest option is to just ignore stuff if it is synthetic or a bridge. + // TODO: can this be removed now? + continue; + } + methodCount++; + Class clazz = method.getDeclaringClass(); + // If you extend a class with an annotated method, the method still "belongs" to the superclass. + // Thus, we need to explicitly scan for children of the class too. + Set> implementingClasses = reflections.get(SubTypes.of(clazz).asClass(loaders)); + if (!implementingClasses.isEmpty()) { + log.info("Class {} has implementors: {}", clazz, implementingClasses); + } + //noinspection SimplifyForEach + Stream.concat(Stream.of(clazz), implementingClasses.stream()) + .filter(AutoScan::isClassInstantiable) + .forEach(cls -> classMethodMap.computeIfAbsent(cls, unused -> new ArrayList<>()).add(method)); + } + log.info("Methods: {}", methodCount); + + + log.info("Loading instances"); + // Preload class instances + classMethodMap.keySet().forEach(instanceProvider::preAdd); + classMethodMap.keySet().forEach(instanceProvider::getInstance); + log.info("Loaded instances"); + scanned = true; + } + + + // Filter out interfaces, abstract classes, and other junk + private static boolean isClassInstantiable(Class clazz) { + return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isAnonymousClass() && (clazz.getDeclaringClass() == null); + } + + private static @Nullable String getJarName(String uriStr) { + Matcher matcher = jarFileName.matcher(uriStr); + if (matcher.find()) { + return matcher.group(1); + } + else { + return null; + } + } +} diff --git a/reevent/src/main/java/gg/xp/reevent/topology/TopologyProvider.java b/reevent/src/main/java/gg/xp/reevent/topology/TopologyProvider.java new file mode 100644 index 000000000000..517ef883c522 --- /dev/null +++ b/reevent/src/main/java/gg/xp/reevent/topology/TopologyProvider.java @@ -0,0 +1,7 @@ +package gg.xp.reevent.topology; + +public interface TopologyProvider { + + Topology getTopology(); + +} diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SqtTemplates.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SqtTemplates.java index ff8b39b704fa..d15fd3bec8bf 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SqtTemplates.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SqtTemplates.java @@ -78,7 +78,7 @@ public static SequentialTrigger sq( /** * Trigger template for when the same event might indicate different things in a fight. *

- * The first time this is called, it will execute the first item in the 'triggers' array. + * The first time this is called, it will execute the first instance in the 'triggers' array. * The second time, it will execute the second, and so on. It will reset back to the first * on a wipe/reset. * diff --git a/triggers/triggers-ew/src/main/java/gg/xp/xivsupport/events/triggers/duties/Dragonsong.java b/triggers/triggers-ew/src/main/java/gg/xp/xivsupport/events/triggers/duties/Dragonsong.java index cb4ca1d48638..595bab16cd55 100644 --- a/triggers/triggers-ew/src/main/java/gg/xp/xivsupport/events/triggers/duties/Dragonsong.java +++ b/triggers/triggers-ew/src/main/java/gg/xp/xivsupport/events/triggers/duties/Dragonsong.java @@ -1159,7 +1159,7 @@ else if (hraesGlow) { } }); - @HandleEvents + @HandleEvents(order = 1) public void p6casts(EventContext context, AbilityCastStart event) { // Hraesvelgr long npc = event.getSource().getbNpcId(); @@ -1317,494 +1317,6 @@ public void vfxHandler(EventContext context, StatusLoopVfxApplied event) { } }); -// @HandleEvents -// public void p6_pyretic(EventContext context, BuffApplied event) { -// if (event.getTarget().isThePlayer() && event.getBuff().getId() == 0xB52) { -// context.accept(pyretic.getModified(event)); -// } -// } - - /* - P7 notes: - - "Alternative End" - 29752: raidwide - - "Exaflare's Edge" - 28059: exaflare cast from real DKT (12616) - 28060: 3x exaflare initial hits from fake DKTs (9020) - 28061: 9x exaflare follow up hits from fake DKTs (later hits are 5x) - - "Trinity" - 0x6D9E: Trinity from real DKT (no telegraph) - 0x6D9F: Trinity (Dark resistance down) - 0x6DA0: Trinity (Light resistance down) - 0x6DA1: Trinity (Light + dark + phys resist down) - 2x trinity - - "Akh Morn's Edge" - 28051: tanks/light parties real cast "Akh Morn's Edge" - 29452/3/4: tanks/light parties actual hits (from fakes) (54 is tanks, 52 and 53 are the side hits) - 28050: ? comes from fake, immediately after (dynamo/chariot?) "Ice of Ascalon" - - "Akh Morn's Edge" - 28052: Comes from real - 28054: hits H/D - 28055: Hits Ts - then 28052 again? - Then lots of 28054 on H/D, 28055 on DPS (akh morns?) - - Is 28051 vs 28052 in vs out? - - Another trinity pair - - 28057: real "Gigaflare's Edge" - 28058, 28114, 28115 from fakes - 28058 hits everyone - 28049 after (chariot/dynamo)? "Flames of Ascalon" - 28114 on everyone - 28115 on everyone - - Another Trinity pair - - Another Exaflare - - Another Trinity pair - - Another tanks/light parties cast - - Another 28052 etc set - - Another Trinity Pair - - Another 28057 mechanic - - Another Trinity pair - - Another Exaflare - - Another Trinity pair - - Another tanks/light parties cast - - Another 28052 set - - Another Trinity pair - - 28206 from real - enrage? - 29455/6/7 from fakes - - - - -{ - "action_data": { - "28049": { - "Name_de": "Flamme von Askalon", - "Name_en": "Flames of Ascalon", - "Name_fr": "Feu d'Ascalon", - "Name_ja": "\u30d5\u30ec\u30a4\u30e0\u30fb\u30aa\u30d6\u30fb\u30a2\u30b9\u30ab\u30ed\u30f3" - }, - "28050": { - "Name_de": "Eis von Askalon", - "Name_en": "Ice of Ascalon", - "Name_fr": "Glace d'Ascalon", - "Name_ja": "\u30a2\u30a4\u30b9\u30fb\u30aa\u30d6\u30fb\u30a2\u30b9\u30ab\u30ed\u30f3" - }, - "28051": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28052": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28053": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28054": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28055": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28056": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "28057": { - "Name_de": "Gigaflare-Klinge", - "Name_en": "Gigaflare's Edge", - "Name_fr": "Lame de GigaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30ae\u30ac\u30d5\u30ec\u30a2" - }, - "28058": { - "Name_de": "Gigaflare-Klinge", - "Name_en": "Gigaflare's Edge", - "Name_fr": "Lame de GigaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30ae\u30ac\u30d5\u30ec\u30a2" - }, - "28059": { - "Name_de": "Exaflare-Klinge", - "Name_en": "Exaflare's Edge", - "Name_fr": "Lame d'ExaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30a8\u30af\u30b5\u30d5\u30ec\u30a2" - }, - "28060": { - "Name_de": "Exaflare-Klinge", - "Name_en": "Exaflare's Edge", - "Name_fr": "Lame d'ExaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30a8\u30af\u30b5\u30d5\u30ec\u30a2" - }, - "28061": { - "Name_de": "Exaflare-Klinge", - "Name_en": "Exaflare's Edge", - "Name_fr": "Lame d'ExaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30a8\u30af\u30b5\u30d5\u30ec\u30a2" - }, - "28114": { - "Name_de": "Gigaflare-Klinge", - "Name_en": "Gigaflare's Edge", - "Name_fr": "Lame de GigaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30ae\u30ac\u30d5\u30ec\u30a2" - }, - "28115": { - "Name_de": "Gigaflare-Klinge", - "Name_en": "Gigaflare's Edge", - "Name_fr": "Lame de GigaBrasier", - "Name_ja": "\u9a0e\u7adc\u5263\u30ae\u30ac\u30d5\u30ec\u30a2" - }, - "28206": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "28207": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "28208": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "28209": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "29452": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "29453": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "29454": { - "Name_de": "Akh Morns Klinge", - "Name_en": "Akh Morn's Edge", - "Name_fr": "Lame d'Akh Morn", - "Name_ja": "\u9a0e\u7adc\u5263\u30a2\u30af\u30fb\u30e2\u30fc\u30f3" - }, - "29455": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "29456": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "29457": { - "Name_de": "Morn Afahs Klinge", - "Name_en": "Morn Afah's Edge", - "Name_fr": "Lame de Morn Afah", - "Name_ja": "\u9a0e\u7adc\u5263\u30e2\u30fc\u30f3\u30fb\u30a2\u30d5\u30a1\u30fc" - }, - "29752": { - "Name_de": "Ein neues Ende", - "Name_en": "Alternative End", - "Name_fr": "Fin alternative", - "Name_ja": "\u30a2\u30eb\u30c6\u30a3\u30e1\u30c3\u30c8\u30a8\u30f3\u30c9\u30fb\u30aa\u30eb\u30bf\u30ca" - }, - "9259": { - "Name_ko": "\uadf8\ub79c\ub4dc\ud06c\ub85c\uc2a4: \uc54c\ud30c" - }, - "9260": { - "Name_ko": "\uadf8\ub79c\ub4dc\ud06c\ub85c\uc2a4: \ub378\ud0c0" - }, - "9261": { - "Name_ko": "\uadf8\ub79c\ub4dc\ud06c\ub85c\uc2a4: \uc624\uba54\uac00" - } - }, - "bnpc_data": { - "11319": { - "Singular_de": "K\u00f6nig Thordan", - "Singular_en": "Dragon-king Thordan", - "Singular_fr": "Thordan le Dieu Dragon", - "Singular_ja": "\u9a0e\u7adc\u795e\u30c8\u30fc\u30eb\u30c0\u30f3" - }, - "6055": { - "Singular_ko": "\ub124\uc624 \uc5d1\uc2a4\ub370\uc2a4" - } - }, - "instancecontenttextdata_data": { - "17817": { - "Text_ko": "\ubaa8\ub4e0 \uae30\uc5b5, \ubaa8\ub4e0 \uc874\uc7ac, \ubaa8\ub4e0 \ucc28\uc6d0\uc744 \uc18c\uba78\uc2dc\ud0a4\uace0\u2026\u2026" - }, - "17818": { - "Text_ko": "\uadf8\ub9ac\uace0 \ub098\ub3c4 \uc18c\uba78\ud560 \uac83\uc774\ub2e4\u2026\u2026." - }, - "17819": { - "Text_ko": "\uc601\uc6d0\ud788!!" - }, - "32600": { - "Text_de": "Den Lauf der Geschichte vermag niemand umzukehren. Und doch erwacht ein jeder hin wieder aus einem Traum, in dem es ihm gelang. Dies ist das Lied einer ertr\u00e4umten Zukunft, in der ein teurer Freund dem Tode knapp entrann...", - "Text_en": "'Tis said that there are no ifs in history, yet man is wont to dream. Let us dream, then, of a future where a dear comrade lived...", - "Text_fr": "Le pass\u00e9 a beau \u00eatre immuable, il n'emp\u00eachera jamais les hommes de r\u00eaver... Et si nous r\u00eavions tous ensemble \u00e0 la survie d'un ami cher disparu dans la fleur de l'\u00e2ge?", - "Text_ja": "\u6b74\u53f2\u306b\u3082\u3057\u3082\u306f\u306a\u3044\u3068\u8a00\u3046\u304c \u305d\u308c\u3067\u3082\u4eba\u306f\u5922\u60f3\u3059\u308b\u3082\u306e\u306a\u3089\u3070 \u3053\u3053\u306b\u8a60\u304a\u3046 \u76df\u53cb\u3092\u6551\u3063\u305f\u5148\u306b\u5f85\u3064\u672a\u6765\u3092\u2015\u2015" - }, - "32613": { - "Text_de": "Und so ward die erste Strophe einer anderen Zukunft geschrieben. Doch die Tr\u00e4ume eines fahrenden S\u00e4ngers sind lang, und das ganze Lied \u00fcber die vielen Pfade der Geschichte noch nicht zuende gesungen...", - "Text_en": "Thus did one song draw to a close. But here and now, this minstrel shall perform anothera song of imagination transcending...", - "Text_fr": "Ainsi s'ach\u00e8ve ce chant des dragons, mais le m\u00e9nestrel errant a bien d'autres vers tenant du miracle au bout des cordes de sa harpe...", - "Text_ja": "\u304b\u304f\u3066 \u3072\u3068\u3064\u306e\u7adc\u8a69\u306f\u7d42\u308f\u308a\u3092\u544a\u3052\u308b\u3060\u304c \u8a69\u4eba\u3068\u3057\u3066\u3053\u3053\u306b\u8a60\u304a\u3046 \u8d77\u3053\u308b\u306f\u305a\u306e\u306a\u3044\u5947\u8de1\u306e\u8a69\u3092\u2015\u2015" - }, - "32615": { - "Text_de": "Hahahaha! Seht mich an! Erzittert vor der Macht des allm\u00e4chtigen Gottes, gen\u00e4hrt von den Augen seiner Feinde!", - "Text_en": "Hahahaha! By the power of mine enemy's eyes, I am become a god eternal!", - "Text_fr": "Hahahahaha! Admirez la puissance des Yeux du dragon, et tremblez devant votre nouveau Dieu!", - "Text_ja": "\u30cf\u30cf\u30cf\u30cf\u30cf\uff01\u3000\u898b\u3088\uff01\u3000\u754f\u308c\u3088\uff01\u3059\u3079\u3066\u306e\u7adc\u306e\u773c\u3092\u5f97\u3066\u3001\u6c38\u9060\u306e\u795e\u304c\u3001\u4eca\u3053\u3053\u306b\u964d\u81e8\u3059\u308b\u306e\u3060\uff01" - }, - "32617": { - "Text_de": "O Askalon! L\u00e4utere die vom Licht Befleckten mit unermesslichen Qualen!", - "Text_en": "O Ascalon! Purge the tainted with the light of sorrow immeasurable!", - "Text_fr": "\u00d4 Ascalon, sainte \u00e9p\u00e9e! Que ta lame funeste pourfende la Lumi\u00e8re!", - "Text_ja": "\u773c\u3092\u55b0\u3089\u3044\u3057\u8056\u5263\u3088\uff01\u305d\u306e\u60b2\u5606\u306e\u8f1d\u304d\u3067\u3001\u5149\u306e\u4f7f\u5f92\u3092\u713c\u304d\u5c3d\u304f\u305b\uff01" - }, - "32618": { - "Text_de": "O Askalon! Zerr\u00fctte die Unw\u00fcrdigen mit dem Glei\u00dfen endlosen Grolls!", - "Text_en": "O Ascalon! Consign the wicked with the light of rancor unquenchable!", - "Text_fr": "\u00d4 Ascalon, sainte \u00e9p\u00e9e! Que ta lame rageuse s'abatte et donne la mort!", - "Text_ja": "\u773c\u3092\u55b0\u3089\u3044\u3057\u8056\u5263\u3088\uff01\u305d\u306e\u6028\u5ff5\u306e\u8f1d\u304d\u3067\u3001\u6b7b\u306e\u5e95\u306b\u53e9\u304d\u843d\u3068\u305b\uff01" - }, - "32619": { - "Text_de": "O Askalon! Versenge die Erde mit der bei\u00dfenden Glut des Grams!", - "Text_en": "O Ascalon! Scorch the earth with the light of regret unfathomable!", - "Text_fr": "\u00d4 Ascalon, sainte \u00e9p\u00e9e! Que ta lame sinistre consume la terre enti\u00e8re!", - "Text_ja": "\u773c\u3092\u55b0\u3089\u3044\u3057\u8056\u5263\u3088\uff01\u305d\u306e\u6094\u6068\u306e\u8f1d\u304d\u3067\u3001\u5927\u5730\u3082\u308d\u3068\u3082\u713c\u304d\u7126\u304c\u305b\uff01" - }, - "32620": { - "Text_de": "Augen des Feindes! Es ist Zeit, die Ungl\u00e4ubigen zur Strecke zu bringen!", - "Text_en": "Your time is come! Eyes of mine enemy, bring oblivion unto the unbelievers!", - "Text_fr": "Yeux du dragon! Soyez magnanimes, et offrez la mort \u00e9ternelle aux ennemis du Saint-Si\u00e8ge!", - "Text_ja": "\u305d\u308d\u305d\u308d\u7d42\u3044\u3060\u2026\u2026\uff01\u3059\u3079\u3066\u306e\u773c\u3088\u3001\u6c38\u9060\u306e\u6b7b\u3092\u3053\u3053\u306b\u2026\u2026\uff01" - }, - "32622": { - "Text_de": "Fortschritt im alternativen Drachenkrieg", - "Text_en": "Alternate Dragonsong War Progress", - "Text_fr": "Avancement de la Guerre du chant des dragons alternative", - "Text_ja": "\u507d\u5178\u7adc\u8a69\u6226\u4e89\u9032\u884c\u5ea6" - }, - "32625": { - "Text_de": "Ein einziges Leben vermag den Lauf der Geschichte zu \u00e4ndern... Unterjocht von der Kraft eines uralten Reliktes, stellte der heilige Drache Hraesvelgr sich auf die Seite seines rachs\u00fcchtigen Bruders.", - "Text_en": "A single life can alter the course of history... Enslaved by an ancient relic, the great wyrm Hraesvelgr descended upon Ishgard at his vengeful brother's side.", - "Text_fr": "Il suffit parfois d'un seul \u00eatre pour changer le cours de l'Histoire, et c'est ainsi que le dragon sacr\u00e9 Hraesvelgr, sous l'influence de la technologie allagoise, plongea sur Ishgard pour se battre aux c\u00f4t\u00e9s de son fr\u00e8re...", - "Text_ja": "\u3072\u3068\u3064\u306e\u547d\u304c \u6b74\u53f2\u306e\u6d41\u308c\u3092\u5909\u3048\u3066\u3086\u304f\u2015\u2015\u53e4\u4ee3\u306e\u907a\u7269\u306b\u3088\u308a\u64cd\u3089\u308c\u3057\u8056\u7adc\u304c \u90aa\u7adc\u3068\u5171\u306b\u821e\u3044\u964d\u308a\u308b" - }, - "32626": { - "Text_de": "K\u00f6nig Thordan nutzte die dunkle Gunst der Stunde und verleibte sich die Lebenskraft beider Drachen ein, die ihm eine noch g\u00f6ttlichere, noch entsetzlichere Macht verliehen als zuvor.", - "Text_en": "Thus did a dreadful new divinity ariseone endowed with the nigh-irrepressible life force of two great wyrms...", - "Text_fr": "Conscient de cette opportunit\u00e9 unique, le roi profita de l'occasion pour immoler les deux cr\u00e9atures anciennes sur l'autel d'une divinit\u00e9 dont la f\u00e9rocit\u00e9 d\u00e9passe l'imagination: Thordan le Dieu Dragon...", - "Text_ja": "\u90aa\u7adc\u3068\u8056\u7adc\u306e\u547d\u3092\u7ce7\u3068\u3057 \u65b0\u305f\u306a\u308b\u6c38\u9060\u306e\u795e\u304c\u964d\u81e8\u3057\u305f\u3042\u3048\u3066 \u305d\u306e\u540d\u3092\u3053\u3046\u547c\u307c\u3046 \u9a0e\u7adc\u795e\u30c8\u30fc\u30eb\u30c0\u30f3\u3068\u2015\u2015" - }, - "32627": { - "Text_de": "Nun, Krieger des Lichts? Sag mir, wie gedenkst du, diesem Krieg ein Ende zu setzen?", - "Text_en": "Tell me, Warrior of Light. How do you propose to end this conflict?", - "Text_fr": "Laisse-moi te poser une question, Guerri\u0002\b\r\u0005\u0005\u00e8re\u0003er\u0003 de la Lumi\u00e8re... Comment comptes-tu mettre fin \u00e0 ce conflit mill\u00e9naire?", - "Text_ja": "\u3055\u3066\u3001\u5149\u306e\u4f7f\u5f92\u3088\u3001\u3053\u3053\u3067\u554f\u304a\u3046\u3002\u8cb4\u69d8\u306f\u3044\u304b\u306b\u3057\u3066\u3001\u3053\u306e\u5343\u5e74\u6226\u4e89\u3092\u7d42\u308f\u3089\u305b\u3088\u3046\u3068\u3044\u3046\u306e\u3060\uff1f" - }, - "32628": { - "Text_de": "Glaubst du, mein Tod k\u00f6nne den tausend Jahre w\u00e4hrenden Blutdurst des Schlachtens stillen? Dann bist du ein t\u00f6richter Narr des Lichts!", - "Text_en": "If you believe that eliminating me will undo a thousand years of strife and suffering, then you are a fool.", - "Text_fr": "Ne crois pas que tu y parviendras simplement en m'\u00e9liminant... Rien, pas m\u00eame ma disparition, ne saurait effacer mille ans de souffrance et de chaos...", - "Text_ja": "\u30ef\u30b7\u3092\u6392\u305b\u3070\u3001\u3053\u306e\u6226\u304c\u7d42\u308f\u308a\u3001\u5343\u5e74\u306e\u798d\u6839\u304c\u65ad\u3066\u308b\u3068\u3067\u3082\uff1f\u3082\u3046\u3084\u3081\u3088\u3046\u3067\u306f\u306a\u3044\u304b\u3001\u5149\u306e\u4f7f\u5f92\u3088\u2026\u2026\u3002" - } - }, - "logmessage_data": { - "2853": { - "Text_ko": "\uc6b0 \uc8fc \uc758 \ubc95 \uce59 \uc774 \ud750 \ud2b8 \ub7ec \uc9d1 \ub2c8 \ub2e4 !!" - } - }, - "npcyell_data": { - "13580": { - "Text_de": "Du... bist wohlauf? Welch ein Gl\u00fcck...", - "Text_en": "You...you are unharmed? Thank goodness...", - "Text_fr": "Tu n'as rien... Tant mieux...", - "Text_ja": "\u7121\u4e8b\u2026\u3060\u3063\u305f\u306e\u3060\u306a\u2026\u3088\u304b\u3063\u305f\u2026\u672c\u5f53\u306b\u2026" - }, - "13586": { - "Text_de": "Ich ergebe mich... Bitte, hab Gnade...", - "Text_en": "I yield... Have mercy, I beg you...", - "Text_fr": "Je me rends... Par piti\u00e9, \u00e9pargne-moi...", - "Text_ja": "\u8ca0\u3051\u3092\u2026\u8a8d\u3081\u3088\u3046\u2026\u3086\u3048\u306b\u3001\u547d\u3060\u3051\u306f\u2026\u983c\u3080\u2026" - }, - "13587": { - "Text_de": "Das ist Thordan! Was um alles in der Welt hat er vor...?!", - "Text_en": "Thordan!? Seven hells, he cannot mean to!", - "Text_fr": "... Thordan!! Voil\u00e0 donc le v\u00e9ritable pouvoir des Yeux du dragon...", - "Text_ja": "\u2026\u3042\u308c\u306f\u3001\u6559\u7687\u30c8\u30fc\u30eb\u30c0\u30f3\uff01\u307e\u3055\u304b\u3001\u3059\u3079\u3066\u306e\u7adc\u306e\u773c\u3092\u2026\uff01\uff1f" - }, - "13588": { - "Text_de": "Ha! Dein Mitgef\u00fchl wird dein Untergang sein...", - "Text_en": "Hmph, your compassion will be the end of you...", - "Text_fr": "Tu regretteras ta mis\u00e9ricorde, disciple de la Lumi\u00e8re...", - "Text_ja": "\u7518\u3044\u306a\u3001\u5149\u306e\u4f7f\u5f92\u3088\u2026\u305d\u306e\u7518\u3055\u3092\u5f8c\u6094\u305b\u306c\u3053\u3068\u3060\u2026" - }, - "13597": { - "Text_de": "Bringen wir Estinien in Sicherheit! Rasch! ", - "Text_en": "We must see Estinien to safety!", - "Text_fr": "Ugh... Il faut mettre Estinien en s\u00e9curit\u00e9 imm\u00e9diatement!", - "Text_ja": "\u304f\u3063\u2026\u4eca\u306f\u3001\u30a8\u30b9\u30c6\u30a3\u30cb\u30a2\u30f3\u6bbf\u3092\u5b89\u5168\u306a\u5834\u6240\u3078\u2026\uff01" - } - }, - "status_data": { - "1379": { - "Description_ko": "\ucc9c\uccb4 \ub9c8\ubc95\uc5d0 \uac78\ub9b0 \uc0c1\ud0dc. HP\uac00 \uc11c\uc11c\ud788 \uc904\uc5b4\ub4e0\ub2e4.", - "Name_ko": "\uc54c\ub9c8\uac8c\uc2a4\ud2b8" - }, - "2748": { - "Description_de": "Eines Freundes inniger Wunsch erm\u00f6glicht den Angriff auf Nidhoggs rechtes Auge.", - "Description_en": "A beloved friend is making it possible to attack Nidhogg's right eye.", - "Description_fr": "Un ami cher permet d'attaquer l'\u0153il droit de Nidhogg.", - "Description_ja": "\u76df\u53cb\u306b\u8a17\u3055\u308c\u305f\u60f3\u3044\u306b\u3088\u3063\u3066\u3001\u90aa\u7adc\u306e\u53f3\u773c\u3078\u306e\u653b\u6483\u304c\u53ef\u80fd\u306a\u72b6\u614b\u3002", - "Name_de": "Essenz der Freundschaft", - "Name_en": "Soul of Friendship", - "Name_fr": "Amiti\u00e9 \u00e9ternelle", - "Name_ja": "\u76df\u53cb\u306e\u60f3\u3044" - }, - "2749": { - "Description_de": "Eisherz' inniger Wunsch erm\u00f6glicht den Angriff auf Nidhoggs linkes Auge.", - "Description_en": "A faithful ally is making it possible to attack Nidhogg's left eye.", - "Description_fr": "C\u0153ur-de-glace permet d'attaquer l'\u0153il gauche de Nidhogg.", - "Description_ja": "\u6c37\u306e\u5deb\u5973\u306b\u8a17\u3055\u308c\u305f\u60f3\u3044\u306b\u3088\u3063\u3066\u3001\u90aa\u7adc\u306e\u5de6\u773c\u3078\u306e\u653b\u6483\u304c\u53ef\u80fd\u306a\u72b6\u614b\u3002", - "Name_de": "Essenz der Tugend", - "Name_en": "Soul of Devotion", - "Name_fr": "D\u00e9votion \u00e9ternelle", - "Name_ja": "\u5deb\u5973\u306e\u60f3\u3044" - }, - "2758": { - "Description_de": "Gefesselt von der Macht Nidhoggs unb\u00e4ndigen Durstes nach Vergeltung. ", - "Description_en": "Powerless against Nidhogg's desire for vengeance.", - "Description_fr": "La cible est assujettie par la volont\u00e9 de vengeance de Nidhogg.", - "Description_ja": "\u90aa\u7adc\u30cb\u30fc\u30ba\u30d8\u30c3\u30b0\u306e\u5fa9\u8b90\u3092\u671b\u3080\u5ff5\u306b\u7e1b\u3089\u308c\u305f\u72b6\u614b\u3002", - "Name_de": "Flammende Rache", - "Name_en": "Spreading Flames", - "Name_fr": "Vengeance consumante", - "Name_ja": "\u5fa9\u8b90\u306e\u708e" - }, - "2759": { - "Description_de": "Wehrlos gegen Nidhoggs Wunsch, sein Leid auch andere sp\u00fcren zu lassen.", - "Description_en": "Powerless against Nidhogg's desire that another share his suffering.", - "Description_fr": "La cible est assujettie par la volont\u00e9 d'enchev\u00eatrement de Nidhogg.", - "Description_ja": "\u90aa\u7adc\u30cb\u30fc\u30ba\u30d8\u30c3\u30b0\u306e\u9053\u9023\u308c\u3092\u671b\u3080\u5ff5\u306b\u7e1b\u3089\u308c\u305f\u72b6\u614b\u3002", - "Name_de": "Verwobene Flammen", - "Name_en": "Entangled Flames", - "Name_fr": "Flammes enchev\u00eatr\u00e9es", - "Name_ja": "\u9053\u9023\u308c\u306e\u708e" - }, - "2777": { - "Description_de": "Kein Zustandswechsel zwischen Nidhoggs Fang und Klaue m\u00f6glich. ", - "Description_en": "Unable to transition between Clawbound and Fangbound states.", - "Description_fr": "Les caract\u00e9ristiques de la griffe et du croc de Nidhogg sont immuables.", - "Description_ja": "\u90aa\u7adc\u306e\u722a\u3068\u90aa\u7adc\u306e\u7259\u306e\u6027\u8cea\u304c\u5909\u5316\u3057\u306a\u304f\u306a\u3063\u305f\u72b6\u614b\u3002", - "Name_de": "Nidhoggs Stigmata", - "Name_en": "Bound and Determined", - "Name_fr": "Stigmates de Nidhogg", - "Name_ja": "\u722a\u7259\u4e0d\u5909" - }, - "2895": { - "Description_de": "H\u00e4lt sein Versprechen an Shiva, kein fremdes Blut mehr zu vergie\u00dfen. ", - "Description_en": "Recognized under the oath Hraesvelgr swore to his beloved Shivathat he would never kill her kin.", - "Description_fr": "Hraesvelgr a promis \u00e0 Shiva de ne plus tuer d'humains.", - "Description_ja": "\u8056\u7adc\u30d5\u30ec\u30fc\u30b9\u30f4\u30a7\u30eb\u30b0\u304c\u3001\u611b\u3059\u308b\u30b7\u30f4\u30a1\u3092\u55b0\u3089\u3063\u305f\u969b\u306b\u7acb\u3066\u305f\u4e0d\u6bba\u306e\u8a93\u3044\u3002", - "Name_de": "Schwur des Friedens", - "Name_en": "Solemn Vow", - "Name_fr": "Serment de paix", - "Name_ja": "\u4e0d\u6bba\u306e\u8a93\u3044" - }, - "2896": { - "Description_de": "Unter Einfluss von Nidhoggs Schwur, sich an Ratatoskrs M\u00f6rdern zu r\u00e4chen. Erhaltene Heileffekte sind verringert und es wird schrittweise Schaden erlitten. Bei Ende des Effekts wird allen Umstehenden schlimmer Schmerz zugef\u00fcgt.", - "Description_en": "Condemned by Nidhogg's vow to avenge his brood-sister. Healing potency is decreased. Taking damage over time, and will inflict anguish on those nearby in turn when this effect expires.", - "Description_fr": "Cible de la haine de Nidhogg ayant pour cause la perte de Ratatosk. Les d\u00e9g\u00e2ts inflig\u00e9s et la puissance des effets curatifs prodigu\u00e9s sont r\u00e9duits, et des d\u00e9g\u00e2ts p\u00e9riodiques sont subis. Lorsque l'effet prend fin, une douleur atroce est inflig\u00e9e aux alentours.", - "Description_ja": "\u90aa\u7adc\u30cb\u30fc\u30ba\u30d8\u30c3\u30b0\u304c\u611b\u3059\u308b\u8a69\u7adc\u3092\u5931\u3063\u305f\u969b\u306b\u7acb\u3066\u305f\u6ec5\u6bba\u306e\u8a93\u3044\u3001\u305d\u306e\u5bfe\u8c61\u3068\u306a\u3063\u305f\u72b6\u614b\u3002\u81ea\u8eab\u304b\u3089\u306e\uff28\uff30\u56de\u5fa9\u52b9\u679c\u304c\u4f4e\u4e0b\u3057\u3001\u304b\u3064\uff28\uff30\u304c\u5f90\u3005\u306b\u5931\u308f\u308c\u308b\u3002\u307e\u305f\u52b9\u679c\u7d42\u4e86\u6642\u306b\u3001\u5468\u56f2\u306b\u82e6\u75db\u3092\u4e0e\u3048\u308b\u3002", - "Name_de": "Schwur der Vergeltung", - "Name_en": "Mortal Vow", - "Name_fr": "V\u0153u d'an\u00e9antissement", - "Name_ja": "\u6ec5\u6bba\u306e\u8a93\u3044" - }, - "2897": { - "Description_de": "Von Nidhoggs Schwur der Vergeltung befreit.", - "Description_en": "No longer condemned by Nidhogg's Mortal Vow.", - "Description_fr": "La cible est lib\u00e9r\u00e9e du v\u0153u d'an\u00e9antissement de Nidhogg.", - "Description_ja": "\u6ec5\u6bba\u306e\u8a93\u3044\u306e\u5bfe\u8c61\u304b\u3089\u5916\u308c\u305f\u72b6\u614b\u3002", - "Name_de": "Versiegte Vergeltung", - "Name_en": "Mortal Atonement", - "Name_fr": "V\u0153u d'an\u00e9antissement rompu", - "Name_ja": "\u6ec5\u6bba\u306e\u511f\u3044" - }, - "2977": { - "Description_de": "Erhaltene Heileffekte sind um 20 % verringert.", - "Description_en": "HP recovery is reduced by 20%.", - "Description_fr": "L'effet des sorts de restauration des PV est r\u00e9duit de 20%.", - "Description_ja": "\u81ea\u8eab\u306b\u5bfe\u3059\u308b\uff28\uff30\u56de\u5fa9\u52b9\u679c\u304c20\uff05\u4f4e\u4e0b\u3057\u305f\u72b6\u614b\u3002", - "Name_de": "Heilung -", - "Name_en": "HP Recovery Down", - "Name_fr": "Soins diminu\u00e9s", - "Name_ja": "\u88ab\u56de\u5fa9\u4f4e\u4e0b" - }, - "2978": { - "Description_de": "Erhaltene Heileffekte sind um 100 % verringert.", - "Description_en": "HP recovery is reduced by 100%.", - "Description_fr": "L'effet des sorts de restauration des PV est r\u00e9duit de 100%.", - "Description_ja": "\u81ea\u8eab\u306b\u5bfe\u3059\u308b\uff28\uff30\u56de\u5fa9\u52b9\u679c\u304c100\uff05\u4f4e\u4e0b\u3057\u305f\u72b6\u614b\u3002", - "Name_de": "Heilung -", - "Name_en": "HP Recovery Down", - "Name_fr": "Soins diminu\u00e9s", - "Name_ja": "\u88ab\u56de\u5fa9\u4f4e\u4e0b" - } - } -} - - */ public BooleanSetting getP5_thunderstruckAutoMarks() { return p5_thunderstruckAutoMarks; diff --git a/xivsupport/src/main/java/gg/xp/compmonitor/CompListener.java b/xivsupport/src/main/java/gg/xp/compmonitor/CompListener.java new file mode 100644 index 000000000000..2b61ceda0b1b --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/compmonitor/CompListener.java @@ -0,0 +1,8 @@ +package gg.xp.compmonitor; + +@FunctionalInterface +public interface CompListener { + + void added(InstantiatedItem item); + +} diff --git a/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java b/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java new file mode 100644 index 000000000000..d5b1fa889dc8 --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java @@ -0,0 +1,55 @@ +package gg.xp.compmonitor; + +import org.picocontainer.ComponentAdapter; +import org.picocontainer.PicoContainer; +import org.picocontainer.monitors.NullComponentMonitor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serial; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; + +public class CompMonitor extends NullComponentMonitor { + @Serial + private static final long serialVersionUID = -4049648364319492087L; + + private static final Logger log = LoggerFactory.getLogger(CompMonitor.class); + private final List> all = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private PicoContainer container; + + @Override + public void instantiated(PicoContainer container, ComponentAdapter componentAdapter, Constructor constructor, Object instantiated, Object[] injected, long duration) { + // ONLY apply to a single container + if (!checkContainer(container)) { + return; + } + InstantiatedItem inst = new InstantiatedItem<>(constructor.getDeclaringClass(), (T) instantiated); + all.add(inst); + listeners.forEach(listener -> listener.added(inst)); + if (duration >= 100) { + log.warn("CompMonitor: {}ms to instantiate {}", duration, instantiated); + } + } + + private boolean checkContainer(PicoContainer container) { + if (this.container == null) { + this.container = container; + return true; + } + else { + return this.container == container; + } + } + + public void addListener(CompListener listener) { + listeners.add(listener); + } + + public void addAndRunListener(CompListener listener) { + addListener(listener); + all.forEach(listener::added); + } +} diff --git a/xivsupport/src/main/java/gg/xp/compmonitor/FilteredCompListener.java b/xivsupport/src/main/java/gg/xp/compmonitor/FilteredCompListener.java new file mode 100644 index 000000000000..4c3208597e4f --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/compmonitor/FilteredCompListener.java @@ -0,0 +1,31 @@ +package gg.xp.compmonitor; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +public final class FilteredCompListener implements CompListener { + private final Class type; + private final Predicate> filter; + private final Consumer> action; + + public FilteredCompListener(Class type, Consumer> action) { + this.type = type; + this.filter = unused -> true; + this.action = action; + } + + public FilteredCompListener(Class type, Predicate> filter, Consumer> action) { + this.type = type; + this.filter = filter; + this.action = action; + } + + + @Override + public void added(InstantiatedItem item) { + if (type.isInstance(item.cls()) && filter.test((InstantiatedItem) item)) { + action.accept((InstantiatedItem) item); + } + } + +} diff --git a/xivsupport/src/main/java/gg/xp/compmonitor/InstantiatedItem.java b/xivsupport/src/main/java/gg/xp/compmonitor/InstantiatedItem.java new file mode 100644 index 000000000000..2897540d951a --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/compmonitor/InstantiatedItem.java @@ -0,0 +1,4 @@ +package gg.xp.compmonitor; + +public record InstantiatedItem(Class cls, X instance) { +} diff --git a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java new file mode 100644 index 000000000000..7719be855de1 --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java @@ -0,0 +1,115 @@ +package gg.xp.reevent.events; + +import gg.xp.compmonitor.CompMonitor; +import gg.xp.reevent.context.StateStore; +import gg.xp.reevent.scan.AutoHandler; +import gg.xp.reevent.scan.AutoHandlerConfig; +import gg.xp.reevent.scan.AutoScan; +import gg.xp.reevent.scan.HandleEvents; +import gg.xp.reevent.topology.Topology; +import gg.xp.reevent.topology.TopologyInfo; +import gg.xp.reevent.topology.TopologyProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MonitoringEventDistributor extends BasicEventDistributor implements TopologyProvider { + private static final Logger log = LoggerFactory.getLogger(MonitoringEventDistributor.class); + private final AutoScan scanner; + private final TopologyInfo topoInfo; + private final Object loadLock = new Object(); + private final Map, List>> eventClassMap = new HashMap<>(); + private final List> autoHandlers = new ArrayList<>(); + private final List> manualHandlers = new ArrayList<>(); + private volatile boolean dirty = true; + private Topology topology; + + public MonitoringEventDistributor(StateStore state, AutoScan scanner, TopologyInfo topoInfo, CompMonitor mon, AutoHandlerConfig config) { + super(state); + mon.addAndRunListener(item -> { + boolean dirty = false; + Object inst = item.instance(); + if (inst instanceof EventHandler eh) { + autoHandlers.add((EventHandler) eh); + dirty = true; + } + Class clazz = inst.getClass(); + Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(HandleEvents.class)) { + AutoHandler rawEvh = new AutoHandler(clazz, method, inst, config); + autoHandlers.add(rawEvh); + dirty = true; + } + } + if (dirty) { + this.dirty = true; + } + }); + this.scanner = scanner; + this.topoInfo = topoInfo; + topology = Topology.fromHandlers(Collections.emptyList(), this.topoInfo); + } + + @Override + public synchronized void registerHandler(EventHandler handler) { + manualHandlers.add(handler); + dirty = true; + } + + // TODO: this just doesn't work well until event sources are also auto-ified + // We get double events after reloading + public void reloadIfNeeded() { + if (!dirty) { + return; + } + scanner.doScanIfNeeded(); + handlers.clear(); + handlers.addAll(manualHandlers); + handlers.addAll(autoHandlers); + sortHandlers(); + topology = Topology.fromHandlers(new ArrayList<>(this.handlers), topoInfo); + dirty = false; + } + + @Override + protected void sortHandlers() { + super.sortHandlers(); + eventClassMap.clear(); + } + + @Override + protected List> getHandlersForEvent(Event event) { + Class eventClass = event.getClass(); + return eventClassMap.computeIfAbsent(eventClass, cls -> handlers.stream().filter(eh -> { + if (eh instanceof TypedEventHandler teh) { + return teh.getType().isAssignableFrom(eventClass); + } + else { + return true; + } + }).sorted(Comparator.comparing(EventHandler::getOrder)).toList()); + } + + @Override + public Topology getTopology() { + return topology; + } + + // TODO: is there a better place to put this? + @Override + public void acceptEvent(Event event) { + reloadIfNeeded(); + super.acceptEvent(event); + } + + +} diff --git a/xivsupport/src/main/java/gg/xp/services/ServiceSelectorGui.java b/xivsupport/src/main/java/gg/xp/services/ServiceSelectorGui.java index f13cac469d76..d0cb02790f68 100644 --- a/xivsupport/src/main/java/gg/xp/services/ServiceSelectorGui.java +++ b/xivsupport/src/main/java/gg/xp/services/ServiceSelectorGui.java @@ -52,7 +52,7 @@ else if (item instanceof ServiceHandle handle) { handle.setEnabled(); } else { - log.warn("Unknown item: {}", item); + log.warn("Unknown instance: {}", item); } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/cdsupport/CustomCooldownManager.java b/xivsupport/src/main/java/gg/xp/xivsupport/cdsupport/CustomCooldownManager.java index ee165b1757ec..c7e4e4463a3b 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/cdsupport/CustomCooldownManager.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/cdsupport/CustomCooldownManager.java @@ -1,9 +1,6 @@ package gg.xp.xivsupport.cdsupport; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import gg.xp.reevent.events.EventMaster; import gg.xp.reevent.scan.ScanMe; import gg.xp.xivsupport.persistence.PersistenceProvider; 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 3c4aa67a44c5..d3a56e4a3aef 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 @@ -1,6 +1,7 @@ package gg.xp.xivsupport.events.state; import gg.xp.reevent.context.SubState; +import gg.xp.reevent.scan.Alias; import gg.xp.xivdata.data.Job; import gg.xp.xivdata.data.XivMap; import gg.xp.xivdata.data.duties.*; @@ -19,6 +20,8 @@ import java.util.Map; import java.util.function.Predicate; +@Alias("xivState") +@Alias("state") public interface XivState extends SubState { // Note: can be null until we have all the required data, but this should only happen very early on in init XivPlayerCharacter getPlayer(); 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 f7e3b550e1a3..7c43abeefaf3 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,10 +1,12 @@ package gg.xp.xivsupport.events.state.combatstate; +import gg.xp.reevent.scan.Alias; import gg.xp.xivsupport.models.XivCombatant; import org.jetbrains.annotations.Nullable; import java.util.List; +@Alias("casts") public interface ActiveCastRepository { @Nullable CastTracker getCastFor(XivCombatant cbt); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java index f11b74e8d200..cc98aa358ef5 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/events/state/combatstate/StatusEffectRepository.java @@ -2,6 +2,7 @@ import gg.xp.reevent.events.Event; import gg.xp.reevent.events.EventContext; +import gg.xp.reevent.scan.Alias; import gg.xp.reevent.scan.HandleEvents; import gg.xp.xivdata.data.*; import gg.xp.xivsupport.events.actionresolution.SequenceIdTracker; @@ -37,6 +38,8 @@ import java.util.Set; import java.util.function.Predicate; +@Alias("buffs") +@Alias("statuses") public class StatusEffectRepository { private static final Logger log = LoggerFactory.getLogger(StatusEffectRepository.class); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/groovy/GroovyManager.java b/xivsupport/src/main/java/gg/xp/xivsupport/groovy/GroovyManager.java index 5658e68314f8..fbaf85f81dc9 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/groovy/GroovyManager.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/groovy/GroovyManager.java @@ -1,9 +1,11 @@ package gg.xp.xivsupport.groovy; +import gg.xp.compmonitor.CompMonitor; import gg.xp.reevent.events.Event; import gg.xp.reevent.events.EventContext; import gg.xp.reevent.events.EventMaster; import gg.xp.reevent.events.InitEvent; +import gg.xp.reevent.scan.Alias; import gg.xp.reevent.scan.AutoHandlerConfig; import gg.xp.reevent.scan.HandleEvents; import gg.xp.reevent.scan.ScanMe; @@ -27,6 +29,7 @@ import groovy.lang.Script; import groovy.transform.CompileStatic; import groovy.transform.TypeChecked; +import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.StringUtils; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.ImportCustomizer; @@ -48,6 +51,8 @@ import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; import static org.reflections.scanners.Scanners.SubTypes; @@ -224,29 +229,36 @@ public Binding makeBinding() { private Binding makeGlobalBinding() { Binding binding = new Binding(); binding.setVariable("globals", binding); + // TODO: external classes can't have annotations added here at the moment, so do them manually + binding.setVariable("pico", container); + binding.setVariable("container", container); + binding.setVariable("picoContainer", container); + binding.setVariable("log", scriptLogger); + + container.getComponent(CompMonitor.class).addAndRunListener(item -> { + Object instance = item.instance(); + String mainName = StringUtils.uncapitalize(instance.getClass().getSimpleName()); + binding.setVariable(mainName, instance); + Class itemCls = item.cls(); + List> ifaces = new ArrayList<>(ClassUtils.getAllInterfaces(itemCls)); + ifaces.addAll(ClassUtils.getAllSuperclasses(itemCls)); + ifaces.add(instance.getClass()); + for (Class iface : ifaces) { + Alias[] aliases = iface.getAnnotationsByType(Alias.class); + for (Alias alias : aliases) { + String aliasName = alias.value(); + binding.setVariable(aliasName, instance); + } + } + }); + return binding; } @HandleEvents(order = -10_000_000) public void finishInit(EventContext context, InitEvent init) { - Binding binding = getGlobalBinding(); - container.getComponents().forEach(item -> { - String simpleName = item.getClass().getSimpleName(); - simpleName = StringUtils.uncapitalize(simpleName); - binding.setProperty(simpleName, item); - }); - // TODO: find a way to systematically do these exceptions - // TODO: can't these be in makeGlobalBinding? - binding.setVariable("pico", container); - binding.setVariable("container", container); - binding.setVariable("picoContainer", container); - binding.setVariable("xivState", container.getComponent(XivState.class)); - binding.setVariable("state", container.getComponent(XivState.class)); - binding.setVariable("master", container.getComponent(EventMaster.class)); - binding.setVariable("buffs", container.getComponent(StatusEffectRepository.class)); - binding.setVariable("casts", container.getComponent(ActiveCastRepository.class)); - binding.setVariable("log", scriptLogger); // Precache some common calls + getGlobalBinding(); new Thread("GroovyStartupHelper") { @Override public void run() { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java index 509fd14d9922..eceda59922d4 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/map/MapTab.java @@ -25,6 +25,8 @@ import java.util.Map; import java.util.stream.Collectors; +// TODO: introduce another layer here so that the tab can load async +// This tab takes about 1/4 second to load @ScanMe public class MapTab extends JPanel { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/nav/GlobalUiRegistry.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/nav/GlobalUiRegistry.java index 35d6b4657c82..ef6beacd78ce 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/nav/GlobalUiRegistry.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/nav/GlobalUiRegistry.java @@ -78,7 +78,7 @@ public List search(String searchTerm) { public boolean activateItem(Object key) { GuiRef guiRef = mapping.get(key); if (guiRef == null) { - log.warn("Did not find registered item for ({})!", key); + log.warn("Did not find registered instance for ({})!", key); return false; } else { diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/RefreshLoop.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/RefreshLoop.java index 89e829d9e55c..e419bbdca8ab 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/RefreshLoop.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/RefreshLoop.java @@ -25,7 +25,7 @@ public RefreshLoop(String threadNameStub, X item, Consumer periodicTask, Func try { X actualItem = this.item.get(); if (actualItem == null) { - log.info("Stopping refresh loop because refreshable item no longer exists"); + log.info("Stopping refresh loop because refreshable instance no longer exists"); return; } periodicTask.accept(actualItem); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomColumn.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomColumn.java index 21a3ad2aca16..bb7f57f3c0bc 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomColumn.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomColumn.java @@ -21,7 +21,7 @@ public CustomColumn(String columnName, Function getter) { * Custom table column * * @param columnName Name of the column - * @param getter Function to convert an item in the table to whatever this column cares about. Note that this + * @param getter Function to convert an instance in the table to whatever this column cares about. Note that this * is executed **in the table rendering code** so it should under no circumstances involve * non-trivial computation or access. * @param columnConfigurer Lets you configure the column, e.g. to override the renderer. diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomTableModel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomTableModel.java index 0dff11ddba19..2455cabf71ea 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomTableModel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/CustomTableModel.java @@ -137,14 +137,14 @@ public void configureColumns(JTable table) { } } - // public void refreshItem(X item) { + // public void refreshItem(X instance) { // JTable table = getTable(); // if (table == null) { // data = dataGetter.get(); // fireTableDataChanged(); // } // else { -// int oldIndex = data.indexOf(item); +// int oldIndex = data.indexOf(instance); // ListSelectionModel selectionModel = table.getSelectionModel(); // int[] oldSelectionIndices = selectionModel.getSelectedIndices(); // List oldSelections = Arrays.stream(oldSelectionIndices) @@ -161,7 +161,7 @@ public void configureColumns(JTable table) { // log.warn("Slow Data Getter performance: took {}ms to refresh", delta); // } // } -// int newIndex = data.indexOf(item); +// int newIndex = data.indexOf(instance); // // TODO: more optimizations could be done in XivState to only report changed combatants // if (oldIndex == newIndex && oldIndex >= 0) { // fireTableRowsUpdated(oldIndex, newIndex); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/TableWithFilterAndDetails.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/TableWithFilterAndDetails.java index 2e2c4905a41a..ca5668787126 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/TableWithFilterAndDetails.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/TableWithFilterAndDetails.java @@ -342,7 +342,7 @@ private void updateFiltering() { Integer offset = mainModel.getSelectedItemViewportOffsetIfVisible(); filterFully(); updateModel(); - // Only scroll back to selected item if auto scroll is disabled + // Only scroll back to selected instance if auto scroll is disabled if (scroller != null && !scroller.isAutoScrollEnabled() && offset != null) { mainModel.setVisibleItemScrollOffset(offset); log.info("Offset: {}", offset); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/GroovyFilter.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/GroovyFilter.java index b0ecb3d41570..45c30d5ef720 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/GroovyFilter.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/GroovyFilter.java @@ -110,7 +110,7 @@ public boolean passesFilter(X item) { if (filterScript == null) { return true; } -// shell.setVariable(varName, item); +// shell.setVariable(varName, instance); boolean result; try { result = filterScript.test(item); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/TextBasedFilter.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/TextBasedFilter.java index 3f2a185d35b7..03c921da7d83 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/TextBasedFilter.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/TextBasedFilter.java @@ -111,16 +111,16 @@ private void update() { /** * Meant to be overridden. Pre-filter items in a smart way. Basically, allow this - * filter to specify whether it actually cares about a particular item. If there is - * a filter set, then any item that returns false when passed into this will fail + * filter to specify whether it actually cares about a particular instance. If there is + * a filter set, then any instance that returns false when passed into this will fail * automatically. If there is no filter set, everything will pass. *

* If there is no filter whatsoever, let everything pass. *

* If there is any filter, then apply this pre-filter. * - * @param item The item to filter - * @return whether to let the item proceed to the main filter + * @param item The instance to filter + * @return whether to let the instance proceed to the main filter */ protected boolean preFilter(X item) { return true; diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/VisualFilter.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/VisualFilter.java index edfa936f2e49..76791125ad0b 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/VisualFilter.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tables/filters/VisualFilter.java @@ -12,7 +12,7 @@ public interface VisualFilter { /** * Whether it passes the filter * - * @param item The item to filter + * @param item The instance to filter * @return Whether it passed */ boolean passesFilter(X item); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/PluginTopologyPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/PluginTopologyPanel.java index 18f933e79b9f..3028d8b5deeb 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/PluginTopologyPanel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/PluginTopologyPanel.java @@ -1,6 +1,6 @@ package gg.xp.xivsupport.gui.tabs; -import gg.xp.reevent.events.AutoEventDistributor; +import gg.xp.reevent.topology.TopologyProvider; import gg.xp.xivsupport.gui.TitleBorderFullsizePanel; import gg.xp.xivsupport.gui.components.ReadOnlyText; import gg.xp.xivsupport.gui.tree.TopologyTreeEditor; @@ -16,7 +16,7 @@ public class PluginTopologyPanel extends TitleBorderFullsizePanel { public PluginTopologyPanel(PicoContainer container) { super("Topology"); setLayout(new BorderLayout()); - JTree tree = new JTree(new TopologyTreeModel(container.getComponent(AutoEventDistributor.class))); + JTree tree = new JTree(new TopologyTreeModel(container.getComponent(TopologyProvider.class).getTopology())); TopologyTreeRenderer renderer = new TopologyTreeRenderer(); tree.setCellRenderer(renderer); tree.setCellEditor(new TopologyTreeEditor(tree)); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeModel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeModel.java index 715ecd832f77..03777dca3891 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeModel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeModel.java @@ -13,8 +13,8 @@ public class TopologyTreeModel implements TreeModel { private final Topology topology; - public TopologyTreeModel(AutoEventDistributor auto) { - topology = auto.getTopology(); + public TopologyTreeModel(Topology topology) { + this.topology = topology; } @Override diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeRenderer.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeRenderer.java index 9cd45432da47..da3b3c5a85bd 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeRenderer.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tree/TopologyTreeRenderer.java @@ -14,7 +14,7 @@ public class TopologyTreeRenderer implements TreeCellRenderer { @Override public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof TopoItem ti) { -// checkBox.setEnabled(item.canBeDisabled()); +// checkBox.setEnabled(instance.canBeDisabled()); return new CheckboxTreeNode(tree, ti, selected, expanded, leaf, row, hasFocus); } return defaultRenderer.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/models/ArenaSector.java b/xivsupport/src/main/java/gg/xp/xivsupport/models/ArenaSector.java index 433c73800a06..b9d119e70bd3 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/models/ArenaSector.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/models/ArenaSector.java @@ -75,7 +75,7 @@ public String getAbbreviation() { * r e.g. NE, NW == north; NE, SE == south; NE, SW == null *

* null will also be returned if the input is invalid, e.g. if the list size was not 2, - * or if one or more item was not an intercard. + * or if one or more instance was not an intercard. * * @param quadrants The two quadrants * @return The adjacent cardinal, or null they are opposites, or if the input is invalid. @@ -113,7 +113,7 @@ public String getAbbreviation() { * r e.g. W, N == NW; S, E == SE; W, E == null *

* null will also be returned if the input is invalid, e.g. if the list size was not 2, - * or if one or more item was not a cardinal. + * or if one or more instance was not a cardinal. * * @param cardinals The two cardinals * @return The adjacent intercardinal, or null they are opposites, or if the input is invalid. @@ -147,7 +147,7 @@ public String getAbbreviation() { /** * Like {@link #tryCombineTwoQuadrants(List)} (List)}, but returns a list. If they were combined, the list will - * contain the single combined item. Otherwise, returns the original input. + * contain the single combined instance. Otherwise, returns the original input. * @param quadrants The quadrants to combine. * @return The original input if no combination possible, otherwise the combination. */ diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/models/XivCombatant.java b/xivsupport/src/main/java/gg/xp/xivsupport/models/XivCombatant.java index 8095dd04e575..fb1640306153 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/models/XivCombatant.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/models/XivCombatant.java @@ -144,7 +144,7 @@ else if (rawType == 3) { * 6 = Gathering point? I got "Mature Tree" in here * 7 = Gardening patch? * 9 = Minion? - * 12 = Interactable housing item? + * 12 = Interactable housing instance? * * @return Raw type from ACT */ diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/sys/XivMain.java b/xivsupport/src/main/java/gg/xp/xivsupport/sys/XivMain.java index f548ff83dca2..6ccea0528d79 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/sys/XivMain.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/sys/XivMain.java @@ -1,12 +1,16 @@ package gg.xp.xivsupport.sys; +import gg.xp.compmonitor.CompMonitor; import gg.xp.reevent.events.AutoEventDistributor; import gg.xp.reevent.events.BasicEventDistributor; import gg.xp.reevent.events.BasicEventQueue; +import gg.xp.reevent.events.EventDistributor; import gg.xp.reevent.events.EventMaster; import gg.xp.reevent.events.InitEvent; +import gg.xp.reevent.events.MonitoringEventDistributor; import gg.xp.reevent.scan.AutoHandlerConfig; import gg.xp.reevent.scan.AutoHandlerScan; +import gg.xp.reevent.scan.AutoScan; import gg.xp.reevent.topology.TopoInfoImpl; import gg.xp.xivdata.data.ActionLibrary; import gg.xp.xivdata.data.StatusEffectLibrary; @@ -17,7 +21,6 @@ import gg.xp.xivsupport.persistence.InMemoryMapPersistenceProvider; import gg.xp.xivsupport.persistence.PersistenceProvider; import gg.xp.xivsupport.persistence.Platform; -import gg.xp.xivsupport.persistence.PropertiesFilePersistenceProvider; import gg.xp.xivsupport.persistence.UserDirPropsPersistenceProvider; import groovy.lang.GroovyShell; import org.picocontainer.MutablePicoContainer; @@ -46,14 +49,19 @@ public static void main(String[] args) { // Just the required stuff, doesn't start anything private static MutablePicoContainer requiredComponents() { log.info("Assembling required components"); + CompMonitor cm = new CompMonitor(); MutablePicoContainer pico = new PicoBuilder() .withCaching() .withLifecycle() .withAutomatic() + .withMonitor(cm) .build(); - pico.addComponent(AutoEventDistributor.class); + pico.addComponent(cm); + pico.addComponent(MonitoringEventDistributor.class); + pico.addComponent(AutoScan.class); +// pico.addComponent(AutoEventDistributor.class); +// pico.addComponent(AutoHandlerScan.class); pico.addComponent(AutoHandlerConfig.class); - pico.addComponent(AutoHandlerScan.class); pico.addComponent(EventMaster.class); pico.addComponent(BasicEventQueue.class); pico.addComponent(PicoStateStore.class); @@ -150,7 +158,8 @@ public static MutablePicoContainer masterInit() { }, "StartupHelper").start(); // TODO: use "Startable" interface? - AutoEventDistributor dist = pico.getComponent(AutoEventDistributor.class); +// AutoEventDistributor dist = pico.getComponent(AutoEventDistributor.class); + EventDistributor dist = pico.getComponent(EventDistributor.class); log.info("Init start"); dist.acceptEvent(new InitEvent()); pico.getComponent(EventMaster.class).start(); diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/groovy/GroovyAliasesTest.java b/xivsupport/src/test/java/gg/xp/xivsupport/groovy/GroovyAliasesTest.java new file mode 100644 index 000000000000..b24b3017fd08 --- /dev/null +++ b/xivsupport/src/test/java/gg/xp/xivsupport/groovy/GroovyAliasesTest.java @@ -0,0 +1,34 @@ +package gg.xp.xivsupport.groovy; + +import gg.xp.reevent.events.EventDistributor; +import gg.xp.reevent.events.InitEvent; +import gg.xp.xivsupport.sys.XivMain; +import groovy.lang.Binding; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.picocontainer.MutablePicoContainer; +import org.testng.annotations.Test; + +public class GroovyAliasesTest { + + @Test + void testAliases() { + MutablePicoContainer pico = XivMain.testingMasterInit(); + pico.getComponent(EventDistributor.class).acceptEvent(new InitEvent()); + GroovyManager gm = pico.getComponent(GroovyManager.class); + Binding binding = gm.makeBinding(); + MatcherAssert.assertThat(binding.getVariable("pico"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("container"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("picoContainer"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("state"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("xivState"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("master"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("buffs"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("statuses"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("casts"), Matchers.notNullValue()); + MatcherAssert.assertThat(binding.getVariable("log"), Matchers.notNullValue()); + // Test normal stuff too + MatcherAssert.assertThat(binding.getVariable("statusEffectRepository"), Matchers.notNullValue()); + } + +} From 3fea0a5bb2855c186da9f7e9803f1b60975365fe Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 09:23:50 -0700 Subject: [PATCH 02/15] Run on startup request --- .idea/codeStyles/Project.xml | 3 ++ .../groovy/GroovyScriptManager.java | 5 ++- .../xp/xivsupport/gui/groovy/GroovyPanel.java | 41 ++++++++++++++++++- .../gui/groovy/GroovyScriptHolder.java | 8 +++- .../gui/groovy/ScriptSettingsControl.java | 12 ++++++ .../src/main/resources/te_changelog.html | 28 ++++++++++--- 6 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 xivsupport/src/main/java/gg/xp/xivsupport/gui/groovy/ScriptSettingsControl.java diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7eb17d4bd0c0..612a8fcd6f46 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,6 +1,9 @@

Unreleased

+
    +
  • You can put +
    scriptSettings.requestRunOnStartup()
    + in a script to have it automatically request to be run on startup the first time it is run. This should + make it easier to share scripts that are intended to run on startup. +
  • +
+

Sep 3, 2023

  • Timelines: Support force jumps and labels.
@@ -11,7 +19,9 @@

July 7, 2023

June 29, 2023

    -
  • Added option the party CD tracker to track enemy-targeted CDs (such as Chain or Mug) from out-of-party players.
  • +
  • Added option the party CD tracker to track enemy-targeted CDs (such as Chain or Mug) from out-of-party + players. +
  • Added Chaotic Spring to DoT tracker.

June 22, 2023

@@ -26,7 +36,9 @@

June 11, 2023

June 9, 2023

  • More Anabaseios triggers.
  • -
  • You can hover over the text of a Callout event to see more information, including variables and, for built-in triggers, where it originated.
  • +
  • You can hover over the text of a Callout event to see more information, including variables and, for + built-in triggers, where it originated. +

June 4, 2023

    @@ -43,7 +55,9 @@

    June 1, 2023

May 31, 2023

    -
  • Added support for RSV lines - after zoning in or replaying a log, _rsv actions/buffs for that fight will show their true names instead.
  • +
  • Added support for RSV lines - after zoning in or replaying a log, _rsv actions/buffs for that fight will + show their true names instead. +
  • Performance improvements under heavy load.
  • More P9S and P10S triggers.
@@ -95,7 +109,9 @@

Apr 25, 2023

Apr 10, 2023

    -
  • Added a 'Conga' button to job prio (AM) GUIs - line up in a west-east conga line and click the button to set priority.
  • +
  • Added a 'Conga' button to job prio (AM) GUIs - line up in a west-east conga line and click the button to + set priority. +
  • Added a 'check only' button on the updates tab.
  • Minor improvements to how certain events are displayed in the events table.
@@ -115,7 +131,9 @@

Apr 4, 2023

Mar 30, 2023

  • Redesigned the front page, moved some things onto the new "Summary" tab.
  • -
  • Added theme switcher. There are still a few unintended interactions with overlays, but it works apart from that.
  • +
  • Added theme switcher. There are still a few unintended interactions with overlays, but it works + apart from that. +
  • Made a discord server. Click the button in the top-right.

Mar 29, 2023

From 479ebd1726d3b3229f2938b425e3cfb8ed108434 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 10:25:58 -0700 Subject: [PATCH 03/15] Actions for waiting until a buff/cast duration falls below a certain remaining duration --- .../triggers/easytriggers/EasyTriggers.java | 5 + .../actions/WaitBuffDurationAction.java | 72 +++ .../actions/WaitCastDurationAction.java | 41 ++ .../easytriggers/EasyTriggersTest.java | 476 ++++++++++++------ .../src/main/resources/te_changelog.html | 4 + 5 files changed, 445 insertions(+), 153 deletions(-) create mode 100644 easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitBuffDurationAction.java create mode 100644 easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitCastDurationAction.java diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggers.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggers.java index a508da636ac4..be3559315d1b 100644 --- a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggers.java +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggers.java @@ -50,6 +50,8 @@ import gg.xp.xivsupport.events.triggers.easytriggers.actions.GroovyAction; import gg.xp.xivsupport.events.triggers.easytriggers.actions.SoundAction; import gg.xp.xivsupport.events.triggers.easytriggers.actions.WaitAction; +import gg.xp.xivsupport.events.triggers.easytriggers.actions.WaitBuffDurationAction; +import gg.xp.xivsupport.events.triggers.easytriggers.actions.WaitCastDurationAction; import gg.xp.xivsupport.events.triggers.easytriggers.actions.gui.ConditionalActionEditor; import gg.xp.xivsupport.events.triggers.easytriggers.actions.gui.GroovyActionEditor; import gg.xp.xivsupport.events.triggers.easytriggers.actions.gui.SoundActionEditor; @@ -427,12 +429,15 @@ private Component generic(Object object, Object trigger) { new ConditionDescription<>(ZoneIdFilter.class, Object.class, "Restrict the Zone ID in which this trigger may run", () -> new ZoneIdFilter(inject(XivState.class)), this::generic) )); + // XXX - DO NOT CHANGE NAMES OF THESE CLASSES OR PACKAGE PATH - FQCN IS PART OF DESERIALIZATION!!! private final List> actions = new ArrayList<>(List.of( new ActionDescription<>(CalloutAction.class, Event.class, "Basic TTS/Text Callout", CalloutAction::new, (callout, trigger) -> new CalloutActionPanel(callout)), new ActionDescription<>(DurationBasedCalloutAction.class, HasDuration.class, "Duration-Based TTS/Text Callout", DurationBasedCalloutAction::new, (callout, trigger) -> new CalloutActionPanel(callout)), new ActionDescription<>(AutoMarkTargetAction.class, HasTargetEntity.class, "Mark The Target", () -> new AutoMarkTargetAction(inject(GlobalUiRegistry.class)), this::generic), new ActionDescription<>(ClearAllMarksAction.class, Event.class, "Clear All Marks", () -> new ClearAllMarksAction(inject(GlobalUiRegistry.class)), this::generic), new ActionDescription<>(WaitAction.class, BaseEvent.class, "Wait a fixed time", WaitAction::new, this::generic), + new ActionDescription<>(WaitBuffDurationAction.class, BuffApplied.class, "Wait until buff duration falls below a specified amount", () -> new WaitBuffDurationAction(inject(StatusEffectRepository.class)), this::generic), + new ActionDescription<>(WaitCastDurationAction.class, AbilityCastStart.class, "Wait until cast duration falls below a specified amount", WaitCastDurationAction::new, this::generic), new ActionDescription<>(GroovyAction.class, Event.class, "Custom script action", () -> new GroovyAction(inject(GroovyManager.class)), (action, trigger) -> new GroovyActionEditor<>(action, trigger)), // (ActionDescription, BaseEvent>) new ActionDescription<>(ConditionalAction.class, BaseEvent.class, "If/Else Conditional Action", ConditionalAction::new, (action, trigger) -> new ConditionalActionEditor(this, action)), new ActionDescription<>(SoundAction.class, Event.class, "Play Sound", SoundAction::new, (action, trigger) -> new SoundActionEditor(inject(SoundFilesManager.class), inject(SoundFileTab.class), action)), diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitBuffDurationAction.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitBuffDurationAction.java new file mode 100644 index 000000000000..8cba08464743 --- /dev/null +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitBuffDurationAction.java @@ -0,0 +1,72 @@ +package gg.xp.xivsupport.events.triggers.easytriggers.actions; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.OptBoolean; +import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; +import gg.xp.xivsupport.events.actlines.events.BuffApplied; +import gg.xp.xivsupport.events.actlines.events.HasStatusEffect; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; +import gg.xp.xivsupport.events.triggers.easytriggers.conditions.Description; +import gg.xp.xivsupport.events.triggers.easytriggers.model.EasyTriggerContext; +import gg.xp.xivsupport.events.triggers.easytriggers.model.SqAction; +import gg.xp.xivsupport.events.triggers.marks.gui.AutoMarkGui; +import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerController; +import gg.xp.xivsupport.gui.nav.GlobalUiRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WaitBuffDurationAction implements SqAction { + + private static final Logger log = LoggerFactory.getLogger(WaitBuffDurationAction.class); + + @JsonIgnore + private final StatusEffectRepository buffs; + + public WaitBuffDurationAction(@JacksonInject(useInput = OptBoolean.FALSE) StatusEffectRepository buffs) { + this.buffs = buffs; + } + + @Description("Remaining Duration") + public long remainingDurationMs = 1000; + @Description("Stop Trigger if Buff Removed") + public boolean stopIfGone; + + @Override + public String fixedLabel() { + return "Wait Until Buff Duration Below"; + } + + @Override + public String dynamicLabel() { + return "Wait until remaining cast duration <= %sms".formatted(remainingDurationMs); + } + + @Override + public void accept(SequentialTriggerController stc, EasyTriggerContext context, BuffApplied event) { + while (true) { + BuffApplied latest = buffs.getLatest(event); + if (latest == null) { + if (stopIfGone) { + context.setStopProcessing(true); + } + return; + } + else { + // TODO: this does not handle the case of a buff being replaced with one of a shorter duration + long msToWait = latest.getEstimatedRemainingDuration().minusMillis(remainingDurationMs).toMillis(); + if (msToWait > 0) { + stc.waitMs(msToWait); + } + else { + return; + } + } + } + } + + @Override + public void accept(EasyTriggerContext context, BuffApplied event) { + // Handled above + } +} diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitCastDurationAction.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitCastDurationAction.java new file mode 100644 index 000000000000..831703de6392 --- /dev/null +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/actions/WaitCastDurationAction.java @@ -0,0 +1,41 @@ +package gg.xp.xivsupport.events.triggers.easytriggers.actions; + +import gg.xp.reevent.events.BaseEvent; +import gg.xp.xivsupport.events.actlines.events.AbilityCastStart; +import gg.xp.xivsupport.events.triggers.easytriggers.conditions.Description; +import gg.xp.xivsupport.events.triggers.easytriggers.model.EasyTriggerContext; +import gg.xp.xivsupport.events.triggers.easytriggers.model.SqAction; +import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WaitCastDurationAction implements SqAction { + + private static final Logger log = LoggerFactory.getLogger(WaitCastDurationAction.class); + + @Description("Remaining Duration") + public long remainingDurationMs = 1000; + + @Override + public String fixedLabel() { + return "Wait Until Cast Duration Below"; + } + + @Override + public String dynamicLabel() { + return "Wait until remaining cast duration <= %sms".formatted(remainingDurationMs); + } + + @Override + public void accept(SequentialTriggerController stc, EasyTriggerContext context, AbilityCastStart event) { + long msToWait = event.getEstimatedRemainingDuration().minusMillis(remainingDurationMs).toMillis(); + if (msToWait > 0) { + stc.waitMs(msToWait); + } + } + + @Override + public void accept(EasyTriggerContext context, AbilityCastStart event) { + // Handled above + } +} diff --git a/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersTest.java b/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersTest.java index 20e546f90c53..c483e42f239f 100644 --- a/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersTest.java +++ b/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersTest.java @@ -5,6 +5,7 @@ import gg.xp.reevent.events.EventDistributor; import gg.xp.reevent.events.EventMaster; import gg.xp.reevent.events.TestEventCollector; +import gg.xp.xivdata.data.*; import gg.xp.xivsupport.events.ACTLogLineEvent; import gg.xp.xivsupport.events.ExampleSetup; import gg.xp.xivsupport.events.actlines.events.AbilityCastCancel; @@ -19,8 +20,11 @@ import gg.xp.xivsupport.events.actlines.events.ZoneChangeEvent; import gg.xp.xivsupport.events.actlines.parsers.FakeTimeSource; import gg.xp.xivsupport.events.misc.EchoEvent; +import gg.xp.xivsupport.events.state.combatstate.StatusEffectRepository; import gg.xp.xivsupport.events.triggers.easytriggers.actions.CalloutAction; import gg.xp.xivsupport.events.triggers.easytriggers.actions.GroovyAction; +import gg.xp.xivsupport.events.triggers.easytriggers.actions.WaitBuffDurationAction; +import gg.xp.xivsupport.events.triggers.easytriggers.actions.WaitCastDurationAction; import gg.xp.xivsupport.events.triggers.easytriggers.conditions.AbilityIdFilter; import gg.xp.xivsupport.events.triggers.easytriggers.conditions.ChatLineRegexFilter; import gg.xp.xivsupport.events.triggers.easytriggers.conditions.GroovyEventFilter; @@ -87,6 +91,7 @@ public Duration getEstimatedRemainingDuration() { // TODO: why doesn't parallel work here? Everything should have its own instances, so thread safety should be // completely irrelevant. + // Probably because they share instances of the example events... @DataProvider(parallel = false) private Object[] testCases() { return new TestCase[]{ @@ -218,61 +223,56 @@ void simpleTest() { @Test void extraVarsTest() { - PersistenceProvider pers; + MutablePicoContainer pico = ExampleSetup.setup(); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); { - MutablePicoContainer pico = ExampleSetup.setup(); - pers = pico.getComponent(PersistenceProvider.class); - TestEventCollector coll = new TestEventCollector(); - EventMaster master = pico.getComponent(EventMaster.class); - { - EventDistributor dist = pico.getComponent(EventDistributor.class); - dist.registerHandler(coll); - } - EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); - EasyTrigger trig1 = new EasyTrigger<>(); - AbilityIdFilter cond = new AbilityIdFilter(); - cond.operator = NumericOperator.EQ; - cond.expected = 123; - trig1.setEventType(AbilityUsedEvent.class); - trig1.addCondition(cond); - GroovyManager groovy = pico.getComponent(GroovyManager.class); - GroovyAction action = new GroovyAction(groovy); - action.setGroovyScript("s.waitMs(400); s.accept(new TtsRequest(String.valueOf(context instanceof gg.xp.reevent.events.EventContext))); globals.foo = 'bar'"); - trig1.addAction(action); - ez1.addTrigger(trig1); - - // TODO: make another version of this test with a fake time source - master.pushEventAndWait(abilityUsed1); - { - List calls = coll.getEventsOf(TtsRequest.class); - Assert.assertEquals(calls.size(), 0); - } - try { - Thread.sleep(500); - master.pushEventAndWait(new AbilityUsedEvent(otherAbility, caster, target, Collections.emptyList(), 123, 0, 1)); - Thread.sleep(100); - // Workaround due to slowness... - for (int i = 0; i < 10; i++) { - Thread.sleep(2000); - if (!coll.getEventsOf(TtsRequest.class).isEmpty()) { - break; - } - master.pushEventAndWait(new BaseEvent() { - }); + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + AbilityIdFilter cond = new AbilityIdFilter(); + cond.operator = NumericOperator.EQ; + cond.expected = 123; + trig1.setEventType(AbilityUsedEvent.class); + trig1.addCondition(cond); + GroovyManager groovy = pico.getComponent(GroovyManager.class); + GroovyAction action = new GroovyAction(groovy); + action.setGroovyScript("s.waitMs(400); s.accept(new TtsRequest(String.valueOf(context instanceof gg.xp.reevent.events.EventContext))); globals.foo = 'bar'"); + trig1.addAction(action); + ez1.addTrigger(trig1); + + master.pushEventAndWait(abilityUsed1); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + try { + Thread.sleep(500); + master.pushEventAndWait(new AbilityUsedEvent(otherAbility, caster, target, Collections.emptyList(), 123, 0, 1)); + Thread.sleep(100); + // Workaround due to slowness... + for (int i = 0; i < 10; i++) { + Thread.sleep(2000); + if (!coll.getEventsOf(TtsRequest.class).isEmpty()) { + break; } + master.pushEventAndWait(new BaseEvent() { + }); } - catch (InterruptedException e) { - throw new RuntimeException(e); - } + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } - { - log.info("Done"); - List calls = coll.getEventsOf(TtsRequest.class); - Assert.assertEquals(calls.size(), 1); - TtsRequest theCall = calls.get(0); - Assert.assertEquals(theCall.getTtsString(), "true"); - Assert.assertEquals(groovy.makeBinding().getVariable("foo"), "bar"); - } + { + log.info("Done"); + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 1); + TtsRequest theCall = calls.get(0); + Assert.assertEquals(theCall.getTtsString(), "true"); + Assert.assertEquals(groovy.makeBinding().getVariable("foo"), "bar"); } // Now load the serialized version and make sure it all still works @@ -280,57 +280,51 @@ void extraVarsTest() { @Test void extraVarsTestFakeTimeSource() { - PersistenceProvider pers; + MutablePicoContainer pico = ExampleSetup.setup(); + pico.getComponent(FakeTimeSource.class); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); { - MutablePicoContainer pico = ExampleSetup.setup(); - pico.getComponent(FakeTimeSource.class); - pers = pico.getComponent(PersistenceProvider.class); - TestEventCollector coll = new TestEventCollector(); - EventMaster master = pico.getComponent(EventMaster.class); - { - EventDistributor dist = pico.getComponent(EventDistributor.class); - dist.registerHandler(coll); - } - EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); - EasyTrigger trig1 = new EasyTrigger<>(); - AbilityIdFilter cond = new AbilityIdFilter(); - cond.operator = NumericOperator.EQ; - cond.expected = 123; - trig1.setEventType(AbilityUsedEvent.class); - trig1.addCondition(cond); - GroovyManager groovy = pico.getComponent(GroovyManager.class); - GroovyAction action = new GroovyAction(groovy); - action.setGroovyScript("s.waitMs(400); s.accept(new TtsRequest(String.valueOf(context instanceof gg.xp.reevent.events.EventContext))); globals.foo = 'bar'"); - trig1.addAction(action); - ez1.addTrigger(trig1); - - FakeTimeSource fts = new FakeTimeSource(); - Instant now = Instant.now(); - fts.setNewTime(now); - // TODO: make another version of this test with a fake time source - AbilityUsedEvent abilityUsed1 = new AbilityUsedEvent(matchingAbility, caster, target, Collections.emptyList(), 123, 0, 1); - abilityUsed1.setTimeSource(fts); - abilityUsed1.setHappenedAt(fts.now()); - master.pushEventAndWait(abilityUsed1); - { - List calls = coll.getEventsOf(TtsRequest.class); - Assert.assertEquals(calls.size(), 0); - } - AbilityUsedEvent abilityUsed2 = new AbilityUsedEvent(otherAbility, caster, target, Collections.emptyList(), 123, 0, 1); - fts.setNewTime(now.plusMillis(500)); - abilityUsed2.setHappenedAt(fts.now()); - abilityUsed2.setTimeSource(fts); - master.pushEventAndWait(abilityUsed2); + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + AbilityIdFilter cond = new AbilityIdFilter(); + cond.operator = NumericOperator.EQ; + cond.expected = 123; + trig1.setEventType(AbilityUsedEvent.class); + trig1.addCondition(cond); + GroovyManager groovy = pico.getComponent(GroovyManager.class); + GroovyAction action = new GroovyAction(groovy); + action.setGroovyScript("s.waitMs(400); s.accept(new TtsRequest(String.valueOf(context instanceof gg.xp.reevent.events.EventContext))); globals.foo = 'bar'"); + trig1.addAction(action); + ez1.addTrigger(trig1); + + FakeTimeSource fts = new FakeTimeSource(); + Instant now = Instant.now(); + fts.setNewTime(now); + AbilityUsedEvent abilityUsed1 = new AbilityUsedEvent(matchingAbility, caster, target, Collections.emptyList(), 123, 0, 1); + abilityUsed1.setTimeSource(fts); + abilityUsed1.setHappenedAt(fts.now()); + master.pushEventAndWait(abilityUsed1); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + AbilityUsedEvent abilityUsed2 = new AbilityUsedEvent(otherAbility, caster, target, Collections.emptyList(), 123, 0, 1); + fts.setNewTime(now.plusMillis(500)); + abilityUsed2.setHappenedAt(fts.now()); + abilityUsed2.setTimeSource(fts); + master.pushEventAndWait(abilityUsed2); - { - List calls = coll.getEventsOf(TtsRequest.class); - Assert.assertEquals(calls.size(), 1); - TtsRequest theCall = calls.get(0); - Assert.assertEquals(theCall.getTtsString(), "true"); - Assert.assertEquals(groovy.makeBinding().getVariable("foo"), "bar"); - } + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 1); + TtsRequest theCall = calls.get(0); + Assert.assertEquals(theCall.getTtsString(), "true"); + Assert.assertEquals(groovy.makeBinding().getVariable("foo"), "bar"); } - // Now load the serialized version and make sure it all still works } @@ -487,58 +481,234 @@ void testGroovyTriggerSandboxViolationNoSbx() { Assert.assertTrue(gef.test(etc, new EchoEvent("foobar"))); } - // This test is obsolete, see EasyTriggersPersistenceTest2 -// @Test -// void testLegacyMigration() { -// PersistenceProvider pers; -// { -// MutablePicoContainer pico = ExampleSetup.setup(); -// pers = pico.getComponent(PersistenceProvider.class); -// TestEventCollector coll = new TestEventCollector(); -// EventDistributor dist = pico.getComponent(EventDistributor.class); -// dist.registerHandler(coll); -// EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); -// EasyTrigger trig1 = new EasyTrigger<>(); -// AbilityIdFilter cond = new AbilityIdFilter(); -// cond.operator = NumericOperator.EQ; -// cond.expected = 123; -// trig1.setEventType(AbilityUsedEvent.class); -// trig1.addCondition(cond); -// reflectionSetField(trig1, "text", "{event.getAbility().getId()}"); -// reflectionSetField(trig1, "tts", "{event.getAbility().getId()}"); -// ez1.addTrigger(trig1); -// -// dist.acceptEvent(abilityUsed2); -// dist.acceptEvent(zoneChange); -// dist.acceptEvent(abilityUsed1); -// -// { -// List calls = coll.getEventsOf(CalloutEvent.class); -// Assert.assertEquals(calls.size(), 1); -// CalloutEvent theCall = calls.get(0); -// Assert.assertEquals(theCall.getVisualText(), "123"); -// Assert.assertEquals(theCall.getCallText(), "123"); -// } -// } -// // Now load the serialized version and make sure it all still works -// -// { -// MutablePicoContainer pico = ExampleSetup.setup(pers); -// TestEventCollector coll = new TestEventCollector(); -// EventDistributor dist = pico.getComponent(EventDistributor.class); -// dist.registerHandler(coll); -// -// dist.acceptEvent(abilityUsed2); -// dist.acceptEvent(zoneChange); -// dist.acceptEvent(abilityUsed1); -// -// { -// List calls = coll.getEventsOf(CalloutEvent.class); -// Assert.assertEquals(calls.size(), 1); -// CalloutEvent theCall = calls.get(0); -// Assert.assertEquals(theCall.getVisualText(), "123"); -// Assert.assertEquals(theCall.getCallText(), "123"); -// } -// } -// } + @Test + void testCastDurationAction() { + MutablePicoContainer pico = ExampleSetup.setup(); + pico.getComponent(FakeTimeSource.class); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); + { + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + trig1.setEventType(AbilityCastStart.class); + WaitCastDurationAction action1 = new WaitCastDurationAction(); + action1.remainingDurationMs = 15_000; + CalloutAction action2 = new CalloutAction(); + action2.setText("Foo"); + action2.setTts("Bar"); + + trig1.addAction(action1); + trig1.addAction(action2); + ez1.addTrigger(trig1); + + FakeTimeSource fts = new FakeTimeSource(); + Instant startTime = Instant.now(); + fts.setNewTime(startTime); + + XivCombatant theCbt = new XivCombatant(0x1000_0001, "Foo"); + AbilityCastStart acs = new AbilityCastStart(new XivAbility(125), theCbt, theCbt, 18.0); + acs.setTimeSource(fts); + acs.setHappenedAt(fts.now()); + master.pushEventAndWait(acs); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(2_999)); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(3_001)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 1); + TtsRequest theCall = calls.get(0); + Assert.assertEquals(theCall.getTtsString(), "Bar"); + } + } + @Test + void testBuffDurationWaitAction() { + MutablePicoContainer pico = ExampleSetup.setup(); + pico.getComponent(FakeTimeSource.class); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); + { + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + trig1.setEventType(BuffApplied.class); + WaitBuffDurationAction action1 = new WaitBuffDurationAction(pico.getComponent(StatusEffectRepository.class)); + action1.remainingDurationMs = 15_000; + CalloutAction action2 = new CalloutAction(); + action2.setText("Foo"); + action2.setTts("Bar"); + + trig1.addAction(action1); + trig1.addAction(action2); + ez1.addTrigger(trig1); + + FakeTimeSource fts = new FakeTimeSource(); + Instant startTime = Instant.now(); + fts.setNewTime(startTime); + XivCombatant theCbt = new XivCombatant(0x1000_0001, "Foo"); + BuffApplied ba = new BuffApplied(new XivStatusEffect(0x9E), 18.0, theCbt, theCbt, 0); + ba.setTimeSource(fts); + ba.setHappenedAt(fts.now()); + master.pushEventAndWait(ba); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(2_999)); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(3_001)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 1); + TtsRequest theCall = calls.get(0); + Assert.assertEquals(theCall.getTtsString(), "Bar"); + } + } + + @Test + void testBuffDurationWaitActionRefresh() { + MutablePicoContainer pico = ExampleSetup.setup(); + pico.getComponent(FakeTimeSource.class); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); + { + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + trig1.setEventType(BuffApplied.class); + WaitBuffDurationAction action1 = new WaitBuffDurationAction(pico.getComponent(StatusEffectRepository.class)); + action1.remainingDurationMs = 15_000; + CalloutAction action2 = new CalloutAction(); + action2.setText("Foo"); + action2.setTts("Bar"); + + trig1.addAction(action1); + trig1.addAction(action2); + ez1.addTrigger(trig1); + + FakeTimeSource fts = new FakeTimeSource(); + Instant startTime = Instant.now(); + fts.setNewTime(startTime); + XivCombatant theCbt = new XivCombatant(0x1000_0001, "Foo"); + BuffApplied ba = new BuffApplied(new XivStatusEffect(0x9E), 18.0, theCbt, theCbt, 0); + ba.setTimeSource(fts); + ba.setHappenedAt(fts.now()); + master.pushEventAndWait(ba); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(2_999)); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + BuffApplied ba2 = new BuffApplied(new XivStatusEffect(0x9E), 18.0, theCbt, theCbt, 0); + ba2.setTimeSource(fts); + ba2.setHappenedAt(fts.now()); + master.pushEventAndWait(ba2); + fts.setNewTime(startTime.plusMillis(3_001)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(6_002)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 1); + TtsRequest theCall = calls.get(0); + Assert.assertEquals(theCall.getTtsString(), "Bar"); + } + } + + @Test + void testBuffDurationWaitActionCancel() { + MutablePicoContainer pico = ExampleSetup.setup(); + pico.getComponent(FakeTimeSource.class); + TestEventCollector coll = new TestEventCollector(); + EventMaster master = pico.getComponent(EventMaster.class); + { + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(coll); + } + EasyTriggers ez1 = pico.getComponent(EasyTriggers.class); + EasyTrigger trig1 = new EasyTrigger<>(); + trig1.setEventType(BuffApplied.class); + WaitBuffDurationAction action1 = new WaitBuffDurationAction(pico.getComponent(StatusEffectRepository.class)); + action1.remainingDurationMs = 15_000; + action1.stopIfGone = true; + CalloutAction action2 = new CalloutAction(); + action2.setText("Foo"); + action2.setTts("Bar"); + + trig1.addAction(action1); + trig1.addAction(action2); + ez1.addTrigger(trig1); + + FakeTimeSource fts = new FakeTimeSource(); + Instant startTime = Instant.now(); + fts.setNewTime(startTime); + XivCombatant theCbt = new XivCombatant(0x1000_0001, "Foo"); + BuffApplied ba = new BuffApplied(new XivStatusEffect(0x9E), 18.0, theCbt, theCbt, 0); + ba.setTimeSource(fts); + ba.setHappenedAt(fts.now()); + master.pushEventAndWait(ba); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(2_999)); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + BuffRemoved br = new BuffRemoved(new XivStatusEffect(0x9E), 18.0, theCbt, theCbt, 0); + br.setTimeSource(fts); + br.setHappenedAt(fts.now()); + master.pushEventAndWait(br); + fts.setNewTime(startTime.plusMillis(3_001)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + fts.setNewTime(startTime.plusMillis(6_002)); + // Need to push a dummy event due to fake time source + master.pushEventAndWait(new BaseEvent() { + }); + { + List calls = coll.getEventsOf(TtsRequest.class); + Assert.assertEquals(calls.size(), 0); + } + } } \ No newline at end of file diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 91d4e329a13b..2b5c5888cc11 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -1,5 +1,9 @@ +

Unreleased

+
    +
  • Easy Triggers: New actions to wait for a buff/cast remaining duration to be below a certain time. Useful for having a reminder of when a buff is about to expire.
  • +

Unreleased

  • Timelines: Support force jumps and labels.
  • From 8c1afd353c95f81d9ee5637bca86065e25057c07 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 13:58:03 -0700 Subject: [PATCH 04/15] Sequential and easy trigger concurrency modes --- .../easytriggers/gui/EasyTriggersTab.java | 14 +- .../easytriggers/model/EasyTrigger.java | 137 +++++++++++++---- .../EasyTriggersPersistenceTest2.java | 2 + .../triggers/seq/SequentialTrigger.java | 95 ++++++++++-- .../seq/SequentialTriggerConcurrencyMode.java | 36 +++++ .../seq/SequentialTriggerController.java | 19 +++ .../seq/SequentialTriggerPleaseDie.java | 22 +++ .../xp/xivsupport/groovy/GroovyTriggers.java | 33 +++- .../triggers/seq/SequentialTriggerTest.java | 145 +++++++++++++++++- 9 files changed, 457 insertions(+), 46 deletions(-) create mode 100644 trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerConcurrencyMode.java create mode 100644 trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerPleaseDie.java diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/gui/EasyTriggersTab.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/gui/EasyTriggersTab.java index a93eb425f521..8a684419c8ac 100644 --- a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/gui/EasyTriggersTab.java +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/gui/EasyTriggersTab.java @@ -11,10 +11,12 @@ import gg.xp.xivsupport.events.triggers.easytriggers.model.Condition; import gg.xp.xivsupport.events.triggers.easytriggers.model.EasyTrigger; import gg.xp.xivsupport.events.triggers.easytriggers.model.EventDescription; +import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerConcurrencyMode; import gg.xp.xivsupport.gui.GuiMain; import gg.xp.xivsupport.gui.TitleBorderFullsizePanel; import gg.xp.xivsupport.gui.extra.PluginTab; import gg.xp.xivsupport.gui.library.ChooserDialog; +import gg.xp.xivsupport.gui.lists.FriendlyNameListCellRenderer; import gg.xp.xivsupport.gui.nav.GlobalUiRegistry; import gg.xp.xivsupport.gui.overlay.RefreshLoop; import gg.xp.xivsupport.gui.tables.CustomColumn; @@ -398,7 +400,17 @@ private class TriggerConfigPanel extends JPanel { add(GuiUtil.labelFor("Event", eventTypeField), c); c.gridx++; add(eventTypeField, c); - + c.gridy++; + c.gridx = 0; + JComboBox concModeSelector = new JComboBox<>(SequentialTriggerConcurrencyMode.values()); + concModeSelector.setRenderer(new FriendlyNameListCellRenderer()); + concModeSelector.setSelectedItem(trigger.getConcurrency()); + concModeSelector.addItemListener(l -> { + trigger.setConcurrency((SequentialTriggerConcurrencyMode) concModeSelector.getSelectedItem()); + }); + add(GuiUtil.labelFor("Concurrency", concModeSelector), c); + c.gridx++; + add(concModeSelector, c); c.gridx = 0; c.gridy++; diff --git a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/model/EasyTrigger.java b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/model/EasyTrigger.java index fdd6200fb135..5390442f9e0f 100644 --- a/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/model/EasyTrigger.java +++ b/easytriggers/src/main/java/gg/xp/xivsupport/events/triggers/easytriggers/model/EasyTrigger.java @@ -7,6 +7,7 @@ import gg.xp.reevent.events.Event; import gg.xp.reevent.events.EventContext; import gg.xp.xivsupport.events.triggers.seq.SequentialTrigger; +import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerConcurrencyMode; import gg.xp.xivsupport.events.triggers.seq.SqtTemplates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,57 +31,62 @@ public class EasyTrigger implements HasMutableConditions, HasMutableAction @JsonProperty(defaultValue = "true") private boolean enabled = true; + @JsonProperty + private SequentialTriggerConcurrencyMode concurrency = SequentialTriggerConcurrencyMode.BLOCK_NEW; + private Class eventType = (Class) Event.class; private List> conditions = Collections.emptyList(); private List> actions = Collections.emptyList(); private String name = "Give me a name"; private int timeoutMs = 600_000; - // To account for the fact that the SQ might be recalculated while running, - // sqCurrent holds whatever is running, while sqBase holds the template - // TODO: unit test for this private SequentialTrigger sqBase = SqtTemplates.nothing(); - private SequentialTrigger sqCurrent = sqBase; private EasyTriggerContext ctx; public EasyTrigger() { - recalc(); + // TODO: unit test for updating trigger while running + recalcFully(); } public void handleEvent(EventContext context, Event event) { - if (!(event instanceof BaseEvent)) { - return; - } - if (sqCurrent.isActive()) { - sqCurrent.feed(context, (BaseEvent) event); - } - if (!enabled || eventType == null || !eventType.isInstance(event)) { + if (!(event instanceof BaseEvent) || !enabled || eventType == null) { return; } - X typedEvent = eventType.cast(event); ctx = new EasyTriggerContext(context, this); - if (conditions.stream().allMatch(cond -> cond.test(ctx, typedEvent))) { - hits++; - sqCurrent = sqBase; - sqCurrent.feed(context, (BaseEvent) event); - } - else { - misses++; - } + sqBase.feed(context, (BaseEvent) event); } - public void recalc() { - makeWritable(); - conditions.sort(Comparator.comparing(Condition::sortOrder)); - conditions.forEach(Condition::recalc); - actions.forEach(Action::recalc); + private boolean matchesStartCondition(X event) { + return conditions.stream().allMatch(cond -> cond.test(ctx, event)); + } + + private void recalcFully() { sqBase = SqtTemplates.sq(timeoutMs, eventType, // The start condition is handled externally - se -> true, + event -> { + X typedEvent = eventType.cast(event); + if (matchesStartCondition(typedEvent)) { + hits++; + return true; + } + else { + misses++; + return false; + } + }, (e1, s) -> { ctx.runActions((List) actions, s, (BaseEvent) e1); }); + recalc(); + } + + public void recalc() { + makeWritable(); + conditions.sort(Comparator.comparing(Condition::sortOrder)); + conditions.forEach(Condition::recalc); + actions.forEach(Action::recalc); + sqBase.setConcurrency(concurrency); Stream.concat(conditions.stream(), actions.stream()).forEach(item -> { if (item instanceof HasMutableEventType het) { het.setEventType(getEventType()); @@ -95,7 +101,7 @@ public Class getEventType() { public void setEventType(Class eventType) { this.eventType = eventType; - recalc(); + recalcFully(); } @Override @@ -142,6 +148,15 @@ public void removeCondition(Condition condition) { recalc(); } + public SequentialTriggerConcurrencyMode getConcurrency() { + return concurrency; + } + + public void setConcurrency(SequentialTriggerConcurrencyMode concurrency) { + this.concurrency = concurrency; + recalc(); + } + @Override public List> getActions() { return Collections.unmodifiableList(actions); @@ -211,4 +226,70 @@ public void setEnabled(boolean enabled) { // newTrigger.setConditions(new ArrayList<>(conditions)); // return newTrigger; // } +// public void handleEventOld(EventContext context, Event event) { +// if (!(event instanceof BaseEvent)) { +// return; +// } +// switch (concurrency) { +// // Mirrors standard sequential trigger logic +// case BLOCK_NEW -> { +// // Block new - if currently active, feed. +// if (sqCurrent.isActive()) { +// sqCurrent.feed(context, (BaseEvent) event); +// // TODO: shouldn't there be a 'return' right here? +// return; +// } +// if (!enabled || eventType == null || !eventType.isInstance(event)) { +// return; +// } +// X typedEvent = eventType.cast(event); +// ctx = new EasyTriggerContext(context, this); +// if (matchesStartCondition(typedEvent)) { +// hits++; +// sqCurrent = sqBase; +// sqCurrent.feed(context, (BaseEvent) event); +// } +// else { +// misses++; +// } +// } +// case REPLACE_OLD -> { +// if (!enabled || eventType == null || !eventType.isInstance(event)) { +// return; +// } +// X typedEvent = eventType.cast(event); +// ctx = new EasyTriggerContext(context, this); +// if (matchesStartCondition(typedEvent)) { +// hits++; +// if (sqCurrent != null && sqCurrent.isActive()) { +// sqCurrent.stopSilently(); +// } +// sqCurrent = sqBase; +// sqCurrent.feed(context, (BaseEvent) event); +// } +// else { +// if (sqCurrent.isActive()) { +// sqCurrent.feed(context, (BaseEvent) event); +// } +// misses++; +// } +// } +// case CONCURRENT -> { +// X typedEvent = eventType.cast(event); +// ctx = new EasyTriggerContext(context, this); +// var iter = sqMultiCurrent.iterator(); +// while (iter.hasNext()) { +// SequentialTrigger next = iter.next(); +// next.feed(context, (BaseEvent) event); +// if (!next.isActive()) { +// iter.remove(); +// } +// } +// if (matchesStartCondition(typedEvent)) { +// hits++; +// sqMultiCurrent.add() +// } +// } +// } +// } } diff --git a/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersPersistenceTest2.java b/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersPersistenceTest2.java index df70d9d7b857..331fcb1e7f77 100644 --- a/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersPersistenceTest2.java +++ b/easytriggers/src/test/java/gg/xp/xivsupport/events/triggers/easytriggers/EasyTriggersPersistenceTest2.java @@ -275,6 +275,7 @@ public Duration getEffectiveTimeSince() { [ { "enabled": true, + "concurrency": "BLOCK_NEW", "eventType": "gg.xp.xivsupport.events.actlines.events.AbilityCastStart", "conditions": [ { @@ -303,6 +304,7 @@ public Duration getEffectiveTimeSince() { }, { "enabled": true, + "concurrency": "BLOCK_NEW", "eventType": "gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent", "conditions": [ { diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTrigger.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTrigger.java index e094d4f04d02..469428281633 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTrigger.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTrigger.java @@ -5,16 +5,31 @@ import gg.xp.reevent.events.TypedEventHandler; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Predicate; +/** + * "Sequential Triggers" allow for a sequence of events to be collected interactively within a block of code. + * This massively simplifies the code for complex mechanic triggers, as the code for the entire sequence of events + * can live in one sequential trigger, rather than needing multiple "collector" triggers and having the logic spread + * out. + *

    + * It also simplifies cleanup, since all of your state can be kept as local variables rather than class fields. + * + * @param The event type. Should usually just be 'BaseEvent'. + */ public class SequentialTrigger implements TypedEventHandler { private @Nullable SequentialTriggerController instance; + private final List> instances = new ArrayList<>(); private final int timeoutMs; private Class type; private final Predicate startOn; private final BiConsumer> trigger; + private SequentialTriggerConcurrencyMode concurrency = SequentialTriggerConcurrencyMode.BLOCK_NEW; public SequentialTrigger(int timeoutMs, Class type, Predicate startOn, BiConsumer> trigger) { this.timeoutMs = timeoutMs; @@ -23,19 +38,56 @@ public SequentialTrigger(int timeoutMs, Class type, Predicate startOn, BiC this.trigger = trigger; } + /** + * Feed an event into the sequential trigger + * + * @param ctx The usual event context + * @param event The usual event + */ public void feed(EventContext ctx, X event) { if (!type.isInstance(event)) { return; } - if (instance == null) { - if (startOn.test(event)) { - instance = new SequentialTriggerController<>(ctx, event, trigger, timeoutMs); + switch (concurrency) { + case BLOCK_NEW -> { + if (instance == null) { + if (startOn.test(event)) { + instance = new SequentialTriggerController<>(ctx, event, trigger, timeoutMs); + } + } + else { + instance.provideEvent(ctx, event); + if (instance.isDone()) { + instance = null; + } + } } - } - else { - instance.provideEvent(ctx, event); - if (instance.isDone()) { - instance = null; + case REPLACE_OLD -> { + if (startOn.test(event)) { + if (instance != null) { + instance.stopSilently(); + } + instance = new SequentialTriggerController<>(ctx, event, trigger, timeoutMs); + } + if (instance != null) { + instance.provideEvent(ctx, event); + if (instance.isDone()) { + instance = null; + } + } + } + case CONCURRENT -> { + var iter = instances.iterator(); + while (iter.hasNext()) { + SequentialTriggerController next = iter.next(); + next.provideEvent(ctx, event); + if (next.isDone()) { + iter.remove(); + } + } + if (startOn.test(event)) { + instances.add(new SequentialTriggerController<>(ctx, event, trigger, timeoutMs)); + } } } } @@ -46,6 +98,18 @@ public void forceExpire() { inst.forceExpire(); instance = null; } + instances.forEach(SequentialTriggerController::forceExpire); + instances.clear(); + } + + public void stopSilently() { + SequentialTriggerController inst = instance; + if (inst != null) { + inst.stopSilently(); + instance = null; + } + instances.forEach(SequentialTriggerController::stopSilently); + instances.clear(); } @@ -60,6 +124,19 @@ public Class getType() { } public boolean isActive() { - return instance != null; + return instance != null || !instances.isEmpty(); + } + + /** + * Sets the concurrency policy. See {@link SequentialTriggerConcurrencyMode}. Should be set prior to any actual + * use of the trigger. + * + * @see SequentialTriggerConcurrencyMode + * @param concurrency The new concurrency policy. + * @return This (builder pattern) + */ + public SequentialTrigger setConcurrency(SequentialTriggerConcurrencyMode concurrency) { + this.concurrency = concurrency; + return this; } } diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerConcurrencyMode.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerConcurrencyMode.java new file mode 100644 index 000000000000..2c70c3c2f08c --- /dev/null +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerConcurrencyMode.java @@ -0,0 +1,36 @@ +package gg.xp.xivsupport.events.triggers.seq; + +import gg.xp.xivsupport.gui.util.HasFriendlyName; + +/** + * Determines what happens when a sequential trigger hits its start condition while it is already running. + */ +public enum SequentialTriggerConcurrencyMode implements HasFriendlyName { + + /** + * The default: Once the trigger has started, it cannot be started again until it has finished. Identical to the + * behavior before these settings were added. + */ + BLOCK_NEW("Do not allow new invocations while trigger is running"), + /** + * When the start condition is hit while the trigger is running, the old invocation will be killed and replaced + * with a new invocation. + */ + REPLACE_OLD("New invocation will stop the old invocation and replace it"), + /** + * Multiple invocations can run in parallel. + */ + CONCURRENT("Allow multiple concurrent invocations of this trigger"), + ; + + private final String friendlyName; + + SequentialTriggerConcurrencyMode(String friendlyName) { + this.friendlyName = friendlyName; + } + + @Override + public String getFriendlyName() { + return friendlyName; + } +} diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java index b9d86f6f29a4..ba66f74e7f13 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java @@ -46,6 +46,7 @@ public class SequentialTriggerController { private volatile boolean done; private volatile boolean processing = true; private volatile boolean die; + private volatile boolean dieSilently; private volatile boolean cycleProcessingTimeExceeded; private volatile @Nullable Predicate filter; private final Map params = new LinkedHashMap<>(); @@ -60,6 +61,9 @@ public SequentialTriggerController(EventContext initialEventContext, X initialEv try { triggerCode.accept(initialEvent, this); } + catch (SequentialTriggerPleaseDie e) { + log.info("Sequential Trigger Requested to End"); + } catch (Throwable t) { log.error("Error in sequential trigger", t); } @@ -117,7 +121,15 @@ public void forceExpire() { die = true; lock.notifyAll(); } + } + public void stopSilently() { + synchronized (lock) { + log.info("Sequential trigger stopping by request"); + dieSilently = true; + die = true; + lock.notifyAll(); + } } @SystemEvent @@ -470,11 +482,15 @@ private X waitEvent(Predicate filter) { synchronized (lock) { processing = false; currentEvent = null; + log.error("Clear"); context = null; this.filter = filter; lock.notifyAll(); while (true) { if (die) { + if (dieSilently) { + throw new SequentialTriggerPleaseDie("Sequential trigger stopping by request"); + } // Deprecated, but.......? // Seems better than leaving threads hanging around doing nothing. // thread.stop(); @@ -507,6 +523,7 @@ public void provideEvent(EventContext ctx, X event) { // TODO: expire on wipe? // Also make it configurable as to whether or not a wipe ends the trigger if (expired.getAsBoolean()) { + log.error("End"); // if (event.getHappenedAt().isAfter(expiresAt)) { log.warn("Sequential trigger expired by event after {}/{}ms: {}", initialEvent.getEffectiveTimeSince().toMillis(), timeout, event); die = true; @@ -515,9 +532,11 @@ public void provideEvent(EventContext ctx, X event) { } Predicate filt = filter; if (filt != null && !filt.test(event)) { +// log.error("Filtered"); return; } // First, set fields + log.error("Set ctx: {}", ctx); context = ctx; currentEvent = event; // Indicate that we are currently processing diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerPleaseDie.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerPleaseDie.java new file mode 100644 index 000000000000..e26f78e63b1a --- /dev/null +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerPleaseDie.java @@ -0,0 +1,22 @@ +package gg.xp.xivsupport.events.triggers.seq; + +public class SequentialTriggerPleaseDie extends RuntimeException { + public SequentialTriggerPleaseDie() { + } + + public SequentialTriggerPleaseDie(String message) { + super(message); + } + + public SequentialTriggerPleaseDie(String message, Throwable cause) { + super(message, cause); + } + + public SequentialTriggerPleaseDie(Throwable cause) { + super(cause); + } + + public SequentialTriggerPleaseDie(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java b/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java index 852a67a56168..2f6a72805c83 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java @@ -11,6 +11,7 @@ import gg.xp.xivsupport.callouts.CalloutTrackingKey; import gg.xp.xivsupport.callouts.SingleValueReplacement; import gg.xp.xivsupport.events.triggers.seq.SequentialTrigger; +import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerConcurrencyMode; import gg.xp.xivsupport.events.triggers.seq.SequentialTriggerController; import gg.xp.xivsupport.events.triggers.seq.SqtTemplates; import gg.xp.xivsupport.groovy.helpers.CustomGString; @@ -132,6 +133,7 @@ public class Builder { BiConsumer> sq; int timeout = 120_000; Closure rawSqHandler; + SequentialTriggerConcurrencyMode concurrencyMode = SequentialTriggerConcurrencyMode.BLOCK_NEW; public Builder named(String name) { this.name = name; @@ -167,6 +169,23 @@ public Builder then(Closure handler) { return this; } + public Builder concurrency(SequentialTriggerConcurrencyMode concurrencyMode) { + this.concurrencyMode = concurrencyMode; + return this; + } + + public SequentialTriggerConcurrencyMode getBlock() { + return SequentialTriggerConcurrencyMode.BLOCK_NEW; + } + + public SequentialTriggerConcurrencyMode getReplace() { + return SequentialTriggerConcurrencyMode.REPLACE_OLD; + } + + public SequentialTriggerConcurrencyMode getConcurrent() { + return SequentialTriggerConcurrencyMode.CONCURRENT; + } + public Builder sequence(@DelegatesTo(GroovySqHelper.class) Closure sequentialTriggerBody) { if (sequentialTriggerBody.getMaximumNumberOfParameters() != 2) { throw new IllegalArgumentException("Sequence must have two arguments (event and sequential trigger controller)"); @@ -196,10 +215,19 @@ private void finish() { else { SequentialTrigger sqFinalized = SqtTemplates.sq(timeout, type, condition, (e1, s) -> { try (SandboxScope ignored = sandbox.enter()) { - rawSqHandler.setDelegate(new GroovySqHelper<>(s)); - sq.accept(e1, s); + // This doesn't work right unless we clone + if (concurrencyMode == SequentialTriggerConcurrencyMode.CONCURRENT) { + Closure clonedSqHandler = (Closure) rawSqHandler.clone(); + clonedSqHandler.setDelegate(new GroovySqHelper<>(s)); + clonedSqHandler.call(e1, s); + } + else { + rawSqHandler.setDelegate(new GroovySqHelper<>(s)); + sq.accept(e1, s); + } } }); + sqFinalized.setConcurrency(this.concurrencyMode); addHandler(name, BaseEvent.class, (event, context) -> { try (SandboxScope ignored = sandbox.enter()) { sqFinalized.feed(context, event); @@ -225,6 +253,7 @@ private void finish() { public void add(@DelegatesTo(Builder.class) Closure closure) { Builder builder = new Builder<>(); closure.setDelegate(builder); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); closure.run(); builder.finish(); } diff --git a/trigger-support/src/test/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerTest.java b/trigger-support/src/test/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerTest.java index 51980b4a2485..603b0746a65f 100644 --- a/trigger-support/src/test/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerTest.java +++ b/trigger-support/src/test/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerTest.java @@ -228,12 +228,12 @@ void testMultiInvocation() { } @Test - void testDelay() { - SequentialTrigger trigger = SqtTemplates.sq(30_000, EchoEvent.class, e -> e.getLine().startsWith("Foo"), + void testConcurrencyBlock() { + SequentialTrigger trigger = SqtTemplates.sq(30_000, EchoEvent.class, e -> true, (e1, s) -> { - s.accept(new DebugCommand("Bar1")); + s.accept(new DebugCommand(e1.getLine() + '1')); s.waitMs(500); - s.accept(new DebugCommand("Bar2")); + s.accept(new DebugCommand(e1.getLine() + '2')); }); EchoEvent initial = new EchoEvent("Foo"); @@ -255,7 +255,7 @@ void testDelay() { { time = Instant.EPOCH.plusMillis(499); fts.setNewTime(time); - EchoEvent echo2 = new EchoEvent("Foo"); + EchoEvent echo2 = new EchoEvent("Bar"); echo2.setHappenedAt(time); echo2.setTimeSource(fts); master.pushEventAndWait(echo2); @@ -264,18 +264,151 @@ void testDelay() { List debugs = tec.getEventsOf(DebugCommand.class); MatcherAssert.assertThat(debugs, Matchers.hasSize(1)); } + // Push a dummy event for timing purposes { time = Instant.EPOCH.plusMillis(501); fts.setNewTime(time); - EchoEvent echo2 = new EchoEvent("Foo"); + master.pushEventAndWait(new TtsRequest("Stuff")); + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(2)); + } + { + EchoEvent echo2 = new EchoEvent("Baz"); echo2.setTimeSource(fts); echo2.setHappenedAt(time); master.pushEventAndWait(echo2); } + { + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(3)); + Assert.assertEquals(debugs.get(0).getCommand(), "Foo1"); + Assert.assertEquals(debugs.get(1).getCommand(), "Foo2"); + Assert.assertEquals(debugs.get(2).getCommand(), "Baz1"); + } + } + + @Test + void testConcurrencyReplace() { + SequentialTrigger trigger = SqtTemplates.sq(30_000, EchoEvent.class, e -> true, + (e1, s) -> { + s.accept(new DebugCommand(e1.getLine() + '1')); + s.waitMs(500); + s.accept(new DebugCommand(e1.getLine() + '2')); + }); + trigger.setConcurrency(SequentialTriggerConcurrencyMode.REPLACE_OLD); + + + EchoEvent initial = new EchoEvent("Foo"); + FakeTimeSource fts = new FakeTimeSource(); + Instant time = Instant.EPOCH; + fts.setNewTime(time); + initial.setTimeSource(fts); + initial.setHappenedAt(time); + + MutablePicoContainer pico = XivMain.testingMinimalInit(); + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(BaseEvent.class, trigger::feed); + TestEventCollector tec = new TestEventCollector(); + dist.registerHandler(tec); + + EventMaster master = pico.getComponent(EventMaster.class); + master.pushEventAndWait(initial); + + { + time = Instant.EPOCH.plusMillis(499); + fts.setNewTime(time); + EchoEvent echo2 = new EchoEvent("Bar"); + echo2.setHappenedAt(time); + echo2.setTimeSource(fts); + master.pushEventAndWait(echo2); + } { List debugs = tec.getEventsOf(DebugCommand.class); MatcherAssert.assertThat(debugs, Matchers.hasSize(2)); } + // Push a dummy event for timing purposes + { + time = Instant.EPOCH.plusMillis(501); + fts.setNewTime(time); + master.pushEventAndWait(new TtsRequest("Stuff")); + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(2)); + } + { + EchoEvent echo2 = new EchoEvent("Baz"); + echo2.setTimeSource(fts); + echo2.setHappenedAt(time); + master.pushEventAndWait(echo2); + } + { + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(3)); + Assert.assertEquals(debugs.get(0).getCommand(), "Foo1"); + Assert.assertEquals(debugs.get(1).getCommand(), "Bar1"); + Assert.assertEquals(debugs.get(2).getCommand(), "Baz1"); + } + } + + @Test + void testConcurrencyConcurrent() { + SequentialTrigger trigger = SqtTemplates.sq(30_000, EchoEvent.class, e -> true, + (e1, s) -> { + s.accept(new DebugCommand(e1.getLine() + '1')); + s.waitMs(500); + s.accept(new DebugCommand(e1.getLine() + '2')); + }); + trigger.setConcurrency(SequentialTriggerConcurrencyMode.CONCURRENT); + + EchoEvent initial = new EchoEvent("Foo"); + FakeTimeSource fts = new FakeTimeSource(); + Instant time = Instant.EPOCH; + fts.setNewTime(time); + initial.setTimeSource(fts); + initial.setHappenedAt(time); + + MutablePicoContainer pico = XivMain.testingMinimalInit(); + EventDistributor dist = pico.getComponent(EventDistributor.class); + dist.registerHandler(BaseEvent.class, trigger::feed); + TestEventCollector tec = new TestEventCollector(); + dist.registerHandler(tec); + + EventMaster master = pico.getComponent(EventMaster.class); + master.pushEventAndWait(initial); + + { + time = Instant.EPOCH.plusMillis(499); + fts.setNewTime(time); + EchoEvent echo2 = new EchoEvent("Bar"); + echo2.setHappenedAt(time); + echo2.setTimeSource(fts); + master.pushEventAndWait(echo2); + } + { + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(2)); + } + // Push a dummy event for timing purposes + { + time = Instant.EPOCH.plusMillis(501); + fts.setNewTime(time); + master.pushEventAndWait(new TtsRequest("Stuff")); + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(3)); + } + { + EchoEvent echo2 = new EchoEvent("Baz"); + echo2.setTimeSource(fts); + echo2.setHappenedAt(time); + master.pushEventAndWait(echo2); + } + { + List debugs = tec.getEventsOf(DebugCommand.class); + MatcherAssert.assertThat(debugs, Matchers.hasSize(4)); + Assert.assertEquals(debugs.get(0).getCommand(), "Foo1"); + Assert.assertEquals(debugs.get(1).getCommand(), "Bar1"); + Assert.assertEquals(debugs.get(2).getCommand(), "Foo2"); + Assert.assertEquals(debugs.get(3).getCommand(), "Baz1"); + } } } \ No newline at end of file From fe40e1c7a872567851ace2f4389df57d76cb199c Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 14:39:21 -0700 Subject: [PATCH 05/15] Changelog --- xivsupport/src/main/resources/te_changelog.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 4a30e4652e71..864349445619 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -1,12 +1,16 @@ -

    Unreleased

    +

    Beta

    • You can put
      scriptSettings.requestRunOnStartup()
      in a script to have it automatically request to be run on startup the first time it is run. This should make it easier to share scripts that are intended to run on startup.
    • +
    • Concurrency control for Sequential Triggers. See the the website for + more info. +

    Sep 3, 2023

      @@ -14,7 +18,7 @@

      Sep 3, 2023

    July 7, 2023

      -
    • Improved custom TTS program support, now supports quotes to allow spaces in arguments.
    • +
    • Improved custom TTS program support, now supports quotes to allow spaces in argumentsm ".
    • Minor additions to P10S.

    June 29, 2023

    From 2c5395f65ad92f3d2848d1e3d27f0308b8a86462 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 14:48:57 -0700 Subject: [PATCH 06/15] Fix hyperlinks in changelog area --- .../java/gg/xp/xivsupport/gui/GuiMain.java | 18 ++++++++++++++++-- .../src/main/resources/te_changelog.html | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java index 1655731bbf5a..bb426e28583e 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java @@ -81,6 +81,7 @@ import gg.xp.xivsupport.sys.Threading; import gg.xp.xivsupport.sys.XivMain; import org.apache.commons.io.IOUtils; +import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.picocontainer.MutablePicoContainer; @@ -92,6 +93,7 @@ import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; +import javax.swing.event.HyperlinkEvent; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellEditor; import java.awt.*; @@ -100,7 +102,9 @@ import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.io.IOException; import java.lang.reflect.Field; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; @@ -370,7 +374,7 @@ private class WelcomeTabPanel extends JPanel { private Component changelogPanel() { TitleBorderPanel changelog = new TitleBorderPanel("Changelog"); changelog.setLayout(new BorderLayout()); - String text; + @Language("html") String text; try { text = IOUtils.toString(GuiMain.class.getResource("/te_changelog.html"), StandardCharsets.UTF_8); } @@ -378,7 +382,17 @@ private Component changelogPanel() { log.error("Error loading changelog", e); text = "Error loading changelog"; } - Component rot = new ReadOnlyHtml(text); + ReadOnlyHtml rot = new ReadOnlyHtml(text); + rot.addHyperlinkListener(l -> { + if (l.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + try { + Desktop.getDesktop().browse(l.getURL().toURI()); + } + catch (IOException | URISyntaxException e) { + log.error("Hyperlink error", e); + } + } + }); changelog.add(new JScrollPane(rot)); return changelog; } diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 864349445619..0abae9c8babb 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -3,7 +3,7 @@

    Beta

    • You can put -
      scriptSettings.requestRunOnStartup()
      +
      scriptSettings.requestRunOnStartup()
      in a script to have it automatically request to be run on startup the first time it is run. This should make it easier to share scripts that are intended to run on startup.
    • From 8280c344ad5a54ea6e4d18993c0572c331a0e347 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 14:51:54 -0700 Subject: [PATCH 07/15] Fix changelog area --- xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java | 1 + 1 file changed, 1 insertion(+) diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java index bb426e28583e..7728a254647b 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/GuiMain.java @@ -383,6 +383,7 @@ private Component changelogPanel() { text = "Error loading changelog"; } ReadOnlyHtml rot = new ReadOnlyHtml(text); + rot.setFocusable(true); rot.addHyperlinkListener(l -> { if (l.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { try { From 002efa39cfe09fecd43b53fe7821ce57483461d4 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 17:01:20 -0700 Subject: [PATCH 08/15] Possibly working package loading refactor --- .../state/combatstate/CdTrackerTest.java | 15 ++++++------ .../reevent/events/BasicEventDistributor.java | 3 +++ .../java/gg/xp/reevent/scan/AutoScan.java | 17 +++++++------- .../java/gg/xp/compmonitor/CompMonitor.java | 21 +++++++++++++---- .../events/MonitoringEventDistributor.java | 23 ++++++++++++------- 5 files changed, 50 insertions(+), 29 deletions(-) diff --git a/plugins/cd-tracker/src/test/java/gg/xp/xivsupport/events/state/combatstate/CdTrackerTest.java b/plugins/cd-tracker/src/test/java/gg/xp/xivsupport/events/state/combatstate/CdTrackerTest.java index 88053f2e9446..15971b125eeb 100644 --- a/plugins/cd-tracker/src/test/java/gg/xp/xivsupport/events/state/combatstate/CdTrackerTest.java +++ b/plugins/cd-tracker/src/test/java/gg/xp/xivsupport/events/state/combatstate/CdTrackerTest.java @@ -39,6 +39,7 @@ public class CdTrackerTest { private static final Logger log = LoggerFactory.getLogger(CdTrackerTest.class); + private static final int acceptableErrorMs = 200; private static final Cooldown reprisal = Cooldown.Reprisal; private static final Cooldown draw = Cooldown.Draw; private final XivPlayerCharacter player = new XivPlayerCharacter(0x10000001, "Me, The Player", Job.GNB, XivWorld.of(), true, 1, new HitPoints(123, 123), ManaPoints.of(123, 123), new Position(0, 0, 0, 0), 0, 0, 1, 80, 0, 0); @@ -145,7 +146,7 @@ void testBasicCdTts() { Event event = events.get(0); if (event instanceof CdTracker.DelayedCdCallout dcc) { Assert.assertSame(dcc.originalEvent, myEvent); - MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(60_000 - precallTime, 100)); + MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(60_000 - precallTime, acceptableErrorMs)); } Map personal = tracker.getOverlayPersonalCds(); Assert.assertEquals(personal.size(), 0); @@ -155,7 +156,7 @@ void testBasicCdTts() { Instant replenishedAt = tracker.getReplenishedAt(key); Instant happenedAt = myEvent.getEffectiveHappenedAt(); long delta = Duration.between(happenedAt, replenishedAt).toMillis(); - MatcherAssert.assertThat(delta, new CloseTo(60_000, 100)); + MatcherAssert.assertThat(delta, new CloseTo(60_000, acceptableErrorMs)); } @Test @@ -196,7 +197,7 @@ void testCharges() { Event event = events.get(0); if (event instanceof CdTracker.DelayedCdCallout dcc) { Assert.assertSame(dcc.originalEvent, myEvent1); - MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(30_000 - precallTime, 100)); + MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(30_000 - precallTime, acceptableErrorMs)); } Map personal = tracker.getOverlayPersonalCds(); Assert.assertEquals(personal.size(), 1); @@ -206,7 +207,7 @@ void testCharges() { Instant replenishedAt = tracker.getReplenishedAt(key); Instant happenedAt = myEvent1.getEffectiveHappenedAt(); long delta = Duration.between(happenedAt, replenishedAt).toMillis(); - MatcherAssert.assertThat(delta, new CloseTo(30_000, 100)); + MatcherAssert.assertThat(delta, new CloseTo(30_000, acceptableErrorMs)); } AbilityUsedEvent myEvent2 = drawUsedByPc(); @@ -217,12 +218,12 @@ void testCharges() { Event event1 = events.get(0); if (event1 instanceof CdTracker.DelayedCdCallout dcc) { Assert.assertSame(dcc.originalEvent, myEvent1); - MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(30_000 - precallTime, 100)); + MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(30_000 - precallTime, acceptableErrorMs)); } Event event2 = events.get(1); if (event2 instanceof CdTracker.DelayedCdCallout dcc) { Assert.assertSame(dcc.originalEvent, myEvent2); - MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(2 * 30_000 - precallTime, 100)); + MatcherAssert.assertThat(dcc.delayedEnqueueAt() - dcc.getTimeBasis(), new CloseTo(2 * 30_000 - precallTime, acceptableErrorMs)); } Map personal = tracker.getOverlayPersonalCds(); Assert.assertEquals(personal.size(), 1); @@ -232,7 +233,7 @@ void testCharges() { Instant replenishedAt = tracker.getReplenishedAt(key); Instant happenedAt = myEvent1.getEffectiveHappenedAt(); long delta = Duration.between(happenedAt, replenishedAt).toMillis(); - MatcherAssert.assertThat(delta, new CloseTo(30_000 * 2, 100)); + MatcherAssert.assertThat(delta, new CloseTo(30_000 * 2, acceptableErrorMs)); } } } diff --git a/reevent/src/main/java/gg/xp/reevent/events/BasicEventDistributor.java b/reevent/src/main/java/gg/xp/reevent/events/BasicEventDistributor.java index edf64b73610a..48a5c9c1a9b7 100644 --- a/reevent/src/main/java/gg/xp/reevent/events/BasicEventDistributor.java +++ b/reevent/src/main/java/gg/xp/reevent/events/BasicEventDistributor.java @@ -38,6 +38,9 @@ public BasicEventDistributor(StateStore state) { // todo: sync object doesn't cover direct access as seen in subclass @Override public synchronized void registerHandler(EventHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("Handler was null!"); + } if (!(handler instanceof AutoHandler)) { log.info("Added manual handler: {}", handler); } diff --git a/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java b/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java index 51434d5b76d6..cc72a51db5ca 100644 --- a/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java +++ b/reevent/src/main/java/gg/xp/reevent/scan/AutoScan.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -56,7 +55,8 @@ public class AutoScan { "sfl4j", "xivdata" ); - private boolean scanned; + private volatile boolean scanned; + private final Object scanLock = new Object(); public AutoScan(AutoHandlerInstanceProvider instanceProvider, AutoHandlerConfig config) { this.instanceProvider = instanceProvider; @@ -69,16 +69,16 @@ private List findAddonJars() { public void doScanIfNeeded() { if (!scanned) { - doScan(); + synchronized (scanLock) { + if (!scanned) { + doScan(); + } + } } } - public void doScan() { + private void doScan() { log.info("Scanning packages"); - List out = new ArrayList<>(); -// ClassLoader loader = new ForceReloadClassLoader(); -// ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); - //noinspection EmptyFinallyBlock // TODO: Reload of existing classes is broken because reloading basic classes such as 'Event' // in a new classloader causes the JVM to no longer see it as the same class, causing signature // mismatches. @@ -177,7 +177,6 @@ public void doScan() { scanned = true; } - // Filter out interfaces, abstract classes, and other junk private static boolean isClassInstantiable(Class clazz) { return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isAnonymousClass() && (clazz.getDeclaringClass() == null); diff --git a/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java b/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java index d5b1fa889dc8..1dbaca7e565d 100644 --- a/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java +++ b/xivsupport/src/main/java/gg/xp/compmonitor/CompMonitor.java @@ -18,6 +18,7 @@ public class CompMonitor extends NullComponentMonitor { private static final Logger log = LoggerFactory.getLogger(CompMonitor.class); private final List> all = new ArrayList<>(); private final List listeners = new ArrayList<>(); + private final Object lock = new Object(); private PicoContainer container; @Override @@ -26,9 +27,15 @@ public void instantiated(PicoContainer container, ComponentAdapter compon if (!checkContainer(container)) { return; } + if (instantiated == null) { + log.error("Failed to instantiate {} {} {} {}", componentAdapter, constructor, constructor.getDeclaringClass(), instantiated); + return; + } InstantiatedItem inst = new InstantiatedItem<>(constructor.getDeclaringClass(), (T) instantiated); - all.add(inst); - listeners.forEach(listener -> listener.added(inst)); + synchronized (lock) { + all.add(inst); + listeners.forEach(listener -> listener.added(inst)); + } if (duration >= 100) { log.warn("CompMonitor: {}ms to instantiate {}", duration, instantiated); } @@ -45,11 +52,15 @@ private boolean checkContainer(PicoContainer container) { } public void addListener(CompListener listener) { - listeners.add(listener); +// synchronized (lock) { + listeners.add(listener); +// } } public void addAndRunListener(CompListener listener) { - addListener(listener); - all.forEach(listener::added); +// synchronized (lock) { + addListener(listener); + all.forEach(listener::added); +// } } } diff --git a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java index 7719be855de1..38b62593a006 100644 --- a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java +++ b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java @@ -9,12 +9,12 @@ import gg.xp.reevent.topology.Topology; import gg.xp.reevent.topology.TopologyInfo; import gg.xp.reevent.topology.TopologyProvider; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -27,9 +27,9 @@ public class MonitoringEventDistributor extends BasicEventDistributor implements private final TopologyInfo topoInfo; private final Object loadLock = new Object(); private final Map, List>> eventClassMap = new HashMap<>(); - private final List> autoHandlers = new ArrayList<>(); - private final List> manualHandlers = new ArrayList<>(); - private volatile boolean dirty = true; + private final List<@NotNull EventHandler> autoHandlers = new ArrayList<>(); + private final List<@NotNull EventHandler> manualHandlers = new ArrayList<>(); + private volatile boolean dirty; private Topology topology; public MonitoringEventDistributor(StateStore state, AutoScan scanner, TopologyInfo topoInfo, CompMonitor mon, AutoHandlerConfig config) { @@ -38,16 +38,20 @@ public MonitoringEventDistributor(StateStore state, AutoScan scanner, TopologyIn boolean dirty = false; Object inst = item.instance(); if (inst instanceof EventHandler eh) { +// synchronized (loadLock) { autoHandlers.add((EventHandler) eh); dirty = true; +// } } Class clazz = inst.getClass(); Method[] methods = clazz.getMethods(); for (Method method : methods) { if (method.isAnnotationPresent(HandleEvents.class)) { AutoHandler rawEvh = new AutoHandler(clazz, method, inst, config); +// synchronized (loadLock) { autoHandlers.add(rawEvh); dirty = true; +// } } } if (dirty) { @@ -61,21 +65,25 @@ public MonitoringEventDistributor(StateStore state, AutoScan scanner, TopologyIn @Override public synchronized void registerHandler(EventHandler handler) { + if (handler == null) { + throw new IllegalArgumentException("Handler was null!"); + } manualHandlers.add(handler); dirty = true; } - // TODO: this just doesn't work well until event sources are also auto-ified - // We get double events after reloading public void reloadIfNeeded() { + scanner.doScanIfNeeded(); if (!dirty) { return; } - scanner.doScanIfNeeded(); + log.info("Reloading", new RuntimeException()); +// synchronized (loadLock) { handlers.clear(); handlers.addAll(manualHandlers); handlers.addAll(autoHandlers); sortHandlers(); +// } topology = Topology.fromHandlers(new ArrayList<>(this.handlers), topoInfo); dirty = false; } @@ -104,7 +112,6 @@ public Topology getTopology() { return topology; } - // TODO: is there a better place to put this? @Override public void acceptEvent(Event event) { reloadIfNeeded(); From 39cb10236312c6dd3a3e9223282262cf4197ea90 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 17:11:53 -0700 Subject: [PATCH 09/15] Fix for tests --- .../src/main/java/gg/xp/xivsupport/events/ExampleSetup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/testutils-xiv/src/main/java/gg/xp/xivsupport/events/ExampleSetup.java b/testutils/testutils-xiv/src/main/java/gg/xp/xivsupport/events/ExampleSetup.java index 2e3f6c641afe..4649d66c515e 100644 --- a/testutils/testutils-xiv/src/main/java/gg/xp/xivsupport/events/ExampleSetup.java +++ b/testutils/testutils-xiv/src/main/java/gg/xp/xivsupport/events/ExampleSetup.java @@ -41,7 +41,6 @@ private static void finishSetup(PicoContainer container) { BasicEventQueue queue = container.getComponent(BasicEventQueue.class); queue.waitDrain(); EventDistributor dist = container.getComponent(EventDistributor.class); - dist.acceptEvent(new InitEvent()); XivState state = container.getComponent(XivStateImpl.class); // TODO: find actual solution to race conditions in tests try { @@ -63,6 +62,7 @@ private static void finishSetup(PicoContainer container) { private static void doEvents(EventDistributor dist) { + dist.acceptEvent(new InitEvent()); dist.acceptEvent(new ActWsRawMsg("{\"type\":\"ChangePrimaryPlayer\",\"charID\":22,\"charName\":\"Foo Bar\"}")); dist.acceptEvent(new ActWsRawMsg("{\"type\":\"ChangeZone\",\"zoneID\":777,\"zoneName\":\"the Weapon's Refrain (Ultimate)\"}")); // This player should be sorted first because they are the actual player From d466c09cc837d968826f1321b3a15ea7c757f041 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 20:10:47 -0700 Subject: [PATCH 10/15] Clean up module loading --- .../reevent/events/MonitoringEventDistributor.java | 12 ++++-------- .../java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java | 10 +++++++++- xivsupport/src/main/resources/te_changelog.html | 1 + 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java index 38b62593a006..7165ddcaac08 100644 --- a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java +++ b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java @@ -21,11 +21,13 @@ import java.util.List; import java.util.Map; +/** + * Event distributor that uses a component monitor to add handlers as they are loaded in. + */ public class MonitoringEventDistributor extends BasicEventDistributor implements TopologyProvider { private static final Logger log = LoggerFactory.getLogger(MonitoringEventDistributor.class); private final AutoScan scanner; private final TopologyInfo topoInfo; - private final Object loadLock = new Object(); private final Map, List>> eventClassMap = new HashMap<>(); private final List<@NotNull EventHandler> autoHandlers = new ArrayList<>(); private final List<@NotNull EventHandler> manualHandlers = new ArrayList<>(); @@ -38,20 +40,16 @@ public MonitoringEventDistributor(StateStore state, AutoScan scanner, TopologyIn boolean dirty = false; Object inst = item.instance(); if (inst instanceof EventHandler eh) { -// synchronized (loadLock) { autoHandlers.add((EventHandler) eh); dirty = true; -// } } Class clazz = inst.getClass(); Method[] methods = clazz.getMethods(); for (Method method : methods) { if (method.isAnnotationPresent(HandleEvents.class)) { AutoHandler rawEvh = new AutoHandler(clazz, method, inst, config); -// synchronized (loadLock) { autoHandlers.add(rawEvh); dirty = true; -// } } } if (dirty) { @@ -77,13 +75,11 @@ public void reloadIfNeeded() { if (!dirty) { return; } - log.info("Reloading", new RuntimeException()); -// synchronized (loadLock) { + log.info("Reloading"); handlers.clear(); handlers.addAll(manualHandlers); handlers.addAll(autoHandlers); sortHandlers(); -// } topology = Topology.fromHandlers(new ArrayList<>(this.handlers), topoInfo); dirty = false; } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java index a9f4f291d9cb..5dbc4c14f836 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java @@ -99,7 +99,15 @@ public UpdatesPanel(PersistenceProvider pers, UpdaterConfig updateConfig) { new RefreshLoop<>( "UpdatePeriodicCheck", this, - i -> doUpdateCheckInBackground(), + i -> { + try { + Thread.sleep(10_000); + } + catch (InterruptedException e) { + // + } + doUpdateCheckInBackground(); + }, // 15 minutes * 60 seconds * 1000 ms i -> 15 * 60 * 1000L ).start(); diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 0abae9c8babb..8ac3e573c234 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -11,6 +11,7 @@

      Beta

      href="https://triggevent.io/pages/docs/Sequential-Triggers/#concurrency-mode">the website for more info. +
    • Some internal refactoring.

    Sep 3, 2023

      From 222c03074a5da192d1d0823e727ba8b255392b40 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 22:10:45 -0700 Subject: [PATCH 11/15] Fix changelog --- xivsupport/src/main/resources/te_changelog.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index 056660be50bd..cff00cb8c5be 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -11,12 +11,9 @@

      Beta

      href="https://triggevent.io/pages/docs/Sequential-Triggers/#concurrency-mode">the website for more info. -
    -

    Sep 3, 2023

    -
    • Easy Triggers: New actions to wait for a buff/cast remaining duration to be below a certain time. Useful for having a reminder of when a buff is about to expire.
    -

    Unreleased

    +

    Sep 3, 2023

    • Timelines: Support force jumps and labels.
    From cdc09be554ab4dab7bf72d6995fe4b4a0f7e7970 Mon Sep 17 00:00:00 2001 From: XP Date: Wed, 27 Sep 2023 22:16:52 -0700 Subject: [PATCH 12/15] Removed debug logging --- .../events/triggers/seq/SequentialTriggerController.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java index ba66f74e7f13..7d0b1378823f 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/events/triggers/seq/SequentialTriggerController.java @@ -482,7 +482,6 @@ private X waitEvent(Predicate filter) { synchronized (lock) { processing = false; currentEvent = null; - log.error("Clear"); context = null; this.filter = filter; lock.notifyAll(); @@ -523,7 +522,6 @@ public void provideEvent(EventContext ctx, X event) { // TODO: expire on wipe? // Also make it configurable as to whether or not a wipe ends the trigger if (expired.getAsBoolean()) { - log.error("End"); // if (event.getHappenedAt().isAfter(expiresAt)) { log.warn("Sequential trigger expired by event after {}/{}ms: {}", initialEvent.getEffectiveTimeSince().toMillis(), timeout, event); die = true; @@ -532,11 +530,9 @@ public void provideEvent(EventContext ctx, X event) { } Predicate filt = filter; if (filt != null && !filt.test(event)) { -// log.error("Filtered"); return; } // First, set fields - log.error("Set ctx: {}", ctx); context = ctx; currentEvent = event; // Indicate that we are currently processing From afc6ecf555bbc56b52a19168a08c32f66fd6276f Mon Sep 17 00:00:00 2001 From: XP Date: Fri, 29 Sep 2023 13:31:43 -0700 Subject: [PATCH 13/15] Groovy callout enhancements --- .../java/gg/xp/reevent/events/DummyEvent.java | 10 +++++++ .../xp/xivsupport/groovy/GroovyTriggers.java | 16 +++++++++-- .../events/MonitoringEventDistributor.java | 27 ++++++++++--------- .../callouts/RawModifiedCallout.java | 1 - .../xivsupport/speech/BaseCalloutEvent.java | 12 +++++++++ .../xivsupport/speech/BasicCalloutEvent.java | 2 +- .../speech/DynamicCalloutEvent.java | 2 +- .../speech/ParentedCalloutEvent.java | 2 +- .../speech/ProcessedCalloutEvent.java | 3 ++- .../src/main/resources/te_changelog.html | 8 +++++- 10 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 reevent/src/main/java/gg/xp/reevent/events/DummyEvent.java diff --git a/reevent/src/main/java/gg/xp/reevent/events/DummyEvent.java b/reevent/src/main/java/gg/xp/reevent/events/DummyEvent.java new file mode 100644 index 000000000000..11d1f092e5ab --- /dev/null +++ b/reevent/src/main/java/gg/xp/reevent/events/DummyEvent.java @@ -0,0 +1,10 @@ +package gg.xp.reevent.events; + +import java.io.Serial; + +// Event that should do pretty much nothing. Mostly for testing purposes. +@SystemEvent +public class DummyEvent extends BaseEvent { + @Serial + private static final long serialVersionUID = -1422848827451666698L; +} diff --git a/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java b/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java index 2f6a72805c83..f055e78ce96f 100644 --- a/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java +++ b/trigger-support/src/main/java/gg/xp/xivsupport/groovy/GroovyTriggers.java @@ -258,6 +258,13 @@ public void add(@DelegatesTo(Builder.class) Closure closure) { builder.finish(); } + private BooleanSupplier wrapBooleanSupplier(BooleanSupplier supplier) { + return () -> { + try (SandboxScope ignored = sandbox.enter()) { + return supplier.getAsBoolean(); + } + }; + } private Supplier wrapSupplier(Supplier supplier) { return () -> { try (SandboxScope ignored = sandbox.enter()) { @@ -288,7 +295,7 @@ public CalloutEvent callout(@DelegatesTo(GroovyCalloutBuilder.class) Closure Supplier text = gcb.text; Duration timeBasis = controller.timeSinceStart(); Duration expiresAt = timeBasis.plusMillis(gcb.duration); - BooleanSupplier expired = gcb.expiry == null ? () -> controller.timeSinceStart().compareTo(expiresAt) > 0 : gcb.expiry; + BooleanSupplier expired = gcb.expired == null ? () -> controller.timeSinceStart().compareTo(expiresAt) > 0 : wrapBooleanSupplier(gcb.expired); ProcessedCalloutEvent callout = new ProcessedCalloutEvent( new CalloutTrackingKey(), gcb.tts, @@ -327,7 +334,7 @@ public class GroovyCalloutBuilder extends GroovyObjectSupport { @NotNull Supplier<@Nullable String> text = () -> null; int duration = 5000; @Nullable HasCalloutTrackingKey replaces; - @Nullable BooleanSupplier expiry; + @Nullable BooleanSupplier expired; @Nullable Color color; @Nullable String soundFile; @NotNull Supplier<@Nullable Component> guiProvider = () -> null; @@ -390,6 +397,11 @@ public GroovyCalloutBuilder duration(int duration) { return this; } + public GroovyCalloutBuilder displayWhile(BooleanSupplier displayWhile) { + this.expired = () -> !displayWhile.getAsBoolean(); + return this; + } + public GroovyCalloutBuilder replaces(CalloutTrackingKey replaces) { this.replaces = () -> replaces; return this; diff --git a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java index 38b62593a006..50e43d467668 100644 --- a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java +++ b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java @@ -72,20 +72,21 @@ public synchronized void registerHandler(EventHandler handler) { dirty = true; } - public void reloadIfNeeded() { + public void reloadIfNeeded(Event event) { scanner.doScanIfNeeded(); - if (!dirty) { - return; + if (dirty) { + synchronized (loadLock) { + if (dirty) { + log.info("Reloading due to {}", event); + handlers.clear(); + handlers.addAll(manualHandlers); + handlers.addAll(autoHandlers); + sortHandlers(); + topology = Topology.fromHandlers(new ArrayList<>(this.handlers), topoInfo); + dirty = false; + } + } } - log.info("Reloading", new RuntimeException()); -// synchronized (loadLock) { - handlers.clear(); - handlers.addAll(manualHandlers); - handlers.addAll(autoHandlers); - sortHandlers(); -// } - topology = Topology.fromHandlers(new ArrayList<>(this.handlers), topoInfo); - dirty = false; } @Override @@ -114,7 +115,7 @@ public Topology getTopology() { @Override public void acceptEvent(Event event) { - reloadIfNeeded(); + reloadIfNeeded(event); super.acceptEvent(event); } 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 db546cbc2d3c..7a4650672a96 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/callouts/RawModifiedCallout.java @@ -16,7 +16,6 @@ import java.util.function.Function; import java.util.function.Predicate; -// TODO: make a toString for this @SystemEvent public class RawModifiedCallout extends BaseEvent implements HasCalloutTrackingKey, HasPrimaryValue { private static final Logger log = LoggerFactory.getLogger(RawModifiedCallout.class); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java index 6f515c7ad20a..db50c69b7d8e 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BaseCalloutEvent.java @@ -16,6 +16,7 @@ public abstract class BaseCalloutEvent extends BaseEvent implements CalloutEvent private @Nullable Color colorOverride; private @Nullable HasCalloutTrackingKey replaces; private @Nullable CalloutTraceInfo trace; + private boolean forceExpire; private final CalloutTrackingKey key; @@ -72,4 +73,15 @@ public CalloutTraceInfo getTrace() { public void setTrace(CalloutTraceInfo trace) { this.trace = trace; } + + @Override + public final boolean isExpired() { + return forceExpire || isNaturallyExpired(); + } + + public abstract boolean isNaturallyExpired(); + + public void forceExpire() { + this.forceExpire = true; + } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BasicCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BasicCalloutEvent.java index d1f24db613f8..b05a50f4582f 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/BasicCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/BasicCalloutEvent.java @@ -41,7 +41,7 @@ public BasicCalloutEvent(String callText, String visualText, long hangTime) { } @Override - public boolean isExpired() { + public boolean isNaturallyExpired() { return getTimeSinceCall().compareTo(hangTime) > 0; } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/DynamicCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/DynamicCalloutEvent.java index 3b5cce40dd88..65e5217ca366 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/DynamicCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/DynamicCalloutEvent.java @@ -36,7 +36,7 @@ public DynamicCalloutEvent(CalloutTrackingKey key, String callText, Supplier 0; } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/ParentedCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/ParentedCalloutEvent.java index 2f39bdb9893f..da27d934668a 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/ParentedCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/ParentedCalloutEvent.java @@ -39,7 +39,7 @@ public ParentedCalloutEvent(X event, String callText, Supplier visualTex } @Override - public boolean isExpired() { + public boolean isNaturallyExpired() { return expiryCheck.test(event); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/speech/ProcessedCalloutEvent.java b/xivsupport/src/main/java/gg/xp/xivsupport/speech/ProcessedCalloutEvent.java index a5ac66ee1f62..8f21bd4871dc 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/speech/ProcessedCalloutEvent.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/speech/ProcessedCalloutEvent.java @@ -39,7 +39,7 @@ public ProcessedCalloutEvent(CalloutTrackingKey key, String ttsText, SupplierBeta in a script to have it automatically request to be run on startup the first time it is run. This should make it easier to share scripts that are intended to run on startup. +
  • Better options for managing callouts in Groovy scripted triggers: See documentation + here. +
  • Concurrency control for Sequential Triggers. See the the website for more info.
  • -
  • Easy Triggers: New actions to wait for a buff/cast remaining duration to be below a certain time. Useful for having a reminder of when a buff is about to expire.
  • +
  • Easy Triggers: New actions to wait for a buff/cast remaining duration to be below a certain time. Useful + for having a reminder of when a buff is about to expire. +

Sep 3, 2023

    From 65cab71b789251bf4e6516565cbbba35d2f14cba Mon Sep 17 00:00:00 2001 From: XP Date: Fri, 29 Sep 2023 13:48:20 -0700 Subject: [PATCH 14/15] Fix typo --- xivsupport/src/main/resources/te_changelog.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xivsupport/src/main/resources/te_changelog.html b/xivsupport/src/main/resources/te_changelog.html index b48529de006b..78d686c85cbc 100644 --- a/xivsupport/src/main/resources/te_changelog.html +++ b/xivsupport/src/main/resources/te_changelog.html @@ -25,7 +25,7 @@

    Sep 3, 2023

July 7, 2023

    -
  • Improved custom TTS program support, now supports quotes to allow spaces in argumentsm ".
  • +
  • Improved custom TTS program support, now supports quotes to allow spaces in arguments".
  • Minor additions to P10S.

June 29, 2023

From 5f28dd136e6d90fc7cf1fc9c3b42bd2040ab10bd Mon Sep 17 00:00:00 2001 From: XP Date: Fri, 6 Oct 2023 16:23:13 -0700 Subject: [PATCH 15/15] Fix merge conflict --- .../java/gg/xp/reevent/events/MonitoringEventDistributor.java | 1 + 1 file changed, 1 insertion(+) diff --git a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java index 929c5841e514..44d46389b695 100644 --- a/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java +++ b/xivsupport/src/main/java/gg/xp/reevent/events/MonitoringEventDistributor.java @@ -27,6 +27,7 @@ public class MonitoringEventDistributor extends BasicEventDistributor implements TopologyProvider { private static final Logger log = LoggerFactory.getLogger(MonitoringEventDistributor.class); private final AutoScan scanner; + private final Object loadLock = new Object(); private final TopologyInfo topoInfo; private final Map, List>> eventClassMap = new HashMap<>(); private final List<@NotNull EventHandler> autoHandlers = new ArrayList<>();