This commit is contained in:
Lars Müller 2025-01-02 15:32:31 +00:00 committed by GitHub
commit d4c35475ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 239 additions and 117 deletions

View File

@ -265,7 +265,7 @@ The main Lua script. Running this script should register everything it
wants to register. Subsequent execution depends on Luanti calling the
registered callbacks.
### `textures`, `sounds`, `media`, `models`, `locale`
### `textures`, `sounds`, `media`, `models`, `locale`, `fonts`
Media files (textures, sounds, whatever) that will be transferred to the
client and will be available for use by the mod and translation files for
@ -278,6 +278,7 @@ Accepted formats are:
images: .png, .jpg, .tga
sounds: .ogg vorbis
models: .x, .b3d, .obj, (since version 5.10:) .gltf, .glb
fonts: .ttf (since version 5.?, see notes below) <!-- TODO before merge -->
Other formats won't be sent to the client (e.g. you can store .blend files
in a folder for convenience, without the risk that such files are transferred)
@ -342,6 +343,21 @@ For example, if your model used an emissive material,
you should expect that a future version of Luanti may respect this,
and thus cause your model to render differently there.
#### Custom fonts
You can supply custom fonts in TrueType Font (TTF) format.
In the future, having multiple custom fonts and the ability to switch between them is planned,
but for now this feature is limited to the ability to override Luanti's default fonts via mods.
It is recommended that this only be used by game mods. The names are self-explanatory:
* `regular.ttf`
* `bold.ttf`
* `italic.ttf`
* `bold_italic.ttf`
* `mono.ttf`
* `mono_bold.ttf`
* `mono_bold_italic.ttf`
Naming conventions
------------------

Binary file not shown.

View File

@ -0,0 +1,3 @@
-- Nothing to see here. This just overrides the builtin font with a media-provided font.
-- TODO make this less annoying (choose another font)
-- TODO license

View File

@ -0,0 +1,2 @@
name = fonts
description = Provides font media files

View File

