Modding: allow mods to supply custom fonts (#8715)

* Modding: allow mods to supply custom fonts

* Cleanup

* Code cleanup

---------

Co-authored-by: vegeta1k95 <vfylfhby>
This commit is contained in:
vegeta1k95
2023-02-21 22:09:11 +01:00
committed by GitHub
parent 96fdbbff09
commit c593056e42
10 changed files with 217 additions and 101 deletions

View File

@ -19,7 +19,6 @@ import com.unciv.logic.files.UncivFiles
import com.unciv.logic.event.EventBus
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.Fonts
import com.unciv.utils.Log
import com.unciv.utils.concurrency.Concurrency
import java.io.File
@ -41,7 +40,6 @@ open class AndroidLauncher : AndroidApplication() {
}
val settings = UncivFiles.getSettingsForPlatformLaunchers(filesDir.path)
val fontFamily = settings.fontFamily
// Manage orientation lock and display cutout
val platformSpecificHelper = PlatformSpecificHelpersAndroid(this)
@ -51,7 +49,7 @@ open class AndroidLauncher : AndroidApplication() {
val androidParameters = UncivGameParameters(
crashReportSysInfo = CrashReportSysInfoAndroid,
fontImplementation = NativeFontAndroid((Fonts.ORIGINAL_FONT_SIZE * settings.fontSizeMultiplier).toInt(), fontFamily),
fontImplementation = FontAndroid(),
customFileLocationHelper = customFileLocationHelper,
platformSpecificHelper = platformSpecificHelper
)

View File

@ -9,29 +9,84 @@ import android.graphics.fonts.FontFamily
import android.graphics.fonts.FontStyle
import android.graphics.fonts.SystemFonts
import android.os.Build
import androidx.annotation.RequiresApi
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.NativeFontImplementation
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.Fonts
import java.util.*
import kotlin.math.abs
/**
* Created by tian on 2016/10/2.
*/
class NativeFontAndroid(
private val size: Int,
private val fontFamily: String
) : NativeFontImplementation {
private val fontList by lazy{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) emptySet()
else SystemFonts.getAvailableFonts()
class FontAndroid : FontImplementation {
private val fontList: HashSet<Font> = hashSetOf()
private val paint: Paint = Paint()
private var currentFontFamily: String? = null
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
fontList.addAll(SystemFonts.getAvailableFonts())
paint.isAntiAlias = true
paint.strokeWidth = 0f
paint.setARGB(255, 255, 255, 255)
}
private val paint by lazy{ createPaint() }
fun createPaint() = Paint().apply {
typeface = if (fontFamily.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Helper within the VERSION_CODES.Q gate: Evaluate a Font's desirability (lower = better) for a given family.
fun Font.matchesFamily(family: String): Int {
override fun setFontFamily(fontFamilyData: FontFamilyData, size: Int) {
paint.textSize = size.toFloat()
// Don't have to reload typeface if font-family didn't change
if (currentFontFamily != fontFamilyData.invariantName) {
currentFontFamily = fontFamilyData.invariantName
// Mod font
if (fontFamilyData.filePath != null)
{
paint.typeface = createTypefaceCustom(fontFamilyData.filePath!!)
}
// System font
else
{
paint.typeface = createTypefaceSystem(fontFamilyData.invariantName)
}
}
}
private fun createTypefaceSystem(name: String): Typeface {
if (name.isNotBlank() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
val font = fontList.mapNotNull {
val distanceToRegular = it.matchesFamily(name)
if (distanceToRegular == Int.MAX_VALUE) null else it to distanceToRegular
}.minByOrNull { it.second }?.first
if (font != null)
{
return Typeface.CustomFallbackBuilder(FontFamily.Builder(font).build())
.setSystemFallback(name).build()
}
}
return Typeface.create(name, Typeface.NORMAL)
}
private fun createTypefaceCustom(path: String): Typeface {
return try
{
Typeface.createFromFile(Gdx.files.local(path).file())
}
catch (e: Exception)
{
// Falling back to default
Typeface.create(Fonts.DEFAULT_FONT_FAMILY, Typeface.NORMAL)
}
}
/** Helper within the VERSION_CODES.Q gate: Evaluate a Font's desirability (lower = better) for a given family. */
@RequiresApi(Build.VERSION_CODES.Q)
private fun Font.matchesFamily(family: String): Int {
val name = file?.nameWithoutExtension ?: return Int.MAX_VALUE
if (name == family) return 0
if (!name.startsWith("$family-")) return Int.MAX_VALUE
@ -40,24 +95,9 @@ class NativeFontAndroid(
abs(style.weight - FontStyle.FONT_WEIGHT_NORMAL) / 100 +
abs(style.slant - FontStyle.FONT_SLANT_UPRIGHT)
}
val font = fontList.mapNotNull {
val distanceToRegular = it.matchesFamily(fontFamily)
if (distanceToRegular == Int.MAX_VALUE) null else it to distanceToRegular
}.minByOrNull { it.second }?.first
if (font != null) {
Typeface.CustomFallbackBuilder(FontFamily.Builder(font).build())
.setSystemFallback(fontFamily).build()
} else Typeface.create(fontFamily, Typeface.NORMAL)
} else Typeface.create(fontFamily, Typeface.NORMAL)
isAntiAlias = true
textSize = size.toFloat()
strokeWidth = 0f
setARGB(255, 255, 255, 255)
}
override fun getFontSize(): Int {
return size
return paint.textSize.toInt()
}
override fun getCharPixmap(char: Char): Pixmap {
@ -65,7 +105,7 @@ class NativeFontAndroid(
var width = paint.measureText(char.toString()).toInt()
var height = (metric.descent - metric.ascent).toInt()
if (width == 0) {
height = size
height = getFontSize()
width = height
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
@ -84,7 +124,7 @@ class NativeFontAndroid(
return pixmap
}
override fun getAvailableFontFamilies(): Sequence<FontFamilyData> {
override fun getSystemFonts(): Sequence<FontFamilyData> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
return sequenceOf(FontFamilyData("sans-serif"), FontFamilyData("serif"), FontFamilyData("mono"))

View File

@ -27,6 +27,7 @@ import com.unciv.ui.audio.MusicController
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.crashhandling.CrashScreen
import com.unciv.ui.crashhandling.wrapCrashHandlingUnit
import com.unciv.ui.images.ImageGetter
@ -73,6 +74,10 @@ object GUI {
return UncivGame.Current.settings
}
fun getFontImpl(): FontImplementation {
return UncivGame.Current.fontImplementation!!
}
fun isWorldLoaded(): Boolean {
return UncivGame.Current.worldScreen != null
}
@ -216,7 +221,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
// Loading available fonts can take a long time on Android phones.
// Therefore we initialize the lazy parameters in the font implementation, while we're in another thread, to avoid ANRs on main thread
fontImplementation?.getCharPixmap('S')
fontImplementation?.setFontFamily(settings.fontFamilyData, settings.getFontSize())
// This stuff needs to run on the main thread because it needs the GL context
launchOnGLThread {

View File

@ -4,11 +4,11 @@ import com.unciv.logic.files.CustomFileLocationHelper
import com.unciv.ui.crashhandling.CrashReportSysInfo
import com.unciv.ui.components.AudioExceptionHelper
import com.unciv.ui.components.GeneralPlatformSpecificHelpers
import com.unciv.ui.components.NativeFontImplementation
import com.unciv.ui.components.FontImplementation
class UncivGameParameters(val crashReportSysInfo: CrashReportSysInfo? = null,
val cancelDiscordEvent: (() -> Unit)? = null,
val fontImplementation: NativeFontImplementation? = null,
val fontImplementation: FontImplementation? = null,
val consoleMode: Boolean = false,
val customFileLocationHelper: CustomFileLocationHelper? = null,
val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null,

View File

@ -6,6 +6,7 @@ import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.multiplayer.FriendList
import com.unciv.models.UncivSound
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.Fonts
import java.text.Collator
import java.time.Duration
@ -96,7 +97,7 @@ class GameSettings {
/** Saves the last successful new game's setup */
var lastGameSetup: GameSetupInfo? = null
var fontFamily: String = Fonts.DEFAULT_FONT_FAMILY
var fontFamilyData: FontFamilyData = FontFamilyData.default
var fontSizeMultiplier: Float = 1f
var enableEasterEggs: Boolean = true
@ -139,6 +140,10 @@ class GameSettings {
}
}
fun getFontSize(): Int {
return (Fonts.ORIGINAL_FONT_SIZE * fontSizeMultiplier).toInt()
}
fun getCurrentLocale(): Locale {
if (locale == null)
updateLocaleFromLanguage()

View File

@ -12,15 +12,24 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion
import com.badlogic.gdx.utils.Array
import com.badlogic.gdx.utils.Disposable
import com.unciv.Constants
import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
interface NativeFontImplementation {
interface FontImplementation {
fun setFontFamily(fontFamilyData: FontFamilyData, size: Int)
fun getFontSize(): Int
fun getCharPixmap(char: Char): Pixmap
fun getAvailableFontFamilies(): Sequence<FontFamilyData>
fun getSystemFonts(): Sequence<FontFamilyData>
fun getBitmapFont(): BitmapFont {
val fontData = NativeBitmapFontData(this)
val font = BitmapFont(fontData, fontData.regions, false)
font.setOwnsTexture(true)
return font
}
}
// If save in `GameSettings` need use invariantFamily.
@ -28,8 +37,13 @@ interface NativeFontImplementation {
// If save localName in `GameSettings` may generate garbled characters by encoding.
class FontFamilyData(
val localName: String,
val invariantName: String = localName
val invariantName: String = localName,
val filePath: String? = null
) {
// For serialization
constructor() : this(default.localName, default.invariantName)
// Implement kotlin equality contract such that _only_ the invariantName field is compared.
override fun equals(other: Any?): Boolean {
return if (other is FontFamilyData) invariantName == other.invariantName
@ -39,16 +53,16 @@ class FontFamilyData(
override fun hashCode() = invariantName.hashCode()
/** For SelectBox usage */
override fun toString() = localName
override fun toString() = localName.tr()
companion object {
val default = FontFamilyData("Default Font".tr(), Fonts.DEFAULT_FONT_FAMILY)
val default = FontFamilyData("Default Font", Fonts.DEFAULT_FONT_FAMILY)
}
}
// This class is loosely based on libgdx's FreeTypeBitmapFontData
class NativeBitmapFontData(
private val fontImplementation: NativeFontImplementation
private val fontImplementation: FontImplementation
) : BitmapFontData(), Disposable {
val regions: Array<TextureRegion>
@ -80,6 +94,8 @@ class NativeBitmapFontData(
// Set space glyph.
val spaceGlyph = getGlyph(' ')
spaceXadvance = spaceGlyph.xadvance.toFloat()
setScale(Constants.defaultFontSize / Fonts.ORIGINAL_FONT_SIZE)
}
override fun getGlyph(ch: Char): Glyph {
@ -167,29 +183,17 @@ object Fonts {
* Do not call from normal code - reset the Skin instead: `BaseScreen.setSkin()`
*/
fun resetFont() {
val fontData = NativeBitmapFontData(UncivGame.Current.fontImplementation!!)
font = BitmapFont(fontData, fontData.regions, false)
font.setOwnsTexture(true)
font.data.setScale(Constants.defaultFontSize / ORIGINAL_FONT_SIZE)
}
/** This resets all cached font data and allows changing the font */
fun resetFont(newFamily: String) {
try {
val fontImplementationClass = UncivGame.Current.fontImplementation!!::class.java
val fontImplementationConstructor = fontImplementationClass.constructors.first()
val newFontImpl = fontImplementationConstructor.newInstance((ORIGINAL_FONT_SIZE * UncivGame.Current.settings.fontSizeMultiplier).toInt(), newFamily)
if (newFontImpl is NativeFontImplementation)
UncivGame.Current.fontImplementation = newFontImpl
} catch (ex: Exception) {}
BaseScreen.setSkin() // calls our resetFont() - needed - the Skin seems to cache glyphs
val settings = GUI.getSettings()
val fontImpl = GUI.getFontImpl()
fontImpl.setFontFamily(settings.fontFamilyData, settings.getFontSize())
font = fontImpl.getBitmapFont()
}
/** Reduce the font list returned by platform-specific code to font families (plain variant if possible) */
fun getAvailableFontFamilyNames(): Sequence<FontFamilyData> {
fun getSystemFonts(): Sequence<FontFamilyData> {
val fontImplementation = UncivGame.Current.fontImplementation
?: return emptySequence()
return fontImplementation.getAvailableFontFamilies()
return fontImplementation.getSystemFonts()
.sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.localName })
}

View File

@ -116,25 +116,57 @@ private fun addFontFamilySelect(table: Table, settings: GameSettings, selectBoxM
// Therefore, _selecting_ an item in a `SelectBox` by an instance of `FontFamilyData` where only the `invariantName` is valid won't work properly.
//
// This is why it's _not_ `fontSelectBox.selected = FontFamilyData(settings.fontFamily)`
val fontToSelect = settings.fontFamily
fontSelectBox.selected = fonts.firstOrNull { it.invariantName == fontToSelect } // will default to first entry if `null` is passed
val fontToSelect = settings.fontFamilyData
fontSelectBox.selected = fonts.firstOrNull { it.invariantName == fontToSelect.invariantName } // will default to first entry if `null` is passed
selectCell.setActor(fontSelectBox).minWidth(selectBoxMinWidth).pad(10f)
fontSelectBox.onChange {
settings.fontFamily = fontSelectBox.selected.invariantName
Fonts.resetFont(settings.fontFamily)
settings.fontFamilyData = fontSelectBox.selected
onFontChange()
}
}
Concurrency.run("Add Font Select") {
// This is a heavy operation and causes ANRs
val fonts = Array<FontFamilyData>().apply {
add(FontFamilyData.default)
for (font in Fonts.getAvailableFontFamilyNames())
add(font)
Concurrency.run("Add Font Select") {
val fonts = Array<FontFamilyData>()
// Add default font
fonts.add(FontFamilyData.default)
// Add mods fonts
val modsDir = Gdx.files.local("mods/")
for (mod in modsDir.list()) {
// Not a dir, continue
if (!mod.isDirectory)
continue
val modFontsDir = mod.child("fonts")
// Mod doesn't have fonts, continue
if (!modFontsDir.exists())
continue
// Find .ttf files and add construct FontFamilyData
for (fontFile in modFontsDir.list()) {
if (fontFile.extension().lowercase() == "ttf") {
fonts.add(
FontFamilyData(
"${fontFile.nameWithoutExtension()} (${mod.name()})",
fontFile.nameWithoutExtension(),
fontFile.path())
)
}
}
}
// Add system fonts
for (font in Fonts.getSystemFonts())
fonts.add(font)
launchOnGLThread { loadFontSelect(fonts, selectCell) }
}
}
@ -154,11 +186,9 @@ private fun addFontSizeMultiplier(
settings.save()
}
fontSizeSlider.onChange {
if (!fontSizeSlider.isDragging) {
Fonts.resetFont(settings.fontFamily)
if (!fontSizeSlider.isDragging)
onFontChange()
}
}
table.add(fontSizeSlider).pad(5f).row()
}

View File

@ -15,7 +15,6 @@ import com.unciv.logic.files.SETTINGS_FILE_NAME
import com.unciv.logic.files.UncivFiles
import com.unciv.models.metadata.ScreenSize
import com.unciv.models.metadata.WindowState
import com.unciv.ui.components.Fonts
import com.unciv.utils.Log
import com.unciv.utils.debug
import java.awt.GraphicsEnvironment
@ -73,7 +72,7 @@ internal object DesktopLauncher {
val platformSpecificHelper = PlatformSpecificHelpersDesktop(config)
val desktopParameters = UncivGameParameters(
cancelDiscordEvent = { discordTimer?.cancel() },
fontImplementation = NativeFontDesktop((Fonts.ORIGINAL_FONT_SIZE * settings.fontSizeMultiplier).toInt(), settings.fontFamily),
fontImplementation = FontDesktop(),
customFileLocationHelper = CustomFileLocationHelperDesktop(),
crashReportSysInfo = CrashReportSysInfoDesktop(),
platformSpecificHelper = platformSpecificHelper,

View File

@ -1,35 +1,66 @@
package com.unciv.app.desktop
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Pixmap
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.NativeFontImplementation
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.Fonts
import java.awt.*
import java.awt.image.BufferedImage
import java.util.*
class NativeFontDesktop(private val size: Int, private val fontFamily: String) :
NativeFontImplementation {
private val font by lazy {
Font(fontFamily, Font.PLAIN, size)
class FontDesktop : FontImplementation {
private lateinit var font: Font
private lateinit var metric: FontMetrics
override fun setFontFamily(fontFamilyData: FontFamilyData, size: Int) {
// Mod font
if (fontFamilyData.filePath != null)
{
this.font = createFontFromFile(fontFamilyData.filePath!!, size)
}
private val metric by lazy {
val bi = BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR)
val g = bi.createGraphics()
g.font = font
val fontMetrics = g.fontMetrics
g.dispose()
fontMetrics
// System font
else
{
this.font = Font(fontFamilyData.invariantName, Font.PLAIN, size)
}
val bufferedImage = BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR)
val graphics = bufferedImage.createGraphics()
this.metric = graphics.getFontMetrics(font)
graphics.dispose()
}
private fun createFontFromFile(path: String, size: Int): Font {
var font: Font
try
{
// Try to create and register new font
val fontFile = Gdx.files.local(path).file()
val ge = GraphicsEnvironment.getLocalGraphicsEnvironment()
font = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(size.toFloat())
ge.registerFont(font)
}
catch (e: Exception)
{
// Fallback to default, if failed.
font = Font(Fonts.DEFAULT_FONT_FAMILY, Font.PLAIN, size)
}
return font
}
override fun getFontSize(): Int {
return size
return font.size
}
override fun getCharPixmap(char: Char): Pixmap {
var width = metric.charWidth(char)
var height = metric.ascent + metric.descent
if (width == 0) {
height = size
height = font.size
width = height
}
val bi = BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR)
@ -50,7 +81,7 @@ class NativeFontDesktop(private val size: Int, private val fontFamily: String) :
return pixmap
}
override fun getAvailableFontFamilies(): Sequence<FontFamilyData> {
override fun getSystemFonts(): Sequence<FontFamilyData> {
val cjkLanguage = " CJK " +System.getProperty("user.language").uppercase()
return GraphicsEnvironment.getLocalGraphicsEnvironment().allFonts.asSequence()
.filter { " CJK " !in it.fontName || cjkLanguage in it.fontName }

View File

@ -19,7 +19,7 @@ import com.unciv.ui.images.ImageWithCustomSize
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.FontFamilyData
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.NativeFontImplementation
import com.unciv.ui.components.FontImplementation
import com.unciv.ui.components.extensions.center
import com.unciv.ui.components.extensions.toLabel
import com.unciv.utils.concurrency.Concurrency
@ -62,7 +62,7 @@ object FasterUIDevelopment {
class UIDevGame : Game() {
val game = UncivGame(UncivGameParameters(
fontImplementation = NativeFontDesktop()
fontImplementation = FontDesktop()
))
override fun create() {
UncivGame.Current = game
@ -126,7 +126,7 @@ object FasterUIDevelopment {
}
class NativeFontDesktop : NativeFontImplementation {
class FontDesktop : FontImplementation {
private val font by lazy {
Font(Fonts.DEFAULT_FONT_FAMILY, Font.PLAIN, Fonts.ORIGINAL_FONT_SIZE.toInt())
}
@ -139,6 +139,10 @@ class NativeFontDesktop : NativeFontImplementation {
fontMetrics
}
override fun setFontFamily(fontFamilyData: FontFamilyData, size: Int) {
// Empty
}
override fun getFontSize(): Int {
return Fonts.ORIGINAL_FONT_SIZE.toInt()
}
@ -168,7 +172,7 @@ class NativeFontDesktop : NativeFontImplementation {
return pixmap
}
override fun getAvailableFontFamilies(): Sequence<FontFamilyData> {
override fun getSystemFonts(): Sequence<FontFamilyData> {
return sequenceOf(FontFamilyData(Fonts.DEFAULT_FONT_FAMILY))
}
}