32const std::filesystem::path porytiles_src_marker{
"porytiles_src"};
33const std::filesystem::path porytiles_bin_marker{
"porytiles_bin"};
34const std::filesystem::path porytiles_generated_marker{
"porytiles_generated"};
45enum class ArtifactCategory { porytiles_src, porytiles_bin, special };
55struct ArtifactPathInfo {
56 ArtifactCategory category;
57 std::filesystem::path directory;
71[[nodiscard]] ArtifactPathInfo categorize_artifact_key(
const std::filesystem::path &key_path)
74 std::filesystem::path accumulated;
75 for (
const auto &component : key_path) {
76 accumulated /= component;
78 if (component == porytiles_src_marker) {
79 return ArtifactPathInfo{ArtifactCategory::porytiles_src, accumulated};
81 if (component == porytiles_bin_marker) {
82 return ArtifactPathInfo{ArtifactCategory::porytiles_bin, accumulated};
84 if (component == porytiles_generated_marker) {
86 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
91 return ArtifactPathInfo{ArtifactCategory::special, key_path.parent_path()};
105[[nodiscard]] std::filesystem::path create_project_tmpdir(
const std::filesystem::path &project_root)
107 int max_tries = 1000;
108 std::random_device random_device;
109 std::mt19937 mersenne_prng(random_device());
110 std::uniform_int_distribution<uint64_t> uniform_int_distribution(0);
111 std::filesystem::path path;
113 for (
int i = 0; i <= max_tries; ++i) {
114 std::stringstream string_stream;
115 string_stream << std::hex << uniform_int_distribution(mersenne_prng);
116 path = project_root / (
".porytiles_tmp_" + string_stream.str());
117 if (std::filesystem::create_directory(path)) {
120 if (i == max_tries) {
121 panic(
"create_project_tmpdir: exceeded maximum retries");
124 panic(
"create_project_tmpdir: unreachable");
132 if (!result.has_value()) {
141 const std::filesystem::path &path,
144 auto result = saver.
save_to_file(tiles_png, path, tiles_pal_mode);
145 if (!result.has_value()) {
151ChainableResult<void> save_metatiles_bin(
const std::vector<TilemapEntry> &entries,
const std::filesystem::path &path)
153 std::ofstream out{path};
154 for (
const auto &entry : entries) {
155 const auto tile_value =
static_cast<uint16_t
>(
156 (entry.tile_index() & 0x3ff) | ((entry.h_flip() & 1) << 10) | ((entry.v_flip() & 1) << 11) |
157 ((entry.pal_index() & 0xf) << 12));
158 out << static_cast<std::uint8_t>(tile_value);
159 out << static_cast<std::uint8_t>(tile_value >> 8);
166 const std::vector<MetatileAttribute> &attributes,
const std::filesystem::path &path)
168 std::ofstream out{path};
169 for (
const auto &attribute : attributes) {
170 const std::uint16_t behavior = attribute.behavior();
171 const auto layer_type =
static_cast<std::uint8_t
>(attribute.layer_type());
172 const auto attribute_value =
static_cast<std::uint16_t
>((behavior & 0xff) | ((layer_type & 0xf) << 12));
173 out << static_cast<std::uint8_t>(attribute_value);
174 out << static_cast<std::uint8_t>(attribute_value >> 8);
181 const std::vector<MetatileAttribute> &attributes,
const std::filesystem::path &path)
183 std::ofstream out{path};
184 for (
const auto &attribute : attributes) {
194 const auto attribute_value =
static_cast<std::uint32_t
>(
195 (
static_cast<std::uint32_t
>(attribute.behavior()) & 0x1FF) |
196 ((
static_cast<std::uint32_t
>(attribute.terrain()) & 0x1F) << 9) |
197 ((
static_cast<std::uint32_t
>(attribute.attribute_2()) & 0x0F) << 14) |
198 ((
static_cast<std::uint32_t
>(attribute.attribute_3()) & 0x3F) << 18) |
199 ((
static_cast<std::uint32_t
>(attribute.encounter_type()) & 0x07) << 24) |
200 ((
static_cast<std::uint32_t
>(attribute.attribute_5()) & 0x03) << 27) |
201 ((
static_cast<std::uint32_t
>(attribute.layer_type()) & 0x03) << 29) |
202 ((
static_cast<std::uint32_t
>(attribute.attribute_7()) & 0x01) << 31));
203 out << static_cast<std::uint8_t>(attribute_value);
204 out << static_cast<std::uint8_t>(attribute_value >> 8);
205 out << static_cast<std::uint8_t>(attribute_value >> 16);
206 out << static_cast<std::uint8_t>(attribute_value >> 24);
222 const std::filesystem::path &path,
225 auto result = saver.
save_to_file(frame, path, tiles_pal_mode);
226 if (!result.has_value()) {
251template <
typename StagedDirectory>
253 const std::filesystem::path &transaction_root,
254 const std::filesystem::path &project_root,
256 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
257 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files)
259 if (transaction_root.empty()) {
263 const std::filesystem::path key_path{dest_key.
key()};
264 const auto path_info = categorize_artifact_key(key_path);
266 if (path_info.category == ArtifactCategory::special) {
268 const auto staging_path = transaction_root / key_path;
269 std::filesystem::create_directories(staging_path.parent_path());
272 const auto dest_path = project_root / key_path;
273 staged_special_files.emplace_back(staging_path, dest_path);
279 const auto &category_dir = path_info.directory;
280 const auto dest_dir = project_root / category_dir;
283 if (!staged_directories.contains(dest_dir)) {
285 const auto staging_dir = transaction_root / category_dir;
286 staged_directories[dest_dir] = StagedDirectory{staging_dir, dest_dir};
290 const auto relative_within_category = std::filesystem::relative(key_path, category_dir);
291 const auto staging_path = staged_directories[dest_dir].staging_path / relative_within_category;
294 std::filesystem::create_directories(staging_path.parent_path());
313template <
typename PixelType>
315 const std::vector<
PixelTile<PixelType>> &tiles, std::size_t width_tiles = 0, std::size_t height_tiles = 0)
322 std::size_t tiles_per_row;
323 std::size_t tiles_per_col;
325 if (width_tiles > 0 && height_tiles > 0) {
327 if (width_tiles * height_tiles != tiles.size()) {
330 "tiles_to_image: width_tiles ({}) * height_tiles ({}) != tiles.size() ({})",
335 tiles_per_row = width_tiles;
336 tiles_per_col = height_tiles;
340 tiles_per_row = tiles.size();
349 for (std::size_t tile_idx = 0; tile_idx < tiles.size(); ++tile_idx) {
350 const auto &tile = tiles[tile_idx];
351 const std::size_t tile_row = tile_idx / tiles_per_row;
352 const std::size_t tile_col = tile_idx % tiles_per_row;
358 const std::size_t dest_row = pixel_row_offset + pixel_row;
359 const std::size_t dest_col = pixel_col_offset + pixel_col;
360 img.
set(dest_row, dest_col, tile.at(pixel_row, pixel_col));
393template <SupportsTransparency PixelType,
typename StagedDirectory,
typename ComponentGetter,
typename SaveFunc>
397 const std::string &anim_name,
398 const std::string &frame_name,
399 const std::filesystem::path &transaction_root,
400 const std::filesystem::path &project_root,
401 std::map<std::filesystem::path, StagedDirectory> &staged_directories,
402 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> &staged_special_files,
403 ComponentGetter component_getter,
405 std::string_view component_name)
407 const auto &component = component_getter(src);
409 if (!component.has_anim(anim_name)) {
411 "animation '{}' not found in {} component",
416 const auto &anim = component.anim_for_name(anim_name);
420 if (frame_name !=
"key") {
421 frame_ptr = &anim.frame_for_name(frame_name);
424 frame_ptr = &anim.key_frame();
428 const auto ¶ms = anim.params();
429 auto img = tiles_to_image(frame_ptr->
tiles(), params.width_tiles(), params.height_tiles());
433 const auto &pal = frame_ptr->
palette();
434 std::vector<Rgba32> pal_vec;
435 pal_vec.reserve(pal.
size());
436 for (std::size_t i = 0; i < pal.
size(); ++i) {
437 pal_vec.push_back(pal.
at(i));
439 img.palette(std::move(pal_vec));
444 transaction_dest_path,
445 compute_transaction_dest_path(
446 transaction_root, project_root, dest_key, staged_directories, staged_special_files),
448 "Failed to compute transaction dest path.");
451 return save_func(img, transaction_dest_path);
460 if (!transaction_root_.empty()) {
465 transaction_root_ = create_project_tmpdir(project_root_);
468 staged_directories_.clear();
469 staged_special_files_.clear();
476 if (transaction_root_.empty()) {
481 if (staged_directories_.empty() && staged_special_files_.empty()) {
482 std::filesystem::remove_all(transaction_root_);
483 transaction_root_.clear();
488 const auto backup_root = create_project_tmpdir(project_root_);
491 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> moved_directories;
492 std::vector<std::pair<std::filesystem::path, std::filesystem::path>> backed_up_special_files;
493 std::vector<std::filesystem::path> new_special_files;
497 for (
const auto &[dest_dir, staged_info] : staged_directories_) {
498 if (std::filesystem::exists(dest_dir)) {
500 const auto relative = std::filesystem::relative(dest_dir, project_root_);
501 const auto backup_path = backup_root / relative;
502 std::filesystem::create_directories(backup_path.parent_path());
505 std::filesystem::rename(dest_dir, backup_path);
506 moved_directories.emplace_back(dest_dir, backup_path);
511 for (
const auto &[dest_dir, staged_info] : staged_directories_) {
513 std::filesystem::create_directories(dest_dir.parent_path());
516 std::filesystem::rename(staged_info.staging_path, dest_dir);
520 for (
const auto &[staging_path, dest_path] : staged_special_files_) {
522 if (std::filesystem::exists(dest_path)) {
523 const auto relative = std::filesystem::relative(dest_path, project_root_);
524 const auto backup_path = backup_root / relative;
525 std::filesystem::create_directories(backup_path.parent_path());
526 std::filesystem::copy_file(dest_path, backup_path);
527 backed_up_special_files.emplace_back(dest_path, backup_path);
530 new_special_files.push_back(dest_path);
534 std::filesystem::create_directories(dest_path.parent_path());
535 std::filesystem::copy_file(staging_path, dest_path, std::filesystem::copy_options::overwrite_existing);
539 std::filesystem::remove_all(transaction_root_);
540 std::filesystem::remove_all(backup_root);
541 transaction_root_.clear();
542 staged_directories_.clear();
543 staged_special_files_.clear();
547 catch (
const std::filesystem::filesystem_error &e) {
551 for (
const auto &[original_path, backup_path] : moved_directories) {
552 if (std::filesystem::exists(backup_path)) {
554 if (std::filesystem::exists(original_path)) {
555 std::filesystem::remove_all(original_path);
557 std::filesystem::rename(backup_path, original_path);
562 for (
const auto &[original_path, backup_path] : backed_up_special_files) {
563 if (std::filesystem::exists(backup_path)) {
564 std::filesystem::copy_file(
565 backup_path, original_path, std::filesystem::copy_options::overwrite_existing);
570 for (
const auto &new_file : new_special_files) {
571 if (std::filesystem::exists(new_file)) {
572 std::filesystem::remove(new_file);
576 catch (
const std::filesystem::filesystem_error &) {
581 if (std::filesystem::exists(backup_root)) {
582 std::filesystem::remove_all(backup_root);
584 if (std::filesystem::exists(transaction_root_)) {
585 std::filesystem::remove_all(transaction_root_);
587 transaction_root_.clear();
588 staged_directories_.clear();
589 staged_special_files_.clear();
597 if (transaction_root_.empty()) {
602 if (std::filesystem::exists(transaction_root_)) {
603 std::filesystem::remove_all(transaction_root_);
605 transaction_root_.clear();
606 staged_directories_.clear();
607 staged_special_files_.clear();
610 catch (
const std::filesystem::filesystem_error &e) {
611 transaction_root_.clear();
612 staged_directories_.clear();
613 staged_special_files_.clear();
624 transaction_dest_path,
625 compute_transaction_dest_path(
626 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
628 "Failed to compute transaction dest path.");
636 transaction_dest_path,
637 compute_transaction_dest_path(
638 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
640 "Failed to compute transaction dest path.");
642 return save_firered_metatile_attributes_bin(
645 return save_emerald_metatile_attributes_bin(
652 transaction_dest_path,
653 compute_transaction_dest_path(
654 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
656 "Failed to compute transaction dest path.");
658 tiles_pal_mode_config,
661 "Failed to get tiles_pal_mode config.");
662 return save_tiles_png(
670 transaction_dest_path,
671 compute_transaction_dest_path(
672 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
674 "Failed to compute transaction dest path.");
677 panic(
"attempted to save a Porymap palette containing wildcards");
679 return save_palette(pal, transaction_dest_path, *pal_saver_);
683 const ArtifactKey &dest_key,
const Tileset &src,
const std::string &anim_name,
const std::string &frame_name)
686 tiles_pal_mode_config,
689 "Failed to get tiles_pal_mode config.");
690 return write_anim_frame_impl<IndexPixel>(
698 staged_special_files_,
700 [
this, &tiles_pal_mode_config](
const Image<IndexPixel> &img,
const std::filesystem::path &path) {
701 return save_porymap_anim_frame(*png_indexed_saver_, img, path, tiles_pal_mode_config.value());
710 if (porymap_anims.empty()) {
712 if (std::filesystem::exists(project_root_ / dest_key.
key())) {
713 std::filesystem::remove(project_root_ / dest_key.
key());
718 std::map<DynamicCasedName, AnimParams> anim_params;
719 for (
const auto &[anim_name, anim] : porymap_anims) {
728 "Failed to determine primary/secondary status for '{}'.",
730 const bool is_primary = !is_secondary;
738 "Failed to get tileset bin path config for '{}'.",
740 const std::filesystem::path tileset_path =
745 anim_code_generator_->
generate(src.
name(), tileset_path, anim_params, is_primary),
747 "Failed to generate animation code for '{}'.",
751 transaction_dest_path,
752 compute_transaction_dest_path(
753 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
755 "Failed to compute transaction dest path.");
757 std::ofstream out{transaction_dest_path};
758 if (!out.is_open()) {
762 out << generated_code;
774 transaction_dest_path,
775 compute_transaction_dest_path(
776 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
778 "Failed to compute transaction dest path.");
785 transaction_dest_path,
786 compute_transaction_dest_path(
787 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
789 "Failed to compute transaction dest path.");
796 transaction_dest_path,
797 compute_transaction_dest_path(
798 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
800 "Failed to compute transaction dest path.");
809 constexpr std::uint16_t default_behavior = 0;
810 constexpr std::uint8_t default_terrain = 0;
811 constexpr std::uint8_t default_encounter = 0;
816 std::size_t non_default_count = 0;
817 for (
const auto &attribute : attributes | std::views::values) {
819 if (attribute.behavior() != default_behavior || attribute.terrain() != default_terrain ||
820 attribute.encounter_type() != default_encounter) {
825 if (attribute.behavior() != default_behavior) {
832 transaction_dest_path,
833 compute_transaction_dest_path(
834 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
836 "Failed to compute transaction dest path.");
838 std::ofstream out{transaction_dest_path};
839 if (!out.is_open()) {
846 out <<
"id,behavior,terrainType,encounterType\n";
849 out <<
"id,behavior\n";
852 if (non_default_count == 0) {
859 for (
const auto &[metatile_id, attribute] : attributes) {
861 if (attribute.behavior() == default_behavior && attribute.terrain() == default_terrain &&
862 attribute.encounter_type() == default_encounter) {
867 behavior_map_->
lookup(attribute.behavior()),
869 std::format(
"Failed to lookup behavior name for metatile {}.", metatile_id));
872 terrain_map_->
lookup(attribute.terrain()),
874 std::format(
"Failed to lookup terrain type name for metatile {}.", metatile_id));
877 encounter_map_->
lookup(attribute.encounter_type()),
879 std::format(
"Failed to lookup encounter type name for metatile {}.", metatile_id));
880 out << metatile_id <<
"," << behavior_name <<
"," << terrain_name <<
"," << encounter_name <<
"\n";
883 if (attribute.behavior() == default_behavior) {
889 behavior_map_->
lookup(attribute.behavior()),
891 std::format(
"Failed to lookup behavior name for metatile {}.", metatile_id));
892 out << metatile_id <<
"," << behavior_name <<
"\n";
905 transaction_dest_path,
906 compute_transaction_dest_path(
907 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
909 "Failed to compute transaction dest path.");
919 const ArtifactKey &dest_key,
const Tileset &src,
const std::string &anim_name,
const std::string &frame_name)
921 return write_anim_frame_impl<Rgba32>(
929 staged_special_files_,
931 [
this](
const Image<Rgba32> &img,
const std::filesystem::path &path) {
932 return save_layer_png(*png_rgba_saver_, img, path);
943 if (porytiles_anims.empty() && primary_overrides.empty()) {
953 std::map<DynamicCasedName, AnimParams> anim_params;
954 for (
const auto &[anim_name, anim] : porytiles_anims) {
959 std::map<DynamicCasedName, std::vector<AnimOverrideEntry>> primary_refs;
960 for (
const auto &[name, entries] : primary_overrides) {
965 transaction_dest_path,
966 compute_transaction_dest_path(
967 transaction_root_, project_root_, dest_key, staged_directories_, staged_special_files_),
969 "Failed to compute transaction dest path.");
971 return anim_json_parser_->
write(transaction_dest_path, anim_params, primary_refs);
#define PT_TRY_ASSIGN_CHAIN_ERR(var, expr, return_type,...)
Unwraps a ChainableResult, chaining a new error message on failure.
#define PT_TRY_CALL_CHAIN_ERR(expr, return_type,...)
Unwraps a void ChainableResult, chaining a new error message on failure.
ChainableResult< std::string > generate(const std::string &tileset_name, const std::filesystem::path &tileset_path_from_project_root, const std::map< DynamicCasedName, AnimParams > &animations, bool is_primary) const
Generates the complete generated_anim_code.h content.
Represents a single frame of an animation, containing tiles and a frame name.
const Palette< Rgba32 > & palette() const
const std::vector< PixelTile< PixelType > > & tiles() const
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.
A type-safe wrapper for artifact keys.
const std::string & key() const
virtual ChainableResult< std::uint16_t > lookup(const std::string &behavior_name) const =0
Looks up the numeric value for a behavior constant name.
A result type that maintains a chainable sequence of errors for debugging and error reporting.
ChainableResult< ConfigValue< TilesPalMode > > tiles_pal_mode(ConfigScopeType type, const std::string &scope) const
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.
virtual ChainableResult< std::uint8_t > lookup(const std::string &encounter_name) const =0
Looks up the numeric value for an encounter type constant name.
A service interface that saves a fixed-length Palette to a given file.
virtual ChainableResult< void > save(const Palette< Rgba32, pal::max_size > &pal, const std::filesystem::path &path) const =0
A template for two-dimensional images with arbitrarily typed pixel values.
void set(std::size_t i, PixelType pixel)
Sets the pixel value at a given one-dimensional pixel index.
ChainableResult< ConfigValue< std::string > > tileset_paths_secondary_bin(ConfigScopeType type, const std::string &scope) const
ChainableResult< ConfigValue< std::string > > tileset_paths_primary_bin(ConfigScopeType type, const std::string &scope) const
A generic palette container for colors that support transparency checking.
std::size_t size() const
Returns the number of slots in the palette.
ColorType at(std::size_t index) const
Gets the color at a specific index.
bool has_any_wildcards() const
Checks if the palette contains any wildcard slots.
An 8x8 tile backed by literal-array-based per-pixel storage of an arbitrary pixel type.
An image saver that saves PNG files from an Image with an index pixel type.
virtual ChainableResult< void > save_to_file(const Image< IndexPixel > &image, const std::filesystem::path &path, TilesPalMode mode) const
An image saver that saves PNG files from an Image with an Rgba32 pixel type.
virtual ChainableResult< void > save_to_file(const Image< Rgba32 > &image, const std::filesystem::path &path) const
const std::map< std::string, Animation< IndexPixel > > & anims() const
const std::vector< TilemapEntry > & metatiles_bin() const
const Palette< Rgba32, pal::max_size > & pal_at(std::size_t pal_index) const
const std::vector< MetatileAttribute > & metatile_attributes_bin() const
const Image< IndexPixel > & tiles_png() const
const Image< Rgba32 > & bottom() const
const std::optional< Palette< Rgba32, pal::max_size > > & pal_at(std::size_t pal_index) const
const Image< Rgba32 > & middle() const
const std::map< std::string, Animation< Rgba32 > > & anims() const
const std::map< std::size_t, MetatileAttribute > & metatile_attributes() const
const Image< Rgba32 > & top() const
const std::map< std::string, std::vector< AnimOverrideEntry > > & primary_anim_overrides() const
ChainableResult< void > write_porymap_anim_params(const ArtifactKey &dest_key, const Tileset &src) override
Writes the animation parameters to the Porymap component backing store.
ChainableResult< void > write_porymap_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) override
ChainableResult< void > write_metatiles_bin(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_top_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > begin_transaction() override
Begins a new transaction for atomic write operations.
ChainableResult< void > write_porytiles_anim_params(const ArtifactKey &dest_key, const Tileset &src) override
Writes animation parameters to the Porytiles component backing store.
ChainableResult< void > write_tiles_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_bottom_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > commit() override
Commits all buffered write operations in the current transaction.
ChainableResult< void > write_middle_png(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > write_porymap_anim_frame(const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name) override
ChainableResult< void > write_porytiles_pal_n(const ArtifactKey &dest_key, const Tileset &src, std::size_t index) override
ChainableResult< void > write_metatile_attributes_bin(const ArtifactKey &dest_key, const Tileset &src) override
ChainableResult< void > rollback() override
Rolls back all buffered write operations in the current transaction.
ChainableResult< void > write_porytiles_anim_frame(const ArtifactKey &dest_key, const Tileset &src, const std::string &anim_name, const std::string &frame_name) override
ChainableResult< void > write_attributes_csv(const ArtifactKey &dest_key, const Tileset &src) override
static const Style bold
Bold text formatting.
virtual ChainableResult< std::uint8_t > lookup(const std::string &terrain_name) const =0
Looks up the numeric value for a terrain type constant name.
A complete tileset containing both Porytiles and Porymap components.
const PorytilesTilesetComponent & porytiles_component() const
const PorymapTilesetComponent & porymap_component() const
const std::string & name() const
constexpr std::size_t bytes_per_attr_firered
constexpr std::size_t side_length_pix
TilesPalMode
Controls how tiles.png is rendered.
void panic(const StringViewSourceLoc &s)
Unconditionally terminates the program with a panic message.
DynamicCasedName extract_tileset_cased_name(const std::string &tileset_name)
Extracts the tileset short name and wraps it in a DynamicCasedName.
@ tileset
Configuration scoped to a specific tileset.
Utility functions for string manipulation and formatting.