View on GitHub

FOnline Engine

Flexible cross-platform isometric game engine

Configuration and Data Sources

Engine-owned documentation. This page explains reusable configuration parsing, runtime settings, mounted data sources, file lookup, and cache storage. Project-specific config values and content folder policy belong to the embedding project.

Purpose

Use this page when changing how the engine reads .fomain/config data, applies command-line or sub-config overrides, mounts resource directories/packs, reads files, or stores cached resource data.

Read this together with:

Source paths inspected

Layer map

  1. Config text parserConfigFile parses sections, keys, values, repeated sections, optional collected content, and first-section reads.
  2. Settings modelSettings.inc declares setting groups; Settings.* turns config files, command-line overrides, internal config, defaults, auto-settings, sub-configs, and resource-pack declarations into GlobalSettings.
  3. Data-source abstractionDataSource mounts disk directories and pack files behind a uniform file-list/open interface.
  4. File-system viewFileSystem combines mounted data sources, exposes FileHeader, File, FileReader, and FileCollection, and resolves file reads by path/name.
  5. Cache storageCacheStorage persists named string/data entries for reusable cache consumers.
  6. Settings storeSettingsStorage persists per-user tool/editor preferences (registry on Windows, file store elsewhere), scoped by application name.
  7. Low-level disk accessDiskFileSystem performs direct disk operations below mounted engine resources.

Config parsing

Source/Common/ConfigFile.* owns syntax-level parsing. ConfigFileOption controls optional behavior:

The parser stores owned strings internally and returns string_view values from parsed sections. Consumers must not assume those views outlive the ConfigFile instance.

Runtime settings

Source/Common/Settings.inc is the central generated-like declaration file for setting groups and individual settings. Settings.h exposes:

GlobalSettings applies input through:

ConfigBaker (Source/Tools/ConfigBaker.cpp) bakes the config by re-deriving each sub-config from the root and saving every registered setting; a setting that GlobalSettings::Save() does not emit is reported as Uninitialized server/client setting <name> and fails the bake. Save() only emits settings present in _appliedSettings, which is populated from the keys of every applied config plus a fixed auto-settings allow-list seeded in the baking-mode GlobalSettings constructor. Author-tunable settings reach _appliedSettings by being enumerated in the embedding project’s config; settings that are resolved at runtime and never authored in any config (platform/build flags, monitor size, command-line/git/compatibility values, and Client.UserWritablePath) must be added to that auto-settings allow-list, or baking fails. When adding such a runtime-only engine setting to Settings.inc, register it in the auto-settings list in the same change. Settings consumed only by BuildTools/package.py (the AndroidSettings and PackagingSettings groups in Settings.inc, e.g. Android.Keystore, Packaging.AppIcon, Packaging.MsiUpgradeCode, Packaging.CodeSigningHook) are registered as ordinary settings like everything else — the config baker validates them uniformly and has no packaging-specific allow-list, so an unregistered key in a config is always reported as Unknown setting. Note that setting lookup accepts both the dotted (Group.Name) and bare (Name) spellings, so the bare part of every setting name must stay globally unique across all groups (e.g. Android.Icon already claims Icon, which is why the packaging icon lives under its own Packaging.AppIcon name).

Custom settings have two read shapes. Use FindCustomSetting() when missing keys are normal and should stay in the nullable pointer vocabulary. Use GetCustomSetting() only for compatibility with the historical non-null sentinel behavior: it returns the stored value when present and _emptySetting when absent.

Do not document one embedding project’s .fomain contents as universal engine behavior. Use project docs for concrete values; use this page for the engine mechanics that consume them.

Client startup has one extra resolution step for installed layouts: ResolveUserWritablePath(settings) in Source/Frontend/ApplicationInit.cpp resolves Client.UserWritablePath before the local-config cache is read. The writable-path knobs (Client.UserWritablePath, Baking.CacheResources) live in the config and sub-config, which are applied earlier, so the cache location is known without consulting the command line. The command line is then applied to the live settings exactly once, after the config, sub-config and local config, so it takes final precedence over all of them; a single pass also keeps +-append overrides (-Setting +value) from accumulating twice. That single pass logs each Set <name> to <value> override. In that log, settings whose name contains one of the masking tokens are printed as Set <name> to ***, so a credential such as Auth.WebTokenVerifySecret never appears in plaintext (server logs may be shared). The tokens are the Common.SecretSettingTokens setting (a case-insensitive substring list, default secret token password apikey), which GlobalSettings::IsSecretSettingName() reads. Command-line overrides are logged only on the final pass — after ApplyDefaultSettings() and the config file have run — so the list is already populated, and an embedding project extends it through config to cover credentials the generic tokens miss (Last Frontier sets Common.SecretSettingTokens = secret token password apikey dsn so Sentry.Dsn is masked). Empty means portable unless an INSTALLED marker sits next to the executable; * resolves through Platform::GetUserDataBase() plus Common.GameName; an explicit path is resolved directly. If the target directory or required cache/resource subdirs cannot be created, the resolver logs a warning and reverts to portable layout.

Resource packs and data sources

