This commit is contained in:
mjallen18
2025-02-11 17:53:49 -06:00
parent 1c647c2205
commit 151da6583d
6 changed files with 605 additions and 12 deletions

View File

@@ -1,6 +1,6 @@
{ pkgs, ... }:
let
kernel = pkgs.linuxPackages_latest;
kernel = pkgs.linuxPackages_cachyos;
in
{
# Configure bootloader with lanzaboot and secureboot

View File

@@ -9,6 +9,8 @@
[ # Include the results of the hardware scan.
./boot.nix
./jovian.nix
./steam-shortcuts.nix
# ./steam-game
./hardware-configuration.nix
];
@@ -134,6 +136,8 @@
};
};
chaotic.mesa-git.enable = true;
# List packages installed in system profile. To search, run:
# $ nix search wget
environment = {
@@ -141,6 +145,7 @@
fuse
jq
newt
maliit-keyboard
onlyoffice-bin
python3
rsync
@@ -160,6 +165,27 @@
};
};
# services.steamRomManager = {
# enable = true;
# user = "deck";
# romSources = [
# {
# name = "Nintendo Switch ROMs";
# path = "/home/deck/Emulation/roms/switch";
# platform = "nintendoswitch";
# emulator = "ryujinx";
# }
# ];
# emulators = [
# {
# name = "Ryujinx";
# executable = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
# }
# ];
# };
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;

View File

@@ -0,0 +1,35 @@
{ pkgs, ... }:
{
# You'll need to get an API key from https://www.steamgriddb.com/profile/preferences/api
steamGridDbApiKey = "ada6729c6cd321a4e3792ddd0b83c1ba";
# List your non-Steam games here
games = [
{
name = "Ryujinx";
path = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
extraLaunchOptions = "";
steamGridDbIds = {
grid = "121085";
hero = "63924";
logo = "29735";
list = "5517";
};
}
{
name = "Prismlauncher";
path = "${pkgs.prismlauncher}/bin/prismlauncher";
extraLaunchOptions = "";
steamGridDbIds = {
grid = "267141";
hero = "64303";
logo = "68299";
list = "23582";
};
}
{
name = "Mario Kart 8";
path = "${pkgs.ryujinx-greemdev}/bin/ryujinx";
extraLaunchOptions = "--fullscreen '/home/deck/Emulation/roms/switch/Mario Kart 8 Deluxe [B+U720896].nsp'";
}
];
}

View File

