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 <cstdint>
5
6#include "absl/strings/str_format.h"
7#include "util/log.h"
8
9namespace yaze {
10namespace editor {
11namespace {
12
14 const zelda3::OverworldItem& rhs) {
15 return lhs.id_ == rhs.id_ && lhs.room_map_id_ == rhs.room_map_id_ &&
16 lhs.x_ == rhs.x_ && lhs.y_ == rhs.y_ && lhs.game_x_ == rhs.game_x_ &&
17 lhs.game_y_ == rhs.game_y_ && lhs.bg2_ == rhs.bg2_;
18}
19
20} // namespace
21
22absl::StatusOr<zelda3::OverworldEntrance*> InsertEntrance(
23 zelda3::Overworld* overworld, ImVec2 mouse_pos, int current_map,
24 bool is_hole) {
25 if (!overworld || !overworld->is_loaded()) {
26 return absl::FailedPreconditionError("Overworld not loaded");
27 }
28
29 // Snap to 16x16 grid and clamp to bounds (ZScream: EntranceMode.cs:86-87)
30 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
31
32 // Get parent map ID (ZScream: EntranceMode.cs:78-82)
33 auto* current_ow_map = overworld->overworld_map(current_map);
34 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
35
36 if (is_hole) {
37 // Search for first deleted hole slot (ZScream: EntranceMode.cs:74-100)
38 auto& holes = overworld->holes();
39 for (size_t i = 0; i < holes.size(); ++i) {
40 if (holes[i].deleted) {
41 // Reuse deleted slot
42 holes[i].deleted = false;
43 holes[i].map_id_ = map_id;
44 holes[i].x_ = static_cast<int>(snapped_pos.x);
45 holes[i].y_ = static_cast<int>(snapped_pos.y);
46 holes[i].entrance_id_ = 0; // Default, user configures in popup
47 holes[i].is_hole_ = true;
48
49 // Update map properties (ZScream: EntranceMode.cs:90)
50 holes[i].UpdateMapProperties(map_id, overworld);
51
52 LOG_DEBUG("EntityOps",
53 "Inserted hole at slot %zu: pos=(%d,%d) map=0x%02X", i,
54 holes[i].x_, holes[i].y_, map_id);
55
56 return &holes[i];
57 }
58 }
59 return absl::ResourceExhaustedError(
60 "No space available for new hole. Delete one first.");
61
62 } else {
63 // Search for first deleted entrance slot (ZScream: EntranceMode.cs:104-130)
64 auto* entrances = overworld->mutable_entrances();
65 for (size_t i = 0; i < entrances->size(); ++i) {
66 if (entrances->at(i).deleted) {
67 // Reuse deleted slot
68 entrances->at(i).deleted = false;
69 entrances->at(i).map_id_ = map_id;
70 entrances->at(i).x_ = static_cast<int>(snapped_pos.x);
71 entrances->at(i).y_ = static_cast<int>(snapped_pos.y);
72 entrances->at(i).entrance_id_ = 0; // Default, user configures in popup
73 entrances->at(i).is_hole_ = false;
74
75 // Update map properties (ZScream: EntranceMode.cs:120)
76 entrances->at(i).UpdateMapProperties(map_id, overworld);
77
78 LOG_DEBUG("EntityOps",
79 "Inserted entrance at slot %zu: pos=(%d,%d) map=0x%02X", i,
80 entrances->at(i).x_, entrances->at(i).y_, map_id);
81
82 return &entrances->at(i);
83 }
84 }
85 return absl::ResourceExhaustedError(
86 "No space available for new entrance. Delete one first.");
87 }
88}
89
90absl::StatusOr<zelda3::OverworldExit*> InsertExit(zelda3::Overworld* overworld,
91 ImVec2 mouse_pos,
92 int current_map) {
93 if (!overworld || !overworld->is_loaded()) {
94 return absl::FailedPreconditionError("Overworld not loaded");
95 }
96
97 // Snap to 16x16 grid and clamp to bounds (ZScream: ExitMode.cs:71-72)
98 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
99
100 // Get parent map ID (ZScream: ExitMode.cs:63-67)
101 auto* current_ow_map = overworld->overworld_map(current_map);
102 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
103
104 // Search for first deleted exit slot (ZScream: ExitMode.cs:59-124)
105 auto& exits = *overworld->mutable_exits();
106 for (size_t i = 0; i < exits.size(); ++i) {
107 if (exits[i].deleted_) {
108 // Reuse deleted slot
109 exits[i].deleted_ = false;
110 exits[i].map_id_ = map_id;
111 exits[i].x_ = static_cast<int>(snapped_pos.x);
112 exits[i].y_ = static_cast<int>(snapped_pos.y);
113
114 // Initialize with default values (ZScream: ExitMode.cs:95-112)
115 // User will configure room_id, scroll, camera in popup
116 exits[i].room_id_ = 0;
117 exits[i].x_scroll_ = 0;
118 exits[i].y_scroll_ = 0;
119 exits[i].x_camera_ = 0;
120 exits[i].y_camera_ = 0;
121 exits[i].x_player_ = static_cast<uint16_t>(snapped_pos.x);
122 exits[i].y_player_ = static_cast<uint16_t>(snapped_pos.y);
123 exits[i].scroll_mod_x_ = 0;
124 exits[i].scroll_mod_y_ = 0;
125 exits[i].door_type_1_ = 0;
126 exits[i].door_type_2_ = 0;
127
128 // Update map properties with overworld context for area size detection
129 exits[i].UpdateMapProperties(map_id, overworld);
130
131 LOG_DEBUG("EntityOps",
132 "Inserted exit at slot %zu: pos=(%d,%d) map=0x%02X", i,
133 exits[i].x_, exits[i].y_, map_id);
134
135 return &exits[i];
136 }
137 }
138
139 return absl::ResourceExhaustedError(
140 "No space available for new exit. Delete one first.");
141}
142
143absl::StatusOr<zelda3::Sprite*> InsertSprite(zelda3::Overworld* overworld,
144 ImVec2 mouse_pos, int current_map,
145 int game_state,
146 uint8_t sprite_id) {
147 if (!overworld || !overworld->is_loaded()) {
148 return absl::FailedPreconditionError("Overworld not loaded");
149 }
150
151 if (game_state < 0 || game_state > 2) {
152 return absl::InvalidArgumentError("Invalid game state (must be 0-2)");
153 }
154
155 // Snap to 16x16 grid and clamp to bounds (ZScream: SpriteMode.cs similar
156 // logic)
157 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
158
159 // Get parent map ID (ZScream: SpriteMode.cs:90-95)
160 auto* current_ow_map = overworld->overworld_map(current_map);
161 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
162
163 // Calculate map position (ZScream uses mapHover for parent tracking)
164 // For sprites, we need the actual map coordinates within the 512x512 map
165 int map_local_x = static_cast<int>(snapped_pos.x) % 512;
166 int map_local_y = static_cast<int>(snapped_pos.y) % 512;
167
168 // Convert to game coordinates (0-63 for X/Y within map)
169 uint8_t game_x = static_cast<uint8_t>(map_local_x / 16);
170 uint8_t game_y = static_cast<uint8_t>(map_local_y / 16);
171
172 // Add new sprite to the game state array (ZScream: SpriteMode.cs:34-35)
173 auto& sprites = *overworld->mutable_sprites(game_state);
174
175 // Create new sprite
176 zelda3::Sprite new_sprite(
177 current_ow_map->current_graphics(), static_cast<uint8_t>(map_id),
178 sprite_id, // Sprite ID (user will configure in popup)
179 game_x, // X position in map coordinates
180 game_y, // Y position in map coordinates
181 static_cast<int>(snapped_pos.x), // Real X (world coordinates)
182 static_cast<int>(snapped_pos.y) // Real Y (world coordinates)
183 );
184
185 sprites.push_back(new_sprite);
186
187 // Return pointer to the newly added sprite
188 zelda3::Sprite* inserted_sprite = &sprites.back();
189
190 LOG_DEBUG(
191 "EntityOps",
192 "Inserted sprite at game_state=%d: pos=(%d,%d) map=0x%02X id=0x%02X",
193 game_state, inserted_sprite->x_, inserted_sprite->y_, map_id, sprite_id);
194
195 return inserted_sprite;
196}
197
198absl::StatusOr<zelda3::OverworldItem*> InsertItem(zelda3::Overworld* overworld,
199 ImVec2 mouse_pos,
200 int current_map,
201 uint8_t item_id) {
202 if (!overworld || !overworld->is_loaded()) {
203 return absl::FailedPreconditionError("Overworld not loaded");
204 }
205
206 // Snap to 16x16 grid and clamp to bounds (ZScream: ItemMode.cs similar logic)
207 ImVec2 snapped_pos = ClampToOverworldBounds(SnapToEntityGrid(mouse_pos));
208
209 // Get parent map ID (ZScream: ItemMode.cs:60-64)
210 auto* current_ow_map = overworld->overworld_map(current_map);
211 uint8_t map_id = GetParentMapId(current_ow_map, current_map);
212
213 // Calculate game coordinates (0-63 for X/Y within map)
214 // Following LoadItems logic in overworld.cc:840-854
215 int fake_id = current_map % 0x40;
216 int sy = fake_id / 8;
217 int sx = fake_id - (sy * 8);
218
219 // Calculate map-local coordinates
220 int map_local_x = static_cast<int>(snapped_pos.x) % 512;
221 int map_local_y = static_cast<int>(snapped_pos.y) % 512;
222
223 // Game coordinates (0-63 range)
224 uint8_t game_x = static_cast<uint8_t>(map_local_x / 16);
225 uint8_t game_y = static_cast<uint8_t>(map_local_y / 16);
226
227 // Add new item to the all_items array (ZScream: ItemMode.cs:92-108)
228 auto& items = *overworld->mutable_all_items();
229
230 // Create new item with calculated coordinates
231 items.emplace_back(item_id, // Item ID
232 static_cast<uint16_t>(map_id), // Room map ID
233 static_cast<int>(snapped_pos.x), // X (world coordinates)
234 static_cast<int>(snapped_pos.y), // Y (world coordinates)
235 false // Not deleted
236 );
237
238 // Set game coordinates
239 zelda3::OverworldItem* inserted_item = &items.back();
240 inserted_item->game_x_ = game_x;
241 inserted_item->game_y_ = game_y;
242
243 LOG_DEBUG("EntityOps",
244 "Inserted item: pos=(%d,%d) game=(%d,%d) map=0x%02X id=0x%02X",
245 inserted_item->x_, inserted_item->y_, game_x, game_y, map_id,
246 item_id);
247
248 return inserted_item;
249}
250
251absl::Status RemoveItem(zelda3::Overworld* overworld,
252 const zelda3::OverworldItem* item_ptr) {
253 if (!overworld) {
254 return absl::InvalidArgumentError("Overworld is null");
255 }
256 if (!item_ptr) {
257 return absl::InvalidArgumentError("Item pointer is null");
258 }
259
260 auto* items = overworld->mutable_all_items();
261 auto it = std::find_if(items->begin(), items->end(),
262 [item_ptr](const zelda3::OverworldItem& item) {
263 return &item == item_ptr;
264 });
265 if (it == items->end()) {
266 return absl::NotFoundError("Item pointer not found in overworld item list");
267 }
268
269 items->erase(it);
270 return absl::OkStatus();
271}
272
274 const zelda3::OverworldItem& item_identity) {
275 if (!overworld) {
276 return absl::InvalidArgumentError("Overworld is null");
277 }
278
279 auto* items = overworld->mutable_all_items();
280 auto it = std::find_if(items->begin(), items->end(),
281 [&item_identity](const zelda3::OverworldItem& item) {
282 return MatchesItemIdentity(item, item_identity);
283 });
284 if (it == items->end()) {
285 return absl::NotFoundError("No matching item identity in overworld list");
286 }
287
288 items->erase(it);
289 return absl::OkStatus();
290}
291
293 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity) {
294 if (!overworld) {
295 return nullptr;
296 }
297
298 auto* items = overworld->mutable_all_items();
299 auto it = std::find_if(items->begin(), items->end(),
300 [&item_identity](const zelda3::OverworldItem& item) {
301 return !item.deleted &&
302 MatchesItemIdentity(item, item_identity);
303 });
304 if (it == items->end()) {
305 return nullptr;
306 }
307 return &(*it);
308}
309
310absl::StatusOr<zelda3::OverworldItem*> DuplicateItemByIdentity(
311 zelda3::Overworld* overworld, const zelda3::OverworldItem& item_identity,
312 int offset_x, int offset_y) {
313 if (!overworld) {
314 return absl::InvalidArgumentError("Overworld is null");
315 }
316
317 auto* source_item = FindItemByIdentity(overworld, item_identity);
318 if (!source_item) {
319 return absl::NotFoundError("No matching item identity in overworld list");
320 }
321
322 zelda3::OverworldItem duplicate = *source_item;
323 const ImVec2 clamped_pos = ClampToOverworldBounds(
324 ImVec2(static_cast<float>(source_item->x_ + offset_x),
325 static_cast<float>(source_item->y_ + offset_y)));
326 duplicate.x_ = static_cast<int>(clamped_pos.x);
327 duplicate.y_ = static_cast<int>(clamped_pos.y);
328 duplicate.deleted = false;
329 duplicate.UpdateMapProperties(duplicate.room_map_id_);
330
331 auto* items = overworld->mutable_all_items();
332 items->push_back(duplicate);
333 return &items->back();
334}
335
336absl::Status NudgeItem(zelda3::OverworldItem* item, int delta_x, int delta_y) {
337 if (!item) {
338 return absl::InvalidArgumentError("Item pointer is null");
339 }
340 const ImVec2 clamped_pos =
341 ClampToOverworldBounds(ImVec2(static_cast<float>(item->x_ + delta_x),
342 static_cast<float>(item->y_ + delta_y)));
343 item->x_ = static_cast<int>(clamped_pos.x);
344 item->y_ = static_cast<int>(clamped_pos.y);
346 return absl::OkStatus();
347}
348
350 zelda3::Overworld* overworld,
351 const zelda3::OverworldItem& anchor_identity) {
352 if (!overworld) {
353 return nullptr;
354 }
355
356 auto* items = overworld->mutable_all_items();
357 if (!items || items->empty()) {
358 return nullptr;
359 }
360
361 const int anchor_x = anchor_identity.x_;
362 const int anchor_y = anchor_identity.y_;
363 const uint16_t anchor_map = anchor_identity.room_map_id_;
364
365 auto rank = [&](const zelda3::OverworldItem& item) {
366 const int same_map_rank = (item.room_map_id_ == anchor_map) ? 0 : 1;
367 const std::int64_t dx = static_cast<std::int64_t>(item.x_) - anchor_x;
368 const std::int64_t dy = static_cast<std::int64_t>(item.y_) - anchor_y;
369 const std::int64_t dist2 = (dx * dx) + (dy * dy);
370 return std::pair<int, std::int64_t>(same_map_rank, dist2);
371 };
372
373 auto best_it = items->end();
374 std::pair<int, std::int64_t> best_rank = {2, 0};
375 for (auto it = items->begin(); it != items->end(); ++it) {
376 if (it->deleted) {
377 continue;
378 }
379 const auto current_rank = rank(*it);
380 if (best_it == items->end() || current_rank < best_rank) {
381 best_it = it;
382 best_rank = current_rank;
383 }
384 }
385
386 if (best_it == items->end()) {
387 return nullptr;
388 }
389 return &(*best_it);
390}
391
392} // namespace editor
393} // namespace yaze
void UpdateMapProperties(uint16_t room_map_id, const void *context=nullptr) override
Update entity properties based on map position.
Represents the full Overworld data, light and dark world.
Definition overworld.h:261
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:569
auto is_loaded() const
Definition overworld.h:597
auto overworld_map(int i) const
Definition overworld.h:531
auto mutable_sprites(int state)
Definition overworld.h:553
A class for managing sprites in the overworld and underworld.
Definition sprite.h:35
#define LOG_DEBUG(category, format,...)
Definition log.h:103
bool MatchesItemIdentity(const zelda3::OverworldItem &lhs, const zelda3::OverworldItem &rhs)
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::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.