feat: font loading and dumping (#866)

* feat: font loading and dumping

* chore: make font properties unsigned if possible

* chore: use merge_patch to dump json font

* chore: font material is required

* chore: move static methods from font loader and dumper to anonymous namespace

* chore: make sure loaded fonts have their glyphs sorted

* chore: add macros for json NLOHMANN_TO_JSON_METHOD and NLOHMANN_FROM_JSON_METHOD

* feat: encode printable letters as string in json

Co-authored-by: hindercanrun <[email protected]>

* chore: omit redundant glyph count from jsons

---------

Co-authored-by: Jan Laupetin <[email protected]>
Co-authored-by: hindercanrun <[email protected]>
Co-authored-by: MrIkso <[email protected]>
This commit is contained in:
mo
2026-07-03 19:26:12 +02:00
committed by GitHub
co-authored by hindercanrun Jan Laupetin MrIkso
parent fb4b00398c
commit 3fb8b2bb17
24 changed files with 659 additions and 35 deletions
+254
View File
@@ -0,0 +1,254 @@
#options GAME (IW3, IW4, IW5, T4, T5)
#filename "Game/" + GAME + "/Font/FontLoader" + GAME + ".cpp"
#if GAME == "IW3"
#define GAME_LOWER "iw3"
#elif GAME == "IW4"
#define GAME_LOWER "iw4"
#elif GAME == "IW5"
#define GAME_LOWER "iw5"
#elif GAME == "T4"
#define GAME_LOWER "t4"
#elif GAME == "T5"
#define GAME_LOWER "t5"
#endif
// This file was templated.
// See FontLoader.cpp.template.
// Do not modify, changes will be lost.
#set LOADER_HEADER "\"FontLoader" + GAME + ".h\""
#include LOADER_HEADER
#include "Font/FontCommon.h"
#set JSON_HEADER "\"Game/" + GAME + "/Font/JsonFont" + GAME + ".h\""
#include JSON_HEADER
#include "Utils/Logging/Log.h"
#include <format>
#include <limits>
#include <nlohmann/json.hpp>
using namespace nlohmann;
using namespace GAME;
namespace
{
void PrintError(const Font_s& font, const std::string& message)
{
con::error("Cannot load font \"{}\": {}", font.fontName, message);
}
template<typename FieldType, typename ValueType>
bool AssignIntegerField(FieldType& field, const ValueType value, const Font_s& font, const char* fieldName)
{
const auto minValue = static_cast<ValueType>(std::numeric_limits<FieldType>::min());
const auto maxValue = static_cast<ValueType>(std::numeric_limits<FieldType>::max());
if (value < minValue || value > maxValue)
{
PrintError(font, std::format("{} value {} is outside allowed range {}..{}", fieldName, value, minValue, maxValue));
return false;
}
field = static_cast<FieldType>(value);
return true;
}
bool CreateMaterialDependency(const std::string& materialName,
Material*& material,
AssetRegistration<AssetFont>& registration,
AssetCreationContext& context,
const Font_s& font,
const char* fieldName)
{
auto* materialDependency = context.LoadDependency<AssetMaterial>(materialName);
if (!materialDependency)
{
PrintError(font, std::format("Could not find {} material \"{}\"", fieldName, materialName));
return false;
}
registration.AddDependency(materialDependency);
material = materialDependency->Asset();
return true;
}
bool CreateGlyphFromJson(const JsonGlyph& jGlyph, Glyph& glyph, const Font_s& font)
{
if (!AssignIntegerField(glyph.letter, jGlyph.letter, font, "glyph.letter"))
return false;
if (!AssignIntegerField(glyph.x0, jGlyph.x0, font, "glyph.x0"))
return false;
if (!AssignIntegerField(glyph.y0, jGlyph.y0, font, "glyph.y0"))
return false;
if (!AssignIntegerField(glyph.dx, jGlyph.dx, font, "glyph.dx"))
return false;
if (!AssignIntegerField(glyph.pixelWidth, jGlyph.pixelWidth, font, "glyph.pixelWidth"))
return false;
if (!AssignIntegerField(glyph.pixelHeight, jGlyph.pixelHeight, font, "glyph.pixelHeight"))
return false;
glyph.s0 = jGlyph.s0;
glyph.t0 = jGlyph.t0;
glyph.s1 = jGlyph.s1;
glyph.t1 = jGlyph.t1;
return true;
}
constexpr auto REQUIRED_GLYPH_START = 0x20;
constexpr auto REQUIRED_GLYPH_END_INCLUSIVE = 0x7F;
constexpr unsigned REQUIRED_GLYPH_COUNT = REQUIRED_GLYPH_END_INCLUSIVE + 1 - REQUIRED_GLYPH_START;
constexpr bool IsRequiredGlyph(const unsigned letter)
{
return letter >= REQUIRED_GLYPH_START && letter <= REQUIRED_GLYPH_END_INCLUSIVE;
}
void SortGlyphs(const Font_s& font)
{
std::sort(&font.glyphs[0], &font.glyphs[font.glyphCount], [](const Glyph& a, const Glyph& b)
{
const auto aRequired = IsRequiredGlyph(a.letter);
const auto bRequired = IsRequiredGlyph(b.letter);
if (aRequired != bRequired)
return aRequired;
return a.letter < b.letter;
});
}
bool EnsureFontContainsAllRequiredGlyphs(const Font_s& font)
{
if (static_cast<unsigned>(font.glyphCount) < REQUIRED_GLYPH_COUNT)
{
con::error("Font {} must contain all {} required letters", font.fontName, REQUIRED_GLYPH_COUNT);
return false;
}
for (unsigned i = 0; i < REQUIRED_GLYPH_COUNT; i++)
{
const unsigned requiredGlyphLetter = REQUIRED_GLYPH_START + i;
if (font.glyphs[i].letter != requiredGlyphLetter)
{
con::error("Font {} is missing required letter {} ('{}')", font.fontName, requiredGlyphLetter, static_cast<char>(requiredGlyphLetter));
return false;
}
}
return true;
}
class FontLoader final : public AssetCreator<AssetFont>
{
public:
FontLoader(MemoryManager& memory, ISearchPath& searchPath)
: m_memory(memory),
m_search_path(searchPath)
{
}
AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override
{
const auto file = m_search_path.Open(font::GetJsonFileNameForAssetName(assetName));
if (!file.IsOpen())
return AssetCreationResult::NoAction();
auto* font = m_memory.Alloc<Font_s>();
font->fontName = m_memory.Dup(assetName.c_str());
AssetRegistration<AssetFont> registration(assetName, font);
try
{
const auto jRoot = json::parse(*file.m_stream);
std::string type;
unsigned version;
jRoot.at("_type").get_to(type);
jRoot.at("_version").get_to(version);
if (type != "font" || version != 1u)
{
con::error(R"(Tried to load font "{}" but did not find expected type font of version 1)", assetName);
return AssetCreationResult::Failure();
}
std::string game;
jRoot.at("_game").get_to(game);
if (game != GAME_LOWER)
{
con::error(R"(Tried to load font "{}" but "_game" did not have expected value {})", assetName, GAME_LOWER);
return AssetCreationResult::Failure();
}
const auto jFont = jRoot.get<JsonFont>();
if (CreateFontFromJson(jFont, *font, registration, context))
return AssetCreationResult::Success(context.AddAsset(std::move(registration)));
}
catch (const json::exception& e)
{
con::error("Failed to parse json of font: {}", e.what());
}
return AssetCreationResult::Failure();
}
private:
bool CreateFontFromJson(const JsonFont& jFont, Font_s& font, AssetRegistration<AssetFont>& registration, AssetCreationContext& context) const
{
font.pixelHeight = static_cast<decltype(Font_s::pixelHeight)>(jFont.pixelHeight);
if (!CreateMaterialDependency(jFont.material, font.material, registration, context, font, "font"))
return false;
if (jFont.glowMaterial)
{
if (!CreateMaterialDependency(*jFont.glowMaterial, font.glowMaterial, registration, context, font, "glow"))
return false;
}
constexpr auto MAX_GLYPH_COUNT = std::numeric_limits<decltype(Font_s::glyphCount)>::max();
if (jFont.glyphs.size() > static_cast<size_t>(MAX_GLYPH_COUNT))
{
PrintError(font, std::format("glyph count {} exceeds maximum {}", jFont.glyphs.size(), MAX_GLYPH_COUNT));
return false;
}
font.glyphCount = static_cast<decltype(Font_s::glyphCount)>(jFont.glyphs.size());;
if (font.glyphCount <= 0)
{
font.glyphs = nullptr;
return true;
}
font.glyphs = m_memory.Alloc<Glyph>(font.glyphCount);
for (auto i = 0; i < font.glyphCount; i++)
{
if (!CreateGlyphFromJson(jFont.glyphs[i], font.glyphs[i], font))
return false;
}
SortGlyphs(font);
if (!EnsureFontContainsAllRequiredGlyphs(font))
return false;
return true;
}
MemoryManager& m_memory;
ISearchPath& m_search_path;
};
} // namespace
namespace font
{
#set CREATE_LOADER_METHOD "CreateLoader" + GAME
std::unique_ptr<AssetCreator<AssetFont>> CREATE_LOADER_METHOD(MemoryManager& memory, ISearchPath& searchPath)
{
return std::make_unique<FontLoader>(memory, searchPath);
}
} // namespace font
+23
View File
@@ -0,0 +1,23 @@
#options GAME (IW3, IW4, IW5, T4, T5)
#filename "Game/" + GAME + "/Font/FontLoader" + GAME + ".h"
// This file was templated.
// See FontLoader.h.template.
// Do not modify, changes will be lost.
#pragma once
#include "Asset/IAssetCreator.h"
#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\""
#include GAME_HEADER
#include "SearchPath/ISearchPath.h"
#include "Utils/MemoryManager.h"
#include <memory>
namespace font
{
#set CREATE_LOADER_METHOD "CreateLoader" + GAME
std::unique_ptr<AssetCreator<GAME::AssetFont>> CREATE_LOADER_METHOD(MemoryManager& memory, ISearchPath& searchPath);
} // namespace font