How the DD Poker Property Configuration System Works with User Overrides

The DD Poker property configuration system loads hierarchical .properties files and merges per-user overrides from module/override/<username>.properties when allowOverrides is enabled, with later loads overwriting earlier keys to give user settings highest precedence.

DD Poker implements a sophisticated property configuration system in its com.donohoedigital.config package that allows individual users to override default application settings without modifying shared configuration files. This mechanism, centered around the PropertyConfig class, enables personalized gameplay experiences while maintaining sane defaults for all users.

Initialization and the Loading Sequence

The configuration loading process begins when ConfigManager instantiates PropertyConfig during application startup. The constructor signature determines whether user overrides are permitted:

// Inside ConfigManager constructor
new PropertyConfig(sAppName, modules, type, locale, allowOverrides);

Source: [ConfigManager.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/ConfigManager.java)

The Module Iteration Loop

Inside PropertyConfig.init(), the loader iterates over each module (such as "common" or "poker") and loads configuration files in a specific sequence. This loop establishes the base configuration before applying any user-specific modifications:

for (String module : modules) {
    file = new File(module, PROPS_CONFIG_COMMON);
    loadURL(file, sLocale, false);          // defaults

    // type-specific file
    file = new File(module, name);
    loadURL(file, sLocale, false);

    // per-user override
    if (allowOverrides) {
        String user = ConfigUtils.getUserName();
        file = new File(module + "/override/" + user.toLowerCase() + ".properties");
        loadURL(file, sLocale, true);
    }
}

Source: [PropertyConfig.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java)

Per-User Override Resolution

When the allowOverrides parameter is true, the system calls ConfigUtils.getUserName() to determine the current operating system user. It then constructs a file path following the pattern module/override/<username>.properties (with the username converted to lowercase). The loadURL method receives true for its bOverride parameter, triggering log messages that indicate local overrides are being applied.

Override Precedence and Merging Strategy

The system employs a last-write-wins merging strategy using standard java.util.Properties.load() behavior. Configuration sources are loaded in the following precedence order, with each subsequent source overwriting existing keys:

  1. Base defaults: common.properties for each module
  2. Application-type specific: Files like client.properties or server.properties
  3. Per-user overrides: module/override/<username>.properties (loaded only when allowOverrides is true)
  4. Data-directory overrides: An optional testing.properties file in the user's client home directory

Classpath Resolution and Localization

The loadURL method resolves resources using the pattern classpath*:config/<path>. When a locale is specified, the loader first searches for localized variants (e.g., client.properties.fr) before falling back to the base file. This resolution occurs before the override logic determines whether to apply user-specific values.

Testing Overrides for Development

After the module loop completes, the system checks for ad-hoc overrides intended for unit tests or development tweaks:

RuntimeDirectory dir = new DefaultRuntimeDirectory();
File userdir = dir.getClientHome(appName);
File override = new File(userdir, "testing.properties");
if (override.exists()) {
    load(override);   // forces the testing file on top
}

Source: [PropertyConfig.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/PropertyConfig.java) and [DefaultRuntimeDirectory.java](https://github.com/dougdonohoe/ddpoker/blob/main/code/common/src/main/java/com/donohoedigital/config/DefaultRuntimeDirectory.java)

Accessing Merged Configuration Values

Once initialization completes, the merged Properties object resides in a singleton propConfig instance. All application code accesses values through static helper methods that provide type conversion and default value handling:

String maxPlayers = PropertyConfig.getStringProperty("game.maxplayers", "8");
int timeoutSec   = PropertyConfig.getIntegerProperty("network.timeout", 30);
boolean debug    = PropertyConfig.getBooleanProperty("debug.enabled", false);

These methods read from the already-merged configuration, ensuring that user overrides are transparent to the calling code.

Practical Example: Creating a User Override

Consider the default configuration in poker/client.properties:

game.maxplayers=8

A user named Alice wishes to increase the maximum to 12 seats. She creates the file poker/override/alice.properties with the following content:

game.maxplayers=12

When the application starts with new ConfigManager("poker", ApplicationType.CLIENT, true), the loading sequence executes as follows:

  1. Loads poker/common.properties and poker/client.properties, setting game.maxplayers to 8
  2. Detects allowOverrides=true and locates poker/override/alice.properties
  3. Calls loadURL(..., true), logging the override application
  4. The key game.maxplayers is overwritten to 12

Subsequent calls to PropertyConfig.getIntegerProperty("game.maxplayers", ...) return 12 for Alice, while other users continue to receive the default value of 8.

Summary

  • The property configuration system loads base files (common.properties, type-specific files) for each module before checking for user overrides.
  • When allowOverrides is enabled, the system loads per-user files from module/override/<username>.properties, with usernames normalized to lowercase via ConfigUtils.getUserName().
  • The standard java.util.Properties.load() mechanism ensures that later configuration sources overwrite earlier ones, establishing a clear precedence hierarchy.
  • An optional testing.properties file in the client home directory can supersede all other values for development purposes.
  • Runtime code accesses the merged configuration through static methods like getStringProperty() and getIntegerProperty() without needing to understand the override hierarchy.

Frequently Asked Questions

How does the system determine which user override file to load?

The system calls ConfigUtils.getUserName() to obtain the current operating system username, converts it to lowercase, and appends .properties to construct the filename. It then searches for this file within each module's override directory using the path pattern module/override/<username>.properties.

Can user overrides modify any configuration property?

Yes, the property configuration system treats all keys equally during the merge process. Any property defined in the base configuration files can be overwritten in a per-user override file, including network timeouts, game settings, or debug flags.

What happens if the user override file does not exist?

If the file does not exist, loadURL() simply returns without error, and the system continues using the values loaded from the default configuration files. This allows allowOverrides to remain enabled globally without requiring every user to create an override file.

How do localized property files interact with user overrides?

Localization occurs at the loadURL level before override logic applies. For a given base file, the system first attempts to load the locale-specific variant (e.g., client.properties.fr). The per-user override file then loads on top of whatever localized version was resolved, allowing users to override both default and localized values.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →