dot-settings declares a game’s settings once, with types, ranges and scopes,
and stores them through a pluggable store. The alternative — a dictionary of
whatever somebody happened to save — cannot be enumerated or validated, and a
typo’d key is indistinguishable from a setting nobody has chosen yet.
Requires dot-core.
Usage
schema.add(DotSettingsDef.number(&"master_volume", 0.8, 0.0, 1.0, &"audio"))
schema.add(DotSettingsDef.integer(&"fov", 90, 70, 120, &"video")
.with_scope(DotSettingsDef.Scope.SERVER_CLAMPED))
var settings := DotSettingsManager.new()
settings.schema = my_schema
settings.local_store = DotSettingsStoreFile.new()
settings.app_namespace = &"my_game"
settings.shared_namespace = &"tmc_account" # optional
add_child(settings)
settings.setup()
settings.get_float(&"master_volume")
settings.set_value(&"master_volume", 0.4) # saved, debounced, atomically
A screen enumerates the schema, a console completes against it, a store validates against it and a migration rewrites against it, so a new setting appears in all four without editing four files.
to_config() returns a DotConfig whose properties are the schema’s keys, and
dot-ui generates a panel from any DotConfig — so
the settings screen is free, and nothing in this addon names a Control.
from_config goes the other way, deriving a schema from a DotConfig a project
already has.
Three scopes
DEVICE |
Resolution, audio device, thread count. Never synchronised: a synchronised resolution breaks itself every time a player switches machines. |
ACCOUNT |
Sensitivity, crosshair, subtitles, language. Stored under shared_namespace, so two games that opt into the same name share one sensitivity. |
SERVER_CLAMPED |
Field of view, view models, third person. A server may cap these. |
The newest choice of an account setting wins, key by key. Both the game’s own document and the shared one hold every account key, and each records when a value was last chosen (stamps beside values, _stamps beside the flat values in the shared document). Load takes the shared value when its stamp is at least as new, and a save reads the shared document again first and does the same, so a game left open for an hour neither writes back a sensitivity another game changed nor keeps showing the old one. “The shared document always wins” would only move the lost change one save later, and “the local copy wins” would mean a change never reaches a game that has saved once. A reset is stamped like any other choice. Files from before stamps count as never chosen, so the shared value wins once. Stamps use wall clocks, so two machines whose clocks disagree decide it between them.
A value nobody chose follows its default
A document stores every value, so a value equal to its default looks like a choice. The document therefore records the default each value was saved under (a defaults section beside values): a value still on the default it was saved under moves to the new one when a build changes it, once, and a value that differs from its recorded default is kept. A document saved before that record has no defaults, so a setting declares what it used to be with with_previous_defaults([...]) (previous_defaults on the DotSettingsDef), and a stored value equal to one of those moves once. A player who really chose the old default loses that one choice, the price of not leaving everybody else on the old key for ever. Never strip the defaults section: it is neither a setting nor an unknown key, and is rewritten on every save.
A server may clamp, and may never read
Capping field of view is a legitimate competitive rule. Asking a client to report its key bindings, its audio device or its screen resolution is a fingerprint — a stable identifier assembled from settings, which survives a new account and every ban a moderator issues. There is no read direction in this addon at all.
A clamp is a bound, not a value:
settings.apply_server_clamps({"fov": 90})
settings.get_value(&"fov") # 90 for a player who wanted 110
settings.chosen_value(&"fov") # 110, what the screen shows
What is saved is the choice, never the cap. Games that get this wrong get it wrong in the same direction, and the symptom is a setting that appears to reset itself on every join.
Writes
Debounced by a few hundred milliseconds so dragging a slider is one write, and
written atomically through a temporary file and a rename — a game that saves on
exit loses them whenever it crashes, and shutting down is when it is most likely
to. On the web user:// is an IndexedDB mirror, so every write path flushes it.
Unknown keys are kept, and that is the downgrade path
A player who runs a newer build, changes a setting it added and goes back would otherwise lose it silently, and lose it again every time. The unknown half is stored beside the known half and written back untouched.
A hand-edited file loses one line, not all of them: a value out of range is corrected, a value that cannot be read falls back to the default, and the two are reported separately because they are different things to tell a player.
