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());
124 const auto solutionSequence = jaffarCommon::string::split(solutionFileString,
'\0');
127 ssize_t currentStep = 0;
130 const ssize_t sequenceLength = solutionSequence.size();
134 const uint32_t inverseFrameRate = std::round((1.0 / frameRate) * 1.0e+6);
140 jaffarCommon::logger::refreshTerminal();
149 bool showFrameInfo =
true;
152 bool isFinalize =
false;
155 std::vector<ssize_t> repeatedHashStates;
156 for (ssize_t i = 0; i < sequenceLength; i++)
159 if (repeatedHashSteps.size() > 0) repeatedHashStates.push_back(i);
163 std::vector<ssize_t> notAllowedInputStates;
164 for (ssize_t i = 0; i < sequenceLength; i++)
167 if (isInputAllowed ==
false) notAllowedInputStates.push_back(i);
177 for (ssize_t i = 0; i <= sequenceLength; i++)
180 snprintf(line,
sizeof(line),
"%ld\t%016lX%016lX\n", i, hash.first, hash.second);
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());
194 dump.reserve((
size_t)(sequenceLength + 1) * lram.size);
195 for (ssize_t i = 0; i <= sequenceLength; i++)
198 dump.append((
const char*)lram.pointer, lram.size);
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());
209 for (ssize_t i = 0; i <= sequenceLength; i++)
218 dump += std::string(rbuf) +
"\n";
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());
230 for (ssize_t i = 0; i <= sequenceLength; i++)
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());
246 std::string saveData;
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);
259 while (isFinalize ==
false)
262 if (disableRender ==
false)
273 snprintf(path,
sizeof(path),
"%s/step_%06ld.bmp",
screenshotDir.c_str(), currentStep);
300 showFrameInfo =
false;
308 jaffarCommon::logger::clearTerminal();
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");
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);
321 for (
size_t i = 0; i < 5; i++) jaffarCommon::logger::log(
" %ld ", repeatedHashStates[i]);
322 jaffarCommon::logger::log(
" ... ");
324 jaffarCommon::logger::log(
" ] \n");
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);
331 for (
size_t i = 0; i < 5; i++) jaffarCommon::logger::log(
" %ld ", notAllowedInputStates[i]);
332 jaffarCommon::logger::log(
" ... ");
334 jaffarCommon::logger::log(
" ] \n");
336 jaffarCommon::logger::log(
"[J+] Game Name: '%s'\n", r.
getGame()->
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);
348 jaffarCommon::logger::log(
"[J+] Manual Save Solution: Active: %s, Path: '%s', Last Rule: (Current: %ld), (Prev: %ld)\n", r.
getGame()->
isSaveSolution() ?
"Yes" :
"No",
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");
358 jaffarCommon::logger::refreshTerminal();
362 showFrameInfo =
true;
371 usleep(inverseFrameRate);
377 command = jaffarCommon::logger::getKeyPress();
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;
408 std::string saveFileName =
"quicksave.state";
410 std::string saveData;
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());
419 showFrameInfo =
false;
431 case 'q': isFinalize =
true;
break;
438 if (currentStep < 0) currentStep = 0;
441 if (currentStep > sequenceLength &&
isReload ==
true)
break;
444 if (currentStep > sequenceLength &&
isExitOnEnd ==
true)
break;
447 if (currentStep > sequenceLength)
449 currentStep = sequenceLength;
462 jaffarCommon::logger::log(
"[J+] Final Step: %ld\n", sequenceLength);
463 jaffarCommon::logger::log(
"[J+] Final State Type: %s\n", stateTypeString.c_str());
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);
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());
480 if (isFinalize)
return false;
503int main(
int argc,
char* argv[])
506 argparse::ArgumentParser program(
"jaffar-tester",
"2.0.0");
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(
""));
544 program.parse_args(argc, argv);
546 catch (
const std::runtime_error& err)
548 JAFFAR_THROW_LOGIC(
"%s\n%s", err.what(), program.help().str().c_str());
552 const std::string configFile = program.get<std::string>(
"configFile");
555 const std::string solutionFile = program.get<std::string>(
"solutionFile");
558 bool doReload = program.get<
bool>(
"--reload");
561 bool reproduceStart = program.get<
bool>(
"--reproduce");
564 bool disableRender = program.get<
bool>(
"--disableRender");
569 const auto stepsStr = program.get<std::string>(
"--screenshotSteps");
570 if (stepsStr.empty() ==
false)
571 for (
const auto& tok : jaffarCommon::string::split(stepsStr,
','))
576 bool exitOnEnd = program.get<
bool>(
"--exitOnEnd");
579 bool unattended = program.get<
bool>(
"--unattended");
585 const std::string initialSequence = program.get<std::string>(
"--initialSequence");
588 runCommand = program.get<std::string>(
"--runCommand");
597 dumpRamPath = program.get<std::string>(
"--dumpRam");
604 jaffarCommon::logger::initializeTerminal();
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());
618 nlohmann::json config;
621 config = nlohmann::json::parse(configFileString);
623 catch (
const std::exception& err)
625 JAFFAR_THROW_LOGIC(
"[ERROR] Parsing configuration file %s. Details:\n%s\n", configFile.c_str(), err.what());
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");
634 if (initialSequence !=
"") emulatorConfig[
"Initial Sequence File Path"] = initialSequence;
637 runnerConfig[
"Frameskip"][
"Rate"] = 0;
646 if (disableRender ==
false)
648 r->getGame()->getEmulator()->initializeVideoOutput();
649 r->getGame()->getEmulator()->enableRendering();
653 const auto stateSize = r->getStateSize();
656 std::string initialState;
657 initialState.resize(stateSize);
660 jaffarCommon::serializer::Contiguous s(initialState.data(), initialState.size());
661 r->serializeState(s);
664 bool continueRunning =
true;
665 while (continueRunning ==
true)
668 continueRunning =
mainCycle(*r, solutionFile, disableRender);
671 if (exitOnEnd ==
true)
break;
674 if (continueRunning ==
true)
682 jaffarCommon::deserializer::Contiguous d(initialState.data(), initialState.size());
683 r->deserializeState(d);
688 if (disableRender ==
false) r->getGame()->getEmulator()->finalizeVideoOutput();
691 jaffarCommon::logger::finalizeTerminal();