yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
asm_patch.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cctype>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/ascii.h"
9#include "absl/strings/match.h"
10#include "absl/strings/str_cat.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/strip.h"
13
14namespace yaze::core {
15
16namespace {
17
18// Trim whitespace from both ends of a string
19std::string Trim(const std::string& str) {
20 size_t start = str.find_first_not_of(" \t\r\n");
21 if (start == std::string::npos)
22 return "";
23 size_t end = str.find_last_not_of(" \t\r\n");
24 return str.substr(start, end - start + 1);
25}
26
27// Extract filename from path
28std::string GetFilename(const std::string& path) {
29 size_t pos = path.find_last_of("/\\");
30 if (pos == std::string::npos)
31 return path;
32 return path.substr(pos + 1);
33}
34
35} // namespace
36
37AsmPatch::AsmPatch(const std::string& file_path, const std::string& folder)
38 : folder_(folder), file_path_(file_path) {
39 filename_ = GetFilename(file_path);
40
41 // Read file content
42 std::ifstream file(file_path);
43 if (!file.is_open()) {
44 is_valid_ = false;
45 return;
46 }
47
48 std::stringstream buffer;
49 buffer << file.rdbuf();
50 original_content_ = buffer.str();
51 file.close();
52
54 is_valid_ = true;
55
56 // If no ;#ENABLED= line was found, default to enabled and prepend it
57 if (original_content_.find(";#ENABLED=") == std::string::npos) {
58 original_content_ = ";#ENABLED=true\n" + original_content_;
59 }
60}
61
62void AsmPatch::ParseMetadata(const std::string& content) {
63 std::istringstream stream(content);
64 std::string line;
65
66 bool in_define_block = false;
67 bool reading_description = false;
68 PatchParameter current_param;
69 bool has_param_attributes = false;
70
71 while (std::getline(stream, line)) {
72 // Handle patch name
73 if (line.find(";#PATCH_NAME=") != std::string::npos) {
74 size_t pos = line.find('=');
75 if (pos != std::string::npos) {
76 name_ = Trim(line.substr(pos + 1));
77 }
78 continue;
79 }
80
81 // Handle patch author
82 if (line.find(";#PATCH_AUTHOR=") != std::string::npos) {
83 size_t pos = line.find('=');
84 if (pos != std::string::npos) {
85 author_ = Trim(line.substr(pos + 1));
86 }
87 continue;
88 }
89
90 // Handle patch version
91 if (line.find(";#PATCH_VERSION=") != std::string::npos) {
92 size_t pos = line.find('=');
93 if (pos != std::string::npos) {
94 version_ = Trim(line.substr(pos + 1));
95 }
96 continue;
97 }
98
99 // Handle description start
100 if (line.find(";#PATCH_DESCRIPTION") != std::string::npos) {
101 reading_description = true;
102 description_.clear();
103 continue;
104 }
105
106 // Handle description end
107 if (line.find(";#ENDPATCH_DESCRIPTION") != std::string::npos) {
108 reading_description = false;
109 continue;
110 }
111
112 // Accumulate description lines
113 if (reading_description) {
114 std::string desc_line = line;
115 // Strip leading semicolons
116 while (!desc_line.empty() && desc_line[0] == ';') {
117 desc_line = desc_line.substr(1);
118 }
119 description_ += Trim(desc_line) + "\n";
120 continue;
121 }
122
123 // Handle enabled flag
124 if (line.find(";#ENABLED=") != std::string::npos) {
125 size_t pos = line.find('=');
126 if (pos != std::string::npos) {
127 std::string value = absl::AsciiStrToLower(Trim(line.substr(pos + 1)));
128 enabled_ = (value == "true" || value == "1");
129 }
130 continue;
131 }
132
133 // Handle define block start
134 if (line.find(";#DEFINE_START") != std::string::npos) {
135 in_define_block = true;
136 current_param = PatchParameter{};
137 has_param_attributes = false;
138 continue;
139 }
140
141 // Handle define block end
142 if (line.find(";#DEFINE_END") != std::string::npos) {
143 in_define_block = false;
144 break; // Stop parsing after define block
145 }
146
147 // Parse define block content
148 if (in_define_block) {
149 // Parse attribute comments (;#key=value)
150 if (line.find(";#") != std::string::npos) {
151 size_t hash_pos = line.find(";#");
152 std::string attr_line = line.substr(hash_pos + 2);
153 size_t eq_pos = attr_line.find('=');
154
155 if (eq_pos != std::string::npos) {
156 std::string key =
157 absl::AsciiStrToLower(Trim(attr_line.substr(0, eq_pos)));
158 std::string value = Trim(attr_line.substr(eq_pos + 1));
159
160 if (key == "name") {
161 current_param.display_name = value;
162 has_param_attributes = true;
163 } else if (key == "type") {
164 current_param.type = ParseType(value);
165 has_param_attributes = true;
166 } else if (key == "range") {
167 // Parse "min,max" format
168 size_t comma_pos = value.find(',');
169 if (comma_pos != std::string::npos) {
170 current_param.min_value =
171 ParseValue(Trim(value.substr(0, comma_pos)));
172 current_param.max_value =
173 ParseValue(Trim(value.substr(comma_pos + 1)));
174 }
175 } else if (key == "checkedvalue") {
176 current_param.checked_value = ParseValue(value);
177 } else if (key == "uncheckedvalue") {
178 current_param.unchecked_value = ParseValue(value);
179 } else if (key == "decimal") {
180 current_param.use_decimal = true;
181 } else if (key.starts_with("choice")) {
182 // Handle choice0, choice1, ..., choice9
183 current_param.choices.push_back(value);
184 } else if (key.starts_with("bit")) {
185 // Handle bit0, bit1, ..., bit7
186 // Extract bit index
187 int bit_index = 0;
188 if (key.size() > 3) {
189 bit_index = std::stoi(key.substr(3));
190 }
191 // Ensure choices vector is large enough
192 while (current_param.choices.size() <=
193 static_cast<size_t>(bit_index)) {
194 current_param.choices.push_back("");
195 }
196 current_param.choices[bit_index] = value;
197 }
198 }
199 continue;
200 }
201
202 // Parse Asar define line: !DEFINE_NAME = $VALUE
203 std::string trimmed = Trim(line);
204 if (!trimmed.empty() && trimmed[0] == '!') {
205 size_t eq_pos = trimmed.find('=');
206 if (eq_pos != std::string::npos) {
207 current_param.define_name = Trim(trimmed.substr(0, eq_pos));
208 std::string value_str = Trim(trimmed.substr(eq_pos + 1));
209 current_param.value = ParseValue(value_str);
210
211 // Set default max value based on type if not specified
212 if (current_param.max_value == 0xFF) {
213 switch (current_param.type) {
215 current_param.max_value = 0xFFFF;
216 break;
218 current_param.max_value = 0xFFFFFF;
219 break;
220 default:
221 break;
222 }
223 }
224
225 // Use define name as display name if none specified
226 if (current_param.display_name.empty()) {
227 current_param.display_name = current_param.define_name;
228 }
229
230 parameters_.push_back(std::move(current_param));
231 current_param = PatchParameter{};
232 has_param_attributes = false;
233 }
234 }
235 }
236 }
237
238 // Trim trailing newline from description
239 while (!description_.empty() && description_.back() == '\n') {
240 description_.pop_back();
241 }
242
243 // If no name was found, use filename
244 if (name_.empty()) {
246 // Remove .asm extension
247 if (name_.size() > 4 && name_.substr(name_.size() - 4) == ".asm") {
248 name_ = name_.substr(0, name_.size() - 4);
249 }
250 }
251}
252
253PatchParameterType AsmPatch::ParseType(const std::string& type_str) {
254 std::string lower = absl::AsciiStrToLower(type_str);
255
256 if (lower.find("byte") != std::string::npos) {
258 } else if (lower.find("word") != std::string::npos) {
260 } else if (lower.find("long") != std::string::npos) {
262 } else if (lower.find("bool") != std::string::npos) {
264 } else if (lower.find("choice") != std::string::npos) {
266 } else if (lower.find("bitfield") != std::string::npos) {
268 } else if (lower.find("item") != std::string::npos) {
270 }
271
272 return PatchParameterType::kByte; // Default to byte
273}
274
275int AsmPatch::ParseValue(const std::string& value_str) {
276 std::string trimmed = Trim(value_str);
277 if (trimmed.empty())
278 return 0;
279
280 // Handle hex values (prefixed with $)
281 if (trimmed[0] == '$') {
282 try {
283 return std::stoi(trimmed.substr(1), nullptr, 16);
284 } catch (...) {
285 return 0;
286 }
287 }
288
289 // Handle hex values (prefixed with 0x)
290 if (trimmed.size() > 2 && trimmed[0] == '0' &&
291 (trimmed[1] == 'x' || trimmed[1] == 'X')) {
292 try {
293 return std::stoi(trimmed.substr(2), nullptr, 16);
294 } catch (...) {
295 return 0;
296 }
297 }
298
299 // Handle decimal values
300 try {
301 return std::stoi(trimmed);
302 } catch (...) {
303 return 0;
304 }
305}
306
307bool AsmPatch::SetParameterValue(const std::string& define_name, int value) {
308 for (auto& param : parameters_) {
309 if (param.define_name == define_name) {
310 param.value = std::clamp(value, param.min_value, param.max_value);
311 return true;
312 }
313 }
314 return false;
315}
316
317PatchParameter* AsmPatch::GetParameter(const std::string& define_name) {
318 for (auto& param : parameters_) {
319 if (param.define_name == define_name) {
320 return &param;
321 }
322 }
323 return nullptr;
324}
325
327 const std::string& define_name) const {
328 for (const auto& param : parameters_) {
329 if (param.define_name == define_name) {
330 return &param;
331 }
332 }
333 return nullptr;
334}
335
336std::string AsmPatch::GenerateContent() const {
337 std::string result = original_content_;
338
339 // Update ;#ENABLED= line
340 UpdateLine(result, ";#ENABLED=", enabled_ ? "true" : "false");
341
342 // Update each define value
343 for (const auto& param : parameters_) {
344 UpdateDefineLine(result, param.define_name, param.value);
345 }
346
347 return result;
348}
349
350void AsmPatch::UpdateLine(std::string& content, const std::string& prefix,
351 const std::string& new_value) {
352 size_t pos = content.find(prefix);
353 if (pos == std::string::npos)
354 return;
355
356 size_t line_end = content.find('\n', pos);
357 if (line_end == std::string::npos) {
358 line_end = content.size();
359 }
360
361 // Replace the line content after the prefix
362 std::string new_line = prefix + new_value;
363 content.replace(pos, line_end - pos, new_line);
364}
365
366void AsmPatch::UpdateDefineLine(std::string& content,
367 const std::string& define_name, int value) {
368 // Find the define line (starts with the define name)
369 size_t pos = content.find(define_name);
370 if (pos == std::string::npos)
371 return;
372
373 // Find the end of the line
374 size_t line_end = content.find('\n', pos);
375 if (line_end == std::string::npos) {
376 line_end = content.size();
377 }
378
379 // Create new line with updated value
380 std::string new_line = absl::StrFormat("%s = $%02X", define_name, value);
381 content.replace(pos, line_end - pos, new_line);
382}
383
384absl::Status AsmPatch::Save() {
385 return SaveToPath(file_path_);
386}
387
388absl::Status AsmPatch::SaveToPath(const std::string& output_path) {
389 std::string content = GenerateContent();
390
391 std::ofstream file(output_path);
392 if (!file.is_open()) {
393 return absl::InternalError(
394 absl::StrCat("Failed to open file for writing: ", output_path));
395 }
396
397 file << content;
398 file.close();
399
400 if (file.fail()) {
401 return absl::InternalError(
402 absl::StrCat("Failed to write patch file: ", output_path));
403 }
404
405 return absl::OkStatus();
406}
407
408} // namespace yaze::core
absl::Status Save()
Save the patch to its original file location.
Definition asm_patch.cc:384
std::string description_
Definition asm_patch.h:183
AsmPatch(const std::string &file_path, const std::string &folder)
Construct a patch from a file path.
Definition asm_patch.cc:37
std::string name_
Definition asm_patch.h:180
static void UpdateLine(std::string &content, const std::string &prefix, const std::string &new_value)
Update a line in the content that starts with a prefix.
Definition asm_patch.cc:350
std::vector< PatchParameter > parameters_
Definition asm_patch.h:192
std::string filename_
Definition asm_patch.h:185
std::string file_path_
Definition asm_patch.h:186
bool SetParameterValue(const std::string &define_name, int value)
Set the value of a parameter by its define name.
Definition asm_patch.cc:307
PatchParameter * GetParameter(const std::string &define_name)
Get a parameter by its define name.
Definition asm_patch.cc:317
std::string original_content_
Definition asm_patch.h:187
static void UpdateDefineLine(std::string &content, const std::string &define_name, int value)
Update an Asar define line with a new value.
Definition asm_patch.cc:366
static PatchParameterType ParseType(const std::string &type_str)
Parse a parameter type from a string.
Definition asm_patch.cc:253
std::string GenerateContent() const
Generate the patch file content with current parameter values.
Definition asm_patch.cc:336
std::string author_
Definition asm_patch.h:181
std::string version_
Definition asm_patch.h:182
static int ParseValue(const std::string &value_str)
Parse an integer value from a string (handles $hex and decimal)
Definition asm_patch.cc:275
const std::string & file_path() const
Definition asm_patch.h:90
absl::Status SaveToPath(const std::string &output_path)
Save the patch to a specific path.
Definition asm_patch.cc:388
void ParseMetadata(const std::string &content)
Parse metadata from the patch file content.
Definition asm_patch.cc:62
PatchParameterType
Parameter types supported by ZScream-compatible ASM patches.
Definition asm_patch.h:16
Represents a configurable parameter within an ASM patch.
Definition asm_patch.h:33
PatchParameterType type
Definition asm_patch.h:36
std::vector< std::string > choices
Definition asm_patch.h:43