From 134fe163931a0edb1538a4c7458fb3395b4b2b3a Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 14 Jan 2026 21:49:20 +0100 Subject: [PATCH] feat: TETR.IO (+ TETR.IO PLUS integration) This commit does feature a _few_ changes to my general NixOS config (namely the ideology switch from importing things everywhere to having my own custom "`mpkgs`"). However most of this effort was all thanks to TETR.IO. The former maintainer of `pkgs.tetrio-desktop` and `pkgs.tetrio-plus` seems to not have been playing recently enough to update the packages to v10. Making them unusuable. `mpkgs.tetrio.desktop` is a patched `pkgs.tetrio-desktop` that updates to v10. Alongside this, and a quick discovery that you could make custom home-manager modules, I took it upon myself to make TETR.IO configurations (including those of TETR.IO PLUS) fully generated from Nix. This effort took way too long, and feels slightly hacky (the way injecting configurations works is by generating a LevelDB (Chromium IndexedDB) for the electron instance's Local Storage) and it involves some custom stuff. (LevelDB deriviation, plus home-manager module for copying files rather than linking them `home.initialFile`.) I'm proud of the result, and Tetris is now fully deterministic, reproducible, and Nix-y. --- configuration.nix | 35 +- home.nix | 72 ++- pkgs/default.nix | 55 ++ pkgs/home-manager/initialFile.nix | 172 ++++++ pkgs/leveldb/default.nix | 21 + pkgs/tetrio/default.nix | 30 + pkgs/tetrio/leveldb.nix | 32 ++ pkgs/tetrio/module.nix | 725 +++++++++++++++++++++++++ pkgs/tetrio/skins/simple-connected.nix | 31 ++ 9 files changed, 1157 insertions(+), 16 deletions(-) create mode 100644 pkgs/default.nix create mode 100644 pkgs/home-manager/initialFile.nix create mode 100644 pkgs/leveldb/default.nix create mode 100644 pkgs/tetrio/default.nix create mode 100644 pkgs/tetrio/leveldb.nix create mode 100644 pkgs/tetrio/module.nix create mode 100644 pkgs/tetrio/skins/simple-connected.nix diff --git a/configuration.nix b/configuration.nix index 32369ff..66ae9d0 100644 --- a/configuration.nix +++ b/configuration.nix @@ -8,18 +8,19 @@ let stateVersion = "25.05"; env = import ./.env.nix { inherit pkgs; }; + + mpkgs = import ./pkgs/default.nix { + config = { + system.stateVersion = stateVersion; + }; + }; in { imports = [ # Include the results of the hardware scan. ./hardware-configuration.nix (import (./. + "/${env.hostname}.nix")) - ( - let - home-manager = builtins.fetchTarball "https://github.com/nix-community/home-manager/archive/release-${stateVersion}.tar.gz"; - in - import "${home-manager}/nixos" - ) + mpkgs.home-manager.module ]; nixpkgs.config.allowUnfree = true; @@ -59,6 +60,7 @@ in clang llvmPackages.bintools stylua + go ]; fonts.packages = with pkgs; [ @@ -69,7 +71,7 @@ in nerd-fonts.symbols-only inter times-newer-roman - (import ./pkgs/monaco-font/default.nix { inherit pkgs; }) + (mpkgs.font.monaco) ]; fonts.fontconfig = { @@ -153,9 +155,12 @@ in ]; }; - home-manager.useUserPackages = true; - home-manager.useGlobalPkgs = true; - home-manager.users.anton = import ./home.nix; + home-manager = { + useUserPackages = true; + useGlobalPkgs = true; + sharedModules = [ ] ++ (mpkgs.home-manager.sharedModules); + users.anton = import ./home.nix; + }; # programs.home-manager.enable = true; programs.dconf.enable = true; @@ -165,7 +170,15 @@ in style = "adwaita-dark"; }; - hardware.graphics.enable = true; + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-media-driver + intel-vaapi-driver + libvdpau-va-gl + mesa + ]; + }; hardware.bluetooth = { enable = true; diff --git a/home.nix b/home.nix index e9bc27a..8639d92 100644 --- a/home.nix +++ b/home.nix @@ -5,14 +5,16 @@ }: let - neovim = pkgs.callPackage ./pkgs/neovim/default.nix { }; - fish = pkgs.callPackage ./pkgs/fish/default.nix { }; + mpkgs = import ./pkgs/default.nix { }; + inherit (mpkgs.config) neovim fish; env = import ./.env.nix { inherit pkgs; }; home = /home/anton; in { + imports = [ ]; + home.username = lib.mkDefault "anton"; home.homeDirectory = lib.mkDefault home; @@ -25,7 +27,7 @@ in krita davinci-resolve vscode - godot + godotPackages_4_5.godot neovim btop @@ -133,7 +135,67 @@ in }; }; - programs.vesktop = import ./home/vesktop.nix; + programs.tetrio-desktop = { + enable = true; + package = mpkgs.tetrio.desktop; + + plus = { + enable = true; + package = mpkgs.tetrio.plus; + skin.package = mpkgs.tetrio.skins.simple-connected; + }; + + settings = { + handling = { + auto_repeat_rate = 0; + delayed_auto_shift = 7; + soft_drop_factor = 14; + }; + audio = { + scroll_adjust_volume = false; + stereo = 60; + + music.preferences = { + kaze-no-sanpomichi = "-"; + muscat-to-shiroi-osara = "-"; + akindo = "+"; + yoru-no-niji = "+"; + burari-tokyo = "+"; + fuyu-no-jinkoueisei = "+"; + honemi-ni-shimiiru-karasukaze = "-"; + "21seiki-no-hitobito" = "+"; + haru-wo-machinagara = "++"; + go-go-go-summer = "-"; + sasurai-no-hitoritabi = "++"; + wakana = "-"; + zange-no-ma = "-"; + asphalt = "-"; + madobe-no-hidamari = "--"; + sora-no-sakura = "-"; + suiu = "-"; + burning-heart = "+"; + hayate-no-sei = "-"; + ima-koso = "+"; + chiheisen-wo-koete = "--"; + moyase-toushi-yobisamase-tamashii = "-"; + uchuu-5239 = "+"; + ultra-super-heros = "-"; + }; + }; + visual = { + board_bounciness = 20; + background_opacity = 0; + }; + multiplayer.notifications.suppress_while_playing = true; + + skip_login_screen = "by-url"; + advertisments.i_support_the_devs.i_cannot_play_with_ads.and_i_really_want_to.disable = true; + + devtools = true; + }; + }; + + programs.vesktop = import ./home/vesktop.nix; services.gpg-agent = { enable = true; @@ -157,5 +219,5 @@ in NIXPKGS_ALLOW_UNFREE = 1; }; - home.stateVersion = (pkgs.callPackage { }).system.stateVersion; + home.stateVersion = mpkgs.system.stateVersion; } diff --git a/pkgs/default.nix b/pkgs/default.nix new file mode 100644 index 0000000..5d2608f --- /dev/null +++ b/pkgs/default.nix @@ -0,0 +1,55 @@ +{ + pkgs ? import { config.allowUnfree = true; }, + config ? (import { }).config, + fetchzip ? pkgs.fetchzip, + ... +}: + +let + system = { + stateVersion = config.system.stateVersion; + }; + use = path: pkgs.callPackage (import path) { inherit pkgs; }; +in +{ + inherit system; + + wallpaper = use ./wallpaper/default.nix; + tetrio.desktop = use ./tetrio/default.nix; + tetrio.plus = + let + repo = { + owner = "UniQMG"; + name = "tetrio-plus"; + job = "11675178434"; + hash = "sha256-j3ACcnT64eMQtWYDGOE2oGXpnN5EUqk+lyV6ARBEtU8="; + }; + src = fetchzip { + url = "https://gitlab.com/${repo.owner}/${repo.name}/-/jobs/${repo.job}/artifacts/raw/app.asar.zip"; + hash = repo.hash; + }; + in + "${src}/app.asar"; + tetrio.skins = { + simple-connected = use ./tetrio/skins/simple-connected.nix; + }; + + leveldb-cli = use ./leveldb/default.nix; + + config.neovim = use ./neovim/default.nix; + config.fish = use ./fish/default.nix; + + font.monaco = use ./monaco-font/default.nix; + + home-manager = { + module = + let + home-manager = fetchzip { + url = "https://github.com/nix-community/home-manager/archive/release-${system.stateVersion}.tar.gz"; + hash = "sha256-WHkdBlw6oyxXIra/vQPYLtqY+3G8dUVZM8bEXk0t8x4="; + }; + in + import "${home-manager}/nixos"; + sharedModules = [ ./home-manager/initialFile.nix ./tetrio/module.nix ]; + }; +} diff --git a/pkgs/home-manager/initialFile.nix b/pkgs/home-manager/initialFile.nix new file mode 100644 index 0000000..0e5d70f --- /dev/null +++ b/pkgs/home-manager/initialFile.nix @@ -0,0 +1,172 @@ +{ + pkgs, + lib ? pkgs.lib, + config, + ... +}: + +let + inherit (lib) mkOption types; + + cfg = lib.filterAttrs (n: f: f.enable) config.home.initialFile; + home = config.home.homeDirectory; +in +{ + options = { + home.initialFile = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, config, ... }: + { + options = { + enable = mkOption { + type = types.bool; + default = true; + }; + target = mkOption { + type = types.str; + apply = + path: + let + absPath = if lib.hasPrefix "/" path then path else "${home}/${path}"; + in + lib.removePrefix (home + "/") absPath; + description = "Path to target file relative to home directory"; + default = "/homeless-shelter/home"; + }; + text = mkOption { + type = types.nullOr types.lines; + description = "Text of the file, otherwise copies .source file."; + default = null; + }; + source = mkOption { + type = types.path; + description = "Path of file whose source to copy"; + }; + mode = mkOption { + type = types.nullOr types.str; + description = "File mode to apply to target file (sourced from .source / .text if not specified)"; + default = null; + }; + dir_mode = mkOption { + type = types.str; + description = "File mode to apply to directories"; + default = "0755"; + }; + recursive = mkOption { + type = types.bool; + description = "Whether or not to recursively copy .source as a directory instead of as a file"; + default = false; + }; + force = mkOption { + type = types.bool; + description = "Whether to unconditionally replace the target file, even if it already exists."; + default = false; + }; + }; + config = { + target = lib.mkDefault name; + source = lib.mkIf (config.text != null) ( + lib.mkDefault ( + pkgs.writeTextFile { + inherit (config) text; + name = lib.hm.strings.storeFileName name; + } + ) + ); + }; + } + ) + ); + description = "Attribute set of files to write into the user home (if they don't already exist)."; + default = { }; + }; + }; + + config = { + assertions = [ + ( + let + dups = lib.attrNames ( + lib.filterAttrs (n: v: v > 1) ( + lib.foldAttrs (acc: v: acc + v) 0 (lib.mapAttrsToList (n: v: { ${v.target} = 1; }) cfg) + ) + ); + dupsStr = lib.concatStringsSep ", " dups; + in + { + assertion = dups == [ ]; + message = '' + Conflicting managed target files: ${dupsStr} + + This may happen, for example, if you have a configuration similar to + + home.initialFile = { + conflict1 = { source = ./foo.nix; target = "baz"; }; + conflict2 = { source = ./bar.nix; target = "baz"; }; + }''; + } + ) + ]; + + home.activation.copyInitialFiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ( + let + homeArg = lib.escapeShellArg home; + in + '' + function copyFile() { + local source="$1" + local targetRel="$2" + local mode="$3" + local dirMode="$4" + local recursive="$5" + local force="$6" + + local target="${homeArg}/$targetRel" + + if [[ -e "$target" && "$force" != "true" ]]; then + verboseEcho "Skipping existing $target" + return 0 + fi + + run mkdir -p "$(dirname "$target")" + + if [[ -d "$source" ]]; then + if [[ "$recursive" != "true" ]]; then + errorEcho "Source '$source' is a directory but recursive=false" + return 1 + fi + run rm -rf "$target" + run cp -r "$source" "$target" + else + if [[ -e "$target" && "$force" == "true" ]]; then + run rm -f "$target" + fi + run cp "$source" "$target" + fi + + if [[ -n "$mode" ]]; then + if [[ -d "$target" && "$recursive" == "true" ]]; then + run chmod "$dirMode" "$target" + find "$target" -type f -exec chmod "$mode" {} + + else + run chmod "$mode" "$target" + fi + fi + } + '' + + lib.concatMapStrings ( + v: + let + src = lib.escapeShellArg (toString v.source); + tgt = lib.escapeShellArg v.target; + mode = if v.mode == null then "''" else lib.escapeShellArg v.mode; + in + '' + copyFile ${src} ${tgt} ${mode} ${lib.escapeShellArg v.dir_mode} ${lib.trivial.boolToString v.recursive} ${lib.trivial.boolToString v.force} + '' + ) (lib.attrValues cfg) + ); + + }; +} diff --git a/pkgs/leveldb/default.nix b/pkgs/leveldb/default.nix new file mode 100644 index 0000000..5a45d54 --- /dev/null +++ b/pkgs/leveldb/default.nix @@ -0,0 +1,21 @@ +{ + pkgs ? import { }, +}: + +pkgs.buildGoModule { + name = "leveldb-cli"; + version = "1.0.0"; + + src = pkgs.fetchFromGitHub { + owner = "theblueplum"; + repo = "leveldb-cli"; + rev = "main"; + hash = "sha256-Q4BVmmqc6MPrOLy/lSV1FyhAoKBq0U2UcMHYEMOhtpo="; + }; + + vendorHash = "sha256-b25hlPQft9iKyIw6E9jtORBgoLPnNa4+Z5QoeFoayfc="; + + meta = { + mainProgram = "leveldb"; + }; +} diff --git a/pkgs/tetrio/default.nix b/pkgs/tetrio/default.nix new file mode 100644 index 0000000..ce7b3f4 --- /dev/null +++ b/pkgs/tetrio/default.nix @@ -0,0 +1,30 @@ +{ + pkgs ? import { }, + fetchzip ? pkgs.fetchzip, + withTetrioPlus ? false, + tetrio-plus ? pkgs.tetrio-plus, + ... +}: + +let + tetrio-desktop = + let + version = "10"; + in + { + inherit version; + src = fetchzip { + url = "https://tetr.io/about/desktop/builds/${version}/TETR.IO%20Setup.deb"; + hash = "sha256-2FtFCajNEj7O8DGangDecs2yeKbufYLx1aZb3ShnYvw="; + nativeBuildInputs = with pkgs; [ dpkg ]; + }; + }; + +in + +(pkgs.tetrio-desktop.overrideAttrs { + version = tetrio-desktop.version; + src = tetrio-desktop.src; +}).override { + inherit withTetrioPlus tetrio-plus; +} diff --git a/pkgs/tetrio/leveldb.nix b/pkgs/tetrio/leveldb.nix new file mode 100644 index 0000000..bef5d1a --- /dev/null +++ b/pkgs/tetrio/leveldb.nix @@ -0,0 +1,32 @@ +{ + pkgs ? import { }, + mpkgs ? import /etc/nixos/pkgs/default.nix { }, + origin ? "https://tetr.io", + key ? "userConfig", + value, + ... +}: + +let + src = ./.; +in +pkgs.stdenv.mkDerivation { + pname = "tetrio-leveldb"; + version = "1"; + + inherit src; + + nativeBuildInputs = [ mpkgs.leveldb-cli ]; + + buildPhase = '' + runHook preBuild + + ${pkgs.lib.getExe mpkgs.leveldb-cli} \ + $out \ + ${pkgs.lib.escapeShellArg origin} \ + ${pkgs.lib.escapeShellArg key} \ + ${pkgs.lib.escapeShellArg (builtins.toJSON value)} + + runHook postBuild + ''; +} diff --git a/pkgs/tetrio/module.nix b/pkgs/tetrio/module.nix new file mode 100644 index 0000000..e76184e --- /dev/null +++ b/pkgs/tetrio/module.nix @@ -0,0 +1,725 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkOption mkIf types; + inherit (types) either; + + clamp = + x: min: max: + if x < min then + min + else if x > max then + max + else + x; + + leveldb = import ./leveldb.nix; + + cfg = config.programs.tetrio-desktop; +in +{ + options.programs.tetrio-desktop = { + enable = lib.mkEnableOption "tetrio"; + package = lib.mkPackageOption pkgs "tetrio-desktop" { }; + + plus = { + enable = lib.mkEnableOption "tetrio-plus"; + package = lib.mkOption { + type = types.path; + description = "The package to use for TETR.IO PLUS"; + default = pkgs.tetrio-plus; + }; + + skin = mkOption { + type = types.nullOr ( + types.submodule { + options = { + package = mkOption { + type = types.package; + description = "The package to use for the skin"; + example = "mpkgs.tetrio.skin.simple-connected"; + }; + + nearest = mkOption { + type = types.bool; + description = "Force nearest-neighbor scaling"; + default = false; + }; + }; + } + ); + default = null; + description = "In-game tetrimino skin"; + }; + + hideOnStartup = lib.mkOption { + type = types.bool; + default = true; + description = "Disable the TETR.IO PLUS configuration panel showing"; + }; + }; + settings = { + handling = + let + buffering = types.enum [ + "off" + "hold" + "tap" + ]; + in + { + + auto_repeat_rate = mkOption { + type = types.number; + apply = x: clamp x 0 5; + description = "Automatic Repeat Rate: the speed at which tetrominoes move when holding down movement keys, measured in frames per movement."; + default = 2; + }; + delayed_auto_shift = mkOption { + type = types.number; + apply = x: clamp x 1 20; + description = "Delayed Auto Shift: the time between the initial keypress and the start of its automatic repeat movement, measured in frames."; + default = 10; + }; + das_cut_delay = mkOption { + type = types.number; + apply = x: clamp x 0 20; + + description = "DAS Cut Delay (frames): if not 0, any ongoing DAS movement will pause for a set amount of time after dropping/rotating a piece, measured in frames."; + default = 1; + }; + soft_drop_factor = mkOption { + type = types.number; + apply = x: clamp x 5 41; + description = "Soft Drop Factor: the factor with which soft drops change the gravity speed."; + default = 6; + }; + prevent_hard_drops = mkOption { + type = types.bool; + description = "If enabled, when a piece locks on its own, the hard drop key becomes unavailable for a few frames. This prevents accidental hard drops."; + default = true; + }; + direction_cancel_das = mkOption { + type = types.bool; + description = "If enabled, DAS charge is cancelled when you change directions."; + default = false; + }; + soft_drop_over_movement = mkOption { + type = types.bool; + description = "If enabled, at very high speeds soft drop will always take precedence over horizontal movement, for a more consistent game feel."; + default = true; + }; + rotation_buffering = mkOption { + type = buffering; + description = "When to buffer rotations for the next piece (\"hold\": only if you hold the key as the next piece spawns)."; + default = "tap"; + }; + hold_buffering = mkOption { + type = buffering; + description = "When to buffer holding for the next piece (\"hold\": only if you hold the key as the next piece spawns)."; + default = "tap"; + }; + }; + audio = + let + percentage = x: x / 100.0; + in + { + music = { + volume = mkOption { + type = types.number; + apply = x: percentage (clamp x 0 200); + description = "Volume (%) at which BGM and jingles play."; + default = 100; + }; + reset_on_retry = mkOption { + type = types.bool; + apply = x: !x; + description = "If enabled, the background music will reset whenever you retry a game."; + default = false; + }; + preferences = mkOption { + type = + with types; + attrsOf (enum [ + "off" + "--" + "-" + "" + "+" + "++" + ]); + apply = + x: + pkgs.lib.mapAttrs ( + _: value: + { + "off" = "ban"; + "--" = "minmin"; + "-" = "min"; + "" = "base"; + "+" = "plus"; + "++" = "plusplus"; + } + ."${value}" + ) x; + default = { }; + }; + }; + sfx = { + volume = mkOption { + type = types.number; + apply = x: percentage (clamp x 0 200); + description = "Volume (%) at which sound effects play."; + default = 100; + }; + next_pieces = mkOption { + type = types.bool; + description = "Whether to play a sound effect that signifies the next piece that'll come up."; + default = false; + }; + other_players = mkOption { + type = types.bool; + description = "Whether to hear the sounds of other people playing in Multiplayer."; + default = true; + }; + attacks = mkOption { + type = types.bool; + description = "Whether to hear the sounds of attacks going towards and from you."; + default = true; + }; + speed_changes = mkOption { + type = types.bool; + description = "Whether to hear a sound when your Climb Speed changes in Quick Play."; + default = true; + }; + }; + scroll_adjust_volume = mkOption { + type = types.bool; + description = "Whether to allow adjusting the volume via scrolling in-game (or everywhere while holding ALT)."; + default = true; + }; + mute_unfocused = mkOption { + type = types.bool; + description = "If enabled, the background music will be muted when TETR.IO is minimized or tabbed away."; + default = true; + }; + stereo = mkOption { + type = types.number; + apply = x: percentage (clamp x 0 100); + description = "Intensity (%) of stereo effects. 0% -> Centered; 100% -> Directional"; + default = 50; + }; + disable = mkOption { + type = types.bool; + description = "If enabled, no audio will ever play. This will speed up the game, with a rather obvious drawback."; + default = false; + }; + }; + visual = + let + toFloat = x: if builtins.typeOf x == "str" then builtins.fromJSON x else x; + percentage = + x: min: max: + builtins.toString ((clamp (toFloat x) min max) / 100.0); + in + { + graphics = { + pipeline = mkOption { + type = types.enum [ + "compatability" + "webgl-1" + "webgl-2" + ]; + apply = + x: + { + "compatability" = "legacy"; + "webgl-1" = "webgl1"; + "webgl-2" = "webgl2"; + } + ."${x}"; + description = "The WebGL mode to use for rendering"; + default = "webgl-2"; + }; + tier = mkOption { + type = types.enum [ + "minimal" + "low" + "medium" + "high" + "ultra" + ]; + description = "The graphics tier to use for rendering"; + default = "ultra"; + }; + cache = mkOption { + type = types.bool; + apply = x: if x then "medium" else "low"; + description = "Whether or not to enable caching of resources"; + default = true; + }; + particle_count = mkOption { + type = with types; either str number; + apply = x: percentage x 10 150; + description = "How many particles to display (%)."; + default = 150; + }; + + powersave = mkOption { + type = types.bool; + description = "If enabled, prioritize power saving over performance."; + default = false; + }; + low_resolution = mkOption { + type = types.bool; + description = "If enabled, render blurrier graphics. Not very pretty but helps performance."; + default = false; + }; + low_precision_counters = mkOption { + type = types.bool; + description = "If enabled, don't show as much precision on in-game counters. Speeds up the game significantly."; + default = false; + }; + }; + show_unfocus_warning = mkOption { + type = types.bool; + description = "If enabled, a warning is shown when TETR.IO is out of focus."; + default = true; + }; + action_text = mkOption { + type = types.enum [ + "off" + "some" + "all" + ]; + description = "Determines how much action text to display when performing special attacks."; + default = "some"; + }; + board_bounciness = mkOption { + type = with types; either str number; + apply = x: percentage x 0 200; + description = "How much (%) the board reacts when you move pieces around."; + default = 40; + }; + damage_shakiness = mkOption { + type = with types; either str number; + apply = x: percentage x 0 110; + description = "How much (%) the board reacts when you receive damage."; + default = 100; + }; + grid_opacity = mkOption { + type = with types; either str number; + apply = x: percentage x 0 110; + description = "How visible the grid is (%). 0% makes the grid invisible."; + default = 10; + }; + board_opacity = mkOption { + type = with types; either str number; + apply = x: percentage x 0 110; + description = "How visible the board is (%). 0% makes the board invisible."; + default = 85; + }; + shadow_opacity = mkOption { + type = with types; either str number; + apply = x: percentage x 0 110; + description = "How visible the shadow piece is (%). 0% makes the shadow piece invisible."; + default = 15; + }; + background_opacity = mkOption { + type = with types; either str number; + apply = x: percentage x 0 100; + description = "How visible the background images are. 0% makes the background entirely black."; + default = 5; + }; + show_background_in_menus = mkOption { + type = types.bool; + apply = x: !x; + description = "If enabled, do not show the background when in menus. This will majorly improve performance in menus, but you will no longer see the background."; + default = false; + }; + board_zoom = mkOption { + type = with types; either str number; + apply = x: percentage x 0 110; + description = "How large the board displays. Values over 100% may cause some elements to not be visible."; + default = 100; + }; + spin_board = mkOption { + type = types.bool; + description = "When enabled, the board reacts to T-Spins by rotating a little with it."; + default = true; + }; + fire_meter = mkOption { + type = types.bool; + description = "If enabled, fill a fire meter when doing well, illuminating boards that have a lot of fire."; + default = true; + }; + danger_warning = mkOption { + type = types.bool; + description = "If enabled, makes the board red and play a warning sound when you're in danger."; + default = true; + }; + colored_shadow = mkOption { + type = types.bool; + description = "If enabled, colors the shadow piece."; + default = true; + }; + color_locked_hold_piece = mkOption { + type = types.bool; + description = "If enabled, the HOLD piece will be grayed out if it may not be used."; + default = true; + }; + + }; + multiplayer = { + chat = { + enable = mkOption { + type = types.bool; + apply = x: !x; + description = "If disabled, chat will be hidden when ingame."; + default = true; + }; + filter = mkOption { + type = types.bool; + description = "If enabled, profanity in chat will be filtered. Note that such filters are never perfect."; + default = true; + }; + show_emotes = mkOption { + type = types.bool; + description = "If enabled, show emotes in chat."; + default = true; + }; + show_animated_emotes = mkOption { + type = types.bool; + description = "If enabled, show animated emotes in chat."; + default = true; + }; + invert = mkOption { + type = types.bool; + description = "If enabled, chat text shows in black (good for light backgrounds)."; + default = false; + }; + darken = mkOption { + type = types.bool; + description = "If enabled, show a background behind chat messages when typing."; + default = true; + }; + }; + notifications = + let + notification.optional = types.enum [ + "off" + "in-game" + "desktop" + ]; + notification.required = types.enum [ + "in-game" + "desktop" + ]; + apply = + x: + { + "off" = "off"; + "in-game" = "ingame"; + "desktop" = "both"; + } + ."${x}"; + in + { + when = { + friend_online = mkOption { + type = notification.optional; + inherit apply; + description = "Notify me when a friend goes online"; + default = "in-game"; + }; + friend_offline = mkOption { + type = notification.optional; + inherit apply; + description = "Notify me when a friend goes offline"; + default = "off"; + }; + friend_dm_recieved = mkOption { + type = notification.optional; + inherit apply; + description = "Notify me when a friend sends me a direct message"; + default = "desktop"; + }; + dm_recieved = mkOption { + type = notification.optional; + inherit apply; + description = "Notify me when a non-friend sends me a direct message"; + default = "desktop"; + }; + room_invite = mkOption { + type = notification.required; + inherit apply; + description = "Notify me when someone invites me to a room"; + default = "desktop"; + }; + other = mkOption { + type = notification.required; + inherit apply; + description = "Other notifications"; + default = "desktop"; + }; + }; + enable_desktop_notifications = mkOption { + type = types.bool; + description = "If enabled, show notifications outside of the game."; + default = true; + }; + suppress_while_playing = mkOption { + type = types.bool; + description = "If enabled, unimportant notifications don't show ingame, but will be shown after the game."; + default = false; + }; + full_volume = mkOption { + type = types.bool; + description = "If enabled, notification sounds always play at full volume."; + default = true; + }; + }; + hide_room_ids = mkOption { + type = types.bool; + description = "If enabled, room IDs will not be shown, to protect you from streamsniping."; + default = false; + }; + show_network_warnings = mkOption { + type = types.bool; + apply = x: !x; + description = "If disabled, network warning icons will not be shown."; + default = true; + }; + notify_elim = mkOption { + type = types.bool; + description = "If enabled, show a popup when you KO someone or get KO'd."; + default = true; + }; + duels_side_by_side = mkOption { + type = types.bool; + description = "Whether to display a duel side-by-side. This causes your own board to move slightly to the left."; + default = true; + }; + simplify_thumbnails = mkOption { + type = types.bool; + description = "If enabled, always shows the simpler thumbnails for other players. This increases performance in games between 2 and 8 players, but looks less nice."; + default = false; + }; + animate_super_lobby_background = mkOption { + type = types.bool; + apply = x: !x; + description = "If enabled, the background of Super Lobbies (rooms of 100+ players) animate."; + default = true; + }; + animate_background_in_quickplay = mkOption { + type = types.bool; + apply = x: !x; + description = "If enabled, the background of Quick Play animate."; + default = true; + }; + welcome_guide = mkOption { + type = types.bool; + description = "If enabled, show the simple guide with the keybinds in multiplayer lobbies."; + default = true; + }; + }; + keep_replay_tools_open = mkOption { + type = types.bool; + description = "If enabled, the replay tools do not collapse when you're not using them."; + default = false; + }; + skip_login_screen = mkOption { + type = types.enum [ + "never" + "by-url" + "always" + ]; + apply = + x: + { + "never" = "never"; + "by-url" = "quickjoin"; + "always" = "always"; + } + ."${x}"; + description = "When to skip the login screen (\"by-url\": only when joining matches or viewing replays by url)"; + default = "always"; + }; + discord_rpc = mkOption { + type = types.bool; + description = "If enabled, show your current activity in Discord."; + default = true; + }; + flash_taskbar_icon = mkOption { + type = types.bool; + description = "If enabled, the taskbar icon will flash when something important happens."; + default = true; + }; + devtools = mkOption { + type = types.bool; + description = "If enabled, allow opening the developer toolbox."; + default = false; + }; + advertisments.i_support_the_devs.i_cannot_play_with_ads.and_i_really_want_to.disable = mkOption { + type = types.bool; + description = "If enabled, most/all third-party ads will not show."; + default = false; + }; + }; + }; + + config = mkIf cfg.enable ( + lib.mkMerge [ + { + home.packages = [ + (cfg.package.override { + withTetrioPlus = cfg.plus.enable; + tetrio-plus = cfg.plus.package; + }) + ]; + + home.initialFile.".config/tetrio-desktop/Local Storage/leveldb" = { + mode = "0644"; + source = leveldb { + value = { + handling = + let + set = cfg.settings.handling; + in + { + arr = set.auto_repeat_rate; + das = set.delayed_auto_shift; + dcd = set.das_cut_delay; + sdf = set.soft_drop_factor; + safelock = set.prevent_hard_drops; + cancel = set.direction_cancel_das; + may20g = set.soft_drop_over_movement; + irs = set.rotation_buffering; + ihs = set.hold_buffering; + }; + volume = + let + set = cfg.settings.audio; + in + { + music = set.music.volume; + bgmtweak = set.music.preferences; + sfx = set.sfx.volume; + stereo = set.stereo; + scrollable = set.scroll_adjust_volume; + oof = set.mute_unfocused; + next = set.sfx.next_pieces; + others = set.sfx.other_players; + attacks = set.sfx.attacks; + zenithrank = set.sfx.speed_changes; + disable = set.disable; + }; + video = + let + set = cfg.settings.visual; + mul = cfg.settings.multiplayer; + in + { + actionText = set.action_text; + bounciness = set.board_bounciness; + shakiness = set.damage_shakiness; + gridopacity = set.grid_opacity; + boardopacity = set.board_opacity; + shadowopacity = set.shadow_opacity; + zoom = set.board_zoom; + sidebyside = mul.duels_side_by_side; + spin = set.spin_board; + kos = mul.notify_elim; + siren = set.danger_warning; + colorshadow = set.colored_shadow; + + graphics = set.graphics.tier; + caching = set.graphics.cache; + webgl = set.graphics.pipeline; + particles = set.graphics.particle_count; + background = set.background_opacity; + powersave = set.graphics.powersave; + lowres = set.graphics.low_resolution; + lowrescounters = set.graphics.low_precision_counters; + alwaystiny = mul.simplify_thumbnails; + nosuperlobbyanim = mul.animate_super_lobby_background; + nozenithanim = mul.animate_background_in_quickplay; + nobg = set.show_background_in_menus; + chatfilter = mul.chat.filter; + nochat = mul.chat.enable; + hideroomids = mul.hide_room_ids; + emotes = mul.chat.show_emotes; + emotes_anim = mul.chat.show_animated_emotes; + invert = mul.chat.invert; + chatbg = mul.chat.darken; + replaytoolsnocollapse = cfg.settings.keep_replay_tools_open; + hidenetwork = mul.show_network_warnings; + focuswarning = set.show_unfocus_warning; + guide = mul.welcome_guide; + + desktopnotifications = mul.notifications.enable_desktop_notifications; + }; + notifications = + let + set = cfg.settings.multiplayer.notifications; + in + { + suppress = set.suppress_while_playing; + forcesound = set.full_volume; + online = set.when.friend_online; + offline = set.when.friend_offline; + dm = set.when.friend_dm_recieved; + dm_pending = set.when.dm_recieved; + invite = set.when.room_invite; + other = set.when.other; + }; + electron = { + loginskip = cfg.settings.skip_login_screen; + adblock = + cfg.settings.advertisments.i_support_the_devs.i_cannot_play_with_ads.and_i_really_want_to.disable; + taskbarflash = cfg.settings.flash_taskbar_icon; + autoupdate = false; + anglecompat = false; + presence = cfg.settings.discord_rpc; + devtools = cfg.settings.devtools; + }; + }; + }; + recursive = true; + }; + } + (mkIf (cfg.plus.enable) { + home.file.".config/tetrio-desktop/tetrioplus/tpkey-tetrioPlusEnabled.json" = { + text = '' + { "value": true } + ''; + force = true; + }; + home.file.".config/tetrio-desktop/tetrioplus/tpkey-hideTetrioPlusOnStartup.json" = { + text = '' + { "value": ${lib.trivial.boolToString cfg.plus.hideOnStartup} } + ''; + force = true; + }; + }) + (mkIf (cfg.plus.enable && cfg.plus ? skin) { + home.file.".config/tetrio-desktop/tetrioplus/tpkey-skin.json" = { + source = cfg.plus.skin.package; + force = true; + }; + home.file.".config/tetrio-desktop/tetrioplus/tpkey-forceNearestScaling.json" = { + text = '' + { "value": ${lib.trivial.boolToString cfg.plus.skin.nearest} } + ''; + force = true; + }; + }) + ] + ); +} diff --git a/pkgs/tetrio/skins/simple-connected.nix b/pkgs/tetrio/skins/simple-connected.nix new file mode 100644 index 0000000..7d5f7d5 --- /dev/null +++ b/pkgs/tetrio/skins/simple-connected.nix @@ -0,0 +1,31 @@ +{ + pkgs ? import { }, + ... +}: + +let + src = pkgs.fetchurl { + url = "https://you.have.fail/tetrioplus/data/tpsefiles/skin/SpooKoArts/simple_connected.zip.tpse"; + hash = "sha256-dIrEpEV9Gy2iU6K6rMrNX4XFQEchkJqSmOuQwVF4EQQ="; + }; +in +pkgs.stdenv.mkDerivation { + name = "simple-connected"; + version = "2022-06-26"; + inherit src; + + dontUnpack = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + cp ${src} $out + runHook postInstall + ''; + + fixupPhase = '' + runHook preFixup + sed -i 's/\bskin\b/value/' $out + runHook postFixup + ''; +}