@@ -0,0 +1,371 @@
{ config, lib, pkgs, ... }:
let
# Import the games configuration
gamesConfig = import ./steam-games.nix { inherit pkgs; };
# Create the VDF generator script
vdfGeneratorScript = pkgs.writeText "generate_vdf.py" ''
#!/usr/bin/env python3
import sys
import json
import struct
import time
import hashlib
def generate_app_id(name):
"""Generate Steam app ID for a game using Steam's apparent format."""
# Get the current timestamp
timestamp = int(time.time())
# Create a hash of the name and timestamp
hash_input = f"{name}{timestamp}".encode('utf-8')
hash_obj = hashlib.md5(hash_input)
# Take first 6 bytes of hash and combine with a fixed pattern
hash_bytes = hash_obj.digest()[:6]
# Create a 64-bit number where:
# - First 6 bytes come from hash
# - Last 2 bytes are zeros (matching Steam's pattern)
full_bytes = hash_bytes + b'\x00\x00'
# Convert to 64-bit integer
app_id = struct.unpack(">Q", full_bytes)[0]
# Ensure the number is in the correct range by setting specific bits
app_id |= (1 << 62) # Set the second-to-highest bit
app_id &= ~(1 << 63) # Clear the highest bit
return app_id
def write_vdf(shortcuts):
"""Write VDF format matching the exact byte sequence."""
output = bytearray()
# Header
output.extend(b'\x00shortcuts\x00')
for i, shortcut in enumerate(shortcuts):
# Index
output.extend(f'\x00{i}\x00'.encode('utf-8'))
# AppName
output.extend(b'\x01appname\x00')
output.extend(shortcut['AppName'].encode('utf-8') + b'\x00')
# App ID (this is the key addition)
output.extend(b'\x02appid\x00')
output.extend(f'"{shortcut["Id"]}"'.encode('utf-8') + b'\x00')
# Exe
output.extend(b'\x01exe\x00')
output.extend(f'"{shortcut["ExePath"]}"'.encode('utf-8') + b'\x00')
# StartDir
output.extend(b'\x01StartDir\x00')
output.extend(b'"/home/deck"\x00')
# Icon
output.extend(b'\x01icon\x00\x00')
# ShortcutPath
output.extend(b'\x01ShortcutPath\x00\x00')
# LaunchOptions
output.extend(b'\x01LaunchOptions\x00')
output.extend(f'"{shortcut["LaunchOptions"]}"'.encode('utf-8') + b'\x00')
# IsHidden
output.extend(b'\x02IsHidden\x00\x00\x00\x00\x00')
# AllowDesktopConfig
output.extend(b'\x02AllowDesktopConfig\x00\x01\x00\x00\x00')
# AllowOverlay
output.extend(b'\x02AllowOverlay\x00\x01\x00\x00\x00')
# OpenVR
output.extend(b'\x02OpenVR\x00\x00\x00\x00\x00')
# Tags
output.extend(b'\x00tags\x00\x08')
# Entry terminator
output.extend(b'\x08')
# Final terminators
output.extend(b'\x08\x08')
return output
def main():
try:
shortcuts = json.load(sys.stdin)
vdf_data = write_vdf(shortcuts)
sys.stdout.buffer.write(vdf_data)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
'';
# Create the SteamGridDB image downloader script
steamGridDbScript = pkgs.writeText "fetch_steamgriddb.py" ''
#!/usr/bin/env python3
import sys
import json
import requests
import time
import hashlib
import struct
from pathlib import Path
def generate_app_id(name):
"""Generate Steam app ID for a game using Steam's apparent format."""
# Get the current timestamp
timestamp = int(time.time())
# Create a hash of the name and timestamp
hash_input = f"{name}{timestamp}".encode('utf-8')
hash_obj = hashlib.md5(hash_input)
# Take first 6 bytes of hash and combine with a fixed pattern
hash_bytes = hash_obj.digest()[:6]
# Create a 64-bit number where:
# - First 6 bytes come from hash
# - Last 2 bytes are zeros (matching Steam's pattern)
full_bytes = hash_bytes + b'\x00\x00'
# Convert to 64-bit integer
app_id = struct.unpack(">Q", full_bytes)[0]
# Ensure the number is in the correct range by setting specific bits
app_id |= (1 << 62) # Set the second-to-highest bit
app_id &= ~(1 << 63) # Clear the highest bit
return app_id
def download_image(url, path, headers=None):
"""Download an image from a URL to a specified path."""
if url:
try:
response = requests.get(url) # Don't use auth headers for actual download
if response.status_code == 200:
path.write_bytes(response.content)
return True
else:
pass
except Exception as e:
print(f"Error downloading: {e}")
return False
def fetch_images(api_key, game_name, grid_ids, output_dir):
"""Fetch images for a game from SteamGridDB."""
print(f"\nProcessing images for {game_name}")
headers = {"Authorization": f"Bearer {api_key}"}
base_url = "https://www.steamgriddb.com/api/v2"
app_id = generate_app_id(game_name)
# Create output directories
grid_dir = output_dir / "config/grid"
hero_dir = output_dir / "config/heroes"
logo_dir = output_dir / "config/logos"
print(f"Using directories:")
print(f"Grid: {grid_dir}")
print(f"Hero: {hero_dir}")
print(f"Logo: {logo_dir}")
grid_dir.mkdir(parents=True, exist_ok=True)
hero_dir.mkdir(parents=True, exist_ok=True)
logo_dir.mkdir(parents=True, exist_ok=True)
# Test API connection
test_response = requests.get(f"{base_url}/grids/game/1", headers=headers)
print(f"API test response: {test_response.status_code}")
if test_response.status_code == 401:
print("API authentication failed. Check your API key.")
return
image_types = {
"grid": (grid_dir / f"{app_id}.jpg", grid_ids.get("grid"), "grids"),
"hero": (hero_dir / f"{app_id}.jpg", grid_ids.get("hero"), "heroes"),
"logo": (logo_dir / f"{app_id}.png", grid_ids.get("logo"), "logos"),
"list": (grid_dir / f"{app_id}_list.jpg", grid_ids.get("list"), "grids")
}
for img_type, (output_path, specific_id, asset_type) in image_types.items():
if specific_id:
# Get direct file URL from the API
url = f"{base_url}/{asset_type}/{specific_id}"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
if data.get("success"):
image_url = data["data"]["url"]
if download_image(image_url, output_path):
print(f"Successfully downloaded {img_type} image")
else:
print(f"Failed to download {img_type} image")
else:
print(f"API error: {data}")
else:
print(f"API request failed: status {response.status_code}")
if response.headers.get('content-type', "").startswith('application/json'):
print(f"Response: {response.json()}")
except Exception as e:
print(f"Error processing {img_type}: {e}")
time.sleep(1) # Rate limiting
def main():
if len(sys.argv) != 4:
print("Usage: script.py API_KEY game_data.json output_dir", file=sys.stderr)
sys.exit(1)
api_key = sys.argv[1]
game_data_file = sys.argv[2]
output_dir = Path(sys.argv[3])
print(f"Loading game data from: {game_data_file}")
print(f"Output directory: {output_dir}")
try:
with open(game_data_file) as f:
games = json.load(f)
print(f"Loaded {len(games)} games")
for game in games:
if "steamGridDbIds" in game:
fetch_images(api_key, game["name"], game["steamGridDbIds"], output_dir)
else:
print(f"No steamGridDbIds for {game['name']}, skipping")
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
'';
# Python environment with required packages
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
requests
]);
# Helper function to convert hex char to decimal
hexCharToInt = c:
let
hexChars = {
"0" = 0; "1" = 1; "2" = 2; "3" = 3; "4" = 4;
"5" = 5; "6" = 6; "7" = 7; "8" = 8; "9" = 9;
"a" = 10; "b" = 11; "c" = 12; "d" = 13; "e" = 14; "f" = 15;
"A" = 10; "B" = 11; "C" = 12; "D" = 13; "E" = 14; "F" = 15;
};
in hexChars.${c};
# Convert hex string to decimal number
hexToInt = str:
let
len = builtins.stringLength str;
chars = lib.stringToCharacters str;
go = pos: nums:
if pos == len then 0
else
let
char = builtins.elemAt chars pos;
num = hexCharToInt char;
exp = len - pos - 1;
factor = builtins.foldl' (a: b: a * 16) 1 (lib.range 1 exp);
in
(num * factor) + (go (pos + 1) nums);
in go 0 chars;
# Helper function for the ID generation - handling large numbers
generateAppId = name:
let
hash = builtins.hashString "md5" name;
# Instead of trying to create a massive number, we'll generate a unique pattern
# that will create a consistent large number when interpreted by Python
sixteenHex = builtins.substring 0 16 hash;
# Format it as a string that Python will interpret as a large number
shortcutAppId = "0x${sixteenHex}0000";
in shortcutAppId;
# Convert games to shortcuts format
gameToShortcut = game: {
Id = generateAppId game.name;
AppName = game.name;
ExePath = game.path;
LaunchOptions = game.extraLaunchOptions;
};
# Script to update shortcuts and fetch images
updateSteamShortcuts = pkgs.writeShellScriptBin "update-steam-shortcuts" ''
STEAM_DIR="$HOME/.local/share/Steam"
USERDATA_DIR="$STEAM_DIR/userdata"
CACHE_DIR="$HOME/.cache/steam-shortcuts"
if [ ! -d "$USERDATA_DIR" ]; then
echo "Steam userdata directory not found!"
exit 1
fi
# Make sure Steam is not running
if pgrep -x "steam" > /dev/null; then
echo "Please close Steam before updating shortcuts"
exit 1
fi
mkdir -p "$CACHE_DIR"
# Process each Steam user directory
for USER_DIR in "$USERDATA_DIR"/*; do
if [ -d "$USER_DIR" ]; then
USER_ID="$(basename "$USER_DIR")"
CONFIG_DIR="$USER_DIR/config"
mkdir -p "$CONFIG_DIR"
echo "Processing Steam user $USER_ID..."
# Create games JSON file
GAMES_JSON="$CACHE_DIR/games.json"
echo '${builtins.toJSON (map gameToShortcut gamesConfig.games)}' > "$GAMES_JSON"
# Generate shortcuts.vdf
cat "$GAMES_JSON" | ${pythonEnv}/bin/python3 ${vdfGeneratorScript} > "$CONFIG_DIR/shortcuts.vdf"
# Download images if API key is set
if [ "${gamesConfig.steamGridDbApiKey}" != "YOUR_API_KEY_HERE" ]; then
echo '${builtins.toJSON gamesConfig.games}' > "$CACHE_DIR/games_full.json"
${pythonEnv}/bin/python3 ${steamGridDbScript} "${gamesConfig.steamGridDbApiKey}" "$CACHE_DIR/games_full.json" "$USER_DIR"
fi
echo "Updated shortcuts and images for $USER_ID"
fi
done
echo "Done! You can now start Steam."
'';
in {
# Add the update script to system packages
environment.systemPackages = [ updateSteamShortcuts ];
# Optional: Create a systemd user service to update shortcuts on login
systemd.user.services.update-steam-shortcuts = {
enable = true;
description = "Update Steam shortcuts";
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${updateSteamShortcuts}/bin/update-steam-shortcuts";
RemainAfterExit = true;
};
};
}