Skip to content

Commit 3b76d79

Browse files
committed
misc: add min replay size threshold when splitting replays
1 parent e5b86f3 commit 3b76d79

File tree

1 file changed

+37
-16
lines changed

1 file changed

+37
-16
lines changed

scripts/split_replay.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
#
88
# If splitting a replay test, be sure to delete the original and the qst file (which was copied into the output folder).
99

10-
# TODO: figure out why there are issues with replaying hero_of_dreams.zplay when split up.
11-
1210
import argparse
1311
from dataclasses import dataclass, field
1412
import os
@@ -32,14 +30,17 @@ class ReplayStep:
3230
@dataclass
3331
class ReplayPart:
3432
steps: List[ReplayStep] = field(default_factory=list)
33+
save_index: int = 0
3534
last_key_step: Optional[ReplayStep] = None
3635

3736

38-
def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generation: bool):
37+
def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generation: bool, split_threshold: int):
3938
meta = []
4039
replay_parts = [ReplayPart()]
4140
current_part = replay_parts[0]
41+
previous_part = None
4242
qst_path = Path()
43+
save_index = 0
4344
with replay_path.open('r', encoding='utf-8') as f:
4445
for line in f:
4546
line = line.strip()
@@ -55,41 +56,58 @@ def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generati
5556

5657
if type == 'K':
5758
current_part.last_key_step = step
59+
if previous_part and previous_part.last_key_step.data == step.data:
60+
continue
5861

5962
# We can only split at points where the game engine writes everything to disk via a save file,
6063
# in the game_over function.
6164
if type == 'C' and data == 'init_game' and step.frame != 0 and current_part.steps[-1].data == 'save game':
62-
current_part = ReplayPart()
63-
replay_parts.append(current_part)
65+
# Don't make sub-replays with too little frames. Too many small replays might make more overhead than it saves.
66+
if len(current_part.steps) > split_threshold:
67+
previous_part = current_part
68+
current_part = ReplayPart()
69+
current_part.save_index = save_index
70+
replay_parts.append(current_part)
71+
save_index += 1
6472

6573
current_part.steps.append(step)
6674

6775
total = len(replay_parts)
6876
if total == 1:
6977
raise Exception('Nothing to split. Either the replay file has no saves, or it is missing "save game" comments (in which case you must update it)')
78+
print(f'will split into {total} replays')
79+
maxcol1 = len(str(total))
80+
maxcol2 = len(str(save_index))
81+
for i, part in enumerate(replay_parts):
82+
col1 = str(i + 1).rjust(maxcol1, ' ')
83+
col2 = str(part.save_index).rjust(maxcol2, ' ')
84+
print(f'[{col1}] save {col2} {len(part.steps)}')
7085

7186
build_folder = run_target.get_build_folder()
7287
saves_folder = build_folder / 'saves/current_replay'
7388
if not skip_save_file_generation:
7489
if saves_folder.exists():
7590
shutil.rmtree(saves_folder)
91+
print('running replay w/ -replay-save-games, this may take a few minutes ...')
7692
run_target.run('zplayer', [
93+
'-headless',
7794
'-v0',
7895
'-replay-exit-when-done',
7996
'-replay-save-games',
8097
'-replay', replay_path.absolute(),
81-
])
82-
save_files = list(saves_folder.rglob('*.sav'))
83-
if total - 1 != len(save_files):
84-
raise Exception(f'expected {total - 1} save files, but got {len(save_files)}')
98+
], build_folder)
99+
save_files = list(saves_folder.rglob('*.sav'))
100+
if save_index + 1 != len(save_files):
101+
raise Exception(f'expected {save_index} save files, but got {len(save_files)}')
85102

86103
output_folder.mkdir(exist_ok=True)
87104
# qst file may not be relative to the replay file (ex: quests/Z1 Recreations/classic_1st.qst)
88105
if qst_path.exists():
89106
shutil.copy(qst_path, output_folder)
90107
most_recent_key_step = None
91-
print(f'split into {total} replays:')
92108
for i, part in enumerate(replay_parts):
109+
is_first = i == 0
110+
is_last = i == len(replay_parts) - 1
93111
digits_len = len(str(total))
94112
number_part = f'{(i + 1):0{digits_len}}_of_{total}'
95113
output_replay = output_folder / f'{replay_path.stem}_{number_part}.zplay'
@@ -101,7 +119,7 @@ def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generati
101119
# The hero position is only emitted if the previous frame had a different value.
102120
# When splitting a replay, the previous part's last hero position should be added
103121
# on the first frame.
104-
if i > 0:
122+
if not is_first:
105123
# If the first frame does have a hero position comment, then we are expecting this
106124
# new value. Do nothing.
107125
has_frame_zero_position = False
@@ -142,17 +160,17 @@ def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generati
142160
f.write(line)
143161
f.write('\n')
144162
if line.startswith('M qst '):
145-
if i != 0:
163+
if not is_first:
146164
save_file_name = f'{output_replay.stem}.sav'
147165
f.write('M sav ')
148166
f.write(save_file_name)
149167
f.write('\n')
150-
if not skip_save_file_generation:
151-
shutil.copy(save_files[i - 1], output_folder / save_file_name)
168+
if save_files:
169+
shutil.copy(save_files[part.save_index], output_folder / save_file_name)
152170

153171
# Not used yet, but perhaps would be useful to have a feature to continue playing the next replay
154172
# in a series.
155-
if i != total - 1:
173+
if not is_last:
156174
f.write('M next ')
157175
number_part = f'{(i + 2):0{digits_len}}_of_{total}'
158176
f.write(f'{replay_path.stem}_{number_part}.zplay')
@@ -168,6 +186,8 @@ def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generati
168186
f.write('\n')
169187

170188
for step in part.steps:
189+
if step.frame == 0 and step.type == 'K' and most_recent_key_step and step.data == most_recent_key_step.data:
190+
continue
171191
f.write(' '.join([step.type, str(step.frame), step.data]))
172192
f.write('\n')
173193

@@ -182,6 +202,7 @@ def split_replay(replay_path: Path, output_folder: Path, skip_save_file_generati
182202
parser.add_argument('--replay', required=True)
183203
parser.add_argument('--output-folder', required=True)
184204
parser.add_argument('--skip-save-file-generation', action='store_true', help='Only use this skip if already generated in output folder!')
205+
parser.add_argument('--split-threshold', default=100_000, help='If current replay part length at a save point is less than this threshold, the replay will not be cut. It may be long enough on the next save.')
185206

186207
args = parser.parse_args()
187-
split_replay(Path(args.replay), Path(args.output_folder), skip_save_file_generation=args.skip_save_file_generation)
208+
split_replay(Path(args.replay), Path(args.output_folder), skip_save_file_generation=args.skip_save_file_generation, split_threshold=args.split_threshold)

0 commit comments

Comments
 (0)