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::kNumOverworldMaps ||
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::kNumOverworldMaps ||
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
77} // namespace
78
79absl::StatusOr<zelda3::OverworldItem> BuildOverworldItemForInsertion(
80 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
81 int current_map, uint8_t item_id) {
82 auto position =
83 ResolveEntityInsertionPosition(maps, world_position, current_map);
84 if (!position.ok()) {
85 return position.status();
86 }
87 // This constructor already derives game coordinates from the parent origin.
88 return zelda3::OverworldItem(item_id,
89 static_cast<uint16_t>(position->parent_map),
90 position->x, position->y, false);
91}
92
93absl::StatusOr<zelda3::Sprite> BuildOverworldSpriteForInsertion(
94 const std::vector<zelda3::OverworldMap>& maps, ImVec2 world_position,
95 int current_map, uint8_t sprite_id) {
96 auto position =
97 ResolveEntityInsertionPosition(maps, world_position, current_map);
98 if (!position.ok()) {
99 return position.status();
100 }
101 return zelda3::Sprite(maps[current_map].current_graphics(),
102 static_cast<uint8_t>(position->parent_map), sprite_id,
103 position->game_x, position->game_y, position->x,
104 position->y);
105}
106
107absl::StatusOr<zelda3::OverworldEntrance*> InsertEntrance(
108 zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
109 bool is_hole) {
110 if (!overworld || !overworld->is_loaded()) {
111 return absl::FailedPreconditionError("Overworld not loaded");
112 }
113
114 // Snap to 16x16 grid and clamp to bounds (ZScream: EntranceMode.cs:86-87)
115 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
116
117 // Get parent map ID (ZScream: EntranceMode.cs:78-82)
118 auto* current_ow_map = overworld->overworld_map(current_map);
119 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
120
121 if (is_hole) {
122 // Search for first deleted hole slot (ZScream: EntranceMode.cs:74-100)
123 auto& holes = overworld->holes();
124 for (size_t i = 0; i < holes.size(); ++i) {
125 if (holes[i].deleted) {
126 // Reuse deleted slot
127 holes[i].deleted = false;
128 holes[i].map_id_ = map_id;
129 holes[i].x_ = static_cast<int>(snapped_pos.x);
130 holes[i].y_ = static_cast<int>(snapped_pos.y);
131 holes[i].entrance_id_ = 0; // Default, user configures in popup
132 holes[i].is_hole_ = true;
133
134 // Update map properties (ZScream: EntranceMode.cs:90)
135 holes[i].UpdateMapProperties(map_id, overworld);
136
137 LOG_DEBUG("EntityOps",
138 "Inserted hole at slot %zu: pos=(%d,%d) map=0x%02X", i,
139 holes[i].x_, holes[i].y_, map_id);
140
141 return &holes[i];
142 }
143 }
144 return absl::ResourceExhaustedError(
145 "No space available for new hole. Delete one first.");
146
147 } else {
148 // Search for first deleted entrance slot (ZScream: EntranceMode.cs:104-130)
149 auto* entrances = overworld->mutable_entrances();
150 for (size_t i = 0; i < entrances->size(); ++i) {
151 if (entrances->at(i).deleted) {
152 // Reuse deleted slot
153 entrances->at(i).deleted = false;
154 entrances->at(i).map_id_ = map_id;
155 entrances->at(i).x_ = static_cast<int>(snapped_pos.x);
156 entrances->at(i).y_ = static_cast<int>(snapped_pos.y);
157 entrances->at(i).entrance_id_ = 0; // Default, user configures in popup
158 entrances->at(i).is_hole_ = false;
159
160 // Update map properties (ZScream: EntranceMode.cs:120)
161 entrances->at(i).UpdateMapProperties(map_id, overworld);
162
163 LOG_DEBUG("EntityOps",
164 "Inserted entrance at slot %zu: pos=(%d,%d) map=0x%02X", i,
165 entrances->at(i).x_, entrances->at(i).y_, map_id);
166
167 return &entrances->at(i);
168 }
169 }
170 return absl::ResourceExhaustedError(
171 "No space available for new entrance. Delete one first.");
172 }
173}
174
175absl::StatusOr<zelda3::OverworldExit*> InsertExit(zelda3::Overworld* overworld,
176 ImVec2 mouse_pos,
177 int current_map) {
178 if (!overworld || !overworld->is_loaded()) {
179 return absl::FailedPreconditionError("Overworld not loaded");
180 }
181
182 // Snap to 16x16 grid and clamp to bounds (ZScream: ExitMode.cs:71-72)
183 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
184
185 // Get parent map ID (ZScream: ExitMode.cs:63-67)
186 auto* current_ow_map = overworld->overworld_map(current_map);
187 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
188
189 // Search for first deleted exit slot (ZScream: ExitMode.cs:59-124)
190 auto& exits = *overworld->mutable_exits();
191 for (size_t i = 0; i < exits.size(); ++i) {
192 if (exits[i].deleted_) {
193 // Reuse deleted slot
194 exits[i].deleted_ = false;
195 exits[i].map_id_ = map_id;
196 exits[i].x_ = static_cast<int>(snapped_pos.x);
197 exits[i].y_ = static_cast<int>(snapped_pos.y);
198
199 // Initialize with default values (ZScream: ExitMode.cs:95-112)
200 // User will configure room_id, scroll, camera in popup
201 exits[i].room_id_ = 0;
202 exits[i].x_scroll_ = 0;
203 exits[i].y_scroll_ = 0;
204 exits[i].x_camera_ = 0;
205 exits[i].y_camera_ = 0;
206 exits[i].x_player_ = static_cast<uint16_t>(snapped_pos.x);
207 exits[i].y_player_ = static_cast<uint16_t>(snapped_pos.y);
208 exits[i].scroll_mod_x_ = 0;
209 exits[i].scroll_mod_y_ = 0;
210 exits[i].door_type_1_ = 0;
211 exits[i].door_type_2_ = 0;
212
213 // Update map properties with overworld context for area size detection
214 exits[i].UpdateMapProperties(map_id, overworld);
215
216 LOG_DEBUG("EntityOps",
217 "Inserted exit at slot %zu: pos=(%d,%d) map=0x%02X", i,
218 exits[i].x_, exits[i].y_, map_id);
219
220 return &exits[i];
221 }
222 }
223
224 return absl::ResourceExhaustedError(
225 "No space available for new exit. Delete one first.");
226}
227
228absl::StatusOr<zelda3::Sprite*> InsertSprite(zelda3::Overworld* overworld,
229 ImVec2 mouse_pos, int current_map,
230 int game_state,
231 uint8_t sprite_id) {
232 if (!overworld || !overworld->is_loaded()) {
233 return absl::FailedPreconditionError("Overworld not loaded");
234 }
235
236 if (game_state < 0 || game_state > 2) {
237 return absl::InvalidArgumentError("Invalid game state (must be 0-2)");
238 }
239
241 overworld->overworld_maps(), mouse_pos, current_map, sprite_id);
242 if (!sprite.ok()) {
243 return sprite.status();
244 }
245
246 // Add new sprite to the game state array (ZScream: SpriteMode.cs:34-35)
247 auto& sprites = *overworld->mutable_sprites(game_state);
248
249 sprites.push_back(std::move(*sprite));
250
251 // Return pointer to the newly added sprite
252 zelda3::Sprite* inserted_sprite = &sprites.back();
253
254 LOG_DEBUG(
255 "EntityOps",
256 "Inserted sprite at game_state=%d: pos=(%d,%d) map=0x%02X id=0x%02X",
257 game_state, inserted_sprite->x_, inserted_sprite->y_,
258 inserted_sprite->map_id(), sprite_id);
259
260 return inserted_sprite;
261}
262
263absl::StatusOr<zelda3::OverworldItem*> InsertItem(zelda3::Overworld* overworld,
264 ImVec2 mouse_pos,
265 int current_map,
266 uint8_t item_id) {
267 if (!overworld || !overworld->is_loaded()) {
268 return absl::FailedPreconditionError("Overworld not loaded");
269 }
270
271 auto item = BuildOverworldItemForInsertion(overworld->overworld_maps(),
272 mouse_pos, current_map, item_id);
273 if (!item.ok()) {
274 return item.status();
275 }
276
277 // Add new item to the all_items array (ZScream: ItemMode.cs:92-108)
278 auto& items = *overworld->mutable_all_items();
279
280 items.push_back(std::move(*item));
281
282 zelda3::OverworldItem* inserted_item = &items.back();
283
284 LOG_DEBUG("EntityOps",
285 "Inserted item: pos=(%d,%d) game=(%d,%d) map=0x%02X id=0x%02X",
286 inserted_item->x_, inserted_item->y_, inserted_item->game_x_,
287 inserted_item->game_y_, inserted_item->room_map_id_, item_id);
288
289 return inserted_item;
290}
291
292absl::Status RemoveItem(zelda3::Overworld* overworld,
293 const zelda3::OverworldItem* item_ptr) {
294 if (!overworld) {
295 return absl::InvalidArgumentError("Overworld is null");
296 }
297 if (!item_ptr) {
298 return absl::InvalidArgumentError("Item pointer is null");
299 }
300
301 auto* items = overworld->mutable_all_items();
302 auto it = std::find_if(items->begin(), items->end(),
303 [item_ptr](const zelda3::OverworldItem& item) {
304 return &item == item_ptr;
305 });
306 if (it == items->end()) {
307 return absl::NotFoundError("Item pointer not found in overworld item list");
308 }
309
310 items->erase(it);
311 return absl::OkStatus();
312}
313
315 const zelda3::OverworldItem& item_identity) {
316 if (!overworld) {
317 return absl::InvalidArgumentError("Overworld is null");
318 }
319
320 auto* items = overworld->mutable_all_items();
321 auto it = std::find_if(items->begin(), items->end(),
322 [&item_identity](const zelda3::OverworldItem& item) {
323 return MatchesItemIdentity(item, item_identity);
324 });
325 if (it == items->end()) {
326 return absl::NotFoundError("No matching item identity in overworld list");
327 }
328
329 items->erase(it);
330 return absl::OkStatus();
331}
332
334 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity) {
335 if (!overworld) {
336 return nullptr;
337 }
338
339 auto* items = overworld->mutable_all_items();
340 auto it = std::find_if(items->begin(), items->end(),
341 [&item_identity](const zelda3::OverworldItem& item) {
342 return !item.deleted &&
343 MatchesItemIdentity(item, item_identity);
344 });
345 if (it == items->end()) {
346 return nullptr;
347 }
348 return &(*it);
349}
350
351absl::StatusOr<zelda3::OverworldItem*> DuplicateItemByIdentity(
352 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity,
353 int offset_x, int offset_y) {
354 if (!overworld) {
355 return absl::InvalidArgumentError("Overworld is null");
356 }
357
358 auto* source_item = FindItemByIdentity(overworld, item_identity);
359 if (!source_item) {
360 return absl::NotFoundError("No matching item identity in overworld list");
361 }
362
363 zelda3::OverworldItem duplicate = *source_item;
364 const ImVec2 clamped_pos = ClampToOverworldBounds(
365 ImVec2(static_cast<float>(source_item->x_ + offset_x),
366 static_cast<float>(source_item->y_ + offset_y)));
367 duplicate.x_ = static_cast<int>(clamped_pos.x);
368 duplicate.y_ = static_cast<int>(clamped_pos.y);
369 duplicate.deleted = false;
370 duplicate.UpdateMapProperties(duplicate.room_map_id_);
371
372 auto* items = overworld->mutable_all_items();
373 items->push_back(duplicate);
374 return &items->back();
375}
376
377absl::Status NudgeItem(zelda3::OverworldItem* item, int delta_x, int delta_y) {
378 if (!item) {
379 return absl::InvalidArgumentError("Item pointer is null");
380 }
381 const ImVec2 clamped_pos =
382 ClampToOverworldBounds(ImVec2(static_cast<float>(item->x_ + delta_x),
383 static_cast<float>(item->y_ + delta_y)));
384 item->x_ = static_cast<int>(clamped_pos.x);
385 item->y_ = static_cast<int>(clamped_pos.y);
387 return absl::OkStatus();
388}
389
391 zelda3::Overworld* overworld,
392 const zelda3::OverworldItem& anchor_identity) {
393 if (!overworld) {
394 return nullptr;
395 }
396
397 auto* items = overworld->mutable_all_items();
398 if (!items || items->empty()) {
399 return nullptr;
400 }
401
402 const int anchor_x = anchor_identity.x_;
403 const int anchor_y = anchor_identity.y_;
404 const uint16_t anchor_map = anchor_identity.room_map_id_;
405
406 auto rank = [&](const zelda3::OverworldItem& item) {
407 const int same_map_rank = (item.room_map_id_ == anchor_map) ? 0 : 1;
408 const std::int64_t dx = static_cast<std::int64_t>(item.x_) - anchor_x;
409 const std::int64_t dy = static_cast<std::int64_t>(item.y_) - anchor_y;
410 const std::int64_t dist2 = (dx * dx) + (dy * dy);
411 return std::pair<int, std::int64_t>(same_map_rank, dist2);
412 };
413
414 auto best_it = items->end();
415 std::pair<int, std::int64_t> best_rank = {2, 0};
416 for (auto it = items->begin(); it != items->end(); ++it) {
417 if (it->deleted) {
418 continue;
419 }
420 const auto current_rank = rank(*it);
421 if (best_it == items->end() || current_rank < best_rank) {
422 best_it = it;
423 best_rank = current_rank;
424 }
425 }
426
427 if (best_it == items->end()) {
428 return nullptr;
429 }
430 return &(*best_it);
431}
432
433} // namespace editor
434} // namespace yaze
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:775
auto is_loaded() const
Definition overworld.h:803
const std::vector< OverworldMap > & overworld_maps() const
Definition overworld.h:734
auto overworld_map(int i) const
Definition overworld.h:737
auto mutable_sprites(int state)
Definition overworld.h:759
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::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::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.
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 kNumOverworldMaps
Definition common.h:85