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