/
Mr.Stalin
/
FOnline-Engine
Обзор
Документация
Войти
/
Mr.Stalin
/
FOnline-Engine
Код
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
Source/Tools/ImageBaker.cpp
2 939 строк
110 KB
cvet
Alloc fixes (#192)
27 июл 2026, 14:30
Не верифицирован
27 июл 2026, 14:30
ba3760e
Код
Авторство
О чём код?
// __________ ___ ______ _ // / ____/ __ \____ / (_)___ ___ / ____/___ ____ _(_)___ ___ // / /_ / / / / __ \/ / / __ \/ _ \ / __/ / __ \/ __ `/ / __ \/ _ ` // / __/ / /_/ / / / / / / / / / __/ / /___/ / / / /_/ / / / / / __/ // /_/ \____/_/ /_/_/_/_/ /_/\___/ /_____/_/ /_/\__, /_/_/ /_/\___/ // /____/ // FOnline Engine // https://fonline.ru // https://github.com/cvet/fonline // // MIT License // // Copyright (c) 2006 - 2026, Anton Tsvetinskiy aka cvet <cvet@tut.by> // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // #include "ImageBaker.h" #include "Application.h" #include "ConfigFile.h" #include "FileSystem.h" #include "Settings.h" #include "SpriteMeshing.h" #include "SpriteResource.h" #include "png.h" FO_BEGIN_NAMESPACE static auto PngLoad(ptr<const uint8_t> data, int32_t& result_width, int32_t& result_height) -> vector<uint8_t>; static auto TgaLoad(span<const uint8_t> data, int32_t& result_width, int32_t& result_height) -> vector<uint8_t>; static auto PadSpriteFrame(const ImageBaker::FrameShot& shot, int32_t padding) -> ImageBaker::FrameShot; static auto ResolveSpriteFramePadding(const ImageBaker::FrameShot& shot, const BakedSpriteMesh& mesh, int32_t build_padding) -> int32_t; static void TranslateSpriteMesh(BakedSpriteMesh& mesh, int32_t offset, const ImageBaker::FrameShot& shot); static auto CropSpriteFrameToMeshBounds(const ImageBaker::FrameShot& shot, const ImageBaker::FrameShot& source_shot, int32_t padding, BakedSpriteMesh& mesh) -> optional<ImageBaker::FrameShot>; struct SpriteInfoBakingStats { uint64_t Collections {}; uint64_t Directions {}; uint64_t FrameSlots {}; uint64_t UniqueFrames {}; uint64_t SharedFrameReferences {}; }; // clang-format off alignas(ucolor) static uint8_t FoPalette[] = { // Transparent color 0x00, 0x00, 0x00, 0x00, // Default colors 0xec, 0xec, 0xec, 0xff, 0xdc, 0xdc, 0xdc, 0xff, 0xcc, 0xcc, 0xcc, 0xff, 0xbc, 0xbc, 0xbc, 0xff, 0xb0, 0xb0, 0xb0, 0xff, 0xa0, 0xa0, 0xa0, 0xff, 0x90, 0x90, 0x90, 0xff, 0x80, 0x80, 0x80, 0xff, 0x74, 0x74, 0x74, 0xff, 0x64, 0x64, 0x64, 0xff, 0x54, 0x54, 0x54, 0xff, 0x48, 0x48, 0x48, 0xff, 0x38, 0x38, 0x38, 0xff, 0x28, 0x28, 0x28, 0xff, 0x20, 0x20, 0x20, 0xff, 0xfc, 0xec, 0xec, 0xff, 0xec, 0xd8, 0xd8, 0xff, 0xdc, 0xc4, 0xc4, 0xff, 0xd0, 0xb0, 0xb0, 0xff, 0xc0, 0xa0, 0xa0, 0xff, 0xb0, 0x90, 0x90, 0xff, 0xa4, 0x80, 0x80, 0xff, 0x94, 0x70, 0x70, 0xff, 0x84, 0x60, 0x60, 0xff, 0x78, 0x54, 0x54, 0xff, 0x68, 0x44, 0x44, 0xff, 0x58, 0x38, 0x38, 0xff, 0x4c, 0x2c, 0x2c, 0xff, 0x3c, 0x24, 0x24, 0xff, 0x2c, 0x18, 0x18, 0xff, 0x20, 0x10, 0x10, 0xff, 0xec, 0xec, 0xfc, 0xff, 0xd8, 0xd8, 0xec, 0xff, 0xc4, 0xc4, 0xdc, 0xff, 0xb0, 0xb0, 0xd0, 0xff, 0xa0, 0xa0, 0xc0, 0xff, 0x90, 0x90, 0xb0, 0xff, 0x80, 0x80, 0xa4, 0xff, 0x70, 0x70, 0x94, 0xff, 0x60, 0x60, 0x84, 0xff, 0x54, 0x54, 0x78, 0xff, 0x44, 0x44, 0x68, 0xff, 0x38, 0x38, 0x58, 0xff, 0x2c, 0x2c, 0x4c, 0xff, 0x24, 0x24, 0x3c, 0xff, 0x18, 0x18, 0x2c, 0xff, 0x10, 0x10, 0x20, 0xff, 0xfc, 0xb0, 0xf0, 0xff, 0xc4, 0x60, 0xa8, 0xff, 0x68, 0x24, 0x60, 0xff, 0x4c, 0x14, 0x48, 0xff, 0x38, 0x0c, 0x34, 0xff, 0x28, 0x10, 0x24, 0xff, 0x24, 0x04, 0x24, 0xff, 0x1c, 0x0c, 0x18, 0xff, 0xfc, 0xfc, 0xc8, 0xff, 0xfc, 0xfc, 0x7c, 0xff, 0xe4, 0xd8, 0x0c, 0xff, 0xcc, 0xb8, 0x1c, 0xff, 0xb8, 0x9c, 0x28, 0xff, 0xa4, 0x88, 0x30, 0xff, 0x90, 0x78, 0x24, 0xff, 0x7c, 0x68, 0x18, 0xff, 0x6c, 0x58, 0x10, 0xff, 0x58, 0x48, 0x08, 0xff, 0x48, 0x38, 0x04, 0xff, 0x34, 0x28, 0x00, 0xff, 0x20, 0x18, 0x00, 0xff, 0xd8, 0xfc, 0x9c, 0xff, 0xb4, 0xd8, 0x84, 0xff, 0x98, 0xb8, 0x70, 0xff, 0x78, 0x98, 0x5c, 0xff, 0x5c, 0x78, 0x48, 0xff, 0x40, 0x58, 0x34, 0xff, 0x28, 0x38, 0x20, 0xff, 0x70, 0x60, 0x50, 0xff, 0x54, 0x48, 0x34, 0xff, 0x38, 0x30, 0x20, 0xff, 0x68, 0x78, 0x50, 0xff, 0x70, 0x78, 0x20, 0xff, 0x70, 0x68, 0x28, 0xff, 0x60, 0x60, 0x24, 0xff, 0x4c, 0x44, 0x24, 0xff, 0x38, 0x30, 0x20, 0xff, 0x9c, 0xac, 0x9c, 0xff, 0x78, 0x94, 0x78, 0xff, 0x58, 0x7c, 0x58, 0xff, 0x40, 0x68, 0x40, 0xff, 0x38, 0x58, 0x58, 0xff, 0x30, 0x4c, 0x48, 0xff, 0x28, 0x44, 0x3c, 0xff, 0x20, 0x3c, 0x2c, 0xff, 0x1c, 0x30, 0x24, 0xff, 0x14, 0x28, 0x18, 0xff, 0x10, 0x20, 0x10, 0xff, 0x18, 0x30, 0x18, 0xff, 0x10, 0x24, 0x0c, 0xff, 0x08, 0x1c, 0x04, 0xff, 0x04, 0x14, 0x00, 0xff, 0x04, 0x0c, 0x00, 0xff, 0x8c, 0x9c, 0x9c, 0xff, 0x78, 0x94, 0x98, 0xff, 0x64, 0x88, 0x94, 0xff, 0x50, 0x7c, 0x90, 0xff, 0x40, 0x6c, 0x8c, 0xff, 0x30, 0x58, 0x8c, 0xff, 0x2c, 0x4c, 0x7c, 0xff, 0x28, 0x44, 0x6c, 0xff, 0x20, 0x38, 0x5c, 0xff, 0x1c, 0x30, 0x4c, 0xff, 0x18, 0x28, 0x40, 0xff, 0x9c, 0xa4, 0xa4, 0xff, 0x38, 0x48, 0x68, 0xff, 0x50, 0x58, 0x58, 0xff, 0x58, 0x68, 0x84, 0xff, 0x38, 0x40, 0x50, 0xff, 0xbc, 0xbc, 0xbc, 0xff, 0xac, 0xa4, 0x98, 0xff, 0xa0, 0x90, 0x7c, 0xff, 0x94, 0x7c, 0x60, 0xff, 0x88, 0x68, 0x4c, 0xff, 0x7c, 0x58, 0x34, 0xff, 0x70, 0x48, 0x24, 0xff, 0x64, 0x3c, 0x14, 0xff, 0x58, 0x30, 0x08, 0xff, 0xfc, 0xcc, 0xcc, 0xff, 0xfc, 0xb0, 0xb0, 0xff, 0xfc, 0x98, 0x98, 0xff, 0xfc, 0x7c, 0x7c, 0xff, 0xfc, 0x64, 0x64, 0xff, 0xfc, 0x48, 0x48, 0xff, 0xfc, 0x30, 0x30, 0xff, 0xfc, 0x00, 0x00, 0xff, 0xe0, 0x00, 0x00, 0xff, 0xc4, 0x00, 0x00, 0xff, 0xa8, 0x00, 0x00, 0xff, 0x90, 0x00, 0x00, 0xff, 0x74, 0x00, 0x00, 0xff, 0x58, 0x00, 0x00, 0xff, 0x40, 0x00, 0x00, 0xff, 0xfc, 0xe0, 0xc8, 0xff, 0xfc, 0xc4, 0x94, 0xff, 0xfc, 0xb8, 0x78, 0xff, 0xfc, 0xac, 0x60, 0xff, 0xfc, 0x9c, 0x48, 0xff, 0xfc, 0x94, 0x2c, 0xff, 0xfc, 0x88, 0x14, 0xff, 0xfc, 0x7c, 0x00, 0xff, 0xdc, 0x6c, 0x00, 0xff, 0xc0, 0x60, 0x00, 0xff, 0xa4, 0x50, 0x00, 0xff, 0x84, 0x44, 0x00, 0xff, 0x68, 0x34, 0x00, 0xff, 0x4c, 0x24, 0x00, 0xff, 0x30, 0x18, 0x00, 0xff, 0xf8, 0xd4, 0xa4, 0xff, 0xd8, 0xb0, 0x78, 0xff, 0xc8, 0xa0, 0x64, 0xff, 0xbc, 0x90, 0x54, 0xff, 0xac, 0x80, 0x44, 0xff, 0x9c, 0x74, 0x34, 0xff, 0x8c, 0x64, 0x28, 0xff, 0x7c, 0x58, 0x1c, 0xff, 0x70, 0x4c, 0x14, 0xff, 0x60, 0x40, 0x08, 0xff, 0x50, 0x34, 0x04, 0xff, 0x40, 0x28, 0x00, 0xff, 0x34, 0x20, 0x00, 0xff, 0xfc, 0xe4, 0xb8, 0xff, 0xe8, 0xc8, 0x98, 0xff, 0xd4, 0xac, 0x7c, 0xff, 0xc4, 0x90, 0x64, 0xff, 0xb0, 0x74, 0x4c, 0xff, 0xa0, 0x5c, 0x38, 0xff, 0x90, 0x4c, 0x2c, 0xff, 0x84, 0x3c, 0x20, 0xff, 0x78, 0x2c, 0x18, 0xff, 0x6c, 0x20, 0x10, 0xff, 0x5c, 0x14, 0x08, 0xff, 0x48, 0x0c, 0x04, 0xff, 0x3c, 0x04, 0x00, 0xff, 0xfc, 0xe8, 0xdc, 0xff, 0xf8, 0xd4, 0xbc, 0xff, 0xf4, 0xc0, 0xa0, 0xff, 0xf0, 0xb0, 0x84, 0xff, 0xf0, 0xa0, 0x6c, 0xff, 0xf0, 0x94, 0x5c, 0xff, 0xd8, 0x80, 0x54, 0xff, 0xc0, 0x70, 0x48, 0xff, 0xa8, 0x60, 0x40, 0xff, 0x90, 0x50, 0x38, 0xff, 0x78, 0x40, 0x30, 0xff, 0x60, 0x30, 0x24, 0xff, 0x48, 0x24, 0x1c, 0xff, 0x38, 0x18, 0x14, 0xff, 0x64, 0xe4, 0x64, 0xff, 0x14, 0x98, 0x14, 0xff, 0x00, 0xa4, 0x00, 0xff, 0x50, 0x50, 0x48, 0xff, 0x00, 0x6c, 0x00, 0xff, 0x8c, 0x8c, 0x84, 0xff, 0x1c, 0x1c, 0x1c, 0xff, 0x68, 0x50, 0x38, 0xff, 0x30, 0x28, 0x20, 0xff, 0x8c, 0x70, 0x60, 0xff, 0x48, 0x38, 0x28, 0xff, 0x0c, 0x0c, 0x0c, 0xff, 0x3c, 0x3c, 0x3c, 0xff, 0x6c, 0x74, 0x6c, 0xff, 0x78, 0x84, 0x78, 0xff, 0x88, 0x94, 0x88, 0xff, 0x94, 0xa4, 0x94, 0xff, 0x58, 0x68, 0x60, 0xff, 0x60, 0x70, 0x68, 0xff, 0x3c, 0xf8, 0x00, 0xff, 0x38, 0xd4, 0x08, 0xff, 0x34, 0xb4, 0x10, 0xff, 0x30, 0x94, 0x14, 0xff, 0x28, 0x74, 0x18, 0xff, 0xfc, 0xfc, 0xfc, 0xff, 0xf0, 0xec, 0xd0, 0xff, 0xd0, 0xb8, 0x88, 0xff, 0x98, 0x7c, 0x50, 0xff, 0x68, 0x58, 0x3c, 0xff, 0x50, 0x40, 0x24, 0xff, 0x34, 0x28, 0x1c, 0xff, 0x18, 0x10, 0x0c, 0xff, 0x03, 0x03, 0x03, 0xff, // Slime 0x00, 0x6c, 0x00, 0xff, 0x0b, 0x73, 0x07, 0xff, 0x1b, 0x7b, 0x0f, 0xff, 0x2b, 0x83, 0x1b, 0xff, // Monitors 0x6b, 0x6b, 0x6f, 0xff, 0x63, 0x67, 0x7f, 0xff, 0x57, 0x6b, 0x8f, 0xff, 0x00, 0x93, 0xa3, 0xff, 0x6b, 0xbb, 0xff, 0xff, // FireSlow 0xff, 0x00, 0x00, 0xff, 0xd7, 0x00, 0x00, 0xff, 0x93, 0x2b, 0x0b, 0xff, 0xff, 0x77, 0x00, 0xff, 0xff, 0x3b, 0x00, 0xff, // FireFast 0x47, 0x00, 0x00, 0xff, 0x7b, 0x00, 0x00, 0xff, 0xb3, 0x00, 0x00, 0xff, 0x7b, 0x00, 0x00, 0xff, 0x47, 0x00, 0x00, 0xff, // Shoreline 0x53, 0x3f, 0x2b, 0xff, 0x4b, 0x3b, 0x2b, 0xff, 0x43, 0x37, 0x27, 0xff, 0x3f, 0x33, 0x27, 0xff, 0x37, 0x2f, 0x23, 0xff, 0x33, 0x2b, 0x23, 0xff, // BlinkingRed 0xfc, 0x00, 0x00, 0xff, // Unused 0xff, 0xff, 0xff, 0xff }; static_assert(sizeof(FoPalette) == 1024); // clang-format on ImageBaker::ImageBaker(shared_ptr<BakingContext> ctx) : BaseBaker(std::move(ctx), NAME) { FO_STACK_TRACE_ENTRY(); // Fill default loaders AddLoader(std::bind(&ImageBaker::LoadFofrm, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"fofrm"}); AddLoader(std::bind(&ImageBaker::LoadFrm, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"frm"}); AddLoader(std::bind(&ImageBaker::LoadFrX, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"fr0"}); AddLoader(std::bind(&ImageBaker::LoadRix, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"rix"}); AddLoader(std::bind(&ImageBaker::LoadArt, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"art"}); AddLoader(std::bind(&ImageBaker::LoadSpr, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"spr"}); AddLoader(std::bind(&ImageBaker::LoadZar, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"zar"}); AddLoader(std::bind(&ImageBaker::LoadTil, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"til"}); AddLoader(std::bind(&ImageBaker::LoadMos, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"mos"}); AddLoader(std::bind(&ImageBaker::LoadBam, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"bam"}); AddLoader(std::bind(&ImageBaker::LoadPng, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"png"}); AddLoader(std::bind(&ImageBaker::LoadTga, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4), {"tga"}); } void ImageBaker::AddLoader(const LoadFunc& loader, const vector<string>& file_extensions) { FO_STACK_TRACE_ENTRY(); for (const auto& ext : file_extensions) { _fileLoaders[ext] = loader; } } void ImageBaker::BakeFiles(const FileCollection& files, string_view target_path) const { FO_STACK_TRACE_ENTRY(); if (IsBakingReportEnabled()) { SpriteMeshBakeConfig mesh_config = ResolveSpriteMeshBakeConfig(_context->Settings); RecordSpriteMeshBakingSettings(SpriteMeshBakingReportSettings { .Enabled = mesh_config.Enabled, .AlphaThreshold = mesh_config.AlphaThreshold, .MaxTriangles = mesh_config.MaxTriangles, .AreaSavingsWeight = mesh_config.AreaSavingsWeight, .BaseDilation = SPRITE_MESH_DILATION, .MaximumPadding = SPRITE_MESH_MAXIMUM_PADDING, }); } vector<pair<File, LoadFunc>> files_to_bake; files_to_bake.reserve(files.GetFilesCount()); string sprite_info_path = strex("{}/{}.foinfo", SPRITE_INFO_DIRECTORY, _context->PackName); bool scan_mode = target_path.empty(); bool sprite_info_target = target_path == sprite_info_path; bool bake_sprite_info = scan_mode || sprite_info_target; bool sprite_info_needs_write = !_context->BakeChecker; map<string, SpriteInfoFileEntry> sprite_info_entries; set<string> current_sprite_sources; uint64_t maximum_source_write_time = 0; nptr<const FileSystem> sprite_info_files = _context->PackBakedFiles ? _context->PackBakedFiles : _context->BakedFiles; FO_VERIFY_AND_THROW(sprite_info_files, "Baker context has no baked file registry"); if (scan_mode && sprite_info_files->IsFileExists(sprite_info_path)) { File sprite_info_file = sprite_info_files->ReadFile(sprite_info_path); FO_VERIFY_AND_THROW(sprite_info_file, "Baked sprite info resource is not readable", sprite_info_path); for (SpriteInfoFileEntry& entry : ReadSpriteInfoFile(sprite_info_path, sprite_info_file.GetStr())) { bool inserted = sprite_info_entries.emplace(entry.SourcePath, std::move(entry)).second; FO_VERIFY_AND_THROW(inserted, "Baked sprite info resource contains a duplicate source path", sprite_info_path); } } if (bake_sprite_info) { for (auto&& [ext, loader] : _fileLoaders) { for (const auto& file_header : files) { string file_ext = strex(file_header.GetPath()).get_file_extension(); if (file_ext != ext) { continue; } current_sprite_sources.emplace(file_header.GetPath()); maximum_source_write_time = std::max(maximum_source_write_time, file_header.GetWriteTime()); if (scan_mode && _context->BakeChecker && !_context->BakeChecker(file_header.GetPath(), file_header.GetWriteTime())) { continue; } files_to_bake.emplace_back(File::Load(file_header), loader); } } } else { string ext = strex(target_path).get_file_extension(); if (!_fileLoaders.contains(ext)) { return; } auto file = files.FindFileByPath(target_path); if (!file) { return; } if (_context->BakeChecker && !_context->BakeChecker(target_path, file.GetWriteTime())) { return; } files_to_bake.emplace_back(std::move(file), _fileLoaders.at(ext)); } if (bake_sprite_info && !current_sprite_sources.empty() && _context->BakeChecker) { sprite_info_needs_write = _context->BakeChecker(sprite_info_path, maximum_source_write_time); } vector<std::future<SpriteInfoFileEntry>> file_bakings; for (size_t file_index = 0; file_index < files_to_bake.size(); file_index++) { string task_name = strex("BakeImage-{}", files_to_bake[file_index].first.GetPath()).str(); file_bakings.emplace_back(run_async(GetAsyncMode(), task_name, [this, &files, &files_to_bake, file_index]() FO_DEFERRED -> SpriteInfoFileEntry { auto& file_to_bake = files_to_bake[file_index]; string_view path = file_to_bake.first.GetPath(); auto collection = file_to_bake.second(path, "", file_to_bake.first.GetReader(), files); return BakeCollection(path, collection); })); } size_t errors = 0; for (auto& file_baking : file_bakings) { try { SpriteInfoFileEntry entry = file_baking.get(); sprite_info_entries[entry.SourcePath] = std::move(entry); } catch (const std::exception& ex) { WriteLog("Image baking error: {}", ex.what()); errors++; } } if (errors != 0) { throw ImageBakerException("Errors during images baking", errors); } if (!bake_sprite_info || _context->OutputDiscovery) { return; } size_t sprite_info_entry_count = sprite_info_entries.size(); std::erase_if(sprite_info_entries, [¤t_sprite_sources](const auto& entry) { return !current_sprite_sources.contains(entry.first); }); bool removed_sprite_info_entries = sprite_info_entries.size() != sprite_info_entry_count; FO_VERIFY_AND_THROW(sprite_info_entries.size() == current_sprite_sources.size(), "Sprite info index is incomplete; perform a full resource rebake", _context->PackName, sprite_info_entries.size(), current_sprite_sources.size()); if (!sprite_info_needs_write && files_to_bake.empty() && !removed_sprite_info_entries) { return; } if (!sprite_info_entries.empty()) { vector<SpriteInfoFileEntry> entries; entries.reserve(sprite_info_entries.size()); for (auto& [source_path, entry] : sprite_info_entries) { ignore_unused(source_path); entries.emplace_back(std::move(entry)); } string sprite_info = WriteSpriteInfoFile(entries); vector<uint8_t> sprite_info_data(sprite_info.begin(), sprite_info.end()); _context->WriteData(sprite_info_path, sprite_info_data); } } auto ImageBaker::BakeCollection(string_view fname, const FrameCollection& collection) const -> SpriteInfoFileEntry { FO_STACK_TRACE_ENTRY(); vector<uint8_t> data; auto writer = DataWriter(data); auto dirs = numeric_cast<uint8_t>(collection.HaveDirs ? GameSettings::MAP_DIR_COUNT : 1); SpriteMeshBakeConfig mesh_config = ResolveSpriteMeshBakeConfig(_context->Settings); string output_path = collection.NewName.empty() ? string(fname) : collection.NewName; SpriteInfoFileEntry sprite_info_entry { .SourcePath = string(fname), .ResourcePath = output_path, .Info = { .FrameCount = collection.SequenceSize, .Duration = std::chrono::milliseconds {collection.AnimTicks}, .Directions = vector<SpriteDirInfo>(dirs), }, }; bool report_enabled = IsBakingReportEnabled(); vector<SpriteMeshBakingFrameReport> frame_reports; SpriteInfoBakingStats stats { .Collections = 1, .Directions = dirs, }; writer.Write<uint8_t>(SPRITE_RESOURCE_MAGIC); writer.Write<uint8_t>(SPRITE_RESOURCE_VERSION); writer.Write<uint16_t>(collection.SequenceSize); writer.Write<uint16_t>(collection.AnimTicks); writer.Write<uint8_t>(dirs); for (uint8_t dir = 0; dir < dirs; dir++) { const auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; SpriteDirInfo& direction_info = sprite_info_entry.Info.Directions[dir]; direction_info.Frames.resize(collection.SequenceSize); vector<optional<BakedSpriteMesh>> prepared_meshes(sequence.Frames.size()); vector<int32_t> mesh_build_paddings(sequence.Frames.size()); vector<int32_t> frame_paddings(sequence.Frames.size()); if (mesh_config.Enabled) { for (size_t frame_index = 0; frame_index < sequence.Frames.size(); frame_index++) { const FrameShot& shot = sequence.Frames[frame_index]; if (shot.Shared) { continue; } optional<FrameShot> maximum_padded_shot; ptr<const FrameShot> mesh_shot = &shot; if (shot.Width > 0 && shot.Height > 0) { maximum_padded_shot = PadSpriteFrame(shot, SPRITE_MESH_MAXIMUM_PADDING); mesh_shot = &*maximum_padded_shot; mesh_build_paddings[frame_index] = SPRITE_MESH_MAXIMUM_PADDING; } int64_t reference_quad_double_area = numeric_cast<int64_t>(shot.Width) * shot.Height * 2; prepared_meshes[frame_index] = BuildSpriteMesh(mesh_shot->Data, {mesh_shot->Width, mesh_shot->Height}, mesh_config, reference_quad_double_area); frame_paddings[frame_index] = ResolveSpriteFramePadding(shot, *prepared_meshes[frame_index], mesh_build_paddings[frame_index]); } } for (int32_t i = 0; i < collection.SequenceSize; i++) { const auto& shot = sequence.Frames[i]; SpriteFrameInfo& frame_info = direction_info.Frames[numeric_cast<size_t>(i)]; stats.FrameSlots++; writer.Write<bool>(shot.Shared); if (!shot.Shared) { stats.UniqueFrames++; int32_t frame_padding = frame_paddings[numeric_cast<size_t>(i)]; optional<FrameShot> padded_shot; ptr<const FrameShot> bake_shot = &shot; if (frame_padding > 0) { padded_shot = PadSpriteFrame(shot, frame_padding); bake_shot = &*padded_shot; } FO_VERIFY_AND_THROW(bake_shot->Data.size() == numeric_cast<size_t>(bake_shot->Width) * bake_shot->Height * 4, "Animation frame RGBA payload size does not match frame dimensions", bake_shot->Data.size(), bake_shot->Width, bake_shot->Height); BakedSpriteMesh mesh; if (prepared_meshes[numeric_cast<size_t>(i)].has_value() && prepared_meshes[numeric_cast<size_t>(i)]->Kind != SpriteMeshKind::Quad) { mesh = std::move(*prepared_meshes[numeric_cast<size_t>(i)]); TranslateSpriteMesh(mesh, frame_padding - mesh_build_paddings[numeric_cast<size_t>(i)], *bake_shot); } else { int64_t reference_quad_double_area = numeric_cast<int64_t>(shot.Width) * shot.Height * 2; mesh = BuildSpriteMesh(bake_shot->Data, {bake_shot->Width, bake_shot->Height}, mesh_config, reference_quad_double_area); } optional<FrameShot> cropped_shot; if (mesh.Kind == SpriteMeshKind::Mesh) { cropped_shot = CropSpriteFrameToMeshBounds(*bake_shot, shot, frame_padding, mesh); if (cropped_shot.has_value()) { bake_shot = &*cropped_shot; } } ipos32 source_offset {sequence.OffsX, sequence.OffsY}; isize32 source_size {shot.Width, shot.Height}; isize32 cropped_size {bake_shot->Width, bake_shot->Height}; FO_VERIFY_AND_THROW(mesh.Kind != SpriteMeshKind::Mesh || (source_size.width > 0 && source_size.height > 0 && cropped_size.width > 0 && cropped_size.height > 0), "Sprite frame offset resolution requires positive dimensions", source_size, cropped_size); ipos32 frame_offset = mesh.Kind == SpriteMeshKind::Mesh ? ipos32 { source_offset.x + cropped_size.width / 2 - source_size.width / 2 + mesh.Data.SourceOffset.x, source_offset.y + cropped_size.height - source_size.height + mesh.Data.SourceOffset.y, } : source_offset; if (report_enabled) { optional<SpriteMeshBakingCandidateReport> best_rejected_candidate; if (mesh.BestRejectedCandidate.has_value()) { const SpriteMeshCandidateSummary& candidate = *mesh.BestRejectedCandidate; best_rejected_candidate = SpriteMeshBakingCandidateReport { .SelectionOrigin = string(SpriteMeshCandidateSourceName(candidate.Source)), .TriangleCount = candidate.TriangleCount, .VertexCount = candidate.VertexCount, .SubmittedGeometryDoubleArea = candidate.DoubleArea, .Score = candidate.Score, .SimplifyTolerance = candidate.SimplifyTolerance, .ActualDilation = candidate.ActualDilation, }; } frame_reports.emplace_back(SpriteMeshBakingFrameReport { .SourcePath = string(fname), .OutputPath = output_path, .Direction = dir, .FrameIndex = numeric_cast<uint32_t>(i), .Form = mesh.Kind == SpriteMeshKind::Mesh ? "mesh" : mesh.Kind == SpriteMeshKind::Empty ? "empty" : "quad", .SelectionOrigin = string(SpriteMeshCandidateSourceName(mesh.Source)), .QuadReason = string(SpriteMeshQuadReasonName(mesh.QuadReason)), .TriangleCount = numeric_cast<uint32_t>(mesh.Data.Indices.size() / 3), .VertexCount = numeric_cast<uint32_t>(mesh.Data.Vertices.size()), .SourceComponentCount = mesh.SourceComponentCount, .DilatedComponentCount = mesh.DilatedComponentCount, .Padding = frame_padding, .SimplifyTolerance = mesh.SimplifyTolerance, .ActualDilation = mesh.ActualDilation, .SourceFramePixels = numeric_cast<uint64_t>(shot.Width) * shot.Height, .BakedCanvasPixels = numeric_cast<uint64_t>(bake_shot->Width) * bake_shot->Height, .VisiblePixels = mesh.VisiblePixels, .SubmittedGeometryDoubleArea = mesh.DoubleArea, .SelectionScore = mesh.SelectionScore, .BestRejectedCandidate = std::move(best_rejected_candidate), }); } writer.Write<int16_t>(numeric_cast<int16_t>(frame_offset.x)); writer.Write<int16_t>(numeric_cast<int16_t>(frame_offset.y)); writer.Write<uint16_t>(bake_shot->Width); writer.Write<uint16_t>(bake_shot->Height); writer.Write<int16_t>(bake_shot->NextX); writer.Write<int16_t>(bake_shot->NextY); frame_info.Offset = frame_offset; frame_info.Size = cropped_size; frame_info.NextOffset = {bake_shot->NextX, bake_shot->NextY}; if (!bake_shot->Data.empty()) { writer.WriteBytes({bake_shot->Data.data(), bake_shot->Data.size()}); } writer.Write<uint8_t>(static_cast<uint8_t>(mesh.Kind)); if (mesh.Kind == SpriteMeshKind::Mesh) { writer.Write<uint16_t>(numeric_cast<uint16_t>(mesh.Data.Vertices.size())); writer.Write<uint32_t>(numeric_cast<uint32_t>(mesh.Data.Indices.size())); writer.Write<uint16_t>(numeric_cast<uint16_t>(mesh.Data.SourceSize.width)); writer.Write<uint16_t>(numeric_cast<uint16_t>(mesh.Data.SourceSize.height)); writer.Write<int32_t>(mesh.Data.SourceOffset.x); writer.Write<int32_t>(mesh.Data.SourceOffset.y); for (ipos32 vertex : mesh.Data.Vertices) { writer.Write<uint16_t>(numeric_cast<uint16_t>(vertex.x)); writer.Write<uint16_t>(numeric_cast<uint16_t>(vertex.y)); } for (uint16_t index : mesh.Data.Indices) { writer.Write<uint16_t>(index); } } } else { stats.SharedFrameReferences++; writer.Write<uint16_t>(shot.SharedIndex); FO_VERIFY_AND_THROW(shot.SharedIndex < i, "Shared sprite frame points outside previously baked frames", fname, dir, i, shot.SharedIndex); frame_info = direction_info.Frames[shot.SharedIndex]; frame_info.SharedFrameIndex = shot.SharedIndex; } } } writer.Write<uint8_t>(SPRITE_RESOURCE_MAGIC); _context->WriteData(output_path, data); if (!report_enabled) { return sprite_info_entry; } AddBakingReportCounter("collections", stats.Collections); AddBakingReportCounter("directions", stats.Directions); AddBakingReportCounter("frameSlots", stats.FrameSlots); AddBakingReportCounter("uniqueFrames", stats.UniqueFrames); AddBakingReportCounter("sharedFrameReferences", stats.SharedFrameReferences); AddBakingReportHistogramValue("sourceFormats", strex(fname).get_file_extension()); for (const SpriteMeshBakingFrameReport& frame_report : frame_reports) { RecordSpriteMeshBakingFrame(frame_report); } if (stats.SharedFrameReferences != 0) { RecordSharedSpriteMeshBakingFrames(stats.SharedFrameReferences); } return sprite_info_entry; } static auto PadSpriteFrame(const ImageBaker::FrameShot& shot, int32_t padding) -> ImageBaker::FrameShot { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(padding > 0, "Sprite frame padding must be positive", padding); FO_VERIFY_AND_THROW(shot.Data.size() == numeric_cast<size_t>(shot.Width) * shot.Height * 4, "Animation frame RGBA payload size does not match frame dimensions", shot.Data.size(), shot.Width, shot.Height); int32_t padded_width = numeric_cast<int32_t>(shot.Width) + padding * 2; int32_t padded_height = numeric_cast<int32_t>(shot.Height) + padding * 2; FO_VERIFY_AND_THROW(padded_width <= std::numeric_limits<uint16_t>::max() && padded_height <= std::numeric_limits<uint16_t>::max(), "Sprite frame padding exceeds the serialized dimension range", shot.Width, shot.Height, padding); ImageBaker::FrameShot result; result.Width = numeric_cast<uint16_t>(padded_width); result.Height = numeric_cast<uint16_t>(padded_height); result.NextX = shot.NextX; result.NextY = shot.NextY; result.Data.resize(numeric_cast<size_t>(result.Width) * result.Height * 4); size_t source_row_size = numeric_cast<size_t>(shot.Width) * 4; size_t destination_row_size = numeric_cast<size_t>(result.Width) * 4; size_t destination_x_offset = numeric_cast<size_t>(padding) * 4; for (uint16_t y = 0; y < shot.Height; y++) { size_t source_offset = numeric_cast<size_t>(y) * source_row_size; size_t destination_offset = numeric_cast<size_t>(numeric_cast<int32_t>(y) + padding) * destination_row_size + destination_x_offset; MemCopy(result.Data.data() + destination_offset, shot.Data.data() + source_offset, source_row_size); } return result; } static auto ResolveSpriteFramePadding(const ImageBaker::FrameShot& shot, const BakedSpriteMesh& mesh, int32_t build_padding) -> int32_t { FO_STACK_TRACE_ENTRY(); if (mesh.Kind != SpriteMeshKind::Mesh || mesh.Data.Indices.empty()) { return 0; } int32_t required_padding = 0; for (ipos32 vertex : mesh.Data.Vertices) { int32_t relative_x = vertex.x - build_padding; int32_t relative_y = vertex.y - build_padding; required_padding = std::max({required_padding, -relative_x, relative_x - shot.Width, -relative_y, relative_y - shot.Height}); } FO_VERIFY_AND_THROW(required_padding <= SPRITE_MESH_MAXIMUM_PADDING, "Resolved sprite mesh padding exceeds the internal maximum", required_padding, SPRITE_MESH_MAXIMUM_PADDING); return required_padding; } static void TranslateSpriteMesh(BakedSpriteMesh& mesh, int32_t offset, const ImageBaker::FrameShot& shot) { FO_STACK_TRACE_ENTRY(); if (mesh.Kind != SpriteMeshKind::Mesh || offset == 0) { return; } for (ipos32 vertex : mesh.Data.Vertices) { int64_t translated_x = numeric_cast<int64_t>(vertex.x) + offset; int64_t translated_y = numeric_cast<int64_t>(vertex.y) + offset; FO_VERIFY_AND_THROW(translated_x >= 0 && translated_x <= shot.Width && translated_y >= 0 && translated_y <= shot.Height, "Translated sprite mesh vertex lies outside the baked frame", vertex, shot.Width, shot.Height, offset); } for (ipos32& vertex : mesh.Data.Vertices) { vertex.x = numeric_cast<int32_t>(numeric_cast<int64_t>(vertex.x) + offset); vertex.y = numeric_cast<int32_t>(numeric_cast<int64_t>(vertex.y) + offset); } } static auto CropSpriteFrameToMeshBounds(const ImageBaker::FrameShot& shot, const ImageBaker::FrameShot& source_shot, int32_t padding, BakedSpriteMesh& mesh) -> optional<ImageBaker::FrameShot> { FO_STACK_TRACE_ENTRY(); FO_VERIFY_AND_THROW(mesh.Kind == SpriteMeshKind::Mesh && !mesh.Data.Vertices.empty() && !mesh.Data.Indices.empty(), "Sprite frame cropping requires a non-empty mesh"); FO_VERIFY_AND_THROW(shot.Data.size() == numeric_cast<size_t>(shot.Width) * shot.Height * 4, "Animation frame RGBA payload size does not match frame dimensions", shot.Data.size(), shot.Width, shot.Height); ipos32 minimum_vertex {shot.Width, shot.Height}; ipos32 maximum_vertex {}; for (ipos32 vertex : mesh.Data.Vertices) { FO_VERIFY_AND_THROW(vertex.x >= 0 && vertex.x <= shot.Width && vertex.y >= 0 && vertex.y <= shot.Height, "Sprite mesh vertex lies outside the frame selected for cropping", vertex, shot.Width, shot.Height); minimum_vertex.x = std::min(minimum_vertex.x, vertex.x); minimum_vertex.y = std::min(minimum_vertex.y, vertex.y); maximum_vertex.x = std::max(maximum_vertex.x, vertex.x); maximum_vertex.y = std::max(maximum_vertex.y, vertex.y); } isize32 cropped_size = {maximum_vertex.x - minimum_vertex.x, maximum_vertex.y - minimum_vertex.y}; FO_VERIFY_AND_THROW(cropped_size.width > 0 && cropped_size.height > 0, "Sprite mesh bounds must have positive dimensions", minimum_vertex, maximum_vertex); mesh.Data.SourceSize = {source_shot.Width, source_shot.Height}; mesh.Data.SourceOffset = {minimum_vertex.x - padding, minimum_vertex.y - padding}; for (ipos32& vertex : mesh.Data.Vertices) { vertex -= minimum_vertex; } if (minimum_vertex == ipos32 {} && cropped_size == isize32 {shot.Width, shot.Height}) { return std::nullopt; } ImageBaker::FrameShot result; result.Width = numeric_cast<uint16_t>(cropped_size.width); result.Height = numeric_cast<uint16_t>(cropped_size.height); result.NextX = shot.NextX; result.NextY = shot.NextY; result.Data.resize(numeric_cast<size_t>(result.Width) * result.Height * 4); size_t source_row_size = numeric_cast<size_t>(shot.Width) * 4; size_t cropped_row_size = numeric_cast<size_t>(result.Width) * 4; size_t source_x_offset = numeric_cast<size_t>(minimum_vertex.x) * 4; for (uint16_t y = 0; y < result.Height; y++) { size_t source_y = numeric_cast<size_t>(numeric_cast<int32_t>(y) + minimum_vertex.y); size_t source_offset = source_y * source_row_size + source_x_offset; size_t destination_offset = numeric_cast<size_t>(y) * cropped_row_size; MemCopy(result.Data.data() + destination_offset, shot.Data.data() + source_offset, cropped_row_size); } return optional<ImageBaker::FrameShot> {std::move(result)}; } auto ImageBaker::LoadAny(string_view fname_with_opt, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); string ext = strex(fname_with_opt).get_file_extension(); string dir = strex(fname_with_opt).extract_dir(); string_view name = strvex(fname_with_opt).extract_file_name().erase_file_extension().substring_until('$'); string fname = strex("{}/{}.{}", dir, name, ext); string_view opt = strvex(fname_with_opt).extract_file_name().erase_file_extension().substring_after('$'); auto file = files.FindFileByPath(fname); if (!file) { throw ImageBakerException("Image file not found", fname, fname_with_opt, dir, name, ext); } if (auto it = _fileLoaders.find(ext); it != _fileLoaders.end()) { return it->second(fname, opt, file.GetReader(), files); } else { throw ImageBakerException("Invalid image file extension", fname); } } auto ImageBaker::LoadFofrm(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); FrameCollection collection; ConfigFile fofrm(reader.GetStr()); int32_t frm_fps = fofrm.GetAsInt("", "fps", 10); frm_fps = fofrm.GetAsInt("", "Fps", frm_fps); int32_t frm_count = fofrm.GetAsInt("", "count", 1); frm_count = fofrm.GetAsInt("", "Count", frm_count); FO_VERIFY_AND_THROW(frm_count > 0, "Frame count must be positive", frm_count); int32_t ox = fofrm.GetAsInt("", "offs_x", 0); ox = fofrm.GetAsInt("", "OffsetX", ox); int32_t oy = fofrm.GetAsInt("", "offs_y", 0); oy = fofrm.GetAsInt("", "OffsetY", oy); collection.EffectName = fofrm.GetAsStr("", "effect"); collection.EffectName = fofrm.GetAsStr("", "Effect", collection.EffectName); for (int32_t dir = 0; dir < GameSettings::MAP_DIR_COUNT; dir++) { vector<tuple<FrameCollection, int32_t, int32_t>> sub_collections; sub_collections.reserve(10); string dir_str = strex("dir_{}", dir); if (!fofrm.HasSection(dir_str)) { dir_str = strex("Dir_{}", dir); if (!fofrm.HasSection(dir_str)) { dir_str = ""; } } if (dir_str.empty()) { if (dir == 1) { break; } if (dir > 1) { throw ImageBakerException("FOFRM file invalid apps", fname); } } else { ox = fofrm.GetAsInt(dir_str, "offs_x", ox); ox = fofrm.GetAsInt(dir_str, "OffsetX", ox); oy = fofrm.GetAsInt(dir_str, "offs_y", oy); oy = fofrm.GetAsInt(dir_str, "OffsetY", oy); } string frm_dir = strex(fname).extract_dir(); int32_t frames = 0; bool no_info = false; bool load_fail = false; for (int32_t frm = 0; frm < frm_count; frm++) { string_view frm_name = fofrm.GetAsStr(dir_str, strex("frm_{}", frm)); if (frm_name.empty()) { frm_name = fofrm.GetAsStr(dir_str, strex("Frm_{}", frm), frm_name); } if (frm_name.empty() && frm == 0) { frm_name = fofrm.GetAsStr(dir_str, "frm"); if (frm_name.empty()) { frm_name = fofrm.GetAsStr(dir_str, "Frm"); } } if (frm_name.empty()) { no_info = true; break; } auto sub_collection = LoadAny(strex("{}/{}", frm_dir, frm_name), files); frames += sub_collection.SequenceSize; int32_t next_x = fofrm.GetAsInt(dir_str, strex("next_x_{}", frm), 0); next_x = fofrm.GetAsInt(dir_str, strex("NextX_{}", frm), next_x); int32_t next_y = fofrm.GetAsInt(dir_str, strex("next_y_{}", frm), 0); next_y = fofrm.GetAsInt(dir_str, strex("NextY_{}", frm), next_y); sub_collections.emplace_back(std::move(sub_collection), next_x, next_y); } // No frames found or error if (no_info || load_fail || sub_collections.empty() || (dir > 0 && frames != collection.SequenceSize)) { if (no_info && dir == 1) { break; } throw ImageBakerException("FOFRM file invalid data", fname); } // Allocate animation storage if (dir == 0) { collection.SequenceSize = numeric_cast<uint16_t>(frames); collection.AnimTicks = numeric_cast<uint16_t>(frm_fps != 0 ? 1000 * frm_count / frm_fps : 0); } else { collection.HaveDirs = true; } auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; sequence.OffsX = numeric_cast<int16_t>(ox); sequence.OffsY = numeric_cast<int16_t>(oy); sequence.Frames.resize(collection.SequenceSize); // Merge many animations in one int32_t frm = 0; for (auto&& [sub_collection, next_x, next_y] : sub_collections) { for (int32_t i = 0; i < sub_collection.SequenceSize; i++, frm++) { auto& shot = sequence.Frames[frm]; if (sub_collection.Main.Frames[i].Shared) { throw ImageBakerException("FOFRM file invalid data (shared index)", fname); } shot.Width = sub_collection.Main.Frames[i].Width; shot.Height = sub_collection.Main.Frames[i].Height; shot.Data = sub_collection.Main.Frames[i].Data; shot.NextX = numeric_cast<int16_t>(sub_collection.Main.Frames[i].NextX + next_x); shot.NextY = numeric_cast<int16_t>(sub_collection.Main.Frames[i].NextY + next_y); } } } return collection; } auto ImageBaker::LoadFrm(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); reader.SetCurPos(0x4); uint16_t frm_fps = reader.GetBEUInt16(); reader.SetCurPos(0x8); uint16_t frm_count = reader.GetBEUInt16(); FO_VERIFY_AND_THROW(frm_count > 0, "Frame count must be positive", frm_count); FrameCollection collection; collection.SequenceSize = frm_count; collection.AnimTicks = frm_fps != 0 ? 1000 / frm_fps * frm_count : 0; if (strvex(fname).starts_with("art/critters/")) { collection.NewName = strex(fname).lower(); } // Animate pixels // 0x00 - None // 0x01 - Slime, 229 - 232, 4 frames // 0x02 - Monitors, 233 - 237, 5 frames // 0x04 - FireSlow, 238 - 242, 5 frames // 0x08 - FireFast, 243 - 247, 5 frames // 0x10 - Shoreline, 248 - 253, 6 frames // 0x20 - BlinkingRed, 254, 15 frames uint32_t anim_pix_type = 0; const uint8_t blinking_red_vals[10] = {254, 210, 165, 120, 75, 45, 90, 135, 180, 225}; for (int32_t dir = 0; dir < GameSettings::MAP_DIR_COUNT; dir++) { auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; sequence.Frames.resize(collection.SequenceSize); int32_t dir_frm = dir; if constexpr (GameSettings::SQUARE_GEOMETRY) { if (dir >= 3) { dir_frm--; } if (dir >= 7) { dir_frm--; } } reader.SetCurPos(0xA + dir_frm * 2); sequence.OffsX = reader.GetBEInt16(); reader.SetCurPos(0x16 + dir_frm * 2); sequence.OffsY = reader.GetBEInt16(); reader.SetCurPos(0x22 + dir_frm * 4); uint32_t offset = 0x3E + reader.GetBEUInt32(); if (offset == 0x3E && dir_frm != 0) { if (dir > 1) { throw ImageBakerException("FRM file truncated", fname); } break; } if (dir == 1) { collection.HaveDirs = true; } // Make palette span<ucolor> palette = bytes_to_objects<ucolor>(span {FoPalette}); ucolor custom_palette[256]; File palette_file = files.FindFileByPath(strex("{}.pal", strvex(fname).erase_file_extension())); if (palette_file) { auto palette_reader = palette_file.GetReader(); for (auto& color : custom_palette) { uint8_t r = palette_reader.GetUInt8() * 4; uint8_t g = palette_reader.GetUInt8() * 4; uint8_t b = palette_reader.GetUInt8() * 4; color = ucolor {r, g, b, 0xFF}; } custom_palette[0] = ucolor {0}; palette = span<ucolor> {custom_palette}; } for (uint16_t frm = 0; frm < frm_count; frm++) { auto& shot = sequence.Frames[frm]; reader.SetCurPos(offset); uint16_t w = reader.GetBEUInt16(); uint16_t h = reader.GetBEUInt16(); shot.Width = w; shot.Height = h; reader.GoForward(4); // Frame size shot.NextX = reader.GetBEInt16(); shot.NextY = reader.GetBEInt16(); // Allocate data shot.Data.resize(numeric_cast<size_t>(w) * h * 4); span<ucolor> pixels = bytes_to_objects<ucolor>(span {shot.Data}); reader.SetCurPos(offset + 12); if (anim_pix_type == 0) { for (size_t i = 0, j = numeric_cast<size_t>(w * h); i < j; i++) { pixels[i] = palette[reader.GetUInt8()]; } } else { for (size_t i = 0, j = numeric_cast<size_t>(w * h); i < j; i++) { uint8_t index = reader.GetUInt8(); if (index >= 229 && index < 255) { if (index >= 229 && index <= 232) { index -= frm % 4; if (index < 229) { index += 4; } } else if (index >= 233 && index <= 237) { index -= frm % 5; if (index < 233) { index += 5; } } else if (index >= 238 && index <= 242) { index -= frm % 5; if (index < 238) { index += 5; } } else if (index >= 243 && index <= 247) { index -= frm % 5; if (index < 243) { index += 5; } } else if (index >= 248 && index <= 253) { index -= frm % 6; if (index < 248) { index += 6; } } else { pixels[i] = ucolor {blinking_red_vals[frm % 10], 0, 0}; continue; } } pixels[i] = palette[index]; } } // Check for animate pixels if (anim_pix_type == 0 && frm == 0 && dir == 0 && frm_count == 1 && palette.data() != custom_palette) { reader.SetCurPos(offset + 12); for (int32_t i = 0; i < w * h; i++) { uint8_t index = reader.GetUInt8(); if (index < 229 || index == 255) { continue; } if (index >= 229 && index <= 232) { anim_pix_type |= 0x01; } else if (index >= 233 && index <= 237) { anim_pix_type |= 0x02; } else if (index >= 238 && index <= 242) { anim_pix_type |= 0x04; } else if (index >= 243 && index <= 247) { anim_pix_type |= 0x08; } else if (index >= 248 && index <= 253) { anim_pix_type |= 0x10; } else { anim_pix_type |= 0x20; } } if ((anim_pix_type & 0x01) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x04) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x10) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x08) != 0) { collection.AnimTicks = 142; } if ((anim_pix_type & 0x02) != 0) { collection.AnimTicks = 100; } if ((anim_pix_type & 0x20) != 0) { collection.AnimTicks = 100; } if (anim_pix_type != 0) { int32_t divs[4] {1, 1, 1, 1}; if ((anim_pix_type & 0x01) != 0) { divs[0] = 4; } if ((anim_pix_type & 0x02) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x04) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x08) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x10) != 0) { divs[2] = 6; } if ((anim_pix_type & 0x20) != 0) { divs[3] = 10; } frm_count = 4; for (int32_t i = 0; i < 4; i++) { if (frm_count % divs[i] == 0) { continue; } frm_count++; i = -1; } collection.AnimTicks *= frm_count; collection.SequenceSize = frm_count; sequence.Frames.resize(collection.SequenceSize); } } if (anim_pix_type == 0) { offset += w * h + 12; } } } return collection; } auto ImageBaker::LoadFrX(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); File dir_file; // Load from frm reader.SetCurPos(0x4); uint16_t frm_fps = reader.GetBEUInt16(); reader.SetCurPos(0x8); uint16_t frm_count = reader.GetBEUInt16(); FO_VERIFY_AND_THROW(frm_count > 0, "Frame count must be positive", frm_count); FrameCollection collection; collection.SequenceSize = frm_count; collection.AnimTicks = frm_fps != 0 ? 1000 / frm_fps * frm_count : 0; if (strvex(fname).starts_with("art/critters/")) { collection.NewName = strex("{}.{}", strex(fname).erase_file_extension().lower(), "fofrm"); } else { collection.NewName = strex("{}.{}", strvex(fname).erase_file_extension(), "frm"); } // Animate pixels // 0x00 - None // 0x01 - Slime, 229 - 232, 4 frames // 0x02 - Monitors, 233 - 237, 5 frames // 0x04 - FireSlow, 238 - 242, 5 frames // 0x08 - FireFast, 243 - 247, 5 frames // 0x10 - Shoreline, 248 - 253, 6 frames // 0x20 - BlinkingRed, 254, 15 frames uint32_t anim_pix_type = 0; const uint8_t blinking_red_vals[10] = {254, 210, 165, 120, 75, 45, 90, 135, 180, 225}; for (int32_t dir = 0; dir < GameSettings::MAP_DIR_COUNT; dir++) { auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; sequence.Frames.resize(collection.SequenceSize); int32_t dir_frm = dir; if constexpr (GameSettings::SQUARE_GEOMETRY) { if (dir >= 3) { dir_frm--; } if (dir >= 7) { dir_frm--; } } if (dir_frm != 0) { string next_fname {fname.substr(0, fname.size() - 1)}; next_fname += numeric_cast<char>('0' + dir_frm); dir_file = files.FindFileByPath(next_fname); if (dir_file) { reader = dir_file.GetReader(); } else { if (dir > 1) { throw ImageBakerException("FRX file not found", next_fname); } break; } } reader.SetCurPos(0xA + dir_frm * 2); sequence.OffsX = reader.GetBEInt16(); reader.SetCurPos(0x16 + dir_frm * 2); sequence.OffsY = reader.GetBEInt16(); reader.SetCurPos(0x22 + dir_frm * 4); uint32_t offset = 0x3E + reader.GetBEUInt32(); if (dir == 1) { collection.HaveDirs = true; } // Make palette span<ucolor> palette = bytes_to_objects<ucolor>(span {FoPalette}); ucolor custom_palette[256]; File palette_file = files.FindFileByPath(strex("{}.pal", strvex(fname).erase_file_extension())); if (palette_file) { auto palette_reader = palette_file.GetReader(); for (auto& color : custom_palette) { uint8_t r = palette_reader.GetUInt8() * 4; uint8_t g = palette_reader.GetUInt8() * 4; uint8_t b = palette_reader.GetUInt8() * 4; color = ucolor {r, g, b}; } custom_palette[0] = ucolor {0}; palette = span<ucolor> {custom_palette}; } for (uint16_t frm = 0; frm < frm_count; frm++) { auto& shot = sequence.Frames[frm]; reader.SetCurPos(offset); uint16_t w = reader.GetBEUInt16(); uint16_t h = reader.GetBEUInt16(); shot.Width = w; shot.Height = h; reader.GoForward(4); // Frame size shot.NextX = reader.GetBEInt16(); shot.NextY = reader.GetBEInt16(); // Allocate data shot.Data.resize(numeric_cast<size_t>(w) * h * 4); span<ucolor> pixels = bytes_to_objects<ucolor>(span {shot.Data}); reader.SetCurPos(offset + 12); if (anim_pix_type == 0) { for (size_t i = 0, j = numeric_cast<size_t>(w * h); i < j; i++) { pixels[i] = palette[reader.GetUInt8()]; } } else { for (size_t i = 0, j = numeric_cast<size_t>(w * h); i < j; i++) { uint8_t index = reader.GetUInt8(); if (index >= 229 && index < 255) { if (index >= 229 && index <= 232) { index -= frm % 4; if (index < 229) { index += 4; } } else if (index >= 233 && index <= 237) { index -= frm % 5; if (index < 233) { index += 5; } } else if (index >= 238 && index <= 242) { index -= frm % 5; if (index < 238) { index += 5; } } else if (index >= 243 && index <= 247) { index -= frm % 5; if (index < 243) { index += 5; } } else if (index >= 248 && index <= 253) { index -= frm % 6; if (index < 248) { index += 6; } } else { pixels[i] = ucolor {blinking_red_vals[frm % 10], 0, 0}; continue; } } pixels[i] = palette[index]; } } // Check for animate pixels if (anim_pix_type == 0 && frm == 0 && dir == 0 && frm_count == 1 && palette.data() != custom_palette) { reader.SetCurPos(offset + 12); for (int32_t i = 0; i < w * h; i++) { uint8_t index = reader.GetUInt8(); if (index < 229 || index == 255) { continue; } if (index >= 229 && index <= 232) { anim_pix_type |= 0x01; } else if (index >= 233 && index <= 237) { anim_pix_type |= 0x02; } else if (index >= 238 && index <= 242) { anim_pix_type |= 0x04; } else if (index >= 243 && index <= 247) { anim_pix_type |= 0x08; } else if (index >= 248 && index <= 253) { anim_pix_type |= 0x10; } else { anim_pix_type |= 0x20; } } if ((anim_pix_type & 0x01) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x04) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x10) != 0) { collection.AnimTicks = 200; } if ((anim_pix_type & 0x08) != 0) { collection.AnimTicks = 142; } if ((anim_pix_type & 0x02) != 0) { collection.AnimTicks = 100; } if ((anim_pix_type & 0x20) != 0) { collection.AnimTicks = 100; } if (anim_pix_type != 0) { int32_t divs[4]; divs[0] = 1; divs[1] = 1; divs[2] = 1; divs[3] = 1; if ((anim_pix_type & 0x01) != 0) { divs[0] = 4; } if ((anim_pix_type & 0x02) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x04) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x08) != 0) { divs[1] = 5; } if ((anim_pix_type & 0x10) != 0) { divs[2] = 6; } if ((anim_pix_type & 0x20) != 0) { divs[3] = 10; } frm_count = 4; for (int32_t i = 0; i < 4; i++) { if (frm_count % divs[i] == 0) { continue; } frm_count++; i = -1; } collection.AnimTicks *= frm_count; collection.SequenceSize = frm_count; sequence.Frames.resize(collection.SequenceSize); } } if (anim_pix_type == 0) { offset += w * h + 12; } } } return collection; } auto ImageBaker::LoadRix(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(fname); ignore_unused(opt); ignore_unused(files); reader.SetCurPos(0x4); uint16_t w = 0; reader.ReadObject(w); uint16_t h = 0; reader.ReadObject(h); reader.SetCurPos(0xA); const_span<uint8_t> palette = reader.GetCurDataSpan(256 * 3); vector<uint8_t> data(numeric_cast<size_t>(w) * h * 4); span<ucolor> pixels = bytes_to_objects<ucolor>(span {data}); reader.SetCurPos(0xA + 256 * 3); for (int32_t i = 0; i < w * h; i++) { int32_t index = numeric_cast<int32_t>(reader.GetUInt8()) * 3; auto r = numeric_cast<uint8_t>(palette[index + 2] * 4); auto g = numeric_cast<uint8_t>(palette[index + 1] * 4); auto b = numeric_cast<uint8_t>(palette[index + 0] * 4); pixels[i] = ucolor {r, g, b}; } FrameCollection collection; collection.SequenceSize = 1; collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); collection.Main.Frames[0].Width = w; collection.Main.Frames[0].Height = h; collection.Main.Frames[0].Data = std::move(data); return collection; } auto ImageBaker::LoadArt(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(files); int32_t palette_index = 0; // 0..3 bool transparent = false; bool mirror_hor = false; bool mirror_ver = false; int32_t frm_from = 0; int32_t frm_to = 100000; for (size_t i = 0; i < opt.length(); i++) { switch (opt[i]) { case '0': palette_index = 0; break; case '1': palette_index = 1; break; case '2': palette_index = 2; break; case '3': palette_index = 3; break; case 'T': case 't': transparent = true; break; case 'H': case 'h': mirror_hor = true; break; case 'V': case 'v': mirror_ver = true; break; case 'F': case 'f': { // name$1vf5-7.art size_t frame_spec_len = 0; while (i + 1 + frame_spec_len < opt.length()) { char ch = opt[i + 1 + frame_spec_len]; if (!((ch >= '0' && ch <= '9') || ch == '-')) { break; } frame_spec_len++; } if (frame_spec_len != 0) { string frame_spec {opt.substr(i + 1, frame_spec_len)}; istringstream idelim(frame_spec); char ch = 0; if (frame_spec.find('-') != string::npos) { idelim >> frm_from >> ch >> frm_to; } else { idelim >> frm_from; frm_to = frm_from; } i += frame_spec_len; } } break; default: break; } } struct ArtHeader { int32_t Flags {}; // 0x00000001 = Static - no rotation, contains frames only for south direction. // 0x00000002 = Critter - uses delta attribute, while playing walking animation. // 0x00000004 = Font - X offset is equal to number of pixels to advance horizontally for next // character. 0x00000008 = Facade - requires fackwalk file. 0x00000010 = Unknown - used in eye candy, // for example, DIVINATION.art. int32_t FrameRate {}; int32_t RotationCount {}; uint32_t PaletteList[4] {}; int32_t ActionFrame {}; int32_t FrameCount {}; int32_t InfoList[8] {}; int32_t SizeList[8] {}; int32_t DataList[8] {}; }; ArtHeader header; struct ArtFrameInfo { int32_t FrameWidth {}; int32_t FrameHeight {}; int32_t FrameSize {}; int32_t OffsetX {}; int32_t OffsetY {}; int32_t DeltaX {}; int32_t DeltaY {}; } frame_info; using ArtPalette = uint32_t[256]; ArtPalette palette[4]; reader.ReadObject(header); if ((header.Flags & 0x00000001) != 0) { header.RotationCount = 1; } // Load palettes int32_t palette_count = 0; for (int32_t i = 0; i < 4; i++) { if (header.PaletteList[i] != 0) { reader.ReadObjectArray<uint32_t>(palette[i]); palette_count++; } } if (palette_index >= palette_count) { palette_index = 0; } int32_t frm_fps = header.FrameRate; int32_t frm_count = header.FrameCount; FO_VERIFY_AND_THROW(frm_count > 0, "Frame count must be positive", frm_count); if (frm_from >= frm_count) { frm_from = frm_count - 1; } if (frm_to >= frm_count) { frm_to = frm_count - 1; } int32_t frm_count_target = std::max(frm_from, frm_to) - std::min(frm_from, frm_to) + 1; // Create animation FrameCollection collection; collection.SequenceSize = numeric_cast<uint16_t>(frm_count_target); collection.AnimTicks = numeric_cast<uint16_t>(frm_fps != 0 ? 1000 / frm_fps * frm_count_target : 0); collection.HaveDirs = header.RotationCount == 8; for (int32_t dir = 0; dir < GameSettings::MAP_DIR_COUNT; dir++) { auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; sequence.Frames.resize(collection.SequenceSize); int32_t dir_art = dir; if constexpr (GameSettings::HEXAGONAL_GEOMETRY) { switch (dir_art) { case 0: dir_art = 1; break; case 1: dir_art = 2; break; case 2: dir_art = 3; break; case 3: dir_art = 5; break; case 4: dir_art = 6; break; case 5: dir_art = 7; break; default: throw ImageBakerException("Invalid ART direction", fname); } } else { dir_art = (dir + 1) % 8; } // Read data int32_t frm_read = frm_from; int32_t frm_write = 0; while (true) { reader.SetCurPos(sizeof(ArtHeader) + sizeof(ArtPalette) * palette_count + sizeof(ArtFrameInfo) * dir_art * frm_count + sizeof(ArtFrameInfo) * frm_read); reader.ReadObject(frame_info); int32_t w = frame_info.FrameWidth; int32_t h = frame_info.FrameHeight; vector<uint8_t> data(numeric_cast<size_t>(w) * h * 4); span<uint32_t> pixels = bytes_to_objects<uint32_t>(span {data}); auto& shot = sequence.Frames[frm_write]; shot.Width = numeric_cast<uint16_t>(w); shot.Height = numeric_cast<uint16_t>(h); shot.NextX = numeric_cast<int16_t>((-frame_info.OffsetX + frame_info.FrameWidth / 2) * (mirror_hor ? -1 : 1)); shot.NextY = numeric_cast<int16_t>((-frame_info.OffsetY + frame_info.FrameHeight) * (mirror_ver ? -1 : 1)); uint32_t color = 0; auto art_get_color = [&color, &reader, &palette, &palette_index, &transparent]() { uint8_t index = reader.GetUInt8(); color = palette[palette_index][index]; swap_object_bytes(color, 0, 2); if (index == 0) { color = 0; } else if (transparent) { color |= std::max((color >> 16) & 0xFF, std::max((color >> 8) & 0xFF, color & 0xFF)) << 24; } else { color |= 0xFF000000; } }; int32_t pos = 0; int32_t x = 0; int32_t y = 0; bool mirror = mirror_hor || mirror_ver; auto art_write_color = [&color, &pos, &x, &y, &w, &h, &pixels, &mirror, &mirror_hor, &mirror_ver]() { if (mirror) { pixels[(mirror_ver ? h - y - 1 : y) * w + (mirror_hor ? w - x - 1 : x)] = color; x++; if (x >= w) { x = 0; y++; } } else { pixels[pos] = color; pos++; } }; if (w * h == frame_info.FrameSize) { for (int32_t i = 0; i < frame_info.FrameSize; i++) { art_get_color(); art_write_color(); } } else { for (int32_t i = 0; i < frame_info.FrameSize; i++) { uint8_t cmd = reader.GetUInt8(); if (cmd > 128) { cmd -= 128; i += cmd; for (; cmd > 0; cmd--) { art_get_color(); art_write_color(); } } else { art_get_color(); for (; cmd > 0; cmd--) { art_write_color(); } i++; } } } shot.Data = std::move(data); if (frm_read == frm_to) { break; } if (frm_to > frm_from) { frm_read++; } else { frm_read--; } frm_write++; } if (header.RotationCount != 8) { break; } } return collection; } auto ImageBaker::LoadSpr(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(files); FrameCollection collection; for (int32_t dir = 0; dir < GameSettings::MAP_DIR_COUNT; dir++) { reader.SetCurPos(0); int32_t dir_spr = dir; if constexpr (GameSettings::HEXAGONAL_GEOMETRY) { switch (dir_spr) { case 0: dir_spr = 2; break; case 1: dir_spr = 3; break; case 2: dir_spr = 4; break; case 3: dir_spr = 6; break; case 4: dir_spr = 7; break; case 5: dir_spr = 0; break; default: dir_spr = 0; break; } } else { dir_spr = (dir + 2) % 8; } // Color offsets // 0 - other // 1 - skin // 2 - hair // 3 - armor int32_t rgb_offs[4][3] = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}; // Format: fileName$[1,100,0,0][2,0,0,100]animName.spr auto first = opt.find_first_of('['); auto last = opt.find_last_of(']'); auto seq_name = last != string::npos ? opt.substr(last + 1) : opt; while (first != string::npos) { last = opt.find_first_of(']', first); if (last == string::npos) { break; } string entry = string(opt.substr(first + 1, last - first - 1)); for (char& ch : entry) { if (ch == ',') { ch = ' '; } } istringstream ientry(entry); // Parse numbers int32_t rgb[4]; if (ientry >> rgb[0] >> rgb[1] >> rgb[2] >> rgb[3]) { // To one part if (rgb[0] >= 0 && rgb[0] <= 3) { rgb_offs[rgb[0]][0] = rgb[1]; // R rgb_offs[rgb[0]][1] = rgb[2]; // G rgb_offs[rgb[0]][2] = rgb[3]; // B } // To all parts else { for (auto& rgb_off : rgb_offs) { rgb_off[0] = rgb[1]; // R rgb_off[1] = rgb[2]; // G rgb_off[2] = rgb[3]; // B } } } first = opt.find_first_of('[', last + 1); } // Read header char head[11]; reader.ReadObjectArray<char>(head); if (head[8] != 0 || strcmp(head, "<sprite>") != 0) { throw ImageBakerException("Invalid SPR header", fname); } float32_t dimension_left = numeric_cast<float32_t>(reader.GetUInt8()) * 6.7f; float32_t dimension_up = numeric_cast<float32_t>(reader.GetUInt8()) * 7.6f; ignore_unused(dimension_up); float32_t dimension_right = numeric_cast<float32_t>(reader.GetUInt8()) * 6.7f; int32_t center_x = reader.GetLEInt32(); int32_t center_y = reader.GetLEInt32(); reader.GoForward(2); // uint16 unknown1 sometimes it is 0, and sometimes it is 3 reader.GoForward(1); // CHAR unknown2 0x64, other values were not observed float32_t ta = (127.0f / 2.0f) * DEG_TO_RAD_FLOAT; // Tactics grid angle int32_t center_x_ex = iround<int32_t>((dimension_left * sinf(ta) + dimension_right * sinf(ta)) / 2.0f - dimension_left * sinf(ta)); int32_t center_y_ex = iround<int32_t>((dimension_left * cosf(ta) + dimension_right * cosf(ta)) / 2.0f); int32_t anim_index = 0; vector<int32_t> anim_frames; anim_frames.reserve(1000); // Find sequence bool seq_founded = false; int32_t seq_cnt = reader.GetLEInt32(); for (int32_t seq = 0; seq < seq_cnt; seq++) { // Find by name int32_t item_cnt = reader.GetLEInt32(); reader.GoForward(sizeof(int16_t) * item_cnt); reader.GoForward(sizeof(int32_t) * item_cnt); int32_t name_len = reader.GetLEInt32(); FO_VERIFY_AND_THROW(name_len >= 0, "ART sequence name length is negative", name_len); const_span<uint8_t> name_data = reader.GetCurDataSpan(numeric_cast<size_t>(name_len)); string name = string(!name_data.empty() ? make_ptr(name_data.data()).reinterpret_as<const char>().get() : "", name_data.size()); reader.GoForward(name_data.size()); uint16_t index = reader.GetLEUInt16(); if (seq_name.empty() || strvex(seq_name).compare_ignore_case(name)) { anim_index = index; // Read frame numbers reader.GoBack(sizeof(uint16_t) + name_len + sizeof(int32_t) + sizeof(int32_t) * item_cnt + sizeof(int16_t) * item_cnt); for (int32_t i = 0; i < item_cnt; i++) { int16_t val = reader.GetLEInt16(); if (val >= 0) { anim_frames.push_back(val); } else if (val == -43) { reader.GoForward(sizeof(int16_t) * 3); i += 3; } } // Animation founded, go forward seq_founded = true; break; } } if (!seq_founded) { throw ImageBakerException("Sequence for SPR not found", fname); } // Find animation reader.SetCurPos(0); for (int32_t i = 0; i <= anim_index; i++) { if (!reader.SeekFragment("<spranim>")) { throw ImageBakerException("Spranim for SPR not found", fname); } reader.GoForward(12); } int32_t file_offset = reader.GetLEInt32(); reader.GoForward(reader.GetLEInt32()); // Collection name int32_t frame_cnt = reader.GetLEInt32(); int32_t dir_cnt = reader.GetLEInt32(); vector<int32_t> bboxes; bboxes.resize(numeric_cast<size_t>(frame_cnt) * dir_cnt * 4); if (!bboxes.empty()) { reader.ReadObjectArray(span<int32_t> {bboxes.data(), bboxes.size()}); } // Fix dir if (dir_cnt != 8) { dir_spr = dir_spr * (dir_cnt * 100 / 8) / 100; } if (numeric_cast<int32_t>(dir_spr) >= dir_cnt) { dir_spr = 0; } // Check wrong frames for (size_t i = 0; i < anim_frames.size();) { if (anim_frames[i] >= frame_cnt) { anim_frames.erase(anim_frames.begin() + numeric_cast<ptrdiff_t>(i)); } else { i++; } } // Get images file reader.SetCurPos(file_offset); reader.GoForward(14); // <spranim_img>\0 uint8_t type = reader.GetUInt8(); reader.GoForward(1); // \0 size_t data_len = 0; size_t cur_pos = reader.GetCurPos(); if (reader.SeekFragment("<spranim_img>")) { data_len = reader.GetCurPos() - cur_pos; } else { data_len = reader.GetSize() - cur_pos; } reader.SetCurPos(cur_pos); bool packed = type == 0x32; vector<uint8_t> data; if (packed) { // Unpack with zlib uint32_t unpacked_len = reader.GetLEUInt32(); FO_VERIFY_AND_THROW(data_len != 0, "Packed SPR frame has zero data length"); const_span<uint8_t> spr_data = reader.GetCurDataSpan(data_len); auto unpacked_data = Compressor::Decompress(spr_data, unpacked_len / data_len + 1); if (unpacked_data.empty()) { throw ImageBakerException("Can't unpack SPR data", fname); } data = unpacked_data; } else { data.resize(data_len); const_span<uint8_t> spr_data = reader.GetCurDataSpan(data_len); if (!spr_data.empty()) { MemCopy(data.data(), spr_data.data(), spr_data.size()); } } auto fm_images = FileReader(data); // Read palette typedef ucolor Palette[256]; Palette palette[4]; for (auto& i : palette) { uint32_t palette_count = fm_images.GetLEUInt32(); if (palette_count <= 256) { fm_images.ReadObjectArray(span<ucolor> {i, numeric_cast<size_t>(palette_count)}); } } // Index data offsets vector<size_t> image_indices; image_indices.resize(numeric_cast<size_t>(frame_cnt) * dir_cnt * 4); for (size_t cur = 0; fm_images.GetCurPos() != fm_images.GetSize();) { uint8_t tag = fm_images.GetUInt8(); if (tag == 1) { // Valid index image_indices[cur] = fm_images.GetCurPos(); fm_images.GoForward(8 + 8 + 8 + 1); fm_images.GoForward(fm_images.GetLEUInt32()); cur++; } else if (tag == 0) { // Empty index cur++; } else { break; } } // Create animation if (anim_frames.empty()) { anim_frames.push_back(0); } if (dir == 0) { collection.SequenceSize = numeric_cast<uint16_t>(anim_frames.size()); collection.AnimTicks = numeric_cast<uint16_t>(1000 / 10 * anim_frames.size()); } if (dir == 1) { collection.HaveDirs = true; } auto& sequence = dir == 0 ? collection.Main : collection.Dirs[dir - 1]; sequence.Frames.resize(collection.SequenceSize); for (size_t anim_frame = 0; anim_frame < anim_frames.size(); anim_frame++) { auto& shot = sequence.Frames[anim_frame]; int32_t frm = anim_frames[anim_frame]; // Optimization, share frames bool founded = false; for (size_t i = 0, j = anim_frame; i < j; i++) { if (anim_frames[i] == frm) { shot.Shared = true; shot.SharedIndex = numeric_cast<uint16_t>(i); founded = true; break; } } if (founded) { continue; } // Compute whole image size int32_t whole_width = 0; int32_t whole_height = 0; for (int32_t part = 0; part < 4; part++) { int32_t frm_index = type == 0x32 ? frame_cnt * dir_cnt * part + dir_spr * frame_cnt + frm : (frm * dir_cnt + (dir_spr << 2)) + part; if (image_indices[frm_index] == 0) { continue; } fm_images.SetCurPos(image_indices[frm_index]); int32_t posx = fm_images.GetLEInt32(); int32_t posy = fm_images.GetLEInt32(); fm_images.GoForward(8); int32_t width = fm_images.GetLEInt32(); int32_t height = fm_images.GetLEInt32(); whole_width = std::max(width + posx, whole_width); whole_height = std::max(height + posy, whole_height); } if (whole_width == 0) { whole_width = 1; } if (whole_height == 0) { whole_height = 1; } // Allocate data vector<uint8_t> whole_data(numeric_cast<size_t>(whole_width) * whole_height * 4); span<ucolor> whole_pixels = bytes_to_objects<ucolor>(span {whole_data}); for (int32_t part = 0; part < 4; part++) { int32_t frm_index = type == 0x32 ? frame_cnt * dir_cnt * part + dir_spr * frame_cnt + frm : (frm * dir_cnt + (dir_spr << 2)) + part; if (image_indices[frm_index] == 0) { continue; } fm_images.SetCurPos(image_indices[frm_index]); int32_t posx = fm_images.GetLEInt32(); int32_t posy = fm_images.GetLEInt32(); char zar[8] = {0}; fm_images.ReadObjectArray<char>(zar); uint8_t subtype = zar[6]; int32_t width = fm_images.GetLEInt32(); int32_t height = fm_images.GetLEInt32(); ignore_unused(height); uint8_t palette_present = fm_images.GetUInt8(); uint32_t rle_size = fm_images.GetLEUInt32(); const_span<uint8_t> rle_data = fm_images.GetCurDataSpan(numeric_cast<size_t>(rle_size)); fm_images.GoForward(rle_size); uint8_t def_color = 0; if (zar[5] != 0 || strcmp(zar, "<zar>") != 0 || (subtype != 0x34 && subtype != 0x35) || palette_present == 1) { throw ImageBakerException("Invalid SPR file innded ZAR header", fname); } size_t pixel_pos = numeric_cast<size_t>(posy) * whole_width + posx; int32_t x = posx; int32_t y = posy; size_t rle_pos = 0; while (rle_pos < rle_data.size()) { int32_t control = rle_data[rle_pos]; rle_pos++; int32_t control_mode = control & 3; int32_t control_count = control >> 2; for (int32_t i = 0; i < control_count; i++) { ucolor col; switch (control_mode) { case 1: col = palette[part][rle_data[rle_pos + numeric_cast<size_t>(i)]]; set_object_byte(col, 3, 0xFF); break; case 2: col = palette[part][rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i)]]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i) + 1]); break; case 3: col = palette[part][def_color]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(i)]); break; default: break; } for (int32_t j = 0; j < 3; j++) { if (rgb_offs[part][j] != 0) { size_t color_byte_index = numeric_cast<size_t>(2 - j); int32_t val = numeric_cast<int32_t>(get_object_byte(col, color_byte_index)) + rgb_offs[part][j]; set_object_byte(col, color_byte_index, numeric_cast<uint8_t>(std::clamp(val, 0, 255))); } } swap_object_bytes(col, 0, 2); if (part == 0) { whole_pixels[pixel_pos] = col; } else if (col.comp.a >= 128) { whole_pixels[pixel_pos] = col; } pixel_pos++; if (++x >= width + posx) { x = posx; y++; pixel_pos = numeric_cast<size_t>(y) * whole_width + x; } } if (control_mode != 0) { rle_pos += numeric_cast<size_t>(control_count); if (control_mode == 2) { rle_pos += numeric_cast<size_t>(control_count); } } } } shot.Width = numeric_cast<uint16_t>(whole_width); shot.Height = numeric_cast<uint16_t>(whole_height); shot.NextX = numeric_cast<int16_t>(bboxes[frm * dir_cnt * 4 + dir_spr * 4 + 0] - center_x + center_x_ex + whole_width / 2); shot.NextY = numeric_cast<int16_t>(bboxes[frm * dir_cnt * 4 + dir_spr * 4 + 1] - center_y + center_y_ex + whole_height); shot.Data = std::move(whole_data); } if (dir_cnt == 1) { break; } } return collection; } auto ImageBaker::LoadZar(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); ignore_unused(files); // Read header char head[6]; reader.ReadObjectArray<char>(head); if (head[5] != 0 || strcmp(head, "<zar>") != 0) { throw ImageBakerException("Invalid ZAR header", fname); } uint8_t type = reader.GetUInt8(); reader.GoForward(1); // \0 int32_t width = reader.GetLEInt32(); int32_t height = reader.GetLEInt32(); uint8_t palette_present = reader.GetUInt8(); // Read palette uint32_t palette[256] = {}; uint8_t def_color = 0; if (palette_present != 0) { int32_t palette_count = reader.GetLEInt32(); if (palette_count > 256) { throw ImageBakerException("Invalid ZAR palette count", fname); } reader.ReadObjectArray(span<uint32_t> {palette, numeric_cast<size_t>(palette_count)}); if (type == 0x34) { def_color = reader.GetUInt8(); } } // Read image uint32_t rle_size = reader.GetLEUInt32(); const_span<uint8_t> rle_data = reader.GetCurDataSpan(numeric_cast<size_t>(rle_size)); reader.GoForward(rle_size); // Allocate data vector<uint8_t> data(numeric_cast<size_t>(width) * height * 4); span<uint32_t> pixels = bytes_to_objects<uint32_t>(span {data}); size_t pixel_pos = 0; size_t rle_pos = 0; // Decode while (rle_pos < rle_data.size()) { auto control = rle_data[rle_pos]; rle_pos++; int32_t control_mode = control & 3; int32_t control_count = control >> 2; for (int32_t i = 0; i < control_count; i++) { uint32_t col = 0; switch (control_mode) { case 1: col = palette[rle_data[rle_pos + numeric_cast<size_t>(i)]]; set_object_byte(col, 3, 0xFF); break; case 2: col = palette[rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i)]]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i) + 1]); break; case 3: col = palette[def_color]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(i)]); break; default: break; } pixels[pixel_pos] = col; pixel_pos++; } if (control_mode != 0) { rle_pos += numeric_cast<size_t>(control_count); if (control_mode == 2) { rle_pos += numeric_cast<size_t>(control_count); } } } FrameCollection collection; collection.SequenceSize = 1; collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); collection.Main.Frames[0].Width = numeric_cast<uint16_t>(width); collection.Main.Frames[0].Height = numeric_cast<uint16_t>(height); collection.Main.Frames[0].Data = std::move(data); return collection; } auto ImageBaker::LoadTil(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); ignore_unused(files); // Read header char head[7]; reader.ReadObjectArray<char>(head); if (head[6] != 0 || strcmp(head, "<tile>") != 0) { throw ImageBakerException("Invalid TIL file header", fname); } while (reader.GetUInt8() != 0) { // Nop } reader.GoForward(7 + 4); // Unknown int32_t width = reader.GetLEInt32(); ignore_unused(width); int32_t height = reader.GetLEInt32(); ignore_unused(height); if (!reader.SeekFragment("<tiledata>")) { throw ImageBakerException("Tiledata in TIL file not found", fname); } reader.GoForward(10 + 3); // Signature int32_t frames_count = reader.GetLEInt32(); FrameCollection collection; collection.SequenceSize = numeric_cast<uint16_t>(frames_count); collection.AnimTicks = numeric_cast<uint16_t>(1000 / 10 * frames_count); collection.Main.Frames.resize(collection.SequenceSize); for (int32_t frm = 0; frm < frames_count; frm++) { // Read header char zar_head[6]; reader.ReadObjectArray<char>(zar_head); if (zar_head[5] != 0 || strcmp(zar_head, "<zar>") != 0) { throw ImageBakerException("ZAR header in TIL file not found", fname); } uint8_t type = reader.GetUInt8(); reader.GoForward(1); // \0 int32_t zar_width = reader.GetLEInt32(); int32_t zar_height = reader.GetLEInt32(); uint8_t palette_present = reader.GetUInt8(); // Read palette ucolor palette[256] = {}; uint8_t def_color = 0; if (palette_present != 0) { uint32_t palette_count = reader.GetLEUInt32(); if (palette_count > 256) { throw ImageBakerException("TIL file invalid palettes", fname); } reader.ReadObjectArray(span<ucolor> {palette, numeric_cast<size_t>(palette_count)}); if (type == 0x34) { def_color = reader.GetUInt8(); } } // Read image uint32_t rle_size = reader.GetLEUInt32(); const_span<uint8_t> rle_data = reader.GetCurDataSpan(numeric_cast<size_t>(rle_size)); reader.GoForward(rle_size); // Allocate data vector<uint8_t> data(numeric_cast<size_t>(zar_width) * zar_height * 4); span<ucolor> pixels = bytes_to_objects<ucolor>(span {data}); size_t pixel_pos = 0; size_t rle_pos = 0; // Decode while (rle_pos < rle_data.size()) { int32_t control = rle_data[rle_pos]; rle_pos++; int32_t control_mode = control & 3; int32_t control_count = control >> 2; for (int32_t i = 0; i < control_count; i++) { ucolor col; switch (control_mode) { case 1: col = palette[rle_data[rle_pos + numeric_cast<size_t>(i)]]; set_object_byte(col, 3, 0xFF); break; case 2: col = palette[rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i)]]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(2) * numeric_cast<size_t>(i) + 1]); break; case 3: col = palette[def_color]; set_object_byte(col, 3, rle_data[rle_pos + numeric_cast<size_t>(i)]); break; default: break; } std::swap(col.comp.r, col.comp.b); pixels[pixel_pos] = col; pixel_pos++; } if (control_mode != 0) { rle_pos += numeric_cast<size_t>(control_count); if (control_mode == 2) { rle_pos += numeric_cast<size_t>(control_count); } } } auto& shot = collection.Main.Frames[frm]; shot.Width = numeric_cast<uint16_t>(zar_width); shot.Height = numeric_cast<uint16_t>(zar_height); shot.Data = std::move(data); } return collection; } auto ImageBaker::LoadMos(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(opt); ignore_unused(files); // Read signature char head[8]; reader.ReadObjectArray<char>(head); if (head[0] != 'M' || head[1] != 'O' || head[2] != 'S') { throw ImageBakerException("Invalid MOS file header", fname); } // Packed vector<uint8_t> unpacked_data; if (head[3] == 'C') { uint32_t unpacked_len = reader.GetLEUInt32(); size_t data_len = reader.GetSize() - 12; vector<uint8_t> packed_data = reader.GetData(); packed_data.resize(data_len); FO_VERIFY_AND_THROW(packed_data.size() >= 2, "Packed data is too small to hold a zlib header", packed_data.size()); packed_data[0] = 0x78; packed_data[1] = 0x9C; unpacked_data = Compressor::Decompress(packed_data, unpacked_len / reader.GetSize() + 1); if (unpacked_data.empty()) { throw ImageBakerException("Can't unpack MOS file", fname); } reader = FileReader(unpacked_data); reader.ReadObjectArray<char>(head); if (head[0] != 'M' || head[1] != 'O' || head[2] != 'S') { throw ImageBakerException("Invalid MOS file unpacked header", fname); } } // Read header int32_t width = reader.GetLEUInt16(); // Width (pixels) int32_t height = reader.GetLEUInt16(); // Height (pixels) int32_t col = reader.GetLEUInt16(); // Columns (blocks) int32_t row = reader.GetLEUInt16(); // Rows (blocks) uint32_t block_size = reader.GetLEUInt32(); // Block size (pixels) ignore_unused(block_size); uint32_t palette_offset = reader.GetLEUInt32(); // Offset (from start of file) to palettes uint32_t tiles_offset = palette_offset + col * row * 256 * 4; uint32_t data_offset = tiles_offset + col * row * 4; // Allocate data vector<uint8_t> data(numeric_cast<size_t>(width) * height * 4); span<uint32_t> pixels = bytes_to_objects<uint32_t>(span {data}); // Read image data uint32_t palette[256] = {0}; int32_t block = 0; for (int32_t y = 0; y < row; y++) { for (int32_t x = 0; x < col; x++) { // Get palette for current block reader.SetCurPos(palette_offset + block * 256 * 4); reader.ReadObjectArray<uint32_t>(palette); // Set initial position reader.SetCurPos(tiles_offset + block * 4); reader.SetCurPos(data_offset + reader.GetLEUInt32()); // Calculate current block size int32_t block_w = x == col - 1 ? width % 64 : 64; int32_t block_h = y == row - 1 ? height % 64 : 64; if (block_w == 0) { block_w = 64; } if (block_h == 0) { block_h = 64; } // Read data int32_t pos = y * 64 * width + x * 64; for (int32_t yy = 0; yy < block_h; yy++) { for (int32_t xx = 0; xx < block_w; xx++) { uint32_t color = palette[reader.GetUInt8()]; if (color == 0xFF00) { color = 0; // Green is transparent } else { color |= 0xFF000000; } pixels[pos] = color; pos++; } pos += width - block_w; } // Go to next block block++; } } FrameCollection collection; collection.SequenceSize = 1; collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); collection.Main.Frames[0].Width = numeric_cast<uint16_t>(width); collection.Main.Frames[0].Height = numeric_cast<uint16_t>(height); collection.Main.Frames[0].Data = std::move(data); return collection; } auto ImageBaker::LoadBam(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(files); // Format: fileName$5-6.bam string opt_str = string(opt); istringstream idelim(opt_str); int32_t need_cycle = 0; int32_t specific_frame = -1; idelim >> need_cycle; if (opt.find('-') != string::npos) { char ch = 0; idelim >> ch >> specific_frame; if (specific_frame < 0) { specific_frame = -1; } } // Read signature char head[8]; reader.ReadObjectArray<char>(head); if (head[0] != 'B' || head[1] != 'A' || head[2] != 'M') { throw ImageBakerException("Invalid BAM file header", fname); } // Packed vector<uint8_t> unpacked_data; if (head[3] == 'C') { uint32_t unpacked_len = reader.GetLEUInt32(); size_t data_len = reader.GetSize() - 12; vector<uint8_t> packed_data = reader.GetData(); packed_data.resize(data_len); FO_VERIFY_AND_THROW(packed_data.size() >= 2, "Packed data is too small to hold a zlib header", packed_data.size()); packed_data[0] = 0x78; packed_data[1] = 0x9C; unpacked_data = Compressor::Decompress(packed_data, unpacked_len / reader.GetSize() + 1); if (unpacked_data.empty()) { throw ImageBakerException("Cab't unpack BAM file", fname); } reader = FileReader(unpacked_data); reader.ReadObjectArray<char>(head); if (head[0] != 'B' || head[1] != 'A' || head[2] != 'M') { throw ImageBakerException("Invalid BAM file unpacked header", fname); } } // Read header int32_t frames_count = reader.GetLEUInt16(); int32_t cycles_count = reader.GetUInt8(); uint8_t compr_color = reader.GetUInt8(); uint32_t frames_offset = reader.GetLEUInt32(); uint32_t palette_offset = reader.GetLEUInt32(); uint32_t lookup_table_offset = reader.GetLEUInt32(); // Count whole frames if (need_cycle >= cycles_count) { need_cycle = 0; } reader.SetCurPos(frames_offset + frames_count * 12 + need_cycle * 4); int32_t cycle_frames = reader.GetLEUInt16(); int32_t table_index = reader.GetLEUInt16(); if (specific_frame != -1 && specific_frame >= numeric_cast<int32_t>(cycle_frames)) { specific_frame = 0; } // Create animation FrameCollection collection; collection.SequenceSize = numeric_cast<uint16_t>(specific_frame == -1 ? cycle_frames : 1); collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); // Palette ucolor palette[256] = {}; reader.SetCurPos(palette_offset); reader.ReadObjectArray<ucolor>(palette); // Find in lookup table for (int32_t i = 0; i < cycle_frames; i++) { reader.SetCurPos(lookup_table_offset + table_index * 2); table_index++; int32_t frame_index = reader.GetLEUInt16(); if (specific_frame != -1 && specific_frame != numeric_cast<int32_t>(i)) { continue; } // Get frame data reader.SetCurPos(frames_offset + frame_index * 12); int32_t width = reader.GetLEUInt16(); int32_t height = reader.GetLEUInt16(); int32_t ox = reader.GetLEUInt16(); int32_t oy = reader.GetLEUInt16(); uint32_t data_offset = reader.GetLEUInt32(); bool rle = (data_offset & 0x80000000) == 0; data_offset &= 0x7FFFFFFF; // Allocate data vector<uint8_t> data(numeric_cast<size_t>(width) * height * 4); span<ucolor> pixels = bytes_to_objects<ucolor>(span {data}); size_t pixel_pos = 0; // Fill it reader.SetCurPos(data_offset); for (int32_t k = 0, l = width * height; k < l;) { uint8_t index = reader.GetUInt8(); ucolor color = palette[index]; if (color.comp.b == 255) { color = ucolor::clear; } else { color.comp.a = 255; } std::swap(color.comp.r, color.comp.b); if (rle && index == compr_color) { int32_t copies = reader.GetUInt8(); for (int32_t m = 0; m <= copies; m++, k++) { pixels[pixel_pos] = color; pixel_pos++; } } else { pixels[pixel_pos] = color; pixel_pos++; k++; } } // Set in animation sequence auto& shot = collection.Main.Frames[specific_frame != -1 ? 0 : i]; shot.Width = numeric_cast<uint16_t>(width); shot.Height = numeric_cast<uint16_t>(height); shot.NextX = numeric_cast<int16_t>(-ox + width / 2); shot.NextY = numeric_cast<int16_t>(-oy + height); shot.Data = std::move(data); if (specific_frame != -1) { break; } } return collection; } auto ImageBaker::LoadPng(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(fname); ignore_unused(opt); ignore_unused(files); int32_t width = 0; int32_t height = 0; const_span<uint8_t> png_data = reader.GetDataSpan(); FO_VERIFY_AND_THROW(!png_data.empty(), "PNG file has no data to load"); auto png_data_ptr = make_ptr(png_data.data()); auto data = PngLoad(png_data_ptr, width, height); FrameCollection collection; collection.SequenceSize = 1; collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); collection.Main.Frames[0].Width = numeric_cast<uint16_t>(width); collection.Main.Frames[0].Height = numeric_cast<uint16_t>(height); collection.Main.Frames[0].Data = std::move(data); return collection; } auto ImageBaker::LoadTga(string_view fname, string_view opt, FileReader reader, const FileCollection& files) const -> FrameCollection { FO_STACK_TRACE_ENTRY(); ignore_unused(fname); ignore_unused(opt); ignore_unused(files); int32_t width = 0; int32_t height = 0; const_span<uint8_t> tga_data = reader.GetDataSpan(); auto data = TgaLoad(tga_data, width, height); FrameCollection collection; collection.SequenceSize = 1; collection.AnimTicks = 100; collection.Main.Frames.resize(collection.SequenceSize); collection.Main.Frames[0].Width = numeric_cast<uint16_t>(width); collection.Main.Frames[0].Height = numeric_cast<uint16_t>(height); collection.Main.Frames[0].Data = std::move(data); return collection; } static auto PngMalloc(png_structp png_ptr, png_alloc_size_t size) -> png_voidp { FO_NO_STACK_TRACE_ENTRY(); ignore_unused(png_ptr); return SafeAlloc::MallocRaw(size).get(); } static void PngFree(png_structp png_ptr, png_voidp mem) { FO_NO_STACK_TRACE_ENTRY(); ignore_unused(png_ptr); SafeAlloc::FreeRaw(mem); } static auto PngLoad(ptr<const uint8_t> data, int32_t& result_width, int32_t& result_height) -> vector<uint8_t> { FO_STACK_TRACE_ENTRY(); // Created through the _2 form so libpng allocates from the engine memory system rather than the CRT heap auto png_ptr = make_nptr(png_create_read_struct_2(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr, nullptr, &PngMalloc, &PngFree)); FO_VERIFY_AND_THROW(png_ptr, "Failed to create PNG read structure"); nptr<png_info> info_ptr; try { struct PngMessage { [[noreturn]] static void Error(png_structp png_ptr, png_const_charp error_msg) { ignore_unused(png_ptr); throw ImageBakerException("PNG loading error", error_msg); } static void Warning(png_structp png_ptr, png_const_charp error_msg) { ignore_unused(png_ptr); ignore_unused(error_msg); // WriteLog("PNG loading warning: {}", error_msg); } }; png_set_error_fn(png_ptr.get(), png_get_error_ptr(png_ptr.get()), &PngMessage::Error, &PngMessage::Warning); info_ptr = png_create_info_struct(png_ptr.get()); FO_VERIFY_AND_THROW(info_ptr, "Failed to create PNG info structure"); struct PngReader { static void Read(png_structp png_ptr, png_bytep png_data, png_size_t length) { if (length == 0) { return; } auto io_ptr = cast_from_void<const uint8_t**>(png_get_io_ptr(png_ptr)); FO_VERIFY_AND_THROW(io_ptr, "PNG read cursor is null"); auto source = make_ptr(*io_ptr); auto target = make_ptr(png_data); MemCopy(target, source, length); auto next_source = source.offset(length); *io_ptr = next_source.get(); } }; // Get info nptr<const uint8_t> png_data_cursor = data; png_set_read_fn(png_ptr.get(), make_nptr(png_data_cursor.get_pp()).void_cast(), &PngReader::Read); png_read_info(png_ptr.get(), info_ptr.get()); png_uint_32 width = 0; png_uint_32 height = 0; int32_t bit_depth = 0; int32_t color_type = 0; png_get_IHDR(png_ptr.get(), info_ptr.get(), &width, &height, &bit_depth, &color_type, nullptr, nullptr, nullptr); // Settings png_set_strip_16(png_ptr.get()); png_set_packing(png_ptr.get()); if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { png_set_expand(png_ptr.get()); } if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_expand(png_ptr.get()); } if (png_get_valid(png_ptr.get(), info_ptr.get(), PNG_INFO_tRNS) != 0) { png_set_expand(png_ptr.get()); } png_set_filler(png_ptr.get(), 0x000000ff, PNG_FILLER_AFTER); png_read_update_info(png_ptr.get(), info_ptr.get()); // Read vector<png_bytep> row_pointers; row_pointers.resize(height); vector<uint8_t> result; result.resize(numeric_cast<size_t>(width) * height * 4); for (png_uint_32 i = 0; i < height; i++) { auto row = make_ptr(result.data() + numeric_cast<size_t>(i) * numeric_cast<size_t>(width) * 4); row_pointers[i] = row.get(); } auto rows = make_ptr(row_pointers.data()); png_read_image(png_ptr.get(), rows.get()); png_read_end(png_ptr.get(), info_ptr.get()); png_destroy_read_struct(png_ptr.get_pp(), info_ptr.get_pp(), static_cast<png_infopp>(nullptr)); result_width = numeric_cast<int32_t>(width); result_height = numeric_cast<int32_t>(height); return result; } catch (...) { png_destroy_read_struct(png_ptr.get_pp(), info_ptr.get_pp(), static_cast<png_infopp>(nullptr)); throw; } } static auto TgaLoad(span<const uint8_t> data, int32_t& result_width, int32_t& result_height) -> vector<uint8_t> { FO_STACK_TRACE_ENTRY(); size_t cur_pos = 0; auto read_tga = [&](ptr<void> out, size_t len) { if (cur_pos + len <= data.size()) { if (len != 0) { auto source = make_ptr(data.data() + cur_pos); MemCopy(out, source, len); } cur_pos += (len); } else { throw ImageBakerException("TGA reading error"); } }; // Load header uint8_t type = 0; uint8_t pixel_depth = 0; int16_t width = 0; int16_t height = 0; uint8_t unused_char = 0; int16_t unused_short = 0; read_tga(&unused_char, 1); read_tga(&unused_char, 1); read_tga(&type, 1); read_tga(&unused_short, 2); read_tga(&unused_short, 2); read_tga(&unused_char, 1); read_tga(&unused_short, 2); read_tga(&unused_short, 2); read_tga(&width, 2); read_tga(&height, 2); read_tga(&pixel_depth, 1); read_tga(&unused_char, 1); // Check if the image is color indexed if (type == 1) { throw ImageBakerException("Indexed TGA is not supported"); } // Check for TrueColor if (type != 2 && type != 10) { throw ImageBakerException("TGA TrueColor is not supported"); } // Check for RGB(A) if (pixel_depth != 24 && pixel_depth != 32) { throw ImageBakerException("TGA support only 24 and 32 bpp"); } // Read int32_t bpp = pixel_depth / 8; int32_t read_size = height * width * bpp; vector<uint8_t> read_data; read_data.resize(read_size); if (type == 2) { if (read_size != 0) { auto read_data_begin = make_ptr(read_data.data()); read_tga(make_nptr(read_data_begin.get()).void_cast(), numeric_cast<size_t>(read_size)); } } else { int32_t bytes_read = 0; uint8_t header = 0; uint8_t color[4]; while (bytes_read < read_size) { read_tga(&header, 1); if ((header & 0x00000080) != 0) { header &= ~0x00000080; read_tga(color, bpp); int32_t run_len = (header + 1) * bpp; for (int32_t i = 0; i < run_len; i += bpp) { for (int32_t c = 0; c < bpp && bytes_read + i + c < read_size; c++) { read_data[bytes_read + i + c] = color[c]; } } bytes_read += run_len; } else { int32_t run_len = (header + 1) * bpp; int32_t to_read; if (bytes_read + run_len > read_size) { to_read = read_size - bytes_read; } else { to_read = run_len; } if (to_read != 0) { auto read_data_pos = make_ptr(read_data.data() + numeric_cast<size_t>(bytes_read)); read_tga(make_nptr(read_data_pos.get()).void_cast(), numeric_cast<size_t>(to_read)); } bytes_read += run_len; if (bytes_read + run_len > read_size) { cur_pos += run_len - to_read; } } } } // Copy data vector<uint8_t> result; result.resize(numeric_cast<size_t>(width) * height * 4); for (int16_t y = 0; y < height; y++) { for (int16_t x = 0; x < width; x++) { int32_t i = (height - y - 1) * width + x; int32_t j = y * width + x; result[i * 4 + 0] = read_data[j * bpp + 2]; result[i * 4 + 1] = read_data[j * bpp + 1]; result[i * 4 + 2] = read_data[j * bpp + 0]; result[i * 4 + 3] = bpp == 4 ? read_data[j * bpp + 3] : 0xFF; } } // Return data result_width = width; result_height = height; return result; } FO_END_NAMESPACE