JaffarPlus
High-performance best-first search optimizer for tool-assisted speedruns
Loading...
Searching...
No Matches
player.cpp
Go to the documentation of this file.
1
13#include "emulator.hpp"
14#include "game.hpp"
15#include "playback.hpp"
16#include "runner.hpp"
17#include <argparse/argparse.hpp>
18#include <chrono>
19#include <emulatorList.hpp>
20#include <gameList.hpp>
21#include <jaffarCommon/json.hpp>
22#include <jaffarCommon/logger.hpp>
23#include <jaffarCommon/string.hpp>
24#include <set>
25
27
29
31
33
34size_t frameskip;
35
36std::string runCommand;
37
41
45std::string dumpHashesPath;
46
50std::string dumpRamPath;
51
54std::string dumpRewardPath;
55
58std::string dumpTracePath;
59
62std::string saveStateStepStr;
63
67
69std::string screenshotDir;
71std::set<size_t> screenshotSteps;
72
81static size_t parseUInt(const std::string& value, const std::string& flag)
82{
83 try
84 {
85 size_t consumed = 0;
86 const size_t result = std::stoul(value, &consumed);
87 if (consumed == value.size()) return result;
88 }
89 catch (const std::exception&)
90 {
91 }
92 JAFFAR_THROW_LOGIC("Invalid value '%s' for %s (expected a non-negative integer)\n", value.c_str(), flag.c_str());
93 return 0;
94}
95
116bool mainCycle(jaffarPlus::Runner& r, const std::string& solutionFile, bool disableRender)
117{
118 // If sequence file defined, load it and play it
119 std::string solutionFileString;
120 if (jaffarCommon::file::loadStringFromFile(solutionFileString, solutionFile) == false)
121 JAFFAR_THROW_LOGIC("[ERROR] Could not find or read from solution sequence file: %s\n", solutionFile.c_str());
122
123 // Getting input sequence
124 const auto solutionSequence = jaffarCommon::string::split(solutionFileString, '\0');
125
126 // Variable for current step in view
127 ssize_t currentStep = 0;
128
129 // Getting sequence length
130 const ssize_t sequenceLength = solutionSequence.size();
131
132 // Getting inverse frame rate from game
133 const auto frameRate = r.getGame()->getFrameRate();
134 const uint32_t inverseFrameRate = std::round((1.0 / frameRate) * 1.0e+6);
135
136 // Getting game state size
137 const auto stateSize = r.getStateSize();
138
139 // Printing information
140 jaffarCommon::logger::refreshTerminal();
141
142 // Instantiating playback instance
144
145 // Initializing playback instance
146 p.initialize(solutionSequence);
147
148 // Flag to display frame information
149 bool showFrameInfo = true;
150
151 // Finalization flag
152 bool isFinalize = false;
153
154 // Checking for repeated state hashes
155 std::vector<ssize_t> repeatedHashStates;
156 for (ssize_t i = 0; i < sequenceLength; i++)
157 {
158 const auto repeatedHashSteps = p.getStateRepeatedHashSteps(i);
159 if (repeatedHashSteps.size() > 0) repeatedHashStates.push_back(i);
160 }
161
162 // Checking for not-allowed inputs
163 std::vector<ssize_t> notAllowedInputStates;
164 for (ssize_t i = 0; i < sequenceLength; i++)
165 {
166 const auto isInputAllowed = p.isInputAllowed(i);
167 if (isInputAllowed == false) notAllowedInputStates.push_back(i);
168 }
169
170 // If requested, dump the per-step game-state hash for every step (including the end-of-sequence
171 // step) to a file. Diffing the dumps of two emulators replaying the same solution pinpoints the
172 // exact first frame at which their hashed game RAM diverges.
173 if (dumpHashesPath.empty() == false)
174 {
175 std::string dump;
176 char line[64];
177 for (ssize_t i = 0; i <= sequenceLength; i++)
178 {
179 const auto hash = p.getStateHash(i);
180 snprintf(line, sizeof(line), "%ld\t%016lX%016lX\n", i, hash.first, hash.second);
181 dump += line;
182 }
183 if (jaffarCommon::file::saveStringToFile(dump, dumpHashesPath.c_str()) == false)
184 JAFFAR_THROW_LOGIC("[ERROR] Could not write per-step hash dump to: %s\n", dumpHashesPath.c_str());
185 }
186
187 // If requested, dump the full low work-RAM for every step as a flat binary blob. Reading the RAM
188 // requires restoring each step's state into the live emulator first (loadStepData), so this mutates
189 // the live state -- harmless here since the interactive loop re-loads per step regardless.
190 if (dumpRamPath.empty() == false)
191 {
192 const auto lram = r.getGame()->getEmulator()->getProperty("LRAM");
193 std::string dump;
194 dump.reserve((size_t)(sequenceLength + 1) * lram.size);
195 for (ssize_t i = 0; i <= sequenceLength; i++)
196 {
197 p.loadStepData(i);
198 dump.append((const char*)lram.pointer, lram.size);
199 }
200 if (jaffarCommon::file::saveStringToFile(dump, dumpRamPath.c_str()) == false) JAFFAR_THROW_LOGIC("[ERROR] Could not write per-step RAM dump to: %s\n", dumpRamPath.c_str());
201 }
202
203 // If requested, write the per-step game reward (one value per line) to a file. The reward is part of the
204 // serialized game state, so loadStepData restores each step's reward directly -- this is exactly the value
205 // the search compares against, suitable as a "Reference Reward Floor" trace.
206 if (dumpRewardPath.empty() == false)
207 {
208 std::string dump;
209 for (ssize_t i = 0; i <= sequenceLength; i++)
210 {
211 p.loadStepData(i);
212 // Full precision (NOT std::to_string, which truncates to 6 decimals): the reward is on the 1/256
213 // sub-pixel grid, so 6 decimals (e.g. 0.246094) does not round-trip the exact value (0.24609375),
214 // which makes a "Reference Reward Floor" with tolerance 0 false-cancel on an EXACT match. %.17g
215 // preserves the value so a tol=0 floor only cancels when the search is genuinely behind.
216 char rbuf[64];
217 snprintf(rbuf, sizeof(rbuf), "%.17g", (double)r.getGame()->getReward());
218 dump += std::string(rbuf) + "\n";
219 }
220 if (jaffarCommon::file::saveStringToFile(dump, dumpRewardPath.c_str()) == false)
221 JAFFAR_THROW_LOGIC("[ERROR] Could not write per-step reward dump to: %s\n", dumpRewardPath.c_str());
222 }
223
224 // If requested, write the game's per-step trace line (Game::getTraceLine) to a file, one line per step. Like the
225 // reward dump, loadStepData restores each step's full state first, so the coordinates are exact. Suitable as a
226 // game "Trace File Path" for the trace magnet.
227 if (dumpTracePath.empty() == false)
228 {
229 std::string dump;
230 for (ssize_t i = 0; i <= sequenceLength; i++)
231 {
232 p.loadStepData(i);
233 dump += r.getGame()->getTraceLine() + "\n";
234 }
235 if (jaffarCommon::file::saveStringToFile(dump, dumpTracePath.c_str()) == false)
236 JAFFAR_THROW_LOGIC("[ERROR] Could not write per-step trace dump to: %s\n", dumpTracePath.c_str());
237 }
238
239 // If requested, restore the state at a single step and save the emulator's FULL state to a file (for use
240 // as the Emulator "Initial State File Path" -- a mid-run seed). Prints the bike posX so the caller can set
241 // the game's "Initial Block Transitions" to make _bikePosX absolute. Then exits.
242 if (saveStateFilePath.empty() == false)
243 {
244 const auto step = (ssize_t)parseUInt(saveStateStepStr, "--saveStateStep");
245 p.loadStepData(step);
246 std::string saveData;
247 const size_t stateSize = r.getGame()->getEmulator()->getStateSize();
248 saveData.resize(stateSize);
249 jaffarCommon::serializer::Contiguous s(saveData.data(), stateSize);
251 if (jaffarCommon::file::saveStringToFile(saveData, saveStateFilePath.c_str()) == false)
252 JAFFAR_THROW_LOGIC("[ERROR] Could not write state at step %ld to: %s\n", (long)step, saveStateFilePath.c_str());
253 jaffarCommon::logger::log("[J+] Saved emulator state at step %ld to %s (%lu bytes)\n", (long)step, saveStateFilePath.c_str(), stateSize);
254 r.getGame()->printInfo();
255 return 0;
256 }
257
258 // Interactive section
259 while (isFinalize == false)
260 {
261 // Updating display
262 if (disableRender == false)
263 if (currentStep % frameskip == 0)
264 {
265 p.renderFrame(currentStep);
266
267 // Capture this frame if requested (whole list, or every rendered frame if no list). The
268 // actual save is emulator-specific (no-op for emulators without a screenshot backend), so
269 // the player stays emulator-agnostic and links without SDL/SDLPoP symbols.
270 if (screenshotDir.empty() == false && (screenshotSteps.empty() || screenshotSteps.count((size_t)currentStep) > 0))
271 {
272 char path[1024];
273 snprintf(path, sizeof(path), "%s/step_%06ld.bmp", screenshotDir.c_str(), currentStep);
274 r.getGame()->getEmulator()->saveScreenshot(path);
275 }
276 }
277
278 // Loading step data
279 p.loadStepData(currentStep);
280
281 // Getting input string
282 const auto& inputString = p.getStateInputString(currentStep);
283
284 // Getting input index
285 const auto& inputIndex = p.getStateInputIndex(currentStep);
286
287 // Getting state hash
288 const auto hash = p.getStateHash(currentStep);
289
290 // Getting repeated step hashes (if any)
291 const auto repeatedHashSteps = p.getStateRepeatedHashSteps(currentStep);
292
293 // Checking if the current input is within the allowed inputs for this state
294 const auto isInputAllowed = p.isInputAllowed(currentStep);
295
296 // If running a command, don't print frame info, and finalize immediately after
297 if (runCommand != "")
298 {
299 isFinalize = true;
300 showFrameInfo = false;
301 isReproduce = false;
302 isUnattended = true;
303 }
304
305 // Printing data and commands
306 if (showFrameInfo)
307 {
308 jaffarCommon::logger::clearTerminal();
309
310 jaffarCommon::logger::log("[J+] ----------------------------------------------------------------\n");
311 jaffarCommon::logger::log("[J+] Current Step #: %lu / %lu\n", currentStep, sequenceLength);
312 jaffarCommon::logger::log("[J+] Playback: %s\n", isReproduce ? "Playing" : "Stopped");
313 jaffarCommon::logger::log("[J+] Input: %s (0x%X)\n", inputString.c_str(), inputIndex);
314 jaffarCommon::logger::log("[J+] On Finish: %s\n", isReload ? "Auto Reload" : "Stop");
315
316 jaffarCommon::logger::log("[J+] Repeated Hash Steps: %lu total [ ", repeatedHashStates.size());
317 if (repeatedHashStates.size() < 5)
318 for (const auto step : repeatedHashStates) jaffarCommon::logger::log(" %ld ", step);
319 else
320 {
321 for (size_t i = 0; i < 5; i++) jaffarCommon::logger::log(" %ld ", repeatedHashStates[i]);
322 jaffarCommon::logger::log(" ... ");
323 }
324 jaffarCommon::logger::log(" ] \n");
325
326 jaffarCommon::logger::log("[J+] Not Allowed Input Steps: %lu total [ ", notAllowedInputStates.size());
327 if (notAllowedInputStates.size() < 5)
328 for (const auto step : notAllowedInputStates) jaffarCommon::logger::log(" %ld ", step);
329 else
330 {
331 for (size_t i = 0; i < 5; i++) jaffarCommon::logger::log(" %ld ", notAllowedInputStates[i]);
332 jaffarCommon::logger::log(" ... ");
333 }
334 jaffarCommon::logger::log(" ] \n");
335
336 jaffarCommon::logger::log("[J+] Game Name: '%s'\n", r.getGame()->getName().c_str());
337 jaffarCommon::logger::log("[J+] Emulator Name: '%s'\n", r.getGame()->getEmulator()->getName().c_str());
338 jaffarCommon::logger::log("[J+] State Hash: 0x%lX%lX\n", hash.first, hash.second);
339 jaffarCommon::logger::log("[J+] State Repeated Hash Steps: [ ");
340 for (const auto step : repeatedHashSteps) jaffarCommon::logger::log(" %lu ", step);
341 jaffarCommon::logger::log(" ] \n");
342 jaffarCommon::logger::log("[J+] Is Input Allowed: %s\n", isInputAllowed ? "True" : "False");
343 jaffarCommon::logger::log("[J+] State Size: %lu\n", stateSize);
344 jaffarCommon::logger::log("[J+] Solution File: '%s'\n", solutionFile.c_str());
345 jaffarCommon::logger::log("[J+] Sequence Length: %lu\n", sequenceLength);
346 jaffarCommon::logger::log("[J+] Frame Rate: %f (%u)\n", frameRate, inverseFrameRate);
347 jaffarCommon::logger::log("[J+] Checkpoint: Level: %lu, Tolerance: %lu\n", r.getGame()->getCheckpointLevel(), r.getGame()->getCheckpointTolerance());
348 jaffarCommon::logger::log("[J+] Manual Save Solution: Active: %s, Path: '%s', Last Rule: (Current: %ld), (Prev: %ld)\n", r.getGame()->isSaveSolution() ? "Yes" : "No",
350 p.printInfo();
351
352 // Print General Commands
353 jaffarCommon::logger::log("[J+] Commands: n: -1 m: +1 | h: -10 | j: +10 | y: -100 | u: +100 | k: -1000 | i: +1000 | s: quicksave | p: play | r: autoreload | q: quit\n");
354
355 // Print any game-specific commands (optional)
357
358 jaffarCommon::logger::refreshTerminal();
359 }
360
361 // Resetting show frame info flag
362 showFrameInfo = true;
363
364 // Specifies the command to execute next
365 int command = 0;
366
367 // If it's reproducing,
368 if (isReproduce == true)
369 {
370 // Introducing sleep related to the frame rate
371 usleep(inverseFrameRate);
372
373 // Advance to the next frame
374 currentStep++;
375
376 // Get command without interrupting
377 command = jaffarCommon::logger::getKeyPress();
378 }
379
380 // If it's not reproducing, grab command with a wait
381 if (isReproduce == false && isUnattended == false) command = jaffarCommon::logger::waitForKeyPress();
382
383 // Headless fast-forward: when unattended and not actively reproducing (and not running a one-shot
384 // command), advance through the sequence as fast as possible -- no key wait, no frame-rate sleep.
385 // This is what makes --unattended --exitOnEnd terminate promptly for batch/verification runs
386 // (otherwise neither branch above advances and the loop spins forever).
387 if (isReproduce == false && isUnattended == true && runCommand == "") currentStep++;
388
389 // If running a command given from the console, set it now
390 if (runCommand != "") command = runCommand[0];
391
392 // Handle commands
393 switch (command)
394 {
395 // Advance/Rewind commands
396 case 'n': currentStep = currentStep - 1; break;
397 case 'm': currentStep = currentStep + 1; break;
398 case 'h': currentStep = currentStep - 10; break;
399 case 'j': currentStep = currentStep + 10; break;
400 case 'y': currentStep = currentStep - 100; break;
401 case 'u': currentStep = currentStep + 100; break;
402 case 'k': currentStep = currentStep - 1000; break;
403 case 'i': currentStep = currentStep + 1000; break;
404
405 case 's':
406 {
407 // Storing state file
408 std::string saveFileName = "quicksave.state";
409
410 std::string saveData;
411 size_t stateSize = r.getGame()->getEmulator()->getStateSize();
412 saveData.resize(stateSize);
413 jaffarCommon::serializer::Contiguous s(saveData.data(), stateSize);
415 if (jaffarCommon::file::saveStringToFile(saveData, saveFileName.c_str()) == false) JAFFAR_THROW_LOGIC("[ERROR] Could not save state file: %s\n", saveFileName.c_str());
416 jaffarCommon::logger::log("[J+] Saved state to %s\n", saveFileName.c_str());
417
418 // Do no show frame info again after this action
419 showFrameInfo = false;
420
421 break;
422 }
423
424 // Toggles playback from current point
425 case 'p': isReproduce = !isReproduce; break;
426
427 // Toggles Auto Reload
428 case 'r': isReload = !isReload; break;
429
430 // Triggers the exit
431 case 'q': isFinalize = true; break;
432
433 // Handle any game-specific commands. If such command is executed, do not clear output
434 default: showFrameInfo = r.getGame()->playerParseCommand(command) == false;
435 }
436
437 // Correct current step if requested more than possible
438 if (currentStep < 0) currentStep = 0;
439
440 // If reloading on finish, do it now
441 if (currentStep > sequenceLength && isReload == true) break;
442
443 // If exiting on finish, do it now
444 if (currentStep > sequenceLength && isExitOnEnd == true) break;
445
446 // If not reloading on finish, simply stop
447 if (currentStep > sequenceLength)
448 {
449 currentStep = sequenceLength;
450 isReproduce = false;
451 }
452 }
453
454 // If requested, print a stable summary of the final (end-of-sequence) state. This is the
455 // machine-checkable oracle for headless reproduction tests: the hash is deterministic, so the
456 // same config+solution must always produce the same value here.
457 if (printFinalState)
458 {
459 const auto finalHash = p.getStateHash(sequenceLength);
460 const auto stateType = r.getGame()->getStateType();
461 const std::string stateTypeString = stateType == jaffarPlus::Game::stateType_t::win ? "Win" : (stateType == jaffarPlus::Game::stateType_t::fail ? "Fail" : "Normal");
462 jaffarCommon::logger::log("[J+] Final Step: %ld\n", sequenceLength);
463 jaffarCommon::logger::log("[J+] Final State Type: %s\n", stateTypeString.c_str());
464 // First step (inputs applied) at which the solution reaches a win / fail state. Useful to spot a
465 // movie that wins before its end (wasted trailing inputs) or fails midway. "none" if it never does.
466 const auto firstWinStep = p.getFirstWinStep();
467 const auto firstFailStep = p.getFirstFailStep();
468 const std::string firstWinStepString = firstWinStep < 0 ? "none" : std::to_string(firstWinStep);
469 const std::string firstFailStepString = firstFailStep < 0 ? "none" : std::to_string(firstFailStep);
470 jaffarCommon::logger::log("[J+] First Win Step: %s\n", firstWinStepString.c_str());
471 jaffarCommon::logger::log("[J+] First Fail Step: %s\n", firstFailStepString.c_str());
472 jaffarCommon::logger::log("[J+] Final State Hash: 0x%lX%lX\n", finalHash.first, finalHash.second);
473 // Solution-quality counts: inputs the engine would not have considered at their frame, and
474 // states the engine would have pruned as duplicates. Both are 0 for a clean engine-found path.
475 jaffarCommon::logger::log("[J+] Not Allowed Input Count: %lu\n", notAllowedInputStates.size());
476 jaffarCommon::logger::log("[J+] Repeated State Count: %lu\n", repeatedHashStates.size());
477 }
478
479 // returning false on exit to trigger the finalization
480 if (isFinalize) return false;
481
482 // Otherwise, keep looping
483 return true;
484}
485
503int main(int argc, char* argv[])
504{
505 // Parsing command line arguments
506 argparse::ArgumentParser program("jaffar-tester", "2.0.0");
507
508 program.add_argument("configFile").help("path to the Jaffar configuration script (.jaffar) file to run.").required();
509 program.add_argument("solutionFile").help("path to the solution sequence file (.sol) to reproduce.").required();
510 program.add_argument("--reproduce").help("Starts playing from the start").default_value(false).implicit_value(true);
511 program.add_argument("--reload").help("Reloads the solution after reaching the end").default_value(false).implicit_value(true);
512 program.add_argument("--exitOnEnd").help("Exits the program upon reaching the last step").default_value(false).implicit_value(true);
513 program.add_argument("--unattended").help("Indicates the player not to print the interactive prompt nor wait for inputs").default_value(false).implicit_value(true);
514 program.add_argument("--disableRender").help("Do not render game window.").default_value(false).implicit_value(true);
515 program.add_argument("--frameskip").help("How many frames to skip between renderings.").default_value(std::string("1"));
516 program.add_argument("--initialSequence").help("Overrides the solution file to use as initial sequence to play before starting.").default_value(std::string(""));
517 program.add_argument("--runCommand").help("Specifies a command to run and then exit").default_value(std::string(""));
518 program.add_argument("--screenshotDir").help("Directory to write per-frame screenshots (BMP) into (requires rendering enabled).").default_value(std::string(""));
519 program.add_argument("--screenshotSteps").help("Comma-separated list of steps to screenshot (empty = every rendered frame).").default_value(std::string(""));
520 program.add_argument("--printFinalState")
521 .help("Prints a stable summary (step, state type, state hash) of the final state on exit, for headless verification.")
522 .default_value(false)
523 .implicit_value(true);
524 program.add_argument("--dumpHashes")
525 .help("Writes the per-step game-state hash for every step to the given file (for cross-emulator divergence checks).")
526 .default_value(std::string(""));
527 program.add_argument("--dumpRam")
528 .help("Writes the full low work-RAM (LRAM) for every step to the given file as flat binary (for byte-level cross-emulator diffs).")
529 .default_value(std::string(""));
530 program.add_argument("--dumpReward")
531 .help("Writes the per-step game reward (one value per line) to the given file (for use as a 'Reference Reward Floor' trace).")
532 .default_value(std::string(""));
533 program.add_argument("--dumpTrace")
534 .help("Writes the game's per-step trace line (Game::getTraceLine) to the given file (for use as a game 'Trace File Path' / trace magnet).")
535 .default_value(std::string(""));
536 program.add_argument("--saveStateStep").help("Step at which to save the emulator state (used with --saveStateFile), then exit.").default_value(std::string(""));
537 program.add_argument("--saveStateFile")
538 .help("File to write the emulator's full state at --saveStateStep to (load as Emulator 'Initial State File Path').")
539 .default_value(std::string(""));
540
541 // Try to parse arguments
542 try
543 {
544 program.parse_args(argc, argv);
545 }
546 catch (const std::runtime_error& err)
547 {
548 JAFFAR_THROW_LOGIC("%s\n%s", err.what(), program.help().str().c_str());
549 }
550
551 // Parsing config file
552 const std::string configFile = program.get<std::string>("configFile");
553
554 // Parsin solution file
555 const std::string solutionFile = program.get<std::string>("solutionFile");
556
557 // Getting reload flag
558 bool doReload = program.get<bool>("--reload");
559
560 // Getting reproduce flag
561 bool reproduceStart = program.get<bool>("--reproduce");
562
563 // Getting disablerender flag
564 bool disableRender = program.get<bool>("--disableRender");
565
566 // Getting screenshot options
567 screenshotDir = program.get<std::string>("--screenshotDir");
568 {
569 const auto stepsStr = program.get<std::string>("--screenshotSteps");
570 if (stepsStr.empty() == false)
571 for (const auto& tok : jaffarCommon::string::split(stepsStr, ','))
572 if (tok.empty() == false) screenshotSteps.insert(parseUInt(tok, "--screenshotSteps"));
573 }
574
575 // Getting exitOnEnd flag
576 bool exitOnEnd = program.get<bool>("--exitOnEnd");
577
578 // Getting unattended flag
579 bool unattended = program.get<bool>("--unattended");
580
581 // Getting frameskip
582 frameskip = parseUInt(program.get<std::string>("--frameskip"), "--frameskip");
583
584 // Getting frameskip
585 const std::string initialSequence = program.get<std::string>("--initialSequence");
586
587 // Getting command to run (if any)
588 runCommand = program.get<std::string>("--runCommand");
589
590 // Getting the print-final-state flag
591 printFinalState = program.get<bool>("--printFinalState");
592
593 // Getting the per-step hash dump path (if any)
594 dumpHashesPath = program.get<std::string>("--dumpHashes");
595
596 // Getting the per-step RAM dump path (if any)
597 dumpRamPath = program.get<std::string>("--dumpRam");
598 dumpRewardPath = program.get<std::string>("--dumpReward");
599 dumpTracePath = program.get<std::string>("--dumpTrace");
600 saveStateStepStr = program.get<std::string>("--saveStateStep");
601 saveStateFilePath = program.get<std::string>("--saveStateFile");
602
603 // Initializing terminal
604 jaffarCommon::logger::initializeTerminal();
605
606 // Setting initial reproduction values
607 isReload = doReload;
608 isReproduce = reproduceStart;
609 isExitOnEnd = exitOnEnd;
610 isUnattended = unattended;
611
612 // If config file defined, read it now
613 std::string configFileString;
614 if (jaffarCommon::file::loadStringFromFile(configFileString, configFile) == false)
615 JAFFAR_THROW_LOGIC("[ERROR] Could not find or read from Jaffar config file: %s\n", configFile.c_str());
616
617 // Parsing configuration file
618 nlohmann::json config;
619 try
620 {
621 config = nlohmann::json::parse(configFileString);
622 }
623 catch (const std::exception& err)
624 {
625 JAFFAR_THROW_LOGIC("[ERROR] Parsing configuration file %s. Details:\n%s\n", configFile.c_str(), err.what());
626 }
627
628 // Getting component configurations
629 auto emulatorConfig = jaffarCommon::json::getObject(config, "Emulator Configuration");
630 auto gameConfig = jaffarCommon::json::getObject(config, "Game Configuration");
631 auto runnerConfig = jaffarCommon::json::getObject(config, "Runner Configuration");
632
633 // Overriding initial solution file, if provided
634 if (initialSequence != "") emulatorConfig["Initial Sequence File Path"] = initialSequence;
635
636 // Disabling frameskip, if enabled
637 runnerConfig["Frameskip"]["Rate"] = 0;
638
639 // Creating runner from the configuration
640 auto r = jaffarPlus::Runner::getRunner(emulatorConfig, gameConfig, runnerConfig);
641
642 // Initializing runner
643 r->initialize();
644
645 // Enabling rendering, if required
646 if (disableRender == false)
647 {
648 r->getGame()->getEmulator()->initializeVideoOutput();
649 r->getGame()->getEmulator()->enableRendering();
650 }
651
652 // Getting game state size
653 const auto stateSize = r->getStateSize();
654
655 // Storage for the initial state
656 std::string initialState;
657 initialState.resize(stateSize);
658
659 // Getting initial state
660 jaffarCommon::serializer::Contiguous s(initialState.data(), initialState.size());
661 r->serializeState(s);
662
663 // Running main cycle
664 bool continueRunning = true;
665 while (continueRunning == true)
666 {
667 // Running main cycle
668 continueRunning = mainCycle(*r, solutionFile, disableRender);
669
670 // If the exit-on-end flag is set, then do not repeat reproduction
671 if (exitOnEnd == true) break;
672
673 // If the playback repeats, then sleep and restore the initial state
674 if (continueRunning == true)
675 {
676 // If repeating, then wait a bit before repeating to prevent fast repetition of short movies
677 sleep(1);
678
679 // Reloading the initial state (captured at step 0); the step counter is not in the stream, so reset
680 // it here before deserializing (the player advances it itself as it replays).
681 r->setStepCount(0);
682 jaffarCommon::deserializer::Contiguous d(initialState.data(), initialState.size());
683 r->deserializeState(d);
684 }
685 }
686
687 // If redering was enabled, finish it now
688 if (disableRender == false) r->getGame()->getEmulator()->finalizeVideoOutput();
689
690 // Ending ncurses window
691 jaffarCommon::logger::finalizeTerminal();
692}
virtual void serializeState(jaffarCommon::serializer::Base &serializer) const =0
Serializes the emulator state into the given serializer.
virtual void saveScreenshot(const std::string &)
Saves the currently-rendered frame to an image file at the given destination path.
Definition emulator.hpp:226
std::string getName() const
Returns the emulator's configured name.
Definition emulator.hpp:139
size_t getStateSize() const
Computes the serialized size of the emulator state.
Definition emulator.hpp:128
virtual property_t getProperty(const std::string &propertyName) const =0
Returns a memory property by name.
virtual bool playerParseCommand(const int command)
Handles a game-specific player command.
Definition game.hpp:661
size_t getCheckpointTolerance() const
Returns the current state's checkpoint tolerance.
Definition game.hpp:607
bool isSaveSolution() const
Indicates whether the current state should trigger a save solution.
Definition game.hpp:613
Emulator * getEmulator() const
Returns a pointer to the internal emulator.
Definition game.hpp:570
ssize_t getSaveSolutionCurrentLastRuleIdx() const
Returns the current last rule index that set a save solution.
Definition game.hpp:619
virtual void playerPrintCommands() const
Prints the game's player-specific commands, if any.
Definition game.hpp:655
size_t getCheckpointLevel() const
Returns the current state's checkpoint level.
Definition game.hpp:604
float getFrameRate() const
Returns the configured frame rate.
Definition game.hpp:581
void printInfo() const
Prints the current game state to the logger.
Definition game.hpp:288
float getReward() const
Returns the current state's reward.
Definition game.hpp:584
stateType_t getStateType() const
Returns the current state type (normal, win or fail).
Definition game.hpp:601
@ fail
A fail rule is currently satisfied.
Definition game.hpp:46
@ win
A win rule is currently satisfied.
Definition game.hpp:45
std::string getName() const
Returns the game name used at runtime.
Definition game.hpp:628
const std::string getSaveSolutionPath() const
Returns the save path of the rule that activated the current save solution.
Definition game.hpp:625
virtual std::string getTraceLine() const
One line of a per-step trace for player --dumpTrace (space-separated coordinates the game wants to re...
Definition game.hpp:590
ssize_t getSaveSolutionPrevLastRuleIdx() const
Returns the previous last rule index that set a save solution.
Definition game.hpp:616
Replays a solution's input sequence and caches per-step state for navigation.
Definition playback.hpp:30
ssize_t getFirstWinStep() const
Returns the first step (number of inputs applied) at which the solution reaches a win state,...
Definition playback.hpp:199
void printInfo() const
Prints runner, game, and emulator information.
Definition playback.hpp:233
void initialize(const std::vector< std::string > &inputSequence)
Replays the input sequence, recording one cached step per input (plus a trailing end-of-sequence step...
Definition playback.hpp:81
bool isInputAllowed(const size_t currentStep) const
Returns whether the input of the given step is allowed by the current move set.
Definition playback.hpp:192
ssize_t getFirstFailStep() const
Returns the first step (number of inputs applied) at which the solution reaches a fail state,...
Definition playback.hpp:205
jaffarPlus::InputSet::inputIndex_t getStateInputIndex(const size_t currentStep) const
Returns the input index of the given step.
Definition playback.hpp:184
std::string getStateInputString(const size_t currentStep) const
Returns the input string of the given step.
Definition playback.hpp:182
jaffarCommon::hash::hash_t getStateHash(const size_t currentStep) const
Returns the state hash of the given step.
Definition playback.hpp:190
const std::vector< size_t > getStateRepeatedHashSteps(const size_t currentStep) const
Returns the earlier steps (ascending) sharing the given step's hash.
Definition playback.hpp:188
void loadStepData(const size_t stepId)
Loads the cached game state of the given step back into the runner.
Definition playback.hpp:223
void renderFrame(const size_t currentStep)
Renders the cached frame for the given step into the emulator window.
Definition playback.hpp:211
Owns a Game instance and advances it according to configured inputs.
Definition runner.hpp:38
size_t getStateSize() const
Computes the size in bytes of the serialized runner state.
Definition runner.hpp:384
static std::unique_ptr< Runner > getRunner(const nlohmann::json &emulatorConfig, const nlohmann::json &gameConfig, const nlohmann::json &runnerConfig)
Creates a runner from the emulator, game and runner configurations.
Definition runner.hpp:527
Game * getGame() const
Returns a pointer to the owned game instance.
Definition runner.hpp:516
Abstract emulator interface that concrete emulation cores implement, exposing state load/save,...
Abstract base for a JaffarPlus game: wraps an emulator, registers game properties,...
Solution playback used by the player tool: replays an input sequence through a Runner,...
int main(int argc, char *argv[])
Entry point for the jaffar-player (jaffar-tester) executable.
Definition player.cpp:503
bool isReload
Switch to toggle whether to reload the movie on reaching the end of the sequence.
Definition player.cpp:30
bool printFinalState
When set, prints a stable, machine-readable summary of the final state on exit (for headless verifica...
Definition player.cpp:40
bool isReproduce
Switch to toggle whether to reproduce (auto-advance) the movie.
Definition player.cpp:32
std::string screenshotDir
Directory to write per-frame screenshots (BMP) into; empty disables screenshotting.
Definition player.cpp:69
static size_t parseUInt(const std::string &value, const std::string &flag)
Parses a non-negative integer from a CLI argument value.
Definition player.cpp:81
std::string dumpRewardPath
When non-empty (–dumpReward), writes the per-step game reward (one value per line) for the replayed s...
Definition player.cpp:54
size_t frameskip
Number of frames to skip between renderings.
Definition player.cpp:34
std::string saveStateFilePath
When non-empty (–saveStateFile), the path to write the emulator savestate captured at –saveStateStep ...
Definition player.cpp:66
std::set< size_t > screenshotSteps
Steps to capture as screenshots; empty captures all rendered steps when a dir is given.
Definition player.cpp:71
std::string runCommand
Command to run initially and then exit.
Definition player.cpp:36
std::string dumpHashesPath
When non-empty, writes the per-step game-state hash for every step to this file (one "step\thashHi\th...
Definition player.cpp:45
std::string dumpTracePath
When non-empty (–dumpTrace), writes the game's per-step trace line (Game::getTraceLine,...
Definition player.cpp:58
std::string dumpRamPath
When non-empty, writes the full low work-RAM ("LRAM") segment for every step to this file as a flat b...
Definition player.cpp:50
bool mainCycle(jaffarPlus::Runner &r, const std::string &solutionFile, bool disableRender)
Runs one full pass over a solution sequence, optionally interactive, and reports state info.
Definition player.cpp:116
std::string saveStateStepStr
When set (–saveStateStep), the step at which to capture a full emulator savestate (paired with –saveS...
Definition player.cpp:62
bool isExitOnEnd
Determines that the reproduction must end on reaching the last step.
Definition player.cpp:28
bool isUnattended
Prevents the interactive player from stalling for a keystroke.
Definition player.cpp:26
Drives a Game forward one input at a time, managing the allowed/candidate input sets,...