yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
entity_operations.cc
Go to the documentation of this file.
1#include "entity_operations.h"
2
3#include <algorithm>
4#include <cmath>
5#include <cstdint>
6#include <utility>
7
8#include "absl/strings/str_format.h"
9#include "util/log.h"
10
11namespace yaze {
12namespace editor {
13namespace {
14
17 int x;
18 int y;
19 uint8_t game_x;
20 uint8_t game_y;
21};
22
23absl::StatusOr<EntityInsertionPosition> ResolveEntityInsertionPosition(
24 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
25 int current_map) {
26 if (current_map < 0 || current_map >= zelda3::kExpandedMapCount ||
27 current_map >= static_cast<int>(maps.size())) {
28 return absl::InvalidArgumentError("Invalid entity insertion map");
29 }
30 if (!std::isfinite(world_position.x) || !std::isfinite(world_position.y) ||
31 world_position.x < 0 || world_position.y < 0 ||
32 world_position.x >= 4096 || world_position.y >= 4096) {
33 return absl::InvalidArgumentError("Invalid entity insertion position");
34 }
35
36 const int x = static_cast<int>(world_position.x) / 16 * 16;
37 const int y = static_cast<int>(world_position.y) / 16 * 16;
38 const int world = current_map / 64;
39 const int screen = world * 64 + x / 512 + (y / 512) * 8;
40 if (screen != current_map) {
41 return absl::InvalidArgumentError(
42 "Entity insertion position does not belong to the selected screen");
43 }
44
45 const int raw_parent = maps[current_map].parent();
46 const int parent = raw_parent == 0xFF ? current_map : raw_parent;
47 if (parent < 0 || parent >= zelda3::kExpandedMapCount ||
48 parent >= static_cast<int>(maps.size()) || parent / 64 != world) {
49 return absl::InvalidArgumentError("Invalid entity insertion parent map");
50 }
51 const int parent_parent = maps[parent].parent();
52 if (parent_parent != parent && parent_parent != 0xFF) {
53 return absl::InvalidArgumentError(
54 "Entity insertion parent is not a root map");
55 }
56
57 // Game coordinates span the entire parent area, including the other screens
58 // of a large/wide/tall area. Taking world_position modulo 512 loses that
59 // quadrant offset and causes items to move after save/reload.
60 const int game_x = (x - (parent % 8) * 512) / 16;
61 const int game_y = (y - ((parent % 64) / 8) * 512) / 16;
62 if (game_x < 0 || game_x > 63 || game_y < 0 || game_y > 63) {
63 return absl::InvalidArgumentError(
64 "Entity insertion position is outside the parent coordinate range");
65 }
66 return EntityInsertionPosition{parent, x, y, static_cast<uint8_t>(game_x),
67 static_cast<uint8_t>(game_y)};
68}
69
71 const zelda3::OverworldItem& rhs) {
72 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
73 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.game_x_ == rhs.game_x_ &&
74 lhs.game_y_ == rhs.game_y_ && lhs.bg2_ == rhs.bg2_;
75}
76
77absl::Status ValidateRecordPosition(const zelda3::Overworld& overworld,
78 int map_id, int x, int y) {
79 const auto* parent = overworld.overworld_map(map_id);
80 if (map_id < 0 || map_id >= zelda3::kExpandedMapCount || !parent) {
81 return absl::InvalidArgumentError("The record's map is not available");
82 }
83 if (parent->parent() != map_id && parent->parent() != 0xFF) {
84 return absl::InvalidArgumentError("Choose the area's parent map");
85 }
86 if (x < 0 || y < 0 || x >= 4096 || y >= 4096) {
87 return absl::InvalidArgumentError("Position must be within 0-4095 pixels");
88 }
89 const auto size = parent->area_size();
90 const int width = (size == zelda3::AreaSizeEnum::LargeArea ||
92 ? 1024
93 : 512;
94 const int height = (size == zelda3::AreaSizeEnum::LargeArea ||
96 ? 1024
97 : 512;
98 const int local_x = x - (map_id % 8) * 512;
99 const int local_y = y - ((map_id % 64) / 8) * 512;
100 const int screen = (map_id / 64) * 64 + x / 512 + (y / 512) * 8;
101 const auto* page = overworld.overworld_map(screen);
102 const int page_parent =
103 page ? (page->parent() == 0xFF ? screen : page->parent()) : -1;
104 if (local_x < 0 || local_y < 0 || local_x >= width || local_y >= height ||
105 screen / 64 != map_id / 64 || !page || page_parent != map_id) {
106 return absl::InvalidArgumentError(
107 "Position does not belong to the selected parent area");
108 }
109 return absl::OkStatus();
110}
111
112} // namespace
113
114absl::StatusOr<zelda3::OverworldEntrance> BuildOverworldEntranceEdit(
115 const zelda3::Overworld& overworld,
116 const std::vector<zelda3::OverworldEntrance>& records, size_t row,
117 const zelda3::OverworldEntrance& draft, bool legacy_hole_table) {
118 if (row >= records.size()) {
119 return absl::NotFoundError("Entrance row is no longer available");
120 }
121 if (draft.is_hole_ != records[row].is_hole_) {
122 return absl::InvalidArgumentError("An edit cannot change the record kind");
123 }
124 auto value = draft;
126 value.entity_id_ = static_cast<int>(row);
127 if (value.deleted) {
128 value.map_pos_ = 0xFFFF;
129 return value;
130 }
132 ValidateRecordPosition(overworld, value.map_id_, value.x_, value.y_));
133 if (value.x_ % 16 != 0 || value.y_ % 16 != 0) {
134 return absl::InvalidArgumentError("Entrance position must align to a tile");
135 }
136 const auto& before = records[row];
137 if (value.map_id_ != before.map_id_ || value.x_ != before.x_ ||
138 value.y_ != before.y_) {
139 value.UpdateMapProperties(value.map_id_, &overworld);
140 } else {
141 value.map_pos_ = before.map_pos_;
142 value.game_x_ = before.game_x_;
143 value.game_y_ = before.game_y_;
144 }
145 // The legacy hole decoder adds $400. Its existing non-wrapping codec
146 // cannot round-trip positions in the first eight local tile rows.
147 if (legacy_hole_table && value.map_pos_ < 0x400) {
148 return absl::InvalidArgumentError(
149 "Legacy hole positions must start at local tile row 8");
150 }
151 for (size_t i = row + 1; i < records.size(); ++i) {
152 const auto& other = records[i];
153 if (!other.deleted && other.map_id_ == value.map_id_ &&
154 other.map_pos_ == value.map_pos_ && other.is_hole_ == value.is_hole_) {
155 return absl::FailedPreconditionError(absl::StrFormat(
156 "Entrance row %d is shadowed by row %d at this position", row, i));
157 }
158 }
159 return value;
160}
161
162absl::StatusOr<zelda3::OverworldExit> BuildOverworldExitEdit(
163 const zelda3::Overworld& overworld,
164 const std::vector<zelda3::OverworldExit>& records, size_t row,
165 const zelda3::OverworldExit& draft) {
166 if (row >= records.size()) {
167 return absl::NotFoundError("Return row is no longer available");
168 }
169 if (draft.is_hole_ != records[row].is_hole_) {
170 return absl::InvalidArgumentError("An edit cannot change the record kind");
171 }
172 auto value = draft;
174 value.entity_id_ = static_cast<int>(row);
175 if (value.deleted_) {
176 // The runtime scans room IDs, not the editor's deleted marker. Retire
177 // this authored row's dispatch key as well as its player coordinates.
178 value.room_id_ = 0xFFFF;
179 value.x_player_ = value.y_player_ = 0xFFFF;
180 return value;
181 }
182 if (value.room_id_ == 0xFFFF) {
183 return absl::InvalidArgumentError("Choose a room for this return row");
184 }
186 ValidateRecordPosition(overworld, value.map_id_, value.x_, value.y_));
187 const auto& before = records[row];
188 const bool geometry_changed = value.map_id_ != before.map_id_ ||
189 value.x_ != before.x_ || value.y_ != before.y_;
190 const bool scroll_changed = value.x_scroll_ != before.x_scroll_ ||
191 value.y_scroll_ != before.y_scroll_;
192 const bool camera_changed = value.x_camera_ != before.x_camera_ ||
193 value.y_camera_ != before.y_camera_;
194 // Automatic is an editor preference, not a persisted ROM bit. An untouched
195 // loaded row must retain its authored camera/VRAM rather than normalize it.
196 if (geometry_changed || value.is_automatic_ != before.is_automatic_ ||
197 scroll_changed || (value.is_automatic_ && camera_changed)) {
198 value.UpdateMapProperties(value.map_id_, &overworld);
199 } else {
200 value.x_player_ = before.x_player_;
201 value.y_player_ = before.y_player_;
202 value.map_pos_ = before.map_pos_;
203 value.game_x_ = before.game_x_;
204 value.game_y_ = before.game_y_;
205 }
206 for (size_t i = row + 1; i < records.size(); ++i) {
207 // The native scan compares room IDs, including rows whose player
208 // coordinates contain the unused sentinel.
209 if (records[i].room_id_ == value.room_id_) {
210 return absl::FailedPreconditionError(absl::StrFormat(
211 "Return row %d is shadowed by row %d for this room", row, i));
212 }
213 }
214 return value;
215}
216
217absl::StatusOr<zelda3::OverworldItem> BuildOverworldItemForInsertion(
218 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
219 int current_map, uint8_t item_id) {
220 auto position =
221 ResolveEntityInsertionPosition(maps, world_position, current_map);
222 if (!position.ok()) {
223 return position.status();
224 }
225 // This constructor already derives game coordinates from the parent origin.
226 return zelda3::OverworldItem(item_id,
227 static_cast<uint16_t>(position->parent_map),
228 position->x, position->y, false);
229}
230
231absl::StatusOr<zelda3::Sprite> BuildOverworldSpriteForInsertion(
232 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
233 int current_map, uint8_t sprite_id) {
234 auto position =
235 ResolveEntityInsertionPosition(maps, world_position, current_map);
236 if (!position.ok()) {
237 return position.status();
238 }
239 static const std::vector<uint8_t> empty_gfx;
240 const auto& gfx = current_map < static_cast<int>(maps.size())
241 ? maps[current_map].current_graphics()
242 : empty_gfx;
243 return zelda3::Sprite(gfx, static_cast<uint8_t>(position->parent_map),
244 sprite_id, position->game_x, position->game_y,
245 position->x, position->y);
246}
247
248absl::StatusOr<zelda3::OverworldEntrance*> InsertEntrance(
249 zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
250 bool is_hole) {
251 if (!overworld || !overworld->is_loaded()) {
252 return absl::FailedPreconditionError("Overworld not loaded");
253 }
254
255 if (is_hole && overworld->expanded_entrances()) {
256 return absl::FailedPreconditionError(
257 "Expanded holes use existing combined entrance table rows");
258 }
259
260 // Snap to 16x16 grid and clamp to bounds (ZScream: EntranceMode.cs:86-87)
261 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
262
263 // Get parent map ID (ZScream: EntranceMode.cs:78-82)
264 auto* current_ow_map = overworld->overworld_map(current_map);
265 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
266
267 if (is_hole) {
268 // Search for first deleted hole slot (ZScream: EntranceMode.cs:74-100)
269 auto& holes = overworld->holes();
270 for (size_t i = 0; i < holes.size(); ++i) {
271 if (holes[i].deleted) {
272 // Reuse deleted slot
273 holes[i].deleted = false;
274 holes[i].map_id_ = map_id;
275 holes[i].x_ = static_cast<int>(snapped_pos.x);
276 holes[i].y_ = static_cast<int>(snapped_pos.y);
277 holes[i].entrance_id_ = 0; // Default, user configures in popup
278 holes[i].is_hole_ = true;
279
280 // Update map properties (ZScream: EntranceMode.cs:90)
281 holes[i].UpdateMapProperties(map_id, overworld);
282
283 LOG_DEBUG("EntityOps",
284 "Inserted hole at slot %zu: pos=(%d,%d) map=0x%02X", i,
285 holes[i].x_, holes[i].y_, map_id);
286
287 return &holes[i];
288 }
289 }
290 return absl::ResourceExhaustedError(
291 "No space available for new hole. Delete one first.");
292
293 } else {
294 // Search for first deleted entrance slot (ZScream: EntranceMode.cs:104-130)
295 auto* entrances = overworld->mutable_entrances();
296 for (size_t i = 0; i < entrances->size(); ++i) {
297 if (entrances->at(i).deleted) {
298 // Reuse deleted slot
299 entrances->at(i).deleted = false;
300 entrances->at(i).map_id_ = map_id;
301 entrances->at(i).x_ = static_cast<int>(snapped_pos.x);
302 entrances->at(i).y_ = static_cast<int>(snapped_pos.y);
303 entrances->at(i).entrance_id_ = 0; // Default, user configures in popup
304 entrances->at(i).is_hole_ = false;
305
306 // Update map properties (ZScream: EntranceMode.cs:120)
307 entrances->at(i).UpdateMapProperties(map_id, overworld);
308
309 LOG_DEBUG("EntityOps",
310 "Inserted entrance at slot %zu: pos=(%d,%d) map=0x%02X", i,
311 entrances->at(i).x_, entrances->at(i).y_, map_id);
312
313 return &entrances->at(i);
314 }
315 }
316 return absl::ResourceExhaustedError(
317 "No space available for new entrance. Delete one first.");
318 }
319}
320
321absl::StatusOr<zelda3::OverworldExit*> InsertExit(zelda3::Overworld* overworld,
322 ImVec2 mouse_pos,
323 int current_map) {
324 if (!overworld || !overworld->is_loaded()) {
325 return absl::FailedPreconditionError("Overworld not loaded");
326 }
327
328 // Snap to 16x16 grid and clamp to bounds (ZScream: ExitMode.cs:71-72)
329 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
330
331 // Get parent map ID (ZScream: ExitMode.cs:63-67)
332 auto* current_ow_map = overworld->overworld_map(current_map);
333 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
334
335 // Search for first deleted exit slot (ZScream: ExitMode.cs:59-124)
336 auto& exits = *overworld->mutable_exits();
337 for (size_t i = 0; i < exits.size(); ++i) {
338 if (exits[i].deleted_) {
339 // Reuse deleted slot
340 exits[i].deleted_ = false;
341 exits[i].map_id_ = map_id;
342 exits[i].x_ = static_cast<int>(snapped_pos.x);
343 exits[i].y_ = static_cast<int>(snapped_pos.y);
344
345 // Initialize with default values (ZScream: ExitMode.cs:95-112)
346 // User will configure room_id, scroll, camera in popup
347 exits[i].room_id_ = 0;
348 exits[i].x_scroll_ = 0;
349 exits[i].y_scroll_ = 0;
350 exits[i].x_camera_ = 0;
351 exits[i].y_camera_ = 0;
352 exits[i].x_player_ = static_cast<uint16_t>(snapped_pos.x);
353 exits[i].y_player_ = static_cast<uint16_t>(snapped_pos.y);
354 exits[i].scroll_mod_x_ = 0;
355 exits[i].scroll_mod_y_ = 0;
356 exits[i].door_type_1_ = 0;
357 exits[i].door_type_2_ = 0;
358
359 // Update map properties with overworld context for area size detection
360 exits[i].UpdateMapProperties(map_id, overworld);
361
362 LOG_DEBUG("EntityOps",
363 "Inserted exit at slot %zu: pos=(%d,%d) map=0x%02X", i,
364 exits[i].x_, exits[i].y_, map_id);
365
366 return &exits[i];
367 }
368 }
369
370 return absl::ResourceExhaustedError(
371 "No space available for new exit. Delete one first.");
372}
373
374absl::StatusOr<zelda3::Sprite*> InsertSprite(zelda3::Overworld* overworld,
375 ImVec2 mouse_pos, int current_map,
376 int game_state,
377 uint8_t sprite_id) {
378 if (!overworld || !overworld->is_loaded()) {
379 return absl::FailedPreconditionError("Overworld not loaded");
380 }
381
382 if (game_state < 0 || game_state > 2) {
383 return absl::InvalidArgumentError("Invalid game state (must be 0-2)");
384 }
385
387 overworld->overworld_maps(), mouse_pos, current_map, sprite_id);
388 if (!sprite.ok()) {
389 return sprite.status();
390 }
391
392 // Add new sprite to the game state array (ZScream: SpriteMode.cs:34-35)
393 auto& sprites = *overworld->mutable_sprites(game_state);
394
395 sprites.push_back(std::move(*sprite));
396
397 // Return pointer to the newly added sprite
398 zelda3::Sprite* inserted_sprite = &sprites.back();
399
400 LOG_DEBUG(
401 "EntityOps",
402 "Inserted sprite at game_state=%d: pos=(%d,%d) map=0x%02X id=0x%02X",
403 game_state, inserted_sprite->x_, inserted_sprite->y_,
404 inserted_sprite->map_id(), sprite_id);
405
406 return inserted_sprite;
407}
408
409absl::StatusOr<zelda3::OverworldItem*> InsertItem(zelda3::Overworld* overworld,
410 ImVec2 mouse_pos,
411 int current_map,
412 uint8_t item_id) {
413 if (!overworld || !overworld->is_loaded()) {
414 return absl::FailedPreconditionError("Overworld not loaded");
415 }
416
417 auto item = BuildOverworldItemForInsertion(overworld->overworld_maps(),
418 mouse_pos, current_map, item_id);
419 if (!item.ok()) {
420 return item.status();
421 }
422
423 // Add new item to the all_items array (ZScream: ItemMode.cs:92-108)
424 auto& items = *overworld->mutable_all_items();
425
426 items.push_back(std::move(*item));
427
428 zelda3::OverworldItem* inserted_item = &items.back();
429
430 LOG_DEBUG("EntityOps",
431 "Inserted item: pos=(%d,%d) game=(%d,%d) map=0x%02X id=0x%02X",
432 inserted_item->x_, inserted_item->y_, inserted_item->game_x_,
433 inserted_item->game_y_, inserted_item->room_map_id_, item_id);
434
435 return inserted_item;
436}
437
438absl::Status RemoveItem(zelda3::Overworld* overworld,
439 const zelda3::OverworldItem* item_ptr) {
440 if (!overworld) {
441 return absl::InvalidArgumentError("Overworld is null");
442 }
443 if (!item_ptr) {
444 return absl::InvalidArgumentError("Item pointer is null");
445 }
446
447 auto* items = overworld->mutable_all_items();
448 auto it = std::find_if(items->begin(), items->end(),
449 [item_ptr](const zelda3::OverworldItem& item) {
450 return &item == item_ptr;
451 });
452 if (it == items->end()) {
453 return absl::NotFoundError("Item pointer not found in overworld item list");
454 }
455
456 items->erase(it);
457 return absl::OkStatus();
458}
459
461 const zelda3::OverworldItem& item_identity) {
462 if (!overworld) {
463 return absl::InvalidArgumentError("Overworld is null");
464 }
465
466 auto* items = overworld->mutable_all_items();
467 auto it = std::find_if(items->begin(), items->end(),
468 [&item_identity](const zelda3::OverworldItem& item) {
469 return MatchesItemIdentity(item, item_identity);
470 });
471 if (it == items->end()) {
472 return absl::NotFoundError("No matching item identity in overworld list");
473 }
474
475 items->erase(it);
476 return absl::OkStatus();
477}
478
480 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity) {
481 if (!overworld) {
482 return nullptr;
483 }
484
485 auto* items = overworld->mutable_all_items();
486 auto it = std::find_if(items->begin(), items->end(),
487 [&item_identity](const zelda3::OverworldItem& item) {
488 return !item.deleted &&
489 MatchesItemIdentity(item, item_identity);
490 });
491 if (it == items->end()) {
492 return nullptr;
493 }
494 return &(*it);
495}
496
497absl::StatusOr<zelda3::OverworldItem*> DuplicateItemByIdentity(
498 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity,
499 int offset_x, int offset_y) {
500 if (!overworld) {
501 return absl::InvalidArgumentError("Overworld is null");
502 }
503
504 auto* source_item = FindItemByIdentity(overworld, item_identity);
505 if (!source_item) {
506 return absl::NotFoundError("No matching item identity in overworld list");
507 }
508
509 zelda3::OverworldItem duplicate = *source_item;
510 const ImVec2 clamped_pos = ClampToOverworldBounds(
511 ImVec2(static_cast<float>(source_item->x_ + offset_x),
512 static_cast<float>(source_item->y_ + offset_y)));
513 duplicate.x_ = static_cast<int>(clamped_pos.x);
514 duplicate.y_ = static_cast<int>(clamped_pos.y);
515 duplicate.deleted = false;
516 duplicate.UpdateMapProperties(duplicate.room_map_id_);
517
518 auto* items = overworld->mutable_all_items();
519 items->push_back(duplicate);
520 return &items->back();
521}
522
523absl::Status NudgeItem(zelda3::OverworldItem* item, int delta_x, int delta_y) {
524 if (!item) {
525 return absl::InvalidArgumentError("Item pointer is null");
526 }
527 const ImVec2 clamped_pos =
528 ClampToOverworldBounds(ImVec2(static_cast<float>(item->x_ + delta_x),
529 static_cast<float>(item->y_ + delta_y)));
530 item->x_ = static_cast<int>(clamped_pos.x);
531 item->y_ = static_cast<int>(clamped_pos.y);
533 return absl::OkStatus();
534}
535
537 zelda3::Overworld* overworld,
538 const zelda3::OverworldItem& anchor_identity) {
539 if (!overworld) {
540 return nullptr;
541 }
542
543 auto* items = overworld->mutable_all_items();
544 if (!items || items->empty()) {
545 return nullptr;
546 }
547
548 const int anchor_x = anchor_identity.x_;
549 const int anchor_y = anchor_identity.y_;
550 const uint16_t anchor_map = anchor_identity.room_map_id_;
551
552 auto rank = [&](const zelda3::OverworldItem& item) {
553 const int same_map_rank = (item.room_map_id_ == anchor_map) ? 0 : 1;
554 const std::int64_t dx = static_cast<std::int64_t>(item.x_) - anchor_x;
555 const std::int64_t dy = static_cast<std::int64_t>(item.y_) - anchor_y;
556 const std::int64_t dist2 = (dx * dx) + (dy * dy);
557 return std::pair<int, std::int64_t>(same_map_rank, dist2);
558 };
559
560 auto best_it = items->end();
561 std::pair<int, std::int64_t> best_rank = {2, 0};
562 for (auto it = items->begin(); it != items->end(); ++it) {
563 if (it->deleted) {
564 continue;
565 }
566 const auto current_rank = rank(*it);
567 if (best_it == items->end() || current_rank < best_rank) {
568 best_it = it;
569 best_rank = current_rank;
570 }
571 }
572
573 if (best_it == items->end()) {
574 return nullptr;
575 }
576 return &(*best_it);
577}
578
579} // namespace editor
580} // namespace yaze
enum yaze::zelda3::GameEntity::EntityType entity_type_
Represents an overworld exit that transitions from dungeon to overworld.
void UpdateMapProperties(uint16_t room_map_id, const void *context=nullptr) override
Update entity properties based on map position.
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:808
auto is_loaded() const
Definition overworld.h:836
const std::vector< OverworldMap > & overworld_maps() const
Definition overworld.h:767
auto overworld_map(int i) const
Definition overworld.h:770
auto expanded_entrances() const
Definition overworld.h:839
auto mutable_sprites(int state)
Definition overworld.h:792
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
auto map_id() const
Definition sprite.h:113
#define LOG_DEBUG(category, format,...)
Definition log.h:133
bool MatchesItemIdentity(const zelda3::OverworldItem &lhs, const zelda3::OverworldItem &rhs)
absl::StatusOr< EntityInsertionPosition > ResolveEntityInsertionPosition(const std::vector< zelda3::OverworldMap > &maps, ImVec2 world_position, int current_map)
absl::Status ValidateRecordPosition(const zelda3::Overworld &overworld, int map_id, int x, int y)
absl::StatusOr< zelda3::OverworldItem * > InsertItem(zelda3::Overworld *overworld, ImVec2 mouse_pos, int current_map, uint8_t item_id)
Insert a new item at the specified position.
absl::StatusOr< zelda3::OverworldExit > BuildOverworldExitEdit(const zelda3::Overworld &overworld, const std::vector< zelda3::OverworldExit > &records, size_t row, const zelda3::OverworldExit &draft)
absl::Status RemoveItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity)
Remove an item by value identity instead of pointer identity.
absl::StatusOr< zelda3::OverworldEntrance * > InsertEntrance(zelda3::Overworld *overworld, ImVec2 mouse_pos, int current_map, bool is_hole)
Flat helper functions for entity insertion/manipulation.
absl::Status NudgeItem(zelda3::OverworldItem *item, int delta_x, int delta_y)
Move an item by pixel deltas with overworld bounds clamping.
zelda3::OverworldItem * FindNearestItemForSelection(zelda3::Overworld *overworld, const zelda3::OverworldItem &anchor_identity)
Find the best next item to keep selection continuity after deletion.
absl::Status RemoveItem(zelda3::Overworld *overworld, const zelda3::OverworldItem *item_ptr)
Remove an item from the overworld item list by pointer identity.
zelda3::OverworldItem * FindItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity)
Find a live item by value identity.
absl::StatusOr< zelda3::OverworldItem > BuildOverworldItemForInsertion(const std::vector< zelda3::OverworldMap > &maps, ImVec2 world_position, int current_map, uint8_t item_id)
absl::StatusOr< zelda3::OverworldExit * > InsertExit(zelda3::Overworld *overworld, ImVec2 mouse_pos, int current_map)
Insert a new exit at the specified position.
absl::StatusOr< zelda3::OverworldEntrance > BuildOverworldEntranceEdit(const zelda3::Overworld &overworld, const std::vector< zelda3::OverworldEntrance > &records, size_t row, const zelda3::OverworldEntrance &draft, bool legacy_hole_table)
ImVec2 SnapToEntityGrid(ImVec2 pos)
Snap position to 16x16 grid (standard entity positioning)
absl::StatusOr< zelda3::OverworldItem * > DuplicateItemByIdentity(zelda3::Overworld *overworld, const zelda3::OverworldItem &item_identity, int offset_x, int offset_y)
Duplicate an existing item by identity with a positional offset.
absl::StatusOr< zelda3::Sprite * > InsertSprite(zelda3::Overworld *overworld, ImVec2 mouse_pos, int current_map, int game_state, uint8_t sprite_id)
Insert a new sprite at the specified position.
ImVec2 ClampToOverworldBounds(ImVec2 pos)
Clamp position to valid overworld bounds.
uint8_t GetParentMapId(const zelda3::OverworldMap *map, int current_map)
Helper to get parent map ID for multi-area maps.
absl::StatusOr< zelda3::Sprite > BuildOverworldSpriteForInsertion(const std::vector< zelda3::OverworldMap > &maps, ImVec2 world_position, int current_map, uint8_t sprite_id)
constexpr int kExpandedMapCount
Definition common.h:86
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22