yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
custom_collision_panel.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_DUNGEON_PANELS_CUSTOM_COLLISION_PANEL_H
2#define YAZE_APP_EDITOR_DUNGEON_PANELS_CUSTOM_COLLISION_PANEL_H
3
4#include <exception>
5#include <fstream>
6#include <string>
7#include <vector>
8
9#include "absl/strings/str_format.h"
14#include "app/gui/core/icons.h"
15#include "util/file_util.h"
19
20#include <algorithm>
21
22namespace yaze::editor {
23
25 public:
27 DungeonObjectInteraction* interaction)
28 : viewer_(viewer), interaction_(interaction) {}
29
30 std::string GetId() const override { return "dungeon.custom_collision"; }
31 std::string GetDisplayName() const override { return "Custom Collision"; }
32 std::string GetIcon() const override { return ICON_MD_GRID_ON; }
33 std::string GetEditorCategory() const override { return "Dungeon"; }
34
35 void SetCanvasViewer(DungeonCanvasViewer* viewer) { viewer_ = viewer; }
37 interaction_ = interaction;
38 }
39
40 void Draw(bool* p_open) override {
41 (void)p_open;
42 const auto& theme = AgentUI::GetTheme();
43
44 if (!viewer_ || !viewer_->HasRooms() || !viewer_->rom() ||
45 !viewer_->rom()->is_loaded()) {
46 ImGui::TextDisabled(ICON_MD_INFO " No dungeon rooms loaded.");
47 return;
48 }
49
50 const size_t rom_size = viewer_->rom()->vector().size();
51 const int ptr_table_end =
53 const bool collision_table_present =
55 const bool collision_data_region_present =
57 const bool collision_save_supported =
59 if (!collision_table_present) {
60 ImGui::TextColored(theme.status_error, ICON_MD_ERROR
61 " Custom collision table missing (use an "
62 "expanded-collision Oracle ROM)");
63 ImGui::TextDisabled(
64 "Expected ROM >= 0x%X bytes (custom collision pointer table end). "
65 "Current ROM is %zu bytes.",
66 ptr_table_end, rom_size);
67 ImGui::Separator();
68 } else if (!collision_data_region_present) {
69 ImGui::TextColored(theme.status_error, ICON_MD_ERROR
70 " Custom collision data region missing/truncated");
71 ImGui::TextDisabled(
72 "Expected ROM >= 0x%X bytes (custom collision data soft end). "
73 "Current ROM is %zu bytes.",
75 ImGui::Separator();
76 }
77
78 auto* rooms = viewer_->rooms();
79 int room_id = viewer_->current_room_id();
80 if (room_id < 0 || room_id >= 296) {
81 ImGui::TextDisabled(ICON_MD_INFO " Invalid room ID.");
82 return;
83 }
84
85 auto& room = (*rooms)[room_id];
86 bool has_collision = room.has_custom_collision();
87
88 ImGui::BeginDisabled(!collision_save_supported);
89 if (ImGui::Button(has_collision ? "Disable Custom Collision"
90 : "Enable Custom Collision")) {
91 room.set_has_custom_collision(!has_collision);
92 viewer_->set_show_custom_collision_overlay(room.has_custom_collision());
93 }
94 ImGui::EndDisabled();
95
96 ImGui::Separator();
97
98 ImGui::TextUnformatted("Authoring");
99
100 util::FileDialogOptions json_options;
101 json_options.filters.push_back({"Custom Collision", "json"});
102 json_options.filters.push_back({"All Files", "*"});
103
104 ImGui::BeginDisabled(!collision_save_supported);
105 if (ImGui::Button(ICON_MD_UPLOAD " Import Collision...")) {
106 std::string path =
108 if (!path.empty()) {
109 try {
110 std::string contents = util::LoadFile(path);
111 auto rooms_or =
113 if (!rooms_or.ok()) {
114 last_io_error_ = std::string(rooms_or.status().message());
115 last_io_status_.clear();
116 } else {
117 const auto imported = std::move(rooms_or.value());
118 for (const auto& entry : imported) {
119 if (entry.room_id < 0 ||
120 entry.room_id >= static_cast<int>(rooms->size())) {
121 continue;
122 }
123 ApplyRoomEntry(entry, &(*rooms)[entry.room_id]);
124 }
126 last_io_status_ = absl::StrFormat("Imported %zu room(s) from %s",
127 imported.size(), path.c_str());
128 last_io_error_.clear();
129 }
130 } catch (const std::exception& e) {
131 last_io_error_ = e.what();
132 last_io_status_.clear();
133 }
134 }
135 }
136 ImGui::SameLine();
137 if (ImGui::Button(ICON_MD_DOWNLOAD " Export Collision...")) {
138 auto exported = CollectRoomEntries(*rooms);
139 auto json_or = zelda3::DumpCustomCollisionRoomsToJsonString(exported);
140 if (!json_or.ok()) {
141 last_io_error_ = std::string(json_or.status().message());
142 last_io_status_.clear();
143 } else {
145 "custom_collision.json", "json");
146 if (!path.empty()) {
147 std::ofstream file(path);
148 if (!file.is_open()) {
150 absl::StrFormat("Cannot write file: %s", path.c_str());
151 last_io_status_.clear();
152 } else {
153 file << *json_or;
154 file.close();
155 last_io_status_ = absl::StrFormat("Exported %zu room(s) to %s",
156 exported.size(), path.c_str());
157 last_io_error_.clear();
158 }
159 }
160 }
161 }
162 ImGui::EndDisabled();
163
164 if (!last_io_error_.empty()) {
165 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
166 last_io_error_.c_str());
167 } else if (!last_io_status_.empty()) {
168 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE " %s",
169 last_io_status_.c_str());
170 }
171
172 ImGui::Separator();
173
174 if (has_collision) {
175 if (!collision_save_supported) {
176 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING
177 " This ROM cannot save custom collision edits "
178 "(expanded collision region missing).");
179 }
180
181 bool show_overlay = viewer_->show_custom_collision_overlay();
182 if (ImGui::Checkbox("Show Collision Overlay", &show_overlay)) {
184 }
185
186 if (!interaction_) {
187 ImGui::TextDisabled("Painting requires an active interaction context.");
188 return;
189 }
190
191 bool is_painting = (interaction_->mode_manager().GetMode() ==
193 ImGui::BeginDisabled(!collision_save_supported);
194 if (ImGui::Checkbox("Paint Mode", &is_painting)) {
195 if (is_painting) {
197 } else {
199 }
200 }
201 ImGui::EndDisabled();
202
203 if (is_painting) {
204 ImGui::TextColored(theme.text_warning_yellow,
205 "Click/Drag on canvas to paint");
206
207 auto& state = interaction_->mode_manager().GetModeState();
208 int current_val = state.paint_collision_value;
209
210 int brush_radius = std::clamp(state.paint_brush_radius, 0, 8);
211 if (ImGui::SliderInt("Brush Radius", &brush_radius, 0, 8)) {
212 state.paint_brush_radius = brush_radius;
213 }
214 ImGui::SameLine();
215 ImGui::TextDisabled("%dx%d", (brush_radius * 2) + 1,
216 (brush_radius * 2) + 1);
217
218 const auto& tile_types = zelda3::Zelda3Labels::GetTileTypeNames();
219
220 if (ImGui::BeginCombo("Collision Type",
221 absl::StrFormat("%02X: %s", current_val,
222 (current_val < tile_types.size()
223 ? tile_types[current_val]
224 : "Unknown"))
225 .c_str())) {
226 for (int i = 0; i < tile_types.size(); ++i) {
227 bool selected = (current_val == i);
228 if (ImGui::Selectable(
229 absl::StrFormat("%02X: %s", i, tile_types[i]).c_str(),
230 selected)) {
231 state.paint_collision_value = static_cast<uint8_t>(i);
232 }
233 }
234 ImGui::EndCombo();
235 }
236
237 ImGui::Separator();
238 ImGui::Text("Quick Select:");
239 auto quick_button = [&](const char* label, uint8_t val) {
240 if (ImGui::Button(label)) {
241 state.paint_collision_value = val;
242 }
243 };
244 quick_button("Floor (00)", 0x00);
245 ImGui::SameLine();
246 quick_button("Solid (02)", 0x02);
247 ImGui::SameLine();
248 quick_button("D.Water (08)", 0x08);
249 quick_button("S.Water (09)", 0x09);
250 ImGui::SameLine();
251 quick_button("Pit (1B)", 0x1B);
252 ImGui::SameLine();
253 quick_button("Spikes (0E)", 0x0E);
254 }
255
256 ImGui::Separator();
257 ImGui::BeginDisabled(!collision_save_supported);
258 if (ImGui::Button("Clear All Custom Collision")) {
259 room.custom_collision().tiles.fill(0);
260 // Clearing should remove the override (room falls back to vanilla).
261 room.custom_collision().has_data = false;
262 room.MarkCustomCollisionDirty();
264 }
265 ImGui::EndDisabled();
266 } else {
267 ImGui::TextWrapped(
268 "Custom collision allows you to override the physics of individual "
269 "8x8 tiles in the room. This is useful for creating water, pits, or "
270 "other effects that don't match the background tiles.");
271 }
272 }
273
274 private:
275 static std::vector<zelda3::CustomCollisionRoomEntry> CollectRoomEntries(
276 const std::array<zelda3::Room, 0x128>& rooms) {
277 std::vector<zelda3::CustomCollisionRoomEntry> out;
278 out.reserve(16);
279 for (int rid = 0; rid < static_cast<int>(rooms.size()); ++rid) {
280 const auto& room = rooms[rid];
281
282 // Export only rooms with any non-zero override tiles.
283 bool any = false;
285 entry.room_id = rid;
286 const auto& map = room.custom_collision().tiles;
287 for (size_t off = 0; off < map.size(); ++off) {
288 const uint8_t val = map[off];
289 if (val == 0) {
290 continue;
291 }
292 any = true;
293 entry.tiles.push_back(
294 zelda3::CustomCollisionTileEntry{static_cast<uint16_t>(off), val});
295 }
296 if (!any) {
297 continue;
298 }
299 out.push_back(std::move(entry));
300 }
301 return out;
302 }
303
305 zelda3::Room* room) {
306 if (room == nullptr) {
307 return;
308 }
309 room->custom_collision().tiles.fill(0);
310 bool any = false;
311 for (const auto& t : entry.tiles) {
312 if (t.offset >= room->custom_collision().tiles.size()) {
313 continue;
314 }
315 room->custom_collision().tiles[t.offset] = t.value;
316 if (t.value != 0) {
317 any = true;
318 }
319 }
320 room->custom_collision().has_data = any;
322 }
323
326
327 std::string last_io_status_;
328 std::string last_io_error_;
329};
330
331} // namespace yaze::editor
332
333#endif
const auto & vector() const
Definition rom.h:143
bool is_loaded() const
Definition rom.h:132
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
void SetCanvasViewer(DungeonCanvasViewer *viewer)
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
static void ApplyRoomEntry(const zelda3::CustomCollisionRoomEntry &entry, zelda3::Room *room)
static std::vector< zelda3::CustomCollisionRoomEntry > CollectRoomEntries(const std::array< zelda3::Room, 0x128 > &rooms)
void SetInteraction(DungeonObjectInteraction *interaction)
CustomCollisionPanel(DungeonCanvasViewer *viewer, DungeonObjectInteraction *interaction)
void Draw(bool *p_open) override
Draw the panel content.
std::string GetId() const override
Unique identifier for this panel.
std::array< zelda3::Room, 0x128 > * rooms() const
Handles object selection, placement, and interaction within the dungeon canvas.
Base interface for all logical panel components.
void SetMode(InteractionMode mode)
Set interaction mode.
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
const CustomCollisionMap & custom_collision() const
Definition room.h:384
void MarkCustomCollisionDirty()
Definition room.h:408
#define ICON_MD_INFO
Definition icons.h:993
#define ICON_MD_WARNING
Definition icons.h:2123
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_UPLOAD
Definition icons.h:2048
#define ICON_MD_GRID_ON
Definition icons.h:896
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DOWNLOAD
Definition icons.h:618
const AgentUITheme & GetTheme()
Editors are the view controllers for the application.
std::string LoadFile(const std::string &filename)
Loads the entire contents of a file into a string.
Definition file_util.cc:23
constexpr int kCustomCollisionDataSoftEnd
absl::StatusOr< std::vector< CustomCollisionRoomEntry > > LoadCustomCollisionRoomsFromJsonString(const std::string &json_content)
absl::StatusOr< std::string > DumpCustomCollisionRoomsToJsonString(const std::vector< CustomCollisionRoomEntry > &rooms)
constexpr bool HasCustomCollisionPointerTable(std::size_t rom_size)
constexpr int kNumberOfRooms
constexpr bool HasCustomCollisionDataRegion(std::size_t rom_size)
constexpr int kCustomCollisionRoomPointers
constexpr bool HasCustomCollisionWriteSupport(std::size_t rom_size)
std::vector< FileDialogFilter > filters
Definition file_util.h:17
std::array< uint8_t, 64 *64 > tiles
std::vector< CustomCollisionTileEntry > tiles
static const std::vector< std::string > & GetTileTypeNames()