yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_selection_edit.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstdint>
5#include <limits>
6#include <utility>
7
8#include "absl/strings/str_format.h"
10#include "util/macro.h"
14
15namespace yaze::editor {
16namespace {
17
18template <typename T>
19void AppendUnique(std::vector<T>& values, const T& value) {
20 if (std::find(values.begin(), values.end(), value) == values.end()) {
21 values.push_back(value);
22 }
23}
24
25absl::Status NormalizeSelection(
26 const DungeonSelectionEditState& state, const std::vector<size_t>& objects,
27 const std::vector<SelectedEntity>& entities,
28 std::vector<size_t>& selected_objects,
29 std::vector<SelectedEntity>& selected_entities) {
30 for (size_t index : objects) {
31 if (index >= state.objects.size()) {
32 return absl::OutOfRangeError("Selected object is no longer available");
33 }
34 AppendUnique(selected_objects, index);
35 }
36 for (const auto& entity : entities) {
37 size_t count = 0;
38 switch (entity.type) {
39 case EntityType::Object:
40 if (entity.index >= state.objects.size()) {
41 return absl::OutOfRangeError(
42 "Selected object is no longer available");
43 }
44 AppendUnique(selected_objects, entity.index);
45 continue;
46 case EntityType::Door:
47 count = state.doors.size();
48 break;
49 case EntityType::Sprite:
50 count = state.sprites.size();
51 break;
52 case EntityType::Item:
53 count = state.items.size();
54 break;
55 case EntityType::None:
56 return absl::InvalidArgumentError(
57 "Selection contains an invalid entity");
58 default:
59 return absl::InvalidArgumentError(
60 "Selection contains an unknown entity");
61 }
62 if (entity.index >= count) {
63 return absl::OutOfRangeError("Selected entity is no longer available");
64 }
65 AppendUnique(selected_entities, entity);
66 }
67 return absl::OkStatus();
68}
69
70template <typename T, typename Equal>
71bool SameVector(const std::vector<T>& a, const std::vector<T>& b, Equal equal) {
72 return a.size() == b.size() &&
73 std::equal(a.begin(), a.end(), b.begin(), equal);
74}
75
76bool SameChests(const std::vector<chest_data>& a,
77 const std::vector<chest_data>& b) {
78 return SameVector(a, b, [](const auto& x, const auto& y) {
79 return x.id == y.id && x.size == y.size;
80 });
81}
82
83bool SameObjects(const std::vector<zelda3::RoomObject>& a,
84 const std::vector<zelda3::RoomObject>& b) {
85 return SameVector(a, b, [](const auto& x, const auto& y) {
86 return x.id_ == y.id_ && x.x_ == y.x_ && x.y_ == y.y_ &&
87 x.size_ == y.size_ && x.layer_ == y.layer_ &&
88 x.options() == y.options() && x.all_bgs_ == y.all_bgs_ &&
89 x.lit_ == y.lit_ && x.block_load_order() == y.block_load_order() &&
90 x.block_behavior_layer() == y.block_behavior_layer() &&
91 x.torch_reserved_bit() == y.torch_reserved_bit();
92 });
93}
94
95absl::StatusOr<int> TranslateCoordinate(int64_t coordinate, int delta, int step,
96 int maximum, const char* label) {
97 const int64_t result = coordinate + delta;
98 if (result < 0 || result > maximum || result % step != 0) {
99 return absl::InvalidArgumentError(absl::StrFormat(
100 "%s cannot move to pixel %lld; expected 0..%d in %d-pixel steps", label,
101 static_cast<long long>(result), maximum, step));
102 }
103 return static_cast<int>(result);
104}
105
106absl::Status TranslateObject(zelda3::RoomObject& object, int dx, int dy) {
107 ASSIGN_OR_RETURN(const int x,
108 TranslateCoordinate(object.x_ * 8, dx, 8, 504, "Object X"));
109 ASSIGN_OR_RETURN(const int y,
110 TranslateCoordinate(object.y_ * 8, dy, 8, 504, "Object Y"));
111 object.x_ = x / 8;
112 object.y_ = y / 8;
113 if (zelda3::UsesRoomObjectStream(object)) {
115 }
116 if (object.GetLayerValue() > 1) {
117 return absl::InvalidArgumentError("Special objects require layer 0 or 1");
118 }
119 if ((object.options() & zelda3::ObjectOption::Torch) !=
121 (object.x_ > 62 || object.y_ > 62)) {
122 return absl::InvalidArgumentError("Torch coordinates must be in 0..62");
123 }
124 return absl::OkStatus();
125}
126
127absl::Status TranslateDoor(zelda3::Room::Door& door, int dx, int dy) {
128 const auto type = static_cast<unsigned>(door.type);
129 if (type > 0x66 || (type & 1) != 0 ||
130 static_cast<unsigned>(door.direction) > 3 ||
132 door.direction)) {
133 return absl::InvalidArgumentError(
134 "Selected door has invalid ROM properties");
135 }
136 // Preserve the original bytes and exact slot for an unchanged anchor.
137 if (dx == 0 && dy == 0)
138 return absl::OkStatus();
139 const auto [x, y] = door.GetPixelCoords();
140 const int64_t target_x = static_cast<int64_t>(x) + dx;
141 const int64_t target_y = static_cast<int64_t>(y) + dy;
142 // Translating a selection does not rotate doors. Each direction's usable
143 // anchors are drawn from the ROM tables, not the legacy 32-slot header hint.
144 for (uint8_t position = 0; position < 16; ++position) {
146 continue;
147 const auto [next_x, next_y] =
149 door.direction);
150 if (next_x == target_x && next_y == target_y) {
151 door.position = position;
152 const auto [b1, b2] = door.EncodeBytes();
153 door.byte1 = b1;
154 door.byte2 = b2;
155 return absl::OkStatus();
156 }
157 }
158 return absl::InvalidArgumentError(
159 "Door has no valid slot at the requested position; the selection was not "
160 "moved");
161}
162
163absl::Status TranslateSprite(DungeonSpriteSnapshot& sprite, int dx, int dy) {
164 ASSIGN_OR_RETURN(const int x,
165 TranslateCoordinate(static_cast<int64_t>(sprite.x) * 16, dx,
166 16, 496, "Sprite X"));
167 ASSIGN_OR_RETURN(const int y,
168 TranslateCoordinate(static_cast<int64_t>(sprite.y) * 16, dy,
169 16, 496, "Sprite Y"));
170 sprite.x = x / 16;
171 sprite.y = y / 16;
173 sprite.id, sprite.x, sprite.y, sprite.subtype, sprite.layer,
174 sprite.key_drop);
175}
176
177absl::Status TranslateItem(zelda3::PotItem& item, int dx, int dy) {
178 ASSIGN_OR_RETURN(const int x, TranslateCoordinate(item.GetPixelX(), dx, 8,
179 504, "Pot item X"));
180 ASSIGN_OR_RETURN(const int y, TranslateCoordinate(item.GetPixelY(), dy, 8,
181 504, "Pot item Y"));
182 const auto encoded = zelda3::EncodePotItemPosition(x, y, item.position);
183 if (!encoded)
184 return absl::InvalidArgumentError("Invalid pot item position");
185 item.position = *encoded;
186 return absl::OkStatus();
187}
188
189absl::Status CheckGrowth(size_t before, size_t after, size_t maximum,
190 const char* label) {
191 if (after > maximum && after > before) {
192 return absl::ResourceExhaustedError(
193 absl::StrFormat("%s limit reached (%zu maximum)", label, maximum));
194 }
195 return absl::OkStatus();
196}
197
198template <typename T>
199void RemoveIndices(std::vector<T>& values, std::vector<size_t> indices) {
200 std::sort(indices.begin(), indices.end(), std::greater<size_t>());
201 for (size_t index : indices)
202 values.erase(values.begin() + index);
203}
204
205std::vector<size_t> EntityIndices(const std::vector<SelectedEntity>& entities,
206 EntityType type) {
207 std::vector<size_t> indices;
208 for (const auto& entity : entities) {
209 if (entity.type == type)
210 indices.push_back(entity.index);
211 }
212 return indices;
213}
214
215} // namespace
216
218 const zelda3::Room& room) {
220 state.objects = room.GetTileObjects();
221 state.chests = room.GetChests();
222 state.doors = room.GetDoors();
223 state.items = room.GetPotItems();
224 for (const auto& sprite : room.GetSprites()) {
225 state.sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
226 sprite.subtype(), sprite.layer(),
227 sprite.key_drop(), sprite.deleted()});
228 }
229 return state;
230}
231
233 const DungeonSelectionEditState& b) {
234 uint8_t domains = 0;
235 if (!SameObjects(a.objects, b.objects) || !SameChests(a.chests, b.chests))
236 domains |= kSelectionObjects;
237 if (!SameVector(a.doors, b.doors, [](const auto& x, const auto& y) {
238 return x.position == y.position && x.type == y.type &&
239 x.direction == y.direction && x.byte1 == y.byte1 &&
240 x.byte2 == y.byte2;
241 }))
242 domains |= kSelectionDoors;
243 if (a.sprites != b.sprites)
244 domains |= kSelectionSprites;
245 if (!SameVector(a.items, b.items, [](const auto& x, const auto& y) {
246 return x.position == y.position && x.item == y.item;
247 }))
248 domains |= kSelectionItems;
249 return domains;
250}
251
252absl::StatusOr<DungeonSelectionClipboard> CopyDungeonSelection(
253 const zelda3::Room& room, const std::vector<size_t>& objects,
254 const std::vector<SelectedEntity>& entities) {
255 const auto state = CaptureDungeonSelectionEditState(room);
256 std::vector<size_t> selected_objects;
257 std::vector<SelectedEntity> selected_entities;
258 RETURN_IF_ERROR(NormalizeSelection(state, objects, entities, selected_objects,
259 selected_entities));
261 int min_x = std::numeric_limits<int>::max();
262 int min_y = std::numeric_limits<int>::max();
263 const auto include_anchor = [&](int x, int y) {
264 min_x = std::min(min_x, x);
265 min_y = std::min(min_y, y);
266 };
267 bool checked_chests = false;
268 for (size_t index : selected_objects) {
269 const auto& object = state.objects[index];
270 std::optional<chest_data> chest;
271 if (zelda3::UsesRoomObjectStream(object) &&
273 if (!checked_chests) {
275 zelda3::ValidateChestObjectMapping(state.objects, state.chests));
276 checked_chests = true;
277 }
278 const auto ordinal = zelda3::ChestIndexForObject(state.objects, index);
279 if (!ordinal || *ordinal >= state.chests.size()) {
280 return absl::FailedPreconditionError(
281 "Selected chest has no contents record");
282 }
283 chest = state.chests[*ordinal];
284 }
285 clipboard.objects.push_back(object);
286 clipboard.object_chests.push_back(chest);
287 include_anchor(object.x_ * 8, object.y_ * 8);
288 }
289 for (const auto& entity : selected_entities) {
290 switch (entity.type) {
291 case EntityType::Door: {
292 const auto& door = state.doors[entity.index];
293 clipboard.doors.push_back(door);
294 const auto [x, y] = door.GetPixelCoords();
295 include_anchor(x, y);
296 break;
297 }
298 case EntityType::Sprite: {
299 const auto& sprite = state.sprites[entity.index];
300 clipboard.sprites.push_back(sprite);
301 include_anchor(sprite.x * 16, sprite.y * 16);
302 break;
303 }
304 case EntityType::Item: {
305 const auto& item = state.items[entity.index];
306 clipboard.items.push_back(item);
307 include_anchor(item.GetPixelX(), item.GetPixelY());
308 break;
309 }
310 default:
311 break; // NormalizeSelection accepts only the domains above.
312 }
313 }
314 if (!clipboard.empty()) {
315 clipboard.origin_pixel_x = min_x;
316 clipboard.origin_pixel_y = min_y;
317 }
318 return clipboard;
319}
320
321absl::StatusOr<DungeonSelectionEditPlan> PlanDungeonSelectionEdit(
322 const zelda3::Room& room, const DungeonSelectionEditRequest& request) {
324 plan.room_id = room.id();
325 plan.kind = request.kind;
327 RETURN_IF_ERROR(NormalizeSelection(
328 plan.before, request.objects, request.entities,
330 plan.after = plan.before;
331 auto& after = plan.after;
332 const int dx = request.delta_x_pixels;
333 const int dy = request.delta_y_pixels;
334 std::vector<std::optional<size_t>> sources;
335 for (size_t i = 0; i < after.objects.size(); ++i)
336 sources.push_back(i);
337 std::vector<std::optional<chest_data>> overrides(after.objects.size());
338 switch (request.kind) {
340 RemoveIndices(after.objects, plan.before.selected_objects);
341 RemoveIndices(sources, plan.before.selected_objects);
342 overrides.resize(after.objects.size());
343 RemoveIndices(after.doors, EntityIndices(plan.before.selected_entities,
344 EntityType::Door));
345 RemoveIndices(after.sprites, EntityIndices(plan.before.selected_entities,
346 EntityType::Sprite));
347 RemoveIndices(after.items, EntityIndices(plan.before.selected_entities,
348 EntityType::Item));
349 after.selected_objects.clear();
350 after.selected_entities.clear();
351 break;
353 if (dx == 0 && dy == 0)
354 return plan;
355 for (size_t index : after.selected_objects) {
356 RETURN_IF_ERROR(TranslateObject(after.objects[index], dx, dy));
357 }
358 for (const auto& entity : after.selected_entities) {
359 if (entity.type == EntityType::Door) {
360 RETURN_IF_ERROR(TranslateDoor(after.doors[entity.index], dx, dy));
361 } else if (entity.type == EntityType::Sprite) {
362 RETURN_IF_ERROR(TranslateSprite(after.sprites[entity.index], dx, dy));
363 } else {
364 RETURN_IF_ERROR(TranslateItem(after.items[entity.index], dx, dy));
365 }
366 }
367 break;
371 const DungeonSelectionClipboard* clipboard = request.clipboard;
376 clipboard = &copied;
377 }
378 if (!clipboard)
379 return absl::InvalidArgumentError("Clipboard is unavailable");
380 if (clipboard->object_chests.size() != clipboard->objects.size()) {
381 return absl::InvalidArgumentError(
382 "Clipboard chest metadata is incomplete");
383 }
384 after.selected_objects.clear();
385 after.selected_entities.clear();
386 for (size_t i = 0; i < clipboard->objects.size(); ++i) {
387 auto object = clipboard->objects[i].CopyForNewPlacement();
388 const bool is_chest = zelda3::UsesRoomObjectStream(object) &&
390 if (is_chest != clipboard->object_chests[i].has_value()) {
391 return absl::InvalidArgumentError(
392 "Clipboard chest contents do not match its objects");
393 }
394 if (is_chest)
395 object.set_options(object.options() | zelda3::ObjectOption::Chest);
396 RETURN_IF_ERROR(TranslateObject(object, dx, dy));
397 object.SetRom(room.rom());
398 object.tiles_loaded_ = false;
399 after.selected_objects.push_back(after.objects.size());
400 after.objects.push_back(std::move(object));
401 sources.push_back(std::nullopt);
402 overrides.push_back(clipboard->object_chests[i]);
403 }
404 for (auto door : clipboard->doors) {
405 RETURN_IF_ERROR(TranslateDoor(door, dx, dy));
406 after.selected_entities.push_back(
407 {EntityType::Door, after.doors.size()});
408 after.doors.push_back(door);
409 }
410 for (auto sprite : clipboard->sprites) {
411 RETURN_IF_ERROR(TranslateSprite(sprite, dx, dy));
412 after.selected_entities.push_back(
413 {EntityType::Sprite, after.sprites.size()});
414 after.sprites.push_back(sprite);
415 }
416 for (auto item : clipboard->items) {
417 RETURN_IF_ERROR(TranslateItem(item, dx, dy));
418 after.selected_entities.push_back(
419 {EntityType::Item, after.items.size()});
420 after.items.push_back(item);
421 }
422 break;
423 }
424 default:
425 return absl::InvalidArgumentError("Unknown selection edit operation");
426 }
427 RETURN_IF_ERROR(CheckGrowth(plan.before.objects.size(), after.objects.size(),
428 zelda3::kMaxTileObjects, "Room object"));
429 RETURN_IF_ERROR(CheckGrowth(plan.before.doors.size(), after.doors.size(),
430 zelda3::kMaxDoors, "Door"));
431 RETURN_IF_ERROR(CheckGrowth(plan.before.sprites.size(), after.sprites.size(),
432 zelda3::kMaxTotalSprites, "Sprite"));
435 plan.before.objects, plan.before.chests,
436 after.objects, sources, overrides));
437 }
439 return plan;
440}
441
443 const DungeonSelectionEditState& state,
444 uint8_t domains) {
445 const auto current = CaptureDungeonSelectionEditState(room);
446 domains &= ChangedDungeonSelectionDomains(current, state);
447 if (domains & kSelectionObjects) {
448 if (!SameObjects(current.objects, state.objects)) {
449 room.SetTileObjects(state.objects);
450 }
451 if (!SameChests(current.chests, state.chests)) {
452 room.GetChests() = state.chests;
453 room.MarkChestsDirty();
454 }
455 }
456 if (domains & kSelectionDoors) {
457 room.GetDoors() = state.doors;
459 }
460 if (domains & kSelectionSprites) {
461 std::vector<zelda3::Sprite> sprites;
462 sprites.reserve(state.sprites.size());
463 for (const auto& source : state.sprites) {
464 zelda3::Sprite sprite(source.id, static_cast<uint8_t>(source.x),
465 static_cast<uint8_t>(source.y),
466 static_cast<uint8_t>(source.subtype),
467 static_cast<uint8_t>(source.layer));
468 sprite.set_key_drop(source.key_drop);
469 sprite.set_deleted(source.deleted);
470 sprites.push_back(std::move(sprite));
471 }
472 room.GetSprites() = std::move(sprites);
473 room.MarkSpritesDirty();
474 }
475 if (domains & kSelectionItems) {
476 room.GetPotItems() = state.items;
477 room.MarkPotItemsDirty();
478 }
479}
480
481} // namespace yaze::editor
static absl::Status ValidateSpriteProperties(uint8_t id, int x, int y, int subtype, int layer, int key_drop)
static std::pair< int, int > PositionToPixelCoords(uint8_t position, DoorDirection direction)
Convert encoded position to pixel coordinates.
static bool IsValidPosition(uint8_t position, DoorDirection direction)
Check if a position is valid for door placement.
void MarkPotItemsDirty()
Definition room.h:432
void MarkChestsDirty()
Definition room.h:326
const std::vector< chest_data > & GetChests() const
Definition room.h:323
const std::vector< Door > & GetDoors() const
Definition room.h:413
void MarkSpritesDirty()
Definition room.h:319
auto rom() const
Definition room.h:1082
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
void MarkObjectStreamDirty()
Definition room.h:476
void SetTileObjects(const std::vector< RoomObject > &objects)
Definition room.h:721
const std::vector< PotItem > & GetPotItems() const
Definition room.h:429
int id() const
Definition room.h:1012
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
auto set_key_drop(int key)
Definition sprite.h:134
auto set_deleted(bool deleted)
Definition sprite.h:133
struct chest chest
Treasure chest.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
bool SameChests(const std::vector< chest_data > &a, const std::vector< chest_data > &b)
absl::StatusOr< int > TranslateCoordinate(int64_t coordinate, int delta, int step, int maximum, const char *label)
absl::Status TranslateDoor(zelda3::Room::Door &door, int dx, int dy)
absl::Status TranslateItem(zelda3::PotItem &item, int dx, int dy)
absl::Status CheckGrowth(size_t before, size_t after, size_t maximum, const char *label)
bool SameObjects(const std::vector< zelda3::RoomObject > &a, const std::vector< zelda3::RoomObject > &b)
void AppendUnique(std::vector< T > &values, const T &value)
absl::Status NormalizeSelection(const DungeonSelectionEditState &state, const std::vector< size_t > &objects, const std::vector< SelectedEntity > &entities, std::vector< size_t > &selected_objects, std::vector< SelectedEntity > &selected_entities)
absl::Status TranslateObject(zelda3::RoomObject &object, int dx, int dy)
std::vector< size_t > EntityIndices(const std::vector< SelectedEntity > &entities, EntityType type)
void RemoveIndices(std::vector< T > &values, std::vector< size_t > indices)
bool SameVector(const std::vector< T > &a, const std::vector< T > &b, Equal equal)
absl::Status TranslateSprite(DungeonSpriteSnapshot &sprite, int dx, int dy)
Editors are the view controllers for the application.
absl::StatusOr< DungeonSelectionClipboard > CopyDungeonSelection(const zelda3::Room &room, const std::vector< size_t > &objects, const std::vector< SelectedEntity > &entities)
absl::StatusOr< DungeonSelectionEditPlan > PlanDungeonSelectionEdit(const zelda3::Room &room, const DungeonSelectionEditRequest &request)
DungeonSelectionEditState CaptureDungeonSelectionEditState(const zelda3::Room &room)
constexpr uint8_t kSelectionSprites
constexpr uint8_t kSelectionObjects
uint8_t ChangedDungeonSelectionDomains(const DungeonSelectionEditState &a, const DungeonSelectionEditState &b)
constexpr uint8_t kSelectionDoors
void ApplyDungeonSelectionEditState(zelda3::Room &room, const DungeonSelectionEditState &state, uint8_t domains)
EntityType
Type of entity that can be selected in the dungeon editor.
constexpr uint8_t kSelectionItems
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
Definition chest_edit.cc:74
std::optional< size_t > ChestIndexForObject(const std::vector< RoomObject > &objects, size_t object_index)
Definition chest_edit.cc:63
constexpr size_t kMaxTileObjects
constexpr std::optional< uint16_t > EncodePotItemPosition(int x, int y, uint16_t original=0)
bool UsesRoomObjectStream(const RoomObject &object)
constexpr size_t kMaxDoors
constexpr bool IsStatefulChestObjectId(int object_id)
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
constexpr size_t kMaxTotalSprites
absl::StatusOr< std::vector< chest_data > > PlanChestObjectEdit(const std::vector< RoomObject > &before_objects, const std::vector< chest_data > &before_chests, const std::vector< RoomObject > &after_objects, const std::vector< std::optional< size_t > > &source_indices, const std::vector< std::optional< chest_data > > &chest_overrides)
Definition chest_edit.cc:94
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Treasure chest.
Definition zelda.h:425
std::vector< zelda3::Room::Door > doors
std::vector< zelda3::RoomObject > objects
std::vector< DungeonSpriteSnapshot > sprites
std::vector< std::optional< chest_data > > object_chests
const DungeonSelectionClipboard * clipboard
std::vector< SelectedEntity > selected_entities
std::vector< zelda3::Room::Door > doors
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::RoomObject > objects
int GetPixelX() const
Definition room.h:144
int GetPixelY() const
Definition room.h:145
uint16_t position
Definition room.h:140
Represents a door in a dungeon room.
Definition room.h:339
uint8_t byte1
Original ROM byte 1 (position data)
Definition room.h:344
DoorType type
Door type (determines appearance/behavior)
Definition room.h:341
std::pair< uint8_t, uint8_t > EncodeBytes() const
Encode door data for ROM storage.
Definition room.h:389
DoorDirection direction
Which wall the door is on.
Definition room.h:342
uint8_t position
Encoded position (5-bit, 0-31)
Definition room.h:340
std::pair< int, int > GetPixelCoords() const
Get pixel coordinates for this door.
Definition room.h:353
uint8_t byte2
Original ROM byte 2 (type + direction)
Definition room.h:345