yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_room_transfer.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <utility>
5
6#include "absl/strings/str_format.h"
8#include "util/macro.h"
11
12namespace yaze::editor {
13namespace {
14
17 return a == b;
18}
19
21 const zelda3::WaterFillZoneMap& b) {
22 return a.has_data == b.has_data && a.tiles == b.tiles &&
24}
25
26// A portable document intentionally omits physical block-table identities.
27// Retain the destination's exact identities when its authored object/chest
28// contents already match, instead of turning a no-op into a structural edit.
30 const DungeonSelectionEditState& target) {
31 if (source.objects.size() != target.objects.size())
32 return false;
34 normalized.objects = source.objects;
35 normalized.chests = source.chests;
36 for (size_t i = 0; i < normalized.objects.size(); ++i) {
37 normalized.objects[i].set_block_load_order(
38 target.objects[i].block_load_order());
39 }
40 return (ChangedDungeonSelectionDomains(normalized, target) &
42}
43
45 const DungeonSelectionEditState& contents, size_t previous_object_count) {
46 if (contents.objects.size() > zelda3::kMaxTileObjects &&
47 contents.objects.size() > previous_object_count) {
48 return absl::ResourceExhaustedError(absl::StrFormat(
49 "Room replacement has %zu objects; keep at most %zu or do not grow "
50 "the destination's existing %zu objects",
51 contents.objects.size(), zelda3::kMaxTileObjects,
52 previous_object_count));
53 }
54 if (contents.chests.size() > zelda3::kMaxChests) {
55 return absl::ResourceExhaustedError(
56 "Changed room replacements support at most six chest records; "
57 "preserve legacy contents or exclude objects and chest rewards");
58 }
61 // New room contents must not take the chest planner's unchanged fast path.
62 std::vector<std::optional<size_t>> origins(contents.objects.size());
63 const auto chest_plan =
64 zelda3::PlanChestObjectEdit({}, {}, contents.objects, origins);
65 return chest_plan.status();
66}
67
68absl::Status ValidateDomains(const DungeonRoomDocument& document,
69 uint16_t domains) {
70 if (document.source_room_id < 0 ||
72 return absl::InvalidArgumentError(
73 "Room document has an invalid source room ID");
74 }
75 const auto& contents = document.contents;
76 if (domains & kTransferObjects) {
77 if (contents.objects.size() > kMaxDungeonRoomDocumentObjects ||
78 contents.chests.size() > kMaxDungeonRoomDocumentChestRecords) {
79 return absl::ResourceExhaustedError(
80 "Room document object or chest-record resource limit exceeded");
81 }
82 for (const auto& object : contents.objects) {
83 const int options = static_cast<int>(object.options());
84 const bool torch = (options & 8) != 0;
85 const bool block = (options & 4) != 0;
86 if (options < 0 || (options & ~63) != 0 || (torch && block)) {
87 return absl::InvalidArgumentError(
88 "Object has unsupported option flags");
89 }
90 if (zelda3::UsesRoomObjectStream(object)) {
92 } else if (object.x() > (torch ? 62 : 63) ||
93 object.y() > (torch ? 62 : 63) || object.GetLayerValue() > 1 ||
94 object.size() != 0 || object.id_ != (torch ? 0x150 : 0xE00)) {
95 return absl::InvalidArgumentError(
96 "Special object has unsupported ROM fields");
97 }
98 if (object.block_behavior_layer() > 1 ||
99 object.torch_reserved_bit() > 1 ||
100 object.block_load_order() < zelda3::RoomObject::kBlockLoadOrderNew ||
101 object.block_load_order() > 65535) {
102 return absl::InvalidArgumentError(
103 "Special object metadata is out of range");
104 }
105 }
106 }
107 if (domains & kTransferDoors) {
108 if (contents.doors.size() > zelda3::kMaxDoors) {
109 return absl::ResourceExhaustedError("Room door limit exceeded");
110 }
111 for (const auto& door : contents.doors) {
112 const unsigned type = static_cast<unsigned>(door.type);
113 if (door.position > 15 || static_cast<unsigned>(door.direction) > 3 ||
114 type > 0x66 || (type & 1) != 0) {
115 return absl::InvalidArgumentError(
116 "Door contains unsupported ROM fields");
117 }
118 const auto bytes = door.EncodeBytes();
119 if (bytes.first != door.byte1 || bytes.second != door.byte2) {
120 return absl::InvalidArgumentError(
121 "Door raw bytes do not match its encodable properties");
122 }
123 }
124 }
125 if (domains & kTransferSprites) {
126 if (contents.sprites.size() > zelda3::kMaxTotalSprites) {
127 return absl::ResourceExhaustedError("Room sprite limit exceeded");
128 }
129 for (const auto& sprite : contents.sprites) {
131 sprite.id, sprite.x, sprite.y, sprite.subtype, sprite.layer,
132 sprite.key_drop));
133 }
134 }
135 if (domains & kTransferItems) {
136 if (contents.items.size() > 4096) {
137 return absl::ResourceExhaustedError("Room pot item limit exceeded");
138 }
139 for (const auto& item : contents.items) {
140 // Preserve the encoded tilemap address and layer/control bits. The
141 // current position widget's limited pixel decoder is not a ROM validity
142 // test (for example, vanilla lower-layer positions include bit 0x2000).
143 if (item.position == 0xFFFF) {
144 return absl::InvalidArgumentError(
145 "Pot item position collides with the stream terminator");
146 }
147 }
148 }
149 if (domains & kTransferMetadata) {
150 const auto& m = document.metadata;
151 if (m.layout > 7 || m.floor1 > 15 || m.floor2 > 15 ||
152 static_cast<unsigned>(m.bg2) > 8 || m.layer2_mode > 7 ||
153 static_cast<unsigned>(m.collision) > 7 || m.pit_target_layer > 3 ||
154 m.layer_merging.ID > 8 || m.layer_merging.Name.size() > 64 ||
155 std::any_of(m.staircase_planes.begin(), m.staircase_planes.end(),
156 [](uint8_t value) { return value > 3; })) {
157 return absl::InvalidArgumentError(
158 "Room metadata contains unencodable fields");
159 }
160 // LoadRoomHeaderFromRom and SetBg2 derive this tuple from the same header
161 // byte. A dark room still retains its hidden three-bit BG2 mode.
162 const bool dark = m.bg2 == background2::DarkRoom;
163 const auto& expected_merge =
164 zelda3::kLayerMergeTypeList[dark ? 8 : m.layer2_mode];
165 if (m.is_dark != dark || m.is_light != dark ||
166 (!dark && static_cast<uint8_t>(m.bg2) != m.layer2_mode) ||
167 m.layer_merging != expected_merge) {
168 return absl::InvalidArgumentError(
169 "Room BG2 mode, darkness and derived layer settings disagree");
170 }
171 }
172 if ((domains & kTransferCollision) && !document.collision.has_data &&
173 document.collision.HasOverrides()) {
174 return absl::InvalidArgumentError("Collision tiles require has_data=true");
175 }
176 if (domains & kTransferWater) {
177 const auto& water = document.water;
178 const bool any = std::any_of(water.tiles.begin(), water.tiles.end(),
179 [](uint8_t value) { return value != 0; });
180 if (water.has_data != any ||
181 std::any_of(water.tiles.begin(), water.tiles.end(),
182 [](uint8_t value) { return value > 1; }) ||
183 (water.sram_bit_mask & (water.sram_bit_mask - 1)) != 0) {
184 return absl::InvalidArgumentError(
185 "Water fill needs boolean tiles and a single SRAM bit");
186 }
187 }
188 return absl::OkStatus();
189}
190
191} // namespace
192
194 DungeonRoomDocument result;
195 result.source_room_id = room.id();
197 // RoomObject is copyable but contains rendering caches and a borrowed ROM.
198 // Rebuild each record from authored fields for a detached document.
199 for (auto& source : result.contents.objects) {
200 zelda3::RoomObject object(source.id_, source.x_, source.y_, source.size_,
201 source.GetLayerValue());
202 object.set_options(source.options());
203 object.all_bgs_ = source.all_bgs_;
204 object.lit_ = source.lit_;
205 object.set_block_load_order(source.block_load_order());
206 object.set_block_behavior_layer(source.block_behavior_layer());
207 object.set_torch_reserved_bit(source.torch_reserved_bit());
208 source = std::move(object);
209 }
210 result.metadata = room.CaptureMetadataSnapshot();
211 result.collision = room.custom_collision();
212 result.water = room.water_fill_zone();
213 return result;
214}
215
217 const DungeonRoomDocument& b) {
219 a.metadata == b.metadata && SameCollision(a.collision, b.collision) &&
220 SameWater(a.water, b.water);
221}
222
226
228 RETURN_IF_ERROR(ValidateDomains(document, kTransferAll));
229 return ValidateObjectReplacement(document.contents, 0);
230}
231
233 const DungeonRoomDocument& document) {
234 return ValidateDomains(document, kTransferAll);
235}
236
237absl::StatusOr<DungeonRoomTransferPlan> PlanDungeonRoomTransfer(
238 const zelda3::Room& target, const DungeonRoomDocument& source,
240 if (target.id() < 0 || target.id() >= zelda3::kNumberOfRooms ||
241 (options.domains & ~kTransferAll) != 0) {
242 return absl::InvalidArgumentError(
243 "Invalid room transfer destination or domains");
244 }
245 RETURN_IF_ERROR(ValidateDomains(source, options.domains));
246 // Interchange/undo retain unknown legacy bytes; new authoring cannot choose
247 // graphics past the table consumed by Room::LoadRoomGraphics (ID + 64).
248 if ((options.domains & kTransferMetadata) &&
250 source.metadata.spriteset != target.spriteset()) {
251 return absl::InvalidArgumentError(
252 "Dungeon sprite graphics must be in range 0x00..0x4F");
253 }
255 plan.rom = target.rom();
256 plan.target_room_id = target.id();
257 plan.options = options;
258 plan.before = CaptureDungeonRoomDocument(target);
259 plan.after = plan.before;
260 auto& after = plan.after;
261 if ((options.domains & kTransferObjects) &&
262 !SamePortableObjectContents(source.contents, plan.before.contents)) {
263 RETURN_IF_ERROR(ValidateObjectReplacement(
264 source.contents, plan.before.contents.objects.size()));
265 after.contents.objects = source.contents.objects;
266 for (auto& object : after.contents.objects) {
267 object = object.CopyForNewPlacement();
268 object.SetRom(nullptr);
269 }
270 after.contents.chests = source.contents.chests;
271 }
272 if (options.domains & kTransferDoors)
273 after.contents.doors = source.contents.doors;
274 if (options.domains & kTransferSprites)
275 after.contents.sprites = source.contents.sprites;
276 if (options.domains & kTransferItems)
277 after.contents.items = source.contents.items;
278 if (options.domains & kTransferMetadata) {
279 after.metadata = source.metadata;
280 if (!options.copy_destinations) {
281 after.metadata.holewarp = plan.before.metadata.holewarp;
282 after.metadata.pit_target_layer = plan.before.metadata.pit_target_layer;
283 after.metadata.staircase_rooms = plan.before.metadata.staircase_rooms;
284 after.metadata.staircase_planes = plan.before.metadata.staircase_planes;
285 }
286 }
287 if (options.domains & kTransferCollision)
288 after.collision = source.collision;
289 if (options.domains & kTransferWater)
290 after.water = source.water;
291 RETURN_IF_ERROR(ValidateDomains(after, options.domains));
292 return plan;
293}
294
296 const DungeonRoomDocument& document) {
297 auto contents = document.contents;
298 for (auto& object : contents.objects)
299 object.SetRom(room.rom());
300 ApplyDungeonSelectionEditState(room, contents,
303 room.RestoreMetadataSnapshot(document.metadata);
304 if (!SameCollision(room.custom_collision(), document.collision)) {
305 room.custom_collision() = document.collision;
307 }
308 if (!SameWater(room.water_fill_zone(), document.water)) {
309 room.ClearWaterFillZone();
311 for (size_t tile = 0; tile < document.water.tiles.size(); ++tile) {
312 if (document.water.tiles[tile] != 0) {
313 room.SetWaterFillTile(static_cast<int>(tile % 64),
314 static_cast<int>(tile / 64), true);
315 }
316 }
317 // Keep the cached count from the setters, then restore exact legacy bytes
318 // for history. Newly imported documents already require boolean tiles.
319 room.water_fill_zone() = document.water;
320 room.MarkWaterFillDirty();
321 }
322}
323
324} // namespace yaze::editor
static absl::Status ValidateSpriteProperties(uint8_t id, int x, int y, int subtype, int layer, int key_drop)
static constexpr int kBlockLoadOrderNew
const CustomCollisionMap & custom_collision() const
Definition room.h:559
void set_water_fill_sram_bit_mask(uint8_t mask)
Definition room.h:647
void RestoreMetadataSnapshot(const MetadataSnapshot &snapshot)
Definition room.cc:886
const WaterFillZoneMap & water_fill_zone() const
Definition room.h:601
void ClearWaterFillZone()
Definition room.h:634
auto rom() const
Definition room.h:1082
uint8_t spriteset() const
Definition room.h:1018
void MarkWaterFillDirty()
Definition room.h:656
void MarkCustomCollisionDirty()
Definition room.h:598
MetadataSnapshot CaptureMetadataSnapshot() const
Definition room.cc:861
void SetWaterFillTile(int x, int y, bool filled)
Definition room.h:611
int id() const
Definition room.h:1012
absl::Status ValidateDomains(const DungeonRoomDocument &document, uint16_t domains)
absl::Status ValidateObjectReplacement(const DungeonSelectionEditState &contents, size_t previous_object_count)
bool SameCollision(const zelda3::CustomCollisionMap &a, const zelda3::CustomCollisionMap &b)
bool SamePortableObjectContents(const DungeonSelectionEditState &source, const DungeonSelectionEditState &target)
bool SameWater(const zelda3::WaterFillZoneMap &a, const zelda3::WaterFillZoneMap &b)
Editors are the view controllers for the application.
constexpr uint16_t kTransferMetadata
constexpr uint16_t kTransferAll
constexpr uint16_t kTransferItems
absl::Status ValidateDungeonRoomDocument(const DungeonRoomDocument &document)
constexpr uint16_t kTransferDoors
DungeonSelectionEditState CaptureDungeonSelectionEditState(const zelda3::Room &room)
constexpr uint16_t kTransferCollision
constexpr uint8_t kSelectionSprites
constexpr uint8_t kSelectionObjects
constexpr uint16_t kTransferSprites
absl::StatusOr< DungeonRoomTransferPlan > PlanDungeonRoomTransfer(const zelda3::Room &target, const DungeonRoomDocument &source, DungeonRoomTransferOptions options)
constexpr size_t kMaxDungeonRoomDocumentObjects
uint8_t ChangedDungeonSelectionDomains(const DungeonSelectionEditState &a, const DungeonSelectionEditState &b)
constexpr uint8_t kSelectionDoors
bool SameDungeonRoomDocument(const DungeonRoomDocument &a, const DungeonRoomDocument &b)
void ApplyDungeonSelectionEditState(zelda3::Room &room, const DungeonSelectionEditState &state, uint8_t domains)
absl::Status ValidateDungeonRoomDocumentForInterchange(const DungeonRoomDocument &document)
constexpr uint16_t kTransferWater
void ApplyDungeonRoomDocument(zelda3::Room &room, const DungeonRoomDocument &document)
DungeonRoomDocument CaptureDungeonRoomDocument(const zelda3::Room &room)
constexpr uint16_t kTransferObjects
constexpr uint8_t kSelectionItems
constexpr size_t kMaxDungeonRoomDocumentChestRecords
absl::Status ValidateChestObjectMapping(const std::vector< RoomObject > &objects, const std::vector< chest_data > &chests)
Definition chest_edit.cc:74
constexpr int kMaxDungeonSpriteset
constexpr size_t kMaxTileObjects
bool UsesRoomObjectStream(const RoomObject &object)
constexpr size_t kMaxDoors
constexpr int kNumberOfRooms
absl::Status ValidateRoomObjectStreamEntryForSave(const RoomObject &object)
constexpr size_t kMaxChests
constexpr size_t kMaxTotalSprites
absl::StatusOr< std::vector< chest_data > > PlanChestObjectEdit(const std::vector< RoomObject > &before_objects, const std::vector< chest_data > &before_chests, const std::vector< RoomObject > &after_objects, const std::vector< std::optional< size_t > > &source_indices, const std::vector< std::optional< chest_data > > &chest_overrides)
Definition chest_edit.cc:94
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
zelda3::CustomCollisionMap collision
zelda3::Room::MetadataSnapshot metadata
std::vector< zelda3::Room::Door > doors
std::vector< DungeonSpriteSnapshot > sprites
std::vector< zelda3::RoomObject > objects
std::array< uint8_t, 4 > staircase_planes
Definition room.h:252
std::array< uint8_t, 4 > staircase_rooms
Definition room.h:251
std::array< uint8_t, 64 *64 > tiles
Definition room.h:223