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