From 71bfc9dba022072dc7da8c7fbdb5012c9aa11df0 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:51:58 -0500 Subject: [PATCH 1/2] Replays: Support real-time playback --- play.pokemonshowdown.com/src/battle.ts | 41 +++++++++++++++++-- replay.pokemonshowdown.com/replays.lib.php | 8 ++-- .../src/replays-battle.tsx | 11 ++++- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/play.pokemonshowdown.com/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts index 91154fa6c..783892b1a 100644 --- a/play.pokemonshowdown.com/src/battle.ts +++ b/play.pokemonshowdown.com/src/battle.ts @@ -1135,6 +1135,9 @@ export class Battle { * paused, not just waiting for the opponent to make a move. */ paused: boolean; + realtime = false; + timeWait: number | null = null; + timerCount = 0; constructor(options: { $frame?: JQuery, @@ -3393,6 +3396,32 @@ export class Battle { } runMajor(args: Args, kwArgs: KWArgs, preempt?: boolean) { switch (args[0]) { + case 't:': { + if (this.realtime && this.timerCount) { + if (this.timeWait) { + setTimeout(() => { + this.timeWait = null; + this.nextStep(); + }, this.timeWait); + return true; + } + const curStep = this.stepQueue.indexOf(`|t:|${args[1]}`); + const curTime = Number(args[1]); + + const extractTime = (line: string) => Number(line.split('|')[2]); + const nextStep = this.stepQueue.findIndex((line, index) => { + if (index < curStep) return false; + const parts = line.split('|'); + return parts[1] === 't:' && extractTime(line) > curTime; + }); + if (nextStep > -1) { + const nextTime = extractTime(this.stepQueue[nextStep]); + this.timeWait = (nextTime - curTime) * 1000; + } + } + this.timerCount++; + break; + } case 'start': { this.nearSide.active[0] = null; this.farSide.active[0] = null; @@ -3793,14 +3822,18 @@ export class Battle { if (args[0].startsWith('-') || args[0] === 'detailschange') { this.runMinor(args, kwArgs, nextArgs, nextKwargs); } else { - this.runMajor(args, kwArgs, preempt); + if (this.runMajor(args, kwArgs, preempt)) { + return true; + } } } else { try { if (args[0].startsWith('-') || args[0] === 'detailschange') { this.runMinor(args, kwArgs, nextArgs, nextKwargs); } else { - this.runMajor(args, kwArgs, preempt); + if (this.runMajor(args, kwArgs, preempt)) { + return true; + } } } catch (err: any) { this.log(['majorerror', 'Error parsing: ' + str + ' (' + err + ')']); @@ -3923,7 +3956,9 @@ export class Battle { return; } - this.run(this.stepQueue[this.currentStep]); + if (this.run(this.stepQueue[this.currentStep])) { + break; + } this.currentStep++; if (this.waitForAnimations === true) { animations = this.scene.finishAnimations(); diff --git a/replay.pokemonshowdown.com/replays.lib.php b/replay.pokemonshowdown.com/replays.lib.php index 2a93d928a..d9e13bc33 100644 --- a/replay.pokemonshowdown.com/replays.lib.php +++ b/replay.pokemonshowdown.com/replays.lib.php @@ -56,25 +56,25 @@ class Replays { $res = $this->db->prepare("UPDATE replays SET private = 3, password = NULL WHERE id = ? LIMIT 1"); $res->execute([$replay['id']]); $res = $this->db->prepare("UPDATE replayplayers SET private = 3, password = NULL WHERE id = ?"); - $res->execute([$replay['id']]) + $res->execute([$replay['id']]); } else if ($replay['private'] === 2) { $replay['private'] = 1; $replay['password'] = NULL; $res = $this->db->prepare("UPDATE replays SET private = 1, password = NULL WHERE id = ? LIMIT 1"); $res->execute([$replay['id']]); $res = $this->db->prepare("UPDATE replayplayers SET private = 1, password = NULL WHERE id = ?"); - $res->execute([$replay['id']]) + $res->execute([$replay['id']]); } else if ($replay['private']) { if (!$replay['password']) $replay['password'] = $this->genPassword(); $res = $this->db->prepare("UPDATE replays SET private = 1, password = ? WHERE id = ? LIMIT 1"); $res->execute([$replay['password'], $replay['id']]); $res = $this->db->prepare("UPDATE replayplayers SET private = 1, password = ? WHERE id = ?"); - $res->execute([$replay['password'], $replay['id']]) + $res->execute([$replay['password'], $replay['id']]); } else { $res = $this->db->prepare("UPDATE replays SET private = 0, password = NULL WHERE id = ? LIMIT 1"); $res->execute([$replay['id']]); $res = $this->db->prepare("UPDATE replayplayers SET private = 0, password = NULL WHERE id = ?"); - $res->execute([$replay['id']]) + $res->execute([$replay['id']]); } return; } diff --git a/replay.pokemonshowdown.com/src/replays-battle.tsx b/replay.pokemonshowdown.com/src/replays-battle.tsx index b5514a4e6..7aec5b562 100644 --- a/replay.pokemonshowdown.com/src/replays-battle.tsx +++ b/replay.pokemonshowdown.com/src/replays-battle.tsx @@ -296,6 +296,8 @@ export class BattlePanel extends preact.Component<{ id: string }> { return 'slow'; } else if (this.battle.messageFadeTime >= 1000) { return 'reallyslow'; + } else if (this.battle.realtime) { + return 'realtime'; } return 'normal'; } @@ -316,6 +318,12 @@ export class BattlePanel extends preact.Component<{ id: string }> { reallyslow: 3000, }; if (!this.battle) return; + if (speed === 'realtime') { + this.battle.realtime = true; + this.battle.resetToCurrentTurn(); + return; + } + this.battle.realtime = false; this.battle.messageShownTime = delayTable[speed as 'fast']; this.battle.messageFadeTime = fadeTable[speed as 'fast']; this.battle.scene.updateAcceleration(); @@ -323,7 +331,7 @@ export class BattlePanel extends preact.Component<{ id: string }> { stepSpeed(delta: number) { const target = this.base?.querySelector('select[name=speed]'); if (!target) return; // should never happen - const values = ['reallyslow', 'slow', 'normal', 'fast', 'hyperfast']; + const values = ['realtime', 'reallyslow', 'slow', 'normal', 'fast', 'hyperfast']; const newValue = values[values.indexOf(target.value) + delta]; if (newValue) { target.value = newValue; @@ -471,6 +479,7 @@ export class BattlePanel extends preact.Component<{ id: string }> { + {}