yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
entity_workbench.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <algorithm>
5#include <memory>
6#include <tuple>
7#include <vector>
8
9#include "absl/strings/str_format.h"
10
19#include "imgui/imgui.h"
21
22namespace yaze {
23namespace editor {
24
25namespace {
26
29 return std::tie(a.map_id_, a.x_, a.y_, a.game_x_, a.game_y_, a.map_pos_,
30 a.entrance_id_, a.is_hole_, a.deleted) ==
31 std::tie(b.map_id_, b.x_, b.y_, b.game_x_, b.game_y_, b.map_pos_,
33}
34
48
49template <typename Record>
50std::optional<size_t> FindRecordRow(const std::vector<Record>& records,
51 const Record* target) {
52 for (size_t i = 0; i < records.size(); ++i) {
53 if (&records[i] == target)
54 return i;
55 }
56 return std::nullopt;
57}
58
59template <typename Record, typename Notify>
60absl::Status CommitRecordEdit(std::vector<Record>& records, size_t row,
61 const Record& after, UndoManager& history,
62 Notify notify, const char* description) {
63 const Record before = records[row];
64 if (SameRecord(before, after))
65 return absl::OkStatus();
66 auto restore = [&records, row, notify](const Record& value) -> absl::Status {
67 if (row >= records.size()) {
68 return absl::FailedPreconditionError("The edited table row is gone");
69 }
70 records[row] = value;
71 notify(&records[row]);
72 return absl::OkStatus();
73 };
74 RETURN_IF_ERROR(restore(after));
75 history.Push(std::make_unique<OverworldRecordEditAction<Record>>(
76 before, after, std::move(restore), description));
77 return absl::OkStatus();
78}
79
81 switch (type) {
83 return "Exit";
85 return "Entrance";
87 return "Item";
89 return "Sprite";
91 return "Transport";
93 return "Music";
95 return "Tilemap";
97 return "Properties";
99 return "Dungeon Sprite";
100 default:
101 return "Unknown";
102 }
103}
104
105} // namespace
106
110 return absl::FailedPreconditionError(
111 "Native entity editing is unavailable in preview");
112 }
113 auto* records = overworld_.mutable_entrances();
114 auto row = FindRecordRow(*records, target);
115 if (!row) {
116 records = overworld_.mutable_holes();
117 row = FindRecordRow(*records, target);
118 if (row && rom_ &&
120 return absl::FailedPreconditionError(
121 "Select the hole's row in the expanded entrance table");
122 }
123 }
124 if (!row)
125 return absl::NotFoundError("Select a current entrance table row");
127 overworld_, *records, *row, draft,
128 records == overworld_.mutable_holes()));
129 return CommitRecordEdit(
130 *records, *row, after, undo_manager_,
131 [this](zelda3::OverworldEntrance* value) {
132 if (current_entity_ == value)
133 edit_entrance_ = *value;
135 },
136 "Edit overworld entrance");
137}
138
140 zelda3::OverworldExit* target, const zelda3::OverworldExit& draft) {
142 return absl::FailedPreconditionError(
143 "Native entity editing is unavailable in preview");
144 }
145 auto* records = overworld_.mutable_exits();
146 const auto row = FindRecordRow(*records, target);
147 if (!row)
148 return absl::NotFoundError("Select a current return table row");
149 ASSIGN_OR_RETURN(auto after,
150 BuildOverworldExitEdit(overworld_, *records, *row, draft));
151 return CommitRecordEdit(
152 *records, *row, after, undo_manager_,
153 [this](zelda3::OverworldExit* value) {
154 if (current_entity_ == value)
155 edit_exit_ = *value;
157 },
158 "Edit overworld return");
159}
160
162 (void)p_open;
163 const auto ctx = CurrentOverworldWindowContext();
164 if (!ctx)
165 return;
166
167 auto* editor = ctx.editor;
168 if (editor->is_lyra_preview()) {
169 ImGui::TextDisabled("Lyra preview: entity editing unavailable");
170 return;
171 }
172
173 if (editor->current_map_id() >= 0xA0 && editor->current_map_id() < 0xC0) {
174 ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.3f, 1.0f));
175 ImGui::Text(ICON_MD_INFO " Tail Map $%02X Entity Limitations",
176 editor->current_map_id());
177 ImGui::PopStyleColor();
178 ImGui::TextWrapped(
179 "• Items & Bomb Doors: Disabled (requires SRAM event allocation)\n"
180 "• Exits: 1 free slot (full links require ASM exit table v2)\n"
181 "• Entrances: Trigger table v2 in development (Phase P2)\n"
182 "• Sprites: Draft state (actor compilation needed for ROM)");
183 ImGui::Separator();
184 }
185
186 // Special triggers are tile properties, not entrance entities. Keep their
187 // on-demand inspector reachable even when no marker is selected.
188 const Rom* rom = editor->rom();
189 if (rom != inspected_rom_ ||
190 (rom && (rom->data() != inspected_rom_data_ ||
191 rom->filename() != inspected_filename_)) ||
193 special_entrances_->inspected_map != editor->current_map_id())) {
194 special_entrances_.reset();
195 }
196 if (ImGui::Button(special_entrances_ ? "Refresh special entrances"
197 : "Inspect special entrances")) {
199 rom ? std::optional<zelda3::SpecialEntranceInspection>(
200 zelda3::InspectSpecialEntrances(*rom, editor->overworld(),
201 editor->current_map_id()))
202 : std::nullopt;
203 inspected_rom_ = rom;
204 inspected_rom_data_ = rom ? rom->data() : nullptr;
205 inspected_filename_ = rom ? rom->filename() : "";
206 }
207 if (special_entrances_) {
208 const auto& inspection = *special_entrances_;
209 if (!inspection.available) {
210 ImGui::TextWrapped("%s", inspection.provenance.c_str());
211 } else {
212 ImGui::Text("%zu matching positions", inspection.matches.size());
213 for (size_t i = 0; i < inspection.matches.size(); ++i) {
214 const auto& match = inspection.matches[i];
215 const auto& row = inspection.rows[match.row_index];
216 const int destination = row.destination_area;
217 const std::string name =
218 destination >= 0
220 "overworld_map", destination,
221 absl::StrFormat("Special area $%02X", destination))
222 : "Unresolved destination";
223 ImGui::PushID(static_cast<int>(i));
224 if (ImGui::Button(absl::StrFormat("%s ($%02X) - tile (%d,%d)", name,
225 destination, match.world_x,
226 match.world_y)
227 .c_str())) {
228 editor->LocateOverworldTile(match.screen_id, match.world_x,
229 match.world_y);
230 }
231 ImGui::PopID();
232 }
233 if (ImGui::TreeNode("Technical details and provenance")) {
234 ImGui::TextWrapped("%s", inspection.provenance.c_str());
235 ImGui::TextWrapped("%s", inspection.limitation.c_str());
236 if (inspection.overlay_may_replace_tiles) {
237 ImGui::TextWrapped(
238 "This area has an overlay; the runtime sampled "
239 "tile may depend on overlay state.");
240 }
241 for (const auto& match : inspection.matches) {
242 const auto& row = inspection.rows[match.row_index];
243 ImGui::Text(
244 "Screen $%02X parent $%02X at (%d,%d): Map16 $%03X, "
245 "word $%04X, mask $%03X -> fake exit $%04X "
246 "(arrival direction $%04X)",
247 match.screen_id, match.parent_area, match.tile_x, match.tile_y,
248 match.tile16_id, match.first_word, match.masked_trigger,
249 row.destination_marker, row.direction);
250 }
251 if (ImGui::Button("Copy special entrance details")) {
252 std::string details = inspection.provenance + "\n";
253 for (const auto& match : inspection.matches) {
254 const auto& row = inspection.rows[match.row_index];
255 details += absl::StrFormat(
256 "screen=$%02X parent=$%02X tile=(%d,%d) map16=$%03X "
257 "word=$%04X mask=$%03X marker=$%04X\n",
258 match.screen_id, match.parent_area, match.tile_x, match.tile_y,
259 match.tile16_id, match.first_word, match.masked_trigger,
260 row.destination_marker);
261 }
262 ImGui::SetClipboardText(details.c_str());
263 }
264 ImGui::TreePop();
265 }
266 }
267 ImGui::Separator();
268 }
269
270 auto* active_entity = editor->current_entity();
271 if (active_entity == nullptr) {
272 ImGui::TextDisabled(tr("No entity selected"));
273 return;
274 }
275
276 ImGui::Text(tr("Type: %s"), GetEntityTypeLabel(active_entity->entity_type_));
277 ImGui::Separator();
278
279 if (ImGui::Button(ICON_MD_SETTINGS " Open Popup Editor")) {
280 OpenEditorFor(active_entity);
281 }
282}
283
285 const auto ctx = CurrentOverworldWindowContext();
286 if (ctx) {
287 ctx.editor->SetCurrentEntity(entity);
288 }
289}
290
292 const auto ctx = CurrentOverworldWindowContext();
293 if (!ctx || !entity)
294 return;
295
296 auto* editor = ctx.editor;
297 editor->SetCurrentEntity(entity);
298 editing_entity_ = entity;
299
300 switch (entity->entity_type_) {
302 editor->edit_exit() = *static_cast<zelda3::OverworldExit*>(entity);
303 ImGui::OpenPopup(
305 .c_str());
306 break;
308 editor->edit_entrance() =
309 *static_cast<zelda3::OverworldEntrance*>(entity);
310 ImGui::OpenPopup(
312 .c_str());
313 break;
315 editor->edit_item() = *static_cast<zelda3::OverworldItem*>(entity);
316 ImGui::OpenPopup(
318 .c_str());
319 break;
321 editor->edit_sprite() = *static_cast<zelda3::Sprite*>(entity);
322 ImGui::OpenPopup(
324 .c_str());
325 break;
326 default:
327 break;
328 }
329}
330
332 const auto ctx = CurrentOverworldWindowContext();
333 if (!ctx || !entity) {
334 return;
335 }
336 ctx.editor->SetCurrentEntity(entity);
337 ImGui::OpenPopup(kContextMenuPopupId);
338}
339
341 const auto ctx = CurrentOverworldWindowContext();
342 if (!ctx)
343 return;
344
345 auto* editor = ctx.editor;
346 auto* editing_entity = editing_entity_;
347
348 zelda3::GameEntity* entity_to_edit = nullptr;
349 if (ImGui::BeginPopup(kContextMenuPopupId)) {
350 entity_to_edit = DrawEntityContextMenu();
351 ImGui::EndPopup();
352 }
353 if (entity_to_edit) {
354 OpenEditorFor(entity_to_edit);
355 }
356
357 if (ImGui::BeginPopupModal("Entity Insert Error", nullptr,
358 ImGuiWindowFlags_AlwaysAutoResize)) {
359 ImGui::TextWrapped("%s", editor->insert_error().c_str());
360 ImGui::Spacing();
361 if (ImGui::Button(tr("OK"), ImVec2(120.0f, 0.0f))) {
362 editor->insert_error().clear();
363 ImGui::CloseCurrentPopup();
364 }
365 ImGui::EndPopup();
366 }
367
368 if (DrawExitEditorPopup(editor->edit_exit())) {
369 if (editing_entity &&
370 editing_entity->entity_type_ == zelda3::GameEntity::EntityType::kExit) {
371 const auto status = editor->ApplyExitEdit(
372 static_cast<zelda3::OverworldExit*>(editing_entity),
373 editor->edit_exit());
374 if (!status.ok()) {
375 editor->insert_error() = std::string(status.message());
376 ImGui::OpenPopup("Entity Edit Error");
377 }
378 }
379 }
380 if (DrawOverworldEntrancePopup(editor->edit_entrance())) {
381 if (editing_entity && editing_entity->entity_type_ ==
383 const auto status = editor->ApplyEntranceEdit(
384 static_cast<zelda3::OverworldEntrance*>(editing_entity),
385 editor->edit_entrance());
386 if (!status.ok()) {
387 editor->insert_error() = std::string(status.message());
388 ImGui::OpenPopup("Entity Edit Error");
389 }
390 }
391 }
392 if (DrawItemEditorPopup(editor->edit_item())) {
393 if (editing_entity &&
394 editing_entity->entity_type_ == zelda3::GameEntity::EntityType::kItem) {
395 if (editor->edit_item().deleted) {
396 (void)editor->DeleteSelectedItem();
397 } else {
398 *static_cast<zelda3::OverworldItem*>(editing_entity) =
399 editor->edit_item();
400 editor->NotifyEntityModified(editing_entity);
401 }
402 }
403 }
404 if (DrawSpriteEditorPopup(editor->edit_sprite())) {
405 if (editing_entity && editing_entity->entity_type_ ==
407 *static_cast<zelda3::Sprite*>(editing_entity) = editor->edit_sprite();
408 editor->NotifyEntityModified(editing_entity);
409 }
410 }
411
412 if (ImGui::BeginPopupModal("Entity Edit Error", nullptr,
413 ImGuiWindowFlags_AlwaysAutoResize)) {
414 ImGui::TextWrapped("%s", editor->insert_error().c_str());
415 if (ImGui::Button(tr("OK")))
416 ImGui::CloseCurrentPopup();
417 ImGui::EndPopup();
418 }
419
420 const bool exit_popup_open = ImGui::IsPopupOpen(
421 gui::MakePopupId(gui::EditorNames::kOverworld, "Exit Editor").c_str());
422 const bool entrance_popup_open = ImGui::IsPopupOpen(
424 .c_str());
425 const bool item_popup_open = ImGui::IsPopupOpen(
426 gui::MakePopupId(gui::EditorNames::kOverworld, "Item Editor").c_str());
427 const bool sprite_popup_open = ImGui::IsPopupOpen(
428 gui::MakePopupId(gui::EditorNames::kOverworld, "Sprite Editor").c_str());
429 if (!exit_popup_open && !entrance_popup_open && !item_popup_open &&
430 !sprite_popup_open) {
431 editing_entity_ = nullptr;
432 }
433}
434
436 EntityMutationService* mutation_service) {
437 const auto ctx = CurrentOverworldWindowContext();
438 if (!ctx || !mutation_service)
439 return;
440
441 auto* editor = ctx.editor;
442 auto request = editor->TakePendingEntityInsertion();
443 if (!request)
444 return;
445
446 auto res = mutation_service->InsertEntity(request->type, request->target,
447 request->item_id);
448
449 if (res.ok()) {
450 OpenEditorFor(res.entity);
451 editor->NotifyEntityModified(res.entity);
452 } else {
453 editor->insert_error() = res.error_message;
454 ImGui::OpenPopup("Entity Insert Error");
455 }
456}
457
459 const auto ctx = CurrentOverworldWindowContext();
460 if (!ctx)
461 return nullptr;
462
463 auto* editor = ctx.editor;
464 auto* active_entity = editor->current_entity();
465 if (!active_entity)
466 return nullptr;
467
468 zelda3::GameEntity* entity_to_edit = nullptr;
469 std::vector<gui::MenuItemSpec> items;
470 items.emplace_back(
471 "Edit Properties...", ICON_MD_EDIT,
472 [&entity_to_edit, active_entity]() { entity_to_edit = active_entity; });
473
474 if (active_entity->entity_type_ == zelda3::GameEntity::EntityType::kExit) {
475 auto* exit = static_cast<zelda3::OverworldExit*>(active_entity);
476 items.emplace_back("Jump to Room", ICON_MD_LINK, [editor, exit]() {
477 editor->RequestJumpToRoom(exit->room_id_);
478 });
479 } else if (active_entity->entity_type_ ==
481 auto* entrance = static_cast<zelda3::OverworldEntrance*>(active_entity);
482 items.emplace_back("Jump to Entrance", ICON_MD_LINK, [editor, entrance]() {
483 editor->RequestJumpToEntrance(entrance->entrance_id_);
484 });
485 }
487 return entity_to_edit;
488}
489
491
492} // namespace editor
493} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
auto filename() const
Definition rom.h:219
auto data() const
Definition rom.h:213
UndoManager undo_manager_
Definition editor.h:383
Editor-level service responsible for overworld entity mutations.
MutationResult InsertEntity(const std::string &type, ImVec2 pos, int map_id, int game_state, uint8_t item_id=0)
Dispatches entity insertion based on type string.
absl::Status ApplyEntranceEdit(zelda3::OverworldEntrance *target, const zelda3::OverworldEntrance &draft)
zelda3::OverworldEntrance edit_entrance_
void NotifyEntityModified(zelda3::GameEntity *entity)
Notify that an entity has been modified, marking ROM as dirty.
zelda3::GameEntity * current_entity_
zelda3::OverworldExit edit_exit_
absl::Status ApplyExitEdit(zelda3::OverworldExit *target, const zelda3::OverworldExit &draft)
Authoritative component for entity editing state and UI.
std::optional< zelda3::SpecialEntranceInspection > special_entrances_
void OpenEditorFor(zelda3::GameEntity *entity)
void ProcessPendingInsertion(EntityMutationService *mutation_service)
static constexpr const char * kContextMenuPopupId
void Draw(bool *p_open) override
Draw the panel content.
void OpenContextMenuFor(zelda3::GameEntity *entity)
void SetActiveEntity(zelda3::GameEntity *entity)
Manages undo/redo stacks for a single editor.
void Push(std::unique_ptr< UndoAction > action)
Base class for all overworld and dungeon entities.
Definition common.h:31
enum yaze::zelda3::GameEntity::EntityType entity_type_
Represents an overworld exit that transitions from dungeon to overworld.
bool is_lyra_preview() const
Definition overworld.h:444
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
#define ICON_MD_SETTINGS
Definition icons.h:1699
#define ICON_MD_LINK
Definition icons.h:1090
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_EDIT
Definition icons.h:645
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status CommitRecordEdit(std::vector< Record > &records, size_t row, const Record &after, UndoManager &history, Notify notify, const char *description)
std::optional< size_t > FindRecordRow(const std::vector< Record > &records, const Record *target)
const char * GetEntityTypeLabel(zelda3::GameEntity::EntityType type)
bool SameRecord(const zelda3::OverworldEntrance &a, const zelda3::OverworldEntrance &b)
absl::StatusOr< zelda3::OverworldExit > BuildOverworldExitEdit(const zelda3::Overworld &overworld, const std::vector< zelda3::OverworldExit > &records, size_t row, const zelda3::OverworldExit &draft)
bool DrawSpriteEditorPopup(zelda3::Sprite &sprite)
Definition entity.cc:532
bool DrawOverworldEntrancePopup(zelda3::OverworldEntrance &entrance)
Definition entity.cc:119
bool DrawItemEditorPopup(zelda3::OverworldItem &item)
Definition entity.cc:400
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)
OverworldWindowContext CurrentOverworldWindowContext()
bool DrawExitEditorPopup(zelda3::OverworldExit &exit)
Definition entity.cc:200
constexpr const char * kOverworld
Definition popup_id.h:53
std::string MakePopupId(size_t session_id, const std::string &editor_name, const std::string &popup_name)
Generate session-aware popup IDs to prevent conflicts in multi-editor layouts.
Definition popup_id.h:23
void RenderMenuItems(const std::vector< MenuItemSpec > &items)
SpecialEntranceInspection InspectSpecialEntrances(const Rom &rom, Overworld &overworld, int map_id)
OverworldRomProfile DetectOverworldRomProfile(const Rom &rom)
Definition overworld.h:325
#define REGISTER_PANEL(PanelClass)
Auto-registration macro for panels with default constructors.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Room transition destination.
Definition zelda.h:448
static std::string GetLabel(const std::string &resource_type, int id, const std::string &default_value="")
Get a label by resource type and ID.