yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
overworld_undo_actions.h
Go to the documentation of this file.
1#ifndef YAZE_APP_EDITOR_OVERWORLD_UNDO_ACTIONS_H_
2#define YAZE_APP_EDITOR_OVERWORLD_UNDO_ACTIONS_H_
3
4#include <array>
5#include <chrono>
6#include <cstddef>
7#include <cstdint>
8#include <functional>
9#include <optional>
10#include <string>
11#include <unordered_map>
12#include <utility>
13#include <vector>
14
15#include "absl/status/status.h"
16#include "absl/strings/str_format.h"
19#include "util/macro.h"
22
23namespace yaze {
24namespace editor {
25
30 int x = 0;
31 int y = 0;
32 int old_tile_id = 0;
33 int new_tile_id = 0;
34};
35
48 public:
50 static constexpr int kMergeWindowMs = 500;
51
62 std::vector<OverworldTileChange> tile_changes,
63 zelda3::Overworld* overworld,
64 std::function<void()> refresh_fn,
65 std::function<void(int)> refresh_map_fn = {},
66 bool allow_merge = true)
67 : allow_merge_(allow_merge),
71 overworld_(overworld),
72 refresh_fn_(std::move(refresh_fn)),
73 refresh_map_fn_(std::move(refresh_map_fn)),
74 timestamp_(std::chrono::steady_clock::now()) {}
75
76 absl::Status Undo() override {
77 if (!overworld_) {
78 return absl::InternalError("Overworld pointer is null");
79 }
80 auto& world_tiles = overworld_->GetMapTiles(world_);
81 for (const auto& change : tile_changes_) {
82 world_tiles[change.x][change.y] = change.old_tile_id;
83 }
85 return absl::OkStatus();
86 }
87
88 absl::Status Redo() override {
89 if (!overworld_) {
90 return absl::InternalError("Overworld pointer is null");
91 }
92 auto& world_tiles = overworld_->GetMapTiles(world_);
93 for (const auto& change : tile_changes_) {
94 world_tiles[change.x][change.y] = change.new_tile_id;
95 }
97 return absl::OkStatus();
98 }
99
100 std::string Description() const override {
101 return absl::StrFormat("Paint %d tile%s on map %d", tile_changes_.size(),
102 tile_changes_.size() == 1 ? "" : "s", map_id_);
103 }
104
105 size_t MemoryUsage() const override {
106 return sizeof(*this) + tile_changes_.size() * sizeof(OverworldTileChange);
107 }
108
109 bool CanMergeWith(const UndoAction& prev) const override {
110 if (!allow_merge_)
111 return false;
112 const auto* prev_paint =
113 dynamic_cast<const OverworldTilePaintAction*>(&prev);
114 if (!prev_paint)
115 return false;
116 if (!prev_paint->allow_merge_)
117 return false;
118 if (prev_paint->overworld_ != overworld_)
119 return false;
120 if (prev_paint->world_ != world_)
121 return false;
122
123 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
124 timestamp_ - prev_paint->timestamp_);
125 return elapsed.count() <= kMergeWindowMs;
126 }
127
128 void MergeWith(UndoAction& prev) override {
129 auto& prev_paint = static_cast<OverworldTilePaintAction&>(prev);
130
131 // Build a map of (x,y) -> index in our tile_changes_ for fast lookup
132 // so we can keep the earliest old_tile_id for coordinates that appear
133 // in both actions.
134 std::unordered_map<uint64_t, size_t> coord_index;
135 for (size_t i = 0; i < tile_changes_.size(); ++i) {
136 const uint64_t key = CoordinateKey(tile_changes_[i]);
137 coord_index[key] = i;
138 }
139
140 for (const auto& prev_change : prev_paint.tile_changes_) {
141 const uint64_t key = CoordinateKey(prev_change);
142 auto it = coord_index.find(key);
143 if (it != coord_index.end()) {
144 // Same coordinate exists in both: keep the older old_tile_id
145 tile_changes_[it->second].old_tile_id = prev_change.old_tile_id;
146 } else {
147 // Coordinate only in prev: adopt it as-is
148 coord_index[key] = tile_changes_.size();
149 tile_changes_.push_back(prev_change);
150 }
151 }
152
153 // Keep the earlier timestamp so subsequent merges measure from
154 // the start of the combined stroke.
155 timestamp_ = prev_paint.timestamp_;
156 }
157
158 int map_id() const { return map_id_; }
159 int world() const { return world_; }
160 const std::vector<OverworldTileChange>& tile_changes() const {
161 return tile_changes_;
162 }
163
164 private:
165 static uint64_t CoordinateKey(const OverworldTileChange& change) {
166 return (static_cast<uint64_t>(static_cast<uint32_t>(change.x)) << 32) |
167 static_cast<uint32_t>(change.y);
168 }
169
170 static std::vector<OverworldTileChange> NormalizeChanges(
171 std::vector<OverworldTileChange> changes) {
172 std::unordered_map<uint64_t, size_t> coordinate_indices;
173 std::vector<OverworldTileChange> normalized;
174 normalized.reserve(changes.size());
175 for (const auto& change : changes) {
176 const auto [it, inserted] =
177 coordinate_indices.emplace(CoordinateKey(change), normalized.size());
178 if (inserted) {
179 normalized.push_back(change);
180 } else {
181 // A drag can revisit a tile. Undo restores its value before the stroke,
182 // while redo restores the final value from that stroke.
183 normalized[it->second].new_tile_id = change.new_tile_id;
184 }
185 }
186 return normalized;
187 }
188
189 void RefreshChangedMaps() const {
190 if (!refresh_map_fn_) {
191 if (refresh_fn_)
192 refresh_fn_();
193 return;
194 }
195 // Derive this set at replay time: MergeWith can add changes on other maps.
196 std::array<bool, zelda3::kNumOverworldMaps> changed_maps{};
197 for (const auto& change : tile_changes_) {
198 const int map_id = world_ * 0x40 + change.x / 32 + (change.y / 32) * 8;
200 changed_maps[map_id] = true;
201 }
202 for (int map_id = 0; map_id < zelda3::kNumOverworldMaps; ++map_id) {
203 if (changed_maps[map_id])
205 }
206 }
207
211 std::vector<OverworldTileChange> tile_changes_;
213 std::function<void()> refresh_fn_; // callback to refresh map visuals
214 std::function<void(int)> refresh_map_fn_;
215 std::chrono::steady_clock::time_point timestamp_;
216};
217
224 std::vector<zelda3::OverworldItem> items;
225 std::optional<zelda3::OverworldItem> selected_item_identity;
226};
227
228// Fixed table rows are restored by value, never through a retained element
229// pointer (a load or vector replacement can invalidate those pointers).
230template <typename Record>
232 public:
233 using RestoreFn = std::function<absl::Status(const Record&)>;
234 OverworldRecordEditAction(Record before, Record after, RestoreFn restore,
235 std::string description)
236 : before_(std::move(before)),
237 after_(std::move(after)),
238 restore_(std::move(restore)),
239 description_(std::move(description)) {}
240 absl::Status Undo() override { return Restore(before_); }
241 absl::Status Redo() override { return Restore(after_); }
242 std::string Description() const override { return description_; }
243 size_t MemoryUsage() const override { return sizeof(*this); }
244
245 private:
246 absl::Status Restore(const Record& value) {
247 if (!restore_)
248 return absl::FailedPreconditionError("Record owner is gone");
249 return restore_(value);
250 }
251 Record before_;
252 Record after_;
254 std::string description_;
255};
256
264 public:
265 using RestoreFn = std::function<void(const OverworldItemsSnapshot&)>;
266
268 OverworldItemsSnapshot after, RestoreFn restore,
269 std::string description)
270 : before_(std::move(before)),
271 after_(std::move(after)),
272 restore_(std::move(restore)),
273 description_(std::move(description)) {}
274
275 absl::Status Undo() override {
276 if (!restore_) {
277 return absl::InternalError(
278 "OverworldItemsEditAction: no restore callback");
279 }
281 return absl::OkStatus();
282 }
283
284 absl::Status Redo() override {
285 if (!restore_) {
286 return absl::InternalError(
287 "OverworldItemsEditAction: no restore callback");
288 }
290 return absl::OkStatus();
291 }
292
293 std::string Description() const override { return description_; }
294
295 size_t MemoryUsage() const override {
296 const size_t before_size =
297 before_.items.size() * sizeof(zelda3::OverworldItem);
298 const size_t after_size =
299 after_.items.size() * sizeof(zelda3::OverworldItem);
300 return sizeof(*this) + before_size + after_size;
301 }
302
303 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
304
305 private:
309 std::string description_;
310};
311
313 public:
314 using ApplyFn = std::function<absl::Status(const OverworldPropertyEdit&)>;
315
317 OverworldPropertyEdit after, ApplyFn apply,
318 std::string description)
319 : before_(std::move(before)),
320 after_(std::move(after)),
321 apply_(std::move(apply)),
322 description_(std::move(description)) {}
323
324 absl::Status Undo() override {
325 if (!apply_) {
326 return absl::InternalError(
327 "OverworldMapPropertyEditAction: no apply callback");
328 }
329 return apply_(before_);
330 }
331
332 absl::Status Redo() override {
333 if (!apply_) {
334 return absl::InternalError(
335 "OverworldMapPropertyEditAction: no apply callback");
336 }
337 return apply_(after_);
338 }
339
340 std::string Description() const override { return description_; }
341
342 size_t MemoryUsage() const override {
343 return sizeof(*this) + before_.description.size() +
344 after_.description.size() + description_.size();
345 }
346
347 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
348
349 private:
353 std::string description_;
354};
355
357 public:
358 using ApplyFn = std::function<absl::Status(const OverworldPropertyEdit&)>;
359
360 OverworldMapPropertyBatchEditAction(std::vector<OverworldPropertyEdit> before,
361 std::vector<OverworldPropertyEdit> after,
362 ApplyFn apply, std::string description)
363 : before_(std::move(before)),
364 after_(std::move(after)),
365 apply_(std::move(apply)),
366 description_(std::move(description)) {}
367
368 absl::Status Undo() override {
369 if (!apply_) {
370 return absl::InternalError(
371 "OverworldMapPropertyBatchEditAction: no apply callback");
372 }
373 for (auto it = before_.rbegin(); it != before_.rend(); ++it) {
375 }
376 return absl::OkStatus();
377 }
378
379 absl::Status Redo() override {
380 if (!apply_) {
381 return absl::InternalError(
382 "OverworldMapPropertyBatchEditAction: no apply callback");
383 }
384 for (const auto& edit : after_) {
385 RETURN_IF_ERROR(apply_(edit));
386 }
387 return absl::OkStatus();
388 }
389
390 std::string Description() const override { return description_; }
391
392 size_t MemoryUsage() const override {
393 size_t total = sizeof(*this) + description_.size();
394 for (const auto& edit : before_) {
395 total += sizeof(edit) + edit.description.size();
396 }
397 for (const auto& edit : after_) {
398 total += sizeof(edit) + edit.description.size();
399 }
400 return total;
401 }
402
403 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
404
405 private:
406 std::vector<OverworldPropertyEdit> before_;
407 std::vector<OverworldPropertyEdit> after_;
409 std::string description_;
410};
411
413 public:
414 using ApplyFn = std::function<absl::Status(const std::string&)>;
415
416 OverworldProjectLabelEditAction(std::string before, std::string after,
417 ApplyFn apply, std::string description)
418 : before_(std::move(before)),
419 after_(std::move(after)),
420 apply_(std::move(apply)),
421 description_(std::move(description)) {}
422
423 absl::Status Undo() override {
424 if (!apply_) {
425 return absl::InternalError(
426 "OverworldProjectLabelEditAction: no apply callback");
427 }
428 return apply_(before_);
429 }
430
431 absl::Status Redo() override {
432 if (!apply_) {
433 return absl::InternalError(
434 "OverworldProjectLabelEditAction: no apply callback");
435 }
436 return apply_(after_);
437 }
438
439 std::string Description() const override { return description_; }
440
441 size_t MemoryUsage() const override {
442 return sizeof(*this) + before_.size() + after_.size() + description_.size();
443 }
444
445 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
446
447 private:
448 std::string before_;
449 std::string after_;
451 std::string description_;
452};
453
454} // namespace editor
455} // namespace yaze
456
457#endif // YAZE_APP_EDITOR_OVERWORLD_UNDO_ACTIONS_H_
Undoable action for overworld item mutations.
bool CanMergeWith(const UndoAction &) const override
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
OverworldItemsEditAction(OverworldItemsSnapshot before, OverworldItemsSnapshot after, RestoreFn restore, std::string description)
std::function< void(const OverworldItemsSnapshot &)> RestoreFn
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")
std::function< absl::Status(const OverworldPropertyEdit &)> ApplyFn
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
OverworldMapPropertyBatchEditAction(std::vector< OverworldPropertyEdit > before, std::vector< OverworldPropertyEdit > after, ApplyFn apply, std::string description)
bool CanMergeWith(const UndoAction &) const override
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
bool CanMergeWith(const UndoAction &) const override
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
OverworldMapPropertyEditAction(OverworldPropertyEdit before, OverworldPropertyEdit after, ApplyFn apply, std::string description)
std::function< absl::Status(const OverworldPropertyEdit &)> ApplyFn
OverworldProjectLabelEditAction(std::string before, std::string after, ApplyFn apply, std::string description)
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
std::function< absl::Status(const std::string &)> ApplyFn
bool CanMergeWith(const UndoAction &) const override
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
OverworldRecordEditAction(Record before, Record after, RestoreFn restore, std::string description)
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 Restore(const Record &value)
std::function< absl::Status(const Record &)> RestoreFn
Undoable action for painting tiles on the overworld map.
size_t MemoryUsage() const override
Approximate memory footprint for budget enforcement.
void MergeWith(UndoAction &prev) override
bool CanMergeWith(const UndoAction &prev) const override
static constexpr int kMergeWindowMs
Merge window: consecutive paints within this duration become one step.
std::string Description() const override
Human-readable description (e.g., "Paint 12 tiles on map 5")
std::vector< OverworldTileChange > tile_changes_
const std::vector< OverworldTileChange > & tile_changes() const
static uint64_t CoordinateKey(const OverworldTileChange &change)
static std::vector< OverworldTileChange > NormalizeChanges(std::vector< OverworldTileChange > changes)
std::chrono::steady_clock::time_point timestamp_
OverworldTilePaintAction(int map_id, int world, std::vector< OverworldTileChange > tile_changes, zelda3::Overworld *overworld, std::function< void()> refresh_fn, std::function< void(int)> refresh_map_fn={}, bool allow_merge=true)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
OverworldBlockset & GetMapTiles(int world_type)
Definition overworld.h:754
constexpr int kNumOverworldMaps
Definition common.h:85
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Snapshot of overworld item list + current item selection.
std::vector< zelda3::OverworldItem > items
std::optional< zelda3::OverworldItem > selected_item_identity
A single tile coordinate + old/new value pair for undo/redo.