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-
1210import argparse
1311from dataclasses import dataclass , field
1412import os
@@ -32,14 +30,17 @@ class ReplayStep:
3230@dataclass
3331class 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