From 62b04c26ca3c2f80d13e49e34edbf3dc9beb4f46 Mon Sep 17 00:00:00 2001 From: XP Date: Sun, 3 Dec 2023 15:33:38 -0800 Subject: [PATCH] MVP for new timeline syncs --- .../timelines/CactbotEventTypes.java | 201 ++++++++++++++++++ .../timelines/CustomTimelineEntry.java | 6 + .../timelines/CustomTimelineLabel.java | 5 + .../timelines/EventSyncController.java | 13 ++ .../timelines/FileEventSyncController.java | 35 +++ .../timelines/TextFileLabelEntry.java | 6 + .../timelines/TextFileTimelineEntry.java | 8 +- .../xivsupport/timelines/TimelineEntry.java | 28 ++- .../xivsupport/timelines/TimelineManager.java | 5 +- .../xivsupport/timelines/TimelineParser.java | 55 ++++- .../timelines/TimelineProcessor.java | 146 ++++++++++--- .../timelines/TranslatedTextFileEntry.java | 5 + .../src/main/resources/timeline/a10n.txt | 3 +- timelines/src/main/resources/timeline/a1s.txt | 6 +- timelines/src/main/resources/timeline/a3n.txt | 115 +++++----- timelines/src/main/resources/timeline/a3s.txt | 4 +- timelines/src/main/resources/timeline/a5s.txt | 3 +- timelines/src/main/resources/timeline/a6n.txt | 12 +- timelines/src/main/resources/timeline/a6s.txt | 12 +- timelines/src/main/resources/timeline/a7s.txt | 8 +- timelines/src/main/resources/timeline/a9s.txt | 6 +- .../main/resources/timeline/aetherfont.txt | 12 +- .../aetherochemical_research_facility.txt | 6 +- .../src/main/resources/timeline/aglaia.txt | 29 ++- .../resources/timeline/akadaemia_anyder.txt | 9 +- .../src/main/resources/timeline/ala_mhigo.txt | 9 +- .../main/resources/timeline/aloalo_island.txt | 24 ++- .../resources/timeline/alzadaals_legacy.txt | 12 +- .../src/main/resources/timeline/amaurot.txt | 9 +- .../resources/timeline/anamnesis_anyder.txt | 9 +- .../timeline/another_mount_rokkon-savage.txt | 14 +- .../timeline/another_mount_rokkon.txt | 12 +- .../another_sildihn_subterrane-savage.txt | 14 +- .../timeline/another_sildihn_subterrane.txt | 12 +- .../main/resources/timeline/baelsars_wall.txt | 10 +- .../resources/timeline/bardams_mettle.txt | 9 +- .../timeline/bozjan_southern_front.txt | 21 +- .../resources/timeline/castrum_abania.txt | 9 +- .../resources/timeline/delubrum_reginae.txt | 18 +- .../timeline/delubrum_reginae_savage.txt | 57 +++-- .../src/main/resources/timeline/dohn_mheg.txt | 9 +- .../main/resources/timeline/doma_castle.txt | 9 +- .../timeline/drowned_city_of_skalla.txt | 9 +- .../main/resources/timeline/dun_scaith.txt | 12 +- .../resources/timeline/emerald_weapon-ex.txt | 5 +- .../src/main/resources/timeline/endsinger.txt | 2 +- .../main/resources/timeline/euphrosyne.txt | 15 +- .../resources/timeline/eureka_hydatos.txt | 16 +- .../resources/timeline/fractal_continuum.txt | 9 +- .../timeline/fractal_continuum_hard.txt | 9 +- .../main/resources/timeline/ghimlyt_dark.txt | 9 +- .../resources/timeline/ghimlyt_dark64.txt | 9 +- .../resources/timeline/gubal_library_hard.txt | 9 +- .../src/main/resources/timeline/hades.txt | 2 +- .../src/main/resources/timeline/hells_lid.txt | 9 +- .../resources/timeline/heroes_gauntlet.txt | 9 +- .../resources/timeline/holminster_switch.txt | 9 +- .../resources/timeline/ktisis_hyperboreia.txt | 12 +- .../main/resources/timeline/kugane_castle.txt | 9 +- .../main/resources/timeline/lakshmi-ex.txt | 138 ++++++------ .../main/resources/timeline/lapis_manalis.txt | 12 +- .../main/resources/timeline/malikahs_well.txt | 9 +- .../resources/timeline/matoyas_relict.txt | 9 +- .../main/resources/timeline/mount_rokkon.txt | 21 +- .../src/main/resources/timeline/mt_gulg.txt | 9 +- .../src/main/resources/timeline/o10s.txt | 2 +- .../src/main/resources/timeline/o12n.txt | 3 +- timelines/src/main/resources/timeline/o6s.txt | 8 +- timelines/src/main/resources/timeline/o7s.txt | 6 +- .../resources/timeline/orbonne_monastery.txt | 15 +- .../src/main/resources/timeline/p10n.txt | 2 +- .../src/main/resources/timeline/paglthan.txt | 6 +- .../main/resources/timeline/qitana_ravel.txt | 9 +- .../timeline/ridorana_lighthouse.txt | 12 +- .../timeline/royal_city_of_rabanastre.txt | 12 +- .../main/resources/timeline/sephirot-un.txt | 3 +- .../src/main/resources/timeline/shinryu.txt | 2 +- .../timeline/shisui_of_the_violet_tides.txt | 9 +- .../main/resources/timeline/sirensong_sea.txt | 9 +- .../src/main/resources/timeline/smileton.txt | 12 +- .../src/main/resources/timeline/sohm_al.txt | 9 +- .../main/resources/timeline/sohm_al_hard.txt | 6 +- .../src/main/resources/timeline/sophia-un.txt | 2 + .../resources/timeline/st_mocianne_hard.txt | 9 +- .../resources/timeline/stigma_dreamscape.txt | 12 +- .../resources/timeline/swallows_compass.txt | 9 +- timelines/src/main/resources/timeline/t10.txt | 3 +- timelines/src/main/resources/timeline/t11.txt | 3 +- timelines/src/main/resources/timeline/t6.txt | 3 +- timelines/src/main/resources/timeline/t7.txt | 4 +- timelines/src/main/resources/timeline/t8.txt | 3 +- .../resources/timeline/temple_of_the_fist.txt | 9 +- .../src/main/resources/timeline/test.txt | 18 +- .../src/main/resources/timeline/thaleia.txt | 11 +- .../resources/timeline/the_aitiascope.txt | 12 +- .../src/main/resources/timeline/the_burn.txt | 9 +- .../main/resources/timeline/the_burn64.txt | 9 +- .../resources/timeline/the_copied_factory.txt | 18 +- .../main/resources/timeline/the_dead_ends.txt | 12 +- .../timeline/the_epic_of_alexander.txt | 2 +- .../timeline/the_fell_court_of_troia.txt | 12 +- .../resources/timeline/the_grand_cosmos.txt | 9 +- .../the_lost_city_of_amdapor_hard.txt | 12 +- .../timeline/the_lunar_subterrane.txt | 12 +- .../resources/timeline/the_puppets_bunker.txt | 18 +- .../timeline/the_sildihn_subterrane.txt | 33 ++- .../the_tower_at_paradigms_breach.txt | 27 ++- .../resources/timeline/the_tower_of_babil.txt | 12 +- .../resources/timeline/the_tower_of_zot.txt | 12 +- .../src/main/resources/timeline/the_vault.txt | 9 +- .../main/resources/timeline/thordan-un.txt | 2 + .../translations/aloalo_island.txt.json | 2 +- .../translations/lapis_manalis.txt.json | 2 +- .../timeline/translations/thaleia.txt.json | 2 +- .../the_lunar_subterrane.txt.json | 2 +- .../src/main/resources/timeline/twinning.txt | 9 +- .../src/main/resources/timeline/vanaspati.txt | 12 +- .../main/resources/timeline/weeping_city.txt | 12 +- .../src/main/resources/timeline/xelphatol.txt | 9 +- .../src/main/resources/timeline/zadnor.txt | 18 +- .../src/main/resources/timeline/zurvan-un.txt | 2 + timelines/src/main/resources/timelines.csv | 10 - .../timelines/TimelineParserTest.java | 64 ++++++ 123 files changed, 1372 insertions(+), 550 deletions(-) create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/CactbotEventTypes.java create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java create mode 100644 timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/CactbotEventTypes.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/CactbotEventTypes.java new file mode 100644 index 000000000000..29e7175b918e --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/CactbotEventTypes.java @@ -0,0 +1,201 @@ +package gg.xp.xivsupport.timelines; + +import gg.xp.reevent.events.Event; +import gg.xp.xivsupport.events.actlines.events.AbilityUsedEvent; +import gg.xp.xivsupport.events.actlines.events.ChatLineEvent; +import gg.xp.xivsupport.events.actlines.events.NameIdPair; +import gg.xp.xivsupport.events.actlines.events.SystemLogMessageEvent; +import gg.xp.xivsupport.events.state.InCombatChangeEvent; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +public enum CactbotEventTypes { + + GameLog(ChatLineEvent.class, Map.of( + "code", intConv(ChatLineEvent::getCode, 16), + "line", strConv(ChatLineEvent::getLine), + // TODO + "message", strConv(ChatLineEvent::getLine), + "echo", strConv(ChatLineEvent::getLine), + "dialog", strConv(ChatLineEvent::getLine) + )), + StartsUsing(AbilityUsedEvent.class, Map.of( + "sourceId", id(AbilityUsedEvent::getSource), + "source", name(AbilityUsedEvent::getSource), + "targetId", id(AbilityUsedEvent::getTarget), + "target", name(AbilityUsedEvent::getTarget), + "id", id(AbilityUsedEvent::getAbility), + "ability", name(AbilityUsedEvent::getAbility) + )), + Ability(AbilityUsedEvent.class, Map.of( + "sourceId", id(AbilityUsedEvent::getSource), + "source", name(AbilityUsedEvent::getSource), + "targetId", id(AbilityUsedEvent::getTarget), + "target", name(AbilityUsedEvent::getTarget), + "id", id(AbilityUsedEvent::getAbility), + "ability", name(AbilityUsedEvent::getAbility) + )), + InCombat(InCombatChangeEvent.class, Map.of( + // TODO: kind of fake + "inACTCombat", boolToInt(InCombatChangeEvent::isInCombat), + "inGameCombat", boolToInt(InCombatChangeEvent::isInCombat) + )), + SystemLogMessage(SystemLogMessageEvent.class, Map.of( + "instance", intConv(SystemLogMessageEvent::getUnknown, 16), + "id", intConv(SystemLogMessageEvent::getId, 16), + "param0", intConv(SystemLogMessageEvent::getParam0, 16), + "param1", intConv(SystemLogMessageEvent::getParam1, 16), + "param2", intConv(SystemLogMessageEvent::getParam2, 16) + )) + + + // TODO: the rest of the events + ; + + + private final Holder data; + + CactbotEventTypes(Class eventType, Map> condMap) { + this.data = new Holder<>(eventType, condMap); + } + + public Class eventType() { + return data.eventType; + } + + /** + * Represents a conversion from some field (possibly nested) on an event, to a predicate that matches events. + * All cactbot netregices use string values regardless of the underlying data type, so this always takes a string. + * + * @param The event type. + */ + @FunctionalInterface + private interface ConvToCondition { + /** + * Example: on a 21-line, we want to check if the ability ID is "12AB". + * We would call this with "12AB" as the argument, and it should return a predicate that checks that + * a given AbilityUsedEvent has an ability ID of 0x12AB. + * + * @param input The input string. + * @return The resulting predicate. + */ + Predicate convert(String input); + } + + /** + * Make a combined predicate based on the map of values. + * + * @param values The values + * @return The combined predicate + */ + public Predicate make(Map values) { + return this.data.make(values); + } + + /** + * Convenience function for quickly making a ConvToCondition on an integer/long field. + * If the input string in the resulting ConvToCondition is a plain number (and not something that would require + * us to actually do regex), then the numbers will be compared directly. + * + * @param getter A function for getting the required value out of our event. + * @param base The numerical base, typically 10 or 16 + * @param The event type + * @return The condition matching the above requirements. + */ + private static ConvToCondition intConv(Function getter, int base) { + return intConv(getter, base, 0); + } + + /** + * Convenience function for quickly making a ConvToCondition on an integer/long field. + * If the input string in the resulting ConvToCondition is a plain number (and not something that would require + * us to actually do regex), then the numbers will be compared directly. + *

+ * This version of the method allows you to specify that the number should be left-padded to a minimum number of + * characters, with zeroes. e.g. if the input is "00", and the value is "0", then in order for that to match, you + * would need to specify minDigits == 2. + * + * @param getter A function for getting the required value out of our event. + * @param base The numerical base, typically 10 or 16 + * @param minDigits If ACT would left-pad the number with zeroes, then you should specify the minimum length + * of the number here so that the value can be similarly padded out. + * @param The event type + * @return The condition matching the above requirements. + */ + private static ConvToCondition intConv(Function getter, int base, int minDigits) { + return str -> { + try { + // Fast path - input is a number literal, so do a direct number comparison + long parsed = Long.parseLong(str, base); + return item -> getter.apply(item) == parsed; + } + catch (NumberFormatException ignored) { + // Slow path - input is a regex, so compile to regex first + Pattern pattern = Pattern.compile(str, Pattern.CASE_INSENSITIVE); + return item -> { + String asString = Long.toString(getter.apply(item), base); + if (minDigits > 1) { + asString = StringUtils.leftPad(asString, minDigits, '0'); + } + return pattern.matcher(asString).matches(); + }; + } + }; + } + + private static ConvToCondition strConv(Function getter) { + return str -> { + Pattern pattern = Pattern.compile(str); + return item -> pattern.matcher(getter.apply(item)).matches(); + }; + } + + private static ConvToCondition id(Function getter) { + return intConv(e -> getter.apply(e).getId(), 16); + } + + private static ConvToCondition name(Function getter) { + return strConv(e -> getter.apply(e).getName()); + } + + private static ConvToCondition boolToInt(Function getter) { + return str -> switch (str) { + case "0" -> (item -> !getter.apply(item)); + case "1" -> (getter::apply); + default -> throw new IllegalArgumentException("Expected 0 or 1, got '%s'".formatted(str)); + }; + } + + + private static class Holder { + private static final Logger log = LoggerFactory.getLogger(CactbotEventTypes.class); + private final Class eventType; + private final Map> condMap; + + Holder(Class eventType, Map> condMap) { + this.eventType = eventType; + this.condMap = condMap; + } + + + public Predicate make(Map values) { + Predicate combined = eventType::isInstance; + for (var entry : values.entrySet()) { + ConvToCondition convToCondition = this.condMap.get(entry.getKey()); + if (convToCondition == null) { + throw new IllegalArgumentException("Unknown condition: " + entry); + } + Predicate converted = convToCondition.convert(entry.getValue()); + combined = combined.and(converted); + } + //noinspection unchecked - the first check is the type check + return (Predicate) combined; + } + } +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java index 73ce3cdee834..c27e94833571 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineEntry.java @@ -195,6 +195,12 @@ public int hashCode() { return Objects.hash(time, name, sync, duration, windowStart, windowEnd, jump, jumpLabel, forceJump, icon, replaces, enabled, callout, calloutPreTime, getEnabledJobs()); } + @Override + public @Nullable EventSyncController eventSyncController() { + // TODO + return null; + } + @Override public String toString() { return "CustomTimelineEntry{" + diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineLabel.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineLabel.java index a08790f043c5..cee48c45b980 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineLabel.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/CustomTimelineLabel.java @@ -38,6 +38,11 @@ public boolean isLabel() { return true; } + @Override + public @Nullable EventSyncController eventSyncController() { + return null; + } + @Override public double time() { return time; diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java new file mode 100644 index 000000000000..3a9f6b8794cc --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/EventSyncController.java @@ -0,0 +1,13 @@ +package gg.xp.xivsupport.timelines; + +import gg.xp.reevent.events.Event; + +public interface EventSyncController { + boolean shouldSync(Event event); + + Class eventType(); + + default boolean isEditable() { + return false; + }; +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java new file mode 100644 index 000000000000..ccf76908f261 --- /dev/null +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/FileEventSyncController.java @@ -0,0 +1,35 @@ +package gg.xp.xivsupport.timelines; + +import gg.xp.reevent.events.Event; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; + +public class FileEventSyncController implements EventSyncController { + + private final Class eventType; + private final Predicate predicate; + private final Map original; + + public FileEventSyncController(Class eventType, Predicate predicate, Map original) { + this.eventType = eventType; + this.predicate = predicate; + this.original = new HashMap<>(original); + } + + @Override + public boolean shouldSync(Event event) { + return predicate.test(event); + } + + @Override + public Class eventType() { + return eventType; + } + + @Override + public String toString() { + return eventType.getSimpleName() + original; + } +} diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileLabelEntry.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileLabelEntry.java index 49c395110514..be7c2e8ba8be 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileLabelEntry.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileLabelEntry.java @@ -10,6 +10,7 @@ public record TextFileLabelEntry( double time, String name ) implements TimelineEntry, Serializable { + @Override public String toString() { return "TextFileLabelEntry{" + @@ -62,4 +63,9 @@ public double calloutPreTime() { public boolean isLabel() { return true; } + + @Override + public @Nullable EventSyncController eventSyncController() { + return null; + } } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileTimelineEntry.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileTimelineEntry.java index 1d61685152b1..de0d7f7de9d1 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileTimelineEntry.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TextFileTimelineEntry.java @@ -1,9 +1,11 @@ package gg.xp.xivsupport.timelines; +import gg.xp.reevent.events.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.Serializable; +import java.util.function.Predicate; import java.util.regex.Pattern; public record TextFileTimelineEntry( @@ -14,8 +16,9 @@ public record TextFileTimelineEntry( @NotNull TimelineWindow timelineWindow, @Nullable Double jump, @Nullable String jumpLabel, - boolean forceJump -) implements TimelineEntry, Serializable { + boolean forceJump, + EventSyncController eventSyncController) implements TimelineEntry, Serializable { + @Override public String toString() { return "TextFileTimelineEntry{" + @@ -27,6 +30,7 @@ public String toString() { ", jump=" + jump + ", jumpLabel='" + jumpLabel + '\'' + ", forceJump=" + forceJump + + ", syncCtrl=" + eventSyncController + '}'; } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineEntry.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineEntry.java index 4318f2ea88e1..1c0c8d63a341 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineEntry.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineEntry.java @@ -1,6 +1,7 @@ package gg.xp.xivsupport.timelines; import com.fasterxml.jackson.annotation.JsonIgnore; +import gg.xp.reevent.events.Event; import gg.xp.xivdata.data.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -52,11 +53,34 @@ default boolean shouldSync(double currentTime, String line) { return sync.matcher(line).find(); } + @Nullable EventSyncController eventSyncController(); + + default boolean hasEventSync() { + return eventSyncController() != null; + }; + + default @Nullable Class eventSyncType() { + EventSyncController esc = eventSyncController(); + return esc == null ? null : esc.eventType(); + } + + default boolean shouldSync(double currentTime, Event event) { + EventSyncController syncControl = eventSyncController(); + if (syncControl == null) { + return false; + } + boolean timesMatch = (currentTime >= getMinTime() && currentTime <= getMaxTime()); + if (!timesMatch) { + return false; + } + return syncControl.shouldSync(event); + } + /** * @return true if this timeline entry would ever cause a sync */ default boolean canSync() { - return sync() != null; + return sync() != null || hasEventSync(); } /** @@ -320,7 +344,7 @@ default Stream makeTriggerTimelineEntries() { } String uniqueName = makeUniqueName(); String hideAllLine = "hideall \"%s\"".formatted(uniqueName); - String actualTimelineLine = new TextFileTimelineEntry(time(), uniqueName, null, null, TimelineWindow.DEFAULT, null, null, false).toTextFormat(); + String actualTimelineLine = new TextFileTimelineEntry(time(), uniqueName, null, null, TimelineWindow.DEFAULT, null, null, false, null).toTextFormat(); return Stream.of(hideAllLine, actualTimelineLine); } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java index 43a8911281f4..dd67e28f3fed 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineManager.java @@ -1,6 +1,7 @@ package gg.xp.xivsupport.timelines; import com.fasterxml.jackson.databind.ObjectMapper; +import gg.xp.reevent.events.BaseEvent; import gg.xp.reevent.events.CurrentTimeSource; import gg.xp.reevent.events.EventContext; import gg.xp.reevent.events.EventMaster; @@ -238,10 +239,10 @@ public void changeZone(EventContext context, ZoneChangeEvent zoneChangeEvent) { } @HandleEvents(order = 40_000) - public void actLine(EventContext context, ACTLogLineEvent event) { + public void actLine(EventContext context, BaseEvent event) { TimelineProcessor currentTimeline = this.currentTimeline; if (currentTimeline != null) { - currentTimeline.processActLine(event); + currentTimeline.processEvent(event); } } diff --git a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineParser.java b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineParser.java index 85469607a1d9..5fbbbc55833a 100644 --- a/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineParser.java +++ b/timelines/src/main/java/gg/xp/xivsupport/timelines/TimelineParser.java @@ -1,5 +1,11 @@ package gg.xp.xivsupport.timelines; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import gg.xp.reevent.events.Event; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -7,17 +13,43 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -public class TimelineParser { +public final class TimelineParser { private static final Logger log = LoggerFactory.getLogger(TimelineParser.class); // TODO: there are also windows that do not have a start + end, only a single number - private static final Pattern timelinePatternLong = Pattern.compile("^(?