159 std::string solutionFileString;
160 if (jaffarCommon::file::loadStringFromFile(solutionFileString, solutionFile) ==
false)
161 JAFFAR_THROW_LOGIC(
"[ERROR] Could not find or read from solution sequence file: %s\n", solutionFile.c_str());
164 const auto solutionSequence = jaffarCommon::string::split(solutionFileString,
'\0');
171 std::string data((
const char*)prop.pointer, prop.size);
172 jaffarCommon::file::saveStringToFile(data,
dumpNtabPath);
173 jaffarCommon::logger::log(
"[J+] Dumped NTAB (%lu bytes) to %s\n", prop.size,
dumpNtabPath.c_str());
180 for (
const auto& [addr, val] :
pokeRam)
183 jaffarCommon::logger::log(
"[J+] Poked RAM 0x%04X = 0x%02X (%u)\n", addr, val, val);
189 std::string ntabData;
190 if (jaffarCommon::file::loadStringFromFile(ntabData,
pokeNtabPath) ==
false) JAFFAR_THROW_LOGIC(
"[ERROR] Could not read --pokeNTAB file: %s\n",
pokeNtabPath.c_str());
192 const size_t n = std::min(ntabData.size(), prop.size);
193 memcpy(prop.pointer, ntabData.data(), n);
194 jaffarCommon::logger::log(
"[J+] Poked NTAB with %lu bytes from %s\n", n,
pokeNtabPath.c_str());
205 for (
const auto& inp : solutionSequence)
207 if (advanced >= advanceN)
break;
208 if (inp.empty())
continue;
215 const size_t sz = emu->getStateSize();
217 jaffarCommon::serializer::Contiguous ser(data.data(), sz);
218 emu->serializeState(ser);
220 jaffarCommon::logger::log(
"[J+] Saved poked state (%lu bytes) to %s\n", sz,
savePokedStatePath.c_str());
231 auto rd16 = [&](
int a) {
return ram[a] | (ram[a + 1] << 8); };
232 auto s16 = [&](
int a)
235 return v >= 0x8000 ? v - 0x10000 : v;
239 double vx = s16(0xA614) + rd16(0xA616) / 65536.0, vy = s16(0xA618) + rd16(0xA61A) / 65536.0;
240 return std::sqrt(vx * vx + vy * vy);
242 std::string out =
"frame,gx,gy,spd,posX,posY";
243 for (
int i = 0; i < 234; i++)
246 snprintf(h,
sizeof h,
",b%02X", i);
251 for (
size_t f = 0; f < solutionSequence.size(); f++)
253 r.
advanceState(emu->registerInput(solutionSequence[f]));
254 snprintf(buf,
sizeof buf,
"%zu,%d,%d,%.3f,%d,%d", f, rd16(0xA658) & 31, rd16(0xA656) & 31, spd(), rd16(0x80A6), rd16(0x80A8));
256 for (
int i = 0; i < 234; i++)
258 snprintf(buf,
sizeof buf,
",%d", ram[0xA60C + i]);
263 jaffarCommon::file::saveStringToFile(out,
dumpRacerPath.c_str());
273 auto rd16 = [&](
int a) {
return ram[a] | (ram[a + 1] << 8); };
274 auto s16 = [&](
int a)
277 return v >= 0x8000 ? v - 0x10000 : v;
281 double vx = s16(0xA614) + rd16(0xA616) / 65536.0, vy = s16(0xA618) + rd16(0xA61A) / 65536.0;
282 return std::sqrt(vx * vx + vy * vy);
291 double minSpd = 99, maxSpd = 0, maxDrop = 0;
292 int maxFall = 0, maxZ = -999, minAttr = 99, maxAttr = 0, n = 0, surf = 0;
294 std::map<std::pair<int, int>, Agg> cells;
296 for (
size_t f = 0; f < solutionSequence.size(); f++)
298 r.
advanceState(emu->registerInput(solutionSequence[f]));
299 double s = spd(), drop = prev - s;
301 auto& a = cells[{rd16(0xA658), rd16(0xA656)}];
302 if (s < a.minSpd) a.minSpd = s;
303 if (s > a.maxSpd) a.maxSpd = s;
304 if (drop > a.maxDrop) a.maxDrop = drop;
305 if (ram[0xA66C] > a.maxFall) a.maxFall = ram[0xA66C];
307 if (z > a.maxZ) a.maxZ = z;
308 int at = ram[0xA648];
309 if (at < a.minAttr) a.minAttr = at;
310 if (at > a.maxAttr) a.maxAttr = at;
311 a.surf = ram[0xA649];
314 std::string csv =
"gx,gy,surf,minAttr,maxAttr,minSpd,maxSpd,maxDrop,maxFall,maxZ,n\n";
315 for (
auto& kv : cells)
318 snprintf(buf,
sizeof buf,
"%d,%d,%d,%d,%d,%.2f,%.2f,%.2f,%d,%d,%d\n", kv.first.first, kv.first.second, kv.second.surf, kv.second.minAttr, kv.second.maxAttr, kv.second.minSpd,
319 kv.second.maxSpd, kv.second.maxDrop, kv.second.maxFall, kv.second.maxZ, kv.second.n);
323 jaffarCommon::logger::log(
"[J+] Terrain drive: %lu cells logged to %s\n", cells.size(),
terrainDrivePath.c_str());
333 auto nums = jaffarCommon::string::split(
terrainSweepSpec.substr(colon + 1),
',');
334 int x0 = std::stoi(nums[0]), x1 = std::stoi(nums[1]), y0 = std::stoi(nums[2]), y1 = std::stoi(nums[3]), st = std::stoi(nums[4]);
337 const size_t stateSize = emu->getStateSize();
340 base.resize(stateSize);
342 jaffarCommon::serializer::Contiguous s(base.data(), stateSize);
343 emu->serializeState(s);
345 const auto accelInput = emu->registerInput(
"|..|.....B..|");
351 auto rd16 = [&](
int adr) {
return ram[adr] | (ram[adr + 1] << 8); };
352 auto s16 = [&](
int adr)
355 return v >= 0x8000 ? v - 0x10000 : v;
357 auto setPos = [&](
int a,
int b)
359 ram[0xA60C] = a & 0xFF;
360 ram[0xA60D] = (a >> 8) & 0xFF;
363 ram[0xA610] = b & 0xFF;
364 ram[0xA611] = (b >> 8) & 0xFF;
368 auto setVel = [&](
int vx,
int vy)
370 ram[0xA614] = vx & 0xFF;
371 ram[0xA615] = (vx >> 8) & 0xFF;
374 ram[0xA618] = vy & 0xFF;
375 ram[0xA619] = (vy >> 8) & 0xFF;
381 double vx = rd16(0xA614) + rd16(0xA616) / 65536.0, vy = rd16(0xA618) + rd16(0xA61A) / 65536.0;
382 return std::sqrt(vx * vx + vy * vy);
384 auto gyOf = [&]() {
return ((rd16(0xA60C) >> 3) / 12) & 31; };
385 auto gxOf = [&]() {
return ((rd16(0xA610) >> 3) / 12) & 31; };
392 int reached = 0, valid = 0, blocked = 0, hole = 0, maxZ = -32768, minAttr = 99, surf = -1;
395 auto probe = [&](
int gx,
int gy,
int dir) -> Res
399 jaffarCommon::deserializer::Contiguous d(base.data(), stateSize);
400 emu->deserializeState(d);
405 setPos((gy - 1) * 96 + 48, gx * 96 + 48);
410 setPos((gy + 1) * 96 + 48, gx * 96 + 48);
415 setPos(gy * 96 + 48, (gx - 1) * 96 + 48);
420 setPos(gy * 96 + 48, (gx + 1) * 96 + 48);
424 int lastMove = rd16(dir < 2 ? 0xA60C : 0xA610), stuck = 0, entered = 0;
425 for (
int k = 0; k < 32; k++)
428 bool inTarget = (gyOf() == gy && gxOf() == gx);
429 if (!entered && ram[0xA66C] != 0)
440 if (s < R.minSpd) R.minSpd = s;
441 int at = rd16(0xA648) & 0xFF;
442 if (at < R.minAttr) R.minAttr = at;
443 R.surf = rd16(0xA649) & 0xFF;
445 if (z > R.maxZ) R.maxZ = z;
446 if (ram[0xA66C] != 0) R.hole = 1;
448 int mv = rd16(dir < 2 ? 0xA60C : 0xA610);
449 if (std::abs(mv - lastMove) < 2)
456 if (!entered) R.blocked = 1;
464 if (x0 == x1 && y0 == y1)
467 jaffarCommon::deserializer::Contiguous d(base.data(), stateSize);
468 emu->deserializeState(d);
470 setPos(x0 * 96 + 48, y0 * 96 + 48);
473 std::string dg =
"f,gy,gx,attr,surf,spd,fall,Z\n";
475 for (
int k = 0; k < 90; k++)
478 snprintf(b,
sizeof b,
"%d,%d,%d,%d,%d,%.2f,%d,%d\n", k, gyOf(), gxOf(), rd16(0xA648) & 0xFF, rd16(0xA649) & 0xFF, speed(), ram[0xA66C], s16(0xA64C));
481 jaffarCommon::file::saveStringToFile(dg, outPath.c_str());
485 std::string csv =
"gy,gx,reached,surf,minAttr,minSpd,hole,maxZ,solid,nApproach\n";
486 for (
int gx = y0; gx <= y1; gx += st)
487 for (
int gy = x0; gy <= x1; gy += st)
490 int nApproach = 0, solidVotes = 0;
491 for (
int dir = 0; dir < 4; dir++)
493 Res R = probe(gx, gy, dir);
494 if (!R.valid)
continue;
504 if (R.minAttr < best.minAttr) best.minAttr = R.minAttr;
505 if (R.minSpd < best.minSpd) best.minSpd = R.minSpd;
506 if (R.maxZ > best.maxZ) best.maxZ = R.maxZ;
507 if (R.hole) best.hole = 1;
511 if (best.minSpd > 50) best.minSpd = 0;
513 int solid = (best.reached == 0 && solidVotes > 0) ? 1 : 0;
515 snprintf(buf,
sizeof buf,
"%d,%d,%d,%d,%d,%.2f,%d,%d,%d,%d\n", gy, gx, best.reached, best.surf, best.reached ? best.minAttr : -1, best.minSpd, best.hole, best.maxZ, solid,
519 jaffarCommon::file::saveStringToFile(csv, outPath.c_str());
520 jaffarCommon::logger::log(
"[J+] Terrain sweep written to %s\n", outPath.c_str());
525 ssize_t currentStep = 0;
528 const ssize_t sequenceLength = solutionSequence.size();
532 const uint32_t inverseFrameRate = std::round((1.0 / frameRate) * 1.0e+6);
538 jaffarCommon::logger::refreshTerminal();
551 const auto polls = emu->
getProperty(
"Joypad Read Count");
554 for (ssize_t s = 0; s < sequenceLength; s++)
556 r.
advanceState(emu->registerInput(solutionSequence[s]));
557 snprintf(line,
sizeof(line),
"%ld\t%d\n", s + 1, *(
const int*)polls.pointer);
560 if (jaffarCommon::file::saveStringToFile(dump,
dumpPollsPath.c_str()) ==
false)
561 JAFFAR_THROW_LOGIC(
"[ERROR] Could not write per-step poll dump to: %s\n",
dumpPollsPath.c_str());
576 for (ssize_t s = 0; s < sequenceLength; s++)
578 const auto inputIndex = emu->registerInput(solutionSequence[s]);
583 snprintf(path,
sizeof(path),
"%s/step_%06ld.bmp",
screenshotDir.c_str(), s + 1);
584 emu->saveScreenshot(path);
590 const auto vram = emu->getProperty(
"VRAM");
592 snprintf(vpath,
sizeof(vpath),
"%s/vram_%06ld.bin",
screenshotDir.c_str(), s + 1);
593 std::string vdump((
const char*)vram.pointer, vram.size);
594 jaffarCommon::file::saveStringToFile(vdump, vpath);
596 catch (
const std::exception&)
606 p.
initialize(solutionSequence, !disableRender);
609 bool showFrameInfo =
true;
612 bool isFinalize =
false;
615 std::vector<ssize_t> repeatedHashStates;
616 for (ssize_t i = 0; i < sequenceLength; i++)
619 if (repeatedHashSteps.size() > 0) repeatedHashStates.push_back(i);
623 std::vector<ssize_t> notAllowedInputStates;
624 for (ssize_t i = 0; i < sequenceLength; i++)
627 if (isInputAllowed ==
false) notAllowedInputStates.push_back(i);
637 for (ssize_t i = 0; i <= sequenceLength; i++)
639 jaffarCommon::hash::hash_t hash;
655 snprintf(line,
sizeof(line),
"%ld\t%016lX%016lX\n", i, hash.first, hash.second);
658 if (jaffarCommon::file::saveStringToFile(dump,
dumpHashesPath.c_str()) ==
false)
659 JAFFAR_THROW_LOGIC(
"[ERROR] Could not write per-step hash dump to: %s\n",
dumpHashesPath.c_str());
669 dump.reserve((
size_t)(sequenceLength + 1) * lram.size);
670 for (ssize_t i = 0; i <= sequenceLength; i++)
673 dump.append((
const char*)lram.pointer, lram.size);
675 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());
684 dump.reserve((
size_t)(sequenceLength + 1) * vram.size);
685 for (ssize_t i = 0; i <= sequenceLength; i++)
688 dump.append((
const char*)vram.pointer, vram.size);
690 if (jaffarCommon::file::saveStringToFile(dump,
dumpVramPath.c_str()) ==
false) JAFFAR_THROW_LOGIC(
"[ERROR] Could not write per-step VRAM dump to: %s\n",
dumpVramPath.c_str());
699 for (ssize_t i = 0; i <= sequenceLength; i++)
708 dump += std::string(rbuf) +
"\n";
710 if (jaffarCommon::file::saveStringToFile(dump,
dumpRewardPath.c_str()) ==
false)
711 JAFFAR_THROW_LOGIC(
"[ERROR] Could not write per-step reward dump to: %s\n",
dumpRewardPath.c_str());
720 for (ssize_t i = 0; i <= sequenceLength; i++)
725 if (jaffarCommon::file::saveStringToFile(dump,
dumpTracePath.c_str()) ==
false)
726 JAFFAR_THROW_LOGIC(
"[ERROR] Could not write per-step trace dump to: %s\n",
dumpTracePath.c_str());
742 std::string saveData;
743 const size_t stateSize = saveEmu->getStateSize();
744 saveData.resize(stateSize);
745 jaffarCommon::serializer::Contiguous s(saveData.data(), stateSize);
746 saveEmu->serializeState(s);
747 saveEmu->reapplyDisabledStateProperties();
748 if (jaffarCommon::file::saveStringToFile(saveData,
saveStateFilePath.c_str()) ==
false)
749 JAFFAR_THROW_LOGIC(
"[ERROR] Could not write state at step %ld to: %s\n", (
long)step,
saveStateFilePath.c_str());
750 jaffarCommon::logger::log(
"[J+] Saved emulator state at step %ld to %s (%lu bytes)\n", (
long)step,
saveStateFilePath.c_str(), stateSize);
756 while (isFinalize ==
false)
759 if (disableRender ==
false)
784 showFrameInfo =
false;
792 jaffarCommon::logger::clearTerminal();
794 jaffarCommon::logger::log(
"[J+] ----------------------------------------------------------------\n");
795 jaffarCommon::logger::log(
"[J+] Current Step #: %lu / %lu\n", currentStep, sequenceLength);
796 jaffarCommon::logger::log(
"[J+] Playback: %s\n",
isReproduce ?
"Playing" :
"Stopped");
797 jaffarCommon::logger::log(
"[J+] Input: %s (0x%X)\n", inputString.c_str(), inputIndex);
798 jaffarCommon::logger::log(
"[J+] On Finish: %s\n",
isReload ?
"Auto Reload" :
"Stop");
800 jaffarCommon::logger::log(
"[J+] Repeated Hash Steps: %lu total [ ", repeatedHashStates.size());
801 if (repeatedHashStates.size() < 5)
802 for (
const auto step : repeatedHashStates) jaffarCommon::logger::log(
" %ld ", step);
805 for (
size_t i = 0; i < 5; i++) jaffarCommon::logger::log(
" %ld ", repeatedHashStates[i]);
806 jaffarCommon::logger::log(
" ... ");
808 jaffarCommon::logger::log(
" ] \n");
810 jaffarCommon::logger::log(
"[J+] Not Allowed Input Steps: %lu total [ ", notAllowedInputStates.size());
811 if (notAllowedInputStates.size() < 5)
812 for (
const auto step : notAllowedInputStates) jaffarCommon::logger::log(
" %ld ", step);
815 for (
size_t i = 0; i < 5; i++) jaffarCommon::logger::log(
" %ld ", notAllowedInputStates[i]);
816 jaffarCommon::logger::log(
" ... ");
818 jaffarCommon::logger::log(
" ] \n");
820 jaffarCommon::logger::log(
"[J+] Game Name: '%s'\n", r.
getGame()->
getName().c_str());
822 jaffarCommon::logger::log(
"[J+] State Hash: 0x%lX%lX\n", hash.first, hash.second);
823 jaffarCommon::logger::log(
"[J+] State Repeated Hash Steps: [ ");
824 for (
const auto step : repeatedHashSteps) jaffarCommon::logger::log(
" %lu ", step);
825 jaffarCommon::logger::log(
" ] \n");
826 jaffarCommon::logger::log(
"[J+] Is Input Allowed: %s\n", isInputAllowed ?
"True" :
"False");
827 jaffarCommon::logger::log(
"[J+] State Size: %lu\n", stateSize);
828 jaffarCommon::logger::log(
"[J+] Solution File: '%s'\n", solutionFile.c_str());
829 jaffarCommon::logger::log(
"[J+] Sequence Length: %lu\n", sequenceLength);
830 jaffarCommon::logger::log(
"[J+] Frame Rate: %f (%u)\n", frameRate, inverseFrameRate);
832 jaffarCommon::logger::log(
"[J+] Manual Save Solution: Active: %s, Path: '%s', Last Rule: (Current: %ld), (Prev: %ld)\n", r.
getGame()->
isSaveSolution() ?
"Yes" :
"No",
837 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");
842 jaffarCommon::logger::refreshTerminal();
846 showFrameInfo =
true;
859 usleep(inverseFrameRate);
862 command = jaffarCommon::logger::getKeyPress();
885 case 'n': currentStep = currentStep - 1;
break;
886 case 'm': currentStep = currentStep + 1;
break;
887 case 'h': currentStep = currentStep - 10;
break;
888 case 'j': currentStep = currentStep + 10;
break;
889 case 'y': currentStep = currentStep - 100;
break;
890 case 'u': currentStep = currentStep + 100;
break;
891 case 'k': currentStep = currentStep - 1000;
break;
892 case 'i': currentStep = currentStep + 1000;
break;
897 std::string saveFileName =
"quicksave.state";
899 std::string saveData;
901 saveData.resize(stateSize);
902 jaffarCommon::serializer::Contiguous s(saveData.data(), stateSize);
904 if (jaffarCommon::file::saveStringToFile(saveData, saveFileName.c_str()) ==
false) JAFFAR_THROW_LOGIC(
"[ERROR] Could not save state file: %s\n", saveFileName.c_str());
905 jaffarCommon::logger::log(
"[J+] Saved state to %s\n", saveFileName.c_str());
908 showFrameInfo =
false;
920 case 'q': isFinalize =
true;
break;
927 if (currentStep < 0) currentStep = 0;
930 if (currentStep > sequenceLength &&
isReload ==
true)
break;
933 if (currentStep > sequenceLength &&
isExitOnEnd ==
true)
break;
936 if (currentStep > sequenceLength)
938 currentStep = sequenceLength;
951 jaffarCommon::logger::log(
"[J+] Final Step: %ld\n", sequenceLength);
952 jaffarCommon::logger::log(
"[J+] Final State Type: %s\n", stateTypeString.c_str());
957 const std::string firstWinStepString = firstWinStep < 0 ?
"none" : std::to_string(firstWinStep);
958 const std::string firstFailStepString = firstFailStep < 0 ?
"none" : std::to_string(firstFailStep);
959 jaffarCommon::logger::log(
"[J+] First Win Step: %s\n", firstWinStepString.c_str());
960 jaffarCommon::logger::log(
"[J+] First Fail Step: %s\n", firstFailStepString.c_str());
961 jaffarCommon::logger::log(
"[J+] Final State Hash: 0x%lX%lX\n", finalHash.first, finalHash.second);
964 jaffarCommon::logger::log(
"[J+] Not Allowed Input Count: %lu\n", notAllowedInputStates.size());
965 jaffarCommon::logger::log(
"[J+] Repeated State Count: %lu\n", repeatedHashStates.size());
969 if (isFinalize)
return false;
992int main(
int argc,
char* argv[])
995 argparse::ArgumentParser program(
"jaffar-tester",
"2.0.0");
997 program.add_argument(
"configFile").help(
"path to the Jaffar configuration script (.jaffar) file to run.").required();
998 program.add_argument(
"solutionFile").help(
"path to the solution sequence file (.sol) to reproduce.").required();
999 program.add_argument(
"--reproduce").help(
"Starts playing from the start").default_value(
false).implicit_value(
true);
1000 program.add_argument(
"--reload").help(
"Reloads the solution after reaching the end").default_value(
false).implicit_value(
true);
1001 program.add_argument(
"--exitOnEnd").help(
"Exits the program upon reaching the last step").default_value(
false).implicit_value(
true);
1002 program.add_argument(
"--unattended").help(
"Indicates the player not to print the interactive prompt nor wait for inputs").default_value(
false).implicit_value(
true);
1003 program.add_argument(
"--disableRender").help(
"Do not render game window.").default_value(
false).implicit_value(
true);
1004 program.add_argument(
"--frameskip").help(
"How many frames to skip between renderings.").default_value(std::string(
"1"));
1005 program.add_argument(
"--initialSequence").help(
"Overrides the solution file to use as initial sequence to play before starting.").default_value(std::string(
""));
1006 program.add_argument(
"--runCommand").help(
"Specifies a command to run and then exit").default_value(std::string(
""));
1007 program.add_argument(
"--dumpNTAB").help(
"Dump post-init nametable memory to this file and exit.").default_value(std::string(
""));
1008 program.add_argument(
"--pokeNTAB").help(
"Binary file to copy into nametable memory after the initial sequence.").default_value(std::string(
""));
1009 program.add_argument(
"--savePokedState").help(
"Save the post-poke emulator state to this path and exit.").default_value(std::string(
""));
1010 program.add_argument(
"--savePokedStateStep").help(
"Advance this many solution steps after the poke before saving.").default_value(std::string(
""));
1011 program.add_argument(
"--screenshotDir").help(
"Directory to write per-frame screenshots (BMP) into (requires rendering enabled).").default_value(std::string(
""));
1012 program.add_argument(
"--screenshotSteps").help(
"Comma-separated list of steps to screenshot (empty = every rendered frame).").default_value(std::string(
""));
1013 program.add_argument(
"--printFinalState")
1014 .help(
"Prints a stable summary (step, state type, state hash) of the final state on exit, for headless verification.")
1015 .default_value(
false)
1016 .implicit_value(
true);
1017 program.add_argument(
"--dumpHashes")
1018 .help(
"Writes the per-step game-state hash for every step to the given file (for cross-emulator divergence checks).")
1019 .default_value(std::string(
""));
1020 program.add_argument(
"--dumpHashesLookahead")
1021 .help(
"With --dumpHashes: advance this many null inputs before hashing each step (matches the engine's 'Hash Lookahead' digest, e.g. for pin-hash generation).")
1022 .default_value(std::string(
"0"));
1023 program.add_argument(
"--dumpRam")
1024 .help(
"Writes the full low work-RAM (LRAM) for every step to the given file as flat binary (for byte-level cross-emulator diffs).")
1025 .default_value(std::string(
""));
1026 program.add_argument(
"--dumpVram")
1027 .help(
"Writes the full video RAM (VRAM) for every step to the given file as flat binary (for locating VRAM in a foreign savestate).")
1028 .default_value(std::string(
""));
1029 program.add_argument(
"--dumpPolls")
1030 .help(
"Writes the per-step joypad read count to the given file (one 'step\\tcount' line per step; 0 = lag frame, input not polled).")
1031 .default_value(std::string(
""));
1032 program.add_argument(
"--dumpReward")
1033 .help(
"Writes the per-step game reward (one value per line) to the given file (for use as a 'Reference Reward Floor' trace).")
1034 .default_value(std::string(
""));
1035 program.add_argument(
"--dumpTrace")
1036 .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).")
1037 .default_value(std::string(
""));
1038 program.add_argument(
"--dumpRacer").help(
"Replay solution, dump frame,gx,gy,spd + full 234-byte racer struct per frame to CSV, then exit.").default_value(std::string(
""));
1039 program.add_argument(
"--pokeRAM")
1040 .help(
"Poke work-RAM bytes into the post-initial-sequence state before replay, e.g. --pokeRAM \"0x8010=0x42,0x804e=0x00\" (mechanic experiments).")
1041 .default_value(std::string(
""));
1042 program.add_argument(
"--terrainDrive")
1043 .help(
"Branch off the replayed solution, steer a real car into neighboring cells, log true effect (slowdown/hole/jump) per cell to CSV, then exit.")
1044 .default_value(std::string(
""));
1045 program.add_argument(
"--terrainSweep")
1047 "Probe terrain at a grid of player positions: \"out.csv:x0,x1,y0,y1,step\". Reloads state, pokes 0x80A6/0x80A8, steps, reads 0xA649/0xA648/0xA66C. Writes CSV and exits.")
1048 .default_value(std::string(
""));
1049 program.add_argument(
"--saveStateStep").help(
"Step at which to save the emulator state (used with --saveStateFile), then exit.").default_value(std::string(
""));
1050 program.add_argument(
"--saveStateFile")
1051 .help(
"File to write the emulator's full state at --saveStateStep to (load as Emulator 'Initial State File Path').")
1052 .default_value(std::string(
""));
1057 program.parse_args(argc, argv);
1059 catch (
const std::runtime_error& err)
1061 JAFFAR_THROW_LOGIC(
"%s\n%s", err.what(), program.help().str().c_str());
1065 const std::string configFile = program.get<std::string>(
"configFile");
1068 const std::string solutionFile = program.get<std::string>(
"solutionFile");
1071 bool doReload = program.get<
bool>(
"--reload");
1074 bool reproduceStart = program.get<
bool>(
"--reproduce");
1077 bool disableRender = program.get<
bool>(
"--disableRender");
1081 const auto pokeSpec = program.get<std::string>(
"--pokeRAM");
1082 if (pokeSpec.empty() ==
false)
1083 for (
const auto& tok : jaffarCommon::string::split(pokeSpec,
','))
1085 const auto eq = tok.find(
'=');
1086 if (eq == std::string::npos) JAFFAR_THROW_LOGIC(
"Bad --pokeRAM token (need addr=val): '%s'", tok.c_str());
1087 const uint32_t addr = std::stoul(tok.substr(0, eq),
nullptr, 0);
1088 const uint32_t val = std::stoul(tok.substr(eq + 1),
nullptr, 0);
1089 pokeRam.emplace_back((uint32_t)addr, (uint8_t)val);
1100 screenshotDir = program.get<std::string>(
"--screenshotDir");
1102 const auto stepsStr = program.get<std::string>(
"--screenshotSteps");
1103 if (stepsStr.empty() ==
false)
1104 for (
const auto& tok : jaffarCommon::string::split(stepsStr,
','))
1109 bool exitOnEnd = program.get<
bool>(
"--exitOnEnd");
1112 bool unattended = program.get<
bool>(
"--unattended");
1118 const std::string initialSequence = program.get<std::string>(
"--initialSequence");
1121 runCommand = program.get<std::string>(
"--runCommand");
1128 dumpHashesLookahead = (size_t)std::stoul(program.get<std::string>(
"--dumpHashesLookahead"));
1131 dumpRamPath = program.get<std::string>(
"--dumpRam");
1141 jaffarCommon::logger::initializeTerminal();
1150 std::string configFileString;
1151 if (jaffarCommon::file::loadStringFromFile(configFileString, configFile) ==
false)
1152 JAFFAR_THROW_LOGIC(
"[ERROR] Could not find or read from Jaffar config file: %s\n", configFile.c_str());
1155 nlohmann::json config;
1158 config = nlohmann::json::parse(configFileString);
1160 catch (
const std::exception& err)
1162 JAFFAR_THROW_LOGIC(
"[ERROR] Parsing configuration file %s. Details:\n%s\n", configFile.c_str(), err.what());
1166 auto emulatorConfig = jaffarCommon::json::getObject(config,
"Emulator Configuration");
1167 auto gameConfig = jaffarCommon::json::getObject(config,
"Game Configuration");
1168 auto runnerConfig = jaffarCommon::json::getObject(config,
"Runner Configuration");
1171 if (initialSequence !=
"") emulatorConfig[
"Initial Sequence File Path"] = initialSequence;
1174 runnerConfig[
"Frameskip"][
"Rate"] = 0;
1186 r->getGame()->getEmulator()->enableHeadlessRendering();
1188 else if (disableRender ==
false)
1190 r->getGame()->getEmulator()->initializeVideoOutput();
1191 r->getGame()->getEmulator()->enableRendering();
1195 const auto stateSize = r->getStateSize();
1198 std::string initialState;
1199 initialState.resize(stateSize);
1202 jaffarCommon::serializer::Contiguous s(initialState.data(), initialState.size());
1203 r->serializeState(s);
1209 auto* fullEmu = r->getGame()->getEmulator();
1210 std::string fullInitialState;
1212 fullEmu->enableAllStateProperties();
1213 const size_t fullSize = fullEmu->getStateSize();
1214 fullInitialState.resize(fullSize);
1215 jaffarCommon::serializer::Contiguous fs(fullInitialState.data(), fullInitialState.size());
1216 fullEmu->serializeState(fs);
1217 fullEmu->reapplyDisabledStateProperties();
1221 bool continueRunning =
true;
1222 while (continueRunning ==
true)
1225 continueRunning =
mainCycle(*r, solutionFile, disableRender);
1228 if (exitOnEnd ==
true)
break;
1231 if (continueRunning ==
true)
1241 fullEmu->enableAllStateProperties();
1242 jaffarCommon::deserializer::Contiguous fd(fullInitialState.data(), fullInitialState.size());
1243 fullEmu->deserializeState(fd);
1244 fullEmu->reapplyDisabledStateProperties();
1247 jaffarCommon::deserializer::Contiguous d(initialState.data(), initialState.size());
1248 r->deserializeState(d);
1253 if (disableRender ==
false) r->getGame()->getEmulator()->finalizeVideoOutput();
1256 jaffarCommon::logger::finalizeTerminal();