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:
- BuildWorkflow.md for configure/build entry points.
- BakingPipeline.md for resource-pack production.
- GeneratedApiAndMetadata.md for generated settings and metadata inputs.
- ClientRuntime.md, ServerRuntime.md, and Tools.md for runtime/tool consumers.
Source paths inspected
Source/Common/ConfigFile.hSource/Common/ConfigFile.cppSource/Common/Settings.hSource/Common/Settings.cppSource/Common/Settings.incSource/Common/DataSource.hSource/Common/DataSource.cppSource/Common/FileSystem.hSource/Common/FileSystem.cppSource/Common/CacheStorage.hSource/Common/CacheStorage.cppSource/Common/SettingsStorage.hSource/Common/SettingsStorage.cppSource/Essentials/DiskFileSystem.hSource/Essentials/DiskFileSystem.cppSource/Essentials/Platform.hSource/Essentials/Platform.cppSource/Frontend/ApplicationInit.cppSource/Client/Client.cppSource/Client/Updater.cppSource/Client/ResourceManager.hSource/Client/ResourceManager.cppSource/Tools/Baker.hSource/Tools/Baker.cppSource/Tools/ConfigBaker.hSource/Tools/ConfigBaker.cppBuildTools/cmake/stages/Codegen.cmakeBuildTools/cmake/stages/ScriptsAndBaking.cmakeBuildTools/cmake/stages/Packages.cmake- related tests under
Source/Tests/
Layer map
- Config text parser —
ConfigFileparses sections, keys, values, repeated sections, optional collected content, and first-section reads. - Settings model —
Settings.incdeclares setting groups;Settings.*turns config files, command-line overrides, internal config, defaults, auto-settings, sub-configs, and resource-pack declarations intoGlobalSettings. - Data-source abstraction —
DataSourcemounts disk directories and pack files behind a uniform file-list/open interface. - File-system view —
FileSystemcombines mounted data sources, exposesFileHeader,File,FileReader, andFileCollection, and resolves file reads by path/name. - Cache storage —
CacheStoragepersists named string/data entries for reusable cache consumers. - Settings store —
SettingsStoragepersists per-user tool/editor preferences (registry on Windows, file store elsewhere), scoped by application name. - Low-level disk access —
DiskFileSystemperforms direct disk operations below mounted engine resources.
Config parsing
Source/Common/ConfigFile.* owns syntax-level parsing. ConfigFileOption controls optional behavior:
CollectContentpreserves section content for consumers that need raw block text.SkipNestedSectionsparses only anchor sections and skips nested (/-addressed) section bodies — cheap header enumeration on files with large nested payloads (map files).- Nested sections: a section name containing
/is nested.ConfigFilerecognizes only the syntax - names are stored verbatim and no prefix is ever resolved, so what a prefix means belongs to the consuming format.GetOrderedSections()exposes sections in file order, which is what a consumer needs to bind a nested section to the section it follows (the by-name multimap cannot express that, since repeated names collapse).SkipNestedSectionsparses only non-nested sections and skips nested bodies. ConfigFiletakes only the content: no file identity, no parse callbacks, no format tokens. For map files,MapLoaderowns the interpretation -[ProtoMap]declares a map named by its$Nameor by the file, and a nested$Name/<Type>prefix binds content to the anchor above it.
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:
ResourcePackInfo— name, input directories/files, include/exclude glob patterns, side flags, and baker list.SubConfigInfo— named config overlays and setting maps.GlobalSettings— combined client/server/baking/base settings with apply/save/custom-setting operations.
GlobalSettings applies input through:
ApplyConfigAtPath()andApplyConfigFile()for config files;ApplyCommandLine()for runtime/build-tool overrides;ApplyInternalConfig()for generated internal config;ApplySubConfigSection()for named overlays;ApplyDefaultSettings()andApplyAutoSettings()for engine defaults/derived values.
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.
For ///@ Setting declarations, MetadataBaker requires and writes the configured textual value after the setting name and type for both generated engine settings and custom game settings; a declaration without a value in the applied configuration is a bake error. Runtime metadata registration requires the same three fields. Those values are the baseline for game settings: BaseEngine applies a registered metadata value only to a setting the applied configuration never mentioned (BaseSettings::FindSettingValue() returns nothing), routing a known setting through its generated typed field and an unknown one to custom storage. A setting the config, sub-config, local config or command line did set keeps that value — those layers are the operator’s explicit override and run before the engine exists, so the baseline must not win over them.
ConfigBaker keeps the .fomain-* data patched into packaged binaries small by writing a game-only setting only when the sub-config value differs from the root-config baseline the metadata resource already ships; a setting that is also a native engine setting is always written, because startup may consume it before metadata is available. A game-only delta is written verbatim, bypassing the empty/false skip that applies to the rest — an override that turns something off still has to beat a baseline that says otherwise. This keeps the internal config as bootstrap plus per-package deltas while the metadata resource owns the script-setting baseline. The internal-config patch area is fixed by the engine at 10000 bytes and is not project-configurable. The applied root config write time is a metadata-bake dependency, so changing a configured setting refreshes the metadata resource even when no script declaration changed.
That baseline arrives with BaseEngine, so it is not yet applied while InitApp runs — settings read from ApplicationInitHook or any earlier point see only what the binary config carries. Baking.BootstrapGameSettings names the game settings an embedding project consumes there: ConfigBaker writes each of them in full, exactly as it writes a native engine setting, instead of reducing it to a sub-config delta. Every listed name must be a declared game setting or the bake fails, so a typo cannot quietly restore the delta form. Keep the list to settings that are genuinely read before the engine exists; everything else belongs in the metadata baseline, which is what keeps the patched config inside its fixed 10000-byte area.
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:
*— zero or more characters other than/;?— exactly one character other than/;**— zero or more characters including/;**/also matches zero directory levels.
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:
MountDir(dir, recursive, non_cached, maybe_not_available)for disk directory resources;MountPack(dir, name, maybe_not_available)for packed resource data.
FileSystem then combines sources and offers:
AddDirSource(),AddPackSource(),AddPacksSource(), andAddCustomSource();FilterFiles(),GetAllFiles(), and existence checks;ReadFile(),ReadFileText(), andReadFileHeader();FileReaderhelpers for endian-aware binary reads.
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.
Shared index over mounted sources
A point lookup (ReadFile(), ReadFileHeader(), IsFileExists()) is answered from one shared index when every
mounted source could hand its content over; otherwise the sources are probed in mount order as before.
DataSource::GetIndexSnapshot() is that hand-over: a source whose content is fixed until it is remounted returns
all of it, and a source whose answer depends on the world at call time returns nullopt. The default is nullopt,
so a source that says nothing keeps being probed - a missed override costs a lookup, a wrong one serves a file that
has since moved.
Every pack-backed source offers a snapshot - ZipFile, EmbeddedFile, FalloutDat, FilesList - and so does the
empty stand-in a maybe_not_available mount produces when its pack is absent. That last one is not a detail:
GetClientResources() mounts every pack name a second time against the writable overlay so a downloaded pack wins
over the installed copy, and on a client that has downloaded nothing yet every one of those is absent. If an absent
pack withheld a snapshot, the file system the game actually plays on would be off the index by default.
Directory sources offer none, cached or otherwise. CachedDir could - its file tree is already a snapshot refreshed
only by Reindex, so indexing it would add no staleness of its own - and it is withheld by decision rather than by
capability: unpacked mounts go through CachedDir, and development runs are meant to keep the probe loop. A file
system mounted entirely from packs - what a packaged client, server, mapper and viewer use - therefore resolves a
path with a single hash lookup, while a development run over directories, the baker’s live input dirs and the
on-demand baker data source keep probing. Mixing needs no configuration: one source without a snapshot
disables the index for that file system. The decision is per instance rather than per build, because a packaged
client also builds mixed file systems: the updater’s own resources, and the file system that checks a pushed file
list, both mount the resource directory as a non-cached dir to size the pack files while the updater is rewriting
them, and that directory must not be answered from a snapshot.
The index is filled as sources are mounted. A new source goes in front of the others and claims every path it holds
away from them, which is the shadowing the probe loop already produced; ReindexDataSources() rebuilds it. It is
never populated from a lookup: the read path takes no lock, because the source list is only mutated during setup,
and filling an index lazily from a const lookup would either race or put a mutex on every read.
FilterFiles() and GetAllFiles() stay on the source loop even where the index exists. Their output order is
source by source, and consumers depend on it - script module load order, prototype registration - which a hash
container does not preserve.
Common.Packaged is a fixed auto-setting populated from the executable’s packaged marker by GlobalSettings::ApplyAutoSettings(). After settings are loaded, runtime policy must read that snapshot (settings.Packaged) so copied or injected settings remain internally consistent and testable. Direct IsPackaged() checks are reserved for pre-settings bootstrap decisions and FileSystem::AddPackSource(), where the physical executable marker deliberately selects archive-versus-directory mounting; tests may also inspect that marker when choosing compatible fixtures.
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. GetClientResources() owns that ordering for both the updater’s post-sync metadata check and the gameplay ClientEngine; do not reconstruct the pack view independently in either path. The updater writes resource patches into that overlay, so the exact current files that pass validation also win runtime lookup and hash checks without modifying the install directory. A ZIP entry read failure identifies the archive path and the resource-relative entry in DataSourceException context; short reads also record the expected byte count, actual read result, and close result. Native runtime binary update paths are owned by ClientUpdater.md.
Low-level disk access
Source/Essentials/DiskFileSystem.* performs direct disk operations below mounted engine resources. fs_write_file() writes content at the given path and does not guarantee the resulting directory entry carries that name verbatim: on a case-insensitive filesystem (Windows, default macOS) an existing entry differing only by letter case is reused and keeps its own name. Callers that address files by exact name and rewrite a tree they do not own — the baker being the one in-engine case — reconcile names themselves rather than paying for a check on every write; see BakingPipeline.md.
Cache storage
Source/Common/CacheStorage.* stores named binary/string cache entries behind HasEntry(), GetString(), GetData(), SetString(), SetData(), and RemoveEntry(). Bounded consumers use GetDataBounded(name, max_size), which checks the file size before allocating and distinguishes Success, Missing, TooLarge, and Failed, plus SetDataChecked(...), which reports whether the complete write succeeded. The underlying disk helper fs_read_file_bounded applies the same pre-allocation cap and answers an oversized file with an empty result instead of raising. 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
BuildTools/cmake/stages/Codegen.cmakegenerates internal config inputs used by runtime settings.BuildTools/cmake/stages/ScriptsAndBaking.cmakewires resource baking/script compilation that consumeResourcePackInfoand baking settings.BuildTools/cmake/stages/Packages.cmakepackages resources for runtime targets.Source/Tools/ConfigBaker.*bakes config resources; full bake orchestration is in BakingPipeline.md.
Tests to inspect
Focused tests for this area:
Source/Tests/Test_CacheStorage.cppSource/Tests/Test_SettingsStorage.cppSource/Tests/Test_ConfigFile.cppSource/Tests/Test_DataSource.cppSource/Tests/Test_DiskFileSystem.cppSource/Tests/Test_FileSystem.cppSource/Tests/Test_Settings.cppSource/Tests/Test_ConfigBaker.cpp
Related consumers are covered by resource, client, server, script, and baker tests listed in Testing.md.
Change routing
- Config grammar and parsed section/key behavior:
Source/Common/ConfigFile.*. - Setting groups, defaults, command-line/config/sub-config application:
Source/Common/Settings.*andSettings.inc. - Installed-client writable-root resolution:
Source/Frontend/ApplicationInit.cpp,Source/Essentials/Platform.*, andSource/Essentials/DiskFileSystem.*. - Mounted resource lookup:
Source/Common/DataSource.*andFileSystem.*. - Raw disk operations:
Source/Essentials/DiskFileSystem.*. - Runtime resource consumption:
Source/Client/ResourceManager.*plus owning runtime docs. - Resource-pack generation: BakingPipeline.md and
Source/Tools/*Baker.*.
Validation checklist
- Run the focused parser/settings/filesystem/cache tests for the changed area.
- If resource-pack shape or mount order changes, run at least one bake path and one runtime/tool consumer path.
- If command-line or sub-config behavior changes, verify the embedding project config that exercises it, but keep project-specific values in project docs.
- If packaging/resource staging changes, re-check WebDebugging.md, AndroidDebugging.md, and ClientUpdater.md as applicable.
- Update BakingPipeline.md or BuildToolsPipeline.md when build-stage ownership changes.