yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
rom.cc
Go to the documentation of this file.
1#include "rom.h"
2
3#include <algorithm>
4#include <chrono>
5#include <cstddef>
6#include <cstdint>
7#include <cstring>
8#include <ctime>
9#include <filesystem>
10#include <fstream>
11#include <iostream>
12#include <new>
13#include <string>
14#include <vector>
15
16#include "absl/status/status.h"
17#include "absl/status/statusor.h"
18#include "absl/strings/str_cat.h"
19#include "absl/strings/str_format.h"
20#include "absl/strings/string_view.h"
23#include "rom/write_fence.h"
24#include "util/hex.h"
25#include "util/log.h"
26#include "util/macro.h"
27
28#ifdef __EMSCRIPTEN__
29#include <emscripten.h>
31#endif
32
33#if !defined(__EMSCRIPTEN__)
34#if defined(_WIN32)
35#include <windows.h>
36#else
37#include <fcntl.h>
38#include <unistd.h>
39#endif
40#endif
41
42namespace yaze {
43
44namespace {
45
46// ============================================================================
47// ROM Structure Constants
48// ============================================================================
49
51constexpr size_t kBaseRomSize = 1048576;
52
54constexpr size_t kHeaderSize = 0x200; // 512 bytes
55
56// ============================================================================
57// SMC Header Detection and Removal
58// ============================================================================
59
60void MaybeStripSmcHeader(std::vector<uint8_t>& rom_data, unsigned long& size) {
61 if (size % kBaseRomSize == kHeaderSize && size >= kHeaderSize &&
62 rom_data.size() >= kHeaderSize) {
63 rom_data.erase(rom_data.begin(), rom_data.begin() + kHeaderSize);
64 size -= kHeaderSize;
65 LOG_INFO("Rom", "Stripped SMC header from ROM (new size: %lu)", size);
66 }
67}
68
69std::string MakeSafeTimestamp(std::time_t now_c) {
70 std::string timestamp = std::ctime(&now_c);
71 timestamp.erase(std::remove(timestamp.begin(), timestamp.end(), '\n'),
72 timestamp.end());
73 std::replace(timestamp.begin(), timestamp.end(), ' ', '_');
74
75 // Keep backup/save-new filenames valid across platforms (especially
76 // Windows, where ':' and several other characters are not allowed).
77 for (char& ch : timestamp) {
78 switch (ch) {
79 case '<':
80 case '>':
81 case ':':
82 case '"':
83 case '/':
84 case '\\':
85 case '|':
86 case '?':
87 case '*':
88 ch = '-';
89 break;
90 default:
91 break;
92 }
93 }
94 return timestamp;
95}
96
97#ifdef __EMSCRIPTEN__
98inline void MaybeBroadcastChange(uint32_t offset,
99 const std::vector<uint8_t>& old_bytes,
100 const std::vector<uint8_t>& new_bytes) {
101 if (new_bytes.empty())
102 return;
103 auto& collab = app::platform::GetWasmCollaborationInstance();
104 if (!collab.IsConnected() || collab.IsApplyingRemoteChange()) {
105 return;
106 }
107 (void)collab.BroadcastChange(offset, old_bytes, new_bytes);
108}
109#endif
110
111#if !defined(__EMSCRIPTEN__)
112void BestEffortFsyncFile(const std::filesystem::path& path) {
113#if defined(_WIN32)
114 // FlushFileBuffers requires GENERIC_WRITE access.
115 HANDLE handle =
116 CreateFileW(path.wstring().c_str(), GENERIC_WRITE,
117 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
118 nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
119 if (handle == INVALID_HANDLE_VALUE) {
120 return;
121 }
122 (void)FlushFileBuffers(handle);
123 (void)CloseHandle(handle);
124#else
125 int fd = open(path.c_str(), O_RDONLY);
126 if (fd < 0) {
127 return;
128 }
129 (void)fsync(fd);
130 (void)close(fd);
131#endif
132}
133
134void BestEffortFsyncParentDir(const std::filesystem::path& file_path) {
135#if defined(_WIN32)
136 (void)file_path;
137 // Best-effort only; Windows directory fsync is not portable here.
138#else
139 std::filesystem::path dir_path = file_path.parent_path();
140 if (dir_path.empty()) {
141 dir_path = ".";
142 }
143 int fd = open(dir_path.c_str(), O_RDONLY);
144 if (fd < 0) {
145 return;
146 }
147 (void)fsync(fd);
148 (void)close(fd);
149#endif
150}
151#endif // !defined(__EMSCRIPTEN__)
152
153} // namespace
154
155absl::Status Rom::LoadFromFile(const std::string& filename,
156 const LoadOptions& options) {
157 if (filename.empty()) {
158 return absl::InvalidArgumentError(
159 "Could not load ROM: parameter `filename` is empty.");
160 }
161
162#ifdef __EMSCRIPTEN__
164 std::ifstream test_file(filename_, std::ios::binary);
165 if (!test_file.is_open()) {
166 return absl::NotFoundError(absl::StrCat(
167 "ROM file does not exist or cannot be opened: ", filename_));
168 }
169 test_file.seekg(0, std::ios::end);
170 size_ = test_file.tellg();
171 test_file.close();
172
173 if (size_ < 32768) {
174 return absl::InvalidArgumentError(absl::StrFormat(
175 "ROM file too small (%zu bytes), minimum is 32KB", size_));
176 }
177#else
178 if (!std::filesystem::exists(filename)) {
179 return absl::NotFoundError(
180 absl::StrCat("ROM file does not exist: ", filename));
181 }
182 filename_ = std::filesystem::absolute(filename).string();
183#endif
184 short_name_ = filename_.substr(filename_.find_last_of("/\\") + 1);
185
186 std::ifstream file(filename_, std::ios::binary);
187 if (!file.is_open()) {
188 return absl::NotFoundError(
189 absl::StrCat("Could not open ROM file: ", filename_));
190 }
191
192#ifndef __EMSCRIPTEN__
193 try {
194 size_ = std::filesystem::file_size(filename_);
195 if (size_ < 32768) {
196 return absl::InvalidArgumentError(absl::StrFormat(
197 "ROM file too small (%zu bytes), minimum is 32KB", size_));
198 }
199 } catch (...) {
200 file.seekg(0, std::ios::end);
201 size_ = file.tellg();
202 }
203#endif
204
205 try {
206 rom_data_.resize(size_);
207 file.seekg(0, std::ios::beg);
208 file.read(reinterpret_cast<char*>(rom_data_.data()), size_);
209 } catch (const std::bad_alloc& e) {
210 return absl::ResourceExhaustedError(absl::StrFormat(
211 "Failed to allocate memory for ROM (%zu bytes)", size_));
212 }
213
214 file.close();
215
216 if (options.strip_header) {
217 MaybeStripSmcHeader(rom_data_, size_);
218 }
219 size_ = rom_data_.size();
220
221 if (options.load_resource_labels) {
222 resource_label_manager_.LoadLabels(absl::StrFormat("%s.labels", filename));
223 }
224
225 // Parse SNES Header for Title
226 if (rom_data_.size() >= 0x8000) {
227 // Check LoROM (0x7FC0) vs HiROM (0xFFC0)
228 // Simple heuristic: Z3 is LoROM
229 size_t header_offset = 0x7FC0;
230 if (rom_data_.size() >= 0x10000) {
231 // Compute checksums to verify?
232 // For now default to LoROM
233 }
234
235 if (header_offset + 21 <= rom_data_.size()) {
236 char buffer[22] = {0};
237 for (int i = 0; i < 21; ++i) {
239 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
240 }
241 title_ = std::string(buffer);
242 // Trim trailing spaces safely
243 auto last_non_space = title_.find_last_not_of(' ');
244 if (last_non_space == std::string::npos) {
245 title_.clear();
246 } else {
247 title_.erase(last_non_space + 1);
248 }
249 }
250 }
251
252 return absl::OkStatus();
253}
254
255absl::Status Rom::LoadFromData(const std::vector<uint8_t>& data,
256 const LoadOptions& options) {
257 if (data.empty()) {
258 return absl::InvalidArgumentError(
259 "Could not load ROM: parameter `data` is empty.");
260 }
261 rom_data_ = data;
262 size_ = data.size();
263
264 if (options.strip_header) {
265 MaybeStripSmcHeader(rom_data_, size_);
266 }
267 size_ = rom_data_.size();
268
269 // Parse SNES Header for Title
270 if (rom_data_.size() >= 0x8000) {
271 size_t header_offset = 0x7FC0;
272 if (header_offset + 21 <= rom_data_.size()) {
273 char buffer[22] = {0};
274 for (int i = 0; i < 21; ++i) {
276 buffer[i] = (c >= 32 && c <= 126) ? c : ' ';
277 }
278 title_ = std::string(buffer);
279 auto last_non_space = title_.find_last_not_of(' ');
280 if (last_non_space == std::string::npos) {
281 title_.clear();
282 } else {
283 title_.erase(last_non_space + 1);
284 }
285 }
286 }
287
288 return absl::OkStatus();
289}
290
291absl::Status Rom::SaveToFile(const SaveSettings& settings) {
292 if (rom_data_.empty()) {
293 return absl::InternalError("ROM data is empty.");
294 }
295
296 std::string filename = settings.filename;
297 if (filename.empty()) {
299 }
300
301 if (settings.backup) {
302 auto now = std::chrono::system_clock::now();
303 auto now_c = std::chrono::system_clock::to_time_t(now);
304 std::string backup_filename =
305 absl::StrCat(filename, "_backup_", MakeSafeTimestamp(now_c));
306
307 try {
308 std::filesystem::copy(filename_, backup_filename,
309 std::filesystem::copy_options::overwrite_existing);
310 } catch (const std::filesystem::filesystem_error& e) {
311 LOG_WARN("Rom", "Could not create backup: %s", e.what());
312 }
313 }
314
315 if (settings.save_new) {
316 auto now = std::chrono::system_clock::now();
317 auto now_c = std::chrono::system_clock::to_time_t(now);
318 auto filename_no_ext = filename.substr(0, filename.find_last_of("."));
319 filename =
320 absl::StrCat(filename_no_ext, "_", MakeSafeTimestamp(now_c), ".sfc");
321 }
322
323 // Save stability: write to a temp file in the same directory and rename into
324 // place. If we crash mid-write, the original ROM stays intact.
325 const std::filesystem::path target_path(filename);
326 std::filesystem::path temp_path = target_path;
327 temp_path += ".tmp";
328
329 std::ofstream file(temp_path, std::ios::binary | std::ios::trunc);
330 if (!file) {
331 return absl::InternalError(absl::StrCat(
332 "Could not open temp ROM file for writing: ", temp_path.string()));
333 }
334
335 file.write(reinterpret_cast<const char*>(rom_data_.data()), rom_data_.size());
336 file.flush();
337 if (!file) {
338 file.close();
339 std::error_code rm_ec;
340 std::filesystem::remove(temp_path, rm_ec);
341 return absl::InternalError(
342 absl::StrCat("Error while writing ROM file: ", temp_path.string()));
343 }
344
345 file.close();
346
347#if !defined(__EMSCRIPTEN__)
348 // Best-effort fsync so temp file contents are durable before rename.
349 BestEffortFsyncFile(temp_path);
350#endif
351
352 std::error_code rename_ec;
353 std::filesystem::rename(temp_path, target_path, rename_ec);
354#if defined(_WIN32)
355 // Windows may fail rename when the destination exists; fall back to remove +
356 // rename (non-atomic but still avoids partial/truncated writes).
357 if (rename_ec && std::filesystem::exists(target_path)) {
358 std::error_code rm_target_ec;
359 std::filesystem::remove(target_path, rm_target_ec);
360 rename_ec.clear();
361 std::filesystem::rename(temp_path, target_path, rename_ec);
362 }
363#endif
364 if (rename_ec) {
365 std::error_code rm_ec;
366 std::filesystem::remove(temp_path, rm_ec);
367 return absl::InternalError(absl::StrCat(
368 "Failed to move temp ROM into place: ", rename_ec.message()));
369 }
370
371#if !defined(__EMSCRIPTEN__)
372 // Best-effort fsync the parent dir so the rename is durable.
373 BestEffortFsyncParentDir(target_path);
374#endif
375
376 dirty_ = false;
377 return absl::OkStatus();
378}
379
381 if (fence == nullptr) {
382 return;
383 }
384 write_fence_stack_.push_back(fence);
385}
386
388 if (fence == nullptr) {
389 return;
390 }
391 if (!write_fence_stack_.empty() && write_fence_stack_.back() == fence) {
392 write_fence_stack_.pop_back();
393 return;
394 }
395
396 // Defensive: avoid leaving a stale fence active if call sites mismatch.
397 for (auto it = write_fence_stack_.rbegin(); it != write_fence_stack_.rend();
398 ++it) {
399 if (*it == fence) {
400 write_fence_stack_.erase(std::next(it).base());
401 LOG_WARN("Rom", "Popped non-top write fence (mismatched scope)");
402 return;
403 }
404 }
405 LOG_WARN("Rom", "PopWriteFence called for unknown fence");
406}
407
408absl::StatusOr<uint8_t> Rom::ReadByte(int offset) const {
409 if (offset < 0 || offset >= static_cast<int>(rom_data_.size())) {
410 return absl::OutOfRangeError(absl::StrFormat(
411 "Offset %d out of range (size: %d)", offset, rom_data_.size()));
412 }
413 return rom_data_[offset];
414}
415
416absl::StatusOr<uint16_t> Rom::ReadWord(int offset) const {
417 if (offset < 0 || offset + 1 >= static_cast<int>(rom_data_.size())) {
418 return absl::OutOfRangeError("Offset out of range");
419 }
420 return (uint16_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8));
421}
422
423absl::StatusOr<uint32_t> Rom::ReadLong(int offset) const {
424 if (offset < 0 || offset + 2 >= static_cast<int>(rom_data_.size())) {
425 return absl::OutOfRangeError("Offset out of range");
426 }
427 return (uint32_t)(rom_data_[offset] | (rom_data_[offset + 1] << 8) |
428 (rom_data_[offset + 2] << 16));
429}
430
431absl::StatusOr<std::vector<uint8_t>> Rom::ReadByteVector(
432 uint32_t offset, uint32_t length) const {
433 if (offset + length > static_cast<uint32_t>(rom_data_.size())) {
434 return absl::OutOfRangeError("Offset and length out of range");
435 }
436 std::vector<uint8_t> result;
437 result.reserve(length);
438 for (uint32_t i = offset; i < offset + length; i++) {
439 result.push_back(rom_data_[i]);
440 }
441 return result;
442}
443
444absl::StatusOr<gfx::Tile16> Rom::ReadTile16(uint32_t tile16_id,
445 uint32_t tile16_ptr) {
446 // Skip 8 bytes per tile.
447 auto tpos = tile16_ptr + (tile16_id * 0x08);
448 gfx::Tile16 tile16 = {};
451 tpos += 2;
454 tpos += 2;
457 tpos += 2;
460 return tile16;
461}
462
463absl::Status Rom::WriteTile16(int tile16_id, uint32_t tile16_ptr,
464 const gfx::Tile16& tile) {
465 auto tpos = tile16_ptr + (tile16_id * 0x08);
467 tpos += 2;
469 tpos += 2;
471 tpos += 2;
473 return absl::OkStatus();
474}
475
476absl::Status Rom::WriteByte(int addr, uint8_t value) {
477 if (addr < 0 || addr >= static_cast<int>(rom_data_.size())) {
478 return absl::OutOfRangeError("Address out of range");
479 }
480 for (auto* fence : write_fence_stack_) {
481 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 1, "WriteByte"));
482 }
483 const uint8_t old_val = rom_data_[addr];
484 rom_data_[addr] = value;
485 dirty_ = true;
486#ifdef __EMSCRIPTEN__
487 MaybeBroadcastChange(addr, {old_val}, {value});
488#endif
489 for (auto* fence : write_fence_stack_) {
490 fence->RecordWrite(static_cast<uint32_t>(addr), 1);
491 }
492 return absl::OkStatus();
493}
494
495absl::Status Rom::WriteWord(int addr, uint16_t value) {
496 if (addr < 0 || addr + 1 >= static_cast<int>(rom_data_.size())) {
497 return absl::OutOfRangeError("Address out of range");
498 }
499 for (auto* fence : write_fence_stack_) {
500 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr), 2, "WriteWord"));
501 }
502 const uint8_t old0 = rom_data_[addr];
503 const uint8_t old1 = rom_data_[addr + 1];
504 rom_data_[addr] = (uint8_t)(value & 0xFF);
505 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
506 dirty_ = true;
507#ifdef __EMSCRIPTEN__
509 {static_cast<uint8_t>(value & 0xFF),
510 static_cast<uint8_t>((value >> 8) & 0xFF)});
511#endif
512 for (auto* fence : write_fence_stack_) {
513 fence->RecordWrite(static_cast<uint32_t>(addr), 2);
514 }
515 return absl::OkStatus();
516}
517
518absl::Status Rom::WriteShort(int addr, uint16_t value) {
519 return WriteWord(addr, value);
520}
521
522absl::Status Rom::WriteLong(uint32_t addr, uint32_t value) {
523 if (addr + 2 >= static_cast<uint32_t>(rom_data_.size())) {
524 return absl::OutOfRangeError("Address out of range");
525 }
526 for (auto* fence : write_fence_stack_) {
527 RETURN_IF_ERROR(fence->Check(addr, 3, "WriteLong"));
528 }
529 const uint8_t old0 = rom_data_[addr];
530 const uint8_t old1 = rom_data_[addr + 1];
531 const uint8_t old2 = rom_data_[addr + 2];
532 rom_data_[addr] = (uint8_t)(value & 0xFF);
533 rom_data_[addr + 1] = (uint8_t)((value >> 8) & 0xFF);
534 rom_data_[addr + 2] = (uint8_t)((value >> 16) & 0xFF);
535 dirty_ = true;
536#ifdef __EMSCRIPTEN__
538 {static_cast<uint8_t>(value & 0xFF),
539 static_cast<uint8_t>((value >> 8) & 0xFF),
540 static_cast<uint8_t>((value >> 16) & 0xFF)});
541#endif
542 for (auto* fence : write_fence_stack_) {
543 fence->RecordWrite(addr, 3);
544 }
545 return absl::OkStatus();
546}
547
548absl::Status Rom::WriteVector(int addr, std::vector<uint8_t> data) {
549 if (addr < 0) {
550 return absl::OutOfRangeError("Address out of range");
551 }
552 if (addr + static_cast<int>(data.size()) >
553 static_cast<int>(rom_data_.size())) {
554 return absl::OutOfRangeError("Address out of range");
555 }
556 for (auto* fence : write_fence_stack_) {
557 RETURN_IF_ERROR(fence->Check(static_cast<uint32_t>(addr),
558 static_cast<uint32_t>(data.size()),
559 "WriteVector"));
560 }
561 std::vector<uint8_t> old_data;
562 old_data.reserve(data.size());
563 for (int i = 0; i < static_cast<int>(data.size()); i++) {
564 old_data.push_back(rom_data_[addr + i]);
565 rom_data_[addr + i] = data[i];
566 }
567 dirty_ = true;
568#ifdef __EMSCRIPTEN__
569 MaybeBroadcastChange(addr, old_data, data);
570#endif
571 for (auto* fence : write_fence_stack_) {
572 fence->RecordWrite(static_cast<uint32_t>(addr),
573 static_cast<uint32_t>(data.size()));
574 }
575 return absl::OkStatus();
576}
577
578absl::Status Rom::WriteColor(uint32_t address, const gfx::SnesColor& color) {
579 uint16_t bgr = ((color.snes() >> 10) & 0x1F) | ((color.snes() & 0x1F) << 10) |
580 (color.snes() & 0x7C00);
581 return WriteWord(address, bgr);
582}
583
584absl::Status Rom::WriteHelper(const WriteAction& action) {
585 if (std::holds_alternative<uint8_t>(action.value)) {
586 return WriteByte(action.address, std::get<uint8_t>(action.value));
587 } else if (std::holds_alternative<uint16_t>(action.value) ||
588 std::holds_alternative<short>(action.value)) {
589 return WriteShort(action.address, std::get<uint16_t>(action.value));
590 } else if (std::holds_alternative<std::vector<uint8_t>>(action.value)) {
591 return WriteVector(action.address,
592 std::get<std::vector<uint8_t>>(action.value));
593 } else if (std::holds_alternative<gfx::SnesColor>(action.value)) {
594 return WriteColor(action.address, std::get<gfx::SnesColor>(action.value));
595 }
596 return absl::InvalidArgumentError("Invalid write argument type");
597}
598
599} // namespace yaze
absl::StatusOr< std::vector< uint8_t > > ReadByteVector(uint32_t offset, uint32_t length) const
Definition rom.cc:431
void PushWriteFence(rom::WriteFence *fence)
Definition rom.cc:380
absl::Status LoadFromFile(const std::string &filename, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:155
absl::StatusOr< gfx::Tile16 > ReadTile16(uint32_t tile16_id, uint32_t tile16_ptr)
Definition rom.cc:444
absl::Status WriteColor(uint32_t address, const gfx::SnesColor &color)
Definition rom.cc:578
auto filename() const
Definition rom.h:145
void PopWriteFence(rom::WriteFence *fence)
Definition rom.cc:387
absl::Status WriteByte(int addr, uint8_t value)
Definition rom.cc:476
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:408
absl::Status WriteTile16(int tile16_id, uint32_t tile16_ptr, const gfx::Tile16 &tile)
Definition rom.cc:463
const auto & vector() const
Definition rom.h:143
absl::Status WriteVector(int addr, std::vector< uint8_t > data)
Definition rom.cc:548
std::string title_
Definition rom.h:168
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:291
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:416
std::vector< uint8_t > rom_data_
Definition rom.h:177
auto data() const
Definition rom.h:139
std::vector< rom::WriteFence * > write_fence_stack_
Definition rom.h:186
bool dirty_
Definition rom.h:183
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:255
std::string filename_
Definition rom.h:171
unsigned long size_
Definition rom.h:165
absl::Status WriteShort(int addr, uint16_t value)
Definition rom.cc:518
project::ResourceLabelManager resource_label_manager_
Definition rom.h:180
std::string short_name_
Definition rom.h:174
absl::Status WriteWord(int addr, uint16_t value)
Definition rom.cc:495
virtual absl::Status WriteHelper(const WriteAction &action)
Definition rom.cc:584
absl::Status WriteLong(uint32_t addr, uint32_t value)
Definition rom.cc:522
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:423
SNES Color container.
Definition snes_color.h:110
constexpr uint16_t snes() const
Get SNES 15-bit color.
Definition snes_color.h:193
Tile composition of four 8x8 tiles.
Definition snes_tile.h:142
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
std::string MakeSafeTimestamp(std::time_t now_c)
Definition rom.cc:69
void BestEffortFsyncFile(const std::filesystem::path &path)
Definition rom.cc:112
void BestEffortFsyncParentDir(const std::filesystem::path &file_path)
Definition rom.cc:134
void MaybeStripSmcHeader(std::vector< uint8_t > &rom_data, unsigned long &size)
Definition rom.cc:60
constexpr size_t kHeaderSize
Size of the optional SMC/SFC copier header that some ROM dumps include.
Definition rom.cc:54
constexpr size_t kBaseRomSize
Standard SNES ROM size for The Legend of Zelda: A Link to the Past (1MB)
Definition rom.cc:51
uint16_t TileInfoToWord(TileInfo tile_info)
Definition snes_tile.cc:361
TileInfo WordToTileInfo(uint16_t word)
Definition snes_tile.cc:378
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:38
std::string filename
Definition rom.h:33
ValueType value
Definition rom.h:103
bool LoadLabels(const std::string &filename)
Definition project.cc:1851