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