8#include "absl/strings/str_format.h"
24 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
27 current_map >=
static_cast<int>(maps.size())) {
28 return absl::InvalidArgumentError(
"Invalid entity insertion map");
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");
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");
45 const int raw_parent = maps[current_map].parent();
46 const int parent = raw_parent == 0xFF ? current_map : raw_parent;
48 parent >=
static_cast<int>(maps.size()) || parent / 64 != world) {
49 return absl::InvalidArgumentError(
"Invalid entity insertion parent map");
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");
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");
67 static_cast<uint8_t
>(game_y)};
78 int map_id,
int x,
int y) {
81 return absl::InvalidArgumentError(
"The record's map is not available");
83 if (parent->parent() != map_id && parent->parent() != 0xFF) {
84 return absl::InvalidArgumentError(
"Choose the area's parent map");
86 if (x < 0 || y < 0 || x >= 4096 || y >= 4096) {
87 return absl::InvalidArgumentError(
"Position must be within 0-4095 pixels");
89 const auto size = parent->area_size();
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;
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");
109 return absl::OkStatus();
116 const std::vector<zelda3::OverworldEntrance>& records,
size_t row,
118 if (row >= records.size()) {
119 return absl::NotFoundError(
"Entrance row is no longer available");
121 if (draft.
is_hole_ != records[row].is_hole_) {
122 return absl::InvalidArgumentError(
"An edit cannot change the record kind");
126 value.entity_id_ =
static_cast<int>(row);
128 value.map_pos_ = 0xFFFF;
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");
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);
141 value.map_pos_ = before.map_pos_;
142 value.game_x_ = before.game_x_;
143 value.game_y_ = before.game_y_;
147 if (legacy_hole_table && value.map_pos_ < 0x400) {
148 return absl::InvalidArgumentError(
149 "Legacy hole positions must start at local tile row 8");
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));
164 const std::vector<zelda3::OverworldExit>& records,
size_t row,
166 if (row >= records.size()) {
167 return absl::NotFoundError(
"Return row is no longer available");
169 if (draft.
is_hole_ != records[row].is_hole_) {
170 return absl::InvalidArgumentError(
"An edit cannot change the record kind");
174 value.entity_id_ =
static_cast<int>(row);
175 if (value.deleted_) {
178 value.room_id_ = 0xFFFF;
179 value.x_player_ = value.y_player_ = 0xFFFF;
182 if (value.room_id_ == 0xFFFF) {
183 return absl::InvalidArgumentError(
"Choose a room for this return row");
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_;
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);
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_;
206 for (
size_t i = row + 1; i < records.size(); ++i) {
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));
218 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
219 int current_map, uint8_t item_id) {
221 ResolveEntityInsertionPosition(maps, world_position, current_map);
222 if (!position.ok()) {
223 return position.status();
227 static_cast<uint16_t
>(position->parent_map),
228 position->x, position->y,
false);
232 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
233 int current_map, uint8_t sprite_id) {
235 ResolveEntityInsertionPosition(maps, world_position, current_map);
236 if (!position.ok()) {
237 return position.status();
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()
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);
251 if (!overworld || !overworld->
is_loaded()) {
252 return absl::FailedPreconditionError(
"Overworld not loaded");
256 return absl::FailedPreconditionError(
257 "Expanded holes use existing combined entrance table rows");
264 auto* current_ow_map = overworld->
overworld_map(current_map);
269 auto& holes = overworld->
holes();
270 for (
size_t i = 0; i < holes.size(); ++i) {
271 if (holes[i].deleted) {
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;
278 holes[i].is_hole_ =
true;
281 holes[i].UpdateMapProperties(map_id, overworld);
284 "Inserted hole at slot %zu: pos=(%d,%d) map=0x%02X", i,
285 holes[i].x_, holes[i].y_, map_id);
290 return absl::ResourceExhaustedError(
291 "No space available for new hole. Delete one first.");
296 for (
size_t i = 0; i < entrances->size(); ++i) {
297 if (entrances->at(i).deleted) {
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;
304 entrances->at(i).is_hole_ =
false;
307 entrances->at(i).UpdateMapProperties(map_id, overworld);
310 "Inserted entrance at slot %zu: pos=(%d,%d) map=0x%02X", i,
311 entrances->at(i).x_, entrances->at(i).y_, map_id);
313 return &entrances->at(i);
316 return absl::ResourceExhaustedError(
317 "No space available for new entrance. Delete one first.");
324 if (!overworld || !overworld->
is_loaded()) {
325 return absl::FailedPreconditionError(
"Overworld not loaded");
332 auto* current_ow_map = overworld->
overworld_map(current_map);
337 for (
size_t i = 0; i < exits.size(); ++i) {
338 if (exits[i].deleted_) {
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);
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;
360 exits[i].UpdateMapProperties(map_id, overworld);
363 "Inserted exit at slot %zu: pos=(%d,%d) map=0x%02X", i,
364 exits[i].x_, exits[i].y_, map_id);
370 return absl::ResourceExhaustedError(
371 "No space available for new exit. Delete one first.");
375 ImVec2 mouse_pos,
int current_map,
378 if (!overworld || !overworld->
is_loaded()) {
379 return absl::FailedPreconditionError(
"Overworld not loaded");
382 if (game_state < 0 || game_state > 2) {
383 return absl::InvalidArgumentError(
"Invalid game state (must be 0-2)");
389 return sprite.status();
395 sprites.push_back(std::move(*sprite));
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);
406 return inserted_sprite;
413 if (!overworld || !overworld->
is_loaded()) {
414 return absl::FailedPreconditionError(
"Overworld not loaded");
418 mouse_pos, current_map, item_id);
420 return item.status();
426 items.push_back(std::move(*item));
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_,
435 return inserted_item;
441 return absl::InvalidArgumentError(
"Overworld is null");
444 return absl::InvalidArgumentError(
"Item pointer is null");
448 auto it = std::find_if(items->begin(), items->end(),
450 return &item == item_ptr;
452 if (it == items->end()) {
453 return absl::NotFoundError(
"Item pointer not found in overworld item list");
457 return absl::OkStatus();
463 return absl::InvalidArgumentError(
"Overworld is null");
467 auto it = std::find_if(items->begin(), items->end(),
469 return MatchesItemIdentity(item, item_identity);
471 if (it == items->end()) {
472 return absl::NotFoundError(
"No matching item identity in overworld list");
476 return absl::OkStatus();
486 auto it = std::find_if(items->begin(), items->end(),
488 return !item.deleted &&
489 MatchesItemIdentity(item, item_identity);
491 if (it == items->end()) {
499 int offset_x,
int offset_y) {
501 return absl::InvalidArgumentError(
"Overworld is null");
506 return absl::NotFoundError(
"No matching item identity in overworld list");
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);
519 items->push_back(duplicate);
520 return &items->back();
525 return absl::InvalidArgumentError(
"Item pointer is null");
527 const ImVec2 clamped_pos =
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();
544 if (!items || items->empty()) {
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_;
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);
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) {
566 const auto current_rank = rank(*it);
567 if (best_it == items->end() || current_rank < best_rank) {
569 best_rank = current_rank;
573 if (best_it == items->end()) {
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
const std::vector< OverworldMap > & overworld_maps() const
auto overworld_map(int i) const
auto expanded_entrances() const
auto mutable_sprites(int state)
A class for managing sprites in the overworld and underworld.
#define LOG_DEBUG(category, format,...)
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
#define RETURN_IF_ERROR(expr)