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 <map>
25#include <set>
26
28
30
32
34
35size_t frameskip;
36
37std::string runCommand;
38
42
46std::string dumpHashesPath;
47
50
54std::string dumpRamPath;
56std::string dumpVramPath;
57
63std::string dumpPollsPath;
64
67std::string dumpRewardPath;
68
71std::string dumpTracePath;
73std::string dumpRacerPath;
74
77std::string saveStateStepStr;
78
82
86
88std::string pokeNtabPath;
89
91std::string dumpNtabPath;
92
95
97std::string screenshotDir;
99std::set<size_t> screenshotSteps;
100
103std::vector<std::pair<uint32_t, uint8_t>> pokeRam;
112
121static size_t parseUInt(const std::string& value, const std::string& flag)
122{
123 try
124 {
125 size_t consumed = 0;
126 const size_t result = std::stoul(value, &consumed);
127 if (consumed == value.size()) return result;
128 }
129 catch (const std::exception&)
130 {
131 }
132 JAFFAR_THROW_LOGIC("Invalid value '%s' for %s (expected a non-negative integer)\n", value.c_str(), flag.c_str());
133 return 0;
134}
135
156bool mainCycle(jaffarPlus::Runner& r, const std::string& solutionFile, bool disableRender)
157{
158 // If sequence file defined, load it and play it
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());
162
163 // Getting input sequence
164 const auto solutionSequence = jaffarCommon::string::split(solutionFileString, '\0');
165
166 // Optional RAM poke(s) into the post-initial-sequence state, before any replay (mechanic experiments).
167 // --dumpNTAB: write the current nametable memory to a file and exit (transplant calibration)
168 if (dumpNtabPath.empty() == false)
169 {
170 auto prop = r.getGame()->getEmulator()->getProperty("NTAB");
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());
174 return 0;
175 }
176
177 if (pokeRam.empty() == false)
178 {
179 auto* ram = r.getGame()->getEmulator()->getProperty("LRAM").pointer;
180 for (const auto& [addr, val] : pokeRam)
181 {
182 ram[addr] = val;
183 jaffarCommon::logger::log("[J+] Poked RAM 0x%04X = 0x%02X (%u)\n", addr, val, val);
184 }
185 // --pokeNTAB: block-copy a binary file into the nametable memory (cross-emulator transplant
186 // of PPU background state; pairs with --pokeRAM)
187 if (pokeNtabPath.empty() == false)
188 {
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());
191 auto prop = r.getGame()->getEmulator()->getProperty("NTAB");
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());
195 }
196
197 // --savePokedState: persist the post-poke state as a full-fidelity emulator savestate
198 // (loadable via "Initial State File Path") and exit -- for cross-emulator transplants.
199 // --savePokedStateStep N first advances N solution inputs from the poked state, so the
200 // anchor can sit past inputs the search alphabet cannot express (e.g. the scroll-cancel).
201 if (savePokedStatePath.empty() == false)
202 {
203 const size_t advanceN = savePokedStateStepStr.empty() ? 0 : parseUInt(savePokedStateStepStr, "--savePokedStateStep");
204 size_t advanced = 0;
205 for (const auto& inp : solutionSequence)
206 {
207 if (advanced >= advanceN) break;
208 if (inp.empty()) continue;
210 advanced++;
211 }
212 auto* emu = r.getGame()->getEmulator();
214 std::string data;
215 const size_t sz = emu->getStateSize();
216 data.resize(sz);
217 jaffarCommon::serializer::Contiguous ser(data.data(), sz);
218 emu->serializeState(ser);
219 jaffarCommon::file::saveStringToFile(data, savePokedStatePath);
220 jaffarCommon::logger::log("[J+] Saved poked state (%lu bytes) to %s\n", sz, savePokedStatePath.c_str());
221 return 0;
222 }
223 }
224
225 // Per-frame trace: replay the solution and dump frame,gx,gy,spd + the whole racer struct (234 bytes) so we can
226 // empirically find the byte that flags "on honey" (true on the real slowdown crossing, false in corners).
227 if (dumpRacerPath.empty() == false)
228 {
229 auto* emu = r.getGame()->getEmulator();
230 auto* ram = emu->getProperty("LRAM").pointer;
231 auto rd16 = [&](int a) { return ram[a] | (ram[a + 1] << 8); };
232 auto s16 = [&](int a)
233 {
234 int v = rd16(a);
235 return v >= 0x8000 ? v - 0x10000 : v;
236 };
237 auto spd = [&]()
238 {
239 double vx = s16(0xA614) + rd16(0xA616) / 65536.0, vy = s16(0xA618) + rd16(0xA61A) / 65536.0;
240 return std::sqrt(vx * vx + vy * vy);
241 };
242 std::string out = "frame,gx,gy,spd,posX,posY"; // posX/posY = Player 1 Pos X/Y (0x80A6/0x80A8), the magnet/reference coords
243 for (int i = 0; i < 234; i++)
244 {
245 char h[8];
246 snprintf(h, sizeof h, ",b%02X", i);
247 out += h;
248 }
249 out += "\n";
250 char buf[80];
251 for (size_t f = 0; f < solutionSequence.size(); f++)
252 {
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));
255 out += buf;
256 for (int i = 0; i < 234; i++)
257 {
258 snprintf(buf, sizeof buf, ",%d", ram[0xA60C + i]);
259 out += buf;
260 }
261 out += "\n";
262 }
263 jaffarCommon::file::saveStringToFile(out, dumpRacerPath.c_str());
264 return false;
265 }
266
267 // Coverage drive: branch off the replayed solution and steer a REAL car into neighbors, logging true effects
268 // (min speed = slowdown, fall state = hole, max Z = jump) per grid cell. Consistent physics state throughout.
269 if (terrainDrivePath.empty() == false)
270 {
271 auto* emu = r.getGame()->getEmulator();
272 auto* ram = emu->getProperty("LRAM").pointer;
273 auto rd16 = [&](int a) { return ram[a] | (ram[a + 1] << 8); };
274 auto s16 = [&](int a)
275 {
276 int v = rd16(a);
277 return v >= 0x8000 ? v - 0x10000 : v;
278 };
279 auto spd = [&]()
280 {
281 double vx = s16(0xA614) + rd16(0xA616) / 65536.0, vy = s16(0xA618) + rd16(0xA61A) / 65536.0;
282 return std::sqrt(vx * vx + vy * vy);
283 };
284 // Fan the car widely into adjacent cells (accelerate straight / soft & hard L/R) so the probe reaches
285 // off-racing-line cells from their normal neighbours — the "drive through it from an adjacent block" method.
286 // Faithful terrain map = the REAL driven racing line (stays on-track by construction). For each cell the
287 // car actually drives through we record the true effect: attr (0xA648, honey sinks it), surf category
288 // (0xA649), speed band, fall (0xA66C, hole), Z (0xA64C, jump/ramp). No teleport, no off-road excursions.
289 struct Agg
290 {
291 double minSpd = 99, maxSpd = 0, maxDrop = 0;
292 int maxFall = 0, maxZ = -999, minAttr = 99, maxAttr = 0, n = 0, surf = 0;
293 };
294 std::map<std::pair<int, int>, Agg> cells;
295 double prev = spd();
296 for (size_t f = 0; f < solutionSequence.size(); f++)
297 {
298 r.advanceState(emu->registerInput(solutionSequence[f]));
299 double s = spd(), drop = prev - s;
300 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];
306 int z = s16(0xA64C);
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];
312 a.n++;
313 }
314 std::string csv = "gx,gy,surf,minAttr,maxAttr,minSpd,maxSpd,maxDrop,maxFall,maxZ,n\n";
315 for (auto& kv : cells)
316 {
317 char buf[160];
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);
320 csv += buf;
321 }
322 jaffarCommon::file::saveStringToFile(csv, terrainDrivePath.c_str());
323 jaffarCommon::logger::log("[J+] Terrain drive: %lu cells logged to %s\n", cells.size(), terrainDrivePath.c_str());
324 return false;
325 }
326
327 // Synthetic terrain sweep: probe the surface the game computes at a grid of player positions.
328 if (terrainSweepSpec.empty() == false)
329 {
330 // parse "out.csv:x0,x1,y0,y1,step"
331 auto colon = terrainSweepSpec.find(':');
332 std::string outPath = terrainSweepSpec.substr(0, colon);
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]);
335 auto* emu = r.getGame()->getEmulator();
336 auto* ram = emu->getProperty("LRAM").pointer;
337 const size_t stateSize = emu->getStateSize();
338 // capture the base (post-init) state
339 std::string base;
340 base.resize(stateSize);
341 {
342 jaffarCommon::serializer::Contiguous s(base.data(), stateSize);
343 emu->serializeState(s);
344 }
345 const auto accelInput = emu->registerInput("|..|.....B..|"); // hold accelerate (B) so the car actually DRIVES
346 // Force the whole player position block to the target world (x,y) every frame so no velocity update can drift
347 // it, then read the surface (0xA649), attribute (0xA648), checkpoint (0xA6DB) and fall (0xA66C) the game computes.
348 // DRIVE-THROUGH PROBE: for each grid cell, place a MOVING car one cell before it (on a road approach) and drive
349 // it INTO the cell, measuring the true physics response: slowdown, slip, fall (hole), bump (solid), jump (ramp).
350 // grid gy <-> racer[0x00](0xA60C) (worldX), grid gx <-> racer[0x04](0xA610) (worldY); vel = racer[0x08]/[0x0C].
351 auto rd16 = [&](int adr) { return ram[adr] | (ram[adr + 1] << 8); };
352 auto s16 = [&](int adr)
353 {
354 int v = rd16(adr);
355 return v >= 0x8000 ? v - 0x10000 : v;
356 };
357 auto setPos = [&](int a, int b)
358 {
359 ram[0xA60C] = a & 0xFF;
360 ram[0xA60D] = (a >> 8) & 0xFF;
361 ram[0xA60E] = 0;
362 ram[0xA60F] = 0;
363 ram[0xA610] = b & 0xFF;
364 ram[0xA611] = (b >> 8) & 0xFF;
365 ram[0xA612] = 0;
366 ram[0xA613] = 0;
367 };
368 auto setVel = [&](int vx, int vy)
369 {
370 ram[0xA614] = vx & 0xFF;
371 ram[0xA615] = (vx >> 8) & 0xFF;
372 ram[0xA616] = 0;
373 ram[0xA617] = 0;
374 ram[0xA618] = vy & 0xFF;
375 ram[0xA619] = (vy >> 8) & 0xFF;
376 ram[0xA61A] = 0;
377 ram[0xA61B] = 0;
378 };
379 auto speed = [&]()
380 {
381 double vx = rd16(0xA614) + rd16(0xA616) / 65536.0, vy = rd16(0xA618) + rd16(0xA61A) / 65536.0;
382 return std::sqrt(vx * vx + vy * vy);
383 };
384 auto gyOf = [&]() { return ((rd16(0xA60C) >> 3) / 12) & 31; };
385 auto gxOf = [&]() { return ((rd16(0xA610) >> 3) / 12) & 31; };
386 // Drive the car INTO target cell (gx,gy) from one adjacent cell, along one of the 4 axes. The approach cell
387 // and the whole path must stay ON-track: if recovery (0xA66C) fires BEFORE we reach the target, this approach
388 // started/passed through void -> discard it. A hole = 0xA66C fires only AFTER we cleanly enter the target.
389 // Returns a per-approach result. dir: 0=+X(from gy-1) 1=-X(gy+1) 2=+Y(gx-1) 3=-Y(gx+1).
390 struct Res
391 {
392 int reached = 0, valid = 0, blocked = 0, hole = 0, maxZ = -32768, minAttr = 99, surf = -1;
393 double minSpd = 99;
394 };
395 auto probe = [&](int gx, int gy, int dir) -> Res
396 {
397 Res R;
398 {
399 jaffarCommon::deserializer::Contiguous d(base.data(), stateSize);
400 emu->deserializeState(d);
401 }
402 const int V = 6; // approach speed (px/frame): ~16 frames to cross a 96u cell
403 if (dir == 0)
404 {
405 setPos((gy - 1) * 96 + 48, gx * 96 + 48);
406 setVel(+V, 0);
407 }
408 else if (dir == 1)
409 {
410 setPos((gy + 1) * 96 + 48, gx * 96 + 48);
411 setVel(-V, 0);
412 }
413 else if (dir == 2)
414 {
415 setPos(gy * 96 + 48, (gx - 1) * 96 + 48);
416 setVel(0, +V);
417 }
418 else
419 {
420 setPos(gy * 96 + 48, (gx + 1) * 96 + 48);
421 setVel(0, -V);
422 }
423 ram[0xA66C] = 0;
424 int lastMove = rd16(dir < 2 ? 0xA60C : 0xA610), stuck = 0, entered = 0;
425 for (int k = 0; k < 32; k++)
426 {
427 r.advanceState(accelInput);
428 bool inTarget = (gyOf() == gy && gxOf() == gx);
429 if (!entered && ram[0xA66C] != 0)
430 {
431 R.valid = 0;
432 return R;
433 } // recovery before entry -> void approach, discard
434 if (inTarget)
435 {
436 entered = 1;
437 R.reached = 1;
438 R.valid = 1;
439 double s = speed();
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;
444 int z = s16(0xA64C);
445 if (z > R.maxZ) R.maxZ = z;
446 if (ram[0xA66C] != 0) R.hole = 1; // fell AT the target on a clean approach = real hole
447 }
448 int mv = rd16(dir < 2 ? 0xA60C : 0xA610);
449 if (std::abs(mv - lastMove) < 2)
450 stuck++;
451 else
452 stuck = 0;
453 lastMove = mv;
454 if (stuck >= 6)
455 {
456 if (!entered) R.blocked = 1;
457 R.valid = 1;
458 break;
459 } // stopped before entering = solid wall
460 }
461 return R;
462 };
463 // DIAGNOSTIC: single-cell range -> poke car onto the tile, hold accelerate, print attr/surf/spd/fall per frame
464 if (x0 == x1 && y0 == y1)
465 {
466 {
467 jaffarCommon::deserializer::Contiguous d(base.data(), stateSize);
468 emu->deserializeState(d);
469 }
470 setPos(x0 * 96 + 48, y0 * 96 + 48);
471 setVel(2, 0);
472 ram[0xA66C] = 0;
473 std::string dg = "f,gy,gx,attr,surf,spd,fall,Z\n";
474 char b[128];
475 for (int k = 0; k < 90; k++)
476 {
477 r.advanceState(accelInput);
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));
479 dg += b;
480 }
481 jaffarCommon::file::saveStringToFile(dg, outPath.c_str());
482 return false;
483 }
484 // interpret range as GRID cells: gy in [x0,x1], gx in [y0,y1]
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)
488 {
489 Res best;
490 int nApproach = 0, solidVotes = 0;
491 for (int dir = 0; dir < 4; dir++)
492 {
493 Res R = probe(gx, gy, dir);
494 if (!R.valid) continue; // approach came through void; ignore
495 nApproach++;
496 if (R.blocked)
497 {
498 solidVotes++;
499 continue;
500 }
501 if (R.reached)
502 { // merge the effect from any clean reaching approach
503 best.reached = 1;
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;
508 best.surf = R.surf;
509 }
510 }
511 if (best.minSpd > 50) best.minSpd = 0;
512 // solid = a clean road approach reached the adjacent cell but was walled out, and NO approach entered
513 int solid = (best.reached == 0 && solidVotes > 0) ? 1 : 0;
514 char buf[160];
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,
516 nApproach);
517 csv += buf;
518 }
519 jaffarCommon::file::saveStringToFile(csv, outPath.c_str());
520 jaffarCommon::logger::log("[J+] Terrain sweep written to %s\n", outPath.c_str());
521 return false;
522 }
523
524 // Variable for current step in view
525 ssize_t currentStep = 0;
526
527 // Getting sequence length
528 const ssize_t sequenceLength = solutionSequence.size();
529
530 // Getting inverse frame rate from game
531 const auto frameRate = r.getGame()->getFrameRate();
532 const uint32_t inverseFrameRate = std::round((1.0 / frameRate) * 1.0e+6);
533
534 // Getting game state size
535 const auto stateSize = r.getStateSize();
536
537 // Printing information
538 jaffarCommon::logger::refreshTerminal();
539
540 // Instantiating playback instance
542
543 // Per-step joypad-poll dump: the read counter is reset and updated inside each frame advance and is
544 // NOT part of the serialized state, so it cannot be recovered from restored per-step states like the
545 // RAM/hash dumps below. Instead, replay the solution LIVE from the emulator's fresh step-0 state and
546 // sample the counter right after each advance. One-shot terminal pass (returns false) so the caller's
547 // playback loop does not re-run it.
548 if (dumpPollsPath.empty() == false)
549 {
550 auto* emu = r.getGame()->getEmulator();
551 const auto polls = emu->getProperty("Joypad Read Count");
552 std::string dump;
553 char line[64];
554 for (ssize_t s = 0; s < sequenceLength; s++)
555 {
556 r.advanceState(emu->registerInput(solutionSequence[s]));
557 snprintf(line, sizeof(line), "%ld\t%d\n", s + 1, *(const int*)polls.pointer);
558 dump += line;
559 }
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());
562 return false;
563 }
564
565 // Headless screenshot pass: capture each requested step to BMP. A per-step state RESTORE does NOT correctly
566 // repaint the framebuffer (GPGX's tile caches are not rebuilt by a state-load), so we replay the solution
567 // linearly from the emulator's FRESH step-0 state (as left by the runner's init sequence, before the playback
568 // build advances it) -- each advanceState() paints its frame -- and screenshot along the way, then finalize.
569 // This is a one-shot terminal pass: we MUST return false (finalize/quit) so the caller's playback loop does
570 // not re-run us. A repeat pass would restore the initial state via deserialize (which cannot repaint GPGX),
571 // then overwrite every good frame with a black one -- exactly the black-frame regression this avoids.
572 if (screenshotDir.empty() == false)
573 {
574 auto* emu = r.getGame()->getEmulator();
576 for (ssize_t s = 0; s < sequenceLength; s++)
577 {
578 const auto inputIndex = emu->registerInput(solutionSequence[s]);
579 r.advanceState(inputIndex);
580 if (screenshotSteps.empty() || screenshotSteps.count((size_t)(s + 1)) > 0)
581 {
582 char path[1024];
583 snprintf(path, sizeof(path), "%s/step_%06ld.bmp", screenshotDir.c_str(), s + 1);
584 emu->saveScreenshot(path);
585 // Also dump the LIVE video RAM (correct here, since we advanced via advanceState rather than a
586 // state-load) so it can be lifted into a transplanted state whose vram is from a different track.
587 // Not all emulators expose a VRAM property (e.g. QuickerNES) -- skip quietly if absent.
588 try
589 {
590 const auto vram = emu->getProperty("VRAM");
591 char vpath[1024];
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);
595 }
596 catch (const std::exception&)
597 {
598 }
599 }
600 }
601 return false;
602 }
603
604 // Initializing playback instance. In headless mode (--disableRender) renderFrame is never called,
605 // so skip caching the per-step renderer framebuffer (~256KB/step) -- critical for long movies.
606 p.initialize(solutionSequence, !disableRender);
607
608 // Flag to display frame information
609 bool showFrameInfo = true;
610
611 // Finalization flag
612 bool isFinalize = false;
613
614 // Checking for repeated state hashes
615 std::vector<ssize_t> repeatedHashStates;
616 for (ssize_t i = 0; i < sequenceLength; i++)
617 {
618 const auto repeatedHashSteps = p.getStateRepeatedHashSteps(i);
619 if (repeatedHashSteps.size() > 0) repeatedHashStates.push_back(i);
620 }
621
622 // Checking for not-allowed inputs
623 std::vector<ssize_t> notAllowedInputStates;
624 for (ssize_t i = 0; i < sequenceLength; i++)
625 {
626 const auto isInputAllowed = p.isInputAllowed(i);
627 if (isInputAllowed == false) notAllowedInputStates.push_back(i);
628 }
629
630 // If requested, dump the per-step game-state hash for every step (including the end-of-sequence
631 // step) to a file. Diffing the dumps of two emulators replaying the same solution pinpoints the
632 // exact first frame at which their hashed game RAM diverges.
633 if (dumpHashesPath.empty() == false)
634 {
635 std::string dump;
636 char line[64];
637 for (ssize_t i = 0; i <= sequenceLength; i++)
638 {
639 jaffarCommon::hash::hash_t hash;
640 if (dumpHashesLookahead == 0)
641 hash = p.getStateHash(i);
642 else
643 {
644 // Lookahead-aware hash (mirrors the engine's "Hash Lookahead" digest): restore the
645 // step's state into the live runner, advance N null inputs, hash the resulting state.
646 // The next loadStepData() restores whatever this mutates, as in the RAM dump below.
647 p.loadStepData(i);
648 // The runner's step counter is not serialized: restore it so the hash's step-tolerance
649 // stage matches the engine's ((depth + lookahead) mod (tolerance+1)) for depth-i states.
650 r.setSearchStep((size_t)i);
651 const auto nullIdx = r.getGame()->getNullInputIndex();
652 for (size_t k = 0; k < dumpHashesLookahead; k++) r.advanceState(nullIdx);
653 hash = r.computeHash();
654 }
655 snprintf(line, sizeof(line), "%ld\t%016lX%016lX\n", i, hash.first, hash.second);
656 dump += line;
657 }
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());
660 }
661
662 // If requested, dump the full low work-RAM for every step as a flat binary blob. Reading the RAM
663 // requires restoring each step's state into the live emulator first (loadStepData), so this mutates
664 // the live state -- harmless here since the interactive loop re-loads per step regardless.
665 if (dumpRamPath.empty() == false)
666 {
667 const auto lram = r.getGame()->getEmulator()->getProperty("LRAM");
668 std::string dump;
669 dump.reserve((size_t)(sequenceLength + 1) * lram.size);
670 for (ssize_t i = 0; i <= sequenceLength; i++)
671 {
672 p.loadStepData(i);
673 dump.append((const char*)lram.pointer, lram.size);
674 }
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());
676 }
677
678 // If requested, dump the full video RAM (VRAM) for every step as a flat binary blob (for finding the
679 // VRAM region inside a foreign emulator savestate by cross-correlation).
680 if (dumpVramPath.empty() == false)
681 {
682 const auto vram = r.getGame()->getEmulator()->getProperty("VRAM");
683 std::string dump;
684 dump.reserve((size_t)(sequenceLength + 1) * vram.size);
685 for (ssize_t i = 0; i <= sequenceLength; i++)
686 {
687 p.loadStepData(i);
688 dump.append((const char*)vram.pointer, vram.size);
689 }
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());
691 }
692
693 // If requested, write the per-step game reward (one value per line) to a file. The reward is part of the
694 // serialized game state, so loadStepData restores each step's reward directly -- this is exactly the value
695 // the search compares against, suitable as a "Reference Reward Floor" trace.
696 if (dumpRewardPath.empty() == false)
697 {
698 std::string dump;
699 for (ssize_t i = 0; i <= sequenceLength; i++)
700 {
701 p.loadStepData(i);
702 // Full precision (NOT std::to_string, which truncates to 6 decimals): the reward is on the 1/256
703 // sub-pixel grid, so 6 decimals (e.g. 0.246094) does not round-trip the exact value (0.24609375),
704 // which makes a "Reference Reward Floor" with tolerance 0 false-cancel on an EXACT match. %.17g
705 // preserves the value so a tol=0 floor only cancels when the search is genuinely behind.
706 char rbuf[64];
707 snprintf(rbuf, sizeof(rbuf), "%.17g", (double)r.getGame()->getReward());
708 dump += std::string(rbuf) + "\n";
709 }
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());
712 }
713
714 // If requested, write the game's per-step trace line (Game::getTraceLine) to a file, one line per step. Like the
715 // reward dump, loadStepData restores each step's full state first, so the coordinates are exact. Suitable as a
716 // game "Trace File Path" for the trace magnet.
717 if (dumpTracePath.empty() == false)
718 {
719 std::string dump;
720 for (ssize_t i = 0; i <= sequenceLength; i++)
721 {
722 p.loadStepData(i);
723 dump += r.getGame()->getTraceLine() + "\n";
724 }
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());
727 }
728
729 // If requested, restore the state at a single step and save the emulator's FULL state to a file (for use
730 // as the Emulator "Initial State File Path" -- a mid-run seed). Prints the bike posX so the caller can set
731 // the game's "Initial Block Transitions" to make _bikePosX absolute. Then exits.
732 if (saveStateFilePath.empty() == false)
733 {
734 const auto step = (ssize_t)parseUInt(saveStateStepStr, "--saveStateStep");
735 p.loadStepData(step);
736 // Full-fidelity save: enable ALL state properties (incl. the search-disabled SPRT/NTAB/CHRR/
737 // SRAM) so the blob is loadable as an "Initial State File Path" by a freshly-constructed
738 // emulator (which has everything enabled). Saving the reduced hot state produced an
739 // undersized blob that the loader rejected ("Maximum input data position reached").
740 auto* saveEmu = r.getGame()->getEmulator();
741 saveEmu->enableAllStateProperties();
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);
751 r.getGame()->printInfo();
752 return 0;
753 }
754
755 // Interactive section
756 while (isFinalize == false)
757 {
758 // Updating the SDL display window (headless screenshots are handled by a separate pass above)
759 if (disableRender == false)
760 if (currentStep % frameskip == 0) p.renderFrame(currentStep);
761
762 // Loading step data
763 p.loadStepData(currentStep);
764
765 // Getting input string
766 const auto& inputString = p.getStateInputString(currentStep);
767
768 // Getting input index
769 const auto& inputIndex = p.getStateInputIndex(currentStep);
770
771 // Getting state hash
772 const auto hash = p.getStateHash(currentStep);
773
774 // Getting repeated step hashes (if any)
775 const auto repeatedHashSteps = p.getStateRepeatedHashSteps(currentStep);
776
777 // Checking if the current input is within the allowed inputs for this state
778 const auto isInputAllowed = p.isInputAllowed(currentStep);
779
780 // If running a command, don't print frame info, and finalize immediately after
781 if (runCommand != "")
782 {
783 isFinalize = true;
784 showFrameInfo = false;
785 isReproduce = false;
786 isUnattended = true;
787 }
788
789 // Printing data and commands
790 if (showFrameInfo)
791 {
792 jaffarCommon::logger::clearTerminal();
793
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");
799
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);
803 else
804 {
805 for (size_t i = 0; i < 5; i++) jaffarCommon::logger::log(" %ld ", repeatedHashStates[i]);
806 jaffarCommon::logger::log(" ... ");
807 }
808 jaffarCommon::logger::log(" ] \n");
809
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);
813 else
814 {
815 for (size_t i = 0; i < 5; i++) jaffarCommon::logger::log(" %ld ", notAllowedInputStates[i]);
816 jaffarCommon::logger::log(" ... ");
817 }
818 jaffarCommon::logger::log(" ] \n");
819
820 jaffarCommon::logger::log("[J+] Game Name: '%s'\n", r.getGame()->getName().c_str());
821 jaffarCommon::logger::log("[J+] Emulator Name: '%s'\n", r.getGame()->getEmulator()->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);
831 jaffarCommon::logger::log("[J+] Checkpoint: Level: %lu, Tolerance: %lu\n", r.getGame()->getCheckpointLevel(), r.getGame()->getCheckpointTolerance());
832 jaffarCommon::logger::log("[J+] Manual Save Solution: Active: %s, Path: '%s', Last Rule: (Current: %ld), (Prev: %ld)\n", r.getGame()->isSaveSolution() ? "Yes" : "No",
834 p.printInfo();
835
836 // Print General Commands
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");
838
839 // Print any game-specific commands (optional)
841
842 jaffarCommon::logger::refreshTerminal();
843 }
844
845 // Resetting show frame info flag
846 showFrameInfo = true;
847
848 // Specifies the command to execute next
849 int command = 0;
850
851 // If it's reproducing,
852 if (isReproduce == true)
853 {
854 // Headless batch reproduction (no renderer, unattended) has nobody watching or typing, so skip
855 // the frame-rate pacing sleep and key polling and advance at full emulation speed instead.
856 if (disableRender == false || isUnattended == false)
857 {
858 // Introducing sleep related to the frame rate
859 usleep(inverseFrameRate);
860
861 // Get command without interrupting
862 command = jaffarCommon::logger::getKeyPress();
863 }
864
865 // Advance to the next frame
866 currentStep++;
867 }
868
869 // If it's not reproducing, grab command with a wait
870 if (isReproduce == false && isUnattended == false) command = jaffarCommon::logger::waitForKeyPress();
871
872 // Headless fast-forward: when unattended and not actively reproducing (and not running a one-shot
873 // command), advance through the sequence as fast as possible -- no key wait, no frame-rate sleep.
874 // This is what makes --unattended --exitOnEnd terminate promptly for batch/verification runs
875 // (otherwise neither branch above advances and the loop spins forever).
876 if (isReproduce == false && isUnattended == true && runCommand == "") currentStep++;
877
878 // If running a command given from the console, set it now
879 if (runCommand != "") command = runCommand[0];
880
881 // Handle commands
882 switch (command)
883 {
884 // Advance/Rewind commands
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;
893
894 case 's':
895 {
896 // Storing state file
897 std::string saveFileName = "quicksave.state";
898
899 std::string saveData;
900 size_t stateSize = r.getGame()->getEmulator()->getStateSize();
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());
906
907 // Do no show frame info again after this action
908 showFrameInfo = false;
909
910 break;
911 }
912
913 // Toggles playback from current point
914 case 'p': isReproduce = !isReproduce; break;
915
916 // Toggles Auto Reload
917 case 'r': isReload = !isReload; break;
918
919 // Triggers the exit
920 case 'q': isFinalize = true; break;
921
922 // Handle any game-specific commands. If such command is executed, do not clear output
923 default: showFrameInfo = r.getGame()->playerParseCommand(command) == false;
924 }
925
926 // Correct current step if requested more than possible
927 if (currentStep < 0) currentStep = 0;
928
929 // If reloading on finish, do it now
930 if (currentStep > sequenceLength && isReload == true) break;
931
932 // If exiting on finish, do it now
933 if (currentStep > sequenceLength && isExitOnEnd == true) break;
934
935 // If not reloading on finish, simply stop
936 if (currentStep > sequenceLength)
937 {
938 currentStep = sequenceLength;
939 isReproduce = false;
940 }
941 }
942
943 // If requested, print a stable summary of the final (end-of-sequence) state. This is the
944 // machine-checkable oracle for headless reproduction tests: the hash is deterministic, so the
945 // same config+solution must always produce the same value here.
946 if (printFinalState)
947 {
948 const auto finalHash = p.getStateHash(sequenceLength);
949 const auto stateType = r.getGame()->getStateType();
950 const std::string stateTypeString = stateType == jaffarPlus::Game::stateType_t::win ? "Win" : (stateType == jaffarPlus::Game::stateType_t::fail ? "Fail" : "Normal");
951 jaffarCommon::logger::log("[J+] Final Step: %ld\n", sequenceLength);
952 jaffarCommon::logger::log("[J+] Final State Type: %s\n", stateTypeString.c_str());
953 // First step (inputs applied) at which the solution reaches a win / fail state. Useful to spot a
954 // movie that wins before its end (wasted trailing inputs) or fails midway. "none" if it never does.
955 const auto firstWinStep = p.getFirstWinStep();
956 const auto firstFailStep = p.getFirstFailStep();
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);
962 // Solution-quality counts: inputs the engine would not have considered at their frame, and
963 // states the engine would have pruned as duplicates. Both are 0 for a clean engine-found path.
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());
966 }
967
968 // returning false on exit to trigger the finalization
969 if (isFinalize) return false;
970
971 // Otherwise, keep looping
972 return true;
973}
974
992int main(int argc, char* argv[])
993{
994 // Parsing command line arguments
995 argparse::ArgumentParser program("jaffar-tester", "2.0.0");
996
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")
1046 .help(
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(""));
1053
1054 // Try to parse arguments
1055 try
1056 {
1057 program.parse_args(argc, argv);
1058 }
1059 catch (const std::runtime_error& err)
1060 {
1061 JAFFAR_THROW_LOGIC("%s\n%s", err.what(), program.help().str().c_str());
1062 }
1063
1064 // Parsing config file
1065 const std::string configFile = program.get<std::string>("configFile");
1066
1067 // Parsin solution file
1068 const std::string solutionFile = program.get<std::string>("solutionFile");
1069
1070 // Getting reload flag
1071 bool doReload = program.get<bool>("--reload");
1072
1073 // Getting reproduce flag
1074 bool reproduceStart = program.get<bool>("--reproduce");
1075
1076 // Getting disablerender flag
1077 bool disableRender = program.get<bool>("--disableRender");
1078
1079 // Parsing --pokeRAM "0xADDR=VAL,0xADDR=VAL" (addresses/values accept 0x hex or decimal)
1080 {
1081 const auto pokeSpec = program.get<std::string>("--pokeRAM");
1082 if (pokeSpec.empty() == false)
1083 for (const auto& tok : jaffarCommon::string::split(pokeSpec, ','))
1084 {
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);
1090 }
1091 }
1092
1093 // Getting screenshot options
1094 terrainDrivePath = program.get<std::string>("--terrainDrive");
1095 terrainSweepSpec = program.get<std::string>("--terrainSweep");
1096 pokeNtabPath = program.get<std::string>("--pokeNTAB");
1097 dumpNtabPath = program.get<std::string>("--dumpNTAB");
1098 savePokedStatePath = program.get<std::string>("--savePokedState");
1099 savePokedStateStepStr = program.get<std::string>("--savePokedStateStep");
1100 screenshotDir = program.get<std::string>("--screenshotDir");
1101 {
1102 const auto stepsStr = program.get<std::string>("--screenshotSteps");
1103 if (stepsStr.empty() == false)
1104 for (const auto& tok : jaffarCommon::string::split(stepsStr, ','))
1105 if (tok.empty() == false) screenshotSteps.insert(parseUInt(tok, "--screenshotSteps"));
1106 }
1107
1108 // Getting exitOnEnd flag
1109 bool exitOnEnd = program.get<bool>("--exitOnEnd");
1110
1111 // Getting unattended flag
1112 bool unattended = program.get<bool>("--unattended");
1113
1114 // Getting frameskip
1115 frameskip = parseUInt(program.get<std::string>("--frameskip"), "--frameskip");
1116
1117 // Getting frameskip
1118 const std::string initialSequence = program.get<std::string>("--initialSequence");
1119
1120 // Getting command to run (if any)
1121 runCommand = program.get<std::string>("--runCommand");
1122
1123 // Getting the print-final-state flag
1124 printFinalState = program.get<bool>("--printFinalState");
1125
1126 // Getting the per-step hash dump path (if any)
1127 dumpHashesPath = program.get<std::string>("--dumpHashes");
1128 dumpHashesLookahead = (size_t)std::stoul(program.get<std::string>("--dumpHashesLookahead"));
1129
1130 // Getting the per-step RAM dump path (if any)
1131 dumpRamPath = program.get<std::string>("--dumpRam");
1132 dumpVramPath = program.get<std::string>("--dumpVram");
1133 dumpPollsPath = program.get<std::string>("--dumpPolls");
1134 dumpRewardPath = program.get<std::string>("--dumpReward");
1135 dumpTracePath = program.get<std::string>("--dumpTrace");
1136 dumpRacerPath = program.get<std::string>("--dumpRacer");
1137 saveStateStepStr = program.get<std::string>("--saveStateStep");
1138 saveStateFilePath = program.get<std::string>("--saveStateFile");
1139
1140 // Initializing terminal
1141 jaffarCommon::logger::initializeTerminal();
1142
1143 // Setting initial reproduction values
1144 isReload = doReload;
1145 isReproduce = reproduceStart;
1146 isExitOnEnd = exitOnEnd;
1147 isUnattended = unattended;
1148
1149 // If config file defined, read it now
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());
1153
1154 // Parsing configuration file
1155 nlohmann::json config;
1156 try
1157 {
1158 config = nlohmann::json::parse(configFileString);
1159 }
1160 catch (const std::exception& err)
1161 {
1162 JAFFAR_THROW_LOGIC("[ERROR] Parsing configuration file %s. Details:\n%s\n", configFile.c_str(), err.what());
1163 }
1164
1165 // Getting component configurations
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");
1169
1170 // Overriding initial solution file, if provided
1171 if (initialSequence != "") emulatorConfig["Initial Sequence File Path"] = initialSequence;
1172
1173 // Disabling frameskip, if enabled
1174 runnerConfig["Frameskip"]["Rate"] = 0;
1175
1176 // Creating runner from the configuration
1177 auto r = jaffarPlus::Runner::getRunner(emulatorConfig, gameConfig, runnerConfig);
1178
1179 // Initializing runner
1180 r->initialize();
1181
1182 // Enabling rendering, if required
1183 if (screenshotDir.empty() == false)
1184 {
1185 // Headless screenshot mode: render frames into the emulator's framebuffer without opening an SDL window.
1186 r->getGame()->getEmulator()->enableHeadlessRendering();
1187 }
1188 else if (disableRender == false)
1189 {
1190 r->getGame()->getEmulator()->initializeVideoOutput();
1191 r->getGame()->getEmulator()->enableRendering();
1192 }
1193
1194 // Getting game state size
1195 const auto stateSize = r->getStateSize();
1196
1197 // Storage for the initial state
1198 std::string initialState;
1199 initialState.resize(stateSize);
1200
1201 // Getting initial state
1202 jaffarCommon::serializer::Contiguous s(initialState.data(), initialState.size());
1203 r->serializeState(s);
1204
1205 // ALSO capture a FULL-fidelity emulator snapshot (all state properties enabled, including any
1206 // configured-out blocks like NES nametables). The reduced runner state above restores game RAM
1207 // but leaves excluded video state (e.g. NTAB) at its end-of-movie contents, so every reload
1208 // cycle displayed the PREVIOUS cycle's background. Restoring this full snapshot first fixes it.
1209 auto* fullEmu = r->getGame()->getEmulator();
1210 std::string fullInitialState;
1211 {
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();
1218 }
1219
1220 // Running main cycle
1221 bool continueRunning = true;
1222 while (continueRunning == true)
1223 {
1224 // Running main cycle
1225 continueRunning = mainCycle(*r, solutionFile, disableRender);
1226
1227 // If the exit-on-end flag is set, then do not repeat reproduction
1228 if (exitOnEnd == true) break;
1229
1230 // If the playback repeats, then sleep and restore the initial state
1231 if (continueRunning == true)
1232 {
1233 // If repeating, then wait a bit before repeating to prevent fast repetition of short movies
1234 sleep(1);
1235
1236 // Reloading the initial state (captured at step 0); the step counter is not in the stream, so reset
1237 // it here before deserializing (the player advances it itself as it replays).
1238 // Full-fidelity emulator restore FIRST (brings back excluded blocks like nametables), then
1239 // the reduced runner restore for game/runner bookkeeping.
1240 {
1241 fullEmu->enableAllStateProperties();
1242 jaffarCommon::deserializer::Contiguous fd(fullInitialState.data(), fullInitialState.size());
1243 fullEmu->deserializeState(fd);
1244 fullEmu->reapplyDisabledStateProperties();
1245 }
1246 r->setStepCount(0);
1247 jaffarCommon::deserializer::Contiguous d(initialState.data(), initialState.size());
1248 r->deserializeState(d);
1249 }
1250 }
1251
1252 // If redering was enabled, finish it now
1253 if (disableRender == false) r->getGame()->getEmulator()->finalizeVideoOutput();
1254
1255 // Ending ncurses window
1256 jaffarCommon::logger::finalizeTerminal();
1257}
InputSet::inputIndex_t registerInput(const std::string inputString)
Registers an input string, avoiding repeated decoding of the same input.
Definition emulator.hpp:95
virtual void enableAllStateProperties()
Temporarily re-enables ALL state properties disabled via configuration (e.g.
Definition emulator.hpp:149
virtual void enableHeadlessRendering()
Enables offline (no-window) frame rendering so subsequent steps can be screenshotted headlessly.
Definition emulator.hpp:243
virtual void serializeState(jaffarCommon::serializer::Base &serializer) const =0
Serializes the emulator state into the given serializer.
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:677
size_t getCheckpointTolerance() const
Returns the current state's checkpoint tolerance.
Definition game.hpp:623
virtual InputSet::inputIndex_t getNullInputIndex() const
The input index representing "no buttons pressed" – used by the engine's Hash Lookahead (advance-with...
Definition game.hpp:614
bool isSaveSolution() const
Indicates whether the current state should trigger a save solution.
Definition game.hpp:629
Emulator * getEmulator() const
Returns a pointer to the internal emulator.
Definition game.hpp:582
ssize_t getSaveSolutionCurrentLastRuleIdx() const
Returns the current last rule index that set a save solution.
Definition game.hpp:635
virtual void playerPrintCommands() const
Prints the game's player-specific commands, if any.
Definition game.hpp:671
size_t getCheckpointLevel() const
Returns the current state's checkpoint level.
Definition game.hpp:620
float getFrameRate() const
Returns the configured frame rate.
Definition game.hpp:593
void printInfo() const
Prints the current game state to the logger.
Definition game.hpp:300
float getReward() const
Returns the current state's reward.
Definition game.hpp:596
stateType_t getStateType() const
Returns the current state type (normal, win or fail).
Definition game.hpp:617
@ 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:644
const std::string getSaveSolutionPath() const
Returns the save path of the rule that activated the current save solution.
Definition game.hpp:641
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:602
ssize_t getSaveSolutionPrevLastRuleIdx() const
Returns the previous last rule index that set a save solution.
Definition game.hpp:632
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:237
void printInfo() const
Prints runner, game, and emulator information.
Definition playback.hpp:271
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:230
ssize_t getFirstFailStep() const
Returns the first step (number of inputs applied) at which the solution reaches a fail state,...
Definition playback.hpp:243
void initialize(const std::vector< std::string > &inputSequence, bool storeRendererState=true)
Replays the input sequence, recording one cached step per input (plus a trailing end-of-sequence step...
Definition playback.hpp:84
jaffarPlus::InputSet::inputIndex_t getStateInputIndex(const size_t currentStep) const
Returns the input index of the given step.
Definition playback.hpp:222
std::string getStateInputString(const size_t currentStep) const
Returns the input string of the given step.
Definition playback.hpp:220
jaffarCommon::hash::hash_t getStateHash(const size_t currentStep) const
Returns the state hash of the given step.
Definition playback.hpp:228
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:226
void loadStepData(const size_t stepId)
Loads the cached game state of the given step back into the runner.
Definition playback.hpp:261
void renderFrame(const size_t currentStep)
Renders the cached frame for the given step into the emulator window.
Definition playback.hpp:249
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
void advanceState(const InputSet::inputIndex_t inputIdx)
Advances the game by one input, then by the configured number of frameskip frames.
Definition runner.hpp:309
void setSearchStep(const size_t searchStep)
Sets the step counter from a search step.
Definition runner.hpp:351
jaffarCommon::hash::hash_t computeHash() const
Computes a hash of the current runner state.
Definition runner.hpp:426
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,...
std::string pokeNtabPath
When non-empty (–pokeNTAB), binary file to block-copy into nametable memory post-init.
Definition player.cpp:88
int main(int argc, char *argv[])
Entry point for the jaffar-player (jaffar-tester) executable.
Definition player.cpp:992
bool isReload
Switch to toggle whether to reload the movie on reaching the end of the sequence.
Definition player.cpp:31
bool printFinalState
When set, prints a stable, machine-readable summary of the final state on exit (for headless verifica...
Definition player.cpp:41
bool isReproduce
Switch to toggle whether to reproduce (auto-advance) the movie.
Definition player.cpp:33
std::string screenshotDir
Directory to write per-frame screenshots (BMP) into; empty disables screenshotting.
Definition player.cpp:97
size_t dumpHashesLookahead
Null-input advances applied before hashing each step in the –dumpHashes pass (mirrors engine "Hash Lo...
Definition player.cpp:49
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:121
std::vector< std::pair< uint32_t, uint8_t > > pokeRam
Work-RAM bytes to poke into the post-initial-sequence state before replaying (for mechanic experiment...
Definition player.cpp:103
std::string dumpRewardPath
When non-empty (–dumpReward), writes the per-step game reward (one value per line) for the replayed s...
Definition player.cpp:67
size_t frameskip
Number of frames to skip between renderings.
Definition player.cpp:35
std::string saveStateFilePath
When non-empty (–saveStateFile), the path to write the emulator savestate captured at –saveStateStep ...
Definition player.cpp:81
std::set< size_t > screenshotSteps
Steps to capture as screenshots; empty captures all rendered steps when a dir is given.
Definition player.cpp:99
std::string runCommand
Command to run initially and then exit.
Definition player.cpp:37
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:46
std::string dumpTracePath
When non-empty (–dumpTrace), writes the game's per-step trace line (Game::getTraceLine,...
Definition player.cpp:71
std::string savePokedStatePath
When non-empty (–savePokedState), save the post-poke full-fidelity emulator state to this path and ex...
Definition player.cpp:85
std::string terrainSweepSpec
When set (–terrainSweep "out.csv:x0,x1,y0,y1,step"), probe the terrain the game computes at a grid of...
Definition player.cpp:107
std::string dumpVramPath
When non-empty, writes the full VRAM for every step to this file as a flat binary blob.
Definition player.cpp:56
std::string dumpNtabPath
When non-empty (–dumpNTAB), dump post-init nametable memory to this file and exit.
Definition player.cpp:91
std::string terrainDrivePath
When set (–terrainDrive out.csv), branch off the replayed solution every few frames and steer a REAL ...
Definition player.cpp:111
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:54
std::string dumpRacerPath
When non-empty (–dumpRacer), dumps frame,gx,gy,spd + the full 234-byte racer struct per frame.
Definition player.cpp:73
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:156
std::string saveStateStepStr
When set (–saveStateStep), the step at which to capture a full emulator savestate (paired with –saveS...
Definition player.cpp:77
bool isExitOnEnd
Determines that the reproduction must end on reaching the last step.
Definition player.cpp:29
std::string savePokedStateStepStr
Steps of the solution to advance after the poke before saving (–savePokedStateStep).
Definition player.cpp:94
bool isUnattended
Prevents the interactive player from stalling for a keystroke.
Definition player.cpp:27
std::string dumpPollsPath
When non-empty (–dumpPolls), writes the per-step joypad read count ("step\tcount" per line) for the r...
Definition player.cpp:63
Drives a Game forward one input at a time, managing the allowed/candidate input sets,...
uint8_t * pointer
Pointer to the start of the property's memory segment.
Definition emulator.hpp:26