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