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