yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
custom_object.cc
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <iostream>
6
7#include "util/log.h"
8
9namespace yaze {
10namespace zelda3 {
11
12namespace {
13
14bool ResolveCornerOverrideIndex(int object_id, int* out_index) {
15 if (out_index == nullptr) {
16 return false;
17 }
18
19 switch (object_id) {
20 case 0x100:
21 *out_index = 2; // track_corner_TL.bin
22 return true;
23 case 0x101:
24 *out_index = 4; // track_corner_BL.bin
25 return true;
26 case 0x102:
27 *out_index = 3; // track_corner_TR.bin
28 return true;
29 case 0x103:
30 *out_index = 5; // track_corner_BR.bin
31 return true;
32 default:
33 return false;
34 }
35}
36
37} // namespace
38
39const std::vector<std::string> CustomObjectManager::kSubtype1Filenames = {
40 "track_LR.bin", // 00
41 "track_UD.bin", // 01
42 "track_corner_TL.bin", // 02
43 "track_corner_TR.bin", // 03
44 "track_corner_BL.bin", // 04
45 "track_corner_BR.bin", // 05
46 "track_floor_UD.bin", // 06
47 "track_floor_LR.bin", // 07
48 "track_floor_corner_TL.bin", // 08
49 "track_floor_corner_TR.bin", // 09
50 "track_floor_corner_BL.bin", // 10
51 "track_floor_corner_BR.bin", // 11
52 "track_floor_any.bin", // 12
53 "wall_sword_house.bin", // 13
54 "track_any.bin", // 14
55 "small_statue.bin", // 15
56};
57
58const std::vector<std::string> CustomObjectManager::kSubtype2Filenames = {
59 "furnace.bin", // 00
60 "firewood.bin", // 01
61 "ice_chair.bin", // 02
62};
63
65 static CustomObjectManager instance;
66 return instance;
67}
68
69void CustomObjectManager::Initialize(const std::string& custom_objects_folder) {
70 base_path_ = custom_objects_folder;
71 cache_.clear();
72#if !defined(NDEBUG)
73 LOG_INFO("CustomObjectManager", "Initialize: base_path='%s'",
74 base_path_.c_str());
75 // Verify corner override files for object 0x31 (minecart tracks)
76 if (const auto* list = ResolveFileList(0x31)) {
77 LOG_INFO("CustomObjectManager",
78 "Object 0x31 file list has %zu entries (corners need indices 2-5)",
79 list->size());
80 } else {
81 LOG_WARN("CustomObjectManager",
82 "Object 0x31 not mapped - corner overrides 0x100-0x103 will fail");
83 }
84#endif
85}
86
88 const std::unordered_map<int, std::vector<std::string>>& map) {
89 custom_file_map_ = map;
90 cache_.clear();
91}
92
97
98const std::vector<std::string>* CustomObjectManager::ResolveFileList(
99 int object_id) const {
100 auto custom_it = custom_file_map_.find(object_id);
101 if (custom_it != custom_file_map_.end()) {
102 return &custom_it->second;
103 }
104 if (object_id == 0x31) {
105 return &kSubtype1Filenames;
106 }
107 if (object_id == 0x32) {
108 return &kSubtype2Filenames;
109 }
110 return nullptr;
111}
112
114 int resolved_index) const {
115 // Guardrail: subtype-2 corner aliases (0x100..0x103) should only route
116 // through custom payloads when the project explicitly provides an object 0x31
117 // file map. Folder-only custom-object contexts must keep vanilla wall-corner
118 // behavior.
119 const auto custom_it = custom_file_map_.find(0x31);
120 if (custom_it == custom_file_map_.end()) {
121 return false;
122 }
123
124 const auto& list = custom_it->second;
125 if (resolved_index < 0 || resolved_index >= static_cast<int>(list.size())) {
126 return false;
127 }
128
129 const std::string& filename = list[resolved_index];
130 if (filename.empty()) {
131 return false;
132 }
133
134 std::filesystem::path candidate = filename;
135 if (!candidate.is_absolute() && !base_path_.empty()) {
136 candidate = std::filesystem::path(base_path_) / candidate;
137 }
138 return std::filesystem::exists(candidate);
139}
140
141absl::StatusOr<std::shared_ptr<CustomObject>> CustomObjectManager::LoadObject(
142 const std::string& filename) {
143 if (cache_.contains(filename)) {
144 return cache_[filename];
145 }
146
147 // base_path_ should be the full path to the custom objects folder (e.g., Dungeons/Objects/Data)
148 std::filesystem::path full_path =
149 std::filesystem::path(base_path_) / filename;
150
151 std::ifstream file(full_path, std::ios::binary);
152 if (!file) {
153 LOG_ERROR("CustomObjectManager", "Failed to open file: %s",
154 full_path.c_str());
155 return absl::NotFoundError("Could not open file: " + full_path.string());
156 }
157
158 // Read entire file into buffer
159 std::vector<uint8_t> buffer((std::istreambuf_iterator<char>(file)),
160 std::istreambuf_iterator<char>());
161
162 auto object_or_error = ParseBinaryData(buffer);
163 if (!object_or_error.ok()) {
164 return object_or_error.status();
165 }
166
167 auto object_ptr =
168 std::make_shared<CustomObject>(std::move(object_or_error.value()));
169 cache_[filename] = object_ptr;
170
171 return object_ptr;
172}
173
174absl::StatusOr<CustomObject> CustomObjectManager::ParseBinaryData(
175 const std::vector<uint8_t>& data) {
176 CustomObject obj;
177 size_t cursor = 0;
178 int current_buffer_pos = 0;
179
180 // Safety check for empty data
181 if (data.empty()) {
182 return obj;
183 }
184
185 // Dungeon room tilemap buffer stride = 128 bytes (64 tiles per row, 2 bytes per tile).
186 // This matches the SNES dungeon room buffer layout where rooms can be up to 64 tiles wide.
187 // The jump offset of 0x80 (128) advances by exactly 1 row.
188 constexpr int kBufferStride = 128;
189
190 while (cursor + 1 < data.size()) {
191 // Read Header (little endian)
192 uint16_t header = data[cursor] | (data[cursor + 1] << 8);
193 cursor += 2;
194
195 if (header == 0)
196 break;
197
198 int count = header & 0x001F;
199 int jump_offset = (header >> 8) & 0xFF;
200
201 // ASM behavior: PHY saves the row start position, tiles are drawn at
202 // incrementing positions, then PLA restores the original position and
203 // the jump offset is added to that original position (not the post-tile position).
204 int row_start_pos = current_buffer_pos;
205
206 // Line Loop
207 for (int i = 0; i < count; ++i) {
208 if (cursor + 1 >= data.size()) {
209 LOG_WARN("CustomObjectManager",
210 "Unexpected end of file parsing object");
211 break;
212 }
213
214 uint16_t tile_data = data[cursor] | (data[cursor + 1] << 8);
215 cursor += 2;
216
217 // Calculate relative X/Y from current buffer position
218 // Buffer stride = 128 bytes (64 tiles per row)
219 int rel_y = current_buffer_pos / kBufferStride;
220 int rel_x = (current_buffer_pos % kBufferStride) / 2; // 2 bytes per tile
221
222 obj.tiles.push_back({rel_x, rel_y, tile_data});
223
224 current_buffer_pos += 2; // Advance 1 tile in buffer
225 }
226
227 // Advance buffer position for next segment from the ROW START, not current position.
228 // This matches the ASM: PLA (restore original Y) then ADC jump_offset.
229 current_buffer_pos = row_start_pos + jump_offset;
230 }
231
232 return obj;
233}
234
235absl::StatusOr<std::shared_ptr<CustomObject>>
236CustomObjectManager::GetObjectInternal(int object_id, int subtype) {
237 const std::vector<std::string>* list = ResolveFileList(object_id);
238 int index = subtype;
239
240 if (!list && ResolveCornerOverrideIndex(object_id, &index)) {
241 if (!IsCornerAliasOverrideEnabled(index)) {
242 return absl::NotFoundError(
243 "Corner alias override not configured for current runtime context");
244 }
245 // Minecart track corner aliases for subtype-2 wall-corner objects.
246 list = ResolveFileList(0x31);
247 }
248
249 if (!list) {
250 return absl::NotFoundError("Object ID not mapped to custom object");
251 }
252
253 if (index < 0 || index >= static_cast<int>(list->size())) {
254 return absl::OutOfRangeError("Subtype index out of range");
255 }
256
257 return LoadObject((*list)[index]);
258}
259
260int CustomObjectManager::GetSubtypeCount(int object_id) const {
261 if (const auto* list = ResolveFileList(object_id)) {
262 return static_cast<int>(list->size());
263 }
264 if (object_id >= 0x100 && object_id <= 0x103) {
265 if (const auto* list = ResolveFileList(0x31)) {
266 return static_cast<int>(list->size());
267 }
268 }
269 return 0;
270}
271
273 const std::string& filename) {
274 // If no custom_file_map_ entry exists, seed from static defaults
275 if (custom_file_map_.find(object_id) == custom_file_map_.end()) {
276 const auto& defaults = DefaultSubtypeFilenamesForObject(object_id);
277 if (!defaults.empty()) {
278 custom_file_map_[object_id] = defaults;
279 }
280 }
281 custom_file_map_[object_id].push_back(filename);
282
283 // Clear cache for the new file so it loads fresh
284 cache_.erase(filename);
285}
286
288 int object_id) const {
289 const auto* list = ResolveFileList(object_id);
290 if (list)
291 return *list;
292 return {};
293}
294
295const std::vector<std::string>&
297 static const std::vector<std::string> kEmpty;
298 if (object_id == 0x31) {
299 return kSubtype1Filenames;
300 }
301 if (object_id == 0x32) {
302 return kSubtype2Filenames;
303 }
304 return kEmpty;
305}
306
308 cache_.clear();
309}
310
311std::string CustomObjectManager::ResolveFilename(int object_id,
312 int subtype) const {
313 const auto* list = ResolveFileList(object_id);
314 int index = subtype;
315 if (!list && ResolveCornerOverrideIndex(object_id, &index)) {
316 list = ResolveFileList(0x31);
317 }
318 if (list && index >= 0 && index < static_cast<int>(list->size())) {
319 return (*list)[index];
320 }
321 return "";
322}
323
327
329 base_path_ = state.base_path;
331 cache_.clear();
332}
333
334} // namespace zelda3
335} // namespace yaze
Manages loading and caching of custom object binary files.
int GetSubtypeCount(int object_id) const
void RestoreState(const State &state)
bool IsCornerAliasOverrideEnabled(int resolved_index) const
absl::StatusOr< CustomObject > ParseBinaryData(const std::vector< uint8_t > &data)
static const std::vector< std::string > & DefaultSubtypeFilenamesForObject(int object_id)
void SetObjectFileMap(const std::unordered_map< int, std::vector< std::string > > &map)
static CustomObjectManager & Get()
absl::StatusOr< std::shared_ptr< CustomObject > > LoadObject(const std::string &filename)
void AddObjectFile(int object_id, const std::string &filename)
absl::StatusOr< std::shared_ptr< CustomObject > > GetObjectInternal(int object_id, int subtype)
static const std::vector< std::string > kSubtype2Filenames
void Initialize(const std::string &custom_objects_folder)
std::unordered_map< std::string, std::shared_ptr< CustomObject > > cache_
const std::vector< std::string > * ResolveFileList(int object_id) const
std::unordered_map< int, std::vector< std::string > > custom_file_map_
std::string ResolveFilename(int object_id, int subtype) const
static const std::vector< std::string > kSubtype1Filenames
std::vector< std::string > GetEffectiveFileList(int object_id) const
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
bool ResolveCornerOverrideIndex(int object_id, int *out_index)
std::unordered_map< int, std::vector< std::string > > custom_file_map
Represents a decoded custom object (from binary format)
std::vector< TileMapEntry > tiles