The in-memory shape of a Scylla JSON configuration file, giving you deterministic ordering, type-tagged values, and a merge primitive that folds user overrides on top of built-in defaults without losing information.
Summary
Scylla utilizes a small, focused collection of utils and data model used to create and maintain config files. The ConfigFile family is the in-memory shape of one parsed Scylla configuration file: a root document, named property groups, type-tagged values, and the JSON I/O plus deep-merge primitives that the layered configuration system is built on top of.
Four types cover the entire surface:
ConfigFileis the root document and holds the dictionary of named groups.ConfigFileGroupis a named collection of properties (the JSON-object level of the document).ConfigFilePropertyis the tagged-union container that holds one typed value.ConfigFilePropertyTypeis the enum that identifies which type the value carries.
The four types are pure data containers. They have no Unity dependencies, no I/O of their own, and no opinions about where their JSON ends up on disk. The orchestration around them (locating files via the four-tier fallback, applying overrides to ScyllaConfiguration assets, exporting from the Inspector) lives in Configuration. The file-system helpers for resolving paths and reading bytes live in Files and IO. This page covers only the data shape that sits between those layers.
Two design choices give the model its character. Groups and properties are stored in case-insensitive alphabetical order everywhere, so a ToJSON / FromJSON round trip produces deterministic output and a stable diff. Every value carries a ConfigFilePropertyType tag, and the model never silently coerces across category boundaries (numeric vs string vs group vs array): direct accessors on ConfigFileProperty throw on a type mismatch by design, and the TryGet* convenience methods on ConfigFile and ConfigFileGroup only widen numerically (Int to Float to Double) and never across categories. The JSON-side rules complete the model: null round-trips to a String with a null value; integer literals that fall outside the Int32 range widen to Double with a warning; floating-point values are rounded to six decimal places by ToJSON.
The model is also small. Four types, one supporting enum, and one static factory family on ConfigFileProperty cover the entire surface. Anything beyond that is policy and lives elsewhere: the Configuration system folds the four-tier override chain into a single ConfigFile via ConfigFile.Merge, then hands that to ScyllaConfiguration.ApplyConfigFileOverrides. The split is intentional: pure data here, policy and orchestration in the configuration system.
Use the ConfigFile family for:
- The in-memory representation of a parsed Scylla JSON config file (the canonical use).
- Deterministic, diff-friendly serialization of nested configuration trees.
- Layered override merging via
ConfigFile.Merge(higher, lower). - Programmatic configuration authoring in code (test fixtures, runtime-generated configs, migration tools).
- Any case where a small, type-tagged, JSON-round-trippable property bag is needed and the framework’s Serialization engine is overkill.
Features
- Four-type surface.
ConfigFile,ConfigFileGroup,ConfigFileProperty,ConfigFilePropertyType. The entire model fits on a single page; there is nothing else to learn. - Deterministic alphabetical ordering. Groups inside a
ConfigFileand properties inside aConfigFileGroupare stored in case-insensitive alphabetical order. JSON output is deterministic; diffs between two saved configs are stable and meaningful. - Type-tagged values.
ConfigFilePropertycarries aConfigFilePropertyTypetag (Bool,Int,Float,Double,String,Group,Array). Direct typed accessors throw on a type mismatch; mistakes are loud and immediate, not silent. - Safe numeric widening.
TryGetFloatacceptsIntandFloat.TryGetDoubleacceptsInt,Float, andDouble. The widening never crosses category boundaries; anIntis never read as a string. - JSON round-trip.
ConfigFile.FromJSON(string)andConfigFile.ToJSON()are the I/O surface. The result is canonical, alphabetically ordered, and human-readable (floats rounded to six decimals, hex colors uppercased). - Deep merge.
ConfigFile.Merge(higher, lower)produces a freshConfigFilein whichhigherwins on conflict. Sub-groups merge recursively; primitives and arrays are replaced wholesale. Neither input is mutated. - Color support.
SetColorwritesColoras an 8-digit ARGB hex string ("#AARRGGBB");TryGetColoraccepts both 8-digit ARGB and 6-digit RGB, with an optional leading#. The hex form survives JSON round trips. - Group and per-group accessors. Both
ConfigFileandConfigFileGroupexpose the full set ofSetX/TryGetXmethods. Code that works with a group directly can stay on that group rather than passing the parent file plus group name. - List/record helpers.
SetList<T>(...)andTryGetList<T>(...)take per-element conversion delegates so collections of records round-trip with one call instead of hand-rolled array element loops. ConfigFilePropertyfactories.ConfigFileProperty.Bool(v),Int(v),Float(v),Double(v),String(v),Group(v),Array(v)are the only construction path. Adefault(ConfigFileProperty)is detectable viaIsInitializedand throws on typed-accessor access.- Bounded recursion depth. Clone and merge depth is capped at 32 levels. A pathological structure logs a warning to
LogCategory.Coreand returns the partial result rather than overflowing the stack. nullround-trips as aString. A JSONnullliteral parses to aConfigFileProperty.String(null). The property is initialized (useIsInitializedto distinguish it fromdefault(ConfigFileProperty)).
Type hierarchy
The four types are arranged as a strict containment tree. A ConfigFile owns a set of ConfigFileGroup instances; a ConfigFileGroup owns a set of ConfigFileProperty instances; a ConfigFileProperty may itself be a Group or an Array that recursively owns more properties.
flowchart TB
File[ConfigFile<br/>root document]
Group[ConfigFileGroup<br/>named property bag]
Prop[ConfigFileProperty<br/>typed value]
Type[ConfigFilePropertyType<br/>Bool / Int / Float / Double / String / Group / Array]
File -->|0..N groups| Group
Group -->|0..N properties| Prop
Prop -->|tagged by| Type
ConfigFile
The root document. Holds a sorted dictionary of ConfigFileGroup instances keyed by group name (case-insensitive). Owns the JSON I/O via ToJSON and FromJSON, the deep-merge primitive via Merge(higher, lower), and the typed convenience accessors (SetBool, TryGetFloat, …) that take a (groupName, propertyName) pair.
ConfigFileGroup
A named bag of properties. Mirrors a JSON object: a ConfigFile serializes to { "GroupName": { ... }, ... }, and each group’s properties are the inner key/value pairs. Groups can be nested by storing a ConfigFileProperty of type Group inside another group’s property map. The group exposes the same typed accessors as ConfigFile, plus GetProperty / SetProperty / RemoveProperty for raw property work.
ConfigFileProperty
A readonly struct that holds one typed value plus its Type tag. Created exclusively through the static factory methods (Bool, Int, Float, Double, String, Group, Array). A default(ConfigFileProperty) is detectable via the IsInitialized flag, and accessing typed accessors on an uninitialized instance throws. The typed *Value properties (BoolValue, IntValue, FloatValue, …) throw InvalidOperationException on a tag mismatch.
ConfigFilePropertyType
The enum tag identifying a property’s runtime shape. The seven values are Bool, Int, Float, Double, String, Group, and Array. The tag is preserved when round-tripping through JSON, with one exception: integer literals outside the Int32 range are widened to Double during parsing and a warning is logged to LogCategory.Core.
JSON round-trip rules
| JSON token | Resulting property type |
|---|---|
true / false | Bool |
integer in Int32 range | Int |
integer outside Int32 range | Double (warning logged) |
| floating-point literal | Double |
| string literal | String |
null | String with a null value |
{ ... } | Group (parsed recursively) |
[ ... ] | Array (parsed recursively) |
Floating-point values are rounded to six decimal places by ToJSON for human-readable output. NaN and infinity are preserved using the JSON writer’s encoding. The Float tag is never produced by FromJSON: a ConfigFileProperty.Float exists only when set programmatically (the JSON parser cannot tell a float literal from a double literal, so it always picks Double).
Colors are represented as 8-digit uppercase ARGB hex strings ("#AARRGGBB") stored under the String tag. ConfigFile.SetColor writes the hex form; TryGetColor accepts both 8-digit ARGB and 6-digit RGB (alpha = 1), with an optional leading #.
Recipes
These recipes cover every public surface in the ConfigFile family. Each one is a self-contained answer to a single practical problem – scan the recipe names, find the one that matches what you are building, and copy the solution. The first recipe (building a file in code) creates the structure the rest of the recipes build on, so start there if you are new to the API.
Expose tunable settings to designers in a JSON file
Problem. You want a simple JSON file that designers and players can edit outside the game to tune gameplay values: movement speeds, spawn counts, level names. You need to write the default values in code, serialize them to disk, and read them back safely when the file might have been hand-edited incorrectly.
Solution. Construct a ConfigFile, write your defaults with SetX calls, serialize to JSON with ToJSON, and read back with TryGet* methods that return false on a missing or mistyped value rather than throwing. Named arguments on SetX and TryGetX keep the call sites readable.
/* Build a config file from code to ship as defaults. Each SetX call creates
* the group if it does not exist yet and inserts the property alphabetically.
* The "Tuning" group is the canonical shape for per-system gameplay knobs. */
var file = new ConfigFile();
file.SetFloat (group: "Tuning", property: "MaxSpeed", value: 25.0f);
file.SetBool (group: "Tuning", property: "EnableLogging", value: false);
file.SetInt (group: "Tuning", property: "ProjectileCount", value: 256);
file.SetString(group: "Tuning", property: "DefaultLevel", value: "Forest");
/* Serialize to disk. Output is alphabetically ordered and human-readable,
* so designers can find any field without knowing the insertion order. */
string json = file.ToJSON();
System.IO.File.WriteAllText(Application.dataPath + "/Config/tuning.json", json);
{
"Tuning": {
"DefaultLevel": "Forest",
"EnableLogging": false,
"MaxSpeed": 25,
"ProjectileCount": 256
}
}
/* Read back safely. TryGet* returns false on a missing group, missing
* property, or type mismatch - it never throws, so a player who types
* "MaxSpeed": "fast" does not crash the game at startup. */
var loaded = ConfigFile.FromJSON(System.IO.File.ReadAllText(configPath));
if (loaded.TryGetFloat("Tuning", "MaxSpeed", out var maxSpeed))
{
ApplyMaxSpeed(maxSpeed);
}
if (loaded.TryGetBool("Tuning", "EnableLogging", out var enableLogging))
{
SetLogging(enableLogging);
}
/* TryGetFloat and TryGetDouble accept widened integers transparently.
* The Int stored under "ProjectileCount" reads cleanly as a float. */
if (loaded.TryGetFloat("Tuning", "ProjectileCount", out var projAsFloat))
{
Log.Info($"Projectiles as float: {projAsFloat}", LogCategory.Core);
}
/* Cross-category reads always fail safely. An Int never reads as a string.
* The method returns false and leaves asString at its default. */
if (!loaded.TryGetString("Tuning", "ProjectileCount", out var asString))
{
Log.Warning("ProjectileCount is not a string", LogCategory.Core);
}
Layer user overrides on top of built-in defaults
Problem. You want to ship game defaults baked into your asset, let players drop a JSON override file in their Documents folder to tune settings without patching the asset, and never lose a default when the user’s file only covers a subset of properties. This is the classic config-layering story: defaults at the bottom, user on top, the effective result is a merge of both.
Solution. ConfigFile.Merge(higher, lower) produces a fresh ConfigFile where higher wins on every conflict. Sub-groups merge recursively so a user override of a single field inside a group preserves the rest of the group’s defaults. Neither input is mutated, so you can call Merge repeatedly on the same defaults file safely.
/* The defaults shipped inside the game asset. These come from ToJSON
* of the ScyllaConfiguration asset's export, or from a hand-authored
* JSON file bundled in StreamingAssets. */
var defaults = ConfigFile.FromJSON(builtInDefaultsJSON);
/* The user's override file, which may only contain a handful of fields.
* The four-tier fallback in the Configuration system resolves the path;
* here we illustrate the merge directly. */
var userOverride = ConfigFile.FromJSON(userOverrideJSON);
/* Apply the user file on top of defaults. Properties present in both
* files take the user's value; properties only in defaults are preserved. */
var effective = ConfigFile.Merge(higher: userOverride, lower: defaults);
/* Example: user changed only Camera.Damping in their override file.
* The merge preserves Camera.FieldOfView and Camera.Mode from defaults. */
if (effective.TryGetFloat("Camera", "Damping", out var damping))
{
ApplyDamping(damping); /* user's value */
}
if (effective.TryGetFloat("Camera", "FieldOfView", out var fov))
{
ApplyFOV(fov); /* default's value, since user did not override it */
}
Organize settings into nested groups
Problem. Your configuration has grown beyond one flat group. Graphics settings have sub-sections for post-processing, shadows, and anti-aliasing; audio has mixer, SFX, and music. You want the JSON structure to reflect that hierarchy, with each sub-section as its own object in the file.
Solution. Groups nest by storing a ConfigFileProperty of type Group inside another group’s property map. The SetGroup(parentGroupName, propertyName, subGroup) overload is the ergonomic path; reading the nested group back out works with TryGetGroup.
/* Build a nested camera config. The "Camera" group will live inside the
* parent "Modules" group in the JSON, giving you "Modules.Camera.*". */
var camera = new ConfigFileGroup("Camera");
camera.SetFloat("Damping", 0.15f);
camera.SetFloat("FieldOfView", 60f);
camera.SetString("Mode", "Follow");
/* Insert the Camera group as a nested property inside "Modules". */
file.SetGroup(group: "Modules", property: "Camera", value: camera);
/* Reading the nested group back for inspection or further mutation. */
if (file.TryGetGroup("Modules", "Camera", out var cameraGroup))
{
if (cameraGroup.TryGetString("Mode", out var mode))
{
Log.Info($"Camera mode: {mode}", LogCategory.Core);
}
}
Store and read a list of spawn points
Problem. Some of your settings are lists of records, not singletons: a list of enemy spawn points with name, X, and Y; a list of available weapon loadouts; a list of dialogue triggers. You want them to round-trip cleanly through JSON without hand-rolling the array element loops.
Solution. SetList<T> and TryGetList<T> take per-element conversion delegates that map between your record type and a ConfigFileGroup. The result is a list of Group properties under the array, which survives JSON round trips cleanly. For plain primitive arrays (a list of difficulty multipliers, an array of available resolutions), use SetArray directly with ConfigFileProperty factories.
/* A small record type. */
public readonly struct SpawnPoint
{
public string Name { get; }
public float X { get; }
public float Y { get; }
public SpawnPoint(string name, float x, float y)
{
Name = name; X = x; Y = y;
}
}
var spawnPoints = new List<SpawnPoint>
{
new("Start", 0f, 0f),
new("Checkpoint1", 64f, 0f),
new("Boss", 256f, 128f),
};
/* Write the records as an array of groups. The second delegate parameter
* is the element's index, useful for fields like "Order" or "ID" derived
* from position. */
file.SetList(
group: "Spawn",
property: "Points",
items: spawnPoints,
toGroup: (sp, i) =>
{
var g = new ConfigFileGroup($"SpawnPoint{i}");
g.SetString("Name", sp.Name);
g.SetFloat("X", sp.X);
g.SetFloat("Y", sp.Y);
return g;
});
/* Read them back into a typed list. */
if (file.TryGetList(
group: "Spawn",
property: "Points",
items: out var parsed,
fromGroup: g =>
{
g.TryGetString("Name", out var name);
g.TryGetFloat("X", out var x);
g.TryGetFloat("Y", out var y);
return new SpawnPoint(name, x, y);
}))
{
foreach (var sp in parsed)
{
Spawn(sp.Name, new Vector2(sp.X, sp.Y));
}
}
/* Primitive arrays: a list of stage difficulty multipliers. */
var multipliers = new List<ConfigFileProperty>
{
ConfigFileProperty.Float(1.0f),
ConfigFileProperty.Float(1.3f),
ConfigFileProperty.Float(1.8f),
ConfigFileProperty.Float(2.5f),
};
file.SetArray(group: "Difficulty", property: "StageMultipliers", items: multipliers);
/* Reading the array back. Each element's Type tells you what it carries. */
if (file.TryGetArray("Difficulty", "StageMultipliers", out var mults))
{
foreach (var entry in mults)
{
if (entry.Type == ConfigFilePropertyType.Double)
{
Log.Info($"Multiplier: {entry.DoubleValue}", LogCategory.Core);
}
}
}
Store Unity colors in a config file
Problem. You want to ship UI color palettes or ambient light tints in a JSON config that designers can tune without rebuilding the game. Unity’s Color (four floats) does not have a natural JSON shape, and you want something a designer can edit in a text editor without making arithmetic errors.
Solution. Colors live under the String tag as 8-digit uppercase ARGB hex ("#AARRGGBB"). The dedicated SetColor and TryGetColor helpers handle the encoding so your code never sees the hex string. TryGetColor also accepts 6-digit RGB on read, so a designer who drops the alpha channel in their override file still gets a valid parse.
/* Write colors. The hex form is human-readable in the JSON output and
* survives round trips without floating-point precision loss. */
file.SetColor("UI", "Primary", new Color(0.2f, 0.4f, 0.8f, 1f));
file.SetColor("UI", "Destructive", new Color(0.9f, 0.2f, 0.2f, 1f));
/* Read back. Works whether the file was generated by SetColor or
* hand-edited by a designer in a text editor. */
if (file.TryGetColor("UI", "Primary", out var primary))
{
ApplyPrimary(primary);
}
/* TryGetColor accepts both 8-digit ARGB and 6-digit RGB (alpha = 1),
* with or without a leading "#". The hand-edited config below is valid. */
var hand = ConfigFile.FromJSON("""
{
"UI": {
"Primary": "#FF3366CC",
"Accent": "FFCC66"
}
}
""");
hand.TryGetColor("UI", "Primary", out var p); /* full ARGB */
hand.TryGetColor("UI", "Accent", out var a); /* RGB, alpha = 1 */
Work with a config group directly
Problem. A single subsystem cares about exactly one group of settings, and threading the group name through every accessor call gets tedious. You want to retrieve the group once and operate on it directly, with the same typed API.
Solution. GetOrAddGroup retrieves or creates the group by name and returns a ConfigFileGroup you can work with directly. The group exposes the same typed accessors as ConfigFile (one fewer string argument per call), plus GetProperty / SetProperty / RemoveProperty for raw property work.
/* Retrieve or create the group in one call. GetOrAddGroup is the idiomatic
* "I want this group whether or not it exists yet" path. AddGroup throws on
* duplicates; GetGroup throws on missing. Use those only when guarded. */
var camera = file.GetOrAddGroup("Camera");
/* Per-property typed accessors on the group itself. Reads cleaner when
* a subsystem only touches one group and does not need the parent file. */
camera.SetFloat("Damping", 0.18f);
camera.SetString("Mode", "Orbit");
/* Property enumeration in the alphabetical storage order. Useful for a
* debug inspector that wants to dump every property in a group. */
foreach (var (name, prop) in camera.GetProperties())
{
Log.Info($"{name}: {prop.Type} = {prop}", LogCategory.Core);
}
/* Remove a property. Safe to call when the property is not present. */
var existed = camera.RemoveProperty("Mode");
/* Group-level property names (sorted). Useful for building a settings UI
* that discovers its own fields at runtime. */
foreach (var name in camera.GetPropertyNames())
{
HandleProperty(name);
}
Inspect and construct raw properties
Problem. You want to walk a config file generically, inspect type tags before deciding what to do with a value, or construct ConfigFileProperty instances to feed into a serializer or a migration tool that does not know about the typed convenience helpers.
Solution. ConfigFileProperty is a readonly struct tagged-union with static factory methods as the only legal construction path. Inspect the Type property before accessing any typed *Value accessor; they throw InvalidOperationException on a tag mismatch. Use IsInitialized to detect a default(ConfigFileProperty) that should not reach typed accessors at all.
/* Construct properties via the factories. These are the only legal
* construction paths; the default struct value is a sentinel, not a valid
* property, and will throw on typed-accessor access. */
var bool1 = ConfigFileProperty.Bool(true);
var int1 = ConfigFileProperty.Int(42);
var str1 = ConfigFileProperty.String("hello");
var grp1 = ConfigFileProperty.Group(new ConfigFileGroup("inner"));
/* Inspect the type tag before accessing the typed value. The direct
* *Value accessors throw on a mismatch; check Type first in generic code. */
if (int1.Type == ConfigFilePropertyType.Int)
{
var i = int1.IntValue;
}
/* Detect an uninitialized property (default-constructed) at the boundary.
* Typed-accessor calls on an uninitialized property throw; IsInitialized
* is the cheap guard to run before you get there. */
var uninit = default(ConfigFileProperty);
if (!uninit.IsInitialized)
{
/* Accessing uninit.IntValue, StringValue, etc. would throw. */
}
/* A String(null) is initialized; the value happens to be null.
* IsInitialized distinguishes it from a default-constructed property. */
var nullable = ConfigFileProperty.String(null);
bool initialized = nullable.IsInitialized; /* true */
bool isNull = nullable.StringValue == null; /* true */
/* Equality on properties is value equality on the tag and stored value. */
bool same = ConfigFileProperty.Int(42) == ConfigFileProperty.Int(42);
Serialize a config file to JSON and back
Problem. You want to write a config file to disk so it survives application restarts, and read it back deterministically the next time the game launches. You want the JSON to be human-readable and produce stable diffs when only one value changes between versions.
Solution. ConfigFile.ToJSON() produces canonical, alphabetically ordered JSON. ConfigFile.FromJSON(string) parses it back into a fresh ConfigFile. The round trip is lossless for all types except Float, which becomes Double on re-parse (the JSON format has no distinct float type).
/* Build a small file. */
var src = new ConfigFile();
src.SetFloat("Tuning", "MaxSpeed", 25f);
src.SetBool ("Tuning", "EnableLogging", false);
/* Serialize. Alphabetically ordered, deterministic, human-readable.
* MaxSpeed serializes as 25 (integer form) because 25.0 rounds cleanly. */
string json = src.ToJSON();
/* Parse JSON back into a ConfigFile. */
var parsed = ConfigFile.FromJSON(json);
bool same =
parsed.TryGetFloat("Tuning", "MaxSpeed", out var s) && Math.Abs(s - 25f) < 1e-6f &&
parsed.TryGetBool ("Tuning", "EnableLogging", out var log) && log == false;
The canonical output for the snippet above:
{
"Tuning": {
"EnableLogging": false,
"MaxSpeed": 25
}
}
API Sketch
namespace Scylla.Core.Util.File
{
public sealed class ConfigFile
{
public int GroupCount { get; }
public static ConfigFile FromJSON(string json);
public static ConfigFile Merge(ConfigFile higher, ConfigFile lower);
public string ToJSON();
public bool HasGroup(string groupName);
public ConfigFileGroup GetGroup(string groupName);
public ConfigFileGroup GetOrAddGroup(string groupName);
public ConfigFileGroup AddGroup(string groupName);
public bool RemoveGroup(string groupName);
public IReadOnlyCollection<string> GetGroupNames();
public bool HasProperty(string groupName, string propertyName);
public bool TryGetBool (string group, string property, out bool value);
public bool TryGetInt (string group, string property, out int value);
public bool TryGetFloat (string group, string property, out float value);
public bool TryGetDouble (string group, string property, out double value);
public bool TryGetString (string group, string property, out string value);
public bool TryGetColor (string group, string property, out Color value);
public bool TryGetGroup (string group, string property, out ConfigFileGroup value);
public bool TryGetArray (string group, string property, out IReadOnlyList<ConfigFileProperty> value);
public void SetBool (string group, string property, bool value);
public void SetInt (string group, string property, int value);
public void SetFloat (string group, string property, float value);
public void SetDouble (string group, string property, double value);
public void SetString (string group, string property, string value);
public void SetColor (string group, string property, Color value);
public void SetGroup (string group, string property, ConfigFileGroup value);
public void SetArray (string group, string property, List<ConfigFileProperty> value);
public void SetList<T>(string group, string property,
IReadOnlyList<T> items, Func<T, int, ConfigFileGroup> toGroup);
public bool TryGetList<T>(string group, string property,
out List<T> items, Func<ConfigFileGroup, T> fromGroup);
}
public sealed class ConfigFileGroup
{
public ConfigFileGroup(string name);
public string Name { get; }
public int PropertyCount { get; }
public bool HasProperty(string propertyName);
public ConfigFileProperty GetProperty(string propertyName);
public bool TryGetProperty(string propertyName, out ConfigFileProperty value);
public void SetProperty(string propertyName, ConfigFileProperty value);
public bool RemoveProperty(string propertyName);
public IReadOnlyCollection<string> GetPropertyNames();
public IEnumerable<KeyValuePair<string, ConfigFileProperty>> GetProperties();
/* Typed convenience accessors mirror the ConfigFile API (one fewer string argument). */
public void SetBool (string property, bool value);
public void SetInt (string property, int value);
public void SetFloat (string property, float value);
public void SetDouble (string property, double value);
public void SetString (string property, string value);
public void SetColor (string property, Color value);
public void SetGroup (string property, ConfigFileGroup value);
public void SetArray (string property, List<ConfigFileProperty> value);
public bool TryGetBool (string property, out bool value);
public bool TryGetInt (string property, out int value);
public bool TryGetFloat (string property, out float value);
public bool TryGetDouble (string property, out double value);
public bool TryGetString (string property, out string value);
public bool TryGetColor (string property, out Color value);
public bool TryGetGroup (string property, out ConfigFileGroup value);
public bool TryGetArray (string property, out IReadOnlyList<ConfigFileProperty> value);
}
public readonly struct ConfigFileProperty : IEquatable<ConfigFileProperty>
{
public ConfigFilePropertyType Type { get; }
public bool IsInitialized { get; }
public bool BoolValue { get; } /* throws if Type != Bool */
public int IntValue { get; } /* throws if Type != Int */
public float FloatValue { get; } /* throws if Type != Float */
public double DoubleValue { get; } /* throws if Type != Double */
public string StringValue { get; } /* throws if Type != String */
public ConfigFileGroup GroupValue { get; } /* throws if Type != Group */
public IReadOnlyList<ConfigFileProperty> ArrayValue { get; } /* throws if Type != Array */
public static ConfigFileProperty Bool (bool v);
public static ConfigFileProperty Int (int v);
public static ConfigFileProperty Float (float v);
public static ConfigFileProperty Double(double v);
public static ConfigFileProperty String(string v);
public static ConfigFileProperty Group (ConfigFileGroup v);
public static ConfigFileProperty Array (List<ConfigFileProperty> v);
public static bool operator ==(ConfigFileProperty left, ConfigFileProperty right);
public static bool operator !=(ConfigFileProperty left, ConfigFileProperty right);
}
public enum ConfigFilePropertyType
{
Bool = 0,
Int = 1,
Float = 2,
Double = 3,
String = 4,
Group = 5,
Array = 6
}
}
See the API reference for full signatures, remarks, and exception contracts on every member.
Best Practices
- Treat the four types as pure data. The configuration-system layer (
ConfigFileManager,ScyllaConfiguration) owns the policy decisions about where files live, which override wins, and how runtime assets respond to changes. The types on this page just store and serialize. Keep that split clean. - Reach for
TryGet*methods onConfigFileandConfigFileGroupwhen reading parsed JSON. Direct typed accessors onConfigFilePropertythrow on a type mismatch and are for code paths where the type is already known (callers that have just inspectedType). - Use named arguments in
SetX/TryGetXcalls.file.SetFloat(group: "Tuning", property: "MaxSpeed", value: 25f)is dramatically more readable thanfile.SetFloat("Tuning", "MaxSpeed", 25f)for a reader who has not seen the API before. - Use
SetList<T>andTryGetList<T>for record arrays. The delegate-based pattern keeps the record’s field list in one place and removes the index bookkeeping of hand-rolled array element loops. - Use
SetColor/TryGetColorrather than encoding hex strings by hand. The helpers handle the 8-digit ARGB encoding, accept 6-digit RGB on read, and normalize casing on output. - Keep one element type per array. Mixed-type arrays are supported but indicate a schema problem in almost every real-world case. Reach for a
Groupwith named fields when polymorphism is actually needed. - Cache the result of
Mergerather than recomputing per frame. Merge deep-clones every value and the cost is proportional to the size of the inputs; caching the effective file is the right shape for steady-state config reads. - Reach for
GetOrAddGroupwhen authoring configuration. It is the right abstraction for “I want this group whether or not it exists yet”.AddGroupthrows on duplicates;GetGroupthrows on missing. Both are appropriate when guarded;GetOrAddGroupdoes not need a guard. - Detect a freshly-constructed
default(ConfigFileProperty)viaIsInitialized. Typed-accessor calls on a default-constructed property throw;IsInitializedis the cheap check to run at the boundary. - Stay alert to numeric widening rules.
Intreads asFloatandDouble;Floatreads asDouble; butDoubledoes not read asIntorFloat. The type stored on disk controls the conversion, not the runtime value of the number. - Stay alert to the
FloatvsDoubleJSON ambiguity. A JSON literal like1.5always parses asDoublebecause the parser cannot tell which numeric category the author intended.SetFloatprogrammatically writes theFloattag; round-tripping that field through JSON converts the tag toDoubleon re-parse. - Treat the alphabetical ordering as a feature, not a constraint. Diffs between two saved configs are stable and meaningful precisely because the output is deterministic. Resist the temptation to introduce an “insertion order” preservation mode; it breaks the diff story.
Pitfalls
Related
- Configuration
- Serialization Utils
- File IO Utils
- API reference:
Scylla.Core.Util.File.ConfigFile - API reference:
Scylla.Core.Util.File.ConfigFileGroup - API reference:
Scylla.Core.Util.File.ConfigFileProperty - API reference:
Scylla.Core.Util.File.ConfigFilePropertyType