From 8ea002ac8f832f2ff7a9aa4e3d9c7965a97fd226 Mon Sep 17 00:00:00 2001 From: Alexei Frolov Date: Sun, 25 Oct 2026 13:17:19 -0700 Subject: [PATCH] Support overall times in stage lifecycle messages Updates stage start and end messages to optionally send an overall time if it is known. Adds overall time tracking to the base challenge class, defaulting to empty, for individual challenges to override if they support it. Overall times can either be server-authoritative or a local counter (like in Inferno). Adds TFA overall time tracking by reading the game script that updates the raid timer widget and comparing against chat messages when they are sent. --- .../challenges/inferno/InfernoChallenge.java | 9 ++ .../challenges/tfa/ArchiveChallenge.java | 98 +++++++++++-------- .../tfa/bosses/BossDataTracker.java | 6 +- .../blert/client/WebSocketEventHandler.java | 14 +++ src/main/java/io/blert/core/DataTracker.java | 97 +++++++++++++++--- src/main/java/io/blert/core/OverallTicks.java | 68 +++++++++++++ .../io/blert/core/RecordableChallenge.java | 7 ++ .../io/blert/events/StageUpdateEvent.java | 33 +++++-- .../java/io/blert/json/ChallengeUpdate.java | 11 +++ 9 files changed, 276 insertions(+), 67 deletions(-) create mode 100644 src/main/java/io/blert/core/OverallTicks.java diff --git a/src/main/java/io/blert/challenges/inferno/InfernoChallenge.java b/src/main/java/io/blert/challenges/inferno/InfernoChallenge.java index 6a6d790..b7c8927 100644 --- a/src/main/java/io/blert/challenges/inferno/InfernoChallenge.java +++ b/src/main/java/io/blert/challenges/inferno/InfernoChallenge.java @@ -31,6 +31,7 @@ import io.blert.util.Location; import io.blert.util.Tick; import java.util.List; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -99,6 +100,14 @@ public boolean hasLogged() { return hasLogged; } + @Override + public Optional getOverallTicks() { + if (hasLogged || getState().isInactive() || challengeStartTick == -1) { + return Optional.empty(); + } + return Optional.of(OverallTicks.local(recordedDurationTicks())); + } + @Override public boolean containsLocation(WorldPoint worldPoint) { return worldPoint.getRegionID() == INFERNO_REGION_ID diff --git a/src/main/java/io/blert/challenges/tfa/ArchiveChallenge.java b/src/main/java/io/blert/challenges/tfa/ArchiveChallenge.java index a28d41d..4f5a97a 100644 --- a/src/main/java/io/blert/challenges/tfa/ArchiveChallenge.java +++ b/src/main/java/io/blert/challenges/tfa/ArchiveChallenge.java @@ -41,20 +41,21 @@ import io.blert.util.Tick; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.function.BiConsumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; import joptsimple.internal.Strings; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.runelite.api.ChatMessageType; import net.runelite.api.Client; +import net.runelite.api.GameState; import net.runelite.api.Player; import net.runelite.api.coords.WorldPoint; -import net.runelite.api.events.ChatMessage; -import net.runelite.api.events.NpcSpawned; -import net.runelite.api.events.VarbitChanged; +import net.runelite.api.events.*; import net.runelite.client.callback.ClientThread; import net.runelite.client.util.Text; import org.apache.commons.lang3.tuple.Pair; @@ -63,6 +64,7 @@ public class ArchiveChallenge extends RecordableChallenge { private static final int TFA_PARTY_VARBIT = 16386; private static final int TFA_PARTY_LIST_COMPONENT_ID = 63897605; + private static final int TFA_RAID_TIMER_SCRIPT_ID = 10036; private static final int TFA_VARBIT_NO_PARTY = 0; private static final int TFA_VARBIT_IN_PARTY = 1; @@ -73,8 +75,6 @@ public class ArchiveChallenge extends RecordableChallenge { Pattern.compile("(.+) has entered the Fractured Archive\\. Step inside to join (her|him|them)\\.\\.\\."); private static final String RAID_WIPE_MESSAGE = "You failed to survive the Fractured Archive."; private static final String RAID_ABANDON_MESSAGE = "You abandon the raid and leave the Fractured Archive."; - private static final Pattern BOSS_COMPLETION_TOTAL_REGEX = Pattern.compile("Challenge complete: .+?\\. Duration: " - + Tick.TIME_STRING_REGEX + "\\. Total: (?" + Tick.TIME_STRING_REGEX + ")"); private static final Pattern RAID_COMPLETION_REGEX = Pattern.compile("The Fractured Archive: Total completion time: (" + Tick.TIME_STRING_REGEX + ").*"); @@ -86,8 +86,16 @@ public class ArchiveChallenge extends RecordableChallenge { private @Nullable DeferredTask deferredTask = null; - // Ticks since Guardian start, from the latest boss completion message. - private int reportedOverallTime = -1; + @AllArgsConstructor + private static class RaidTimer { + private final int clientTick; + private final int ticks; + private final boolean stopped; + } + + // The raid timer as of the latest run of its script. + @Nullable + private RaidTimer raidTimer = null; public ArchiveChallenge(Client client, ClientThread clientThread) { super(Challenge.TFA, client, clientThread); @@ -105,37 +113,13 @@ public boolean containsLocation(WorldPoint worldPoint) { } @Override - protected void onInitialize() { - // TODO(frolv): Temporary, for comparing TFA lifecycle against captures. Remove once verified. - addEventHandler((clientTick, event) -> { - switch (event.getType()) { - case CHALLENGE_START: - case CHALLENGE_UPDATE: - case CHALLENGE_END: - case PLAYER_DEATH: - log.warn("TFA-VERIFY tick={} state={} {}", clientTick, getState(), event); - break; - case STAGE_UPDATE: - StageUpdateEvent update = (StageUpdateEvent) event; - log.warn( - "TFA-VERIFY tick={} state={} {} inGameTicks={} accurate={} overall={}", - clientTick, - getState(), - event, - update.getInGameTicks().orElse(-1), - update.isAccurate(), - reportedOverallTime); - break; - default: - break; - } - }); - } + protected void onInitialize() {} @Override protected void onTerminate() { clearBossDataTracker(); resetParty(); + raidTimer = null; // The same instance records the next raid, so drop anything still // pending from this one. @@ -204,6 +188,21 @@ protected Stage getStage() { return bossDataTracker != null ? bossDataTracker.getStage() : null; } + /** + * Returns the raid's overall time in ticks, from the start of Guardian, if known. + */ + @Override + public Optional getOverallTicks() { + if (raidTimer == null) { + return Optional.empty(); + } + // The raid timer is always precise. + if (raidTimer.stopped) { + return Optional.of(OverallTicks.server(raidTimer.ticks, true)); + } + return Optional.of(OverallTicks.server(raidTimer.ticks + client.getTickCount() - raidTimer.clientTick, true)); + } + /** * Invokes a callback for each username displayed in the TFA orb list. * @@ -224,7 +223,6 @@ public void forEachOrb(BiConsumer callback) { private void queueRaidStart(boolean reinitializeParty) { log.info("Starting new TFA raid"); setState(ChallengeState.PREPARING); - reportedOverallTime = -1; if (reinitializeParty) { // The in-raid party list is written 7 ticks after the player's entry message; @@ -276,6 +274,7 @@ private void queueRaidEnd(ChallengeState state, int overallTime) { clearBossDataTracker(); clearPendingEvents(); resetParty(); + raidTimer = null; deferredTask = null; log.info("TFA raid ended before it started"); return; @@ -294,6 +293,7 @@ private void endRaid(ChallengeState state, int overallTime) { clearBossDataTracker(); setState(state); resetParty(); + raidTimer = null; getClientThread().invokeAtTickEnd(() -> dispatchEvent(new ChallengeEndEvent(-1, overallTime, soft))); } @@ -370,6 +370,27 @@ public void onVarbitChanged(VarbitChanged varbit) { super.onVarbitChanged(varbit); } + @Override + public void onScriptPreFired(ScriptPreFired event) { + if (event.getScriptId() == TFA_RAID_TIMER_SCRIPT_ID) { + // The script runs every tick with the raid's overall time in ticks, + // and whether the timer is stopped. + Object[] args = event.getScriptEvent().getArguments(); + raidTimer = new RaidTimer(client.getTickCount(), (int) args[1], (int) args[2] == 1); + } + + super.onScriptPreFired(event); + } + + @Override + public void onGameStateChanged(GameStateChanged event) { + if (event.getGameState() == GameState.LOGIN_SCREEN) { + raidTimer = null; + } + + super.onGameStateChanged(event); + } + @Override public void onChatMessage(ChatMessage message) { updateLocation(); @@ -396,13 +417,6 @@ public void onChatMessage(ChatMessage message) { return; } - Matcher matcher = BOSS_COMPLETION_TOTAL_REGEX.matcher(stripped); - if (matcher.find()) { - reportedOverallTime = Tick.fromTimeString(matcher.group("total")) - .map(Pair::getLeft) - .orElse(-1); - } - if (getState() == ChallengeState.ACTIVE && (stripped.equals(RAID_WIPE_MESSAGE) || stripped.equals(RAID_ABANDON_MESSAGE))) { if (bossDataTracker != null) { @@ -413,7 +427,7 @@ public void onChatMessage(ChatMessage message) { queueRaidEnd(ChallengeState.COMPLETE); } - matcher = RAID_COMPLETION_REGEX.matcher(stripped); + Matcher matcher = RAID_COMPLETION_REGEX.matcher(stripped); if (getState() == ChallengeState.ACTIVE && matcher.matches()) { int overallTime = Tick.fromTimeString(matcher.group(1)).map(Pair::getLeft).orElse(-1); diff --git a/src/main/java/io/blert/challenges/tfa/bosses/BossDataTracker.java b/src/main/java/io/blert/challenges/tfa/bosses/BossDataTracker.java index 179b7b4..94736a0 100644 --- a/src/main/java/io/blert/challenges/tfa/bosses/BossDataTracker.java +++ b/src/main/java/io/blert/challenges/tfa/bosses/BossDataTracker.java @@ -45,8 +45,8 @@ public abstract class BossDataTracker extends DataTracker { private static final int TFA_ORB1_VARBIT = 16387; private static final Pattern BOSS_START_REGEX = Pattern.compile("Challenge started: (.+)\\."); - private static final Pattern BOSS_END_REGEX = - Pattern.compile("Challenge complete: (.+?)\\. Duration: (" + Tick.TIME_STRING_REGEX + ")"); + private static final Pattern BOSS_END_REGEX = Pattern.compile("Challenge complete: (.+?)\\. Duration: (?" + + Tick.TIME_STRING_REGEX + ")(\\. Total: (?" + Tick.TIME_STRING_REGEX + "))?"); // Boss names as they appear in the start and completion messages. private static final Map BOSS_NAMES = Map.of( @@ -145,7 +145,7 @@ protected final void onMessage(ChatMessage chatMessage) { matcher = BOSS_END_REGEX.matcher(stripped); if (matcher.find() && isBossStartMessage(matcher.group(1))) { completionReceived = true; - finish(matcher.group(2)); + finish(matcher.group("duration"), matcher.group("total")); } } diff --git a/src/main/java/io/blert/client/WebSocketEventHandler.java b/src/main/java/io/blert/client/WebSocketEventHandler.java index a85ffdb..e51fb33 100644 --- a/src/main/java/io/blert/client/WebSocketEventHandler.java +++ b/src/main/java/io/blert/client/WebSocketEventHandler.java @@ -27,6 +27,7 @@ import io.blert.BlertPluginPanel; import io.blert.core.Challenge; import io.blert.core.ChallengeMode; +import io.blert.core.OverallTicks; import io.blert.core.RecordableChallenge; import io.blert.core.Stage; import io.blert.events.*; @@ -416,6 +417,7 @@ void updateChallenge(@Nullable ChallengeUpdateEvent challenge, @Nullable StageUp stageUpdate.recordedTicks = stage.getTick(); stageUpdate.gameTicksPrecise = stage.isGameTicksPrecise(); stage.getInGameTicks().ifPresent(t -> stageUpdate.gameServerTicks = t); + stage.getOverallTicks().ifPresent(t -> stageUpdate.overallTicks = translateOverallTicks(t)); challengeUpdate.stageUpdate = stageUpdate; } @@ -1072,4 +1074,16 @@ private static int translateStageStatus(StageUpdateEvent.Status status) { throw new NotImplementedException("Stage status translation not implemented for " + status); } } + + private static ChallengeUpdate.OverallTicks translateOverallTicks(OverallTicks overallTicks) { + ChallengeUpdate.OverallTicks json = new ChallengeUpdate.OverallTicks(); + if (overallTicks instanceof OverallTicks.Server) { + json.server = new ChallengeUpdate.OverallTicks.Server(); + json.server.ticks = overallTicks.getTicks(); + json.server.precise = ((OverallTicks.Server) overallTicks).isPrecise(); + } else { + json.local = overallTicks.getTicks(); + } + return json; + } } diff --git a/src/main/java/io/blert/core/DataTracker.java b/src/main/java/io/blert/core/DataTracker.java index 6ad0ad7..b76db71 100644 --- a/src/main/java/io/blert/core/DataTracker.java +++ b/src/main/java/io/blert/core/DataTracker.java @@ -35,6 +35,7 @@ import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.*; import net.runelite.client.callback.ClientThread; +import org.apache.commons.lang3.tuple.Pair; @Slf4j public abstract class DataTracker implements RuneliteEventHandler { @@ -50,6 +51,8 @@ private static class PendingCompletion { final boolean completion; final int inGameStageTicks; final boolean gameTicksPrecise; + final int inGameOverallTicks; + final boolean overallTicksPrecise; } @Getter(AccessLevel.PROTECTED) @@ -419,7 +422,7 @@ protected void start(int tickOffset) { // are interested in. npcScanPending = true; - dispatchEvent(StageUpdateEvent.started(getStage(), recordingConfig.getVersion())); + dispatchEvent(StageUpdateEvent.started(getStage(), challenge.getOverallTicks(), recordingConfig.getVersion())); } /** @@ -434,32 +437,60 @@ protected void finish(boolean completion) { } protected void finish(String inGameTime) { + finish(inGameTime, null); + } + + /** + * Marks the stage as complete with known times from the game server. + * + * @param inGameTime The stage's in-game time. + * @param inGameOverallTime The challenge's in-game overall time at the end of the stage, or null if not displayed. + */ + protected void finish(String inGameTime, @Nullable String inGameOverallTime) { var ticks = Tick.fromTimeString(inGameTime); - if (ticks.isPresent()) { - finish(true, ticks.get().getLeft(), ticks.get().getRight()); - } else { - finish(true); - } + Optional> overallTicks = + inGameOverallTime != null ? Tick.fromTimeString(inGameOverallTime) : Optional.empty(); + finish( + true, + ticks.map(Pair::getLeft).orElse(-1), + ticks.map(Pair::getRight).orElse(false), + overallTicks.map(Pair::getLeft).orElse(-1), + overallTicks.map(Pair::getRight).orElse(false)); + } + + protected void finish(boolean completion, int inGameStageTicks, boolean gameTicksPrecise) { + finish(completion, inGameStageTicks, gameTicksPrecise, -1, false); } /** * Marks the stage as complete, queueing cleanup to run at the end of the tick. * - * @param completion Whether the stage was completed successfully. - * @param inGameStageTicks The number of in-game ticks the stage took to complete, or -1 if the in-game timer is not - * available. If provided, it is used to verify the accuracy of the recorded stage time. - * @param gameTicksPrecise Whether the in-game tick timer is precise or rounded. + * @param completion Whether the stage was completed successfully. + * @param inGameStageTicks The number of in-game ticks the stage took to complete, or -1 if the in-game timer is + * not available. If provided, it is used to verify the accuracy of the recorded stage + * time. + * @param gameTicksPrecise Whether the in-game tick timer is precise or rounded. + * @param inGameOverallTicks The challenge's in-game overall time in ticks at the end of the stage, or -1 if not + * available. If provided, it is used to verify the challenge's overall time. + * @param overallTicksPrecise Whether the in-game overall time is precise or rounded. */ - protected void finish(boolean completion, int inGameStageTicks, boolean gameTicksPrecise) { + protected void finish( + boolean completion, + int inGameStageTicks, + boolean gameTicksPrecise, + int inGameOverallTicks, + boolean overallTicksPrecise) { switch (state) { case TERMINATING: log.debug("Forcefully terminating stage {}", stage); - pendingCompletion = new PendingCompletion(completion, inGameStageTicks, gameTicksPrecise); + pendingCompletion = new PendingCompletion( + completion, inGameStageTicks, gameTicksPrecise, inGameOverallTicks, overallTicksPrecise); completeStage(false); break; case IN_PROGRESS: if (pendingCompletion == null) { - pendingCompletion = new PendingCompletion(completion, inGameStageTicks, gameTicksPrecise); + pendingCompletion = new PendingCompletion( + completion, inGameStageTicks, gameTicksPrecise, inGameOverallTicks, overallTicksPrecise); } break; default: @@ -519,11 +550,18 @@ private void completeStage(boolean waitToDispatch) { log.info("Stage {} finished, status: {}", stage, status); Optional gameTicks = inGameStageTicks == -1 ? Optional.empty() : Optional.of(inGameStageTicks); + Optional overallTicks = + checkOverallTicks(pending.inGameOverallTicks, pending.overallTicksPrecise); Runnable dispatch = () -> challenge.dispatchEvent( completion ? StageUpdateEvent.completed( - getStage(), lastRecordedRoomTick, accurate, gameTicks, gameTicksPrecise) - : StageUpdateEvent.wiped(getStage(), lastRecordedRoomTick)); + getStage(), + lastRecordedRoomTick, + accurate, + gameTicks, + gameTicksPrecise, + overallTicks) + : StageUpdateEvent.wiped(getStage(), lastRecordedRoomTick, overallTicks)); if (waitToDispatch) { // Don't send the final room status immediately; allow other pending subscribers to run and dispatch @@ -536,6 +574,35 @@ private void completeStage(boolean waitToDispatch) { } } + /** + * Verifies the challenge's overall time against a time reported in game. + * + * @param inGameOverallTicks The in-game reported time, or -1 if not known. + * @param overallTicksPrecise Whether the in-game time is precise or rounded. + * @return The overall time to report. + */ + private Optional checkOverallTicks(int inGameOverallTicks, boolean overallTicksPrecise) { + Optional overall = challenge.getOverallTicks(); + if (inGameOverallTicks == -1) { + return overall; + } + if (overall.isEmpty()) { + return Optional.of(OverallTicks.server(inGameOverallTicks, overallTicksPrecise)); + } + + int ticks = overall.get().getTicks(); + if (overallTicksPrecise && ticks != inGameOverallTicks) { + log.warn( + "Stage {} overall time mismatch: in-game overall ticks = {}, challenge ticks = {}", + stage, + inGameOverallTicks, + ticks); + return Optional.of(OverallTicks.server(inGameOverallTicks, true)); + } + + return overall; + } + private void updatePlayers() { int tick = getTick(); diff --git a/src/main/java/io/blert/core/OverallTicks.java b/src/main/java/io/blert/core/OverallTicks.java new file mode 100644 index 0000000..2df65e3 --- /dev/null +++ b/src/main/java/io/blert/core/OverallTicks.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2026 Alexei Frolov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the “Software”), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.blert.core; + +import lombok.Getter; + +@Getter +public abstract class OverallTicks { + private final int ticks; + + /** + * An overall time reported by the game server. + */ + @Getter + public static final class Server extends OverallTicks { + /** + * Whether the time is precise, or rounded to the nearest second. + */ + private final boolean precise; + + private Server(int ticks, boolean precise) { + super(ticks); + this.precise = precise; + } + } + + /** + * An overall time measured by the plugin. + */ + public static final class Local extends OverallTicks { + private Local(int ticks) { + super(ticks); + } + } + + public static OverallTicks server(int ticks, boolean precise) { + return new Server(ticks, precise); + } + + public static OverallTicks local(int ticks) { + return new Local(ticks); + } + + private OverallTicks(int ticks) { + this.ticks = ticks; + } +} diff --git a/src/main/java/io/blert/core/RecordableChallenge.java b/src/main/java/io/blert/core/RecordableChallenge.java index 0c129fd..b604c87 100644 --- a/src/main/java/io/blert/core/RecordableChallenge.java +++ b/src/main/java/io/blert/core/RecordableChallenge.java @@ -127,6 +127,13 @@ protected RecordableChallenge(Challenge challenge, Client client, ClientThread c protected abstract @Nullable Stage getStage(); + /** + * Returns the challenge's overall time in ticks, if known. + */ + public Optional getOverallTicks() { + return Optional.empty(); + } + public String getName() { return challenge.getName(); } diff --git a/src/main/java/io/blert/events/StageUpdateEvent.java b/src/main/java/io/blert/events/StageUpdateEvent.java index 21b3bc6..ea6e0e2 100644 --- a/src/main/java/io/blert/events/StageUpdateEvent.java +++ b/src/main/java/io/blert/events/StageUpdateEvent.java @@ -23,6 +23,7 @@ package io.blert.events; +import io.blert.core.OverallTicks; import io.blert.core.Stage; import java.util.Optional; import lombok.Getter; @@ -40,25 +41,41 @@ public enum Status { private final boolean accurate; private final Optional inGameTicks; private final boolean gameTicksPrecise; + private final Optional overallTicks; private final Optional recordingConfigVersion; public static StageUpdateEvent entered(Stage stage) { - return new StageUpdateEvent(stage, 0, Status.ENTERED, false, Optional.empty(), false, Optional.empty()); + return new StageUpdateEvent( + stage, 0, Status.ENTERED, false, Optional.empty(), false, Optional.empty(), Optional.empty()); } - public static StageUpdateEvent started(Stage stage, int recordingConfigVersion) { + public static StageUpdateEvent started( + Stage stage, Optional overallTicks, int recordingConfigVersion) { return new StageUpdateEvent( - stage, 0, Status.STARTED, false, Optional.empty(), false, Optional.of(recordingConfigVersion)); + stage, + 0, + Status.STARTED, + false, + Optional.empty(), + false, + overallTicks, + Optional.of(recordingConfigVersion)); } public static StageUpdateEvent completed( - Stage stage, int tick, boolean accurate, Optional inGameTicks, boolean gameTicksPrecise) { + Stage stage, + int tick, + boolean accurate, + Optional inGameTicks, + boolean gameTicksPrecise, + Optional overallTicks) { return new StageUpdateEvent( - stage, tick, Status.COMPLETED, accurate, inGameTicks, gameTicksPrecise, Optional.empty()); + stage, tick, Status.COMPLETED, accurate, inGameTicks, gameTicksPrecise, overallTicks, Optional.empty()); } - public static StageUpdateEvent wiped(Stage stage, int tick) { - return new StageUpdateEvent(stage, tick, Status.WIPED, false, Optional.empty(), false, Optional.empty()); + public static StageUpdateEvent wiped(Stage stage, int tick, Optional overallTicks) { + return new StageUpdateEvent( + stage, tick, Status.WIPED, false, Optional.empty(), false, overallTicks, Optional.empty()); } private StageUpdateEvent( @@ -68,12 +85,14 @@ private StageUpdateEvent( boolean accurate, Optional inGameTicks, boolean gameTicksPrecise, + Optional overallTicks, Optional recordingConfigVersion) { super(EventType.STAGE_UPDATE, stage, tick, null); this.status = status; this.accurate = accurate; this.inGameTicks = inGameTicks; this.gameTicksPrecise = gameTicksPrecise; + this.overallTicks = overallTicks; this.recordingConfigVersion = recordingConfigVersion; } diff --git a/src/main/java/io/blert/json/ChallengeUpdate.java b/src/main/java/io/blert/json/ChallengeUpdate.java index 7a61f12..9b738f0 100644 --- a/src/main/java/io/blert/json/ChallengeUpdate.java +++ b/src/main/java/io/blert/json/ChallengeUpdate.java @@ -42,5 +42,16 @@ public static class StageUpdate { public int recordedTicks; public Integer gameServerTicks; public boolean gameTicksPrecise; + public OverallTicks overallTicks; + } + + public static class OverallTicks { + public static class Server { + public int ticks; + public boolean precise; + } + + public Server server; + public Integer local; } }