yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_editor_v2_connection_edits.cc
Go to the documentation of this file.
2
3#include <utility>
4
6#include "rom/snes.h"
7#include "util/macro.h"
10
11namespace yaze::editor {
12namespace {
13
14// The legacy room loader has a void object parser. Validate the unopened
15// stream with the strict persistence parser before using that loader, so a
16// truncated return-door list cannot look like an empty destination.
17absl::Status ValidateConnectionRoomSource(const Rom& rom, int room_id) {
18 const auto stream = zelda3::ReadDungeonObjectStream(rom, room_id);
19 if (!stream.ok()) {
20 return absl::FailedPreconditionError(
21 absl::StrFormat("Cannot load connection room %03X: %s", room_id,
22 stream.status().message()));
23 }
24 ASSIGN_OR_RETURN(const auto header_table,
26 const auto header_table_bank = (header_table >> 16) & 0xFF;
27 if ((header_table & 0xFFFF) < 0x8000 || header_table_bank == 0x7E ||
28 header_table_bank == 0x7F || header_table_bank == 0xFE ||
29 header_table_bank == 0xFF) {
30 return absl::FailedPreconditionError("Invalid room header pointer table");
31 }
32 ASSIGN_OR_RETURN(const auto header,
33 rom.ReadWord(SnesToPc(header_table) + room_id * 2));
34 ASSIGN_OR_RETURN(const auto bank,
36 const auto header_pc = SnesToPc((bank << 16) | header);
37 if (header < 0x8000 || bank == 0x7E || bank == 0x7F || bank == 0xFE ||
38 bank == 0xFF || static_cast<size_t>(header_pc) + 14 > rom.size()) {
39 return absl::FailedPreconditionError("Invalid connection room header");
40 }
41 return absl::OkStatus();
42}
43
54
55} // namespace
56
58 if (!rom_ || !rom_->is_loaded() || !IsValidRoomId(room_id)) {
59 return absl::FailedPreconditionError("Connection room is unavailable");
60 }
61 auto* existing = rooms_.GetIfMaterialized(room_id);
62 if (existing && existing->rom() != rom_) {
63 return absl::FailedPreconditionError(
64 "Connection room belongs to another ROM");
65 }
66 if (existing && existing->IsLoaded() && existing->AreObjectsLoaded()) {
67 return absl::OkStatus();
68 }
69 if (existing && existing->HasUnsavedChanges()) {
70 return absl::FailedPreconditionError(
71 "Open the destination room to finish loading its unsaved edits");
72 }
73 RETURN_IF_ERROR(ValidateConnectionRoomSource(*rom_, room_id));
74 auto loaded = zelda3::LoadRoomFromRom(rom_, room_id);
75 if (!loaded.AreObjectsLoaded()) {
76 return absl::FailedPreconditionError(
77 "Connection room objects did not load");
78 }
79 if (existing) {
80 // ReloadWaterFillZones attaches these project overlays before rooms load.
81 // LoadRoomFromRom does not read them, and a later global zone save collects
82 // every materialized room, including clean zones. Preserve the tiles and
83 // mask here; use the setter so the cached tile count remains consistent.
84 const auto& zone = existing->water_fill_zone();
85 loaded.set_water_fill_sram_bit_mask(zone.sram_bit_mask);
86 for (size_t tile = 0; tile < zone.tiles.size(); ++tile) {
87 if (zone.tiles[tile] != 0) {
88 loaded.SetWaterFillTile(static_cast<int>(tile % 64),
89 static_cast<int>(tile / 64), true);
90 }
91 }
92 loaded.ClearWaterFillDirty();
93 }
94 loaded.SetGameData(game_data_);
95 rooms_[room_id] = std::move(loaded);
96 return absl::OkStatus();
97}
98
99absl::StatusOr<DungeonConnectionPlan> DungeonEditorV2::PreviewDoorConnection(
100 const DungeonConnectionRequest& request) {
101 const auto* source = rooms_.GetIfLoaded(request.source_room_id);
102 if (!rom_ || !rom_->is_loaded() || !source || source->rom() != rom_ ||
103 !source->AreObjectsLoaded()) {
104 return absl::FailedPreconditionError(
105 "Source connection room is not loaded");
106 }
107 if (request.source_door_index >= source->GetDoors().size()) {
108 return absl::OutOfRangeError("Selected door is no longer available");
109 }
110 ASSIGN_OR_RETURN(const int target_id,
112 request.source_room_id,
113 source->GetDoors()[request.source_door_index]));
115 return PlanDungeonDoorConnection(*source, *rooms_.GetIfLoaded(target_id),
116 request);
117}
118
120 const DungeonConnectionPlan& preview) {
121 if (preview.rom != rom_) {
122 return absl::FailedPreconditionError(
123 "Connection preview belongs to another ROM");
124 }
125 ASSIGN_OR_RETURN(auto current, PreviewDoorConnection(preview.request));
126 if (!SameConnectionPlan(preview, current)) {
127 return absl::FailedPreconditionError(
128 "Connection changed since preview; review the endpoints again");
129 }
130 if (!current.changed()) {
131 return absl::OkStatus();
132 }
134 // Finishing an active single-door preview can change either endpoint. Never
135 // overwrite that newer edit with the candidate shown before release.
137 if (!SameConnectionPlan(preview, current)) {
138 return absl::FailedPreconditionError(
139 "Connection changed while finishing a gesture; review the endpoints "
140 "again");
141 }
142
143 auto capture = [&](int room_id, const std::vector<zelda3::Room::Door>& before,
144 const std::vector<zelda3::Room::Door>& after) {
146 plan.room_id = room_id;
147 plan.domains = SameDungeonDoors(before, after) ? 0 : kSelectionDoors;
148 plan.before.doors = before;
149 bool captured = false;
150 auto read_selection = [&](DungeonCanvasViewer* viewer) {
151 if (!viewer || captured || viewer->current_room_id() != room_id)
152 return;
153 auto& interaction = viewer->object_interaction();
154 const auto* context =
155 interaction.entity_coordinator().tile_handler().context();
156 if (!context || context->current_room_id != room_id)
157 return;
158 plan.before.selected_objects = interaction.GetSelectedObjectIndices();
160 interaction.entity_coordinator().SelectedEntitiesForEdit();
161 captured = true;
162 };
163 read_selection(workbench_viewer_.get());
164 room_viewers_.ForEach(
165 [&](int, std::unique_ptr<DungeonCanvasViewer>& viewer) {
166 read_selection(viewer.get());
167 });
168 read_selection(workbench_compare_viewer_.get());
169 plan.after = plan.before;
170 plan.after.doors = after;
171 return plan;
172 };
173 std::vector<DungeonSelectionEditPlan> edits;
174 edits.push_back(capture(current.request.source_room_id, current.source_before,
175 current.source_after));
176 edits.push_back(capture(current.target_room_id, current.target_before,
177 current.target_after));
178 // Plan both endpoints together: independent checks can reserve the same
179 // remaining allocation space and admit a connection that cannot be saved.
185 "dungeon connection authoring", "DungeonEditorV2", nullptr));
186 }
187 // Reuse the same masked collection publisher and batch restore as selection
188 // history. Both endpoints become visible before any renderer is refreshed.
191 std::move(edits),
192 absl::StrFormat("Connect rooms %03X and %03X",
193 current.request.source_room_id, current.target_room_id));
194 return absl::OkStatus();
195}
196
197} // 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:49
absl::StatusOr< uint8_t > ReadByte(int offset) const
Definition rom.cc:785
absl::StatusOr< uint16_t > ReadWord(int offset) const
Definition rom.cc:793
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
absl::StatusOr< uint32_t > ReadLong(int offset) const
Definition rom.cc:800
bool loaded() const
Check if the manifest has been loaded.
util::LruCache< int, std::unique_ptr< DungeonCanvasViewer > > room_viewers_
absl::Status ApplyDoorConnection(const DungeonConnectionPlan &plan)
absl::StatusOr< std::vector< std::pair< uint32_t, uint32_t > > > PlanPendingRoomStreamRanges(const std::vector< DungeonSelectionEditPlan > &candidates) const
absl::Status RestoreSelectionEditBatch(const std::vector< DungeonSelectionEditPlan > &plans, bool after)
std::unique_ptr< DungeonCanvasViewer > workbench_compare_viewer_
static bool IsValidRoomId(int room_id)
std::unique_ptr< DungeonCanvasViewer > workbench_viewer_
absl::StatusOr< DungeonConnectionPlan > PreviewDoorConnection(const DungeonConnectionRequest &request)
void PushSelectionUndoBatch(std::vector< DungeonSelectionEditPlan > plans, std::string description)
zelda3::Room * GetIfMaterialized(int room_id)
zelda3::Room * GetIfLoaded(int room_id)
EditorContext context() const
Definition editor.h:364
EditorDependencies dependencies_
Definition editor.h:370
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
bool SameConnectionPlan(const DungeonConnectionPlan &a, const DungeonConnectionPlan &b)
Editors are the view controllers for the application.
bool SameDungeonDoors(const std::vector< Door > &first, const std::vector< Door > &second)
absl::Status ValidateHackManifestSaveConflicts(const core::HackManifest &manifest, project::RomWritePolicy write_policy, const std::vector< std::pair< uint32_t, uint32_t > > &ranges, absl::string_view save_scope, const char *log_tag, ToastManager *toast_manager)
absl::StatusOr< int > DungeonConnectionTargetRoom(int source_room_id, const Door &door)
constexpr uint8_t kSelectionDoors
absl::StatusOr< DungeonConnectionPlan > PlanDungeonDoorConnection(const zelda3::Room &source, const zelda3::Room &target, const DungeonConnectionRequest &request)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
absl::StatusOr< DungeonStreamRecord > ReadDungeonObjectStream(const Rom &rom, int room_id)
constexpr int kRoomHeaderPointer
constexpr int kRoomHeaderPointerBank
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
std::vector< zelda3::Room::Door > target_after
std::vector< zelda3::Room::Door > target_before
std::vector< zelda3::Room::Door > source_before
std::vector< zelda3::Room::Door > source_after
std::vector< SelectedEntity > selected_entities
std::vector< zelda3::Room::Door > doors
project::YazeProject * project
Definition editor.h:174
RomWritePolicy write_policy
Definition project.h:225
core::HackManifest hack_manifest
Definition project.h:360