{ config, pkgs, lib, inputs, ... }: let inherit (inputs.self.lib.lib) userIs; inherit (config.lib.steam) mkSharePath; inherit (lib.options) mkOption mkEnableOption; inherit (lib.modules) mkIf mkMerge mkDefault mkOptionDefault; inherit (lib.strings) hasSuffix replaceStrings optionalString concatStringsSep escapeShellArg makeBinPath versionOlder; inherit (lib.attrsets) attrValues filterAttrs mapAttrs mapAttrs' mapAttrsToList listToAttrs nameValuePair; inherit (lib.lists) concatMap head last filter sort singleton; inherit (config.services.steam) accountSwitch; cfg = config.services.steam.beatsaber; sortedVersions = sort (a: b: versionOlder a.version b.version) (attrValues cfg.versions); prevVersionFor = version: let olderVersions = filter (v: versionOlder v.version version) sortedVersions; in if olderVersions != [] then last olderVersions else null; versionModule = { config, name, ... }: { options = with lib.types; { version = mkOption { type = str; default = name; }; previousVersion = mkOption { type = nullOr str; }; __toString = mkOption { type = functionTo str; }; }; config = { __toString = mkOptionDefault ( config: config.version ); previousVersion = mkOptionDefault ( prevVersionFor config.version ); }; }; fileModule = { config, name, ... }: { options = with lib.types; { relativePath = mkOption { type = str; default = name; }; type = mkOption { type = enum [ "file" "directory" ]; default = "file"; }; versioned = mkOption { type = bool; default = false; }; target = mkOption { type = enum [ "user" "shared" "game" ]; default = "user"; }; mode = { file = mkOption { type = str; default = if hasSuffix ".exe" config.relativePath || hasSuffix ".dll" config.relativePath then "775" else "664"; }; dir = mkOption { type = str; default = "2775"; }; }; ownerFor = mkOption { type = functionTo str; }; srcPathFor = mkOption { type = functionTo path; }; srcStyle = mkOption { type = enum [ "empty" "copy" "symlink" "symlink-shallow" ]; default = "symlink"; }; workingPathFor = mkOption { type = functionTo path; }; init = mkOption { type = nullOr path; }; initFor = mkOption { type = functionTo (nullOr path); }; initStyle = mkOption { type = enum [ "none" "copy" "symlink" "symlink-shallow" ]; default = "copy"; }; setup = { shared = mkOption { type = functionTo lines; internal = true; }; script = mkOption { type = functionTo lines; internal = true; }; }; }; config = let versionPathFor = version: optionalString config.versioned "/${version}"; in { init = mkOptionDefault ( if config.target == "game" then null else if config.type == "directory" then "${emptyDir}" else if hasSuffix ".json" config.relativePath then "${emptyJson}" else if hasSuffix ".dll" config.relativePath || hasSuffix ".exe" config.relativePath then "${emptyExecutable}" else "${emptyFile}" ); initFor = mkOptionDefault ( { user, version }: config.init ); ownerFor = mkOptionDefault (user: if config.target == "user" then user else "admin" ); srcPathFor = mkOptionDefault ({ user, version }: { shared = cfg.sharedDataDir + versionPathFor version; user = cfg.dataDirFor user + versionPathFor version; game = cfg.gameDirFor version; }.${config.target} or (throw "unsupported target") + "/${config.relativePath}" ); workingPathFor = mkOptionDefault ({ user, version }: cfg.workingDirFor { inherit user version; } + "/${config.relativePath}" ); # TODO: setup.shared and do inits seperately! setup.script = { user, version }@args: let owner = config.ownerFor user; srcPath = config.srcPathFor args; workingPath = config.workingPathFor args; initPath = config.initFor args; parentWorkingPath = dirOf workingPath; mkdir = dest: '' if [[ -L ${escapeShellArg dest} ]]; then rm -f ${escapeShellArg dest} fi if [[ ! -d ${escapeShellArg dest} ]]; then mkdir -m${config.mode.dir} ${escapeShellArg dest} else chmod ${config.mode.dir} ${escapeShellArg dest} fi chown ${owner}:${cfg.group} ${escapeShellArg dest} ''; mkStyle = { style, src }: if style != "none" && src == { file = "${emptyFile}"; directory = "${emptyDir}"; }.${config.type} then "empty" else style; doInit = { style, src, dest }: { none = "true"; copy = { file = '' if [[ -L ${escapeShellArg dest} ]]; then rm -f ${escapeShellArg dest} elif [[ -e ${escapeShellArg dest} ]]; then echo ERR: something is in the way of copying ${escapeShellArg dest} >&2 exit 1 fi cp -TP --no-preserve=all ${escapeShellArg src} ${escapeShellArg dest} chmod ${config.mode.file} ${escapeShellArg dest} chown ${owner}:${cfg.group} ${escapeShellArg dest} ''; directory = '' ${mkdir dest} cp -rTP --no-preserve=all ${escapeShellArg src} ${escapeShellArg dest} chown -R ${owner}:${cfg.group} ${escapeShellArg dest} find ${escapeShellArg dest} -type f -exec chmod -m${config.mode.file} "{}" \; ''; }.${config.type}; empty = { directory = '' ${mkdir dest} ''; file = '' touch ${escapeShellArg dest} chmod ${config.mode.file} ${escapeShellArg dest} chown ${owner}:${cfg.group} ${escapeShellArg dest} ''; }.${config.type}; symlink = '' if [[ -e ${escapeShellArg dest} && ! -L ${escapeShellArg dest} ]]; then echo ERR: something is in the way of linking ${escapeShellArg dest} >&2 exit 1 fi ln -sfT ${escapeShellArg src} ${escapeShellArg dest} ''; symlink-shallow = { directory = '' ${mkdir dest} ln -sf ${escapeShellArg src}/* ${escapeShellArg dest}/ ''; }.${config.type}; }.${mkStyle { inherit style src; }}; doSetup = { style, src, dest }: rec { none = "true"; copy = { file = '' ${empty} ''; directory = '' ${empty} if [[ ${escapeShellArg dest}/* != ${escapeShellArg dest}/\* ]]; then chmod -m${config.mode.file} ${escapeShellArg dest}/* fi ''; }.${config.type}; empty = { directory = '' chmod ${config.mode.dir} ${escapeShellArg dest} chown ${owner}:${cfg.group} ${escapeShellArg dest} ''; file = '' chmod ${config.mode.file} ${escapeShellArg dest} chown ${owner}:${cfg.group} ${escapeShellArg dest} ''; }.${config.type}; symlink = "true"; symlink-shallow = { directory = '' ${mkdir.directory} ''; }.${config.type}; }.${mkStyle { inherit style src; }}; init = doInit { style = config.initStyle; src = initPath; dest = srcPath; }; setup = doSetup { style = config.initStyle; src = initPath; dest = srcPath; }; src = doInit { style = config.srcStyle; src = srcPath; dest = workingPath; }; checkFlag = { file = { none = "e"; copy = "f"; symlink = "L"; }.${config.initStyle}; directory = { none = "e"; copy = "d"; symlink-shallow = "d"; symlink = "L"; }.${config.initStyle}; }.${config.type}; checkParent = '' if [[ ! -d ${escapeShellArg parentWorkingPath} ]]; then echo ERR: parent of ${escapeShellArg workingPath} does not exist >&2 exit 1 fi ''; check = if initPath != null then '' if [[ ! -${checkFlag} ${escapeShellArg srcPath} ]]; then ${init} else ${setup} fi '' else '' if [[ ! -${checkFlag} ${escapeShellArg srcPath} ]]; then echo ERR: src ${escapeShellArg srcPath} for ${escapeShellArg workingPath} does not exist >&2 exit 1 fi ''; in '' ${checkParent} ${check} ${src} ''; }; }; emptyFile = pkgs.writeText "empty.txt" ""; emptyJson = pkgs.writeText "empty.json" "{}"; emptyDir = pkgs.runCommand "empty" { } '' mkdir $out ''; emptyExecutable = pkgs.writeTextFile { name = "empty.exe"; executable = true; text = ""; }; bsdata = "Beat Saber_Data"; vars = let bsUserData = mkSharePath (cfg.dataDirFor "%GENSO_STEAM_USER%"); bsWorkingData = mkSharePath (cfg.workingDirFor { user = "%GENSO_STEAM_USER%"; version = "%GENSO_STEAM_BS_VERSION%"; }); in '' if "%GENSO_STEAM_MACHINE%" == "" set "GENSO_STEAM_MACHINE=%COMPUTERNAME%" if "%GENSO_STEAM_LOCAL_DATA%" == "" set "GENSO_STEAM_LOCAL_DATA=C:\Program Files\GensokyoZone" if "%GENSO_STEAM_LOCAL_DATA_BS%" == "" set "GENSO_STEAM_LOCAL_DATA_BS=%GENSO_STEAM_LOCAL_DATA%\${cfg.dirName}" if "%GENSO_STEAM_INSTALL%" == "" set "GENSO_STEAM_INSTALL=C:\Program Files (x86)\Steam" if "%GENSO_STEAM_LIBRARY_BS%" == "" set "GENSO_STEAM_LIBRARY_BS=%GENSO_STEAM_INSTALL%" if "%GENSO_STEAM_BS_VERSION%" == "" set "GENSO_STEAM_BS_VERSION=${cfg.defaultVersion}" if "%GENSO_SMB_HOST%" == "" set "GENSO_SMB_HOST=smb.${config.networking.domain}" if "%GENSO_SMB_SHARED_MOUNT%" == "" set "GENSO_SMB_SHARED_MOUNT=\\%GENSO_SMB_HOST%\shared" set "STEAM_BS_LIBRARY=%GENSO_STEAM_LIBRARY_BS%\steamapps\common\Beat Saber" set "STEAM_BS_APPDATA=%USERPROFILE%\AppData\LocalLow\Hyperbolic Magnetism\Beat Saber" set "STEAM_BS_LOCAL_DATA=%GENSO_STEAM_LOCAL_DATA_BS%\%GENSO_STEAM_BS_VERSION%" set "STEAM_BS_USER_DATA=${bsUserData}" set "STEAM_BS_WORKING_DATA=${bsWorkingData}" if "%GENSO_STEAM_BS_LOCAL%" == "1" ( set "STEAM_BS_LAUNCH=%STEAM_BS_LOCAL_DATA%" set "STEAM_BS_LAUNCH_APPDATA=%STEAM_BS_LOCAL_DATA%\AppData" ) else ( set "STEAM_BS_LAUNCH=%STEAM_BS_WORKING_DATA%" set "STEAM_BS_LAUNCH_APPDATA=%STEAM_BS_USER_DATA%\AppData" ) if "%GENSO_STEAM_USER%" == "" goto NOUSER ''; eof = '' goto:eof :NOUSER echo no steam user set ''; mount = '' rmdir "%STEAM_BS_APPDATA%" mklink /D "%STEAM_BS_APPDATA%" "%STEAM_BS_LAUNCH_APPDATA%" rmdir "%STEAM_BS_LIBRARY%" mklink /D "%STEAM_BS_LIBRARY%" "%STEAM_BS_LAUNCH%" ''; setup = '' rmdir "%STEAM_BS_APPDATA%" rmdir "%STEAM_BS_LIBRARY%" mkdir "%GENSO_STEAM_LOCAL_DATA_BS%" move /Y "%STEAM_BS_LIBRARY%" "%GENSO_STEAM_LOCAL_DATA_BS%\Vanilla" move /Y "%STEAM_BS_APPDATA%" "%GENSO_STEAM_LOCAL_DATA_BS%\Vanilla\AppData" ''; mountbeatsaber = '' ${vars} ${mount} ${eof} ''; launchbeatsaber = '' ${vars} ${mount} cd /d "%STEAM_BS_LIBRARY%" "%STEAM_BS_LIBRARY%\Beat Saber.exe" ${eof} ''; fpfcbeatsaber = '' ${vars} cd /d "%STEAM_BS_LIBRARY%" "%STEAM_BS_LIBRARY%\Beat Saber.exe" fpfc ${eof} ''; setupbeatsaber = '' ${vars} ${setup} pause ${eof} ''; localbeatsaber-mount = '' set GENSO_STEAM_BS_LOCAL=1 ${vars} ${mount} if NOT "%GENSO_STEAM_BS_VERSION%" == "Vanilla" ( rmdir "%STEAM_BS_LIBRARY%\UserData" mklink /D "%STEAM_BS_LIBRARY%\UserData" "%STEAM_BS_WORKING_DATA%\UserData" ) ${eof} ''; localbeatsaber-vanilla = '' set GENSO_STEAM_BS_VERSION=Vanilla set GENSO_STEAM_BS_LOCAL=1 ${vars} ${mount} ${eof} ''; localbeatsaber-launch = '' set GENSO_STEAM_BS_LOCAL=1 ${vars} ${mount} cd /d "%STEAM_BS_LIBRARY%" "%STEAM_BS_LIBRARY%\Beat Saber.exe" ${eof} ''; vanilla = '' setx GENSO_STEAM_BS_VERSION Vanilla ''; mksetupbeatsaber = { user, version }: let setupFiles = mapAttrsToList (_: file: file.setup.script { inherit user version; }) cfg.files; in pkgs.writeShellScript "setupbeatsaber-${user}-${version}" '' set -eu export PATH="$PATH:${makeBinPath [ pkgs.coreutils ]}" ${concatStringsSep "\n" setupFiles} ''; in { options.services.steam.beatsaber = with lib.types; { enable = mkEnableOption "beatsaber scripts"; setup = mkEnableOption "beatsaber data" // { default = accountSwitch.setup; }; group = mkOption { type = str; default = "beatsaber"; }; defaultVersion = mkOption { type = str; }; versions = mkOption { type = attrsOf (submodule versionModule); default = { }; }; setupServiceNames = mkOption { type = listOf str; readOnly = true; }; files = mkOption { type = attrsOf (submodule fileModule); default = { }; }; users = mkOption { type = listOf str; }; dirName = mkOption { type = str; default = "BeatSaber"; }; binDir = mkOption { type = path; default = accountSwitch.binDir + "/beatsaber"; }; gamesDir = mkOption { type = path; default = accountSwitch.gamesDir + "/${cfg.dirName}"; }; gameDirFor = mkOption { type = functionTo path; default = version: cfg.gamesDir + "/${version}"; }; sharedDataDir = mkOption { type = path; default = accountSwitch.sharedDataDir + "/${cfg.dirName}"; }; userDataDir = mkOption { type = path; default = accountSwitch.dataDir; }; dataDirFor = mkOption { type = functionTo path; default = user: cfg.userDataDir + "/${user}/${cfg.dirName}"; }; userWorkingDir = mkOption { type = path; default = accountSwitch.workingDir; }; userWorkingDirFor = mkOption { type = functionTo path; default = user: cfg.userWorkingDir + "/${user}/${cfg.dirName}"; }; workingDirFor = mkOption { type = functionTo path; default = { user, version }: cfg.userWorkingDirFor user + "/${version}"; }; }; config = { services.steam.beatsaber = let bsUsers = filterAttrs (_: userIs cfg.group) config.users.users; allVersions = mapAttrsToList (_: version: version.version) cfg.versions; gameFiles = { "Beat Saber.exe" = { }; "UnityCrashHandler64.exe" = { }; "UnityPlayer.dll" = { }; "MonoBleedingEdge".type = "directory"; }; sharedFiles = { DynamicOpenVR = { type = "directory"; versioned = true; }; IPA = { type = "directory"; versioned = true; }; Libs = { type = "directory"; versioned = true; }; Plugins = { type = "directory"; versioned = true; }; Logs = { type = "directory"; versioned = true; }; "BeatSaberVersion.txt" = { versioned = true; initFor = { version, ... }: pkgs.writeText "BeatSaberVersion-${version}.txt" version; }; "IPA.exe".versioned = true; "IPA.exe.config".versioned = true; "IPA.runtimeconfig.json".versioned = true; "winhttp.dll".versioned = true; ${bsdata} = { type = "directory"; versioned = true; #initStyle = "symlink-shallow"; #initFor = { version, ... }: cfg.gameDirFor version + "/${bsdata}"; initStyle = "none"; srcPathFor = { version, ... }: cfg.gameDirFor version + "/${bsdata}"; srcStyle = "symlink-shallow"; }; "${bsdata}/Managed" = { type = "directory"; versioned = true; initFor = { version, ... }: cfg.gameDirFor version + "/${bsdata}/Managed"; }; # TODO: remove this to use multiple folders "${bsdata}/CustomLevels" = { type = "directory"; initStyle = "none"; srcPathFor = { ... }: cfg.sharedDataDir + "/CustomLevels"; }; CustomAvatars = { type = "directory"; initStyle = "none"; }; CustomNotes = { type = "directory"; initStyle = "none"; }; CustomPlatforms = { type = "directory"; initStyle = "none"; }; CustomSabers = { type = "directory"; initStyle = "none"; }; CustomWalls = { type = "directory"; initStyle = "none"; }; Playlists = { type = "directory"; initStyle = "none"; }; "UserData/ScoreSaber/Replays" = { type = "directory"; initStyle = "none"; srcPathFor = { ... }: cfg.sharedDataDir + "/Replays"; }; "UserData/Beat Saber IPA.json".versioned = true; "UserData/SongCore/" = { versioned = true; relativePath = "UserData/SongCore"; type = "directory"; srcStyle = "empty"; }; "UserData/SongCore/folders.xml" = { versioned = true; }; "UserData/SongCore/SongCoreExtraData.dat" = { versioned = true; init = "${emptyJson}"; }; "UserData/SongCore/SongDurationCache.dat" = { versioned = true; init = "${emptyJson}"; }; "UserData/SongCore/SongHashData.dat" = { versioned = true; init = "${emptyJson}"; }; "UserData/Chroma".type = "directory"; "UserData/Nya".type = "directory"; "UserData/SongRankedBadge".type = "directory"; "UserData/DrinkWater".type = "directory"; "UserData/Enhancements".type = "directory"; "UserData/HitScoreVisualizer" = { type = "directory"; # TODO: initStyle = "symlink"; init = "${myhitscorejsons}"; }; "UserData/Saber Factory/" = { relativePath = "UserData/Saber Factory"; type = "directory"; srcStyle = "empty"; }; "UserData/Saber Factory/Cache".type = "directory"; "UserData/Saber Factory/Textures".type = "directory"; "UserData/BeatSaverDownloader.ini" = { }; "UserData/BeatSaverUpdater.json" = { }; "UserData/SongDetailsCache.proto".versioned = true; "UserData/SongDetailsCache.proto.Direct.etag".versioned = true; }; userFiles = { "UserData" = { type = "directory"; versioned = true; srcStyle = "empty"; }; "UserData/Camera2".type = "directory"; "UserData/Saber Factory" = { type = "directory"; srcStyle = "empty"; }; "UserData/Saber Factory/Presets".type = "directory"; "UserData/Saber Factory/TrailConfig.json" = { }; "UserData/SongCore" = { type = "directory"; versioned = true; srcStyle = "empty"; }; "UserData/SongCore/SongCore.json" = { versioned = true; }; "UserData/ScoreSaber" = { type = "directory"; versioned = true; srcStyle = "empty"; }; "UserData/ScoreSaber/ScoreSaber.json" = { versioned = true; }; "UserData/Disabled Mods.json".versioned = true; "UserData/modprefs.ini".versioned = true; "UserData/JDFixer.json".versioned = true; }; userDataFiles = [ "modprefs.ini" "Disabled Mods.json" "AutoPauseStealth.json" "BeatSaberMarkupLanguage.json" "BeatSaviorData.ini" "BetterSongList.json" "BetterSongSearch.json" "bookmarkedSongs.json" "votedSongs.json" "Chroma.json" "Cinema.json" "CountersPlus.json" "CustomAvatars.CalibrationData.dat" "CustomAvatars.json" "CustomNotes.json" "Custom Platforms.json" "CustomWalls.json" "DrinkWater.json" "EasyOffset.json" "Enhancements.json" "FasterScroll.json" "FastFail.json" "Fifth Anniversary Mod.json" "Gotta Go Fast.json" "Heck.json" "HitScoreVisualizer.json" "HitsoundTweaks.json" "Intro Skip.json" "JDFixer.json" "ModelDownloader.json" "Music Spatializer.json" "Nya.json" "ParticleOverdrive.ini" "PerformanceMeter.json" "PlayFirst.json" "PlaylistManager.json" "PracticePlugin.json" "Saber Factory.json" "SaberTailor.json" "ScorePercentage.json" "SiraUtil.json" "SmoothCamPlus.json" "SongChartVisualizer.json" "SongPlayData.json" "SongPlayHistory.json" "SongRankedBadge.json" "Technicolor.json" "Tweaks55.json" "UITweaks.json" ]; mapSharedFile = file: file // { target = "shared"; }; mapGameFile = file: file // { target = "game"; }; mapUserDataFile = file: nameValuePair "UserData/${file}" { target = "user"; }; in { defaultVersion = mkIf (allVersions != [ ]) (mkOptionDefault ( head allVersions )); users = mkOptionDefault ( mapAttrsToList (_: user: user.name) bsUsers ); setupServiceNames = mkOptionDefault ( map (user: "steam-setup-beatsaber-${user}.service") cfg.users ); files = mkMerge [ userFiles (listToAttrs (map mapUserDataFile userDataFiles)) (mapAttrs (_: mapGameFile) gameFiles) (mapAttrs (_: mapSharedFile) sharedFiles) ]; }; systemd.services.steam-setup-beatsaber = mkIf cfg.setup { wantedBy = [ "multi-user.target" ]; after = [ "tmpfiles.service" ]; serviceConfig = { Type = mkOptionDefault "oneshot"; RemainAfterExit = mkOptionDefault true; ExecStart = mkMerge (map (user: (mapAttrsToList (_: version: "${mksetupbeatsaber { inherit user; inherit (version) version; }}" ) cfg.versions) ) cfg.users); }; }; services.tmpfiles = let toplevel = { owner = mkDefault "admin"; group = mkDefault cfg.group; mode = mkDefault "3775"; }; shared = { inherit (toplevel) owner group; mode = mkDefault "2775"; }; personal = owner: { inherit owner; inherit (shared) group mode; }; bin = { inherit (toplevel) owner group; mode = "0755"; type = "copy"; }; sharedFolders = [ "CustomAvatars" "CustomLevels" "CustomNotes" "CustomPlatforms" "CustomSabers" "CustomWalls" "Playlists" "Replays" "AppData" "UserData" ]; setupFiles = [ { ${cfg.sharedDataDir} = toplevel; ${cfg.binDir} = shared; } (listToAttrs ( map (folder: nameValuePair "${cfg.sharedDataDir}/${folder}" shared ) sharedFolders )) ] ++ concatMap (owner: singleton { ${cfg.dataDirFor owner} = personal owner; "${cfg.dataDirFor owner}/AppData" = personal owner; "${cfg.dataDirFor owner}/UserData" = personal owner; } ++ mapAttrsToList (_: version: { "${cfg.dataDirFor owner}/${version.version}" = personal owner; ${cfg.userWorkingDirFor owner} = personal owner; ${cfg.workingDirFor { user = owner; inherit (version) version; }} = personal owner; }) cfg.versions ) cfg.users ++ mapAttrsToList (_: version: { "${cfg.sharedDataDir}/${version.version}" = shared; }) cfg.versions; versionBinFiles = mapAttrs' (_: version: nameValuePair "${cfg.binDir}/${replaceStrings [ "." ] [ "_" ] version.version}.bat" { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-${version.version}.bat"; executable = true; text = '' setx GENSO_STEAM_BS_VERSION ${version.version} ''; }; } ) cfg.versions; binFiles = { "${cfg.binDir}/mount.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-mount.bat"; executable = true; text = mountbeatsaber; }; }; "${cfg.binDir}/launch.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-launch.bat"; executable = true; text = launchbeatsaber; }; }; "${cfg.binDir}/fpfc.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-fpfc.bat"; executable = true; text = fpfcbeatsaber; }; }; "${cfg.binDir}/setup.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-setup.bat"; executable = true; text = setupbeatsaber; }; }; "${cfg.binDir}/local-launch.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-local-launch.bat"; executable = true; text = localbeatsaber-launch; }; }; "${cfg.binDir}/local-mount.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-local-mount.bat"; executable = true; text = localbeatsaber-mount; }; }; "${cfg.binDir}/local-vanilla.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-local-vanilla.bat"; executable = true; text = localbeatsaber-vanilla; }; }; "${cfg.binDir}/vanilla.bat" = { inherit (bin) owner group mode type; src = pkgs.writeTextFile { name = "beatsaber-version-vanilla.bat"; executable = true; text = vanilla; }; }; "${cfg.binDir}/ModAssistant.exe" = { inherit (bin) owner group mode type; src = pkgs.fetchurl { url = "https://github.com/Assistant/ModAssistant/releases/download/v1.1.32/ModAssistant.exe"; hash = "sha256-ozu2gYFiz+2BjptqL80DmUopbahbyGKFO1IPd7BhVPM="; executable = true; }; }; } // versionBinFiles; in { enable = mkIf cfg.setup true; files = mkIf cfg.setup (mkMerge ( singleton binFiles ++ setupFiles )); }; }; }