yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_group_editor_main.cc
Go to the documentation of this file.
2
3#include <utility>
4
5#include "absl/strings/str_format.h"
10#include "imgui/imgui.h"
11#include "util/macro.h"
13
14namespace yaze::editor {
15namespace {
16
18 public:
19 using Apply = std::function<absl::Status(uint8_t, uint8_t)>;
20 GraphicsSheetAssignmentAction(const char* kind, int group, int slot,
21 uint8_t before, uint8_t after, Apply apply)
22 : description_(absl::StrFormat("%s graphics group %02X slot %d", kind,
23 group, slot)),
24 before_(before),
25 after_(after),
26 apply_(std::move(apply)) {}
27 absl::Status Undo() override { return apply_(after_, before_); }
28 absl::Status Redo() override { return apply_(before_, after_); }
29 std::string Description() const override { return description_; }
30 size_t MemoryUsage() const override { return sizeof(*this); }
31
32 private:
33 std::string description_;
34 uint8_t before_;
35 uint8_t after_;
37};
38
39void DrawIds(const char* label, const std::vector<int>& ids, int digits) {
40 ImGui::TextWrapped("%s (%zu)", label, ids.size());
41 std::string text;
42 for (const int id : ids) {
43 if (!text.empty())
44 text += ", ";
45 text += absl::StrFormat("%0*X", digits, id);
46 }
47 ImGui::TextWrapped("%s", text.empty() ? "None" : text.c_str());
48}
49
50} // namespace
51
54 for (auto& preview : blockset_previews_)
56 for (auto& preview : roomset_previews_)
58 for (auto& preview : spriteset_previews_)
60}
61
63 show_sheet_picker_ = false;
64 pending_sheet_ = -1;
65 picker_group_ = -1;
66 for (auto& [id, preview] : picker_previews_)
68 picker_previews_.clear();
69 assignment_error_.clear();
70}
71
73 if (rom_ != rom) {
74 ++assignment_session_->generation;
76 usage_areas_.reset();
77 usage_rooms_.reset();
78 current_palette_ = nullptr;
79 }
80 rom_ = rom;
82}
83
85 // EditorSet rebinds after ROM replacement/load, including in-place reloads
86 // and rollback where both pointer addresses are unchanged. Every binding
87 // notification starts a new assignment generation.
88 ++assignment_session_->generation;
90 current_palette_ = nullptr;
91 game_data_ = data;
92 assignment_session_->data = data;
93}
94
96 SetRom(nullptr);
97 SetGameData(nullptr);
98 undo_ = nullptr;
99 main_usage_ = {};
100 roomset_usage_ = {};
101 workspace_ = nullptr;
102}
103
104absl::Status GfxGroupEditor::EditMainGraphicsSheet(int group, int slot,
105 int sheet) {
106 return EditGraphicsSheet(GroupKind::kMain, group, slot, sheet);
107}
108
109absl::Status GfxGroupEditor::EditRoomsetGraphicsSheet(int group, int entry,
110 int sheet) {
111 return EditGraphicsSheet(GroupKind::kRoomset, group, entry, sheet);
112}
113
114absl::Status GfxGroupEditor::EditGraphicsSheet(GroupKind kind, int group,
115 int slot, int sheet) {
116 if (!rom_ || !rom_->is_loaded() || !game_data_ ||
118 return absl::FailedPreconditionError("No editable graphics session");
119 }
120 const bool roomset = kind == GroupKind::kRoomset;
121 const int groups = roomset ? game_data_->room_blockset_ids.size()
123 if (group < 0 || group >= groups || slot < 0 || slot >= (roomset ? 4 : 8) ||
124 sheet < 0 || sheet >= zelda3::GraphicsSheetStore::kSheetCount) {
125 return absl::InvalidArgumentError(
126 "Choose a valid graphics group, slot and sheet");
127 }
128 if (!(roomset && sheet == 0) &&
129 (!game_data_->sheet_store.HasSheet(sheet) ||
132 return absl::InvalidArgumentError(
133 "Backgrounds require a loaded 3bpp sheet");
134 }
135 const uint8_t before = roomset ? game_data_->room_blockset_ids[group][slot]
136 : game_data_->main_blockset_ids[group][slot];
137 const auto after = static_cast<uint8_t>(sheet);
138 if (before == after)
139 return absl::OkStatus();
140 const std::weak_ptr<AssignmentSession> weak = assignment_session_;
141 auto apply = [weak, generation = assignment_session_->generation, group, slot,
142 roomset](uint8_t expected, uint8_t value) -> absl::Status {
143 const auto session = weak.lock();
144 if (!session || session->generation != generation || !session->rom ||
145 !session->rom->is_loaded() || !session->data ||
146 !session->data->gfx_groups_loaded) {
147 return absl::FailedPreconditionError(
148 "Graphics assignment belongs to another session");
149 }
150 auto& assigned = roomset ? session->data->room_blockset_ids[group][slot]
151 : session->data->main_blockset_ids[group][slot];
152 if (assigned != expected) {
153 return absl::FailedPreconditionError(
154 "Graphics assignment changed elsewhere; Undo was not applied");
155 }
156 assigned = value;
157 // Rooms and maps compare resolved source IDs before rendering. Leave their
158 // selection and entrance context alone; they refresh on their next draw.
159 session->rom->set_dirty(true);
160 return absl::OkStatus();
161 };
162 RETURN_IF_ERROR(apply(before, after));
163 undo_->Push(std::make_unique<GraphicsSheetAssignmentAction>(
164 roomset ? "Roomset" : "Main", group, roomset ? slot + 3 : slot, before,
165 after, std::move(apply)));
166 return absl::OkStatus();
167}
168
170 if (!main_usage_) {
171 ImGui::TextDisabled("Main graphics usage unavailable.");
172 return;
173 }
174 const auto usage = main_usage_(Ws().selected_blockset, selected_main_slot_);
175 if (!usage.ok()) {
176 ImGui::TextWrapped("Usage unavailable: %s",
177 std::string(usage.status().message()).c_str());
178 return;
179 }
180 const auto room_count = usage->explicit_preview_rooms.size() +
181 usage->direct_default_rooms.size() +
182 usage->inferred_default_rooms.size() +
183 usage->fallback_rooms.size();
184 ImGui::TextWrapped("Affects %zu room previews and %zu map screens",
185 room_count, usage->ow_areas.size());
186 ImGui::TextWrapped(
187 "Rooms use current/default entrance previews. This is not a census of "
188 "all in-game entrance routes.");
189 if (usage->slot7_bottom_half_only) {
190 ImGui::TextWrapped(
191 "Map slot 7: bottom half only. The animated top half has a separate "
192 "source.");
193 }
194 if (usage->unscanned_rooms || usage->unscanned_ow_areas) {
195 ImGui::TextWrapped("Incomplete scan: %d rooms / %d maps unavailable.",
196 usage->unscanned_rooms, usage->unscanned_ow_areas);
197 }
198 if (ImGui::TreeNode("Main assignment details")) {
199 ImGui::TextWrapped(
200 "%s", usage->uses_live_overworld
201 ? "Includes unsaved room and map selections."
202 : "Includes unsaved rooms; maps read from ROM until loaded.");
203 if (ImGui::BeginChild("##MainUsage", ImVec2(0, 150), true)) {
204 DrawIds("Explicit room previews", usage->explicit_preview_rooms, 3);
205 DrawIds("Direct entrance defaults", usage->direct_default_rooms, 3);
206 DrawIds("Inferred entrance defaults", usage->inferred_default_rooms, 3);
207 DrawIds("Fallback previews", usage->fallback_rooms, 3);
208 DrawIds("Room previews overridden in this slot", usage->overridden_rooms,
209 3);
210 DrawIds("Affected map screens", usage->ow_areas, 2);
211 DrawIds("Overridden map slots", usage->overridden_ow_areas, 2);
212 DrawIds("Maps using an independent table", usage->independent_ow_areas,
213 2);
214 }
215 ImGui::EndChild();
216 ImGui::TreePop();
217 }
218}
219
221 if (!roomset_usage_) {
222 ImGui::TextDisabled("Roomset usage unavailable.");
223 return;
224 }
225 const auto usage = roomset_usage_(Ws().selected_roomset);
226 if (!usage.ok()) {
227 ImGui::TextWrapped("Usage unavailable: %s",
228 std::string(usage.status().message()).c_str());
229 return;
230 }
231 ImGui::TextWrapped("Shared by %zu rooms and %zu map screens",
232 usage->rooms.size(), usage->ow_areas.size());
233 if (usage->unscanned_rooms || usage->unscanned_ow_areas) {
234 ImGui::TextWrapped("Incomplete scan: %d rooms / %d maps unavailable.",
235 usage->unscanned_rooms, usage->unscanned_ow_areas);
236 }
237 if (ImGui::TreeNode("Roomset assignment details")) {
238 ImGui::TextWrapped(
239 "%s", usage->uses_live_overworld
240 ? "Includes unsaved room and map selections."
241 : "Includes unsaved rooms; maps read from ROM until loaded.");
242 if (ImGui::BeginChild("##RoomsetUsage", ImVec2(0, 150), true)) {
243 DrawIds("Rooms", usage->rooms, 3);
244 DrawIds("Affected map screens", usage->ow_areas, 2);
245 DrawIds("Maps using custom sheets", usage->overridden_ow_areas, 2);
246 DrawIds("Maps using an independent table", usage->independent_ow_areas,
247 2);
248 }
249 ImGui::EndChild();
250 ImGui::TreePop();
251 }
252}
253
255 const bool roomset = kind == GroupKind::kRoomset;
256 const int group = roomset ? Ws().selected_roomset : Ws().selected_blockset;
257 int& selected = roomset ? selected_roomset_entry_ : selected_main_slot_;
258 if (picker_kind_ != kind || (picker_group_ != -1 && picker_group_ != group))
260 picker_kind_ = kind;
261 const auto* entries = roomset ? game_data_->room_blockset_ids[group].data()
262 : game_data_->main_blockset_ids[group].data();
263 for (int slot = 0; slot < (roomset ? 4 : 8); ++slot) {
264 const int display_slot = roomset ? slot + 3 : slot;
265 const auto label = roomset && entries[slot] == 0
266 ? absl::StrFormat("Slot %d / Inherit", display_slot)
267 : absl::StrFormat("Slot %d / Sheet %02X",
268 display_slot, entries[slot]);
269 if (ImGui::Selectable(label.c_str(), selected == slot)) {
270 if (selected != slot)
272 selected = slot;
273 }
275 "selectable",
276 absl::StrFormat("%s slot %d", roomset ? "Roomset" : "Main",
277 display_slot));
278 }
279 if (roomset)
281 else
283 ImGui::BeginDisabled(!undo_ || (roomset ? !roomset_usage_ : !main_usage_) ||
285 if (ImGui::Button("Choose replacement sheet")) {
286 const bool open = !show_sheet_picker_;
288 show_sheet_picker_ = open;
289 picker_group_ = group;
290 picker_expected_sheet_ = entries[selected];
291 }
292 gui::AutoRegisterLastItem("button", "Choose replacement sheet");
293 ImGui::EndDisabled();
296 if (!assignment_error_.empty())
297 ImGui::TextWrapped("%s", assignment_error_.c_str());
298}
299
301 const bool roomset = picker_kind_ == GroupKind::kRoomset;
302 const int selected = roomset ? selected_roomset_entry_ : selected_main_slot_;
303 ImGui::TextWrapped("Preview uses the selected palette or slot default.");
304 if (roomset) {
305 ImGui::TextWrapped(
306 "Inherit uses each room or map's main group. Its appearance can differ "
307 "between areas.");
308 if (ImGui::Selectable("Inherit from main group (00)", pending_sheet_ == 0))
309 pending_sheet_ = 0;
310 gui::AutoRegisterLastItem("selectable", "Inherit from main group");
311 } else {
312 ImGui::TextWrapped("Sheet 00 is a physical sheet, not inheritance.");
313 }
314 if (pending_sheet_ >= 0) {
315 const char* apply_label =
316 roomset ? "Apply to shared roomset" : "Apply to shared main group";
317 if (ImGui::Button(apply_label)) {
318 const auto current =
319 roomset ? game_data_->room_blockset_ids[picker_group_][selected]
321 if (current != picker_expected_sheet_) {
323 "Assignment changed while choosing. Reopen the picker.";
324 } else {
325 const auto status = EditGraphicsSheet(picker_kind_, picker_group_,
326 selected, pending_sheet_);
327 assignment_error_ = status.ok() ? "" : std::string(status.message());
328 if (status.ok())
329 show_sheet_picker_ = false;
330 }
331 }
332 gui::AutoRegisterLastItem("button", apply_label);
333 if (roomset && pending_sheet_ == 0)
334 ImGui::TextUnformatted("Selected: Inherit");
335 else
336 ImGui::Text("Selected sheet %02X", pending_sheet_);
337 }
338 if (ImGui::BeginChild("##SheetPicker", ImVec2(0, 220), true)) {
339 std::vector<int> candidates;
340 for (int id = roomset ? 1 : 0; id < zelda3::GraphicsSheetStore::kSheetCount;
341 ++id) {
345 candidates.push_back(id);
346 }
347 ImGuiListClipper clipper;
348 clipper.Begin(static_cast<int>(candidates.size()), 54.0f);
349 while (clipper.Step()) {
350 for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
351 const int id = candidates[row];
352 auto* preview =
354 roomset ? gfx::RoleForRoomsetSlot(selected)
355 : gfx::RoleForBlocksetSlot(selected),
356 picker_previews_[id]);
357 const auto position = ImGui::GetCursorScreenPos();
358 const auto label = absl::StrFormat("Sheet %02X##replacement", id);
359 ImGui::PushID(id);
360 if (ImGui::Selectable(label.c_str(), pending_sheet_ == id, 0,
361 ImVec2(0, 50)))
362 pending_sheet_ = id;
364 "selectable",
365 absl::StrFormat("%s sheet %02X", roomset ? "Roomset" : "Main", id));
366 if (preview && preview->texture()) {
367 ImGui::GetWindowDrawList()->AddImage(
368 (ImTextureID)preview->texture(),
369 ImVec2(position.x + 84, position.y + 8),
370 ImVec2(position.x + 212, position.y + 40), ImVec2(0, 0),
371 ImVec2(1, 1), ImGui::GetColorU32(ImVec4(1, 1, 1, 1)));
372 }
373 ImGui::PopID();
374 }
375 }
376 }
377 ImGui::EndChild();
378}
379
380} // 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:49
bool is_loaded() const
Definition rom.h:187
std::array< gfx::Bitmap, 4 > roomset_previews_
RoomsetUsageCallback roomset_usage_
absl::Status EditRoomsetGraphicsSheet(int group, int entry, int sheet)
absl::Status EditGraphicsSheet(GroupKind kind, int group, int slot, int sheet)
std::shared_ptr< AssignmentSession > assignment_session_
void DrawAssignmentControls(GroupKind kind)
std::optional< std::map< int, zelda3::OverworldAreaGfxInfo > > usage_areas_
GfxGroupWorkspaceState * workspace_
std::array< gfx::Bitmap, 4 > spriteset_previews_
absl::Status EditMainGraphicsSheet(int group, int slot, int sheet)
std::map< int, gfx::Bitmap > picker_previews_
GfxGroupWorkspaceState & Ws()
gfx::SnesPalette * current_palette_
std::optional< std::map< int, zelda3::RoomGfxInfo > > usage_rooms_
std::array< gfx::Bitmap, 8 > blockset_previews_
void SetGameData(zelda3::GameData *data)
gfx::Bitmap * PrepareSheetPreview(int sheet_id, gfx::SheetRole role, gfx::Bitmap &preview)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
void Push(std::unique_ptr< UndoAction > action)
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
GraphicsSheetAssignmentAction(const char *kind, int group, int slot, uint8_t before, uint8_t after, Apply apply)
void RetireBitmap(Bitmap &bitmap)
Explicitly retire resources owned by a bitmap that is being erased.
Definition arena.cc:63
static Arena & Get()
Definition arena.cc:24
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
static constexpr uint16_t kSheetCount
Locates graphics sheets in the ROM and writes edited sheets back.
void DrawIds(const char *label, const std::vector< int > &ids, int digits)
Editors are the view controllers for the application.
SheetRole RoleForRoomsetSlot(int slot_index)
SheetRole RoleForBlocksetSlot(int slot_index)
void AutoRegisterLastItem(const std::string &widget_type, const std::string &explicit_label, const std::string &description)
Automatically register the last ImGui item.
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::array< std::array< uint8_t, 4 >, kNumRoomBlocksets > room_blockset_ids
Definition game_data.h:98
std::array< std::array< uint8_t, 8 >, kNumMainBlocksets > main_blockset_ids
Definition game_data.h:97
Automatic widget registration helpers for ImGui Test Engine integration.