@@ -126,6 +126,7 @@ class ReplayEngine {
126126 int deferredReleaseA[2 ] = { -1 , -1 };
127127 int deferredInputTick[2 ] = { -1 , -1 };
128128 int deferredReleaseB[2 ][2 ] = { { -1 , -1 }, { -1 , -1 } };
129+ std::array<std::array<bool , 3 >, 2 > checkpointResumedHolds = {};
129130 bool activeButtons[6 ] = { false , false , false , false , false , false };
130131 bool priorButtonState[6 ] = { false , false , false , false , false , false };
131132 bool lateralInputPending[2 ] = { false , false };
@@ -168,79 +169,76 @@ class ReplayEngine {
168169 cbfMacros.clear ();
169170 ttrMacros.clear ();
170171 auto directory = geode::prelude::Mod::get ()->getSaveDir () / " replays" ;
171- if (!std::filesystem::exists (directory)) {
172+ std::error_code ec;
173+ if (!std::filesystem::exists (directory, ec) || ec) {
172174 return ;
173175 }
174176
175- for (auto & entry : std::filesystem::directory_iterator (directory)) {
176- if (!entry.is_regular_file ()) {
177+ auto markIncompatible = [&](std::string const & stem) {
178+ storedMacros.push_back (stem);
179+ incompatibleMacros.insert (stem);
180+ };
181+
182+ for (std::filesystem::directory_iterator it (directory, ec), end; !ec && it != end; it.increment (ec)) {
183+ if (!it->is_regular_file ()) {
177184 continue ;
178185 }
179186
180- std::string stem = entry.path ().stem ().string ();
181- auto extension = entry.path ().extension ();
187+ auto path = it->path ();
188+ auto stem = path.stem ().string ();
189+ auto extension = path.extension ();
182190
183191 if (extension == " .ttr" ) {
184- std::ifstream input (entry.path (), std::ios::binary);
185- if (!input.is_open ()) {
186- storedMacros.push_back (stem);
187- incompatibleMacros.insert (stem);
192+ auto bytes = ReplayStorage::readReplayBytes (path);
193+ if (!bytes || bytes->size () < 10 ) {
194+ markIncompatible (stem);
188195 continue ;
189196 }
190197
191- char header[10 ] = {};
192- input.read (header, 10 );
193- input.close ();
198+ auto const & header = *bytes;
194199 if (header[0 ] == ' T' && header[1 ] == ' T' && header[2 ] == ' R' && header[3 ] == ' \0 ' ) {
195200 storedMacros.push_back (stem);
196201 ttrMacros.insert (stem);
197202 uint32_t flags = 0 ;
198- std::memcpy (&flags, header + 6 , sizeof (uint32_t ));
203+ std::memcpy (&flags, header. data () + 6 , sizeof (uint32_t ));
199204 if ((flags & TTR_FLAG_ACCURACY_CBF ) != 0 ) {
200205 cbfMacros.insert (stem);
201206 } else if ((flags & TTR_FLAG_ACCURACY_CBS ) != 0 ) {
202207 cbsMacros.insert (stem);
203208 }
204209 } else {
205- storedMacros.push_back (stem);
206- incompatibleMacros.insert (stem);
210+ markIncompatible (stem);
207211 }
208212 continue ;
209213 }
210214
211215 if (extension != " .gdr" ) {
212- storedMacros.push_back (stem);
213- incompatibleMacros.insert (stem);
216+ markIncompatible (stem);
214217 continue ;
215218 }
216219
217- std::ifstream input (entry.path (), std::ios::binary);
218- if (!input.is_open ()) {
219- storedMacros.push_back (stem);
220- incompatibleMacros.insert (stem);
220+ auto bytes = ReplayStorage::readReplayBytes (path);
221+ if (!bytes) {
222+ markIncompatible (stem);
221223 continue ;
222224 }
223225
224- input.seekg (0 , std::ios::end);
225- auto fileSize = input.tellg ();
226- input.seekg (0 , std::ios::beg);
227- std::vector<uint8_t > bytes (static_cast <size_t >(fileSize));
228- input.read (reinterpret_cast <char *>(bytes.data ()), fileSize);
229- input.close ();
230-
231226 try {
232- MacroSequence temp = MacroSequence::importData (bytes);
227+ MacroSequence temp = MacroSequence::importData (* bytes);
233228 storedMacros.push_back (stem);
234229 if (temp.accuracyMode == AccuracyMode::CBF ) {
235230 cbfMacros.insert (stem);
236231 } else if (temp.accuracyMode == AccuracyMode::CBS ) {
237232 cbsMacros.insert (stem);
238233 }
239234 } catch (...) {
240- storedMacros.push_back (stem);
241- incompatibleMacros.insert (stem);
235+ markIncompatible (stem);
242236 }
243237 }
238+
239+ if (ec) {
240+ log::warn (" Failed while scanning replay directory {}: {}" , directory.string (), ec.message ());
241+ }
244242 }
245243
246244 void beginCapture (GJGameLevel* level) {
@@ -536,6 +534,32 @@ class ReplayEngine {
536534 return rng;
537535 }
538536
537+ void clearCheckpointResumedHolds () {
538+ for (auto & playerHolds : checkpointResumedHolds) {
539+ playerHolds.fill (false );
540+ }
541+ }
542+
543+ bool isCheckpointHoldResumed (int playerSlot, int button) const {
544+ if (playerSlot < 0 || playerSlot >= static_cast <int >(checkpointResumedHolds.size ())) {
545+ return false ;
546+ }
547+ if (button < 1 || button > 3 ) {
548+ return false ;
549+ }
550+ return checkpointResumedHolds[playerSlot][static_cast <size_t >(button - 1 )];
551+ }
552+
553+ void setCheckpointHoldResumed (int playerSlot, int button, bool resumed) {
554+ if (playerSlot < 0 || playerSlot >= static_cast <int >(checkpointResumedHolds.size ())) {
555+ return ;
556+ }
557+ if (button < 1 || button > 3 ) {
558+ return ;
559+ }
560+ checkpointResumedHolds[playerSlot][static_cast <size_t >(button - 1 )] = resumed;
561+ }
562+
539563 Renderer renderer;
540564
541565 static ReplayEngine* get () {
0 commit comments