modde-games 0.2.1

Game plugin implementations for modde
Documentation
//! Loads user-defined `OptiScaler` profiles from per-game `*.optiscaler.toml`
//! files in the modde data directory, converting the deserialized spec types
//! into leaked-`'static` [`OptiScalerProfile`] values cached per game id.

use std::fs;
use std::sync::OnceLock;

use dashmap::DashMap;
use tracing::warn;

use crate::generic::leak::{slice as leak_slice, str as leak_str};

use super::spec::{IniOverrideSpec, OptiScalerProfileSpec, OptiScalerProfilesSpec};
use super::{OptiScalerIniOverride, OptiScalerProfile};

static USER_PROFILE_CACHE: OnceLock<DashMap<String, &'static [OptiScalerProfile]>> =
    OnceLock::new();

fn cache() -> &'static DashMap<String, &'static [OptiScalerProfile]> {
    USER_PROFILE_CACHE.get_or_init(DashMap::new)
}

#[must_use]
pub fn load_user_profiles(game_id: &str) -> &'static [OptiScalerProfile] {
    if let Some(profiles) = cache().get(game_id) {
        return *profiles;
    }

    let path = modde_core::paths::modde_data_dir()
        .join("games")
        .join(format!("{game_id}.optiscaler.toml"));

    let profiles = match fs::read_to_string(&path) {
        Ok(content) => match toml::from_str::<OptiScalerProfilesSpec>(&content) {
            Ok(spec) => load_profiles_from_spec(spec),
            Err(error) => {
                warn!(path = %path.display(), error = %error, "skipping user OptiScaler profile spec");
                &[]
            }
        },
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => &[],
        Err(error) => {
            warn!(path = %path.display(), error = %error, "skipping user OptiScaler profile spec");
            &[]
        }
    };

    *cache().entry(game_id.to_string()).or_insert(profiles)
}

fn load_profiles_from_spec(spec: OptiScalerProfilesSpec) -> &'static [OptiScalerProfile] {
    let profiles = spec
        .profile
        .into_iter()
        .map(convert_profile)
        .collect::<Vec<_>>();
    leak_slice(profiles)
}

fn convert_profile(spec: OptiScalerProfileSpec) -> OptiScalerProfile {
    OptiScalerProfile {
        id: leak_str(spec.id),
        name: leak_str(spec.name),
        source_url: leak_str(spec.source_url),
        tested_optiscaler_version: leak_str(spec.tested_optiscaler_version),
        source_mode: spec.source_mode.map(leak_str),
        goverlay_channel: spec.goverlay_channel.map(leak_str),
        proxy_dll: leak_str(spec.proxy_dll),
        release_tag: spec.release_tag.map(leak_str),
        release_asset: spec.release_asset.map(leak_str),
        wine_dll_overrides: leak_slice(
            spec.wine_dll_overrides
                .into_iter()
                .map(leak_str)
                .collect::<Vec<_>>(),
        ),
        copy_companion_files: spec.copy_companion_files,
        enable_optipatcher: spec.enable_optipatcher,
        fsr4_variant: spec.fsr4_variant.map(leak_str),
        emulate_fp8: spec.emulate_fp8,
        spoof_dlss: spec.spoof_dlss,
        ini_overrides: leak_slice(
            spec.ini_overrides
                .into_iter()
                .map(convert_ini_override)
                .collect::<Vec<_>>(),
        ),
        notes: leak_str(spec.notes),
    }
}

fn convert_ini_override(spec: IniOverrideSpec) -> OptiScalerIniOverride {
    OptiScalerIniOverride {
        key: leak_str(spec.key),
        value: leak_str(spec.value),
    }
}