ResourcePackInfo describes resource-pack inputs that bakers and runtimes consume. The bake side uses BakingContext / BakerDataSource in Source/Tools/Baker.*; the runtime side uses mounted DataSource and FileSystem abstractions.

Resource-pack input directories are mounted recursively. IncludePatterns and ExcludePatterns are optional space-separated glob lists applied to normalized resource-relative paths before any baker runs. An empty include list accepts every path; exclusion is evaluated after inclusion and wins. Patterns are case-sensitive and support:

Both / and \ are accepted as pattern separators and normalized to /. For example, IncludePatterns = **/*.fomap selects maps at any depth, while ExcludePatterns = **/_*.fomap removes scratch maps such as Generated/_compose.fomap. Multiple packs may mount the same InputDirs and select disjoint resources with different pattern lists. Use IncludePatterns = * to reproduce the former top-level-only input behavior.

DataSource provides two built-in mount shapes:

FileSystem then combines sources and offers:

Cached directory mounts snapshot their file index when mounted. Long-running tools can call FileSystem::ReindexDataSources() to ask every mounted source to refresh that snapshot; the method returns true when indexed paths, sizes, or write times changed. Sources that do not cache disk state keep the default no-op behavior. Custom sources can override DataSource::Reindex(); BakerDataSource uses it to rebuild input mounts and bake newly added or changed resources on demand.

Mount order matters for lookup behavior. When changing it, verify the runtime/tool path that owns the resource pack, not only the parser.

Installed clients keep the read-only base resources mounted from ClientResources and layer the writable resource overlay from fs_make_writable_path(UserWritablePath, ClientResources) on top in client/updater paths. The updater writes resource patches into that overlay, so current files win lookup/hash checks without modifying the install directory. Native runtime binary update paths are owned by ClientUpdater.md.

Cache storage

Source/Common/CacheStorage.* stores named binary/string cache entries behind HasEntry(), GetString(), GetData(), SetString(), SetData(), and RemoveEntry(). It is separate from resource packs: cache entries are mutable runtime/tool artifacts, while baked resources are generated from configured inputs. Client-side cache consumers resolve relative cache paths through fs_make_writable_path(UserWritablePath, CacheResources), so portable clients keep cache next to the executable and installed clients write under the per-user root.

An entry is stored as one plain file named after the entry, with path separators folded to _, so the cache directory stays readable and inspectable. Two entry names that differ only in those separators therefore map to the same file — acceptable because an entry is only ever a cache, where a miss is always recoverable, but it means a caller that needs distinct entries must not rely on directory structure alone to separate them. The cache is not a confidentiality boundary: anything that must not be readable at rest has to be protected by its owner before it is handed over (the embedding project’s secure-storage bridge does exactly that).

Settings store

Source/Common/SettingsStorage.* persists small per-user tool/editor preferences (ImGui window layout, view options, last selection) behind GetString()/SetString(), typed GetInt/SetInt, GetBool/SetBool, GetFloat/SetFloat, HasKey(), and Remove(). It is scoped by an application name passed to the constructor so different tools never collide. The backend is platform-specific through a pimpl: on FO_WINDOWS the values are REG_SZ entries under HKCU\Software\FOnline\<app_name> (Win32 headers are confined to the .cpp behind WIN32_LEAN_AND_MEAN + WinApiUndef.inc, using the explicit *A registry entry points); on other platforms it is a per-application CacheStorage under Platform::GetUserDataBase()/FOnline/<app_name>. Every value is stored as a string (the typed accessors serialize through it), so both backends behave identically, and the multi-line ImGui imgui.ini blob round-trips verbatim. Persistence is best-effort: a backend failure is logged, never thrown, so a tool never dies because its settings could not be written. It differs from CacheStorage in intent (durable user preferences vs. regenerable cache artifacts) and, on Windows, in medium (registry vs. files).

Only the GUI tools reference it (Mapper MapperEngine::_uiSettings, migrated from the resource Cache; standalone AnimationViewer / ParticleViewer, each loading in its constructor and saving on shutdown). It lives in CommonLib for simplicity, but because the client and server reference no SettingsStorage symbol, the linker (/OPT:REF plus on-demand static-library inclusion) drops the object from the shipped client/server binaries — so the Windows registry calls never land where antivirus heuristics might flag them. ImGui’s own imgui.ini autosave stays disabled (Application.cpp), so all layout persistence flows through this store.

Build and package routing

Tests to inspect

Focused tests for this area:

Related consumers are covered by resource, client, server, script, and baker tests listed in Testing.md.

Change routing

Validation checklist

  1. Run the focused parser/settings/filesystem/cache tests for the changed area.
  2. If resource-pack shape or mount order changes, run at least one bake path and one runtime/tool consumer path.
  3. If command-line or sub-config behavior changes, verify the embedding project config that exercises it, but keep project-specific values in project docs.
  4. If packaging/resource staging changes, re-check WebDebugging.md, AndroidDebugging.md, and ClientUpdater.md as applicable.
  5. Update BakingPipeline.md or BuildToolsPipeline.md when build-stage ownership changes.