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