fix(steam): better data management and setup

This commit is contained in:
arcnmx 2024-02-16 10:49:55 -08:00
parent 81bd1a1a15
commit 475273692d
5 changed files with 822 additions and 284 deletions

View file

@ -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,8 +93,7 @@ in {
inherit owner;
inherit (shared) group mode;
};
setupFiles =
singleton {
setupFiles = singleton {
${cfg.rootDir} = toplevel;
${cfg.binDir} = toplevel;
${cfg.binDir + "/users"} = shared;
@ -85,14 +101,15 @@ in {
${cfg.sharedDataDir} = shared;
${cfg.workingDir} = toplevel;
${cfg.sharedWorkingDir} = shared;
}
++ map (owner: {
} ++ map (owner: {
${cfg.dataDir + "/${owner}"} = personal owner;
${cfg.workingDir + "/${owner}"} = personal owner;
})
cfg.users;
userBinFiles = listToAttrs (map (user:
nameValuePair "${cfg.binDir}/users/${user}.bat" {
}) 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";
@ -103,8 +120,7 @@ in {
setx GENSO_STEAM_USER ${user}
'';
};
})
cfg.users);
}) 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;
};
};
};
}

View file

@ -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,43 +357,56 @@
'';
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"
// {
setup = mkEnableOption "beatsaber data" // {
default = accountSwitch.setup;
};
group = mkOption {
@ -123,16 +420,277 @@ in {
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 = 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 (
head allVersions
@ -140,22 +698,17 @@ in {
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
];
systemd.services.steam-setup-beatsaber = mkIf cfg.setup {
wantedBy = [
"multi-user.target"
];
@ -163,11 +716,15 @@ in {
"tmpfiles.service"
];
serviceConfig = {
Type = mkOptionDefault "oneshot";
RemainAfterExit = mkOptionDefault true;
User = mkOptionDefault user;
ExecStart = mkMerge (map (user:
(mapAttrsToList (_: version:
"${mksetupbeatsaber { inherit user; inherit (version) version; }}"
) cfg.versions)
) cfg.users);
};
};
})
cfg.users));
services.tmpfiles = let
toplevel = {
owner = mkDefault "admin";
@ -194,45 +751,37 @@ in {
"CustomPlatforms"
"CustomSabers"
"CustomWalls"
"Playlists"
"Replays"
"AppData"
"UserData"
];
setupFiles =
[
setupFiles = [
{
"${accountSwitch.sharedDataDir}/BeatSaber" = toplevel;
"${accountSwitch.binDir}/beatsaber" = shared;
${cfg.sharedDataDir} = toplevel;
${cfg.binDir} = shared;
}
(listToAttrs (
map (
folder:
nameValuePair "${accountSwitch.sharedDataDir}/BeatSaber/${folder}" shared
)
sharedFolders
map (folder:
nameValuePair "${cfg.sharedDataDir}/${folder}" shared
) sharedFolders
))
]
++ concatMap (
owner:
] ++ concatMap (owner:
singleton {
"${accountSwitch.dataDir}/${owner}/BeatSaber" = personal owner;
"${accountSwitch.dataDir}/${owner}/BeatSaber/AppData" = personal owner;
"${accountSwitch.dataDir}/${owner}/BeatSaber/UserData" = personal owner;
}
${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: {
"${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"
"${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 {
@ -243,11 +792,9 @@ in {
'';
};
}
)
cfg.versions;
binFiles =
{
"${accountSwitch.binDir}/beatsaber/mount.bat" = {
) cfg.versions;
binFiles = {
"${cfg.binDir}/mount.bat" = {
inherit (bin) owner group mode type;
src = pkgs.writeTextFile {
name = "beatsaber-mount.bat";
@ -255,7 +802,7 @@ in {
text = mountbeatsaber;
};
};
"${accountSwitch.binDir}/beatsaber/launch.bat" = {
"${cfg.binDir}/launch.bat" = {
inherit (bin) owner group mode type;
src = pkgs.writeTextFile {
name = "beatsaber-launch.bat";
@ -263,7 +810,7 @@ in {
text = launchbeatsaber;
};
};
"${accountSwitch.binDir}/beatsaber/fpfc.bat" = {
"${cfg.binDir}/fpfc.bat" = {
inherit (bin) owner group mode type;
src = pkgs.writeTextFile {
name = "beatsaber-fpfc.bat";
@ -271,24 +818,61 @@ in {
text = fpfcbeatsaber;
};
};
"${accountSwitch.binDir}/beatsaber/ModAssistant.exe" = {
inherit (toplevel) owner group;
mode = "0755";
type = "copy";
"${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;
} // 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
));
};
};
}

View file

@ -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/

View file

@ -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;
};
};
})
];

View file

@ -3,5 +3,9 @@
in {
services.steam.accountSwitch = {
enable = mkDefault true;
machines = {
hourai.owner = "arc";
chen.owner = "kat";
};
};
}