beyond-all-reason / spring

A powerful free cross-platform RTS game engine
https://beyond-all-reason.github.io/spring/
Other
224 stars 102 forks source link

Full control over font fallback/substitution #537

Open WatchTheFort opened 2 years ago

WatchTheFort commented 2 years ago

Issue https://github.com/beyond-all-reason/spring/issues/87 was raised regarding automatic font substitution, which is something handled at the OS level.

Unfortunately, this means the font that is substituted is dependent on which fonts the user has installed on their system. We want to maintain control over the appearance of all aspects of the game, which includes how text is displayed.

What alternatives do we have to OS level font substitution?

  1. Perform all font substitution in engine
  2. Ship all fonts with BAR, switch them dynamically when language changes. This is what is currently done for Chinese, but requires a LuaUI reload, however it does not solve the problem of font substitution, and instead relies on all needed characters to be present in the font.
  3. Pack all fonts into a single monolithic font file.
  4. Something else...?
lhog commented 1 year ago

This is somewhat complicated...

Fonts substitution library is set up and configured even before spring hits the main menu, therefore the engine knows nothing about a game a user is about to launch nor about the fonts it might carry. I doubt even VFS exists at that stage of the engine loading.

Therefore the only way is to make it game specific is to expose fonts substitution to Lua, with that however there are certain concerns too:

Is the current way of doing substitution not good enough? Can I have screenshots where it fails spectacularly?

WatchTheFort commented 1 year ago

If you mean option 2 from above, it results in the user experiencing an undesirable long freeze while LuaUI reloads, and it changes all glyphs displayed using that font, even if we don't want their appearance to change (e.g. numbers).

lhog commented 1 year ago

Nope, didn't mean that. Both options above are implementation possibilities. Right now all fonts in engine's fonts and system wide fonts directory are matched for substitution.

undesirable long freeze while LuaUI reloads

yes, it's not great but it's one time off operation, people don't change their language every minute

it changes all glyphs displayed using that font, even if we don't want their appearance to change (e.g. numbers).

This is something I'd like to hear more about. Screenshot or short video or reproduction steps are needed

WatchTheFort commented 1 year ago

See the code here When an Asian language is used, it's switching the value of the config strings bar_font and bar_font2

Resource bar with English (default fonts): image

Resource bar with Chinese (SourceHanSans-Regular): image

lhog commented 1 year ago

This is not what fontconfig/fonts substitution does. Here you just change one font to another and no surprise they're looking differently.

What fonts substitution does instead it keeps the original font and draws all glyphs found in this font from that font file and only tries to replace glyphs NOT found in that font file from some other font file. For that fontconfig lib builds a database and tries its best to match the style of original fonts with replacement fonts. Things like font family, font make, slant, style, etc are used to select the best font candidate. After that the bitmap of particular "replaced" glyph is added into per-font texture atlas and kept there until the font is destroyed.

icexuick commented 1 year ago

In short I can say i don't minde the LuaUI reload/freeze. This is a one-time operation and i think people likely understand or even expect a 'reload' (or even a restart).

Regarding different font-styles - yes, this can be a big thing, since fonts do a LOT in design. Primarily it's of course about readability, so if a substitute is super readable and has support for exotic glyphs/whatnot AND we can still say which font-substitute this will be, then i think we should be able to find the most fitting font, that hurts the design the least.

lhog commented 1 year ago

It's worth trying to see how things look if you keep the original (mostly English-only) fonts and rely on fontconfig to do substitution of non-English glyphs.

WatchTheFort commented 1 year ago

But are we able to specify which fallback fonts fontconfig is using? If not, different players will see different things, and from a professional presentation standpoint, we want the game to have a single, standard appearance.

lhog commented 1 year ago

Yes I understand what you mean, but I estimate it's not going to be an easy thing to do. First it's worth doing an experiment with keeping the original font in place and seeing how bad or good the substitution will be.

WatchTheFort commented 1 year ago

This is what my game looks like using the original font substitution, ignore the English parts, that's due to widget bugs. Some parts are good, some parts are completely illegible. screen00041

Note that there is a missing glyph in the ready button, where the empty space is should be a character (U+FF01 : FULLWIDTH EXCLAMATION MARK). I don't know why font substitution is failing for this particular character.

Note that even if it was completely fine for me, that doesn't mean it will appear the same way to other players, and control over that is what we want.

lhog commented 1 year ago

Drop me a code to run this, there's a lot to check here.

WatchTheFort commented 1 year ago

I just commented out these lines: https://github.com/beyond-all-reason/Beyond-All-Reason/blob/904742cdaf8e67a7e38c0c9a966671d07e6045ae/modules/i18n/i18n.lua#L38-L46

Language can be switched in game from Settings -> Dev -> Language (first column near the bottom)

saurtron commented 2 weeks ago

Looks like fontconfig and engine support allows a lot of possibilities.

I think we can approach this in different ways:

with our provided fontconfig xml (fontconfig way)

I tried the following next to where we have <cachedir>fontcache</cachedir> in cacheDirFmt:

<match>
    <test name="family"><string>FreeSans</string></test>
    <edit name="family" mode="prepend" binding="strong">
        <string>Noto Sans</string>
        <string>Noto Emoji</string>
        <string>Noto Emoji Regular</string>
    </edit>
</match>

With that I managed to make fontconfig prefer NotoEmoji-VariableFont_wght.ttf inside spring/fonts instead of my os preferred one FreeSerif.otf to render 🔥.

I also tried <alias> with <accept>, <prefer> or <default> but could not get that to work.

in-code by always checking our fonts first (the override way)

Instead of always delegating to fontconfig to search for font alternatives, due to how the engine support is written, it wouldn't be hard to always manually search first in some preffered set of fonts, before delegating to fontconfig.

general remarks

With both approaches, we could provide some lua api (didn't check to see if we have one already), to allow games to add folders/fonts to the preferred lists. Didn't think much about this yet, but wanted to hear what others think about both approaches, they look feasible, I think the first one can be better but maybe in the end the last one is easier to control.

The tricky part with the first approach can actually be making fontconfig include VFS memory fonts from games into its search db I looked a bit and doesn't seem to be a direct way to do that, but will investigate and try to find a way. update: looks like it can be done

WatchTheFort commented 2 weeks ago

At the end of the day, games need complete control of how text appears, so that every player sees the same thing.