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