10#include "fmt/format.h"
11#include "fmt/ranges.h"
12#include "nlohmann/json.hpp"
25[[nodiscard]] std::optional<metatile::Layer> layer_from_string(
const std::string &str)
27 if (str ==
"bottom") {
28 return metatile::Layer::bottom;
30 if (str ==
"middle") {
31 return metatile::Layer::middle;
34 return metatile::Layer::top;
39[[nodiscard]] std::optional<metatile::Subtile> subtile_from_string(
const std::string &str)
41 if (str ==
"northwest") {
42 return metatile::Subtile::northwest;
44 if (str ==
"northeast") {
45 return metatile::Subtile::northeast;
47 if (str ==
"southwest") {
48 return metatile::Subtile::southwest;
50 if (str ==
"southeast") {
51 return metatile::Subtile::southeast;
59 case metatile::Subtile::northwest:
61 case metatile::Subtile::northeast:
63 case metatile::Subtile::southwest:
65 case metatile::Subtile::southeast:
68 panic(
"unhandled Subtile value");
80[[nodiscard]] std::size_t byte_offset_to_line_index(
const std::filesystem::path &json_path, std::size_t byte_offset)
82 std::ifstream in{json_path};
87 std::size_t line_index = 0;
88 std::size_t current_byte = 0;
90 while (in.get(ch) && current_byte < byte_offset) {
108[[nodiscard]] std::size_t find_key_line_index(
const std::filesystem::path &json_path,
const std::string &key_name)
110 std::ifstream in{json_path};
115 const std::string pattern =
"\"" + key_name +
"\"";
117 std::size_t line_index = 0;
118 while (std::getline(in, line)) {
119 if (line.find(pattern) != std::string::npos) {
127[[nodiscard]] std::vector<AnimOverrideEntry>
128parse_override_entries(
const std::string &context_name,
const nlohmann::json &overrides_node)
130 std::vector<AnimOverrideEntry> overrides;
131 for (
const auto &entry_node : overrides_node) {
134 entry.
metatile_id = entry_node.at(
"id").get<std::size_t>();
136 const auto layer_str = entry_node.at(
"layer").get<std::string>();
137 const auto layer_opt = layer_from_string(layer_str);
138 if (!layer_opt.has_value()) {
139 panic(
"anim.json: '" + context_name +
"' override has invalid layer '" + layer_str +
"'");
141 entry.layer = *layer_opt;
143 const auto subtile_str = entry_node.at(
"subtile").get<std::string>();
144 const auto subtile_opt = subtile_from_string(subtile_str);
145 if (!subtile_opt.has_value()) {
146 panic(
"anim.json: '" + context_name +
"' override has invalid subtile '" + subtile_str +
"'");
148 entry.subtile = *subtile_opt;
150 entry.frame_subtile = entry_node.at(
"frame_subtile").get<std::size_t>();
151 entry.palette_index = entry_node.at(
"palette_index").get<std::size_t>();
152 entry.h_flip = entry_node.at(
"hflip").get<
bool>();
153 entry.v_flip = entry_node.at(
"vflip").get<
bool>();
155 overrides.push_back(entry);
160[[nodiscard]] nlohmann::ordered_json serialize_override_entries(
const std::vector<AnimOverrideEntry> &entries)
162 nlohmann::ordered_json overrides_array = nlohmann::ordered_json::array();
163 for (
const auto &entry : entries) {
164 nlohmann::ordered_json obj;
165 obj[
"id"] = entry.metatile_id;
167 obj[
"subtile"] = subtile_to_json_string(entry.subtile);
168 obj[
"frame_subtile"] = entry.frame_subtile;
169 obj[
"palette_index"] = entry.palette_index;
170 obj[
"hflip"] = entry.h_flip;
171 obj[
"vflip"] = entry.v_flip;
172 overrides_array.push_back(std::move(obj));
174 return overrides_array;
177AnimParams parse_animation_params(
const std::string &anim_name,
const nlohmann::json &node)
181 if (node.contains(
"frame_factor")) {
182 params.
frame_factor(node[
"frame_factor"].get<std::size_t>());
185 if (node.contains(
"frame_offset")) {
186 params.
frame_offset(node[
"frame_offset"].get<std::size_t>());
190 if (node.contains(
"frames")) {
191 std::vector<DynamicCasedName> frame_names;
192 for (
const auto &frame : node[
"frames"]) {
200 if (node.contains(
"frame_order")) {
201 std::vector<DynamicCasedName> frame_order;
202 for (
const auto &frame : node[
"frame_order"]) {
212 if (node.contains(
"counter_max")) {
213 params.
counter_max(node[
"counter_max"].get<std::size_t>());
216 if (node.contains(
"overrides")) {
217 if (!node[
"overrides"].is_array()) {
218 panic(
"anim.json: animation '" + anim_name +
"' overrides must be an array");
220 params.
overrides(parse_override_entries(anim_name, node[
"overrides"]));
223 if (node.contains(
"tile_offset")) {
224 params.
tile_offset(node[
"tile_offset"].get<std::size_t>());
231nlohmann::ordered_json serialize_animation_params(
const AnimParams ¶ms)
233 nlohmann::ordered_json node;
245 nlohmann::ordered_json frames_array = nlohmann::ordered_json::array();
246 for (
const auto &frame : params.frame_names()) {
247 frames_array.push_back(frame.to_snake_case());
249 node[
"frames"] = frames_array;
252 nlohmann::ordered_json frame_order_array = nlohmann::ordered_json::array();
253 for (
const auto &frame : params.frame_order()) {
254 frame_order_array.push_back(frame.to_snake_case());
256 node[
"frame_order"] = frame_order_array;
267 node[
"overrides"] = serialize_override_entries(params.
overrides());
282 if (!std::filesystem::exists(json_path)) {
284 std::vector<std::string>{
"Parameters file not found.",
"Expected file: {}"},
289 std::ifstream in{json_path};
292 std::vector<std::string>{
"Failed to open anim.json for reading.",
"path: {}"},
296 nlohmann::json root = nlohmann::json::parse(in);
298 std::map<DynamicCasedName, AnimParams> result;
300 if (!root.is_object()) {
302 std::vector<std::string> err_lines;
303 err_lines.push_back(format_->
format(
304 "{}:1: Invalid anim.json format, expected a JSON object at the root level.",
306 err_lines.emplace_back();
307 auto context = printer.print(json_path, std::vector<std::size_t>{0});
308 err_lines.insert(err_lines.end(), context.begin(), context.end());
312 for (
const auto &[anim_name, anim_node] : root.items()) {
313 if (anim_name ==
"primary_references") {
317 const std::size_t line_idx = find_key_line_index(json_path, anim_name);
321 if (expected_snake != anim_name) {
323 std::vector<std::string> err_lines;
324 err_lines.push_back(format_->
format(
325 "{}:{}: Animation name '{}' must be snake_case (expected '{}').",
330 err_lines.emplace_back();
331 auto context = printer.print(json_path, std::vector{line_idx});
332 err_lines.insert(err_lines.end(), context.begin(), context.end());
336 if (!anim_node.is_object()) {
338 std::vector<std::string> err_lines;
339 err_lines.push_back(format_->
format(
340 "{}:{}: Invalid animation entry, '{}' should be a JSON object with frame_factor, frame_offset, "
345 err_lines.emplace_back();
346 auto context = printer.print(json_path, std::vector{line_idx});
347 err_lines.insert(err_lines.end(), context.begin(), context.end());
351 auto parsed = parse_animation_params(anim_name, anim_node);
356 std::set<DynamicCasedName> valid_frames(
357 parsed_params.frame_names().begin(), parsed_params.frame_names().end());
359 for (
const auto &frame : parsed_params.frame_order()) {
360 if (!valid_frames.contains(frame)) {
362 std::vector<std::string> err_lines;
363 err_lines.push_back(format_->
format(
364 "{}:{}: frame_order entry '{}' does not exist in frames list.",
368 err_lines.emplace_back();
371 std::vector<std::string> frame_strs;
372 frame_strs.reserve(parsed_params.frame_names().size());
373 for (
const auto &f : parsed_params.frame_names()) {
374 frame_strs.push_back(f.to_snake_case());
376 err_lines.push_back(format_->
format(
377 "Valid frames are: {}.",
378 FormatParam{fmt::format(
"[{}]", fmt::join(frame_strs,
", ")), Style::bold}));
379 err_lines.emplace_back();
380 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
381 err_lines.insert(err_lines.end(), context.begin(), context.end());
389 catch (
const nlohmann::json::parse_error &e) {
391 std::vector<std::string> err_lines;
393 const auto byte_offset =
static_cast<std::size_t
>(e.byte);
394 const auto line_idx = byte_offset_to_line_index(json_path, byte_offset);
395 err_lines.push_back(format_->
format(
396 "{}:{}: Failed to parse anim.json: {}.",
400 err_lines.emplace_back();
401 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
402 err_lines.insert(err_lines.end(), context.begin(), context.end());
406 catch (
const nlohmann::json::exception &e) {
408 std::vector<std::string>{
"{}: Failed to parse anim.json: {}."},
409 std::vector<std::vector<FormatParam>>{
417 if (!std::filesystem::exists(json_path)) {
419 std::vector<std::string>{
"Parameters file not found.",
"Expected file: {}"},
424 std::ifstream in{json_path};
427 std::vector<std::string>{
"Failed to open anim.json for reading.",
"path: {}"},
431 nlohmann::json root = nlohmann::json::parse(in);
433 std::map<DynamicCasedName, std::vector<AnimOverrideEntry>> result;
435 if (!root.is_object() || !root.contains(
"primary_references")) {
439 const auto &refs_node = root[
"primary_references"];
440 if (!refs_node.is_object()) {
442 const auto line_idx = find_key_line_index(json_path,
"primary_references");
443 std::vector<std::string> err_lines;
444 err_lines.push_back(format_->
format(
445 "{}:{}: 'primary_references' must be a JSON object.",
448 err_lines.emplace_back();
449 auto context = printer.print(json_path, std::vector{line_idx});
450 err_lines.insert(err_lines.end(), context.begin(), context.end());
454 for (
const auto &[prim_anim_name, prim_anim_node] : refs_node.items()) {
455 const auto line_idx = find_key_line_index(json_path, prim_anim_name);
457 if (!prim_anim_node.is_object()) {
459 std::vector<std::string> err_lines;
460 err_lines.push_back(format_->
format(
461 "{}:{}: Primary reference '{}' must be a JSON object.",
465 err_lines.emplace_back();
466 auto context = printer.print(json_path, std::vector{line_idx});
467 err_lines.insert(err_lines.end(), context.begin(), context.end());
471 if (!prim_anim_node.contains(
"overrides") || !prim_anim_node[
"overrides"].is_array()) {
473 std::vector<std::string> err_lines;
474 err_lines.push_back(format_->
format(
475 "{}:{}: Primary reference '{}' must contain an 'overrides' array.",
479 err_lines.emplace_back();
480 auto context = printer.print(json_path, std::vector{line_idx});
481 err_lines.insert(err_lines.end(), context.begin(), context.end());
485 auto entries = parse_override_entries(prim_anim_name, prim_anim_node[
"overrides"]);
491 catch (
const nlohmann::json::parse_error &e) {
493 std::vector<std::string> err_lines;
495 const auto byte_offset =
static_cast<std::size_t
>(e.byte);
496 const auto line_idx = byte_offset_to_line_index(json_path, byte_offset);
497 err_lines.push_back(format_->
format(
498 "{}:{}: Failed to parse anim.json: {}.",
502 err_lines.emplace_back();
503 auto context = printer.print(json_path, std::vector<std::size_t>{line_idx});
504 err_lines.insert(err_lines.end(), context.begin(), context.end());
508 catch (
const nlohmann::json::exception &e) {
510 std::vector<std::string>{
"{}: Failed to parse anim.json: {}."},
511 std::vector<std::vector<FormatParam>>{
517 const std::filesystem::path &json_path,
518 const std::map<DynamicCasedName, AnimParams> ¶ms,
519 const std::map<
DynamicCasedName, std::vector<AnimOverrideEntry>> &primary_references)
const
523 if (json_path.has_parent_path()) {
524 std::filesystem::create_directories(json_path.parent_path());
527 nlohmann::ordered_json root;
528 for (
const auto &[
name, anim_params] : params) {
529 root[
name.to_snake_case()] = serialize_animation_params(anim_params);
532 if (!primary_references.empty()) {
533 nlohmann::ordered_json refs_node;
534 for (
const auto &[prim_anim_name, entries] : primary_references) {
535 nlohmann::ordered_json anim_ref_node;
536 anim_ref_node[
"overrides"] = serialize_override_entries(entries);
537 refs_node[prim_anim_name.to_snake_case()] = std::move(anim_ref_node);
539 root[
"primary_references"] = std::move(refs_node);
542 std::ofstream out(json_path);
545 std::vector<std::string>{
"Failed to open anim.json for writing.",
"path: {}"},
555 std::vector<std::string>{
"Failed to write anim.json.",
"Error occurred while writing to: {}"},
561 catch (
const nlohmann::json::exception &e) {
563 std::vector<std::string>{
"Failed to serialize anim.json.",
"JSON error: {}"},
564 std::vector<std::vector<FormatParam>>{{}, {
FormatParam{e.what()}}}};
AnimJsonParser(gsl::not_null< const TextFormatter * > format)
ChainableResult< void > write(const std::filesystem::path &json_path, const std::map< DynamicCasedName, AnimParams > ¶ms, const std::map< DynamicCasedName, std::vector< AnimOverrideEntry > > &primary_references={}) const
Writes animation parameters to an anim.json file.
ChainableResult< std::map< DynamicCasedName, std::vector< AnimOverrideEntry > > > parse_primary_references(const std::filesystem::path &json_path) const
Parses the primary_references section from an anim.json file.
ChainableResult< std::map< DynamicCasedName, AnimParams > > parse(const std::filesystem::path &json_path) const
Parses an anim.json file into a map of animation parameters.
Configuration parameters for a single tileset animation.
const DynamicCasedName & cased_name() const
Returns the structured name for this animation, preserving case format information.
const std::vector< AnimOverrideEntry > & overrides() const
Returns the manual override entries for this animation.
const std::vector< DynamicCasedName > & frame_order() const
Returns the playback sequence.
std::size_t frame_offset() const
Returns the frame offset (remainder value for timer modulo check).
std::size_t tile_offset() const
Returns the VRAM tile offset for this animation.
const std::vector< DynamicCasedName > & frame_names() const
Returns the unique frame definitions.
std::size_t frame_factor() const
Returns the frame factor (modulus divisor for timer).
std::size_t counter_max() const
Returns the animation counter maximum value.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
A smart string wrapper that preserves word structure for lossless case format conversion.
std::string to_snake_case() const
Outputs all words flattened and joined with underscores.
static DynamicCasedName from_snake_case(const std::string &input)
Constructs from a snake_case input string.
A service for printing file lines with highlighted lines and line numbers.
static const Style bold
Bold text formatting.
virtual std::string format(const std::string &format_str, const std::vector< FormatParam > ¶ms) const
Formats a string with styled parameters using fmtlib syntax.
constexpr std::size_t default_counter_max
constexpr std::size_t default_frame_offset
constexpr std::size_t default_frame_factor
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
A manual override that maps a specific metatile entry to an animation subtile.
std::size_t metatile_id
The metatile ID this override applies to (corresponds to JSON "id" field).