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 "absl/strings/numbers.h"
4#include "absl/strings/str_format.h"
5#include "absl/strings/str_split.h"
6#include "cli/util/hex_util.h"
7#include "rom/rom.h"
8#include "rom/snes.h"
9#include "util/macro.h"
10#include "zelda3/dungeon/room.h"
15
16namespace yaze {
17namespace cli {
18namespace handlers {
19
21
22namespace {
23
24absl::StatusOr<std::string> GetRequiredString(
25 const resources::ArgumentParser& parser, const char* name) {
26 auto value = parser.GetString(name);
27 if (!value.has_value()) {
28 return absl::InvalidArgumentError(
29 absl::StrFormat("Missing required argument '--%s'", name));
30 }
31 return *value;
32}
33
34absl::StatusOr<int> GetRequiredInt(const resources::ArgumentParser& parser,
35 const char* name) {
36 auto parsed = parser.GetInt(name);
37 if (!parsed.ok()) {
38 return parsed.status();
39 }
40 return parsed.value();
41}
42
43absl::StatusOr<int> GetOptionalInt(const resources::ArgumentParser& parser,
44 const char* name, int default_value) {
45 if (!parser.GetString(name).has_value()) {
46 return default_value;
47 }
48
49 auto parsed = parser.GetInt(name);
50 if (!parsed.ok()) {
51 return parsed.status();
52 }
53 return parsed.value();
54}
55
56absl::Status ValidateRoomId(int room_id) {
57 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
58 return absl::InvalidArgumentError(
59 absl::StrFormat("Room ID out of range: 0x%X (expected 0x00-0x%02X)",
60 room_id, zelda3::kNumberOfRooms - 1));
61 }
62 return absl::OkStatus();
63}
64
65absl::Status ValidateSpriteCoord(int value, char axis) {
66 if (value < 0 || value > 31) {
67 return absl::InvalidArgumentError(
68 absl::StrFormat("%c must be 0-31 (5-bit tile coord)", axis));
69 }
70 return absl::OkStatus();
71}
72
73absl::Status SaveRomWithBackup(Rom* rom,
74 resources::OutputFormatter& formatter) {
75 Rom::SaveSettings save_settings;
76 save_settings.backup = true;
77 auto disk_status = rom->SaveToFile(save_settings);
78 if (!disk_status.ok()) {
79 formatter.AddField("save_error", std::string(disk_status.message()));
80 return disk_status;
81 }
82
83 formatter.AddField("save_status", "saved");
84 return absl::OkStatus();
85}
86
87} // namespace
88
89// ---------------------------------------------------------------------------
90// dungeon-place-sprite
91// ---------------------------------------------------------------------------
92
94 Rom* rom, const resources::ArgumentParser& parser,
95 resources::OutputFormatter& formatter) {
96 auto room_id_str_or = GetRequiredString(parser, "room");
97 if (!room_id_str_or.ok()) {
98 return room_id_str_or.status();
99 }
100 auto sprite_id_str_or = GetRequiredString(parser, "id");
101 if (!sprite_id_str_or.ok()) {
102 return sprite_id_str_or.status();
103 }
104 const std::string room_id_str = room_id_str_or.value();
105 const std::string sprite_id_str = sprite_id_str_or.value();
106
107 int room_id, sprite_id;
108 if (!ParseHexString(room_id_str, &room_id)) {
109 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
110 }
111 if (!ParseHexString(sprite_id_str, &sprite_id)) {
112 return absl::InvalidArgumentError("Invalid sprite ID. Must be hex.");
113 }
114 auto room_status = ValidateRoomId(room_id);
115 if (!room_status.ok()) {
116 return room_status;
117 }
118
119 auto x_or = GetRequiredInt(parser, "x");
120 if (!x_or.ok()) {
121 return x_or.status();
122 }
123 auto y_or = GetRequiredInt(parser, "y");
124 if (!y_or.ok()) {
125 return y_or.status();
126 }
127 auto subtype_or = GetOptionalInt(parser, "subtype", 0);
128 if (!subtype_or.ok()) {
129 return subtype_or.status();
130 }
131 auto layer_or = GetOptionalInt(parser, "layer", 0);
132 if (!layer_or.ok()) {
133 return layer_or.status();
134 }
135
136 int x = x_or.value();
137 int y = y_or.value();
138 int subtype = subtype_or.value();
139 int layer = layer_or.value();
140 bool do_write = parser.HasFlag("write");
141
142 // Validate ranges
143 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
144 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
145 if (sprite_id < 0 || sprite_id > 0xFF) {
146 return absl::InvalidArgumentError("Sprite ID must be 0x00-0xFF");
147 }
148 if (subtype < 0 || subtype > 0x1F) {
149 return absl::InvalidArgumentError("Subtype must be 0-31 (5-bit flags)");
150 }
151 if (layer < 0 || layer > 1) {
152 return absl::InvalidArgumentError("Layer must be 0 or 1");
153 }
154
155 // Load room and its sprites
156 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
157 room.LoadSprites();
158
159 int count_before = static_cast<int>(room.GetSprites().size());
160
161 // Add the new sprite
162 room.GetSprites().emplace_back(
163 static_cast<uint8_t>(sprite_id), static_cast<uint8_t>(x),
164 static_cast<uint8_t>(y), static_cast<uint8_t>(subtype),
165 static_cast<uint8_t>(layer));
166
167 formatter.BeginObject("Place Sprite");
168 formatter.AddHexField("room_id", room_id, 2);
169 formatter.AddHexField("sprite_id", sprite_id, 2);
170 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(sprite_id));
171 formatter.AddField("x", x);
172 formatter.AddField("y", y);
173 formatter.AddField("subtype", subtype);
174 formatter.AddField("layer", layer);
175 formatter.AddField("sprites_before", count_before);
176 formatter.AddField("sprites_after",
177 static_cast<int>(room.GetSprites().size()));
178 formatter.AddField("mode", do_write ? "write" : "dry-run");
179
180 if (do_write) {
181 auto save_status = room.SaveSprites();
182 if (!save_status.ok()) {
183 formatter.AddField("write_error", std::string(save_status.message()));
184 formatter.EndObject();
185 return save_status;
186 }
187 formatter.AddField("write_status", "success");
188
189 auto disk_status = SaveRomWithBackup(rom, formatter);
190 if (!disk_status.ok()) {
191 formatter.EndObject();
192 return disk_status;
193 }
194 }
195
196 formatter.EndObject();
197 return absl::OkStatus();
198}
199
200// ---------------------------------------------------------------------------
201// dungeon-remove-sprite
202// ---------------------------------------------------------------------------
203
205 Rom* rom, const resources::ArgumentParser& parser,
206 resources::OutputFormatter& formatter) {
207 auto room_id_str_or = GetRequiredString(parser, "room");
208 if (!room_id_str_or.ok()) {
209 return room_id_str_or.status();
210 }
211 const std::string room_id_str = room_id_str_or.value();
212
213 int room_id;
214 if (!ParseHexString(room_id_str, &room_id)) {
215 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
216 }
217 auto room_status = ValidateRoomId(room_id);
218 if (!room_status.ok()) {
219 return room_status;
220 }
221
222 bool do_write = parser.HasFlag("write");
223
224 // Load room and its sprites
225 zelda3::Room room = zelda3::LoadRoomHeaderFromRom(rom, room_id);
226 room.LoadSprites();
227
228 auto& sprites = room.GetSprites();
229 int count_before = static_cast<int>(sprites.size());
230
231 // Find sprite to remove: by --index or by --x/--y position.
232 const bool has_index = parser.GetString("index").has_value();
233 const bool has_x = parser.GetString("x").has_value();
234 const bool has_y = parser.GetString("y").has_value();
235 if (has_index && (has_x || has_y)) {
236 return absl::InvalidArgumentError(
237 "Use either --index or --x/--y, not both");
238 }
239 if (!has_index && has_x != has_y) {
240 return absl::InvalidArgumentError(
241 "Both --x and --y are required when removing by position");
242 }
243 if (!has_index && !has_x) {
244 return absl::InvalidArgumentError(
245 "Either --index or both --x and --y are required");
246 }
247
248 int remove_index = -1;
249 if (has_index) {
250 auto index_or = GetRequiredInt(parser, "index");
251 if (!index_or.ok()) {
252 return index_or.status();
253 }
254 remove_index = index_or.value();
255 } else {
256 auto x_or = GetRequiredInt(parser, "x");
257 if (!x_or.ok()) {
258 return x_or.status();
259 }
260 auto y_or = GetRequiredInt(parser, "y");
261 if (!y_or.ok()) {
262 return y_or.status();
263 }
264
265 const int x = x_or.value();
266 const int y = y_or.value();
267 RETURN_IF_ERROR(ValidateSpriteCoord(x, 'X'));
268 RETURN_IF_ERROR(ValidateSpriteCoord(y, 'Y'));
269
270 for (int i = 0; i < static_cast<int>(sprites.size()); ++i) {
271 if (sprites[i].x() == x && sprites[i].y() == y) {
272 remove_index = i;
273 break;
274 }
275 }
276 if (remove_index < 0) {
277 return absl::NotFoundError(absl::StrFormat(
278 "No sprite at (%d, %d) in room 0x%02X", x, y, room_id));
279 }
280 }
281
282 if (remove_index < 0 || remove_index >= static_cast<int>(sprites.size())) {
283 return absl::OutOfRangeError(
284 absl::StrFormat("Sprite index %d out of range (room has %d sprites)",
285 remove_index, count_before));
286 }
287
288 // Report which sprite we're removing
289 const auto& target = sprites[remove_index];
290 formatter.BeginObject("Remove Sprite");
291 formatter.AddHexField("room_id", room_id, 2);
292 formatter.AddField("removed_index", remove_index);
293 formatter.AddHexField("sprite_id", target.id(), 2);
294 formatter.AddField("sprite_name", zelda3::ResolveSpriteName(target.id()));
295 formatter.AddField("x", target.x());
296 formatter.AddField("y", target.y());
297 formatter.AddField("sprites_before", count_before);
298
299 // Remove
300 sprites.erase(sprites.begin() + remove_index);
301 formatter.AddField("sprites_after", static_cast<int>(sprites.size()));
302 formatter.AddField("mode", do_write ? "write" : "dry-run");
303
304 if (do_write) {
305 auto save_status = room.SaveSprites();
306 if (!save_status.ok()) {
307 formatter.AddField("write_error", std::string(save_status.message()));
308 formatter.EndObject();
309 return save_status;
310 }
311 formatter.AddField("write_status", "success");
312
313 auto disk_status = SaveRomWithBackup(rom, formatter);
314 if (!disk_status.ok()) {
315 formatter.EndObject();
316 return disk_status;
317 }
318 }
319
320 formatter.EndObject();
321 return absl::OkStatus();
322}
323
324// ---------------------------------------------------------------------------
325// dungeon-place-object
326// ---------------------------------------------------------------------------
327
329 Rom* rom, const resources::ArgumentParser& parser,
330 resources::OutputFormatter& formatter) {
331 auto room_id_str_or = GetRequiredString(parser, "room");
332 if (!room_id_str_or.ok()) {
333 return room_id_str_or.status();
334 }
335 auto object_id_str_or = GetRequiredString(parser, "id");
336 if (!object_id_str_or.ok()) {
337 return object_id_str_or.status();
338 }
339 const std::string room_id_str = room_id_str_or.value();
340 const std::string object_id_str = object_id_str_or.value();
341
342 int room_id, object_id;
343 if (!ParseHexString(room_id_str, &room_id)) {
344 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
345 }
346 if (!ParseHexString(object_id_str, &object_id)) {
347 return absl::InvalidArgumentError("Invalid object ID. Must be hex.");
348 }
349 auto room_status = ValidateRoomId(room_id);
350 if (!room_status.ok()) {
351 return room_status;
352 }
353 if (object_id < 0 || object_id > 0xFFFF) {
354 return absl::InvalidArgumentError("Object ID must be 0x0000-0xFFFF");
355 }
356
357 auto x_or = GetRequiredInt(parser, "x");
358 if (!x_or.ok()) {
359 return x_or.status();
360 }
361 auto y_or = GetRequiredInt(parser, "y");
362 if (!y_or.ok()) {
363 return y_or.status();
364 }
365 auto size_or = GetOptionalInt(parser, "size", 0);
366 if (!size_or.ok()) {
367 return size_or.status();
368 }
369 auto layer_or = GetOptionalInt(parser, "layer", 0);
370 if (!layer_or.ok()) {
371 return layer_or.status();
372 }
373
374 int x = x_or.value();
375 int y = y_or.value();
376 int size = size_or.value();
377 int layer = layer_or.value();
378 bool do_write = parser.HasFlag("write");
379
380 // Validate ranges
381 if (x < 0 || x > 63) {
382 return absl::InvalidArgumentError("X must be 0-63");
383 }
384 if (y < 0 || y > 63) {
385 return absl::InvalidArgumentError("Y must be 0-63");
386 }
387 if (layer < 0 || layer > 2) {
388 return absl::InvalidArgumentError("Layer must be 0, 1, or 2");
389 }
390 if (size < 0 || size > 0xFF) {
391 return absl::InvalidArgumentError("Size must be 0-255");
392 }
393
394 // Load room with full objects
395 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
396
397 int count_before = static_cast<int>(room.GetTileObjects().size());
398
399 // Create the new object
400 zelda3::RoomObject obj(static_cast<int16_t>(object_id),
401 static_cast<uint8_t>(x), static_cast<uint8_t>(y),
402 static_cast<uint8_t>(size),
403 static_cast<uint8_t>(layer));
404
405 // Determine type for reporting
406 int type = zelda3::RoomObject::DetermineObjectType((object_id & 0xFF),
407 (object_id >> 8));
408
409 formatter.BeginObject("Place Object");
410 formatter.AddHexField("room_id", room_id, 2);
411 formatter.AddHexField("object_id", object_id, 4);
412 formatter.AddField("object_name", zelda3::GetObjectName(object_id));
413 formatter.AddField("object_type", type);
414 formatter.AddField("x", x);
415 formatter.AddField("y", y);
416 formatter.AddField("size", size);
417 formatter.AddField("layer", layer);
418 formatter.AddField("objects_before", count_before);
419
420 // Add the object
421 auto add_status = room.AddObject(obj);
422 if (!add_status.ok()) {
423 formatter.AddField("error", std::string(add_status.message()));
424 formatter.EndObject();
425 return add_status;
426 }
427
428 formatter.AddField("objects_after",
429 static_cast<int>(room.GetTileObjects().size()));
430 formatter.AddField("mode", do_write ? "write" : "dry-run");
431
432 if (do_write) {
433 auto save_status = room.SaveObjects();
434 if (!save_status.ok()) {
435 formatter.AddField("write_error", std::string(save_status.message()));
436 formatter.EndObject();
437 return save_status;
438 }
439 formatter.AddField("write_status", "success");
440
441 auto disk_status = SaveRomWithBackup(rom, formatter);
442 if (!disk_status.ok()) {
443 formatter.EndObject();
444 return disk_status;
445 }
446 }
447
448 formatter.EndObject();
449 return absl::OkStatus();
450}
451
452// ---------------------------------------------------------------------------
453// dungeon-set-collision-tile
454// ---------------------------------------------------------------------------
455
457 Rom* rom, const resources::ArgumentParser& parser,
458 resources::OutputFormatter& formatter) {
459 auto room_id_str_or = GetRequiredString(parser, "room");
460 if (!room_id_str_or.ok()) {
461 return room_id_str_or.status();
462 }
463 auto tiles_str_or = GetRequiredString(parser, "tiles");
464 if (!tiles_str_or.ok()) {
465 return tiles_str_or.status();
466 }
467 const std::string room_id_str = room_id_str_or.value();
468 const std::string tiles_str = tiles_str_or.value();
469
470 int room_id;
471 if (!ParseHexString(room_id_str, &room_id)) {
472 return absl::InvalidArgumentError("Invalid room ID. Must be hex.");
473 }
474 auto room_status = ValidateRoomId(room_id);
475 if (!room_status.ok()) {
476 return room_status;
477 }
478
479 bool do_write = parser.HasFlag("write");
480
481 // Parse tile specifications: "x,y,tile;x,y,tile;..."
482 struct TileSpec {
483 int x, y, tile;
484 };
485 std::vector<TileSpec> specs;
486
487 for (absl::string_view entry :
488 absl::StrSplit(tiles_str, ';', absl::SkipEmpty())) {
489 std::vector<std::string> parts =
490 absl::StrSplit(entry, ',', absl::SkipEmpty());
491 if (parts.size() != 3) {
492 return absl::InvalidArgumentError(absl::StrFormat(
493 "Invalid tile spec '%s'. Expected x,y,tile (e.g. 10,5,0xB7)", entry));
494 }
495 TileSpec spec;
496
497 // x and y are decimal tile coords
498 if (!absl::SimpleAtoi(parts[0], &spec.x)) {
499 return absl::InvalidArgumentError(
500 absl::StrFormat("Invalid X coord '%s'", parts[0]));
501 }
502 if (!absl::SimpleAtoi(parts[1], &spec.y)) {
503 return absl::InvalidArgumentError(
504 absl::StrFormat("Invalid Y coord '%s'", parts[1]));
505 }
506 if (!ParseHexString(parts[2], &spec.tile)) {
507 return absl::InvalidArgumentError(
508 absl::StrFormat("Invalid tile value '%s'. Must be hex.", parts[2]));
509 }
510
511 // Validate ranges
512 if (spec.x < 0 || spec.x > 63 || spec.y < 0 || spec.y > 63) {
513 return absl::InvalidArgumentError(absl::StrFormat(
514 "Tile coords (%d,%d) out of range (0-63)", spec.x, spec.y));
515 }
516 if (spec.tile < 0 || spec.tile > 0xFF) {
517 return absl::InvalidArgumentError("Tile value must be 0x00-0xFF");
518 }
519
520 specs.push_back(spec);
521 }
522
523 if (specs.empty()) {
524 return absl::InvalidArgumentError("No tile specs provided");
525 }
526
527 // Load room with custom collision data
528 zelda3::Room room = zelda3::LoadRoomFromRom(rom, room_id);
529
530 formatter.BeginObject("Set Collision Tiles");
531 formatter.AddHexField("room_id", room_id, 2);
532 formatter.AddField("had_custom_collision", room.has_custom_collision());
533 formatter.AddField("tile_count", static_cast<int>(specs.size()));
534 formatter.AddField("mode", do_write ? "write" : "dry-run");
535
536 // Apply each tile change
537 formatter.BeginArray("changes");
538 for (const auto& spec : specs) {
539 uint8_t old_value = room.GetCollisionTile(spec.x, spec.y);
540 room.SetCollisionTile(spec.x, spec.y, static_cast<uint8_t>(spec.tile));
541
542 formatter.BeginObject();
543 formatter.AddField("x", spec.x);
544 formatter.AddField("y", spec.y);
545 formatter.AddHexField("old_tile", old_value, 2);
546 formatter.AddHexField("new_tile", spec.tile, 2);
547 formatter.EndObject();
548 }
549 formatter.EndArray();
550
551 if (do_write) {
552 // Flush collision changes to ROM
553 std::array<zelda3::Room, 1> rooms_arr = {std::move(room)};
554 auto save_status = zelda3::SaveAllCollision(rom, absl::MakeSpan(rooms_arr));
555 if (!save_status.ok()) {
556 formatter.AddField("write_error", std::string(save_status.message()));
557 formatter.EndObject();
558 return save_status;
559 }
560 formatter.AddField("write_status", "success");
561
562 auto disk_status = SaveRomWithBackup(rom, formatter);
563 if (!disk_status.ok()) {
564 formatter.EndObject();
565 return disk_status;
566 }
567 }
568
569 formatter.EndObject();
570 return absl::OkStatus();
571}
572
573} // namespace handlers
574} // namespace cli
575} // 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
absl::Status SaveToFile(const SaveSettings &settings)
Definition rom.cc:291
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.
static int DetermineObjectType(uint8_t b1, uint8_t b3)
uint8_t GetCollisionTile(int x, int y) const
Definition room.h:394
absl::Status SaveObjects()
Definition room.cc:1681
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:214
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:314
absl::Status SaveSprites()
Definition room.cc:1745
void SetCollisionTile(int x, int y, uint8_t tile)
Definition room.h:399
void LoadSprites()
Definition room.cc:1980
absl::Status AddObject(const RoomObject &object)
Definition room.cc:1872
bool has_custom_collision() const
Definition room.h:386
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< int > GetRequiredInt(const resources::ArgumentParser &parser, const char *name)
absl::Status SaveRomWithBackup(Rom *rom, resources::OutputFormatter &formatter)
bool ParseHexString(absl::string_view str, int *out)
Definition hex_util.h:17
Room LoadRoomHeaderFromRom(Rom *rom, int room_id)
Definition room.cc:274
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:253
std::string GetObjectName(int object_id)
constexpr int kNumberOfRooms
absl::Status SaveAllCollision(Rom *rom, absl::Span< Room > rooms)
Definition room.cc:2370
const char * ResolveSpriteName(uint16_t id)
Definition sprite.cc:284
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22