yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
save_state_manager.cc
Go to the documentation of this file.
2
3#include <cstdio>
4#include <cstdlib>
5#include <fstream>
6#include <sys/stat.h>
7
8#ifndef __EMSCRIPTEN__
9#include <filesystem>
10#endif
11
12#include "app/emu/snes.h"
13#include "rom/rom.h"
14
15namespace {
16
17#ifdef __EMSCRIPTEN__
18// Simple path utilities for WASM builds
19std::string GetParentPath(const std::string& path) {
20 size_t pos = path.find_last_of("/\\");
21 if (pos == std::string::npos) return "";
22 return path.substr(0, pos);
23}
24
25bool FileExists(const std::string& path) {
26 std::ifstream f(path);
27 return f.good();
28}
29
30void CreateDirectories(const std::string& path) {
31 // In WASM/Emscripten, directories are typically auto-created
32 // or we use MEMFS which doesn't require explicit directory creation
33 (void)path;
34}
35#else
36std::string GetParentPath(const std::string& path) {
37 std::filesystem::path p(path);
38 return p.parent_path().string();
39}
40
41bool FileExists(const std::string& path) {
42 return std::filesystem::exists(path);
43}
44
45void CreateDirectories(const std::string& path) {
46 std::filesystem::create_directories(path);
47}
48#endif
49
50} // namespace
51
52namespace yaze {
53namespace emu {
54namespace render {
55
57 : snes_(snes), rom_(rom) {
58 // Default state directory in user's config
59 state_directory_ = "./states";
60}
61
63
65 if (!snes_ || !rom_) {
66 return absl::FailedPreconditionError("SNES or ROM not provided");
67 }
68
69 // Calculate ROM checksum for compatibility checking
71 printf("[StateManager] ROM checksum: 0x%08X\n", rom_checksum_);
72
73 // Use ~/.yaze/states/ directory for state files
74 if (const char* home = std::getenv("HOME")) {
75 state_directory_ = std::string(home) + "/.yaze/states";
76 } else {
77 state_directory_ = "./states";
78 }
79
80 // Ensure state directory exists
81 CreateDirectories(state_directory_);
82 printf("[StateManager] State directory: %s\n", state_directory_.c_str());
83
84 return absl::OkStatus();
85}
86
87absl::Status SaveStateManager::LoadState(StateType type, int context_id) {
88 std::string path = GetStatePath(type, context_id);
89
90 if (!FileExists(path)) {
91 return absl::NotFoundError("State file not found: " + path);
92 }
93
94 // Load metadata and check compatibility
95 std::string meta_path = GetMetadataPath(type, context_id);
96 if (FileExists(meta_path)) {
97 auto meta_result = GetStateMetadata(type, context_id);
98 if (meta_result.ok() && !IsStateCompatible(*meta_result)) {
99 return absl::FailedPreconditionError(
100 "State incompatible with current ROM (checksum mismatch)");
101 }
102 }
103
104 return LoadStateFromFile(path);
105}
106
107absl::Status SaveStateManager::GenerateRoomState(int room_id) {
108 printf("[StateManager] Generating state for room %d...\n", room_id);
109
110 // Boot game to title screen
111 auto status = BootToTitleScreen();
112 if (!status.ok()) {
113 return status;
114 }
115
116 // Navigate to file select
117 status = NavigateToFileSelect();
118 if (!status.ok()) {
119 return status;
120 }
121
122 // Start new game
123 status = StartNewGame();
124 if (!status.ok()) {
125 return status;
126 }
127
128 // Navigate to target room (this is game-specific)
129 status = NavigateToRoom(room_id);
130 if (!status.ok()) {
131 return status;
132 }
133
134 // Save state
135 StateMetadata metadata;
136 metadata.rom_checksum = rom_checksum_;
137 metadata.rom_region = 0; // TODO: detect region
138 metadata.room_id = room_id;
139 metadata.game_module = GetGameModule();
140 metadata.description = "Room " + std::to_string(room_id);
141
142 std::string path = GetStatePath(StateType::kRoomLoaded, room_id);
143 return SaveStateToFile(path, metadata);
144}
145
147 // Generate states for common rooms used in testing and previews
148 const std::vector<std::pair<int, const char*>> baseline_rooms = {
149 {0x0012, "Sanctuary"},
150 {0x0020, "Hyrule Castle Entrance"},
151 {0x0028, "Eastern Palace Entrance"},
152 {0x0004, "Link's House"},
153 {0x0044, "Desert Palace Entrance"},
154 {0x0075, "Tower of Hera Entrance"},
155 };
156
157 int success_count = 0;
158 for (const auto& [room_id, name] : baseline_rooms) {
159 printf("[StateManager] Generating %s (0x%04X)...\n", name, room_id);
160 auto status = GenerateRoomState(room_id);
161 if (status.ok()) {
162 success_count++;
163 } else {
164 printf("[StateManager] Warning: Failed to generate %s: %s\n", name,
165 std::string(status.message()).c_str());
166 }
167 }
168
169 printf("[StateManager] Generated %d/%zu baseline states\n", success_count,
170 baseline_rooms.size());
171 return absl::OkStatus();
172}
173
174bool SaveStateManager::HasCachedState(StateType type, int context_id) const {
175 std::string path = GetStatePath(type, context_id);
176 return FileExists(path);
177}
178
179absl::StatusOr<StateMetadata> SaveStateManager::GetStateMetadata(
180 StateType type, int context_id) const {
181 std::string path = GetMetadataPath(type, context_id);
182
183 std::ifstream file(path, std::ios::binary);
184 if (!file) {
185 return absl::NotFoundError("Metadata file not found");
186 }
187
188 StateMetadata metadata;
189 file.read(reinterpret_cast<char*>(&metadata.version), sizeof(metadata.version));
190 file.read(reinterpret_cast<char*>(&metadata.rom_checksum),
191 sizeof(metadata.rom_checksum));
192 file.read(reinterpret_cast<char*>(&metadata.rom_region),
193 sizeof(metadata.rom_region));
194 file.read(reinterpret_cast<char*>(&metadata.room_id), sizeof(metadata.room_id));
195 file.read(reinterpret_cast<char*>(&metadata.game_module),
196 sizeof(metadata.game_module));
197
198 uint32_t desc_len;
199 file.read(reinterpret_cast<char*>(&desc_len), sizeof(desc_len));
200 if (desc_len > 0 && desc_len < 1024) {
201 metadata.description.resize(desc_len);
202 file.read(metadata.description.data(), desc_len);
203 }
204
205 if (!file) {
206 return absl::InternalError("Failed to read metadata");
207 }
208
209 return metadata;
210}
211
212absl::Status SaveStateManager::SaveStateToFile(const std::string& path,
213 const StateMetadata& metadata) {
214 // Ensure directory exists
215 std::string parent_path = GetParentPath(path);
216 if (!parent_path.empty()) {
217 CreateDirectories(parent_path);
218 }
219
220 // Save SNES state using existing method
221 auto save_status = snes_->saveState(path);
222 if (!save_status.ok()) {
223 return save_status;
224 }
225
226 // Save metadata
227 std::string meta_path = path + ".meta";
228 std::ofstream meta_file(meta_path, std::ios::binary);
229 if (!meta_file) {
230 return absl::InternalError("Failed to create metadata file");
231 }
232
233 meta_file.write(reinterpret_cast<const char*>(&metadata.version),
234 sizeof(metadata.version));
235 meta_file.write(reinterpret_cast<const char*>(&metadata.rom_checksum),
236 sizeof(metadata.rom_checksum));
237 meta_file.write(reinterpret_cast<const char*>(&metadata.rom_region),
238 sizeof(metadata.rom_region));
239 meta_file.write(reinterpret_cast<const char*>(&metadata.room_id),
240 sizeof(metadata.room_id));
241 meta_file.write(reinterpret_cast<const char*>(&metadata.game_module),
242 sizeof(metadata.game_module));
243
244 uint32_t desc_len = metadata.description.size();
245 meta_file.write(reinterpret_cast<const char*>(&desc_len), sizeof(desc_len));
246 meta_file.write(metadata.description.data(), desc_len);
247
248 if (!meta_file) {
249 return absl::InternalError("Failed while writing metadata");
250 }
251 printf("[StateManager] Saved state to %s\n", path.c_str());
252 return absl::OkStatus();
253}
254
255absl::Status SaveStateManager::LoadStateFromFile(const std::string& path) {
256 auto status = snes_->loadState(path);
257 if (!status.ok()) {
258 return status;
259 }
260 printf("[StateManager] Loaded state from %s\n", path.c_str());
261 return absl::OkStatus();
262}
263
265 if (!rom_ || !rom_->is_loaded()) {
266 return 0;
267 }
268 return CalculateCRC32(rom_->data(), rom_->size());
269}
270
272 snes_->Reset(true);
273
274 // Run frames until we reach File Select (module 0x01)
275 // In ALTTP, Module 0x00 is Intro, which transitions to 0x14 (Attract)
276 // unless Start is pressed, which goes to 0x01 (File Select).
277 const int kMaxFrames = 2000;
278 for (int i = 0; i < kMaxFrames; ++i) {
279 snes_->RunFrame();
280 uint8_t module = GetGameModule();
281
282 if (i % 60 == 0) {
283 printf("[StateManager] Frame %d: module=0x%02X\n", i, module);
284 }
285
286 // Reached File Select
287 if (module == 0x01) {
288 printf("[StateManager] Reached File Select (module 0x01) at frame %d\n", i);
289 return absl::OkStatus();
290 }
291
292 // If we hit Attract Mode (0x14), press Start to go to File Select
293 if (module == 0x14) {
294 // Hold Start for 10 frames every 60 frames
295 if ((i % 60) < 10) {
296 if (i % 60 == 0) printf("[StateManager] In Attract Mode, holding Start...\n");
298 } else {
300 }
301 }
302 // Also try pressing Start during Intro (after some initial frames)
303 // Submodule 8 (FadeLogoIn) is when input is accepted
304 else if (module == 0x00 && i > 300) {
305 // Hold Start for 10 frames every 60 frames
306 if ((i % 60) < 10) {
307 if (i % 60 == 0) printf("[StateManager] In Intro, holding Start...\n");
309 } else {
311 }
312 }
313 }
314
315 printf("[StateManager] Boot timeout, module=0x%02X\n", GetGameModule());
316 return absl::DeadlineExceededError("Failed to reach File Select");
317}
318
320 // We should already be at File Select (0x01) from BootToTitleScreen
321 // But if not, press Start
322
323 const int kMaxFrames = 120;
324 for (int i = 0; i < kMaxFrames; ++i) {
325 uint8_t module = GetGameModule();
326 if (module == 0x01) {
327 printf("[StateManager] Navigated to file select (module 0x01)\n");
328 return absl::OkStatus();
329 }
330
331 // If in Intro or Attract, press Start
332 if (module == 0x00 || module == 0x14) {
334 }
335
336 snes_->RunFrame();
338 }
339
340 printf("[StateManager] File select timeout, module=0x%02X (continuing)\n",
341 GetGameModule());
342 return absl::OkStatus();
343}
344
346 printf("[StateManager] Starting new game sequence...\n");
347
348 // Phase 1: File Select (0x01) -> Name Entry (0x04)
349 // Try to select the first file and confirm
350 const int kFileSelectTimeout = 600;
351 for (int i = 0; i < kFileSelectTimeout; ++i) {
352 snes_->RunFrame();
353 uint8_t module = GetGameModule();
354
355 if (module == 0x04) {
356 printf("[StateManager] Reached Name Entry (module 0x04) at frame %d\n", i);
357 break;
358 }
359
360 // If we are in File Select (0x01), press A to select/confirm
361 if (module == 0x01) {
362 if (i % 60 < 10) { // Press A for 10 frames every 60 frames
364 } else {
365 snes_->SetButtonState(0, buttons::kA, false);
366 }
367 } else if (module == 0x03) { // Copy File (shouldn't happen but just in case)
368 // ...
369 }
370
371 if (i == kFileSelectTimeout - 1) {
372 printf("[StateManager] Timeout waiting for Name Entry (current: 0x%02X)\n", module);
373 // Don't fail yet, maybe we skipped it?
374 }
375 }
376
377 // Phase 2: Name Entry (0x04) -> Game Load (0x07/0x09)
378 // Accept default name (Start) and confirm (A)
379 const int kNameEntryTimeout = 2000;
380 for (int i = 0; i < kNameEntryTimeout; ++i) {
381 snes_->RunFrame();
382 uint8_t module = GetGameModule();
383
384 if (module == 0x07 || module == 0x09) {
385 printf("[StateManager] Started new game (module 0x%02X) at frame %d\n", module, i);
386 return absl::OkStatus();
387 }
388
389 // If we are in Name Entry (0x04), press Start then A
390 if (module == 0x04) {
391 // If we've been in Name Entry for a while (e.g. > 600 frames), try to force transition
392 if (i > 600) {
393 printf("[StateManager] Stuck in Name Entry, forcing Module 0x05 (Load Level)...\n");
394 snes_->Write(0x7E0010, 0x05); // Force Load Level module
395
396 // Also set a safe room to load (Link's House = 0x0104)
397 // Or just let it use whatever is in 0xA0 (usually 0)
398 // But we want to exit Name Entry.
399 continue;
400 }
401
402 int cycle = i % 120; // Slower cycle
403 if (cycle < 20) {
404 // Press Start to accept name
406 snes_->SetButtonState(0, buttons::kA, false);
407 } else if (cycle >= 60 && cycle < 80) {
408 // Press A to confirm
411 } else {
413 snes_->SetButtonState(0, buttons::kA, false);
414 }
415 }
416 }
417
418 printf("[StateManager] Game start timeout, module=0x%02X\n", GetGameModule());
419 return absl::DeadlineExceededError("Game failed to start");
420}
421
422absl::Status SaveStateManager::NavigateToRoom(int room_id) {
423 // Try WRAM teleportation first (fast)
424 auto status = TeleportToRoomViaWram(room_id);
425 if (status.ok()) {
426 return absl::OkStatus();
427 }
428
429 // Fall back to TAS navigation if WRAM fails
430 printf("[StateManager] WRAM teleport failed: %s, trying TAS fallback\n",
431 std::string(status.message()).c_str());
432 return NavigateToRoomViaTas(room_id);
433}
434
436 // Set target room
437 snes_->Write(0x7E00A0, room_id & 0xFF);
438 snes_->Write(0x7E00A1, (room_id >> 8) & 0xFF);
439
440 // Set indoor flag for dungeon rooms (rooms < 0x128 are dungeons)
441 bool is_dungeon = (room_id < 0x128);
442 snes_->Write(0x7E001B, is_dungeon ? 0x01 : 0x00);
443
444 // Trigger room transition by setting loading module
445 // Module 0x06 = Underworld Load (0x05 is Load File, which resets state)
446 snes_->Write(0x7E0010, 0x06); // Loading module
447
448 // Set safe center position for Link
449 snes_->Write(0x7E0022, 0x80); // X low
450 snes_->Write(0x7E0023, 0x00); // X high
451 snes_->Write(0x7E0020, 0x80); // Y low
452 snes_->Write(0x7E0021, 0x00); // Y high
453
454 // Wait for room to fully load
455 const int kMaxFrames = 600;
456 for (int i = 0; i < kMaxFrames; ++i) {
457 snes_->RunFrame();
458
459 // Force write the room ID every frame to prevent it from being overwritten
460 snes_->Write(0x7E00A0, room_id & 0xFF);
461 snes_->Write(0x7E00A1, (room_id >> 8) & 0xFF);
462 snes_->Write(0x7E001B, (room_id < 0x128) ? 0x01 : 0x00);
463
464 if (i % 60 == 0) {
465 uint8_t submodule = ReadWram(0x7E0011);
466 printf("[StateManager] Teleport wait frame %d: module=0x%02X, sub=0x%02X, room=0x%04X\n",
467 i, GetGameModule(), submodule, GetCurrentRoom());
468 }
469
470 if (IsRoomFullyLoaded() && GetCurrentRoom() == room_id) {
471 printf("[StateManager] WRAM teleport to room 0x%04X successful\n",
472 room_id);
473 return absl::OkStatus();
474 }
475 }
476
477 return absl::DeadlineExceededError(
478 "WRAM teleport failed for room " + std::to_string(room_id));
479}
480
481absl::Status SaveStateManager::NavigateToRoomViaTas(int room_id) {
482 // TAS fallback: wait for whatever room loads naturally
483 // This is useful when WRAM injection doesn't work
484 const int kMaxFrames = 600; // 10 seconds
485 for (int i = 0; i < kMaxFrames; ++i) {
486 snes_->RunFrame();
487
488 if (IsRoomFullyLoaded()) {
489 int current_room = GetCurrentRoom();
490 printf("[StateManager] TAS: Room loaded: 0x%04X (target: 0x%04X)\n",
491 current_room, room_id);
492 if (current_room == room_id) {
493 return absl::OkStatus();
494 }
495 // Accept whatever room we're in for now
496 // Future: implement actual TAS navigation
497 return absl::OkStatus();
498 }
499 }
500
501 return absl::DeadlineExceededError("TAS navigation timeout");
502}
503
504void SaveStateManager::PressButton(int button, int frames) {
505 for (int i = 0; i < frames; ++i) {
506 snes_->SetButtonState(0, button, true);
507 snes_->RunFrame();
508 }
509 snes_->SetButtonState(0, button, false);
510}
511
513 // Release all buttons (bit indices 0-11)
517 snes_->SetButtonState(0, btn, false);
518 }
519}
520
522 for (int i = 0; i < frames; ++i) {
523 snes_->RunFrame();
524 }
525}
526
527uint8_t SaveStateManager::ReadWram(uint32_t addr) {
528 return snes_->Read(addr);
529}
530
531uint16_t SaveStateManager::ReadWram16(uint32_t addr) {
532 return snes_->Read(addr) | (snes_->Read(addr + 1) << 8);
533}
534
538
542
543bool SaveStateManager::WaitForModule(uint8_t target, int max_frames) {
544 for (int i = 0; i < max_frames; ++i) {
545 snes_->RunFrame();
546 if (GetGameModule() == target) {
547 return true;
548 }
549 }
550 return false;
551}
552
554 // Check if game is in dungeon module (0x07) or overworld module (0x09)
555 // Also verify submodule is 0x00 (fully loaded, not transitioning)
556 uint8_t module = GetGameModule();
557 uint8_t submodule = ReadWram(0x7E0011);
558 // Submodule 0x00 is normal gameplay
559 // Submodule 0x0F is often "Spotlight Open" or similar stable state in dungeons
560 return (module == 0x07 || module == 0x09) && (submodule == 0x00 || submodule == 0x0F);
561}
562
564 int context_id) const {
565 std::string type_str;
566 switch (type) {
568 type_str = "room";
569 break;
571 type_str = "overworld";
572 break;
574 type_str = "blank";
575 break;
576 }
577 return state_directory_ + "/" + type_str + "_" + std::to_string(context_id) +
578 ".state";
579}
580
582 int context_id) const {
583 return GetStatePath(type, context_id) + ".meta";
584}
585
587 return metadata.rom_checksum == rom_checksum_;
588}
589
590} // namespace render
591} // namespace emu
592} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
auto data() const
Definition rom.h:135
auto size() const
Definition rom.h:134
bool is_loaded() const
Definition rom.h:128
absl::Status saveState(const std::string &path)
Definition snes.cc:922
void SetButtonState(int player, int button, bool pressed)
Definition snes.cc:855
uint8_t Read(uint32_t adr)
Definition snes.cc:620
void Reset(bool hard=false)
Definition snes.cc:181
void RunFrame()
Definition snes.cc:229
void Write(uint32_t adr, uint8_t val)
Definition snes.cc:770
absl::Status loadState(const std::string &path)
Definition snes.cc:998
absl::Status GenerateRoomState(int room_id)
bool HasCachedState(StateType type, int context_id=0) const
bool IsStateCompatible(const StateMetadata &metadata) const
SaveStateManager(emu::Snes *snes, Rom *rom)
std::string GetStatePath(StateType type, int context_id) const
absl::Status LoadState(StateType type, int context_id=0)
void PressButton(int button, int frames=1)
absl::Status LoadStateFromFile(const std::string &path)
absl::StatusOr< StateMetadata > GetStateMetadata(StateType type, int context_id=0) const
bool WaitForModule(uint8_t target, int max_frames)
absl::Status TeleportToRoomViaWram(int room_id)
absl::Status NavigateToRoomViaTas(int room_id)
std::string GetMetadataPath(StateType type, int context_id) const
absl::Status SaveStateToFile(const std::string &path, const StateMetadata &metadata)
absl::Status NavigateToRoom(int room_id)
std::string GetParentPath(const std::string &path)
void CreateDirectories(const std::string &path)
uint32_t CalculateCRC32(const uint8_t *data, size_t size)