yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
tile16_proposal_generator.cc
Go to the documentation of this file.
2
3#include <fstream>
4#include <sstream>
5
6#include "absl/strings/match.h"
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_split.h"
9#include "cli/util/hex_util.h"
10#include "nlohmann/json.hpp"
11#include "util/macro.h"
13
14namespace yaze {
15namespace cli {
16
18
19std::string Tile16Change::ToString() const {
20 std::ostringstream oss;
21 oss << "Map " << map_id << " @ (" << x << "," << y << "): "
22 << "0x" << std::hex << old_tile << " → 0x" << new_tile;
23 return oss.str();
24}
25
26std::string Tile16Proposal::ToJson() const {
27 std::ostringstream json;
28 json << "{\n";
29 json << " \"id\": \"" << id << "\",\n";
30 json << " \"prompt\": \"" << prompt << "\",\n";
31 json << " \"ai_service\": \"" << ai_service << "\",\n";
32 json << " \"reasoning\": \"" << reasoning << "\",\n";
33 json << " \"status\": ";
34
35 switch (status) {
36 case Status::PENDING:
37 json << "\"pending\"";
38 break;
40 json << "\"accepted\"";
41 break;
43 json << "\"rejected\"";
44 break;
45 case Status::APPLIED:
46 json << "\"applied\"";
47 break;
48 }
49 json << ",\n";
50
51 json << " \"changes\": [\n";
52 for (size_t i = 0; i < changes.size(); ++i) {
53 const auto& change = changes[i];
54 json << " {\n";
55 json << " \"map_id\": " << change.map_id << ",\n";
56 json << " \"x\": " << change.x << ",\n";
57 json << " \"y\": " << change.y << ",\n";
58 json << " \"old_tile\": \"0x" << std::hex << change.old_tile
59 << "\",\n";
60 json << " \"new_tile\": \"0x" << std::hex << change.new_tile << "\"\n";
61 json << " }";
62 if (i < changes.size() - 1)
63 json << ",";
64 json << "\n";
65 }
66 json << " ]\n";
67 json << "}\n";
68
69 return json.str();
70}
71
72namespace {
73
74absl::StatusOr<uint16_t> ParseTileValue(const nlohmann::json& json,
75 const char* field) {
76 if (!json.contains(field)) {
77 return absl::InvalidArgumentError(
78 absl::StrCat("Missing field '", field, "' in proposal change"));
79 }
80
81 if (json[field].is_number_integer()) {
82 int value = json[field].get<int>();
83 if (value < 0 || value > 0xFFFF) {
84 return absl::InvalidArgumentError(
85 absl::StrCat("Tile value for '", field, "' out of range: ", value));
86 }
87 return static_cast<uint16_t>(value);
88 }
89
90 if (json[field].is_string()) {
91 std::string value = json[field].get<std::string>();
92 if (value.empty()) {
93 return absl::InvalidArgumentError(
94 absl::StrCat("Tile value for '", field, "' is empty"));
95 }
96
97 // Support hex strings in 0xFFFF format or plain decimal strings
98 if (absl::StartsWith(value, "0x") || absl::StartsWith(value, "0X")) {
99 if (value.size() <= 2) {
100 return absl::InvalidArgumentError(
101 absl::StrCat("Invalid hex tile value for '", field,
102 "': ", json[field].get<std::string>()));
103 }
104 value = value.substr(2);
105 unsigned int parsed = 0;
106 if (!ParseHexString(value, &parsed) || parsed > 0xFFFF) {
107 return absl::InvalidArgumentError(
108 absl::StrCat("Invalid hex tile value for '", field,
109 "': ", json[field].get<std::string>()));
110 }
111 return static_cast<uint16_t>(parsed);
112 }
113
114 unsigned int parsed = 0;
115 if (!absl::SimpleAtoi(value, &parsed) || parsed > 0xFFFF) {
116 return absl::InvalidArgumentError(
117 absl::StrCat("Invalid tile value for '", field,
118 "': ", json[field].get<std::string>()));
119 }
120 return static_cast<uint16_t>(parsed);
121 }
122
123 return absl::InvalidArgumentError(
124 absl::StrCat("Unsupported JSON type for tile field '", field, "'"));
125}
126
127Tile16Proposal::Status ParseStatus(absl::string_view status_text) {
128 if (absl::StartsWith(status_text, "accept")) {
130 }
131 if (absl::StartsWith(status_text, "reject")) {
133 }
134 if (absl::StartsWith(status_text, "apply")) {
136 }
138}
139
140} // namespace
141
142absl::StatusOr<Tile16Proposal> Tile16Proposal::FromJson(
143 const std::string& json_text) {
144 nlohmann::json json;
145 try {
146 json = nlohmann::json::parse(json_text);
147 } catch (const nlohmann::json::parse_error& error) {
148 return absl::InvalidArgumentError(
149 absl::StrCat("Failed to parse proposal JSON: ", error.what()));
150 }
151
152 Tile16Proposal proposal;
153
154 if (!json.contains("id") || !json["id"].is_string()) {
155 return absl::InvalidArgumentError(
156 "Proposal JSON must include string field 'id'");
157 }
158 proposal.id = json["id"].get<std::string>();
159
160 if (!json.contains("prompt") || !json["prompt"].is_string()) {
161 return absl::InvalidArgumentError(
162 "Proposal JSON must include string field 'prompt'");
163 }
164 proposal.prompt = json["prompt"].get<std::string>();
165
166 if (json.contains("ai_service") && json["ai_service"].is_string()) {
167 proposal.ai_service = json["ai_service"].get<std::string>();
168 }
169
170 if (json.contains("reasoning") && json["reasoning"].is_string()) {
171 proposal.reasoning = json["reasoning"].get<std::string>();
172 }
173
174 if (json.contains("status")) {
175 if (!json["status"].is_string()) {
176 return absl::InvalidArgumentError(
177 "Proposal 'status' must be a string value");
178 }
179 proposal.status = ParseStatus(json["status"].get<std::string>());
180 } else {
181 proposal.status = Status::PENDING;
182 }
183
184 if (json.contains("changes")) {
185 if (!json["changes"].is_array()) {
186 return absl::InvalidArgumentError(
187 "Proposal 'changes' field must be an array");
188 }
189
190 for (const auto& change_json : json["changes"]) {
191 if (!change_json.is_object()) {
192 return absl::InvalidArgumentError(
193 "Each change entry must be a JSON object");
194 }
195
196 Tile16Change change;
197 if (!change_json.contains("map_id") ||
198 !change_json["map_id"].is_number_integer()) {
199 return absl::InvalidArgumentError(
200 "Tile change missing integer field 'map_id'");
201 }
202 change.map_id = change_json["map_id"].get<int>();
203
204 if (!change_json.contains("x") || !change_json["x"].is_number_integer()) {
205 return absl::InvalidArgumentError(
206 "Tile change missing integer field 'x'");
207 }
208 change.x = change_json["x"].get<int>();
209
210 if (!change_json.contains("y") || !change_json["y"].is_number_integer()) {
211 return absl::InvalidArgumentError(
212 "Tile change missing integer field 'y'");
213 }
214 change.y = change_json["y"].get<int>();
215
217 ParseTileValue(change_json, "old_tile"));
219 ParseTileValue(change_json, "new_tile"));
220
221 proposal.changes.push_back(change);
222 }
223 }
224
225 if (proposal.changes.empty()) {
226 return absl::InvalidArgumentError(
227 "Proposal JSON did not include any tile16 changes");
228 }
229
230 proposal.created_at = std::chrono::system_clock::now();
231 if (json.contains("created_at_ms") && json["created_at_ms"].is_number()) {
232 auto millis = json["created_at_ms"].get<int64_t>();
233 proposal.created_at = std::chrono::system_clock::time_point(
234 std::chrono::milliseconds(millis));
235 }
236
237 return proposal;
238}
239
241 // Generate a simple timestamp-based ID
242 auto now = std::chrono::system_clock::now();
243 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
244 now.time_since_epoch())
245 .count();
246
247 std::ostringstream oss;
248 oss << "proposal_" << ms;
249 return oss.str();
250}
251
253 const std::string& command, Rom* rom) {
254 // Expected format: "overworld set-tile --map 0 --x 10 --y 20 --tile 0x02E"
255 std::vector<std::string> parts = absl::StrSplit(command, ' ');
256
257 if (parts.size() < 10) {
258 return absl::InvalidArgumentError(
259 absl::StrCat("Invalid command format: ", command));
260 }
261
262 if (parts[0] != "overworld" || parts[1] != "set-tile") {
263 return absl::InvalidArgumentError(
264 absl::StrCat("Not a set-tile command: ", command));
265 }
266
267 Tile16Change change;
268
269 // Parse arguments
270 for (size_t i = 2; i < parts.size(); i += 2) {
271 if (i + 1 >= parts.size())
272 break;
273
274 const std::string& flag = parts[i];
275 const std::string& value = parts[i + 1];
276
277 if (flag == "--map") {
278 change.map_id = std::stoi(value);
279 } else if (flag == "--x") {
280 change.x = std::stoi(value);
281 } else if (flag == "--y") {
282 change.y = std::stoi(value);
283 } else if (flag == "--tile") {
284 // Parse as hex (both 0x prefix and plain hex)
285 change.new_tile = static_cast<uint16_t>(std::stoi(value, nullptr, 16));
286 }
287 }
288
289 // Load the ROM to get the old tile value
290 if (rom && rom->is_loaded()) {
291 zelda3::Overworld overworld(rom);
292 auto status = overworld.Load(rom);
293 if (!status.ok()) {
294 return status;
295 }
296
297 // Set the correct world based on map_id
298 if (change.map_id < 0x40) {
299 overworld.set_current_world(0); // Light World
300 } else if (change.map_id < 0x80) {
301 overworld.set_current_world(1); // Dark World
302 } else {
303 overworld.set_current_world(2); // Special World
304 }
305
306 change.old_tile = overworld.GetTile(change.x, change.y);
307 } else {
308 change.old_tile = 0x0000; // Unknown
309 }
310
311 return change;
312}
313
314absl::StatusOr<std::vector<Tile16Change>>
316 Rom* rom) {
317 // Expected format: "overworld set-area --map 0 --x 10 --y 20 --width 5
318 // --height 3 --tile 0x02E"
319 std::vector<std::string> parts = absl::StrSplit(command, ' ');
320
321 if (parts.size() < 12) {
322 return absl::InvalidArgumentError(
323 absl::StrCat("Invalid set-area command format: ", command));
324 }
325
326 if (parts[0] != "overworld" || parts[1] != "set-area") {
327 return absl::InvalidArgumentError(
328 absl::StrCat("Not a set-area command: ", command));
329 }
330
331 int map_id = 0, x = 0, y = 0, width = 1, height = 1;
332 uint16_t new_tile = 0;
333
334 // Parse arguments
335 for (size_t i = 2; i < parts.size(); i += 2) {
336 if (i + 1 >= parts.size())
337 break;
338
339 const std::string& flag = parts[i];
340 const std::string& value = parts[i + 1];
341
342 if (flag == "--map") {
343 map_id = std::stoi(value);
344 } else if (flag == "--x") {
345 x = std::stoi(value);
346 } else if (flag == "--y") {
347 y = std::stoi(value);
348 } else if (flag == "--width") {
349 width = std::stoi(value);
350 } else if (flag == "--height") {
351 height = std::stoi(value);
352 } else if (flag == "--tile") {
353 new_tile = static_cast<uint16_t>(std::stoi(value, nullptr, 16));
354 }
355 }
356
357 // Load the ROM to get the old tile values
358 std::vector<Tile16Change> changes;
359 if (rom && rom->is_loaded()) {
360 zelda3::Overworld overworld(rom);
361 auto status = overworld.Load(rom);
362 if (!status.ok()) {
363 return status;
364 }
365
366 // Set the correct world based on map_id
367 if (map_id < 0x40) {
368 overworld.set_current_world(0); // Light World
369 } else if (map_id < 0x80) {
370 overworld.set_current_world(1); // Dark World
371 } else {
372 overworld.set_current_world(2); // Special World
373 }
374
375 // Generate changes for each tile in the area
376 for (int dy = 0; dy < height; ++dy) {
377 for (int dx = 0; dx < width; ++dx) {
378 Tile16Change change;
379 change.map_id = map_id;
380 change.x = x + dx;
381 change.y = y + dy;
382 change.new_tile = new_tile;
383 change.old_tile = overworld.GetTile(change.x, change.y);
384 changes.push_back(change);
385 }
386 }
387 } else {
388 // If ROM not loaded, just create changes with unknown old values
389 for (int dy = 0; dy < height; ++dy) {
390 for (int dx = 0; dx < width; ++dx) {
391 Tile16Change change;
392 change.map_id = map_id;
393 change.x = x + dx;
394 change.y = y + dy;
395 change.new_tile = new_tile;
396 change.old_tile = 0x0000; // Unknown
397 changes.push_back(change);
398 }
399 }
400 }
401
402 return changes;
403}
404
405absl::StatusOr<std::vector<Tile16Change>>
407 Rom* rom) {
408 // Expected format: "overworld replace-tile --map 0 --old-tile 0x02E
409 // --new-tile 0x030" Optional bounds: --x-min 0 --y-min 0 --x-max 31 --y-max
410 // 31
411 std::vector<std::string> parts = absl::StrSplit(command, ' ');
412
413 if (parts.size() < 8) {
414 return absl::InvalidArgumentError(
415 absl::StrCat("Invalid replace-tile command format: ", command));
416 }
417
418 if (parts[0] != "overworld" || parts[1] != "replace-tile") {
419 return absl::InvalidArgumentError(
420 absl::StrCat("Not a replace-tile command: ", command));
421 }
422
423 int map_id = 0;
424 uint16_t old_tile = 0, new_tile = 0;
425 int x_min = 0, y_min = 0, x_max = 31, y_max = 31;
426
427 // Parse arguments
428 for (size_t i = 2; i < parts.size(); i += 2) {
429 if (i + 1 >= parts.size())
430 break;
431
432 const std::string& flag = parts[i];
433 const std::string& value = parts[i + 1];
434
435 if (flag == "--map") {
436 map_id = std::stoi(value);
437 } else if (flag == "--old-tile") {
438 old_tile = static_cast<uint16_t>(std::stoi(value, nullptr, 16));
439 } else if (flag == "--new-tile") {
440 new_tile = static_cast<uint16_t>(std::stoi(value, nullptr, 16));
441 } else if (flag == "--x-min") {
442 x_min = std::stoi(value);
443 } else if (flag == "--y-min") {
444 y_min = std::stoi(value);
445 } else if (flag == "--x-max") {
446 x_max = std::stoi(value);
447 } else if (flag == "--y-max") {
448 y_max = std::stoi(value);
449 }
450 }
451
452 if (!rom || !rom->is_loaded()) {
453 return absl::FailedPreconditionError(
454 "ROM must be loaded to scan for tiles to replace");
455 }
456
457 zelda3::Overworld overworld(rom);
458 auto status = overworld.Load(rom);
459 if (!status.ok()) {
460 return status;
461 }
462
463 // Set the correct world based on map_id
464 if (map_id < 0x40) {
465 overworld.set_current_world(0); // Light World
466 } else if (map_id < 0x80) {
467 overworld.set_current_world(1); // Dark World
468 } else {
469 overworld.set_current_world(2); // Special World
470 }
471
472 // Scan the specified area for tiles to replace
473 std::vector<Tile16Change> changes;
474 for (int y = y_min; y <= y_max; ++y) {
475 for (int x = x_min; x <= x_max; ++x) {
476 uint16_t current_tile = overworld.GetTile(x, y);
477 if (current_tile == old_tile) {
478 Tile16Change change;
479 change.map_id = map_id;
480 change.x = x;
481 change.y = y;
482 change.old_tile = old_tile;
483 change.new_tile = new_tile;
484 changes.push_back(change);
485 }
486 }
487 }
488
489 if (changes.empty()) {
490 std::ostringstream oss;
491 oss << "0x" << std::hex << old_tile;
492 return absl::NotFoundError(absl::StrCat("No tiles matching ", oss.str(),
493 " found in specified area"));
494 }
495
496 return changes;
497}
498
500 const std::string& prompt, const std::vector<std::string>& commands,
501 const std::string& ai_service, Rom* rom) {
502 Tile16Proposal proposal;
503 proposal.id = GenerateProposalId();
504 proposal.prompt = prompt;
505 proposal.ai_service = ai_service;
506 proposal.created_at = std::chrono::system_clock::now();
508
509 // Parse each command
510 for (const auto& command : commands) {
511 // Skip empty commands or comments
512 if (command.empty() || command[0] == '#') {
513 continue;
514 }
515
516 // Check for different command types
517 if (absl::StrContains(command, "overworld set-tile")) {
518 auto change_or = ParseSetTileCommand(command, rom);
519 if (change_or.ok()) {
520 proposal.changes.push_back(change_or.value());
521 } else {
522 return change_or.status();
523 }
524 } else if (absl::StrContains(command, "overworld set-area")) {
525 auto changes_or = ParseSetAreaCommand(command, rom);
526 if (changes_or.ok()) {
527 proposal.changes.insert(proposal.changes.end(),
528 changes_or.value().begin(),
529 changes_or.value().end());
530 } else {
531 return changes_or.status();
532 }
533 } else if (absl::StrContains(command, "overworld replace-tile")) {
534 auto changes_or = ParseReplaceTileCommand(command, rom);
535 if (changes_or.ok()) {
536 proposal.changes.insert(proposal.changes.end(),
537 changes_or.value().begin(),
538 changes_or.value().end());
539 } else {
540 return changes_or.status();
541 }
542 }
543 }
544
545 if (proposal.changes.empty()) {
546 return absl::InvalidArgumentError(
547 "No valid tile16 changes found in commands");
548 }
549
550 proposal.reasoning = absl::StrCat("Generated ", proposal.changes.size(),
551 " tile16 changes from prompt");
552
553 return proposal;
554}
555
557 const Tile16Proposal& proposal, Rom* rom) {
558 if (!rom || !rom->is_loaded()) {
559 return absl::FailedPreconditionError("ROM not loaded");
560 }
561
562 zelda3::Overworld overworld(rom);
563 auto status = overworld.Load(rom);
564 if (!status.ok()) {
565 return status;
566 }
567
568 // Apply each change
569 for (const auto& change : proposal.changes) {
570 // Set the correct world
571 if (change.map_id < 0x40) {
572 overworld.set_current_world(0); // Light World
573 } else if (change.map_id < 0x80) {
574 overworld.set_current_world(1); // Dark World
575 } else {
576 overworld.set_current_world(2); // Special World
577 }
578
579 // Apply the tile change
580 overworld.SetTile(change.x, change.y, change.new_tile);
581 }
582
583 // Note: We don't save to disk here - that's the caller's responsibility
584 // This allows for sandbox testing before committing
585
586 return absl::OkStatus();
587}
588
589absl::StatusOr<gfx::Bitmap> Tile16ProposalGenerator::GenerateDiff(
590 const Tile16Proposal& proposal, Rom* before_rom, Rom* after_rom) {
591 if (!before_rom || !before_rom->is_loaded()) {
592 return absl::FailedPreconditionError("Before ROM not loaded");
593 }
594
595 if (!after_rom || !after_rom->is_loaded()) {
596 return absl::FailedPreconditionError("After ROM not loaded");
597 }
598
599 if (proposal.changes.empty()) {
600 return absl::InvalidArgumentError("No changes to visualize");
601 }
602
603 // Find the bounding box of all changes
604 int min_x = INT_MAX, min_y = INT_MAX;
605 int max_x = INT_MIN, max_y = INT_MIN;
606 int map_id = proposal.changes[0].map_id;
607
608 for (const auto& change : proposal.changes) {
609 if (change.x < min_x)
610 min_x = change.x;
611 if (change.y < min_y)
612 min_y = change.y;
613 if (change.x > max_x)
614 max_x = change.x;
615 if (change.y > max_y)
616 max_y = change.y;
617 }
618
619 // Add some padding around the changes
620 int padding = 2;
621 min_x = std::max(0, min_x - padding);
622 min_y = std::max(0, min_y - padding);
623 max_x = std::min(31, max_x + padding);
624 max_y = std::min(31, max_y + padding);
625
626 int width = (max_x - min_x + 1) * 16;
627 int height = (max_y - min_y + 1) * 16;
628
629 // Create a side-by-side diff bitmap (before on left, after on right)
630 int diff_width = width * 2 + 8; // 8 pixels separator
631 int diff_height = height;
632
633 std::vector<uint8_t> diff_data(diff_width * diff_height, 0x00);
634 gfx::Bitmap diff_bitmap(diff_width, diff_height, 8, diff_data);
635
636 // Load overworld data from both ROMs
637 zelda3::Overworld before_overworld(before_rom);
638 zelda3::Overworld after_overworld(after_rom);
639
640 auto before_status = before_overworld.Load(before_rom);
641 if (!before_status.ok()) {
642 return before_status;
643 }
644
645 auto after_status = after_overworld.Load(after_rom);
646 if (!after_status.ok()) {
647 return after_status;
648 }
649
650 // Set the correct world for both overworlds
651 int world = 0;
652 if (map_id < 0x40) {
653 world = 0; // Light World
654 } else if (map_id < 0x80) {
655 world = 1; // Dark World
656 } else {
657 world = 2; // Special World
658 }
659
660 before_overworld.set_current_world(world);
661 after_overworld.set_current_world(world);
662
663 // For now, create a simple colored diff representation
664 // Red = changed tiles, Green = unchanged tiles
665 // This is a placeholder until full tile rendering is implemented
666
667 gfx::SnesColor red_color(31, 0, 0); // Red for changed
668 gfx::SnesColor green_color(0, 31, 0); // Green for unchanged
669 gfx::SnesColor separator_color(15, 15, 15); // Gray separator
670
671 for (int y = min_y; y <= max_y; ++y) {
672 for (int x = min_x; x <= max_x; ++x) {
673 uint16_t before_tile = before_overworld.GetTile(x, y);
674 uint16_t after_tile = after_overworld.GetTile(x, y);
675
676 bool is_changed = (before_tile != after_tile);
677 gfx::SnesColor color = is_changed ? red_color : green_color;
678
679 // Draw "before" tile on left side
680 int pixel_x = (x - min_x) * 16;
681 int pixel_y = (y - min_y) * 16;
682 for (int py = 0; py < 16; ++py) {
683 for (int px = 0; px < 16; ++px) {
684 diff_bitmap.SetPixel(pixel_x + px, pixel_y + py, color);
685 }
686 }
687
688 // Draw "after" tile on right side
689 int right_offset = width + 8;
690 for (int py = 0; py < 16; ++py) {
691 for (int px = 0; px < 16; ++px) {
692 diff_bitmap.SetPixel(right_offset + pixel_x + px, pixel_y + py,
693 color);
694 }
695 }
696 }
697 }
698
699 // Draw separator line
700 for (int y = 0; y < diff_height; ++y) {
701 for (int x = 0; x < 8; ++x) {
702 diff_bitmap.SetPixel(width + x, y, separator_color);
703 }
704 }
705
706 return diff_bitmap;
707}
708
710 const Tile16Proposal& proposal, const std::string& path) {
711 std::ofstream file(path);
712 if (!file.is_open()) {
713 return absl::InvalidArgumentError(
714 absl::StrCat("Failed to open file for writing: ", path));
715 }
716
717 file << proposal.ToJson();
718 file.close();
719
720 return absl::OkStatus();
721}
722
723absl::StatusOr<Tile16Proposal> Tile16ProposalGenerator::LoadProposal(
724 const std::string& path) {
725 std::ifstream file(path);
726 if (!file.is_open()) {
727 return absl::InvalidArgumentError(
728 absl::StrCat("Failed to open file for reading: ", path));
729 }
730
731 std::stringstream buffer;
732 buffer << file.rdbuf();
733 file.close();
734
735 return Tile16Proposal::FromJson(buffer.str());
736}
737
738} // namespace cli
739} // 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
bool is_loaded() const
Definition rom.h:128
std::string GenerateProposalId() const
Generate a unique proposal ID.
absl::StatusOr< Tile16Proposal > GenerateFromCommands(const std::string &prompt, const std::vector< std::string > &commands, const std::string &ai_service, Rom *rom)
Generate a tile16 proposal from an AI-generated command list.
absl::StatusOr< Tile16Proposal > LoadProposal(const std::string &path)
Load a proposal from a JSON file.
absl::StatusOr< std::vector< Tile16Change > > ParseReplaceTileCommand(const std::string &command, Rom *rom)
Parse a "overworld replace-tile" command into multiple Tile16Changes.
absl::StatusOr< gfx::Bitmap > GenerateDiff(const Tile16Proposal &proposal, Rom *before_rom, Rom *after_rom)
Generate a visual diff bitmap for a proposal.
absl::Status ApplyProposal(const Tile16Proposal &proposal, Rom *rom)
Apply a proposal to a ROM (typically a sandbox).
absl::Status SaveProposal(const Tile16Proposal &proposal, const std::string &path)
Save a proposal to a JSON file for later review.
absl::StatusOr< std::vector< Tile16Change > > ParseSetAreaCommand(const std::string &command, Rom *rom)
Parse a "overworld set-area" command into multiple Tile16Changes.
absl::StatusOr< Tile16Change > ParseSetTileCommand(const std::string &command, Rom *rom)
Parse a single "overworld set-tile" command into a Tile16Change.
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void SetPixel(int x, int y, const SnesColor &color)
Set a pixel at the given x,y coordinates with SNES color.
Definition bitmap.cc:726
SNES Color container.
Definition snes_color.h:110
Represents the full Overworld data, light and dark world.
Definition overworld.h:217
void set_current_world(int world)
Definition overworld.h:535
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:36
uint16_t GetTile(int x, int y) const
Definition overworld.h:536
void SetTile(int x, int y, uint16_t tile_id)
Definition overworld.h:545
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< uint16_t > ParseTileValue(const nlohmann::json &json, const char *field)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
Represents a single tile16 change in a proposal.
Represents a proposal for tile16 edits on the overworld.
std::chrono::system_clock::time_point created_at
std::vector< Tile16Change > changes
static absl::StatusOr< Tile16Proposal > FromJson(const std::string &json)