diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index c53ec082eb9..2d63becc8de 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -124,6 +124,16 @@ jobs:
Copy-Item $_.FullName ci-artifacts/
}
+ if (Test-Path "assets/fonts") {
+ if (-not (Test-Path "assets/fonts/LICENSE.liberation") -or -not (Test-Path "assets/fonts/LICENSE.fontawesome")) {
+ throw "Missing required font license files in assets/fonts"
+ }
+ New-Item -ItemType Directory -Force artifacts/fonts | Out-Null
+ Copy-Item assets/fonts/* artifacts/fonts/
+ New-Item -ItemType Directory -Force ci-artifacts/fonts | Out-Null
+ Copy-Item assets/fonts/* ci-artifacts/fonts/
+ }
+
# Collect Desktop Release CRT DLLs (msvcp140.dll, vcruntime140.dll, etc.)
$vcReleaseDir = Get-ChildItem "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Redist\MSVC" -Directory -Recurse |
Where-Object { $_.FullName -match "\\x86\\" -and $_.FullName -match "Microsoft\.VC143\.CRT$" -and $_.FullName -notmatch "onecore" } |
diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index 96dc3361509..1d00cd86408 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -3,6 +3,7 @@ add_library(corei_gameengine_include INTERFACE)
add_library(corei_libraries_include INTERFACE)
add_library(corei_libraries_source_wwvegas INTERFACE)
add_library(corei_main INTERFACE)
+add_library(corei_platform INTERFACE)
add_library(corei_always INTERFACE)
add_library(corei_always_no_pch INTERFACE) # Use this for Shared Libs with MFC AFX
@@ -16,6 +17,7 @@ endif()
target_include_directories(corei_gameengine_include INTERFACE "GameEngine/Include")
target_include_directories(corei_libraries_include INTERFACE "Libraries/Include")
target_include_directories(corei_libraries_source_wwvegas INTERFACE "Libraries/Source/WWVegas")
+target_include_directories(corei_platform INTERFACE "Libraries/Source/Platform")
target_include_directories(corei_main INTERFACE "Main")
target_sources(corei_libraries_include PRIVATE
diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp
index 9af356cc3a8..6551e88892b 100644
--- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp
+++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp
@@ -72,9 +72,12 @@ FontCharsClass *LoadUnicodeFallbackFont(Int size, Bool bold, const char *base_na
// then known-good Unicode fonts. We iterate all candidates and pick the first that loads AND has a
// different family from the base font. This avoids returning a font that lacks Cyrillic coverage
// (e.g., Arial on macOS) when a better universal font like "Arial Unicode MS" is available.
+ // GeneralsX @feature felipebraz 26/09/2026 Include Liberation Sans in Unicode fallback candidate list
static const char *kFallbackUnicodeFonts[] = {
"Arial Unicode MS",
"Arial Unicode",
+ "Liberation Sans",
+ "Liberation Serif",
"Arial",
"Helvetica Neue",
"Helvetica",
diff --git a/Core/Libraries/Source/Platform/PlatformPaths.h b/Core/Libraries/Source/Platform/PlatformPaths.h
new file mode 100644
index 00000000000..8af0ede142d
--- /dev/null
+++ b/Core/Libraries/Source/Platform/PlatformPaths.h
@@ -0,0 +1,195 @@
+/*
+** Command & Conquer(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// PlatformPaths.h - Cross-platform executable and bundle path resolution
+// GeneralsX @feature felipebraz 26/09/2026 Platform abstraction for bundle paths
+// GeneralsX @refactor felipebraz 26/09/2026 Encapsulate font probing and file accessibility in platform layer
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#if defined(_WIN32)
+#include
+#else
+#include
+#include
+#endif
+
+#if defined(__APPLE__)
+#include
+#endif
+
+namespace Platform {
+
+inline bool IsFileReadable(const char *path)
+{
+ if ( !path || path[0] == '\0' ) {
+ return false;
+ }
+#if defined(_WIN32)
+ return ( _access( path, 4 ) == 0 );
+#else
+ return ( access( path, R_OK ) == 0 );
+#endif
+}
+
+inline bool GetMacOSBundleFontDirectories(char *resFonts, size_t resFontsSize,
+ char *binFonts, size_t binFontsSize)
+{
+#if defined(__APPLE__)
+ char rawExecPath[1024] = {0};
+ uint32_t bufSize = sizeof(rawExecPath);
+ if ( _NSGetExecutablePath( rawExecPath, &bufSize ) != 0 ) {
+ return false;
+ }
+
+ char realExecPath[1024] = {0};
+ if ( realpath( rawExecPath, realExecPath ) == nullptr ) {
+ return false;
+ }
+
+ char *lastSlash = strrchr( realExecPath, '/' );
+ if ( lastSlash == nullptr ) {
+ return false;
+ }
+ *lastSlash = '\0';
+
+ if ( resFonts && resFontsSize > 0 ) {
+ snprintf( resFonts, resFontsSize, "%s/../Resources/fonts", realExecPath );
+ }
+ if ( binFonts && binFontsSize > 0 ) {
+ snprintf( binFonts, binFontsSize, "%s/../fonts", realExecPath );
+ }
+ return true;
+#else
+ (void)resFonts;
+ (void)resFontsSize;
+ (void)binFonts;
+ (void)binFontsSize;
+ return false;
+#endif
+}
+
+inline bool FindLocalFontFile(const char *const *candidates, int candidateCount,
+ char *outPath, size_t outPathSize)
+{
+ if ( !candidates || candidateCount <= 0 || !outPath || outPathSize == 0 ) {
+ return false;
+ }
+
+ const char *searchDirs[24];
+ int searchDirCount = 0;
+
+ char envBundleFonts[512] = {0};
+ const char *gxBundleFonts = getenv( "GX_BUNDLE_FONTS" );
+ if ( gxBundleFonts && gxBundleFonts[0] != '\0' ) {
+ snprintf( envBundleFonts, sizeof(envBundleFonts), "%s", gxBundleFonts );
+ searchDirs[searchDirCount++] = envBundleFonts;
+ }
+
+#if defined(__APPLE__)
+ char macosResFonts[512] = {0};
+ char macosBinFonts[512] = {0};
+ if ( GetMacOSBundleFontDirectories( macosResFonts, sizeof(macosResFonts),
+ macosBinFonts, sizeof(macosBinFonts) ) ) {
+ if ( IsFileReadable( macosResFonts ) && searchDirCount < 24 ) {
+ searchDirs[searchDirCount++] = macosResFonts;
+ }
+ if ( IsFileReadable( macosBinFonts ) && searchDirCount < 24 ) {
+ searchDirs[searchDirCount++] = macosBinFonts;
+ }
+ }
+#endif
+
+ if ( searchDirCount < 24 ) searchDirs[searchDirCount++] = "fonts";
+ if ( searchDirCount < 24 ) searchDirs[searchDirCount++] = "./fonts";
+ if ( searchDirCount < 24 ) searchDirs[searchDirCount++] = "../Resources/fonts";
+ if ( searchDirCount < 24 ) searchDirs[searchDirCount++] = "Resources/fonts";
+ if ( searchDirCount < 24 ) searchDirs[searchDirCount++] = "assets/fonts";
+
+ char envZhFonts[512] = {0};
+ const char *zhPath = getenv( "CNC_GENERALS_ZH_PATH" );
+ if ( zhPath && zhPath[0] != '\0' && searchDirCount < 24 ) {
+ snprintf( envZhFonts, sizeof(envZhFonts), "%s/fonts", zhPath );
+ searchDirs[searchDirCount++] = envZhFonts;
+ }
+
+ char envGenFonts[512] = {0};
+ const char *genPath = getenv( "CNC_GENERALS_PATH" );
+ if ( genPath && genPath[0] != '\0' && searchDirCount < 24 ) {
+ snprintf( envGenFonts, sizeof(envGenFonts), "%s/fonts", genPath );
+ searchDirs[searchDirCount++] = envGenFonts;
+ }
+
+ char homeZhFonts[512] = {0};
+ char homeGenFonts[512] = {0};
+ const char *homePath = getenv( "HOME" );
+ if ( homePath && homePath[0] != '\0' ) {
+ if ( searchDirCount < 24 ) {
+ snprintf( homeZhFonts, sizeof(homeZhFonts), "%s/GeneralsX/GeneralsZH/fonts", homePath );
+ searchDirs[searchDirCount++] = homeZhFonts;
+ }
+ if ( searchDirCount < 24 ) {
+ snprintf( homeGenFonts, sizeof(homeGenFonts), "%s/GeneralsX/Generals/fonts", homePath );
+ searchDirs[searchDirCount++] = homeGenFonts;
+ }
+ }
+
+ if ( searchDirCount < 24 ) {
+ searchDirs[searchDirCount++] = "/app/share/fonts";
+ }
+ if ( searchDirCount < 24 ) {
+ searchDirs[searchDirCount++] = "/usr/share/fonts/truetype/liberation";
+ }
+
+ static const char *extensions[] = { ".ttf", ".otf", ".ttc" };
+ char candidatePath[1024];
+
+ for ( int d = 0; d < searchDirCount; ++d ) {
+ for ( int c = 0; c < candidateCount; ++c ) {
+ const char *cand = candidates[c];
+ if ( !cand || cand[0] == '\0' ) {
+ continue;
+ }
+ if ( strrchr( cand, '.' ) != nullptr ) {
+ snprintf( candidatePath, sizeof(candidatePath), "%s/%s", searchDirs[d], cand );
+ if ( IsFileReadable( candidatePath ) ) {
+ snprintf( outPath, outPathSize, "%s", candidatePath );
+ return true;
+ }
+ } else {
+ for ( size_t e = 0; e < sizeof(extensions) / sizeof(extensions[0]); ++e ) {
+ snprintf( candidatePath, sizeof(candidatePath), "%s/%s%s",
+ searchDirs[d], cand, extensions[e] );
+ if ( IsFileReadable( candidatePath ) ) {
+ snprintf( outPath, outPathSize, "%s", candidatePath );
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+} // namespace Platform
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
index 21f2b1be15e..44fc313e754 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt
@@ -277,6 +277,7 @@ target_link_libraries(corei_ww3d2 INTERFACE
core_browserengine
core_wwlib
core_wwmath
+ corei_platform
$<$:Freetype::Freetype>
$<$:Freetype::Freetype>
$<$:Fontconfig::Fontconfig>
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
index 3f4d802b850..c396aa1ce44 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp
@@ -1636,62 +1636,185 @@ FontCharsClass::Free_GDI_Font ()
#if defined(SAGE_USE_FREETYPE) && !defined(_WIN32)
+#include
+#include
+#include
+#include
+#if defined(__APPLE__)
+#include
+#endif
+#include "PlatformPaths.h"
+
////////////////////////////////////////////////////////////////////////////////////
//
// Locate_Font_FontConfig
//
-// TheSuperHackers @feature FreeType port 10/02/2026 Locate system font using Fontconfig
+// GeneralsX @feature felipebraz 26/09/2026 Universal multi-tiered font resolution:
+// Tier 1: Probe local/bundled fonts/ directory (app bundle, game root, cwd)
+// Tier 2: System Fontconfig match (desktop Linux/macOS)
+// Tier 3: Guaranteed fallback to bundled Arial (Liberation Sans)
////////////////////////////////////////////////////////////////////////////////////
const char *
FontCharsClass::Locate_Font_FontConfig (const char *font_name)
{
- //
- // Initialize Fontconfig library
- //
- FcConfig *config = FcInitLoadConfigAndFonts();
- if ( config == nullptr ) {
+ if ( font_name == nullptr || font_name[0] == '\0' ) {
return nullptr;
}
//
- // Create a pattern for the requested font
+ // Normalize font name (lowercase, strip spaces, hyphens, and underscores)
//
- FcPattern *pattern = FcNameParse( (const FcChar8*)font_name );
- if ( pattern == nullptr ) {
- FcConfigDestroy( config );
- return nullptr;
+ char normalized[128];
+ int n = 0;
+ for ( const char *p = font_name; *p != '\0' && n < (int)sizeof(normalized) - 1; ++p ) {
+ if ( *p == ' ' || *p == '-' || *p == '_' ) {
+ continue;
+ }
+ normalized[n++] = (char)tolower( (unsigned char)*p );
+ }
+ normalized[n] = '\0';
+
+ // Map "generals" to "arial"
+ if ( strcmp( normalized, "generals" ) == 0 ) {
+ strncpy( normalized, "arial", sizeof(normalized) - 1 );
+ normalized[sizeof(normalized) - 1] = '\0';
}
//
- // Configure the pattern
+ // Prepare candidate names based on requested font and bold status
//
- FcConfigSubstitute( config, pattern, FcMatchPattern );
- FcDefaultSubstitute( pattern );
+ bool is_bold = this->IsBold || (strstr( font_name, "Bold" ) != nullptr) || (strstr( font_name, "bold" ) != nullptr);
+
+ const char *candidates[12];
+ int candidate_count = 0;
+
+ char bold_candidate[140];
+ if ( is_bold && !strstr( normalized, "bold" ) ) {
+ snprintf( bold_candidate, sizeof(bold_candidate), "%sbold", normalized );
+ candidates[candidate_count++] = bold_candidate;
+ }
+
+ candidates[candidate_count++] = normalized;
+
+ // Add common Liberation and standard aliases (exact normalized matches)
+ // GeneralsX @bugfix felipebraz 26/09/2026 Use exact alias checks to prevent matching wide-coverage fonts like Arial Unicode MS
+ bool is_arial = (strcmp( normalized, "arial" ) == 0 ||
+ strcmp( normalized, "arialbold" ) == 0 ||
+ strcmp( normalized, "generals" ) == 0 ||
+ strcmp( normalized, "generalsbold" ) == 0 ||
+ strcmp( normalized, "liberationsans" ) == 0 ||
+ strcmp( normalized, "liberationsansbold" ) == 0);
+
+ bool is_times = (strcmp( normalized, "times" ) == 0 ||
+ strcmp( normalized, "timesbold" ) == 0 ||
+ strcmp( normalized, "timesnewroman" ) == 0 ||
+ strcmp( normalized, "timesnewromanbold" ) == 0 ||
+ strcmp( normalized, "liberationserif" ) == 0 ||
+ strcmp( normalized, "liberationserifbold" ) == 0);
+
+ bool is_courier = (strcmp( normalized, "courier" ) == 0 ||
+ strcmp( normalized, "courierbold" ) == 0 ||
+ strcmp( normalized, "couriernew" ) == 0 ||
+ strcmp( normalized, "couriernewbold" ) == 0 ||
+ strcmp( normalized, "liberationmono" ) == 0 ||
+ strcmp( normalized, "liberationmonobold" ) == 0);
+
+ if ( is_arial ) {
+ if ( is_bold ) {
+ candidates[candidate_count++] = "LiberationSans-Bold";
+ candidates[candidate_count++] = "liberationsans-bold";
+ }
+ candidates[candidate_count++] = "LiberationSans-Regular";
+ candidates[candidate_count++] = "liberationsans-regular";
+ candidates[candidate_count++] = "LiberationSans";
+ } else if ( is_times ) {
+ if ( is_bold ) {
+ candidates[candidate_count++] = "LiberationSerif-Bold";
+ candidates[candidate_count++] = "liberationserif-bold";
+ }
+ candidates[candidate_count++] = "LiberationSerif-Regular";
+ candidates[candidate_count++] = "liberationserif-regular";
+ candidates[candidate_count++] = "LiberationSerif";
+ } else if ( is_courier ) {
+ if ( is_bold ) {
+ candidates[candidate_count++] = "LiberationMono-Bold";
+ candidates[candidate_count++] = "liberationmono-bold";
+ }
+ candidates[candidate_count++] = "LiberationMono-Regular";
+ candidates[candidate_count++] = "liberationmono-regular";
+ candidates[candidate_count++] = "LiberationMono";
+ }
//
- // Find the best match
+ // Tier 1: Probe local candidate font files via platform abstraction
+ // GeneralsX @refactor felipebraz 26/09/2026 Encapsulate font probing in platform layer
//
- FcResult result = FcResultNoMatch;
- FcPattern *font = FcFontMatch( config, pattern, &result );
+ char candidate_path[1024];
+ if ( Platform::FindLocalFontFile( candidates, candidate_count, candidate_path, sizeof(candidate_path) ) ) {
+ FreetypeFontPath = candidate_path;
+ return FreetypeFontPath;
+ }
- const char *font_path = nullptr;
- if ( font != nullptr && result == FcResultMatch ) {
- //
- // Extract the font file path
- //
- FcChar8 *file_path = nullptr;
- if ( FcPatternGetString( font, FC_FILE, 0, &file_path ) == FcResultMatch ) {
- FreetypeFontPath = (const char*)file_path;
- font_path = FreetypeFontPath;
+ //
+ // Tier 2: System Fontconfig match (desktop Linux/macOS)
+ //
+#if !(defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE)
+ FcConfig *config = FcInitLoadConfigAndFonts();
+ if ( config != nullptr ) {
+ FcPattern *pattern = FcNameParse( (const FcChar8*)font_name );
+ if ( pattern != nullptr ) {
+ FcConfigSubstitute( config, pattern, FcMatchPattern );
+ FcDefaultSubstitute( pattern );
+
+ FcResult result = FcResultNoMatch;
+ FcPattern *font = FcFontMatch( config, pattern, &result );
+ if ( font != nullptr && result == FcResultMatch ) {
+ FcChar8 *file_path = nullptr;
+ if ( FcPatternGetString( font, FC_FILE, 0, &file_path ) == FcResultMatch && file_path != nullptr ) {
+ if ( Platform::IsFileReadable( (const char*)file_path ) ) {
+ FreetypeFontPath = (const char*)file_path;
+ FcPatternDestroy( font );
+ FcPatternDestroy( pattern );
+ FcConfigDestroy( config );
+ return FreetypeFontPath;
+ }
+ }
+ FcPatternDestroy( font );
+ }
+ FcPatternDestroy( pattern );
}
-
- FcPatternDestroy( font );
+ FcConfigDestroy( config );
}
+#endif
- FcPatternDestroy( pattern );
- FcConfigDestroy( config );
+ //
+ // Tier 3: Universal fallback to bundled Arial/Liberation font for recognized core UI fonts
+ // GeneralsX @bugfix felipebraz 26/09/2026 Guard Tier 3 so unresolved Unicode/CJK fonts return nullptr and allow caller fallback delegation
+ //
+ if ( is_arial || is_times || is_courier ) {
+ static const char *fallback_names_bold[] = {
+ "arialbold.ttf",
+ "arial.ttf",
+ "LiberationSans-Bold.ttf",
+ "LiberationSans-Regular.ttf"
+ };
+ static const char *fallback_names_reg[] = {
+ "arial.ttf",
+ "LiberationSans-Regular.ttf"
+ };
+ const char *const *fnames = is_bold ? fallback_names_bold : fallback_names_reg;
+ int fcount = is_bold ? 4 : 2;
+ if ( Platform::FindLocalFontFile( fnames, fcount, candidate_path, sizeof(candidate_path) ) ) {
+ fprintf( stderr, "[Font] Note: Font '%s' not found; falling back to bundled '%s'\n", font_name, candidate_path );
+ fflush( stderr );
+ FreetypeFontPath = candidate_path;
+ return FreetypeFontPath;
+ }
+ }
- return font_path;
+ fprintf( stderr, "[Font] WARNING: Unable to resolve font '%s' (all tiers exhausted)\n", font_name );
+ fflush( stderr );
+ return nullptr;
}
diff --git a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
index 6f5af9f45fe..a122acd5704 100644
--- a/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
+++ b/Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.h
@@ -44,10 +44,16 @@
#include "WWLib/win.h"
// GeneralsX @build fbraz 11/02/2026 BenderAI - FreeType2 for Linux text rendering (Phase 1.5)
+// GeneralsX @feature felipebraz 26/09/2026 Guard fontconfig header for platforms without fontconfig (e.g. iOS)
#if defined(SAGE_USE_FREETYPE) && !defined(_WIN32)
#include
#include FT_FREETYPE_H
- #include
+ #if defined(__APPLE__)
+ #include
+ #endif
+ #if !(defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE)
+ #include
+ #endif
#endif
/*
diff --git a/assets/fonts/LICENSE.fontawesome b/assets/fonts/LICENSE.fontawesome
new file mode 100644
index 00000000000..e69c5e39a30
--- /dev/null
+++ b/assets/fonts/LICENSE.fontawesome
@@ -0,0 +1,165 @@
+Fonticons, Inc. (https://fontawesome.com)
+
+--------------------------------------------------------------------------------
+
+Font Awesome Free License
+
+Font Awesome Free is free, open source, and GPL friendly. You can use it for
+commercial projects, open source projects, or really almost whatever you want.
+Full Font Awesome Free license: https://fontawesome.com/license/free.
+
+--------------------------------------------------------------------------------
+
+# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
+
+The Font Awesome Free download is licensed under a Creative Commons
+Attribution 4.0 International License and applies to all icons packaged
+as SVG and JS file types.
+
+--------------------------------------------------------------------------------
+
+# Fonts: SIL OFL 1.1 License
+
+In the Font Awesome Free download, the SIL OFL license applies to all icons
+packaged as web and desktop font files.
+
+Copyright (c) 2024 Fonticons, Inc. (https://fontawesome.com)
+with Reserved Font Name: "Font Awesome".
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+SIL OPEN FONT LICENSE
+Version 1.1 - 26 February 2007
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting — in part or in whole — any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
+
+--------------------------------------------------------------------------------
+
+# Code: MIT License (https://opensource.org/licenses/MIT)
+
+In the Font Awesome Free download, the MIT license applies to all non-font and
+non-icon files.
+
+Copyright 2024 Fonticons, Inc.
+
+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.
+
+--------------------------------------------------------------------------------
+
+# Attribution
+
+Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
+Awesome Free files already contain embedded comments with sufficient
+attribution, so you shouldn't need to do anything additional when using these
+files normally.
+
+We've kept attribution comments terse, so we ask that you do not actively work
+to remove them from files, especially code. They're a great way for folks to
+learn about Font Awesome.
+
+--------------------------------------------------------------------------------
+
+# Brand Icons
+
+All brand icons are trademarks of their respective owners. The use of these
+trademarks does not indicate endorsement of the trademark holder by Font
+Awesome, nor vice versa. **Please do not use brand logos for any purpose except
+to represent the company, product, or service to which they refer.**
diff --git a/assets/fonts/LICENSE.liberation b/assets/fonts/LICENSE.liberation
new file mode 100644
index 00000000000..aba73e8a403
--- /dev/null
+++ b/assets/fonts/LICENSE.liberation
@@ -0,0 +1,102 @@
+Digitized data copyright (c) 2010 Google Corporation
+ with Reserved Font Arimo, Tinos and Cousine.
+Copyright (c) 2012 Red Hat, Inc.
+ with Reserved Font Name Liberation.
+
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+
+PREAMBLE The goals of the Open Font License (OFL) are to stimulate
+worldwide development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to provide
+a free and open framework in which fonts may be shared and improved in
+partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves.
+The fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such.
+This may include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components
+as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting ? in part or in whole ?
+any of the components of the Original Version, by changing formats or
+by porting the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical writer
+or other person who contributed to the Font Software.
+
+
+PERMISSION & CONDITIONS
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,in
+ Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+ redistributed and/or sold with any software, provided that each copy
+ contains the above copyright notice and this license. These can be
+ included either as stand-alone text files, human-readable headers or
+ in the appropriate machine-readable metadata fields within text or
+ binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+ Name(s) unless explicit written permission is granted by the
+ corresponding Copyright Holder. This restriction only applies to the
+ primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+ Software shall not be used to promote, endorse or advertise any
+ Modified Version, except to acknowledge the contribution(s) of the
+ Copyright Holder(s) and the Author(s) or with their explicit written
+ permission.
+
+5) The Font Software, modified or unmodified, in part or in whole, must
+ be distributed entirely under this license, and must not be distributed
+ under any other license. The requirement for fonts to remain under
+ this license does not apply to any document created using the Font
+ Software.
+
+
+
+TERMINATION
+This license becomes null and void if any of the above conditions are not met.
+
+
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
+DEALINGS IN THE FONT SOFTWARE.
+
diff --git a/assets/fonts/arial.ttf b/assets/fonts/arial.ttf
new file mode 100644
index 00000000000..e6339859d0b
Binary files /dev/null and b/assets/fonts/arial.ttf differ
diff --git a/assets/fonts/arialbold.ttf b/assets/fonts/arialbold.ttf
new file mode 100644
index 00000000000..dc5d57f15f0
Binary files /dev/null and b/assets/fonts/arialbold.ttf differ
diff --git a/assets/fonts/couriernew.ttf b/assets/fonts/couriernew.ttf
new file mode 100644
index 00000000000..e774859cba4
Binary files /dev/null and b/assets/fonts/couriernew.ttf differ
diff --git a/assets/fonts/fa-brands-400.ttf b/assets/fonts/fa-brands-400.ttf
index d05c969ab38..1fbb1f7c32d 100644
Binary files a/assets/fonts/fa-brands-400.ttf and b/assets/fonts/fa-brands-400.ttf differ
diff --git a/assets/fonts/timesnewroman.ttf b/assets/fonts/timesnewroman.ttf
new file mode 100644
index 00000000000..5e5550c0afc
Binary files /dev/null and b/assets/fonts/timesnewroman.ttf differ
diff --git a/docs/WORKLOG/2026-09-DIARY.md b/docs/WORKLOG/2026-09-DIARY.md
index a052820b054..7a72c962fbf 100644
--- a/docs/WORKLOG/2026-09-DIARY.md
+++ b/docs/WORKLOG/2026-09-DIARY.md
@@ -3,6 +3,54 @@
> [!NOTE]
> **AI-Generated Content Disclosure**: This worklog is automatically generated and maintained by AI coding agents to document daily progress, debugging sessions, and technical decisions.
+## 26/09/2026
+### Universal Font Resolution & Bundled Liberation Fonts
+- **Context**: The game engine previously relied exclusively on system `Fontconfig` on non-Win32 platforms (Linux and macOS) to locate fonts. When system fonts (such as Microsoft's proprietary `Arial`) were missing or Fontconfig configuration was absent (e.g. clean macOS installations, stripped Linux/Flatpak environments, or iOS), font resolution failed and resulted in missing in-game text or startup crashes.
+- **Root Cause & Licensing**:
+ - Proprietary Microsoft Windows fonts (`Arial.ttf`, `Times New Roman`, etc.) cannot be legally redistributed with GeneralsX releases.
+ - The iOS port (`ammaarreshi/Generals-Mac-iOS-iPad`) solved this by bundling **Liberation Fonts v2.1.5** (SIL Open Font License 1.1), which are metric-compatible replacements for Arial, Times New Roman, and Courier New with wide multilingual support (Latin, Cyrillic, Greek).
+- **Changes**:
+ - **Asset Staging**:
+ - Added bundled Liberation Fonts into `assets/fonts/`: `arial.ttf` (LiberationSans-Regular), `arialbold.ttf` (LiberationSans-Bold), `couriernew.ttf` (LiberationMono-Regular), `timesnewroman.ttf` (LiberationSerif-Regular), and `LICENSE.liberation` (OFL 1.1).
+ - Created `scripts/env/setup-fonts.sh` to download, verify (SHA-256), and stage Liberation fonts if needed.
+ - **Multi-Tiered Font Resolution (`Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp`)**:
+ - **Tier 1 (Bundled/Local)**: Probes local `fonts/` directory across runtime paths (`fonts/`, `./fonts/`, `Resources/fonts/`, `../Resources/fonts/`, `assets/fonts/`, `$CNC_GENERALS_ZH_PATH/fonts/`, `$CNC_GENERALS_PATH/fonts/`, `$HOME/GeneralsX/...`, `/app/share/fonts/`). Normalizes font names (lowercase, strips spaces/hyphens) and checks bold variants (`arialbold.ttf`) and Liberation aliases.
+ - **Tier 2 (System Fontconfig)**: If local search yields no match and Fontconfig is supported on the target platform, queries Fontconfig for system-installed fonts.
+ - **Tier 3 (Universal Fallback)**: If Fontconfig fails or is disabled/unavailable, falls back to bundled `arial.ttf` (or `arialbold.ttf` if bold) to guarantee text rendering under all circumstances.
+ - **Fontconfig Isolation (`render2dsentence.h`)**:
+ - Guarded `` with `TargetConditionals.h` to skip Fontconfig on iOS/iPadOS while keeping desktop macOS and Linux intact.
+ - **Unicode Fallback (`Core/GameEngineDevice/Source/W3DDevice/GameClient/GUI/W3DGameFont.cpp`)**:
+ - Added `Liberation Sans` and `Liberation Serif` to `kFallbackUnicodeFonts`. Excluded `Liberation Sans` from `kFullCoverageFonts` so CJK text can cleanly delegate to an alternate Unicode font.
+ - **Packaging & Bundling Updates**:
+ - Updated `scripts/build/macos/bundle-macos-zh.sh`, `scripts/build/macos/bundle-macos-generals.sh`, and `deploy-*.sh` scripts to copy `assets/fonts/*.ttf` and `LICENSE.liberation` into runtime/resources `fonts/`.
+ - Updated `flatpak/com.fbraz3.GeneralsXZH.yml` and `flatpak/com.fbraz3.GeneralsX.yml` to install `assets/fonts/*.ttf` and `LICENSE.liberation` into `/app/share/fonts/`.
+ - **CodeRabbit Review Findings Resolution**:
+ - Replaced substring matching (`strstr(normalized, "arial")`) with exact normalized alias checks for Arial, Times, and Courier families in `Core/Libraries/Source/WWVegas/WW3D2/render2dsentence.cpp`. This prevents wide-coverage font requests such as `Arial Unicode MS` from being hijacked in Tier 1.
+ - Guarded Tier 3 universal fallback to recognized core UI aliases (`is_arial || is_times || is_courier`), so unresolved Unicode/CJK requests return `nullptr` and allow caller fallback delegation (such as `LoadUnicodeFallbackFont()`).
+ - Resolved macOS application bundle's `Contents/Resources/fonts` dynamically at runtime via `_NSGetExecutablePath` and exported `GX_BUNDLE_FONTS` in `bundle-macos-zh.sh` and `bundle-macos-generals.sh` launchers, ensuring fonts are found even when the launcher changes CWD to external game data.
+ - Isolated macOS executable and bundle path resolution into a dedicated platform layer helper (`Core/Libraries/Source/Platform/PlatformPaths.h` and `corei_platform` CMake interface target), removing raw `_NSGetExecutablePath` and `` calls from the renderer.
+ - Enforced platform isolation by encapsulating local font search directories, candidate probing, environment variable queries (`GX_BUNDLE_FONTS`, `CNC_GENERALS_ZH_PATH`, `CNC_GENERALS_PATH`, `HOME`), and file readability checks (`Platform::IsFileReadable()`) in `PlatformPaths.h` under `namespace Platform`. Removed direct ``, `getenv()`, and `access()` calls from `render2dsentence.cpp`.
+ - Enforced strict error handling in macOS bundling and Linux/macOS deployment scripts (`deploy-linux.sh`, `deploy-linux-zh.sh`, `deploy-macos-generals.sh`, `deploy-macos-zh.sh`, `bundle-macos-zh.sh`, `bundle-macos-generals.sh`), removing output suppression and `|| true` on font and license copying so missing files fail fast.
+ - **Font Awesome Brands Staging & Licensing**:
+ - Verified and staged clean binary `assets/fonts/fa-brands-400.ttf` (Font Awesome 6 Free Brands, format 4 cmap covering Apple `0xF179`, Windows `0xF17A`, Linux `0xF17C`, Discord `0xF392`, Steam `0xF1B6`, GitHub `0xF09B`).
+ - Staged `assets/fonts/LICENSE.fontawesome` (SIL OFL 1.1).
+ - Hardened `scripts/env/setup-fonts.sh` with atomic staging via temporary directory and fixed SHA-256 integrity verification (`sha256_verify`) for both Liberation fonts/license and Font Awesome assets (pinned to official Font Awesome v6.5.2 release archive fallback). Replaced superficial existence checks with full content hash verification (`is_valid_liberation` and `is_valid_fa`) so truncated, modified, or corrupt files trigger automated recovery and restaging.
+ - **Packaging & Deploy Scripts Parity**:
+ - Updated `scripts/build/macos/deploy-macos-zh.sh`, `scripts/build/macos/deploy-macos-generals.sh`, `scripts/build/linux/deploy-linux-zh.sh`, `scripts/build/linux/deploy-linux.sh`, `scripts/build/macos/bundle-macos-zh.sh`, and `scripts/build/macos/bundle-macos-generals.sh` to explicitly require and copy both `LICENSE.liberation` and `LICENSE.fontawesome` alongside all `*.ttf` fonts.
+ - Updated Flatpak manifests (`flatpak/com.fbraz3.GeneralsX.yml`, `flatpak/com.fbraz3.GeneralsXZH.yml`) to copy both `LICENSE.liberation` and `LICENSE.fontawesome` into `/app/share/fonts/`.
+ - Updated Windows CI workflow (`.github/workflows/build-windows.yml`) to validate presence of both license files and package `assets/fonts/` into Windows release and CI artifacts.
+ - Updated `scripts/env/setup-fonts.sh` to require both license files in completion checks and restore each license independently even if fonts are present.
+ - **Flatpak GCC 15 Build Fix & CI Run 36269858774 Investigation**:
+ - Investigated GitHub Actions run 36269858774: verified Windows builds passed completely (both Zero Hour and base game compiled cleanly and passed replay tests). The CI failure was isolated to Linux Flatpak (`org.freedesktop.Sdk 25.08` with GCC 15.2.0).
+ - Diagnosed GCC 15 compilation failure in `` (`__msg_inc_istreambuf is not a member of __gnu_debug`): identified that `target_include_directories(corei_platform INTERFACE "Libraries/Source")` linked into `corei_always` added `Core/Libraries/Source` to the include path for all targets. When libstdc++ included ``, GCC resolved `Core/Libraries/Source/debug/debug.h` instead of the standard library header, shadowing `__gnu_debug`.
+ - Isolated `corei_platform` include directory to `Libraries/Source/Platform`, removed `corei_platform` from global `corei_always`, and included `"PlatformPaths.h"` in `render2dsentence.cpp`.
+- **Validation**:
+ - Compiled both `z_generals` (GeneralsMD/Zero Hour) and `g_generals` (Generals base game) cleanly with 0 errors.
+ - Deployed to `~/GeneralsX/GeneralsZH/` and `~/GeneralsX/Generals/` and verified all font assets are copied.
+ - Verified `./scripts/env/setup-fonts.sh` with a temporary directory, confirming SHA-256 verification and atomic staging for all fonts and licenses.
+ - Tested headless replay execution with `FONTCONFIG_FILE=""` to simulate a system without Fontconfig; verified clean initialization and asset loading.
+ - Replied to all CodeRabbit review comments on PR #336 via GitHub API.
+
## 25/09/2026
### Saturate Out-of-Range INI Integers to Match Retail Mod Compatibility (#297)
- **Context**: In issue #297, Shockwave mod 1.201 failed to load with `[INI] Cannot parse numeric token '9999999999999999999': out of range` on `SpawnReplaceDelay` in `MinigunnerSquad.ini` and `RecenterTime` in `HellStorm.ini`.
diff --git a/flatpak/com.fbraz3.GeneralsX.yml b/flatpak/com.fbraz3.GeneralsX.yml
index c32d4346e57..cb89576bf0e 100644
--- a/flatpak/com.fbraz3.GeneralsX.yml
+++ b/flatpak/com.fbraz3.GeneralsX.yml
@@ -106,6 +106,8 @@ modules:
- install -Dm644 flatpak/com.fbraz3.GeneralsX.metainfo.xml /app/share/metainfo/com.fbraz3.GeneralsX.metainfo.xml
- install -Dm644 flatpak/generalsx_icon_512.png /app/share/icons/hicolor/512x512/apps/com.fbraz3.GeneralsX.png
- install -Dm644 resources/dxvk/dxvk.conf /app/share/generalsx/dxvk.conf
+ - install -d /app/share/fonts
+ - if [ -d assets/fonts ]; then cp assets/fonts/*.ttf assets/fonts/LICENSE.liberation assets/fonts/LICENSE.fontawesome /app/share/fonts/; fi
- install -d /app/lib
- cp -a /run/build/generalsx/dxvk-src/lib/libdxvk_d3d8.so* /app/lib/
- cp -a /run/build/generalsx/dxvk-src/lib/libdxvk_d3d9.so* /app/lib/
diff --git a/flatpak/com.fbraz3.GeneralsXZH.yml b/flatpak/com.fbraz3.GeneralsXZH.yml
index 136e6eaf1db..475d03e89c5 100644
--- a/flatpak/com.fbraz3.GeneralsXZH.yml
+++ b/flatpak/com.fbraz3.GeneralsXZH.yml
@@ -106,6 +106,8 @@ modules:
- install -Dm644 flatpak/com.fbraz3.GeneralsXZH.metainfo.xml /app/share/metainfo/com.fbraz3.GeneralsXZH.metainfo.xml
- install -Dm644 flatpak/generalsx-zh_icon_512.png /app/share/icons/hicolor/512x512/apps/com.fbraz3.GeneralsXZH.png
- install -Dm644 resources/dxvk/dxvk.conf /app/share/generalsxzh/dxvk.conf
+ - install -d /app/share/fonts
+ - if [ -d assets/fonts ]; then cp assets/fonts/*.ttf assets/fonts/LICENSE.liberation assets/fonts/LICENSE.fontawesome /app/share/fonts/; fi
- install -d /app/lib
- cp -a /run/build/generalsxzh/dxvk-src/lib/libdxvk_d3d8.so* /app/lib/
- cp -a /run/build/generalsxzh/dxvk-src/lib/libdxvk_d3d9.so* /app/lib/
diff --git a/scripts/build/linux/deploy-linux-zh.sh b/scripts/build/linux/deploy-linux-zh.sh
index a2fc36b39fe..22f1e97b46a 100755
--- a/scripts/build/linux/deploy-linux-zh.sh
+++ b/scripts/build/linux/deploy-linux-zh.sh
@@ -183,7 +183,9 @@ fi
echo " Deploying fonts..."
mkdir -p "${RUNTIME_DIR}/fonts"
if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
- cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/" 2>/dev/null || true
+ cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RUNTIME_DIR}/fonts/"
fi
# Copy run wrapper script
diff --git a/scripts/build/linux/deploy-linux.sh b/scripts/build/linux/deploy-linux.sh
index 97ad874c9bb..7f616907d54 100755
--- a/scripts/build/linux/deploy-linux.sh
+++ b/scripts/build/linux/deploy-linux.sh
@@ -152,7 +152,9 @@ patchelf --set-rpath '$ORIGIN' "${RUNTIME_DIR}/GeneralsX" 2>/dev/null || {
echo " Deploying fonts..."
mkdir -p "${RUNTIME_DIR}/fonts"
if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
- cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/" 2>/dev/null || true
+ cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RUNTIME_DIR}/fonts/"
fi
# Copy run wrapper script
diff --git a/scripts/build/macos/bundle-macos-generals.sh b/scripts/build/macos/bundle-macos-generals.sh
index 0489abf492b..6cc988902f7 100755
--- a/scripts/build/macos/bundle-macos-generals.sh
+++ b/scripts/build/macos/bundle-macos-generals.sh
@@ -343,6 +343,18 @@ else
echo "WARNING: ${DXVK_CONF_SRC} not found - terrain shaders may fail on macOS"
fi
+# GeneralsX @feature felipebraz 26/09/2026 Bundle universal fonts into application resources.
+mkdir -p "${RESOURCES_DIR}/fonts"
+if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
+ echo " + Bundled fonts"
+ cp "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RESOURCES_DIR}/fonts/"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RESOURCES_DIR}/fonts/"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RESOURCES_DIR}/fonts/"
+else
+ echo "ERROR: ${PROJECT_ROOT}/assets/fonts directory not found - cannot bundle fonts" >&2
+ exit 1
+fi
+
# App launcher wrapper
echo " + App launcher"
cat > "${MACOS_DIR}/run.sh" << 'WRAPPER'
@@ -389,6 +401,11 @@ if [[ -f "${RESOURCES_DIR}/dxvk.conf" ]]; then
export DXVK_CONFIG_FILE="${RESOURCES_DIR}/dxvk.conf"
fi
+# GeneralsX @bugfix felipebraz 26/09/2026 Export GX_BUNDLE_FONTS so engine resolves staged fonts when CWD changes to asset root.
+if [[ -d "${RESOURCES_DIR}/fonts" ]]; then
+ export GX_BUNDLE_FONTS="${RESOURCES_DIR}/fonts"
+fi
+
# Run from the detected Generals asset root when available.
if [[ -d "${CNC_GENERALS_PATH}" ]]; then
cd "${CNC_GENERALS_PATH}"
diff --git a/scripts/build/macos/bundle-macos-zh.sh b/scripts/build/macos/bundle-macos-zh.sh
index ee7ac608ad0..9849608bbd2 100755
--- a/scripts/build/macos/bundle-macos-zh.sh
+++ b/scripts/build/macos/bundle-macos-zh.sh
@@ -361,6 +361,18 @@ else
echo "WARNING: ${FONTCONFIG_ETC_DIR}/fonts.conf not found - in-game font lookup may fail on macOS"
fi
+# GeneralsX @feature felipebraz 26/09/2026 Bundle universal fonts into application resources.
+mkdir -p "${RESOURCES_DIR}/fonts"
+if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
+ echo " + Bundled fonts"
+ cp "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RESOURCES_DIR}/fonts/"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RESOURCES_DIR}/fonts/"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RESOURCES_DIR}/fonts/"
+else
+ echo "ERROR: ${PROJECT_ROOT}/assets/fonts directory not found - cannot bundle fonts" >&2
+ exit 1
+fi
+
# App launcher wrapper
echo " + App launcher"
cat > "${MACOS_DIR}/run.sh" << 'WRAPPER'
@@ -434,6 +446,11 @@ if [[ -f "${RESOURCES_DIR}/fontconfig/fonts.conf" ]]; then
export FONTCONFIG_PATH="${RESOURCES_DIR}/fontconfig"
fi
+# GeneralsX @bugfix felipebraz 26/09/2026 Export GX_BUNDLE_FONTS so engine resolves staged fonts when CWD changes to asset root.
+if [[ -d "${RESOURCES_DIR}/fonts" ]]; then
+ export GX_BUNDLE_FONTS="${RESOURCES_DIR}/fonts"
+fi
+
# Run from the detected Zero Hour asset root when available.
if [[ -d "${CNC_GENERALS_ZH_PATH}" ]]; then
cd "${CNC_GENERALS_ZH_PATH}"
diff --git a/scripts/build/macos/deploy-macos-generals.sh b/scripts/build/macos/deploy-macos-generals.sh
index 649b5dcc9c9..b3d59a3218e 100755
--- a/scripts/build/macos/deploy-macos-generals.sh
+++ b/scripts/build/macos/deploy-macos-generals.sh
@@ -114,7 +114,9 @@ fi
echo " Deploying Fontconfig config & fonts..."
mkdir -p "${RUNTIME_DIR}/fonts"
if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
- cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/" 2>/dev/null || true
+ cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RUNTIME_DIR}/fonts/"
fi
FONTCONFIG_ETC_DIR="${BUILD_DIR}/vcpkg_installed/arm64-osx/etc/fonts"
diff --git a/scripts/build/macos/deploy-macos-zh.sh b/scripts/build/macos/deploy-macos-zh.sh
index bc782770172..9eecdc27d4a 100755
--- a/scripts/build/macos/deploy-macos-zh.sh
+++ b/scripts/build/macos/deploy-macos-zh.sh
@@ -166,7 +166,9 @@ fi
echo " Deploying Fontconfig config & fonts..."
mkdir -p "${RUNTIME_DIR}/fonts"
if [[ -d "${PROJECT_ROOT}/assets/fonts" ]]; then
- cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/" 2>/dev/null || true
+ cp -v "${PROJECT_ROOT}/assets/fonts"/*.ttf "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${RUNTIME_DIR}/fonts/"
+ cp -v "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${RUNTIME_DIR}/fonts/"
fi
if [[ -f "${FONTCONFIG_ETC_DIR}/fonts.conf" ]]; then
diff --git a/scripts/env/setup-fonts.sh b/scripts/env/setup-fonts.sh
new file mode 100755
index 00000000000..1c65d52968c
--- /dev/null
+++ b/scripts/env/setup-fonts.sh
@@ -0,0 +1,140 @@
+#!/usr/bin/env bash
+# Stage and verify universal Liberation and Font Awesome fonts for GeneralsX
+#
+# Usage:
+# ./scripts/env/setup-fonts.sh [DEST_DIR]
+#
+# Environment:
+# GX_FONTS Destination directory for fonts (default: assets/fonts)
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+
+LIB_VERSION="2.1.5"
+LIB_SHA256="7191c669bf38899f73a2094ed00f7b800553364f90e2637010a69c0e268f25d0"
+LIB_ARIAL_SHA256="76d04c18ea243f426b7de1f3ad208e927008f961dc5945e5aad352d0dfde8ee8"
+LIB_ARIALBOLD_SHA256="788abee4c806d660e8aee46689dd8540cd4bb98da03dcc9d171ce3efd99a9173"
+LIB_COURIER_SHA256="f2b83c763e8afd21709333370bed4774337fae82267937e2b5aea7e2fbd922c1"
+LIB_TIMES_SHA256="058ea80864aef09a23f45cbec2bb5400bc3dfbdea01c3f10538a21fcb497fb74"
+LIB_LIC_SHA256="93fed46019c38bbe566b479d22148e2e8a1e85ada614accb0211c37b2c61c19b"
+
+FA_VERSION="6.5.2"
+FA_SHA256="e28096fa75a96ac77020155ea3a6dd7312983e84115366d4cf49a0c312ec6d51"
+FA_LIC_SHA256="9b914eae88817d63b576eab5aafde7068c7a1abae125d7cdfb034f1da43a9afc"
+
+DEST="${1:-${GX_FONTS:-${PROJECT_ROOT}/assets/fonts}}"
+TMP="$(mktemp -d)"
+trap 'rm -rf "${TMP}"' EXIT
+
+sha256_verify() {
+ local checksum="$1"
+ local file="$2"
+ if command -v sha256sum >/dev/null 2>&1; then
+ echo "${checksum} ${file}" | sha256sum -c -
+ else
+ echo "${checksum} ${file}" | shasum -a 256 -c -
+ fi
+}
+
+mkdir -p "${DEST}"
+
+is_valid_fa() {
+ [[ -f "${DEST}/fa-brands-400.ttf" ]] && \
+ [[ -f "${DEST}/LICENSE.fontawesome" ]] && \
+ sha256_verify "${FA_SHA256}" "${DEST}/fa-brands-400.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${FA_LIC_SHA256}" "${DEST}/LICENSE.fontawesome" >/dev/null 2>&1
+}
+
+is_valid_liberation() {
+ [[ -f "${DEST}/arial.ttf" ]] && \
+ [[ -f "${DEST}/arialbold.ttf" ]] && \
+ [[ -f "${DEST}/couriernew.ttf" ]] && \
+ [[ -f "${DEST}/timesnewroman.ttf" ]] && \
+ [[ -f "${DEST}/LICENSE.liberation" ]] && \
+ sha256_verify "${LIB_ARIAL_SHA256}" "${DEST}/arial.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_ARIALBOLD_SHA256}" "${DEST}/arialbold.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_COURIER_SHA256}" "${DEST}/couriernew.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_TIMES_SHA256}" "${DEST}/timesnewroman.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_LIC_SHA256}" "${DEST}/LICENSE.liberation" >/dev/null 2>&1
+}
+
+if is_valid_liberation && is_valid_fa; then
+ echo "==> Fonts and licenses already present and verified at ${DEST}"
+ exit 0
+fi
+
+if ! is_valid_liberation; then
+ if [[ -f "${PROJECT_ROOT}/assets/fonts/arial.ttf" ]] && \
+ sha256_verify "${LIB_ARIAL_SHA256}" "${PROJECT_ROOT}/assets/fonts/arial.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_ARIALBOLD_SHA256}" "${PROJECT_ROOT}/assets/fonts/arialbold.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_COURIER_SHA256}" "${PROJECT_ROOT}/assets/fonts/couriernew.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_TIMES_SHA256}" "${PROJECT_ROOT}/assets/fonts/timesnewroman.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${LIB_LIC_SHA256}" "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" >/dev/null 2>&1; then
+ echo "==> Staging Liberation fonts from repository assets/"
+ cp "${PROJECT_ROOT}/assets/fonts/arial.ttf" "${TMP}/arial.ttf"
+ cp "${PROJECT_ROOT}/assets/fonts/arialbold.ttf" "${TMP}/arialbold.ttf"
+ cp "${PROJECT_ROOT}/assets/fonts/couriernew.ttf" "${TMP}/couriernew.ttf"
+ cp "${PROJECT_ROOT}/assets/fonts/timesnewroman.ttf" "${TMP}/timesnewroman.ttf"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.liberation" "${TMP}/LICENSE.liberation"
+ else
+ echo "==> Downloading Liberation fonts ${LIB_VERSION}"
+ if ! curl -fL -o "${TMP}/liberation.tar.gz" \
+ "https://github.com/liberationfonts/liberation-fonts/files/7261482/liberation-fonts-ttf-${LIB_VERSION}.tar.gz" &&
+ ! curl -fL -o "${TMP}/liberation.tar.gz" \
+ "https://github.com/liberationfonts/liberation-fonts/releases/download/${LIB_VERSION}/liberation-fonts-ttf-${LIB_VERSION}.tar.gz"; then
+ echo "ERROR: Could not download Liberation fonts ${LIB_VERSION} (check network / URLs)." >&2
+ exit 1
+ fi
+
+ sha256_verify "${LIB_SHA256}" "${TMP}/liberation.tar.gz"
+ tar -xzf "${TMP}/liberation.tar.gz" -C "${TMP}"
+ SRC="$(find "${TMP}" -name "LiberationSans-Regular.ttf" -exec dirname {} \; | head -1)"
+ [[ -n "${SRC}" ]] || { echo "ERROR: Liberation fonts not found in extracted archive" >&2; exit 1; }
+
+ cp "${SRC}/LiberationSans-Regular.ttf" "${TMP}/arial.ttf"
+ cp "${SRC}/LiberationSans-Bold.ttf" "${TMP}/arialbold.ttf"
+ cp "${SRC}/LiberationMono-Regular.ttf" "${TMP}/couriernew.ttf"
+ cp "${SRC}/LiberationSerif-Regular.ttf" "${TMP}/timesnewroman.ttf"
+ cp "${SRC}/LICENSE" "${TMP}/LICENSE.liberation"
+ fi
+
+ sha256_verify "${LIB_ARIAL_SHA256}" "${TMP}/arial.ttf"
+ sha256_verify "${LIB_ARIALBOLD_SHA256}" "${TMP}/arialbold.ttf"
+ sha256_verify "${LIB_COURIER_SHA256}" "${TMP}/couriernew.ttf"
+ sha256_verify "${LIB_TIMES_SHA256}" "${TMP}/timesnewroman.ttf"
+ sha256_verify "${LIB_LIC_SHA256}" "${TMP}/LICENSE.liberation"
+
+ cp "${TMP}/arial.ttf" "${DEST}/arial.ttf"
+ cp "${TMP}/arialbold.ttf" "${DEST}/arialbold.ttf"
+ cp "${TMP}/couriernew.ttf" "${DEST}/couriernew.ttf"
+ cp "${TMP}/timesnewroman.ttf" "${DEST}/timesnewroman.ttf"
+ cp "${TMP}/LICENSE.liberation" "${DEST}/LICENSE.liberation"
+fi
+
+if ! is_valid_fa; then
+ if [[ -f "${PROJECT_ROOT}/assets/fonts/fa-brands-400.ttf" && -f "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" ]] && \
+ sha256_verify "${FA_SHA256}" "${PROJECT_ROOT}/assets/fonts/fa-brands-400.ttf" >/dev/null 2>&1 && \
+ sha256_verify "${FA_LIC_SHA256}" "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" >/dev/null 2>&1; then
+ echo "==> Staging Font Awesome assets from repository assets/"
+ cp "${PROJECT_ROOT}/assets/fonts/fa-brands-400.ttf" "${TMP}/fa-brands-400.ttf"
+ cp "${PROJECT_ROOT}/assets/fonts/LICENSE.fontawesome" "${TMP}/LICENSE.fontawesome"
+ else
+ echo "==> Downloading Font Awesome ${FA_VERSION} web archive"
+ if ! curl -fL -o "${TMP}/fa-web.zip" \
+ "https://github.com/FortAwesome/Font-Awesome/releases/download/${FA_VERSION}/fontawesome-free-${FA_VERSION}-web.zip"; then
+ echo "ERROR: Could not download Font Awesome ${FA_VERSION}." >&2
+ exit 1
+ fi
+ unzip -q -j "${TMP}/fa-web.zip" "fontawesome-free-${FA_VERSION}-web/webfonts/fa-brands-400.ttf" -d "${TMP}"
+ unzip -q -j "${TMP}/fa-web.zip" "fontawesome-free-${FA_VERSION}-web/LICENSE.txt" -d "${TMP}"
+ mv "${TMP}/LICENSE.txt" "${TMP}/LICENSE.fontawesome"
+ fi
+
+ sha256_verify "${FA_SHA256}" "${TMP}/fa-brands-400.ttf"
+ sha256_verify "${FA_LIC_SHA256}" "${TMP}/LICENSE.fontawesome"
+ cp "${TMP}/fa-brands-400.ttf" "${DEST}/fa-brands-400.ttf"
+ cp "${TMP}/LICENSE.fontawesome" "${DEST}/LICENSE.fontawesome"
+fi
+
+echo "==> Staged universal fonts to ${DEST}"