yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
palette_pixel_command.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <map>
5#include <memory>
6#include <set>
7#include <tuple>
8
11#include "util/macro.h"
12#include "zelda3/game_data.h"
14
15namespace yaze::editor {
16namespace {
17using AddressKey = std::tuple<std::string, int, int>;
25
26absl::Status CheckLiveSource(const PalettePixelPlan& plan, bool redo) {
27 if (plan.targets.empty())
28 return absl::InvalidArgumentError(
29 "Palette/pixel plan has no selected targets");
30 if (!plan.source.resolve_live_source) {
31 return absl::UnimplementedError(
32 "This source has no authoritative live palette/pixel resolver");
33 }
36 if (fresh.owner != plan.source.owner ||
37 fresh.resolved.surface != plan.source.resolved.surface ||
38 fresh.resolved.id != plan.source.resolved.id ||
40 return absl::FailedPreconditionError(
41 "Palette/pixel source session or area changed");
42 }
43 for (const auto index : plan.targets) {
44 if (index >= fresh.targets.size() || index >= plan.source.targets.size())
45 return absl::FailedPreconditionError("Selected source target changed");
46 const auto& old = plan.source.targets[index];
47 const auto& now = fresh.targets[index];
49 if (!old.writable_palette || old.palette.size() != 8 ||
50 old.sheet_id != now.sheet_id || old.tile_index != now.tile_index ||
51 old.source_orientation_mask != now.source_orientation_mask ||
52 old.writable_palette->row != now.writable_palette->row ||
53 old.writable_palette->half_offset !=
54 now.writable_palette->half_offset ||
55 old.writable_palette->colors != now.writable_palette->colors) {
56 return absl::FailedPreconditionError(
57 "Source assignments changed; reopen Graphics from the source");
58 }
59 for (int raw = 0; raw < 8; ++raw) {
60 auto expected = old.palette[raw];
61 if (!redo && raw != 0) {
62 const auto key = Key(*old.writable_palette->colors[raw]);
63 for (const auto& change : plan.colors)
64 if (Key(change) == key)
65 expected = change.new_color;
66 }
67 if (now.palette[raw].snes() != expected.snes())
68 return absl::FailedPreconditionError(
69 "Source colors changed since planning");
70 }
71 }
72 return absl::OkStatus();
73}
74
75class PalettePixelAction final : public UndoAction {
76 public:
78 : state_(&state), plan_(std::move(plan)) {}
79 absl::Status Undo() override {
80 return ExecutePalettePixelPlan(*state_, plan_, false);
81 }
82 absl::Status Redo() override {
83 return ExecutePalettePixelPlan(*state_, plan_, true);
84 }
85 std::string Description() const override {
86 return "Reorder palette and selected pixels";
87 }
88 size_t MemoryUsage() const override {
89 size_t size = plan_.colors.size() * sizeof(gfx::PaletteColorChange);
90 for (const auto& diff : plan_.sheets)
91 size += diff.MemoryUsage();
92 return size;
93 }
94
95 private:
98};
99} // namespace
100
101absl::StatusOr<PalettePixelPlan> PlanPalettePixelPermutation(
102 const GraphicsEditContext& source, const std::vector<size_t>& targets,
103 const PalettePermutation& permutation) {
104 std::set<uint8_t> values(permutation.begin(), permutation.end());
105 if (permutation[0] != 0 || values.size() != 8 || *values.rbegin() != 7)
106 return absl::InvalidArgumentError(
107 "Permutation must be a bijection on 0..7 with zero fixed");
108 if (!source.owner || targets.empty())
109 return absl::InvalidArgumentError(
110 "Select source tiles in a loaded session");
111 PalettePixelPlan plan;
112 plan.source = source;
113 std::set<size_t> selected(targets.begin(), targets.end());
114 plan.targets.assign(selected.begin(), selected.end());
115 std::map<AddressKey, gfx::PaletteColorChange> colors;
116 std::set<std::pair<uint16_t, uint16_t>> tiles;
117 std::map<uint16_t, std::vector<uint8_t>> sheets;
118 for (const auto index : plan.targets) {
119 if (index >= source.targets.size())
120 return absl::OutOfRangeError("Selected source target is unavailable");
121 const auto& target = source.targets[index];
123 if (target.tile_index >= 64 ||
124 !source.owner->sheet_store.HasSheet(target.sheet_id) ||
125 zelda3::GetGfxSheetStorageKind(target.sheet_id) ==
127 return absl::UnimplementedError(
128 "Only loaded static 3bpp tiles can be permuted");
129 }
130 for (int old_raw = 1; old_raw < 8; ++old_raw) {
131 const int new_raw = permutation[old_raw];
132 const auto key = Key(*target.writable_palette->colors[new_raw]);
134 std::get<0>(key), std::get<1>(key), std::get<2>(key),
135 target.palette[new_raw], target.palette[old_raw], 0};
136 const auto [at, inserted] = colors.emplace(key, change);
137 if (!inserted &&
138 (at->second.original_color.snes() != change.original_color.snes() ||
139 at->second.new_color.snes() != change.new_color.snes()))
140 return absl::FailedPreconditionError(
141 "Selected palette bindings require conflicting writes");
142 }
143 if (!tiles.emplace(target.sheet_id, target.tile_index).second)
144 continue;
145 const auto before = source.owner->sheet_store.Sheet(target.sheet_id);
146 auto [at, inserted] =
147 sheets.try_emplace(target.sheet_id, before.begin(), before.end());
148 const int start =
149 (target.tile_index / 16) * 8 * 128 + (target.tile_index % 16) * 8;
150 for (int y = 0; y < 8; ++y) {
151 for (int x = 0; x < 8; ++x) {
152 auto& pixel = at->second[start + y * 128 + x];
153 if (pixel > 7)
154 return absl::FailedPreconditionError("Selected tile is not raw 3bpp");
155 pixel = permutation[pixel];
156 }
157 }
158 }
159 plan.physical_tiles = tiles.size();
160 for (const auto& [key, change] : colors)
161 if (change.original_color.snes() != change.new_color.snes())
162 plan.colors.push_back(change);
163 for (const auto& [sheet, after] : sheets) {
164 auto diff = MakeSheetPixelDiff(
165 sheet, source.owner->sheet_store.Sheet(sheet), after);
166 if (!diff.empty())
167 plan.sheets.push_back(std::move(diff));
168 }
169 return plan;
170}
171
173 GraphicsEditorState& state, const PalettePixelPlan& plan, bool redo,
174 const std::function<absl::Status(size_t)>& after_sheet_write) {
175 auto* store = ActiveSheetStore(state);
176 if (!store || state.game_data != plan.source.owner ||
178 return absl::FailedPreconditionError(
179 "Palette/pixel command session changed or paste is pending");
180 RETURN_IF_ERROR(CheckLiveSource(plan, redo));
181 std::map<uint16_t, std::vector<uint8_t>> next;
182 for (const auto& diff : plan.sheets) {
183 if (!store->HasSheet(diff.sheet) ||
184 diff.offsets.size() != diff.before.size() ||
185 diff.offsets.size() != diff.after.size() || next.contains(diff.sheet))
186 return absl::InvalidArgumentError("Invalid command sheet diff");
187 const auto current = store->Sheet(diff.sheet);
188 auto& pixels = next[diff.sheet];
189 pixels.assign(current.begin(), current.end());
190 for (size_t i = 0; i < diff.offsets.size(); ++i) {
191 const auto offset = diff.offsets[i];
192 if (offset >= pixels.size() || diff.before[i] > 7 || diff.after[i] > 7)
193 return absl::InvalidArgumentError("Invalid command pixel diff");
194 if (pixels[offset] != (redo ? diff.before[i] : diff.after[i]))
195 return absl::FailedPreconditionError(
196 "Selected pixels changed since planning");
197 pixels[offset] = redo ? diff.after[i] : diff.before[i];
198 }
199 if (std::any_of(pixels.begin(), pixels.end(), [](auto p) { return p > 7; }))
200 return absl::FailedPreconditionError("Sheet is not raw 3bpp");
201 }
202 auto changes = plan.colors;
203 if (!redo)
204 for (auto& c : changes)
205 std::swap(c.original_color, c.new_color);
206 const auto before = *store;
208 state.game_data, changes, [&] {
209 size_t written = 0;
210 for (const auto& [sheet, pixels] : next) {
211 auto status = store->WriteSheet(sheet, pixels, 7);
212 if (status.ok() && after_sheet_write)
213 status = after_sheet_write(++written);
214 if (!status.ok()) {
215 *store = before;
216 return status;
217 }
218 }
219 // No fallible operations remain. Observers now see both completed models.
220 for (const auto& [sheet, pixels] : next) {
221 RefreshArenaSheet(state, sheet);
222 state.MarkSheetModified(sheet);
223 }
224 return absl::OkStatus();
225 });
226}
227
229 GraphicsEditorState& state, UndoManager& history,
230 const std::vector<size_t>& targets, const PalettePermutation& permutation) {
231 if (!state.source_context || state.source_context->owner != state.game_data)
232 return absl::FailedPreconditionError(
233 "Open Graphics from a supported live source");
235 *state.source_context, targets, permutation));
236 RETURN_IF_ERROR(ExecutePalettePixelPlan(state, plan, true));
237 if (!plan.colors.empty() || !plan.sheets.empty())
238 history.Push(std::make_unique<PalettePixelAction>(state, std::move(plan)));
239 return absl::OkStatus();
240}
241} // namespace yaze::editor
Shared state between GraphicsEditor panel components.
void MarkSheetModified(uint16_t sheet_id)
Mark a sheet as modified for save tracking.
std::optional< GraphicsEditContext > source_context
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
Manages undo/redo stacks for a single editor.
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")
absl::Status ApplyExternalChanges(const zelda3::GameData *owner, const std::vector< PaletteColorChange > &changes, const std::function< absl::Status()> &participant)
static PaletteManager & Get()
Get the singleton instance.
bool HasSheet(uint16_t sheet) const
True when the buffer holds all 4096 bytes of sheet.
absl::Span< const uint8_t > Sheet(uint16_t sheet) const
The sheet's 4096 pixels, or an empty span when it is not held.
Locates graphics sheets in the ROM and writes edited sheets back.
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status CheckLiveSource(const PalettePixelPlan &plan, bool redo)
AddressKey Key(const zelda3::PaletteColorAddress &a)
Editors are the view controllers for the application.
zelda3::GraphicsSheetStore * ActiveSheetStore(const GraphicsEditorState &state)
absl::Status ExecutePalettePixelPlan(GraphicsEditorState &state, const PalettePixelPlan &plan, bool redo, const std::function< absl::Status(size_t)> &after_sheet_write)
std::array< uint8_t, 8 > PalettePermutation
absl::StatusOr< PalettePixelPlan > PlanPalettePixelPermutation(const GraphicsEditContext &source, const std::vector< size_t > &targets, const PalettePermutation &permutation)
void RefreshArenaSheet(GraphicsEditorState &state, uint16_t sheet)
absl::Status ValidateGraphicsPaletteAddressing(const GraphicsEditTarget &target, const zelda3::GameData *owner)
absl::Status ApplyPalettePixelPermutation(GraphicsEditorState &state, UndoManager &history, const std::vector< size_t > &targets, const PalettePermutation &permutation)
SheetPixelDiff MakeSheetPixelDiff(uint16_t sheet, absl::Span< const uint8_t > before, absl::Span< const uint8_t > after)
The pixels that differ between two 4096-byte sheets.
const char * PaletteAddressGroupName(PaletteAddressGroup group)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
zelda3::ResolvedGraphicsContext resolved
std::vector< GraphicsEditTarget > targets
std::function< absl::Status(GraphicsEditContext &) resolve_live_source)
std::vector< SheetPixelDiff > sheets
std::vector< gfx::PaletteColorChange > colors
Represents a single color change operation.
int color_index
Index of color within palette.
std::string group_name
Palette group name (e.g., "ow_main")
int palette_index
Index of palette within group.