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