factorio: rudimentary mod support for factorio's nixos module

+111 -14
+38 -10
nixos/modules/services/games/factorio.nix
··· 4 4 5 5 let 6 6 cfg = config.services.factorio; 7 + factorio = pkgs.factorio-headless; 7 8 name = "Factorio"; 8 9 stateDir = "/var/lib/factorio"; 10 + mkSavePath = name: "${stateDir}/saves/${name}.zip"; 9 11 configFile = pkgs.writeText "factorio.conf" '' 10 12 use-system-read-write-data-directories=true 11 13 [path] 12 - read-data=${pkgs.factorio-headless}/share/factorio/data 14 + read-data=${factorio}/share/factorio/data 13 15 write-data=${stateDir} 14 16 ''; 17 + modDir = pkgs.factorio-mkModDirDrv cfg.mods; 15 18 in 16 19 { 17 20 options = { ··· 32 35 description = '' 33 36 The name of the savegame that will be used by the server. 34 37 35 - When not present in ${stateDir}/saves, it will be generated before starting the service. 38 + When not present in ${stateDir}/saves, a new map with default 39 + settings will be generated before starting the service. 36 40 ''; 37 41 }; 38 42 # TODO Add more individual settings as nixos-options? ··· 51 55 customizations. 52 56 ''; 53 57 }; 58 + mods = mkOption { 59 + type = types.listOf types.package; 60 + default = []; 61 + description = '' 62 + Mods the server should install and activate. 63 + 64 + The derivations in this list must "build" the mod by simply copying 65 + the .zip, named correctly, into the output directory. Eventually, 66 + there will be a way to pull in the most up-to-date list of 67 + derivations via nixos-channel. Until then, this is for experts only. 68 + ''; 69 + }; 70 + autosave-interval = mkOption { 71 + type = types.nullOr types.int; 72 + default = null; 73 + example = 2; 74 + description = '' 75 + The time, in minutes, between autosaves. 76 + ''; 77 + }; 54 78 }; 55 79 }; 56 80 ··· 74 98 wantedBy = [ "multi-user.target" ]; 75 99 after = [ "network.target" ]; 76 100 77 - preStart = '' 78 - test -e ${stateDir}/saves/${cfg.saveName}.zip || \ 79 - ${pkgs.factorio-headless}/bin/factorio \ 80 - --config=${cfg.configFile} \ 81 - --create=${stateDir}/saves/${cfg.saveName}.zip 82 - ''; 101 + preStart = toString [ 102 + "test -e ${stateDir}/saves/${cfg.saveName}.zip" 103 + "||" 104 + "${factorio}/bin/factorio" 105 + "--config=${cfg.configFile}" 106 + "--create=${mkSavePath cfg.saveName}" 107 + (optionalString (cfg.mods != []) "--mod-directory=${modDir}") 108 + ]; 83 109 84 110 serviceConfig = { 85 111 User = "factorio"; ··· 90 116 PrivateTmp = true; 91 117 UMask = "0007"; 92 118 ExecStart = toString [ 93 - "${pkgs.factorio-headless}/bin/factorio" 119 + "${factorio}/bin/factorio" 94 120 "--config=${cfg.configFile}" 95 121 "--port=${toString cfg.port}" 96 - "--start-server=${stateDir}/saves/${cfg.saveName}.zip" 122 + "--start-server=${mkSavePath cfg.saveName}" 123 + (optionalString (cfg.mods != []) "--mod-directory=${modDir}") 124 + (optionalString (cfg.autosave-interval != null) "--autosave-interval ${toString cfg.autosave-interval}") 97 125 ]; 98 126 }; 99 127 };
+22 -4
pkgs/games/factorio/default.nix
··· 1 1 { stdenv, callPackage, fetchurl, makeWrapper 2 2 , alsaLib, libX11, libXcursor, libXinerama, libXrandr, libXi, mesa_noglu 3 + , factorio-utils 3 4 , releaseType 5 + , mods ? [] 4 6 , username ? "" , password ? "" 5 7 }: 6 8 ··· 54 56 fi 55 57 ''; 56 58 59 + modDir = factorio-utils.mkModDirDrv mods; 60 + 57 61 base = { 58 62 name = "factorio-${releaseType}-${version}"; 59 63 60 64 src = fetch.${arch.inTar}.${releaseType}; 61 65 66 + preferLocalBuild = true; 62 67 dontBuild = true; 63 68 64 - # TODO detangle headless/normal mode wrapping, libs, etc. test all urls 32/64/headless/gfx 65 69 installPhase = '' 66 70 mkdir -p $out/{bin,share/factorio} 67 71 cp -a data $out/share/factorio ··· 71 75 $out/bin/factorio 72 76 ''; 73 77 74 - preferLocalBuild = true; 75 - 76 78 meta = { 77 79 description = "A game in which you build and maintain factories"; 78 80 longDescription = '' ··· 112 114 wrapProgram $out/bin/factorio \ 113 115 --prefix LD_LIBRARY_PATH : /run/opengl-driver/lib:$libPath \ 114 116 --run "$out/share/factorio/update-config.sh" \ 115 - --add-flags "-c \$HOME/.factorio/config.cfg" 117 + --add-flags "-c \$HOME/.factorio/config.cfg ${optionalString (mods != []) "--mod-directory=${modDir}"}" 118 + 119 + # TODO Currently, every time a mod is changed/added/removed using the 120 + # modlist, a new derivation will take up the entire footprint of the 121 + # client. The only way to avoid this is to remove the mods arg from the 122 + # package function. The modsDir derivation will have to be built 123 + # separately and have the user specify it in the .factorio config or 124 + # right along side it using a symlink into the store I think i will 125 + # just remove mods for the client derivation entirely. this is much 126 + # cleaner and more useful for headless mode. 127 + 128 + # TODO: trying to toggle off a mod will result in read-only-fs-error. 129 + # not much we can do about that except warn the user somewhere. In 130 + # fact, no exit will be clean, since this error will happen on close 131 + # regardless. just prints an ugly stacktrace but seems to be otherwise 132 + # harmless, unless maybe the user forgets and tries to use the mod 133 + # manager. 116 134 117 135 install -m0644 <(cat << EOF 118 136 ${configBaseCfg}
+49
pkgs/games/factorio/utils.nix
··· 1 + # This file provides a top-level function that will be used by both nixpkgs and nixos 2 + # to generate mod directories for use at runtime by factorio. 3 + { stdenv }: 4 + with stdenv.lib; 5 + { 6 + mkModDirDrv = mods: # a list of mod derivations 7 + let 8 + recursiveDeps = modDrv: [modDrv] ++ optionals (modDrv.deps == []) (map recursiveDeps modDrv.deps); 9 + modDrvs = unique (flatten (map recursiveDeps mods)); 10 + in 11 + stdenv.mkDerivation { 12 + name = "factorio-mod-directory"; 13 + 14 + preferLocalBuild = true; 15 + buildCommand = '' 16 + mkdir -p $out 17 + for modDrv in ${toString modDrvs}; do 18 + # NB: there will only ever be a single zip file in each mod derivation's output dir 19 + ln -s $modDrv/*.zip $out 20 + done 21 + ''; 22 + }; 23 + 24 + modDrv = { allRecommendedMods, allOptionalMods }: 25 + { src 26 + , name ? null 27 + , deps ? [] 28 + , optionalDeps ? [] 29 + , recommendedDeps ? [] 30 + }: stdenv.mkDerivation { 31 + 32 + inherit src; 33 + 34 + # Use the name of the zip, but endstrip ".zip" and possibly the querystring that gets left in by fetchurl 35 + name = replaceStrings ["_"] ["-"] (if name != null then name else removeSuffix ".zip" (head (splitString "?" src.name))); 36 + 37 + deps = deps ++ optionals allOptionalMods optionalDeps 38 + ++ optionals allRecommendedMods recommendedDeps; 39 + 40 + preferLocalBuild = true; 41 + buildCommand = '' 42 + mkdir -p $out 43 + srcBase=$(basename $src) 44 + srcBase=''${srcBase#*-} # strip nix hash 45 + srcBase=''${srcBase%\?*} # strip querystring leftover from fetchurl 46 + cp $src $out/$srcBase 47 + ''; 48 + }; 49 + }
+2
pkgs/top-level/all-packages.nix
··· 15594 15594 15595 15595 factorio-headless = callPackage ../games/factorio { releaseType = "headless"; }; 15596 15596 15597 + factorio-utils = callPackage ../games/factorio/utils.nix { }; 15598 + 15597 15599 fairymax = callPackage ../games/fairymax {}; 15598 15600 15599 15601 fish-fillets-ng = callPackage ../games/fish-fillets-ng {};