@ -9,6 +9,7 @@
#include <IFileSystem.h>
#include <json/json.h>
#include "client.h"
#include "client/fontengine.h"
#include "network/clientopcodes.h"
#include "network/connection.h"
#include "network/networkpacket.h"
@ -360,6 +361,9 @@ Client::~Client()
for (auto &csp : m_sounds_client_to_server)
m_sound->freeId(csp.first);
m_sounds_client_to_server.clear();
// Go back to our mainmenu fonts
g_fontengine->clearMediaFonts();
}
void Client::connect(const Address &address, const std::string &address_name,
@ -833,6 +837,13 @@ bool Client::loadMedia(const std::string &data, const std::string &filename,
return true;
}
const char *font_ext[] = {".ttf", NULL};
name = removeStringEnd(filename, font_ext);
if (!name.empty()) {
g_fontengine->setMediaFont(name, data);
return true;
}
errorstream << "Client: Don't know how to load file \""
<< filename << "\"" << std::endl;
return false;

View File

@ -5,10 +5,6 @@
#include "fontengine.h"
#include <cmath>
#include "client/renderingengine.h"
#include "config.h"
#include "porting.h"
#include "filesys.h"
#include "gettext.h"
#include "settings.h"
#include "irrlicht_changes/CGUITTFont.h"
#include "util/numeric.h" // rangelim
@ -35,7 +31,6 @@ static const char *settings[] = {
"dpi_change_notifier", "display_density_factor", "gui_scaling",
};
/******************************************************************************/
FontEngine::FontEngine(gui::IGUIEnvironment* env) :
m_env(env)
{
@ -53,16 +48,14 @@ FontEngine::FontEngine(gui::IGUIEnvironment* env) :
g_settings->registerChangedCallback(name, font_setting_changed, this);
}
/******************************************************************************/
FontEngine::~FontEngine()
{
g_settings->deregisterAllChangedCallbacks(this);
cleanCache();
clearCache();
}
/******************************************************************************/
void FontEngine::cleanCache()
void FontEngine::clearCache()
{
RecursiveMutexAutoLock l(m_font_mutex);
@ -76,7 +69,6 @@ void FontEngine::cleanCache()
}
}
/******************************************************************************/
irr::gui::IGUIFont *FontEngine::getFont(FontSpec spec)
{
return getFont(spec, false);
@ -118,7 +110,6 @@ irr::gui::IGUIFont *FontEngine::getFont(FontSpec spec, bool may_fail)
return font;
}
/******************************************************************************/
unsigned int FontEngine::getTextHeight(const FontSpec &spec)
{
gui::IGUIFont *font = getFont(spec);
@ -126,7 +117,6 @@ unsigned int FontEngine::getTextHeight(const FontSpec &spec)
return font->getDimension(L"Some unimportant example String").Height;
}
/******************************************************************************/
unsigned int FontEngine::getTextWidth(const std::wstring &text, const FontSpec &spec)
{
gui::IGUIFont *font = getFont(spec);
@ -143,7 +133,6 @@ unsigned int FontEngine::getLineHeight(const FontSpec &spec)
+ font->getKerning(L'S').Y;
}
/******************************************************************************/
unsigned int FontEngine::getDefaultFontSize()
{
return m_default_size[m_currentMode];
@ -157,7 +146,6 @@ unsigned int FontEngine::getFontSize(FontMode mode)
return m_default_size[mode];
}
/******************************************************************************/
void FontEngine::readSettings()
{
m_default_size[FM_Standard] = rangelim(g_settings->getU16("font_size"), 5, 72);
@ -167,12 +155,11 @@ void FontEngine::readSettings()
m_default_bold = g_settings->getBool("font_bold");
m_default_italic = g_settings->getBool("font_italic");
cleanCache();
updateFontCache();
clearCache();
updateCache();
updateSkin();
}
/******************************************************************************/
void FontEngine::updateSkin()
{
gui::IGUIFont *font = getFont();
@ -181,15 +168,34 @@ void FontEngine::updateSkin()
m_env->getSkin()->setFont(font);
}
/******************************************************************************/
void FontEngine::updateFontCache()
void FontEngine::updateCache()
{
/* the only font to be initialized is default one,
* all others are re-initialized on demand */
getFont(FONT_SIZE_UNSPECIFIED, FM_Unspecified);
}
/******************************************************************************/
void FontEngine::setMediaFont(const std::string &name, const std::string &data)
{
std::string copy = data;
irr_ptr<gui::SGUITTFace> face(gui::SGUITTFace::createFace(std::move(copy)));
m_media_faces.emplace(name, face);
// HACK dedup this
clearCache();
updateCache();
updateSkin();
}
void FontEngine::clearMediaFonts()
{
RecursiveMutexAutoLock l(m_font_mutex);
m_media_faces.clear();
// HACK dedup this
clearCache();
updateCache();
updateSkin();
}
gui::IGUIFont *FontEngine::initFont(const FontSpec &spec)
{
assert(spec.mode != FM_Unspecified);
@ -235,10 +241,45 @@ gui::IGUIFont *FontEngine::initFont(const FontSpec &spec)
Settings::getLayer(SL_DEFAULTS)->get(path_setting)
};
for (const std::string &font_path : fallback_settings) {
gui::CGUITTFont *font = gui::CGUITTFont::createTTFont(m_env,
font_path.c_str(), size, true, true, font_shadow,
std::string media_name = spec.mode == FM_Mono
? "mono" + setting_suffix
: (setting_suffix.empty() ? "" : setting_suffix.substr(1));
if (media_name == "") media_name = "regular";
auto it = m_media_faces.find(media_name);
if (it != m_media_faces.end()) {
auto *face = it->second.get();
face->grab();
auto *font = gui::CGUITTFont::createTTFont(m_env,
face, size, true, true, font_shadow,
font_shadow_alpha);
if (!font) {
errorstream << "FontEngine: Cannot load media font '" << media_name <<
"'. Falling back to client settings." << std::endl;
}
// HACK this tidbit is duplicated
if (spec.mode != _FM_Fallback) {
FontSpec spec2(spec);
spec2.mode = _FM_Fallback;
font->setFallback(getFont(spec2, true));
}
return font;
}
for (const std::string &font_path : fallback_settings) {
infostream << "Creating new font: " << font_path.c_str()
<< " " << size << "pt" << std::endl;
// Grab the face.
auto *face = irr::gui::SGUITTFace::loadFace(font_path);
gui::CGUITTFont *font = nullptr;
if (face) {
font = gui::CGUITTFont::createTTFont(m_env,
face, size, true, true, font_shadow,
font_shadow_alpha);
}
if (!font) {
errorstream << "FontEngine: Cannot load '" << font_path <<

View File

@ -5,6 +5,9 @@
#pragma once
#include <map>
#include <unordered_map>
#include "irr_ptr.h"
#include "irrlicht_changes/CGUITTFont.h"
#include "util/basic_macros.h"
#include "irrlichttypes.h"
#include "irrString.h" // utf8_to_wide
@ -66,7 +69,7 @@ public:
/** get text height for a specific font */
unsigned int getTextHeight(const FontSpec &spec);
/** get text width if a text for a specific font */
/** get text width of a text for a specific font */
unsigned int getTextHeight(
unsigned int font_size=FONT_SIZE_UNSPECIFIED,
FontMode mode=FM_Unspecified)
@ -77,7 +80,7 @@ public:
unsigned int getTextWidth(const std::wstring &text, const FontSpec &spec);
/** get text width if a text for a specific font */
/** get text width of a text for a specific font */
unsigned int getTextWidth(const std::wstring& text,
unsigned int font_size=FONT_SIZE_UNSPECIFIED,
FontMode mode=FM_Unspecified)
@ -118,11 +121,15 @@ public:
/** update internal parameters from settings */
void readSettings();
void setMediaFont(const std::string &name, const std::string &data);
void clearMediaFonts();
private:
irr::gui::IGUIFont *getFont(FontSpec spec, bool may_fail);
/** update content of font cache in case of a setting change made it invalid */
void updateFontCache();
void updateCache();
/** initialize a new TTF font */
gui::IGUIFont *initFont(const FontSpec &spec);
@ -131,7 +138,7 @@ private:
void updateSkin();
/** clean cache */
void cleanCache();
void clearCache();
/** pointer to irrlicht gui environment */
gui::IGUIEnvironment* m_env = nullptr;
@ -142,6 +149,9 @@ private:
/** internal storage for caching fonts of different size */
std::map<unsigned int, irr::gui::IGUIFont*> m_font_cache[FM_MaxMode << 2];
/** media-provided faces, indexed by filename (without extension) */
std::unordered_map<std::string, irr_ptr<gui::SGUITTFace>> m_media_faces;
/** default font size to use */
unsigned int m_default_size[FM_MaxMode];

View File

@ -31,7 +31,11 @@
john@suckerfreegames.com
*/
#include <cstdlib>
#include <freetype/freetype.h>
#include <iostream>
#include <optional>
#include "irr_ptr.h"
#include "log.h"
#include "filesys.h"
#include "debug.h"
@ -45,29 +49,83 @@ namespace irr
namespace gui
{
// Manages the FT_Face cache.
struct SGUITTFace : public irr::IReferenceCounted
std::map<io::path, SGUITTFace*> SGUITTFace::faces;
std::optional<FT_Library> SGUITTFace::freetype_library;
std::size_t SGUITTFace::n_faces;
std::optional<FT_Library> SGUITTFace::getFreeTypeLibrary()
{
SGUITTFace()
{
memset((void*)&face, 0, sizeof(FT_Face));
if (freetype_library) return *freetype_library;
FT_Library ft;
if (FT_Init_FreeType(&ft))
return std::nullopt; // TODO can't we just bail out entirely if this fails?
return freetype_library = ft;
}
SGUITTFace::SGUITTFace(std::string &&buffer) : face_buffer(std::move(buffer))
{
memset((void*)&face, 0, sizeof(FT_Face));
n_faces++;
}
SGUITTFace::~SGUITTFace()
{
FT_Done_Face(face);
n_faces--;
// If there are no more faces referenced by FreeType, clean up.
if (n_faces == 0) {
assert(freetype_library);
FT_Done_FreeType(*freetype_library);
freetype_library = std::nullopt;
}
}
SGUITTFace* SGUITTFace::createFace(std::string &&buffer)
{
irr_ptr<SGUITTFace> face(new SGUITTFace(std::move(buffer)));
auto ft = getFreeTypeLibrary();
if (!ft) return nullptr;
return (FT_New_Memory_Face(*ft,
reinterpret_cast<const FT_Byte*>(face->face_buffer.data()),
face->face_buffer.size(), 0, &face->face))
? nullptr : face.release();
}
SGUITTFace* SGUITTFace::loadFace(const io::path &filename)
{
auto it = faces.find(filename);
if (it != faces.end()) {
it->second->grab();
return it->second;
}
~SGUITTFace()
{
FT_Done_Face(face);
std::string buffer;
if (!fs::ReadFile(filename.c_str(), buffer, true)) {
errorstream << "CGUITTFont: Reading file " << filename.c_str() << " failed." << std::endl;
return nullptr;
}
FT_Face face;
std::string face_buffer;
};
auto *face = SGUITTFace::createFace(std::move(buffer));
if (!face) {
errorstream << "CGUITTFont: FT_New_Memory_Face failed." << std::endl;
return nullptr;
}
faces.emplace(filename, face);
return face;
}
// Static variables.
FT_Library CGUITTFont::c_library;
std::map<io::path, SGUITTFace*> CGUITTFont::c_faces;
bool CGUITTFont::c_libraryLoaded = false;
void SGUITTFace::dropFilename()
{
if (!filename.has_value()) return;
//
auto it = faces.find(*filename);
if (it == faces.end()) return;
SGUITTFace* f = it->second;
// Drop our face. If this was the last face, the destructor will clean up.
if (f->drop())
faces.erase(*filename);
}
video::IImage* SGUITTGlyph::createGlyphImage(const FT_Bitmap& bits, video::IVideoDriver* driver) const
{
@ -203,17 +261,14 @@ void SGUITTGlyph::unload()
//////////////////////
CGUITTFont* CGUITTFont::createTTFont(IGUIEnvironment *env, const io::path& filename, const u32 size, const bool antialias, const bool transparency, const u32 shadow, const u32 shadow_alpha)
{
if (!c_libraryLoaded)
{
if (FT_Init_FreeType(&c_library))
return 0;
c_libraryLoaded = true;
}
// TODO constructor which takes bogus filename and data
CGUITTFont* CGUITTFont::createTTFont(IGUIEnvironment *env,
SGUITTFace *face, u32 size, bool antialias,
bool transparency, u32 shadow, u32 shadow_alpha)
{
CGUITTFont* font = new CGUITTFont(env);
bool ret = font->load(filename, size, antialias, transparency);
bool ret = font->load(face, size, antialias, transparency);
if (!ret)
{
font->drop();
@ -246,53 +301,20 @@ shadow_offset(0), shadow_alpha(0), fallback(0)
setInvisibleCharacters(L" ");
}
bool CGUITTFont::load(const io::path& filename, const u32 size, const bool antialias, const bool transparency)
bool CGUITTFont::load(SGUITTFace *face, const u32 size, const bool antialias, const bool transparency)
{
// Some sanity checks.
if (!Driver) return false;
if (size == 0) return false;
if (filename.empty()) return false;
if (!face) return false;
this->size = size;
this->filename = filename;
// Update the font loading flags when the font is first loaded.
this->use_monochrome = !antialias;
this->use_transparency = transparency;
update_load_flags();
infostream << "CGUITTFont: Creating new font: " << filename.c_str() << " "
<< size << "pt " << (antialias ? "+antialias " : "-antialias ")
<< (transparency ? "+transparency" : "-transparency") << std::endl;
// Grab the face.
SGUITTFace* face = nullptr;
auto node = c_faces.find(filename);
if (node == c_faces.end()) {
face = new SGUITTFace();
if (!fs::ReadFile(filename.c_str(), face->face_buffer, true)) {
delete face;
return false;
}
// Create the face.
if (FT_New_Memory_Face(c_library,
reinterpret_cast<const FT_Byte*>(face->face_buffer.data()),
face->face_buffer.size(), 0, &face->face))
{
errorstream << "CGUITTFont: FT_New_Memory_Face failed." << std::endl;
delete face;
return false;
}
c_faces.emplace(filename, face);
} else {
// Using another instance of this face.
face = node->second;
face->grab();
}
// Store our face.
tt_face = face->face;
@ -322,24 +344,6 @@ CGUITTFont::~CGUITTFont()
reset_images();
Glyphs.clear();
// We aren't using this face anymore.
auto n = c_faces.find(filename);
if (n != c_faces.end())
{
SGUITTFace* f = n->second;
// Drop our face. If this was the last face, the destructor will clean up.
if (f->drop())
c_faces.erase(filename);
// If there are no more faces referenced by FreeType, clean up.
if (c_faces.empty())
{
FT_Done_FreeType(c_library);
c_libraryLoaded = false;
}
}
// Drop our driver now.
if (Driver)
Driver->drop();

View File

@ -33,6 +33,7 @@
#pragma once
#include <map>
#include <ft2build.h>
#include FT_FREETYPE_H
@ -44,11 +45,45 @@
#include "util/enriched_string.h"
#include "util/basic_macros.h"
// TODO these should be in the C++ when the struct is properly split into C++ & header
#include <optional>
#include "irr_ptr.h"
#include "log.h"
#include "filesys.h"
namespace irr
{
namespace gui
{
struct SGUITTFace;
// Manages the FT_Face cache.
struct SGUITTFace : public irr::IReferenceCounted
{
private:
static std::map<io::path, SGUITTFace*> faces;
static std::optional<FT_Library> freetype_library;
static std::size_t n_faces;
static std::optional<FT_Library> getFreeTypeLibrary();
public:
SGUITTFace(std::string &&buffer);
~SGUITTFace();
std::optional<std::string> filename;
FT_Face face;
/// Must not be deallocated until we are done with the face!
std::string face_buffer;
static SGUITTFace* createFace(std::string &&buffer);
static SGUITTFace* loadFace(const io::path &filename);
void dropFilename();
};
class CGUITTFont;
//! Structure representing a single TrueType glyph.
@ -215,12 +250,13 @@ namespace gui
public:
//! Creates a new TrueType font and returns a pointer to it. The pointer must be drop()'ed when finished.
//! \param env The IGUIEnvironment the font loads out of.
//! \param filename The filename of the font.
//! \param size The size of the font glyphs in pixels. Since this is the size of the individual glyphs, the true height of the font may change depending on the characters used.
//! \param antialias set the use_monochrome (opposite to antialias) flag
//! \param transparency set the use_transparency flag
//! \return Returns a pointer to a CGUITTFont. Will return 0 if the font failed to load.
static CGUITTFont* createTTFont(IGUIEnvironment *env, const io::path& filename, const u32 size, const bool antialias = true, const bool transparency = true, const u32 shadow = 0, const u32 shadow_alpha = 255);
static CGUITTFont* createTTFont(IGUIEnvironment *env,
SGUITTFace *face, u32 size, bool antialias = true,
bool transparency = true, u32 shadow = 0, u32 shadow_alpha = 255);
//! Destructor
virtual ~CGUITTFont();
@ -329,11 +365,6 @@ namespace gui
core::dimension2du max_page_texture_size;
private:
// Manages the FreeType library.
static FT_Library c_library;
static std::map<io::path, SGUITTFace*> c_faces;
static bool c_libraryLoaded;
// Helper functions for the same-named public member functions above
// (Since std::u32string is nicer to work with than wchar_t *)
core::dimension2d<u32> getDimension(const std::u32string& text) const;
@ -343,7 +374,7 @@ namespace gui
std::u32string convertWCharToU32String(const wchar_t* const) const;
CGUITTFont(IGUIEnvironment *env);
bool load(const io::path& filename, const u32 size, const bool antialias, const bool transparency);
bool load(SGUITTFace *face, const u32 size, const bool antialias, const bool transparency);
void reset_images();
void update_glyph_pages() const;
void update_load_flags()
@ -361,7 +392,7 @@ namespace gui
core::vector2di getKerning(const char32_t thisLetter, const char32_t previousLetter) const;
video::IVideoDriver* Driver;
io::path filename;
std::optional<io::path> filename;
FT_Face tt_face;
FT_Size_Metrics font_metrics;
FT_Int32 load_flags;

View File

@ -2541,6 +2541,9 @@ bool Server::addMediaFile(const std::string &filename,
".x", ".b3d", ".obj", ".gltf", ".glb",
// Translation file formats
".tr", ".po", ".mo",
// Fonts
// TODO throw a warning and ignore file if name is not one of the few recognized ones?
".ttf",
NULL
};
if (removeStringEnd(filename, supported_ext).empty()) {

View File

@ -87,5 +87,6 @@ void ServerModManager::getModsMediaPaths(std::vector<std::string> &paths) const
fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "media");
fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "models");
fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "locale");
fs::GetRecursiveDirs(paths, spec.path + DIR_DELIM + "fonts");
}
}

View File

@ -107,7 +107,7 @@ void TestServerModManager::testGetMods()
ServerModManager sm(m_worlddir);
const auto &mods = sm.getMods();
// `ls ./games/devtest/mods | wc -l` + 1 (test mod)
UASSERTEQ(std::size_t, mods.size(), 34 + 1);
UASSERTEQ(std::size_t, mods.size(), 35 + 1);
// Ensure we found basenodes mod (part of devtest)
// and test_mod (for testing MINETEST_MOD_PATH).