yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
custom_collision_panel.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <exception>
7#include <fstream>
8#include <string>
9#include <utility>
10#include <vector>
11
12#include "absl/strings/str_format.h"
15#include "imgui/imgui.h"
16#include "util/file_util.h"
17#include "util/i18n/tr.h"
20
21namespace yaze::editor {
22
23void CustomCollisionPanel::Draw(bool* p_open) {
24 (void)p_open;
25 gui::AutoWidgetScope automation_scope("Dungeon/CustomCollision");
26 const auto& theme = AgentUI::GetTheme();
27
28 if (!viewer_ || !viewer_->HasRooms() || !viewer_->rom() ||
29 !viewer_->rom()->is_loaded()) {
30 ImGui::TextDisabled(ICON_MD_INFO " No dungeon rooms loaded.");
31 return;
32 }
33
34 const size_t rom_size = viewer_->rom()->vector().size();
35 const int ptr_table_end =
37 const bool collision_table_present =
39 const bool collision_data_region_present =
41 const bool collision_save_supported =
43 if (!collision_table_present) {
44 ImGui::TextColored(theme.status_error, ICON_MD_ERROR
45 " Custom collision table missing (use an "
46 "expanded-collision Oracle ROM)");
47 ImGui::TextDisabled(
48 tr("Expected ROM >= 0x%X bytes (custom collision pointer table end). "
49 "Current ROM is %zu bytes."),
50 ptr_table_end, rom_size);
51 ImGui::Separator();
52 } else if (!collision_data_region_present) {
53 ImGui::TextColored(theme.status_error, ICON_MD_ERROR
54 " Custom collision data region missing/truncated");
55 ImGui::TextDisabled(
56 tr("Expected ROM >= 0x%X bytes (custom collision data soft end). "
57 "Current ROM is %zu bytes."),
59 ImGui::Separator();
60 }
61
62 auto* rooms = viewer_->rooms();
63 int room_id = viewer_->current_room_id();
64 if (room_id < 0 || room_id >= 296) {
65 ImGui::TextDisabled(ICON_MD_INFO " Invalid room ID.");
66 return;
67 }
68
69 auto& room = (*rooms)[room_id];
70 bool has_collision = room.has_custom_collision();
71
72 ImGui::BeginDisabled(!collision_save_supported);
73 if (ImGui::Button(has_collision ? "Disable Custom Collision"
74 : "Enable Custom Collision")) {
75 if (interaction_) {
77 }
78 room.set_has_custom_collision(!has_collision);
79 if (!room.has_custom_collision() && interaction_ &&
83 }
84 if (interaction_) {
86 }
87 viewer_->set_show_custom_collision_overlay(room.has_custom_collision());
88 has_collision = room.has_custom_collision();
89 }
90 gui::AutoRegisterLastItem("button", "toggle_custom_collision");
91 ImGui::EndDisabled();
92
93 ImGui::Separator();
94
95 ImGui::TextUnformatted(tr("Authoring"));
96
97 util::FileDialogOptions json_options;
98 json_options.filters.push_back({"Custom Collision", "json"});
99 json_options.filters.push_back({"All Files", "*"});
100
101 ImGui::BeginDisabled(!collision_save_supported);
102 if (ImGui::Button(ICON_MD_UPLOAD " Import Collision...")) {
103 std::string path =
105 if (!path.empty()) {
106 try {
107 std::string contents = util::LoadFile(path);
108 (void)ImportCollisionJson(contents, path);
109 } catch (const std::exception& e) {
110 last_io_error_ = e.what();
111 last_io_status_.clear();
112 }
113 }
114 }
115 ImGui::SameLine();
116 if (ImGui::Button(ICON_MD_DOWNLOAD " Export Collision...")) {
117 auto exported_or = CollectRoomEntries(*rooms);
118 if (!exported_or.ok()) {
119 last_io_error_ = std::string(exported_or.status().message());
120 last_io_status_.clear();
121 } else {
123 "custom_collision.json", "json");
124 if (!path.empty()) {
125 (void)ExportCollisionToFile(*rooms, path);
126 }
127 }
128 }
129 ImGui::EndDisabled();
130
131 if (!last_io_error_.empty()) {
132 ImGui::TextColored(theme.status_error, ICON_MD_ERROR " %s",
133 last_io_error_.c_str());
134 } else if (!last_io_status_.empty()) {
135 ImGui::TextColored(theme.status_success, ICON_MD_CHECK_CIRCLE " %s",
136 last_io_status_.c_str());
137 }
138
139 ImGui::Separator();
140
141 if (has_collision) {
142 if (!collision_save_supported) {
143 ImGui::TextColored(theme.text_warning_yellow, ICON_MD_WARNING
144 " This ROM cannot save custom collision edits "
145 "(expanded collision region missing).");
146 }
147
148 const auto& map = room.custom_collision();
149 size_t override_count = 0;
150 size_t floor_override_count = 0;
151 for (size_t off = 0; off < map.tiles.size(); ++off) {
152 if (!map.HasOverride(off)) {
153 continue;
154 }
155 ++override_count;
156 if (map.tiles[off] == 0x00) {
157 ++floor_override_count;
158 }
159 }
160 ImGui::Text(tr("Override tiles: %zu (%zu floor 00)"), override_count,
161 floor_override_count);
162
163 bool show_overlay = viewer_->show_custom_collision_overlay();
164 if (ImGui::Checkbox(tr("Show Collision Overlay"), &show_overlay)) {
166 }
167 gui::AutoRegisterLastItem("checkbox", "show_collision_overlay");
168
169 if (!interaction_) {
170 ImGui::TextDisabled(
171 tr("Painting requires an active interaction context."));
172 return;
173 }
174
175 bool is_painting = (interaction_->mode_manager().GetMode() ==
177 ImGui::BeginDisabled(!collision_save_supported);
178 if (ImGui::Checkbox(tr("Paint Mode"), &is_painting)) {
179 if (is_painting) {
182 } else {
184 }
185 }
186 gui::AutoRegisterLastItem("checkbox", "collision_paint_mode");
187 ImGui::EndDisabled();
188
189 if (is_painting) {
190 ImGui::TextColored(
191 theme.text_warning_yellow,
192 tr("Left-drag paints; Alt-drag or Erase mode clears override"));
193
194 auto& state = interaction_->mode_manager().GetModeState();
195 int current_val = state.paint_collision_value;
196
197 int brush_radius = std::clamp(state.paint_brush_radius, 0, 8);
198 if (ImGui::SliderInt(tr("Brush Radius"), &brush_radius, 0, 8)) {
199 state.paint_brush_radius = brush_radius;
200 }
201 gui::AutoRegisterLastItem("slider_int", "collision_brush_radius");
202 ImGui::SameLine();
203 ImGui::TextDisabled(tr("%dx%d"), (brush_radius * 2) + 1,
204 (brush_radius * 2) + 1);
205
206 bool erase_override = state.paint_collision_erase;
207 if (ImGui::Checkbox(tr("Erase Override (Unset)"), &erase_override)) {
208 state.paint_collision_erase = erase_override;
209 }
210 gui::AutoRegisterLastItem("checkbox", "collision_erase_override");
211 if (ImGui::IsItemHovered()) {
212 ImGui::SetTooltip(
213 tr("Removes custom collision overrides so tiles fall back to "
214 "underlying object collision. Hold Alt while painting to erase "
215 "momentarily."));
216 }
217
218 const auto& tile_types = zelda3::Zelda3Labels::GetTileTypeNames();
219
220 ImGui::BeginDisabled(state.paint_collision_erase);
221 if (ImGui::BeginCombo(tr("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 < static_cast<int>(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 state.paint_collision_erase = false;
234 }
235 }
236 ImGui::EndCombo();
237 }
238 gui::AutoRegisterLastItem("combo", "collision_tile_type");
239 ImGui::EndDisabled();
240
241 ImGui::Separator();
242 ImGui::Text(tr("Quick Select:"));
243 auto quick_button = [&](const char* label, const char* id, uint8_t val,
244 const char* tooltip = nullptr) {
245 if (ImGui::Button(label)) {
246 state.paint_collision_value = val;
247 state.paint_collision_erase = false;
248 }
249 gui::AutoRegisterLastItem("button", id);
250 if (tooltip && ImGui::IsItemHovered()) {
251 ImGui::SetTooltip("%s", tooltip);
252 }
253 };
254 quick_button(
255 "Floor Override (00)", "quick_collision_floor_00", 0x00,
256 "Forces walkable floor (0x00) over solid object tiles. Use Erase "
257 "Override (or Alt-drag) to remove an override instead.");
258 ImGui::SameLine();
259 quick_button("Solid (02)", "quick_collision_solid_02", 0x02);
260 ImGui::SameLine();
261 quick_button("D.Water (08)", "quick_collision_dwater_08", 0x08);
262 quick_button("S.Water (09)", "quick_collision_swater_09", 0x09);
263 ImGui::SameLine();
264 quick_button("Pit (1B)", "quick_collision_pit_1b", 0x1B);
265 ImGui::SameLine();
266 quick_button("Spikes (0E)", "quick_collision_spikes_0e", 0x0E);
267 }
268
269 ImGui::Separator();
270 ImGui::BeginDisabled(!collision_save_supported);
271 if (ImGui::Button(tr("Clear All Custom Collision"))) {
272 if (interaction_) {
274 }
275 // Clearing should remove the override (room falls back to vanilla).
276 room.custom_collision() = {};
277 room.MarkCustomCollisionDirty();
281 }
282 if (interaction_) {
284 }
286 }
287 gui::AutoRegisterLastItem("button", "clear_all_custom_collision");
288 ImGui::EndDisabled();
289 } else {
290 ImGui::TextWrapped(
291 tr("Custom collision allows you to override the physics of individual "
292 "8x8 tiles in the room. This is useful for creating water, pits, or "
293 "walkable floor passages over solid objects."));
294 }
295}
296
298 const std::vector<zelda3::CustomCollisionRoomEntry>& entries,
299 std::string_view source_label) {
300 if (!viewer_ || !viewer_->rooms() || !viewer_->rom() ||
301 !viewer_->rom()->is_loaded()) {
302 last_io_error_ = "No dungeon rooms loaded.";
303 last_io_status_.clear();
304 return absl::FailedPreconditionError(last_io_error_);
305 }
306 auto* rooms = viewer_->rooms();
308 auto status = interaction_->ApplyCustomCollisionImportBatch(entries);
309 if (!status.ok()) {
310 last_io_error_ = std::string(status.message());
311 last_io_status_.clear();
312 return status;
313 }
314 } else {
315 const int active_room_id = viewer_->current_room_id();
316 for (const auto& entry : entries) {
317 if (entry.room_id < 0 ||
318 entry.room_id >= static_cast<int>(rooms->size())) {
319 last_io_error_ = absl::StrFormat(
320 "Collision import room 0x%03X is out of range", entry.room_id);
321 last_io_status_.clear();
322 return absl::OutOfRangeError(last_io_error_);
323 }
324 }
325 for (const auto& entry : entries) {
326 const bool is_active = (entry.room_id == active_room_id);
327 if (is_active && interaction_) {
329 }
330 ApplyRoomEntry(entry, &(*rooms)[entry.room_id]);
331 if (is_active && interaction_) {
333 }
334 }
335 }
338 absl::StrFormat("Imported %zu room(s) from %s", entries.size(),
339 std::string(source_label).c_str());
340 last_io_error_.clear();
341 return absl::OkStatus();
342}
343
345 std::string_view json_content, std::string_view source_label) {
346 auto rooms_or =
347 zelda3::LoadCustomCollisionRoomsFromJsonString(std::string(json_content));
348 if (!rooms_or.ok()) {
349 last_io_error_ = std::string(rooms_or.status().message());
350 last_io_status_.clear();
351 return rooms_or.status();
352 }
353 return ImportCollisionRooms(*rooms_or, source_label);
354}
355
357 const DungeonRoomStore& rooms, const std::string& path) {
358 auto exported_or = CollectRoomEntries(rooms);
359 if (!exported_or.ok()) {
360 last_io_error_ = std::string(exported_or.status().message());
361 last_io_status_.clear();
362 return exported_or.status();
363 }
364 auto json_or = zelda3::DumpCustomCollisionRoomsToJsonString(*exported_or);
365 if (!json_or.ok()) {
366 last_io_error_ = std::string(json_or.status().message());
367 last_io_status_.clear();
368 return json_or.status();
369 }
370 std::ofstream file(path);
371 if (!file.is_open()) {
372 last_io_error_ = absl::StrFormat("Cannot write file: %s", path.c_str());
373 last_io_status_.clear();
374 return absl::PermissionDeniedError(last_io_error_);
375 }
376 file << *json_or;
377 file.close();
378 if (!file) {
379 last_io_error_ = absl::StrFormat("Failed writing file: %s", path.c_str());
380 last_io_status_.clear();
381 return absl::DataLossError(last_io_error_);
382 }
383 last_io_status_ = absl::StrFormat("Exported %zu room(s) to %s",
384 exported_or->size(), path.c_str());
385 last_io_error_.clear();
386 return absl::OkStatus();
387}
388
389absl::StatusOr<std::vector<zelda3::CustomCollisionRoomEntry>>
391 std::vector<zelda3::CustomCollisionRoomEntry> out;
392 out.reserve(16);
393 Rom* rom = rooms.rom();
394 const bool can_read_rom =
395 rom != nullptr && rom->is_loaded() &&
397 for (int rid = 0; rid < static_cast<int>(rooms.size()); ++rid) {
398 const zelda3::Room* materialized = rooms.GetIfMaterialized(rid);
399 const zelda3::CustomCollisionMap* map_ptr = nullptr;
400 zelda3::CustomCollisionMap loaded_from_rom;
401 if (materialized != nullptr &&
402 (materialized->IsLoaded() || materialized->custom_collision_dirty() ||
403 materialized->has_custom_collision())) {
404 map_ptr = &materialized->custom_collision();
405 } else if (can_read_rom) {
406 auto map_or = zelda3::LoadCustomCollisionMap(rom, rid);
407 if (!map_or.ok()) {
408 return absl::Status(
409 map_or.status().code(),
410 absl::StrFormat("Room 0x%03X: %s", rid, map_or.status().message()));
411 }
412 if (map_or->has_data) {
413 loaded_from_rom = std::move(*map_or);
414 map_ptr = &loaded_from_rom;
415 }
416 }
417 if (map_ptr == nullptr || !map_ptr->has_data) {
418 continue;
419 }
420
421 // Export explicit overrides, including floor (00), without inherited cells.
422 bool any = false;
424 entry.room_id = rid;
425 for (size_t off = 0; off < map_ptr->tiles.size(); ++off) {
426 if (!map_ptr->HasOverride(off)) {
427 continue;
428 }
429 any = true;
431 static_cast<uint16_t>(off), map_ptr->tiles[off]});
432 }
433 if (!any) {
434 continue;
435 }
436 out.push_back(std::move(entry));
437 }
438 return out;
439}
440
443 if (room == nullptr) {
444 return;
445 }
446 room->custom_collision() = {};
447 for (const auto& t : entry.tiles) {
448 if (t.offset >= room->custom_collision().tiles.size()) {
449 continue;
450 }
451 room->custom_collision().SetOverride(t.offset, t.value);
452 }
454}
455
456} // namespace yaze::editor
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
const auto & vector() const
Definition rom.h:217
bool is_loaded() const
Definition rom.h:199
static absl::StatusOr< std::vector< zelda3::CustomCollisionRoomEntry > > CollectRoomEntries(const DungeonRoomStore &rooms)
DungeonObjectInteraction * interaction_
absl::Status ImportCollisionRooms(const std::vector< zelda3::CustomCollisionRoomEntry > &entries, std::string_view source_label="import")
absl::Status ExportCollisionToFile(const DungeonRoomStore &rooms, const std::string &path)
absl::Status ImportCollisionJson(std::string_view json_content, std::string_view source_label="import")
static void ApplyRoomEntry(const zelda3::CustomCollisionRoomEntry &entry, zelda3::Room *room)
void Draw(bool *p_open) override
Draw the panel content.
absl::Status ApplyCustomCollisionImportBatch(const std::vector< zelda3::CustomCollisionRoomEntry > &entries)
zelda3::Room * GetIfMaterialized(int room_id)
void SetMode(InteractionMode mode)
Set interaction mode.
InteractionMode GetMode() const
Get current interaction mode.
ModeState & GetModeState()
Get mutable reference to mode state.
RAII scope that enables automatic widget registration.
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:559
bool custom_collision_dirty() const
Definition room.h:596
bool IsLoaded() const
Definition room.h:970
void MarkCustomCollisionDirty()
Definition room.h:598
bool has_custom_collision() const
Definition room.h:563
#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_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.
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
std::string LoadFile(const std::string &filename)
Loads the entire contents of a file into a string.
Definition file_util.cc:63
constexpr int kCustomCollisionDataSoftEnd
absl::StatusOr< std::vector< CustomCollisionRoomEntry > > LoadCustomCollisionRoomsFromJsonString(const std::string &json_content)
absl::StatusOr< std::string > DumpCustomCollisionRoomsToJsonString(const std::vector< CustomCollisionRoomEntry > &rooms)
absl::StatusOr< CustomCollisionMap > LoadCustomCollisionMap(Rom *rom, int room_id)
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:18
std::array< uint8_t, 64 *64 > tiles
bool HasOverride(size_t offset) const
void SetOverride(size_t offset, uint8_t tile)
std::vector< CustomCollisionTileEntry > tiles
static const std::vector< std::string > & GetTileTypeNames()
Automatic widget registration helpers for ImGui Test Engine integration.