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