yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_edit_commands.cc
Go to the documentation of this file.
2
3#include <cstdint>
4#include <limits>
5#include <optional>
6#include <utility>
7#include <vector>
8
9#include "absl/strings/numbers.h"
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_split.h"
12#include "cli/util/hex_util.h"
14#include "core/hack_manifest.h"
15#include "core/project.h"
16#include "rom/rom.h"
17#include "rom/snes.h"
18#include "rom/transaction.h"
19#include "util/macro.h"
21#include "zelda3/dungeon/room.h"
26
27namespace yaze {
28namespace cli {
29namespace handlers {
30
32
33namespace {
34
35absl::StatusOr<std::string> GetRequiredString(
36 const resources::ArgumentParser& parser, const char* name) {
37 auto value = parser.GetString(name);
38 if (!value.has_value()) {
39 return absl::InvalidArgumentError(
40 absl::StrFormat("Missing required argument '--%s'", name));
41 }
42 return *value;
43}
44
45absl::StatusOr<int> GetRequiredInt(const resources::ArgumentParser& parser,
46 const char* name) {
47 auto parsed = parser.GetInt(name);
48 if (!parsed.ok()) {
49 return parsed.status();
50 }
51 return parsed.value();
52}
53
54absl::StatusOr<int> GetOptionalInt(const resources::ArgumentParser& parser,
55 const char* name, int default_value) {
56 if (!parser.GetString(name).has_value()) {
57 return default_value;
58 }
59
60 auto parsed = parser.GetInt(name);
61 if (!parsed.ok()) {
62 return parsed.status();
63 }
64 return parsed.value();
65}
66
67absl::Status ValidateRoomId(int room_id) {
68 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
69 return absl::InvalidArgumentError(
70 absl::StrFormat("Room ID out of range: 0x%X (expected 0x00-0x%02X)",
71 room_id, zelda3::kNumberOfRooms - 1));
72 }
73 return absl::OkStatus();
74}
75
76absl::Status ValidateSpriteCoord(int value, char axis) {
77 if (value < 0 || value > 31) {
78 return absl::InvalidArgumentError(
79 absl::StrFormat("%c must be 0-31 (5-bit tile coord)", axis));
80 }
81 return absl::OkStatus();
82}
83
84absl::Status SaveRomWithBackup(Rom* rom,
85 resources::OutputFormatter& formatter) {
86 Rom::SaveSettings save_settings;
87 save_settings.require_backup = true;
88 auto disk_status = rom->SaveToFile(save_settings);
89 if (!disk_status.ok()) {
90 formatter.AddField("save_error", std::string(disk_status.message()));
91 return disk_status;
92 }
93
94 formatter.AddField("save_status", "saved");
95 return absl::OkStatus();
96}
97
98template <typename Mutation>
101 Mutation&& mutation) {
102 ScopedRomTransaction transaction(*rom);
103
104 auto write_status = std::forward<Mutation>(mutation)();
105 if (!write_status.ok()) {
106 formatter.AddField("write_error", std::string(write_status.message()));
107 return write_status;
108 }
109 formatter.AddField("write_status", "success");
110
111 RETURN_IF_ERROR(SaveRomWithBackup(rom, formatter));
112 transaction.Commit();
113 return absl::OkStatus();
114}
115
119 // A standalone --manifest is a safety capability, not permission to ignore
120 // ownership. Unlike a project, the CLI has no separate policy setting, so
121 // protected writes are always blocked.
122 project::RomWritePolicy write_policy = project::RomWritePolicy::kBlock;
123};
124
125absl::StatusOr<std::optional<ObjectSaveManifestContext>>
127 const auto manifest_path = parser.GetString("manifest");
128 if (!manifest_path.has_value()) {
129 return std::nullopt;
130 }
131
133 RETURN_IF_ERROR(context.manifest.LoadFromFile(*manifest_path));
134 const core::DungeonStreamLayout* manifest_layout =
137 if (manifest_layout == nullptr) {
138 return absl::FailedPreconditionError(
139 "Manifest does not define dungeon_stream_regions.objects");
140 }
141 if (manifest_layout->strategy != core::DungeonWriteStrategy::kCopyOnWrite) {
142 return absl::FailedPreconditionError(
143 "dungeon_stream_regions.objects must use copy_on_write");
144 }
145
148 core::DungeonStreamType::kObjects, *manifest_layout));
149 return std::optional<ObjectSaveManifestContext>(std::move(context));
150}
151
153 const Rom& source_rom, int room_id, const zelda3::Room& pending_room,
154 const ObjectSaveManifestContext& context) {
155 std::vector<std::pair<uint32_t, uint32_t>> ranges;
156
157 // Match DungeonEditorV2's conservative object-save prediction: validate the
158 // selected in-place stream, the door pointer, every possible COW allocation,
159 // and the selected object-pointer slot before exercising either save path.
160 uint32_t object_pointer_table_snes = 0;
161 ASSIGN_OR_RETURN(object_pointer_table_snes,
163 const uint32_t object_pointer_table_pc = SnesToPc(object_pointer_table_snes);
164 const uint64_t current_pointer_slot =
165 static_cast<uint64_t>(object_pointer_table_pc) +
166 static_cast<uint64_t>(room_id) * 3u;
167 if (current_pointer_slot + 3u > source_rom.size()) {
168 return absl::OutOfRangeError(
169 "Selected object pointer slot is outside the ROM");
170 }
171
172 uint32_t current_stream_snes = 0;
173 ASSIGN_OR_RETURN(current_stream_snes,
174 source_rom.ReadLong(static_cast<int>(current_pointer_slot)));
175 const uint32_t current_stream_pc = SnesToPc(current_stream_snes);
176 const uint64_t current_stream_end = static_cast<uint64_t>(current_stream_pc) +
177 pending_room.EncodeObjects().size() + 2u;
178 if (current_stream_end > std::numeric_limits<uint32_t>::max()) {
179 return absl::OutOfRangeError("Selected object write range overflows");
180 }
181 ranges.emplace_back(current_stream_pc,
182 static_cast<uint32_t>(current_stream_end));
183
184 const uint64_t door_pointer_slot =
185 static_cast<uint64_t>(zelda3::kDoorPointers) +
186 static_cast<uint64_t>(room_id) * 3u;
187 if (door_pointer_slot + 3u > std::numeric_limits<uint32_t>::max()) {
188 return absl::OutOfRangeError("Selected door pointer range overflows");
189 }
190 ranges.emplace_back(static_cast<uint32_t>(door_pointer_slot),
191 static_cast<uint32_t>(door_pointer_slot + 3u));
192
193 for (const auto& range : context.allocator_layout.allocation_ranges) {
194 ranges.emplace_back(range.begin, range.end);
195 }
196 const uint32_t pointer_width = context.allocator_layout.pointer_encoding ==
198 ? 3u
199 : 2u;
200 const uint64_t cow_pointer_slot =
201 static_cast<uint64_t>(context.allocator_layout.pointer_table_pc) +
202 static_cast<uint64_t>(room_id) * pointer_width;
203 if (cow_pointer_slot + pointer_width > std::numeric_limits<uint32_t>::max()) {
204 return absl::OutOfRangeError("Selected COW pointer range overflows");
205 }
206 ranges.emplace_back(static_cast<uint32_t>(cow_pointer_slot),
207 static_cast<uint32_t>(cow_pointer_slot + pointer_width));
208
209 const auto conflicts = context.manifest.AnalyzePcWriteRanges(ranges);
210 if (conflicts.empty() ||
212 return absl::OkStatus();
213 }
214 return absl::PermissionDeniedError("Write conflict with Hack Manifest");
215}
216
218 const Rom& source_rom, int room_id, const zelda3::RoomObject& object,
219 const zelda3::Room& pending_room,
220 const ObjectSaveManifestContext* manifest_context) {
221 if (manifest_context != nullptr) {
223 source_rom, room_id, pending_room, *manifest_context));
224 }
225
226 // Exercise the exact Room::SaveObjects path against an isolated snapshot so
227 // dry-run capacity and allocator outcomes match --write without mutating the
228 // caller's ROM.
229 Rom scratch_rom;
230 Rom::LoadOptions options;
231 options.strip_header = false;
232 options.load_resource_labels = false;
233 RETURN_IF_ERROR(scratch_rom.LoadFromData(source_rom.vector(), options));
234
235 zelda3::Room scratch_room = zelda3::LoadRoomFromRom(&scratch_rom, room_id);
236 RETURN_IF_ERROR(scratch_room.AddObject(object));
237 return scratch_room.SaveObjects(manifest_context != nullptr
238 ? &manifest_context->allocator_layout
239 : nullptr);
240}
241
242} // namespace
243
244// ---------------------------------------------------------------------------
245// dungeon-place-sprite
246// ---------------------------------------------------------------------------
247
249 Rom* rom, const resources::ArgumentParser& parser,
250 resources::OutputFormatter& formatter) {
251 auto room_id_str_or = GetRequiredString(parser, "room");
252 if (!room_id_str_or.ok()) {
253 return room_id_str_or.status();
254 }
255 auto sprite_id_str_or = GetRequiredString(parser, "id");
256 if (!sprite_id_str_or.ok()) {
257 return sprite_id_str_or.status();
258 }
259 const std::string room_id_str = room_id_str_or.value();
260 const std::string sprite_id_str = sprite_id_str_or.value();
261
262 int room_id, sprite_id;
263 if (!ParseHexString(room_id_str, &room_id)) {
264 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
265 }
266 if (!ParseHexString(sprite_id_str, &sprite_id)) {
267 return absl::InvalidArgumentError("Invalid sprite ID. Must be hex.");
268 }
269 auto room_status = ValidateRoomId(room_id);
270 if (!room_status.ok()) {
271 return room_status;
272 }
273
274 auto x_or = GetRequiredInt(parser, "x");
275 if (!x_or.ok()) {
276 return x_or.status();
277 }
278 auto y_or = GetRequiredInt(parser, "y");
279 if (!y_or.ok()) {
280 return y_or.status();
281 }
282 auto subtype_or = GetOptionalInt(parser, "subtype", 0);
283 if (!subtype_or.ok()) {
284 return subtype_or.status();
285 }
286 auto layer_or = GetOptionalInt(parser, "layer", 0);
287 if (!layer_or.ok()) {
288 return layer_or.status();
289 }
290
291 int x = x_or.value();
292 int y = y_or.value();
293 int subtype = subtype_or.value();
294 int layer = layer_or.value();
295 bool do_write = parser.HasFlag("write");
296
297 // Validate ranges
298 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
299 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
300 if (sprite_id < 0 || sprite_id > 0xFF) {
301 return absl::InvalidArgumentError("Sprite ID must be 0x00-0xFF");
302 }
303 if (subtype < 0 || subtype > 0x1F) {
304 return absl::InvalidArgumentError("Subtype must be 0-31 (5-bit flags)");
305 }
306 if (layer < 0 || layer > 1) {
307 return absl::InvalidArgumentError("Layer must be 0 or 1");
308 }
309
310 // Load room and its sprites
311 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
312 room.LoadSprites();
313
314 int count_before = static_cast<int>(room.GetSprites().size());
315
316 // Add the new sprite
317 room.GetSprites().emplace_back(
318 static_cast<uint8_t>(sprite_id), static_cast<uint8_t>(x),
319 static_cast<uint8_t>(y), static_cast<uint8_t>(subtype),
320 static_cast<uint8_t>(layer));
321 room.MarkSpritesDirty();
322
323 formatter.BeginObject("Place Sprite");
324 formatter.AddHexField("room_id", room_id, 2);
325 formatter.AddHexField("sprite_id", sprite_id, 2);
326 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(sprite_id));
327 formatter.AddField("x", x);
328 formatter.AddField("y", y);
329 formatter.AddField("subtype", subtype);
330 formatter.AddField("layer", layer);
331 formatter.AddField("sprites_before", count_before);
332 formatter.AddField("sprites_after",
333 static_cast<int>(room.GetSprites().size()));
334 formatter.AddField("mode", do_write ? "write" : "dry-run");
335
336 if (do_write) {
337 auto save_status = MutateAndSaveRomWithBackup(
338 rom, formatter, [&room]() { return room.SaveSprites(); });
339 if (!save_status.ok()) {
340 formatter.EndObject();
341 return save_status;
342 }
343 }
344
345 formatter.EndObject();
346 return absl::OkStatus();
347}
348
349// ---------------------------------------------------------------------------
350// dungeon-remove-sprite
351// ---------------------------------------------------------------------------
352
354 Rom* rom, const resources::ArgumentParser& parser,
355 resources::OutputFormatter& formatter) {
356 auto room_id_str_or = GetRequiredString(parser, "room");
357 if (!room_id_str_or.ok()) {
358 return room_id_str_or.status();
359 }
360 const std::string room_id_str = room_id_str_or.value();
361
362 int room_id;
363 if (!ParseHexString(room_id_str, &room_id)) {
364 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
365 }
366 auto room_status = ValidateRoomId(room_id);
367 if (!room_status.ok()) {
368 return room_status;
369 }
370
371 bool do_write = parser.HasFlag("write");
372
373 // Load room and its sprites
374 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
375 room.LoadSprites();
376
377 auto& sprites = room.GetSprites();
378 int count_before = static_cast<int>(sprites.size());
379
380 // Find sprite to remove: by --index or by --x/--y position.
381 const bool has_index = parser.GetString("index").has_value();
382 const bool has_x = parser.GetString("x").has_value();
383 const bool has_y = parser.GetString("y").has_value();
384 if (has_index && (has_x || has_y)) {
385 return absl::InvalidArgumentError(
386 "Use either --index or --x/--y, not both");
387 }
388 if (!has_index && has_x != has_y) {
389 return absl::InvalidArgumentError(
390 "Both --x and --y are required when removing by position");
391 }
392 if (!has_index && !has_x) {
393 return absl::InvalidArgumentError(
394 "Either --index or both --x and --y are required");
395 }
396
397 int remove_index = -1;
398 if (has_index) {
399 auto index_or = GetRequiredInt(parser, "index");
400 if (!index_or.ok()) {
401 return index_or.status();
402 }
403 remove_index = index_or.value();
404 } else {
405 auto x_or = GetRequiredInt(parser, "x");
406 if (!x_or.ok()) {
407 return x_or.status();
408 }
409 auto y_or = GetRequiredInt(parser, "y");
410 if (!y_or.ok()) {
411 return y_or.status();
412 }
413
414 const int x = x_or.value();
415 const int y = y_or.value();
416 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
417 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
418
419 for (int i = 0; i < static_cast<int>(sprites.size()); ++i) {
420 if (sprites[i].x() == x && sprites[i].y() == y) {
421 remove_index = i;
422 break;
423 }
424 }
425 if (remove_index < 0) {
426 return absl::NotFoundError(absl::StrFormat(
427 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
428 }
429 }
430
431 if (remove_index < 0 || remove_index >= static_cast<int>(sprites.size())) {
432 return absl::OutOfRangeError(
433 absl::StrFormat("Sprite index %d out of range (room has %d sprites)",
434 remove_index, count_before));
435 }
436
437 // Report which sprite we're removing
438 const auto& target = sprites[remove_index];
439 formatter.BeginObject("Remove Sprite");
440 formatter.AddHexField("room_id", room_id, 2);
441 formatter.AddField("removed_index", remove_index);
442 formatter.AddHexField("sprite_id", target.id(), 2);
443 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(target.id()));
444 formatter.AddField("x", target.x());
445 formatter.AddField("y", target.y());
446 formatter.AddField("sprites_before", count_before);
447
448 // Remove
449 sprites.erase(sprites.begin() + remove_index);
450 room.MarkSpritesDirty();
451 formatter.AddField("sprites_after", static_cast<int>(sprites.size()));
452 formatter.AddField("mode", do_write ? "write" : "dry-run");
453
454 if (do_write) {
455 auto save_status = MutateAndSaveRomWithBackup(
456 rom, formatter, [&room]() { return room.SaveSprites(); });
457 if (!save_status.ok()) {
458 formatter.EndObject();
459 return save_status;
460 }
461 }
462
463 formatter.EndObject();
464 return absl::OkStatus();
465}
466
467// ---------------------------------------------------------------------------
468// dungeon-place-object
469// ---------------------------------------------------------------------------
470
472 Rom* rom, const resources::ArgumentParser& parser,
473 resources::OutputFormatter& formatter) {
474 auto room_id_str_or = GetRequiredString(parser, "room");
475 if (!room_id_str_or.ok()) {
476 return room_id_str_or.status();
477 }
478 auto object_id_str_or = GetRequiredString(parser, "id");
479 if (!object_id_str_or.ok()) {
480 return object_id_str_or.status();
481 }
482 const std::string room_id_str = room_id_str_or.value();
483 const std::string object_id_str = object_id_str_or.value();
484
485 int room_id, object_id;
486 if (!ParseHexString(room_id_str, &room_id)) {
487 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
488 }
489 if (!ParseHexString(object_id_str, &object_id)) {
490 return absl::InvalidArgumentError("Invalid object ID. Must be hex.");
491 }
492 auto room_status = ValidateRoomId(room_id);
493 if (!room_status.ok()) {
494 return room_status;
495 }
496 if (object_id < 0 || object_id > 0xFFFF) {
497 return absl::InvalidArgumentError("Object ID must be 0x0000-0xFFFF");
498 }
499
500 auto x_or = GetRequiredInt(parser, "x");
501 if (!x_or.ok()) {
502 return x_or.status();
503 }
504 auto y_or = GetRequiredInt(parser, "y");
505 if (!y_or.ok()) {
506 return y_or.status();
507 }
508 auto size_or = GetOptionalInt(parser, "size", 0);
509 if (!size_or.ok()) {
510 return size_or.status();
511 }
512 auto layer_or = GetOptionalInt(parser, "layer", 0);
513 if (!layer_or.ok()) {
514 return layer_or.status();
515 }
516
517 int x = x_or.value();
518 int y = y_or.value();
519 int size = size_or.value();
520 int layer = layer_or.value();
521 bool do_write = parser.HasFlag("write");
522
523 // Validate ranges
524 if (x < 0 || x > 63) {
525 return absl::InvalidArgumentError("X must be 0-63");
526 }
527 if (y < 0 || y > 63) {
528 return absl::InvalidArgumentError("Y must be 0-63");
529 }
530 if (layer < 0 || layer > 2) {
531 return absl::InvalidArgumentError("Layer must be 0, 1, or 2");
532 }
533 if (size < 0 || size > 0xFF) {
534 return absl::InvalidArgumentError("Size must be 0-255");
535 }
536
537 std::optional<ObjectSaveManifestContext> manifest_context;
538 ASSIGN_OR_RETURN(manifest_context, LoadObjectSaveManifestContext(parser));
539 const zelda3::DungeonStreamLayout* layout =
540 manifest_context.has_value() ? &manifest_context->allocator_layout
541 : nullptr;
542
543 // Load room with full objects
544 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
545
546 int count_before = static_cast<int>(room.GetTileObjects().size());
547
548 // Create the new object
549 zelda3::RoomObject obj(static_cast<int16_t>(object_id),
550 static_cast<uint8_t>(x), static_cast<uint8_t>(y),
551 static_cast<uint8_t>(size),
552 static_cast<uint8_t>(layer));
553
554 // Determine type for reporting
555 int type = zelda3::RoomObject::DetermineObjectType((object_id & 0xFF),
556 (object_id >> 8));
557
558 formatter.BeginObject("Place Object");
559 formatter.AddHexField("room_id", room_id, 2);
560 formatter.AddHexField("object_id", object_id, 4);
561 formatter.AddField("object_name", zelda3::GetObjectName(object_id));
562 formatter.AddField("object_type", type);
563 formatter.AddField("x", x);
564 formatter.AddField("y", y);
565 formatter.AddField("size", size);
566 formatter.AddField("layer", layer);
567 formatter.AddField("objects_before", count_before);
568 if (const auto manifest_path = parser.GetString("manifest");
569 manifest_path.has_value()) {
570 formatter.AddField("manifest", *manifest_path);
571 formatter.AddField("manifest_write_policy", "block");
572 }
573 formatter.AddField("allocator_capability",
574 layout != nullptr ? "copy_on_write" : "none");
575
576 // Add the object
577 auto add_status = room.AddObject(obj);
578 if (!add_status.ok()) {
579 formatter.AddField("error", std::string(add_status.message()));
580 formatter.EndObject();
581 return add_status;
582 }
583
584 formatter.AddField("objects_after",
585 static_cast<int>(room.GetTileObjects().size()));
586 formatter.AddField("mode", do_write ? "write" : "dry-run");
587
588 const auto preflight_status = PreflightObjectSave(
589 *rom, room_id, obj, room,
590 manifest_context.has_value() ? &*manifest_context : nullptr);
591 if (!preflight_status.ok()) {
592 formatter.AddField("preflight_status", "failed");
593 formatter.AddField("preflight_error",
594 std::string(preflight_status.message()));
595 formatter.EndObject();
596 return preflight_status;
597 }
598 formatter.AddField("preflight_status", "success");
599
600 if (do_write) {
601 auto save_status = MutateAndSaveRomWithBackup(
602 rom, formatter, [&room, layout]() { return room.SaveObjects(layout); });
603 if (!save_status.ok()) {
604 formatter.EndObject();
605 return save_status;
606 }
607 }
608
609 formatter.EndObject();
610 return absl::OkStatus();
611}
612
613// ---------------------------------------------------------------------------
614// dungeon-set-collision-tile
615// ---------------------------------------------------------------------------
616
618 Rom* rom, const resources::ArgumentParser& parser,
619 resources::OutputFormatter& formatter) {
620 auto room_id_str_or = GetRequiredString(parser, "room");
621 if (!room_id_str_or.ok()) {
622 return room_id_str_or.status();
623 }
624 auto tiles_str_or = GetRequiredString(parser, "tiles");
625 if (!tiles_str_or.ok()) {
626 return tiles_str_or.status();
627 }
628 const std::string room_id_str = room_id_str_or.value();
629 const std::string tiles_str = tiles_str_or.value();
630
631 int room_id;
632 if (!ParseHexString(room_id_str, &room_id)) {
633 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
634 }
635 auto room_status = ValidateRoomId(room_id);
636 if (!room_status.ok()) {
637 return room_status;
638 }
639
640 bool do_write = parser.HasFlag("write");
641
642 // Parse tile specifications: "x,y,tile;x,y,tile;..."
643 struct TileSpec {
644 int x, y, tile;
645 };
646 std::vector<TileSpec> specs;
647
648 for (absl::string_view entry :
649 absl::StrSplit(tiles_str, ';', absl::SkipEmpty())) {
650 std::vector<std::string> parts =
651 absl::StrSplit(entry, ',', absl::SkipEmpty());
652 if (parts.size() != 3) {
653 return absl::InvalidArgumentError(absl::StrFormat(
654 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
655 }
656 TileSpec spec;
657
658 // x and y are decimal tile coords
659 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
660 return absl::InvalidArgumentError(
661 absl::StrFormat("Invalid X coord '%s'", parts[0]));
662 }
663 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
664 return absl::InvalidArgumentError(
665 absl::StrFormat("Invalid Y coord '%s'", parts[1]));
666 }
667 if (!ParseHexString(parts[2], &spec.tile)) {
668 return absl::InvalidArgumentError(
669 absl::StrFormat("Invalid tile value '%s'. Must be hex.", parts[2]));
670 }
671
672 // Validate ranges
673 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
674 return absl::InvalidArgumentError(absl::StrFormat(
675 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
676 }
677 if (spec.tile < 0 || spec.tile > 0xFF) {
678 return absl::InvalidArgumentError("Tile value must be 0x00-0xFF");
679 }
680
681 specs.push_back(spec);
682 }
683
684 if (specs.empty()) {
685 return absl::InvalidArgumentError("No tile specs provided");
686 }
687
688 // Load room with custom collision data
689 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
690
691 formatter.BeginObject("Set Collision Tiles");
692 formatter.AddHexField("room_id", room_id, 2);
693 formatter.AddField("had_custom_collision", room.has_custom_collision());
694 formatter.AddField("tile_count", static_cast<int>(specs.size()));
695 formatter.AddField("mode", do_write ? "write" : "dry-run");
696
697 // Apply each tile change
698 formatter.BeginArray("changes");
699 for (const auto& spec : specs) {
700 uint8_t old_value = room.GetCollisionTile(spec.x, spec.y);
701 room.SetCollisionTile(spec.x, spec.y, static_cast<uint8_t>(spec.tile));
702
703 formatter.BeginObject();
704 formatter.AddField("x", spec.x);
705 formatter.AddField("y", spec.y);
706 formatter.AddHexField("old_tile", old_value, 2);
707 formatter.AddHexField("new_tile", spec.tile, 2);
708 formatter.EndObject();
709 }
710 formatter.EndArray();
711
712 if (do_write) {
713 auto save_status =
714 MutateAndSaveRomWithBackup(rom, formatter, [&rom, &room]() mutable {
715 // Flush collision changes to ROM.
716 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
717 return zelda3::SaveAllCollision(rom, absl::MakeSpan(rooms_arr));
718 });
719 if (!save_status.ok()) {
720 formatter.EndObject();
721 return save_status;
722 }
723 }
724
725 formatter.EndObject();
726 return absl::OkStatus();
727}
728
729} // namespace handlers
730} // namespace cli
731} // 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:28
const auto & vector() const
Definition rom.h:155
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:371
auto size() const
Definition rom.h:150
absl::Status LoadFromData(const std::vector< uint8_t > &data, const LoadOptions &options=LoadOptions::Defaults())
Definition rom.cc:335
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:533
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
absl::StatusOr< int > GetInt(const std::string &name) const
Parse an integer argument (supports hex with 0x prefix)
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void BeginObject(const std::string &title="")
Start a JSON object or text section.
void EndObject()
End a JSON object or text section.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
void AddHexField(const std::string &key, uint64_t value, int width=2)
Add a hex-formatted field.
Loads and queries the hack manifest JSON for yaze-ASM integration.
const DungeonStreamLayout * GetDungeonStreamLayout(DungeonStreamType stream) const
Get an explicitly declared dungeon stream layout.
absl::Status LoadFromFile(const std::string &filepath)
Load manifest from a JSON file path.
std::vector< WriteConflict > AnalyzePcWriteRanges(const std::vector< std::pair< uint32_t, uint32_t > > &pc_ranges) const
Analyze a set of PC-offset ranges for write conflicts.
static int DetermineObjectType(uint8_t b1, uint8_t b3)
uint8_t GetCollisionTile(int x, int y) const
Definition room.h:508
void MarkSpritesDirty()
Definition room.h:256
absl::Status SaveObjects(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2020
std::vector< uint8_t > EncodeObjects() const
Definition room.cc:1795
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:253
absl::Status SaveSprites(const DungeonStreamLayout *layout=nullptr)
Definition room.cc:2208
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:382
void SetCollisionTile(int x, int y, uint8_t tile)
Definition room.h:514
void LoadSprites()
Definition room.cc:2485
absl::Status AddObject(const RoomObject &object)
Definition room.cc:2372
bool has_custom_collision() const
Definition room.h:500
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status ValidateObjectSaveManifestConflicts(const Rom &source_rom, int room_id, const zelda3::Room &pending_room, const ObjectSaveManifestContext &context)
absl::StatusOr< int > GetOptionalInt(const resources::ArgumentParser &parser, const char *name, int default_value)
absl::StatusOr< std::string > GetRequiredString(const resources::ArgumentParser &parser, const char *name)
absl::StatusOr< std::optional< ObjectSaveManifestContext > > LoadObjectSaveManifestContext(const resources::ArgumentParser &parser)
absl::StatusOr< int > GetRequiredInt(const resources::ArgumentParser &parser, const char *name)
absl::Status PreflightObjectSave(const Rom &source_rom, int room_id, const zelda3::RoomObject &object, const zelda3::Room &pending_room, const ObjectSaveManifestContext *manifest_context)
absl::Status MutateAndSaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter, Mutation &&mutation)
absl::Status SaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
absl::StatusOr< zelda3::DungeonStreamLayout > ToDungeonStreamAllocatorLayout(DungeonStreamType stream_type, const DungeonStreamLayout &manifest_layout)
constexpr int kDoorPointers
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:543
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:518
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:3413
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
constexpr int kRoomObjectPointer
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
bool load_resource_labels
Definition rom.h:43
Explicit pointer and storage layout for one dungeon stream.
DungeonWriteStrategy strategy
std::vector< DungeonStreamPcRange > allocation_ranges