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
235 public:
236 using RestoreFn = std::function<void(const OverworldItemsSnapshot&)>;
237
239 OverworldItemsSnapshot after, RestoreFn restore,
240 std::string description)
241 : before_(std::move(before)),
242 after_(std::move(after)),
243 restore_(std::move(restore)),
244 description_(std::move(description)) {}
245
246 absl::Status Undo() override {
247 if (!restore_) {
248 return absl::InternalError(
249 "OverworldItemsEditAction: no restore callback");
250 }
252 return absl::OkStatus();
253 }
254
255 absl::Status Redo() override {
256 if (!restore_) {
257 return absl::InternalError(
258 "OverworldItemsEditAction: no restore callback");
259 }
261 return absl::OkStatus();
262 }
263
264 std::string Description() const override { return description_; }
265
266 size_t MemoryUsage() const override {
267 const size_t before_size =
268 before_.items.size() * sizeof(zelda3::OverworldItem);
269 const size_t after_size =
270 after_.items.size() * sizeof(zelda3::OverworldItem);
271 return sizeof(*this) + before_size + after_size;
272 }
273
274 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
275
276 private:
280 std::string description_;
281};
282
284 public:
285 using ApplyFn = std::function<absl::Status(const OverworldPropertyEdit&)>;
286
288 OverworldPropertyEdit after, ApplyFn apply,
289 std::string description)
290 : before_(std::move(before)),
291 after_(std::move(after)),
292 apply_(std::move(apply)),
293 description_(std::move(description)) {}
294
295 absl::Status Undo() override {
296 if (!apply_) {
297 return absl::InternalError(
298 "OverworldMapPropertyEditAction: no apply callback");
299 }
300 return apply_(before_);
301 }
302
303 absl::Status Redo() override {
304 if (!apply_) {
305 return absl::InternalError(
306 "OverworldMapPropertyEditAction: no apply callback");
307 }
308 return apply_(after_);
309 }
310
311 std::string Description() const override { return description_; }
312
313 size_t MemoryUsage() const override {
314 return sizeof(*this) + before_.description.size() +
315 after_.description.size() + description_.size();
316 }
317
318 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
319
320 private:
324 std::string description_;
325};
326
328 public:
329 using ApplyFn = std::function<absl::Status(const OverworldPropertyEdit&)>;
330
331 OverworldMapPropertyBatchEditAction(std::vector<OverworldPropertyEdit> before,
332 std::vector<OverworldPropertyEdit> after,
333 ApplyFn apply, std::string description)
334 : before_(std::move(before)),
335 after_(std::move(after)),
336 apply_(std::move(apply)),
337 description_(std::move(description)) {}
338
339 absl::Status Undo() override {
340 if (!apply_) {
341 return absl::InternalError(
342 "OverworldMapPropertyBatchEditAction: no apply callback");
343 }
344 for (auto it = before_.rbegin(); it != before_.rend(); ++it) {
346 }
347 return absl::OkStatus();
348 }
349
350 absl::Status Redo() override {
351 if (!apply_) {
352 return absl::InternalError(
353 "OverworldMapPropertyBatchEditAction: no apply callback");
354 }
355 for (const auto& edit : after_) {
356 RETURN_IF_ERROR(apply_(edit));
357 }
358 return absl::OkStatus();
359 }
360
361 std::string Description() const override { return description_; }
362
363 size_t MemoryUsage() const override {
364 size_t total = sizeof(*this) + description_.size();
365 for (const auto& edit : before_) {
366 total += sizeof(edit) + edit.description.size();
367 }
368 for (const auto& edit : after_) {
369 total += sizeof(edit) + edit.description.size();
370 }
371 return total;
372 }
373
374 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
375
376 private:
377 std::vector<OverworldPropertyEdit> before_;
378 std::vector<OverworldPropertyEdit> after_;
380 std::string description_;
381};
382
384 public:
385 using ApplyFn = std::function<absl::Status(const std::string&)>;
386
387 OverworldProjectLabelEditAction(std::string before, std::string after,
388 ApplyFn apply, std::string description)
389 : before_(std::move(before)),
390 after_(std::move(after)),
391 apply_(std::move(apply)),
392 description_(std::move(description)) {}
393
394 absl::Status Undo() override {
395 if (!apply_) {
396 return absl::InternalError(
397 "OverworldProjectLabelEditAction: no apply callback");
398 }
399 return apply_(before_);
400 }
401
402 absl::Status Redo() override {
403 if (!apply_) {
404 return absl::InternalError(
405 "OverworldProjectLabelEditAction: no apply callback");
406 }
407 return apply_(after_);
408 }
409
410 std::string Description() const override { return description_; }
411
412 size_t MemoryUsage() const override {
413 return sizeof(*this) + before_.size() + after_.size() + description_.size();
414 }
415
416 bool CanMergeWith(const UndoAction& /*prev*/) const override { return false; }
417
418 private:
419 std::string before_;
420 std::string after_;
422 std::string description_;
423};
424
425} // namespace editor
426} // namespace yaze
427
428#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")
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:721
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.