yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_minecart_nodes.cc
Go to the documentation of this file.
2
3#include <functional>
4#include <memory>
5#include <utility>
6
7#include "absl/status/status.h"
11#include "util/macro.h"
13
14namespace yaze::editor {
15namespace {
16
17bool IsRail(uint8_t value) {
18 return value >= 0xB0 && value <= 0xB6;
19}
20
22 const Rom& rom, int target_room,
23 int identity) {
24 for (int id = 0; id < zelda3::kNumberOfRooms; ++id) {
25 if (id == target_room)
26 continue;
27 if (const auto* authored = rooms.GetIfMaterialized(id);
28 authored &&
29 (authored->AreSpritesLoaded() || authored->sprites_dirty())) {
30 for (const auto& sprite : authored->GetSprites()) {
31 if (!sprite.deleted() && sprite.id() == 0xA3 &&
32 sprite.subtype() == identity)
33 return absl::FailedPreconditionError(
34 "This cart identity is already used in another room");
35 }
36 continue;
37 }
38 // Strict read-only parsing rejects malformed unopened streams rather than
39 // treating an unreadable room as evidence that an identity is unused.
41 const auto& bytes = record.encoded_stream;
42 for (size_t cursor = 1; cursor + 2 < bytes.size(); cursor += 3) {
43 if (bytes[cursor] == 0xFF)
44 break;
45 const int subtype =
46 ((bytes[cursor + 1] & 0xE0) >> 5) | ((bytes[cursor] & 0x60) >> 2);
47 if (bytes[cursor + 2] == 0xA3 && subtype == identity)
48 return absl::FailedPreconditionError(
49 "This cart identity is already used in another room");
50 }
51 }
52 return absl::OkStatus();
53}
54
55bool SameSprites(const std::vector<zelda3::Sprite>& lhs,
56 const std::vector<zelda3::Sprite>& rhs) {
57 if (lhs.size() != rhs.size())
58 return false;
59 for (size_t i = 0; i < lhs.size(); ++i) {
60 const auto& a = lhs[i];
61 const auto& b = rhs[i];
62 if (a.id() != b.id() || a.x() != b.x() || a.y() != b.y() ||
63 a.subtype() != b.subtype() || a.layer() != b.layer() ||
64 a.key_drop() != b.key_drop() || a.deleted() != b.deleted())
65 return false;
66 }
67 return true;
68}
69
70class MinecartNodeAction final : public UndoAction {
71 public:
72 using Restore = std::function<absl::Status(bool)>;
73 MinecartNodeAction(std::string description, Restore restore, size_t bytes)
74 : description_(std::move(description)),
75 restore_(std::move(restore)),
76 bytes_(bytes) {}
77 absl::Status Undo() override { return restore_(false); }
78 absl::Status Redo() override { return restore_(true); }
79 std::string Description() const override { return description_; }
80 size_t MemoryUsage() const override { return bytes_; }
81
82 private:
83 std::string description_;
85 size_t bytes_;
86};
87
88} // namespace
89
90absl::StatusOr<MinecartNodePlacement> PreviewMinecartNodePlacement(
91 const zelda3::Room& room, int tile_x, int tile_y,
92 const MinecartNodeBrush& brush, std::span<const MinecartTrack> tracks,
93 int layer) {
94 if (layer != 0)
95 return absl::FailedPreconditionError(
96 "Minecart nodes require the upper layer");
97 if (room.id() < 0 || room.id() >= zelda3::kNumberOfRooms || tile_x < 0 ||
98 tile_y < 0 || tile_x > 62 || tile_y > 62 || tile_x % 2 || tile_y % 2)
99 return absl::InvalidArgumentError(
100 "Place the node inside the room on the 16 pixel sprite grid");
101 if (brush.id < 0 || brush.id >= static_cast<int>(kMinecartTrackSlotCount))
102 return absl::InvalidArgumentError(
103 "Cart identity or switch channel is 0..31");
104
105 uint8_t sprite_id = 0;
106 switch (brush.kind) {
108 sprite_id = 0xA3;
109 if (room.id() == 0)
110 return absl::InvalidArgumentError(
111 "Room zero is reserved for inactive cart starts");
112 if (brush.collision < 0xB7 || brush.collision > 0xBA)
113 return absl::InvalidArgumentError("Choose a cart stop B7..BA");
114 break;
116 sprite_id = 0xAF;
117 break;
119 sprite_id = 0xB0;
120 if (brush.collision < 0xD0 || brush.collision > 0xD3)
121 return absl::InvalidArgumentError("Choose a switch track D0..D3");
122 break;
123 default:
124 return absl::InvalidArgumentError("Unknown minecart node kind");
125 }
126
127 const bool moving = brush.source_x != -1 || brush.source_y != -1;
128 if (brush.expected_collision &&
129 !(room.custom_collision() == *brush.expected_collision))
130 return absl::FailedPreconditionError(
131 "Collision changed; start the node move again");
132 if (moving && brush.kind == MinecartNodeKind::kCartStart)
133 return absl::FailedPreconditionError(
134 "Move carts with their existing Move Cart start operation");
135 if (moving &&
136 (brush.source_x < 0 || brush.source_y < 0 || brush.source_x > 62 ||
137 brush.source_y > 62 || brush.source_x % 2 || brush.source_y % 2 ||
138 !brush.expected_collision))
139 return absl::InvalidArgumentError(
140 "A node move needs its 16 pixel source and original collision map");
141
142 MinecartNodePlacement candidate{room.GetSprites(), room.custom_collision(),
143 std::nullopt, std::nullopt};
144 std::optional<size_t> source_index;
145 if (moving) {
146 for (size_t i = 0; i < candidate.sprites.size(); ++i) {
147 const auto& sprite = candidate.sprites[i];
148 if (sprite.deleted() || sprite.layer() != 0 ||
149 sprite.x() * 2 != brush.source_x || sprite.y() * 2 != brush.source_y)
150 continue;
151 if (source_index || sprite.id() != sprite_id ||
152 sprite.subtype() != brush.id ||
153 sprite.key_drop() != brush.source_key_drop)
154 return absl::FailedPreconditionError(
155 "The source node changed or is not unique; start the move again");
156 source_index = i;
157 }
158 if (!source_index)
159 return absl::FailedPreconditionError(
160 "The source node is no longer present on the upper layer");
162 const size_t source_offset = brush.source_y * 64 + brush.source_x;
163 if (!candidate.collision.HasOverride(source_offset) ||
164 candidate.collision.tiles[source_offset] != brush.collision)
165 return absl::FailedPreconditionError(
166 "The switch track no longer matches its exact D0..D3 source cell");
167 }
168 }
169 for (size_t i = 0; i < candidate.sprites.size(); ++i) {
170 const auto& sprite = candidate.sprites[i];
171 if (sprite.deleted() || source_index == i)
172 continue;
173 if (sprite.layer() == 0 && sprite.x() * 2 == tile_x &&
174 sprite.y() * 2 == tile_y)
175 return absl::FailedPreconditionError(
176 "A sprite already occupies this node");
177 if (sprite.id() == 0xA3 && brush.kind == MinecartNodeKind::kCartStart &&
178 sprite.subtype() == brush.id)
179 return absl::FailedPreconditionError(
180 "This cart identity is already placed; move its existing start");
181 if (sprite.id() == 0xAF && brush.kind == MinecartNodeKind::kSwitch &&
182 sprite.subtype() == brush.id)
183 return absl::FailedPreconditionError(
184 "This room already has a lever for that switch channel");
185 }
186
187 const size_t offset = tile_y * 64 + tile_x;
188 if (candidate.collision.HasOverride(offset)) {
189 const auto value = candidate.collision.tiles[offset];
190 const bool lever = brush.kind == MinecartNodeKind::kSwitch;
191 if ((lever && value != 0) ||
192 (!lever && !IsRail(value) && value != brush.collision))
193 return absl::FailedPreconditionError(
194 "Other collision occupies this node; choose a clear anchor");
195 }
196 if (brush.kind == MinecartNodeKind::kCartStart) {
197 if (tracks.size() != kMinecartTrackSlotCount)
198 return absl::FailedPreconditionError(
199 "Load the complete 32 slot minecart source first");
200 const auto& before = tracks[brush.id];
201 if (before.id != brush.id || before.room_id != 0)
202 return absl::FailedPreconditionError(
203 "Choose a reserved cart slot; move existing starts with Move Cart");
204 candidate.track_before = before;
205 candidate.track_after =
206 MinecartTrack{brush.id, room.id(), (room.id() & 15) * 512 + tile_x * 8,
207 (room.id() >> 4) * 512 + tile_y * 8};
208 }
209 if (source_index) {
210 auto& sprite = candidate.sprites[*source_index];
211 sprite.set_x(tile_x / 2);
212 sprite.set_y(tile_y / 2);
215 candidate.collision,
217 candidate.collision,
219 tile_x, tile_y, 1, brush.collision,
220 MinecartCollisionSource{brush.source_x, brush.source_y}}));
221 }
222 } else {
223 candidate.sprites.emplace_back(sprite_id, tile_x / 2, tile_y / 2, brush.id,
224 0);
225 if (brush.kind != MinecartNodeKind::kSwitch)
226 candidate.collision.SetOverride(offset, brush.collision);
227 }
228 return candidate;
229}
230
231absl::Status DungeonEditorV2::PlaceMinecartNode(
232 int room_id, int tile_x, int tile_y, const MinecartNodeBrush& brush) {
233 // Finalization can retire the canvas gesture that owns the caller's brush.
234 const auto brush_snapshot = brush;
235 FinalizePendingUndoActions();
236 auto* panel = minecart_track_editor_panel_;
237 auto* room = rooms_.GetIfLoaded(room_id);
238 if (!panel || !room || !rom_ || !rom_->is_loaded() || room->rom() != rom_)
239 return absl::FailedPreconditionError(
240 "Load the project's minecart document and target room first");
241 if (auto* viewer = GetViewerForRoom(room_id);
242 viewer && viewer->header_read_only())
243 return absl::FailedPreconditionError("This room is read only");
244 const auto& tracks = panel->GetTracks();
245 ASSIGN_OR_RETURN(auto candidate,
246 PreviewMinecartNodePlacement(*room, tile_x, tile_y,
247 brush_snapshot, tracks));
248 if (!candidate.track_before &&
249 SameSprites(room->GetSprites(), candidate.sprites) &&
250 room->custom_collision() == candidate.collision)
251 return absl::OkStatus();
252 // Coordinated node candidates use the same stream validation service while
253 // generic sprite transforms remain guarded against separating B0/collision.
254 const auto preflight_node_sprites =
255 [this,
256 room_id](const std::vector<zelda3::Sprite>& sprites) -> absl::Status {
257 const auto* target = rooms_.GetIfLoaded(room_id);
258 if (!target || target->rom() != rom_)
259 return absl::FailedPreconditionError("Node room is no longer loaded");
260 auto state = CaptureDungeonSelectionEditState(*target);
261 state.sprites.clear();
262 for (const auto& sprite : sprites) {
263 state.sprites.push_back({sprite.id(), sprite.x(), sprite.y(),
264 sprite.subtype(), sprite.layer(),
265 sprite.key_drop(), sprite.deleted()});
266 }
267 return PreflightStreamMutation(room_id, state, kSelectionSprites);
268 };
269 RETURN_IF_ERROR(preflight_node_sprites(candidate.sprites));
270 const auto sprites_before = room->GetSprites();
271 const auto collision_before = room->custom_collision();
272 auto* source_rom = rom_;
273 const uint64_t generation = minecart_collision_generation_;
274 const uint64_t source_generation = panel->track_generation();
275 const size_t bytes = sizeof(collision_before) + sizeof(candidate.collision) +
276 (sprites_before.size() + candidate.sprites.size()) *
277 sizeof(zelda3::Sprite);
278 auto restore =
279 [this, room_id, source_rom, panel, generation, source_generation,
280 sprites_before, collision_before, preflight_node_sprites,
281 candidate = std::move(candidate)](bool forward) -> absl::Status {
282 auto* current = rooms_.GetIfLoaded(room_id);
283 if (rom_ != source_rom || !current || current->rom() != source_rom ||
284 minecart_collision_generation_ != generation ||
285 minecart_track_editor_panel_ != panel ||
286 panel->track_generation() != source_generation)
287 return absl::FailedPreconditionError(
288 "Minecart node belongs to a different project generation");
289 if (auto* viewer = GetViewerForRoom(room_id);
290 viewer && viewer->header_read_only())
291 return absl::FailedPreconditionError("This room is read only");
292 const auto& expected_sprites = forward ? sprites_before : candidate.sprites;
293 const auto& expected_collision =
294 forward ? collision_before : candidate.collision;
295 if (!SameSprites(current->GetSprites(), expected_sprites) ||
296 !(current->custom_collision() == expected_collision))
297 return absl::FailedPreconditionError(
298 "Minecart node sprites or collision changed outside Undo");
299 const auto& replacement_sprites =
300 forward ? candidate.sprites : sprites_before;
301 RETURN_IF_ERROR(preflight_node_sprites(replacement_sprites));
302 if (candidate.track_before) {
303 if (forward)
304 RETURN_IF_ERROR(ValidateUnusedCartIdentity(rooms_, *rom_, room_id,
305 candidate.track_after->id));
306 RETURN_IF_ERROR(panel->RestoreTrackDraft(
307 source_generation,
308 forward ? *candidate.track_before : *candidate.track_after,
309 forward ? *candidate.track_after : *candidate.track_before));
310 }
311 // All fallible work is complete. Publish only the in-memory candidate.
312 current->GetSprites() = replacement_sprites;
313 current->MarkSpritesDirty();
314 const auto& replacement_collision =
315 forward ? candidate.collision : collision_before;
316 if (!(current->custom_collision() == replacement_collision))
317 RestoreRoomCustomCollision(room_id, replacement_collision);
318 panel->InvalidateRoomAudit();
319 if (candidate.track_before)
320 panel->MarkPairedTrackDraft();
321 rom_->set_dirty(true);
322 current->MarkObjectsDirty();
323 InvalidateConnectedRoomGraphs();
324 if (auto* viewer = GetViewerForRoom(room_id))
325 viewer->TriggerChangePing();
326 return absl::OkStatus();
327 };
328 RETURN_IF_ERROR(restore(true));
329 const char* description = brush_snapshot.kind == MinecartNodeKind::kCartStart
330 ? "Place cart start"
331 : brush_snapshot.kind == MinecartNodeKind::kSwitch
332 ? "Place minecart switch"
333 : "Place switch track";
334 undo_manager_.Push(std::make_unique<MinecartNodeAction>(
335 description, std::move(restore), bytes));
336 return absl::OkStatus();
337}
338
339} // 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
zelda3::Room * GetIfMaterialized(int room_id)
Abstract base for all undoable actions (Command pattern)
Definition undo_action.h:20
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")
const CustomCollisionMap & custom_collision() const
Definition room.h:559
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
int id() const
Definition room.h:1012
A class for managing sprites in the overworld and underworld.
Definition sprite.h:39
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::Status ValidateUnusedCartIdentity(const DungeonRoomStore &rooms, const Rom &rom, int target_room, int identity)
bool SameSprites(const std::vector< zelda3::Sprite > &lhs, const std::vector< zelda3::Sprite > &rhs)
Editors are the view controllers for the application.
absl::StatusOr< MinecartNodePlacement > PreviewMinecartNodePlacement(const zelda3::Room &room, int tile_x, int tile_y, const MinecartNodeBrush &brush, std::span< const MinecartTrack > tracks, int layer)
DungeonSelectionEditState CaptureDungeonSelectionEditState(const zelda3::Room &room)
constexpr uint8_t kSelectionSprites
constexpr size_t kMinecartTrackSlotCount
absl::StatusOr< zelda3::CustomCollisionMap > PreviewMinecartCollisionStamp(const zelda3::CustomCollisionMap &before, const MinecartCollisionStamp &stamp)
constexpr int kNumberOfRooms
absl::StatusOr< DungeonStreamRecord > ReadDungeonSpriteStream(const Rom &rom, int room_id)
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::optional< zelda3::CustomCollisionMap > expected_collision