From 475273692dd9492a2f0ad1ee8df6440ecdd4c468 Mon Sep 17 00:00:00 2001 From: arcnmx Date: Fri, 16 Feb 2024 10:49:55 -0800 Subject: [PATCH] fix(steam): better data management and setup --- modules/nixos/steam/account-switch.nix | 89 ++- modules/nixos/steam/beatsaber.nix | 922 ++++++++++++++++++++----- modules/nixos/steam/mkbeatsaber.sh | 83 --- nixos/kyuuto/mount.nix | 8 +- nixos/steam/account-switch.nix | 4 + 5 files changed, 822 insertions(+), 284 deletions(-) delete mode 100644 modules/nixos/steam/mkbeatsaber.sh diff --git a/modules/nixos/steam/account-switch.nix b/modules/nixos/steam/account-switch.nix index 9b41bbba..333844c1 100644 --- a/modules/nixos/steam/account-switch.nix +++ b/modules/nixos/steam/account-switch.nix @@ -5,12 +5,25 @@ pkgs, ... }: let - inherit (inputs.self.lib.lib) userIs; + inherit (inputs.self.lib.lib) mkWinPath userIs; inherit (lib.options) mkOption mkEnableOption; inherit (lib.modules) mkIf mkMerge mkDefault mkOptionDefault; inherit (lib.attrsets) filterAttrs mapAttrsToList listToAttrs nameValuePair; inherit (lib.lists) singleton; + inherit (lib.strings) removePrefix; cfg = config.services.steam.accountSwitch; + machineModule = { config, name, ... }: { + options = with lib.types; { + name = mkOption { + type = str; + default = name; + }; + owner = mkOption { + type = str; + default = "admin"; + }; + }; + }; in { options.services.steam.accountSwitch = with lib.types; { enable = mkEnableOption "steam-account-switch"; @@ -52,6 +65,10 @@ in { users = mkOption { type = listOf str; }; + machines = mkOption { + type = attrsOf (submodule machineModule); + default = { }; + }; }; config = let @@ -76,35 +93,34 @@ in { inherit owner; inherit (shared) group mode; }; - setupFiles = - singleton { - ${cfg.rootDir} = toplevel; - ${cfg.binDir} = toplevel; - ${cfg.binDir + "/users"} = shared; - ${cfg.dataDir} = toplevel; - ${cfg.sharedDataDir} = shared; - ${cfg.workingDir} = toplevel; - ${cfg.sharedWorkingDir} = shared; - } - ++ map (owner: { - ${cfg.dataDir + "/${owner}"} = personal owner; - ${cfg.workingDir + "/${owner}"} = personal owner; - }) - cfg.users; - userBinFiles = listToAttrs (map (user: - nameValuePair "${cfg.binDir}/users/${user}.bat" { - inherit (toplevel) owner group; - mode = "0755"; - type = "copy"; - src = pkgs.writeTextFile { - name = "steam-${user}.bat"; - executable = true; - text = '' - setx GENSO_STEAM_USER ${user} - ''; - }; - }) - cfg.users); + setupFiles = singleton { + ${cfg.rootDir} = toplevel; + ${cfg.binDir} = toplevel; + ${cfg.binDir + "/users"} = shared; + ${cfg.dataDir} = toplevel; + ${cfg.sharedDataDir} = shared; + ${cfg.workingDir} = toplevel; + ${cfg.sharedWorkingDir} = shared; + } ++ map (owner: { + ${cfg.dataDir + "/${owner}"} = personal owner; + ${cfg.workingDir + "/${owner}"} = personal owner; + }) cfg.users + ++ mapAttrsToList (_: machine: { + ${cfg.dataDir + "/${machine.name}"} = personal machine.owner; + ${cfg.workingDir + "/${machine.name}"} = personal machine.owner; + }) cfg.machines; + userBinFiles = listToAttrs (map (user: nameValuePair "${cfg.binDir}/users/${user}.bat" { + inherit (toplevel) owner group; + mode = "0755"; + type = "copy"; + src = pkgs.writeTextFile { + name = "steam-${user}.bat"; + executable = true; + text = '' + setx GENSO_STEAM_USER ${user} + ''; + }; + }) cfg.users); in { enable = mkIf (cfg.enable || cfg.setup) true; files = mkMerge [ @@ -112,5 +128,18 @@ in { (mkIf cfg.enable userBinFiles) ]; }; + lib.steam = { + mkSharePathWith = { + path, + winRoot ? "%GENSO_SMB_SHARED_MOUNT%", + }: mkWinPath ( + winRoot + + "/${cfg.sharePath}" + + "/${removePrefix (cfg.rootDir + "/") path}" + ); + mkSharePath = path: config.lib.steam.mkSharePathWith { + inherit path; + }; + }; }; } diff --git a/modules/nixos/steam/beatsaber.nix b/modules/nixos/steam/beatsaber.nix index 295a96d4..383a02a9 100644 --- a/modules/nixos/steam/beatsaber.nix +++ b/modules/nixos/steam/beatsaber.nix @@ -5,35 +5,304 @@ inputs, ... }: let - inherit (inputs.self.lib.lib) mkWinPath userIs; + 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) removePrefix replaceStrings; - inherit (lib.attrsets) filterAttrs mapAttrs' mapAttrsToList listToAttrs nameValuePair; - inherit (lib.lists) concatMap head singleton; - inherit (lib.meta) getExe; + 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; - versionModule = { - config, - name, - ... - }: { + 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 = ""; + }; - mkSharePath = path: - mkWinPath ( - "%GENSO_SMB_SHARED_MOUNT%" - + "/${accountSwitch.sharePath}" - + "/${removePrefix (accountSwitch.rootDir + "/") path}" - ); - vars = '' + 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}" @@ -41,9 +310,16 @@ 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_USER_DATA=${mkSharePath accountSwitch.dataDir}\%GENSO_STEAM_USER%" - set "STEAM_WORKING_DATA=${mkSharePath accountSwitch.workingDir}\%GENSO_STEAM_USER%" - set "STEAM_BINDIR=${mkSharePath accountSwitch.binDir}" + 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 = '' @@ -54,10 +330,18 @@ ''; mount = '' rmdir "%STEAM_BS_APPDATA%" - mklink /D "%STEAM_BS_APPDATA%" "%STEAM_USER_DATA%\BeatSaber\AppData" + mklink /D "%STEAM_BS_APPDATA%" "%STEAM_BS_LAUNCH_APPDATA%" rmdir "%STEAM_BS_LIBRARY%" - mklink /D "%STEAM_BS_LIBRARY%" "%STEAM_WORKING_DATA%\BeatSaber\%GENSO_STEAM_BS_VERSION%" + 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} @@ -73,45 +357,58 @@ ''; fpfcbeatsaber = '' ${vars} - ${mount} cd /d "%STEAM_BS_LIBRARY%" "%STEAM_BS_LIBRARY%\Beat Saber.exe" fpfc ${eof} ''; - - mkbeatsabersh = pkgs.writeShellScriptBin "mkbeatsaber.sh" '' - source ${./mkbeatsaber.sh} + setupbeatsaber = '' + ${vars} + ${setup} + pause + ${eof} ''; - mkbeatsaber = pkgs.writeShellScriptBin "mkbeatsaber" '' + 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 - - ARG_GAME_VERSION=$1 - shift - if [[ $# -gt 0 ]]; then - ARG_USER=$1 - shift - else - ARG_USER=$(${pkgs.coreutils}/bin/id -un) - fi - - cd ${accountSwitch.workingDir} - mkdir -m2775 -p "$ARG_USER/BeatSaber/$ARG_GAME_VERSION" - chown "$ARG_USER" "$ARG_USER" "$ARG_USER/BeatSaber" - cd "$ARG_USER/BeatSaber/$ARG_GAME_VERSION" - ${getExe mkbeatsabersh} \ - "${accountSwitch.gamesDir}/BeatSaber" \ - "$ARG_GAME_VERSION" \ - "${accountSwitch.sharedDataDir}/BeatSaber" \ - "${accountSwitch.dataDir}/$ARG_USER/BeatSaber" + 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; - }; + setup = mkEnableOption "beatsaber data" // { + default = accountSwitch.setup; + }; group = mkOption { type = str; default = "beatsaber"; @@ -121,53 +418,313 @@ in { }; versions = mkOption { type = attrsOf (submodule versionModule); - default = {}; + 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 = let - in { + 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 ( + defaultVersion = mkIf (allVersions != [ ]) (mkOptionDefault ( head allVersions )); users = mkOptionDefault ( mapAttrsToList (_: user: user.name) bsUsers ); - }; - environment = mkIf cfg.enable { - systemPackages = [ - mkbeatsaber - mkbeatsabersh + 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 = mkIf cfg.setup (listToAttrs (map (user: - nameValuePair "steam-setup-beatsaber-${user}" { - script = mkMerge (mapAttrsToList (_: version: '' - ${getExe mkbeatsaber} ${version.version} ${user} - '') - cfg.versions); - path = [ - pkgs.coreutils - ]; - wantedBy = [ - "multi-user.target" - ]; - after = [ - "tmpfiles.service" - ]; - serviceConfig = { - RemainAfterExit = mkOptionDefault true; - User = mkOptionDefault user; - }; - }) - cfg.users)); + 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"; @@ -194,101 +751,128 @@ in { "CustomPlatforms" "CustomSabers" "CustomWalls" + "Playlists" + "Replays" "AppData" "UserData" ]; - setupFiles = - [ - { - "${accountSwitch.sharedDataDir}/BeatSaber" = toplevel; - "${accountSwitch.binDir}/beatsaber" = shared; - } - (listToAttrs ( - map ( - folder: - nameValuePair "${accountSwitch.sharedDataDir}/BeatSaber/${folder}" shared - ) - sharedFolders - )) - ] - ++ concatMap ( - owner: - singleton { - "${accountSwitch.dataDir}/${owner}/BeatSaber" = personal owner; - "${accountSwitch.dataDir}/${owner}/BeatSaber/AppData" = personal owner; - "${accountSwitch.dataDir}/${owner}/BeatSaber/UserData" = personal owner; - } - ++ mapAttrsToList (_: version: { - "${accountSwitch.dataDir}/${owner}/BeatSaber/${version.version}" = personal owner; - }) - cfg.versions - ) - accountSwitch.users - ++ mapAttrsToList (_: version: { - "${accountSwitch.sharedDataDir}/BeatSaber/${version.version}" = shared; - }) - cfg.versions; - versionBinFiles = - mapAttrs' ( - _: version: - nameValuePair - "${accountSwitch.binDir}/beatsaber/${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 = + setupFiles = [ { - "${accountSwitch.binDir}/beatsaber/mount.bat" = { - inherit (bin) owner group mode type; - src = pkgs.writeTextFile { - name = "beatsaber-mount.bat"; - executable = true; - text = mountbeatsaber; - }; - }; - "${accountSwitch.binDir}/beatsaber/launch.bat" = { - inherit (bin) owner group mode type; - src = pkgs.writeTextFile { - name = "beatsaber-launch.bat"; - executable = true; - text = launchbeatsaber; - }; - }; - "${accountSwitch.binDir}/beatsaber/fpfc.bat" = { - inherit (bin) owner group mode type; - src = pkgs.writeTextFile { - name = "beatsaber-fpfc.bat"; - executable = true; - text = fpfcbeatsaber; - }; - }; - "${accountSwitch.binDir}/beatsaber/ModAssistant.exe" = { - inherit (toplevel) owner group; - mode = "0755"; - type = "copy"; - src = pkgs.fetchurl { - url = "https://github.com/Assistant/ModAssistant/releases/download/v1.1.32/ModAssistant.exe"; - hash = "sha256-ozu2gYFiz+2BjptqL80DmUopbahbyGKFO1IPd7BhVPM="; - executable = true; - }; + ${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} + ''; }; } - // versionBinFiles; + ) 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.enable || cfg.setup) true; - files = mkMerge [ - (mkIf cfg.setup (mkMerge setupFiles)) - (mkIf cfg.enable binFiles) - ]; + enable = mkIf cfg.setup true; + files = mkIf cfg.setup (mkMerge ( + singleton binFiles + ++ setupFiles + )); }; }; } diff --git a/modules/nixos/steam/mkbeatsaber.sh b/modules/nixos/steam/mkbeatsaber.sh deleted file mode 100644 index fb4485d1..00000000 --- a/modules/nixos/steam/mkbeatsaber.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -set -eu - -ARG_GAME_SRC=$1 -ARG_GAME_VERSION=$2 -ARG_SHARED_DATA=$3 -ARG_USER_DATA=$4 -shift 4 - -if ! [[ -e "$ARG_GAME_SRC/$ARG_GAME_VERSION/Beat Saber.exe" ]]; then - echo unexpected game src >&2 - exit 1 -fi - -ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/"*.{exe,dll} ./ -ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/"{MonoBleedingEdge,Plugins} ./ -rm "Beat Saber.exe" -cp "$ARG_GAME_SRC/$ARG_GAME_VERSION/Beat Saber.exe" ./ -chmod 0775 "Beat Saber.exe" - -BSDATA="Beat Saber_Data" -mkdir -pm2775 "$BSDATA" -ln -srf "$ARG_GAME_SRC/$ARG_GAME_VERSION/$BSDATA/"* "$BSDATA/" || true -ln -srf "$ARG_SHARED_DATA/CustomLevels" "$BSDATA/" -rm -f "$BSDATA/Managed" - -mkdir -pm2775 UserData - -ln -srf "$ARG_SHARED_DATA/"{CustomAvatars,CustomNotes,CustomPlatforms,CustomSabers,CustomWalls,Playlists} ./ -for shareddir in DynamicOpenVR IPA Libs Logs Plugins "$BSDATA/Managed" UserData/SongCore; do - shareddirsrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/$shareddir" - if [[ ! -e $shareddirsrc ]]; then - mkdir -pm2775 "$shareddirsrc" - if [[ $shareddir = */Managed ]]; then - cp "$ARG_GAME_SRC/$ARG_GAME_VERSION/$BSDATA/Managed/"* "$shareddirsrc/" || true - chmod 0775 "$shareddirsrc/"*.dll || true - fi - fi - ln -srf "$shareddirsrc" "./$(dirname "$shareddir")" -done -for sharedfile in IPA.exe IPA.exe.config IPA.runtimeconfig.json winhttp.dll; do - sharedfilesrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/$sharedfile" - if [[ ! -e "$sharedfilesrc" ]]; then - mkdir -pm2775 "$(dirname "$sharedfilesrc")" - if [[ $sharedfile = *.json ]]; then - echo '{}' > "$sharedfilesrc" - else - touch "$sharedfilesrc" - fi - chmod 0775 "$sharedfilesrc" || true - fi - ln -f "$sharedfilesrc" ./ -done - -for sharedfile in "Beat Saber IPA.json"; do - sharedfilesrc="$ARG_SHARED_DATA/$ARG_GAME_VERSION/UserData/$sharedfile" - if [[ ! -e "$sharedfilesrc" ]]; then - mkdir -pm2775 "$(dirname "$sharedfilesrc")" - if [[ $sharedfile = *.json ]]; then - echo '{}' > "$sharedfilesrc" - else - touch "$sharedfilesrc" - fi - fi - ln -f "$sharedfilesrc" "UserData/$(dirname "$sharedfile")" -done -ln -f "$ARG_SHARED_DATA/UserData/"*.{json,ini,proto,etag} UserData/ -ln -srf "$ARG_SHARED_DATA/UserData/"{ScoreSaber,Chroma,Nya,SongRankedBadge,HitScoreVisualizer}/ UserData/ - -SFDATA="UserData/Saber Factory" -mkdir -pm2775 "$SFDATA" -ln -srf "$ARG_SHARED_DATA/$SFDATA/"*/ "$SFDATA/" -ln -srf "$ARG_USER_DATA/$SFDATA/"*/ "$SFDATA/" -ln -f "$ARG_USER_DATA/$SFDATA/"*.json "$SFDATA/" - -for userdir in Camera2 DrinkWater Enhancements; do - userdirsrc="$ARG_USER_DATA/UserData/$userdir" - if [[ ! -e $userdirsrc ]]; then - mkdir -pm3775 "$userdirsrc" - fi - ln -srf "$userdirsrc" UserData/ -done -ln -f "$ARG_USER_DATA/UserData/"*.{json,ini,dat} UserData/ diff --git a/nixos/kyuuto/mount.nix b/nixos/kyuuto/mount.nix index 4cee3dc5..e60364df 100644 --- a/nixos/kyuuto/mount.nix +++ b/nixos/kyuuto/mount.nix @@ -7,7 +7,7 @@ inherit (lib.modules) mkIf mkMerge mkDefault; inherit (lib.strings) removePrefix; inherit (lib.attrsets) listToAttrs nameValuePair; - inherit (config.services.steam) accountSwitch; + inherit (config.services.steam) accountSwitch beatsaber; cfg = config.kyuuto; in { options.kyuuto = with lib.types; { @@ -106,11 +106,15 @@ in { enable = mkIf cfg.setup true; files = mkMerge [ (mkIf cfg.setup (mkMerge setupFiles)) - (mkIf accountSwitch.enable { + (mkIf (accountSwitch.enable || beatsaber.setup) { ${accountSwitch.gamesDir} = { type = "bind"; bindReadOnly = true; src = cfg.gameLibraryDir + "/PC"; + systemd.mountSettings = rec { + wantedBy = mkIf beatsaber.setup beatsaber.setupServiceNames; + before = wantedBy; + }; }; }) ]; diff --git a/nixos/steam/account-switch.nix b/nixos/steam/account-switch.nix index 0394641d..882d6946 100644 --- a/nixos/steam/account-switch.nix +++ b/nixos/steam/account-switch.nix @@ -3,5 +3,9 @@ in { services.steam.accountSwitch = { enable = mkDefault true; + machines = { + hourai.owner = "arc"; + chen.owner = "kat"; + }; }; }