yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
patch_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/match.h"
9#include "absl/strings/str_cat.h"
10#include "absl/strings/str_join.h"
11#include "core/asar_wrapper.h"
12#include "rom/rom.h"
14#ifdef YAZE_WITH_Z3DK
15#include "core/z3dk_wrapper.h"
16#endif
17
18namespace yaze::core {
19
20namespace fs = std::filesystem;
21
22absl::Status PatchManager::LoadPatches(const std::string& patches_dir) {
23 // Clear existing data
24 patches_.clear();
25 folders_.clear();
26 is_loaded_ = false;
27
28 patches_directory_ = patches_dir;
29
30 // Check if directory exists
31 if (!fs::exists(patches_dir)) {
32 return absl::NotFoundError(
33 absl::StrCat("Patches directory not found: ", patches_dir));
34 }
35
36 // Create default folders if they don't exist
37 std::vector<std::string> default_folders = {"Misc", "Hex Edits", "Sprites",
38 "Items", "Npcs"};
39
40 for (const auto& folder : default_folders) {
41 fs::path folder_path = fs::path(patches_dir) / folder;
42 if (!fs::exists(folder_path)) {
43 std::error_code ec;
44 fs::create_directories(folder_path, ec);
45 }
46 }
47
48 // Scan all subdirectories
49 for (const auto& entry : fs::directory_iterator(patches_dir)) {
50 if (entry.is_directory()) {
51 std::string folder_name = entry.path().filename().string();
52
53 // Skip UNPATCHED folder
54 if (folder_name == "UNPATCHED") {
55 continue;
56 }
57
58 folders_.push_back(folder_name);
59 ScanDirectory(entry.path().string(), folder_name);
60 }
61 }
62
63 // Sort folders alphabetically
64 std::sort(folders_.begin(), folders_.end());
65
66 is_loaded_ = true;
67 return absl::OkStatus();
68}
69
71 if (patches_directory_.empty()) {
72 return absl::FailedPreconditionError("No patches directory set");
73 }
75}
76
77void PatchManager::ScanDirectory(const std::string& dir_path,
78 const std::string& folder_name) {
79 for (const auto& entry : fs::directory_iterator(dir_path)) {
80 if (entry.is_regular_file() && entry.path().extension() == ".asm") {
81 auto patch =
82 std::make_unique<AsmPatch>(entry.path().string(), folder_name);
83 if (patch->is_valid()) {
84 patches_.push_back(std::move(patch));
85 }
86 }
87 }
88}
89
90std::vector<AsmPatch*> PatchManager::GetPatchesInFolder(
91 const std::string& folder) {
92 std::vector<AsmPatch*> result;
93 for (const auto& patch : patches_) {
94 if (patch->folder() == folder) {
95 result.push_back(patch.get());
96 }
97 }
98 // Sort by name
99 std::sort(result.begin(), result.end(),
100 [](AsmPatch* a, AsmPatch* b) { return a->name() < b->name(); });
101 return result;
102}
103
104AsmPatch* PatchManager::GetPatch(const std::string& folder,
105 const std::string& filename) {
106 for (const auto& patch : patches_) {
107 if (patch->folder() == folder && patch->filename() == filename) {
108 return patch.get();
109 }
110 }
111 return nullptr;
112}
113
115 int count = 0;
116 for (const auto& patch : patches_) {
117 if (patch->enabled()) {
118 ++count;
119 }
120 }
121 return count;
122}
123
125 if (!rom || !rom->is_loaded()) {
126 return absl::FailedPreconditionError("ROM not loaded");
127 }
128
129 int enabled_count = GetEnabledPatchCount();
130 if (enabled_count == 0) {
131 return absl::OkStatus(); // Nothing to apply
132 }
133
134 // TailMapExpansion.asm overwrites fixed addresses; refuse it when the
135 // compatibility check (z3ed overworld-allocation --tail-check) says unsafe.
136 for (const auto& patch : patches_) {
137 if (patch->enabled() &&
138 absl::EqualsIgnoreCase(patch->filename(), "TailMapExpansion.asm")) {
139 if (auto guard = zelda3::TailMapExpansionApplyGuard(*rom); !guard.ok()) {
140 return guard;
141 }
142 }
143 }
144
145 // Generate temporary combined patch file
146 std::string temp_dir = std::filesystem::temp_directory_path().string();
147 std::string temp_path = temp_dir + "/yaze_combined_patches.asm";
148
149 auto status = GenerateCombinedPatch(temp_path);
150 if (!status.ok()) {
151 return status;
152 }
153
154#if defined(YAZE_WITH_Z3DK)
155 // z3dk backend path — equivalent flow, just a different assembler.
157 if (auto init_status = z3dk.Initialize(); !init_status.ok()) {
158 std::filesystem::remove(temp_path);
159 return init_status;
160 }
161
162 auto& rom_data = rom->mutable_vector();
163 Z3dkAssembleOptions options = z3dk_options_;
164 if (std::find(options.include_paths.begin(), options.include_paths.end(),
165 patches_directory_) == options.include_paths.end()) {
166 options.include_paths.push_back(patches_directory_);
167 }
168 auto result_or_error = z3dk.ApplyPatch(temp_path, rom_data, options);
169
170 std::filesystem::remove(temp_path);
171
172 if (!result_or_error.ok()) {
173 return result_or_error.status();
174 }
175 const auto& result = *result_or_error;
176 if (!result.success) {
177 return absl::InternalError(absl::StrCat(
178 "Failed to apply patches:\n", absl::StrJoin(result.errors, "\n")));
179 }
180 return absl::OkStatus();
181#elif defined(YAZE_ENABLE_ASAR)
182 // Apply using AsarWrapper
183 AsarWrapper asar;
184 auto init_status = asar.Initialize();
185 if (!init_status.ok()) {
186 return init_status;
187 }
188
189 // Get mutable ROM data
190 auto& rom_data = rom->mutable_vector();
191
192 // Apply the combined patch
193 auto result_or_error =
194 asar.ApplyPatch(temp_path, rom_data, {patches_directory_});
195
196 // Clean up temp file
197 std::filesystem::remove(temp_path);
198
199 if (!result_or_error.ok()) {
200 return result_or_error.status();
201 }
202
203 const auto& result = *result_or_error;
204
205 if (!result.success) {
206 return absl::InternalError(absl::StrCat(
207 "Failed to apply patches:\n", absl::StrJoin(result.errors, "\n")));
208 }
209
210 return absl::OkStatus();
211#else
212 // Clean up temp file
213 std::filesystem::remove(temp_path);
214 return absl::UnimplementedError(
215 "No assembler backend enabled. Build with YAZE_ENABLE_ASAR=ON or "
216 "YAZE_ENABLE_Z3DK=ON.");
217#endif
218}
219
221 const std::string& output_path) {
222 std::ostringstream combined;
223
224 combined << "; "
225 "================================================================"
226 "=============\n";
227 combined << "; Combined Patch File\n";
228 combined << "; Generated by yaze - Yet Another Zelda3 Editor\n";
229 combined << "; "
230 "================================================================"
231 "=============\n";
232 combined << "; This file includes all enabled patches.\n";
233 combined << "; Do not edit this file directly - modify individual patches "
234 "instead.\n";
235 combined << "; "
236 "================================================================"
237 "=============\n\n";
238
239 combined << "lorom\n\n";
240
241 // Include each enabled patch
242 int patch_count = 0;
243 for (const auto& patch : patches_) {
244 if (patch->enabled()) {
245 // Use relative path from patches directory
246 std::string relative_path = patch->folder() + "/" + patch->filename();
247 combined << "incsrc \"" << relative_path << "\"\n";
248 ++patch_count;
249 }
250 }
251
252 if (patch_count == 0) {
253 combined << "; No patches enabled\n";
254 } else {
255 combined << "\n; Total patches: " << patch_count << "\n";
256 }
257
258 // Write to file
259 std::ofstream file(output_path);
260 if (!file.is_open()) {
261 return absl::InternalError(
262 absl::StrCat("Failed to create combined patch file: ", output_path));
263 }
264
265 file << combined.str();
266 file.close();
267
268 if (file.fail()) {
269 return absl::InternalError(
270 absl::StrCat("Failed to write combined patch file: ", output_path));
271 }
272
273 return absl::OkStatus();
274}
275
277 std::vector<std::string> errors;
278
279 for (const auto& patch : patches_) {
280 auto status = patch->Save();
281 if (!status.ok()) {
282 errors.push_back(absl::StrCat(patch->filename(), ": ", status.message()));
283 }
284 }
285
286 if (!errors.empty()) {
287 return absl::InternalError(absl::StrCat("Failed to save some patches:\n",
288 absl::StrJoin(errors, "\n")));
289 }
290
291 return absl::OkStatus();
292}
293
294absl::Status PatchManager::CreatePatchFolder(const std::string& folder_name) {
295 if (patches_directory_.empty()) {
296 return absl::FailedPreconditionError("No patches directory set");
297 }
298
299 // Check if folder already exists
300 for (const auto& folder : folders_) {
301 if (folder == folder_name) {
302 return absl::AlreadyExistsError(
303 absl::StrCat("Folder already exists: ", folder_name));
304 }
305 }
306
307 // Create the directory
308 fs::path folder_path = fs::path(patches_directory_) / folder_name;
309 std::error_code ec;
310 fs::create_directories(folder_path, ec);
311 if (ec) {
312 return absl::InternalError(
313 absl::StrCat("Failed to create folder: ", ec.message()));
314 }
315
316 folders_.push_back(folder_name);
317 std::sort(folders_.begin(), folders_.end());
318
319 return absl::OkStatus();
320}
321
322absl::Status PatchManager::RemovePatchFolder(const std::string& folder_name) {
323 if (patches_directory_.empty()) {
324 return absl::FailedPreconditionError("No patches directory set");
325 }
326
327 // Check if this is the last folder
328 if (folders_.size() <= 1) {
329 return absl::FailedPreconditionError(
330 "Cannot remove the last remaining folder");
331 }
332
333 // Find and remove the folder
334 auto it = std::find(folders_.begin(), folders_.end(), folder_name);
335 if (it == folders_.end()) {
336 return absl::NotFoundError(absl::StrCat("Folder not found: ", folder_name));
337 }
338
339 // Remove all patches in this folder from our list
340 patches_.erase(
341 std::remove_if(patches_.begin(), patches_.end(),
342 [&folder_name](const std::unique_ptr<AsmPatch>& patch) {
343 return patch->folder() == folder_name;
344 }),
345 patches_.end());
346
347 // Remove the directory
348 fs::path folder_path = fs::path(patches_directory_) / folder_name;
349 std::error_code ec;
350 fs::remove_all(folder_path, ec);
351 if (ec) {
352 return absl::InternalError(
353 absl::StrCat("Failed to remove folder: ", ec.message()));
354 }
355
356 folders_.erase(it);
357
358 return absl::OkStatus();
359}
360
361absl::Status PatchManager::AddPatchFile(const std::string& source_path,
362 const std::string& target_folder) {
363 if (patches_directory_.empty()) {
364 return absl::FailedPreconditionError("No patches directory set");
365 }
366
367 // Check if source file exists
368 if (!fs::exists(source_path)) {
369 return absl::NotFoundError(
370 absl::StrCat("Source file not found: ", source_path));
371 }
372
373 // Check if target folder exists
374 auto it = std::find(folders_.begin(), folders_.end(), target_folder);
375 if (it == folders_.end()) {
376 return absl::NotFoundError(
377 absl::StrCat("Target folder not found: ", target_folder));
378 }
379
380 // Get filename and create target path
381 std::string filename = fs::path(source_path).filename().string();
382 fs::path target_path =
383 fs::path(patches_directory_) / target_folder / filename;
384
385 // Copy the file
386 std::error_code ec;
387 fs::copy_file(source_path, target_path, fs::copy_options::overwrite_existing,
388 ec);
389 if (ec) {
390 return absl::InternalError(
391 absl::StrCat("Failed to copy file: ", ec.message()));
392 }
393
394 // Load the new patch
395 auto patch = std::make_unique<AsmPatch>(target_path.string(), target_folder);
396 if (patch->is_valid()) {
397 patches_.push_back(std::move(patch));
398 }
399
400 return absl::OkStatus();
401}
402
403absl::Status PatchManager::RemovePatchFile(const std::string& folder,
404 const std::string& filename) {
405 if (patches_directory_.empty()) {
406 return absl::FailedPreconditionError("No patches directory set");
407 }
408
409 // Find the patch
410 auto it = std::find_if(
411 patches_.begin(), patches_.end(),
412 [&folder, &filename](const std::unique_ptr<AsmPatch>& patch) {
413 return patch->folder() == folder && patch->filename() == filename;
414 });
415
416 if (it == patches_.end()) {
417 return absl::NotFoundError(
418 absl::StrCat("Patch not found: ", folder, "/", filename));
419 }
420
421 // Get the file path before removing from list
422 std::string file_path = (*it)->file_path();
423
424 // Remove from patches list
425 patches_.erase(it);
426
427 // Delete the file
428 std::error_code ec;
429 fs::remove(file_path, ec);
430 if (ec) {
431 return absl::InternalError(
432 absl::StrCat("Failed to delete file: ", ec.message()));
433 }
434
435 return absl::OkStatus();
436}
437
438} // namespace yaze::core
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
auto & mutable_vector()
Definition rom.h:206
bool is_loaded() const
Definition rom.h:187
Modern C++ wrapper for Asar 65816 assembler integration.
absl::StatusOr< AsarPatchResult > ApplyPatch(const std::string &patch_path, std::vector< uint8_t > &rom_data, const std::vector< std::string > &include_paths={})
absl::Status Initialize()
Represents a ZScream-compatible ASM patch file.
Definition asm_patch.h:74
absl::Status ApplyEnabledPatches(Rom *rom)
Apply all enabled patches to a ROM.
absl::Status RemovePatchFile(const std::string &folder, const std::string &filename)
Remove a patch file.
std::vector< std::string > folders_
void ScanDirectory(const std::string &dir_path, const std::string &folder_name)
Scan a directory for .asm files.
absl::Status AddPatchFile(const std::string &source_path, const std::string &target_folder)
Add a patch file from an external source.
AsmPatch * GetPatch(const std::string &folder, const std::string &filename)
Get a specific patch by folder and filename.
absl::Status SaveAllPatches()
Save all patches to their files.
int GetEnabledPatchCount() const
Get count of enabled patches.
std::vector< AsmPatch * > GetPatchesInFolder(const std::string &folder)
Get all patches in a specific folder.
absl::Status CreatePatchFolder(const std::string &folder_name)
Create a new patch folder.
absl::Status ReloadPatches()
Reload patches from the current directory.
std::vector< std::unique_ptr< AsmPatch > > patches_
absl::Status LoadPatches(const std::string &patches_dir)
Load all patches from a directory structure.
absl::Status RemovePatchFolder(const std::string &folder_name)
Remove a patch folder and all its contents.
absl::Status GenerateCombinedPatch(const std::string &output_path)
Generate a combined .asm file that includes all enabled patches.
absl::Status TailMapExpansionApplyGuard(const Rom &rom)
Read-only map of overworld area IDs $00-$BF: which IDs are used, which per-area tables cover each ID,...
std::vector< std::string > include_paths