95 return absl::FailedPreconditionError(
96 "Minecart nodes require the upper layer");
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");
102 return absl::InvalidArgumentError(
103 "Cart identity or switch channel is 0..31");
105 uint8_t sprite_id = 0;
106 switch (brush.
kind) {
110 return absl::InvalidArgumentError(
111 "Room zero is reserved for inactive cart starts");
113 return absl::InvalidArgumentError(
"Choose a cart stop B7..BA");
121 return absl::InvalidArgumentError(
"Choose a switch track D0..D3");
124 return absl::InvalidArgumentError(
"Unknown minecart node kind");
130 return absl::FailedPreconditionError(
131 "Collision changed; start the node move again");
133 return absl::FailedPreconditionError(
134 "Move carts with their existing Move Cart start operation");
139 return absl::InvalidArgumentError(
140 "A node move needs its 16 pixel source and original collision map");
143 std::nullopt, std::nullopt};
144 std::optional<size_t> source_index;
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 ||
151 if (source_index || sprite.id() != sprite_id ||
152 sprite.subtype() != brush.
id ||
154 return absl::FailedPreconditionError(
155 "The source node changed or is not unique; start the move again");
159 return absl::FailedPreconditionError(
160 "The source node is no longer present on the upper layer");
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");
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)
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");
178 sprite.subtype() == brush.
id)
179 return absl::FailedPreconditionError(
180 "This cart identity is already placed; move its existing start");
182 sprite.subtype() == brush.
id)
183 return absl::FailedPreconditionError(
184 "This room already has a lever for that switch channel");
187 const size_t offset = tile_y * 64 + tile_x;
188 if (candidate.collision.HasOverride(offset)) {
189 const auto value = candidate.collision.tiles[offset];
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");
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 =
207 (room.
id() >> 4) * 512 + tile_y * 8};
210 auto& sprite = candidate.sprites[*source_index];
211 sprite.set_x(tile_x / 2);
212 sprite.set_y(tile_y / 2);
219 tile_x, tile_y, 1, brush.collision,
220 MinecartCollisionSource{brush.source_x, brush.source_y}}));
223 candidate.sprites.emplace_back(sprite_id, tile_x / 2, tile_y / 2, brush.id,
226 candidate.collision.SetOverride(offset, brush.collision);
231absl::Status DungeonEditorV2::PlaceMinecartNode(
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();
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();
254 const auto preflight_node_sprites =
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");
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()});
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()) *
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;
302 if (candidate.track_before) {
305 candidate.track_after->id));
308 forward ? *candidate.track_before : *candidate.track_after,
309 forward ? *candidate.track_after : *candidate.track_before));
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();
329 const char* description = brush_snapshot.kind == MinecartNodeKind::kCartStart
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();