Initial Commit

This commit is contained in:
Price Hiller 2023-12-18 09:37:50 -06:00
commit 8bdb2453d4
Signed by: Price
SSH Key Fingerprint: SHA256:Y4S9ZzYphRn1W1kbJerJFO6GGsfu9O70VaBSxJO7dF8
4 changed files with 1067 additions and 0 deletions

0
README.org Normal file
View File

61
flake.lock Normal file
View File

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1702312524,
"narHash": "sha256-gkZJRDBUCpTPBvQk25G0B7vfbpEYM5s5OZqghkjZsnE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a9bf124c46ef298113270b1f84a164865987a91c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

20
flake.nix Normal file
View File

@ -0,0 +1,20 @@
{
description = "Squad Servers NixOS Module";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, flake-utils, nixpkgs }: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
nixpkgs-fmt
];
};
nixosModules.default = (import ./nixos-modules);
});
}

View File

@ -0,0 +1,986 @@
{ config, lib, pkgs, ... }:
let
cfg = config.services.squad-server;
settingsFormat = pkgs.formats.keyValue { };
replaceNonAlum = rep: str: (builtins.foldl' (x: y: if builtins.isString y then x + y else x + rep)
""
(builtins.split "[^[:alnum:]]" str));
in
{
options.services.squad-server = {
servers = lib.mkOption {
description = ''
The squad servers to create and run.
Defined as `servers.<name>`. By default the `<name>` will be used as the
`servers.<name>.config.server.settings.ServerName`.
'';
type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
options = rec {
enable = lib.mkEnableOption "Enable Squad Server";
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open ports in the firewall for the server.
'';
};
gamePort = lib.mkOption {
type = lib.types.port;
default = 7787;
apply = (port: [ port (port + 1) ]);
description = ''
The server's game port. This will open the port specified here and the `gamePort + 1` as
Squad needs both open.
'';
};
queryPort = lib.mkOption {
type = lib.types.port;
apply = (port: [ port (port + 1) ]);
default = 27165;
description = ''
The server's query port. This will open the port specified here and the `queryPort + 1` as
Squad needs both open.
'';
};
rconPort = lib.mkOption {
type = lib.types.port;
apply = (port: [ port ]);
default = 21114;
description = ''
The server's rcon port. This is needed for remote administration of the server.
'';
};
beaconPort = lib.mkOption {
type = lib.types.port;
apply = (port: [ port ]);
default = 15000;
description = ''
The server's Epic Online Services beacon port.
'';
};
stateDir = lib.mkOption {
type = lib.types.str;
default = "squad/${replaceNonAlum "-" name}";
description = ''
State directory for the systemd user service. This is where the Squad Server will be
installed to along with configuration.
'';
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "squad/${replaceNonAlum "-" name}";
description = ''
State directory for the systemd user service.
'';
};
config = {
rcon = {
settings = lib.mkOption {
description = ''
Options to be defined in Rcon.cfg.
See https://squad.fandom.com/wiki/Server_Configuration#Rcon_control_in_Rcon.cfg for more
details.
'';
default = { };
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
IP = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = ''
IP to bind the RCON socket to an alternate IP address.
'';
};
MaxConnections = lib.mkOption {
type = lib.types.ints.positive;
default = 5;
description = ''
Maximum number of allowable concurrent RCON connections
'';
};
Password = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
The password to provide to RCON. If this is empty (default) then RCON is disabled.
Prefer the `config.rcon.passwordFile` option so the password is not copied into
the Nix Store.
'';
};
ConnectionTimeout = lib.mkOption {
type =
lib.types.addCheck lib.types.ints.unsigned (x: x <= 86400);
default = 300;
description = ''
Number of seconds without contact from a connected console before the server
checks to see if the session is still active or if it got disconnected. Supports
values between 0 and 86400 (1 day). Set to zero to disable the timeout.
'';
};
SecondsBeforeTimeoutCheck = lib.mkOption {
type = lib.types.addCheck lib.types.ints.positive
(x: x >= 30 && x <= 3600);
default = 120;
description = ''
Number of seconds without contact from a connected console before the server sends
a TCP KEEPALIVE to check if the session is still active or if it dog disconnected.
Supports values between 30 and 3600 (1 hour).
'';
};
AuthenticationTimeout = lib.mkOption {
type =
lib.types.addCheck lib.types.ints.unsigned (x: x <= 3600);
default = 5;
description = ''
Number of seconds the server will wait for the console to authenticate when a
connection has been established. Supports values between 0 and 3600 (1 hour). Set
to zero to disable the timeout.
'';
};
};
};
};
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = "The file to read the rcon password from.";
};
};
admins = lib.mkOption {
description = ''
Groups to be defined in the Admin config along with users in the groups.
'';
default = { };
apply = groups: lib.attrsets.foldlAttrs
(acc: groupName: group: ''
${acc}${lib.optionalString (group.comment != null) ''
// ${lib.concatStringsSep "\n// " (lib.splitString "\n" (lib.removeSuffix "\n" group.comment))}''}
Group=${groupName}:${lib.concatStringsSep "," group.accessLevels}
${builtins.foldl' (acc: user: ''
${acc}Admin=${user.id}:${groupName} ${lib.optionalString (user.comment != null) "// ${user.comment}"}
'') "" group.members}
'') ""
groups;
type = lib.types.attrsOf (lib.types.submodule {
options = {
comment = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = ''
Optionally add a comment for the group in the Admin config.
'';
};
accessLevels = lib.mkOption {
type = lib.types.listOf (lib.types.enum [
"startvote"
"changemap"
"pause"
"cheat"
"private"
"balance"
"chat"
"kick"
"ban"
"config"
"cameraman"
"immune"
"manageserver"
"featuretest"
"reserve"
"demos"
"clientdemos"
"debug"
"teamchange"
"forceteamchange"
"canseeadminchat"
]);
default = [ ];
description = ''
A list of strings relating to valid access levels for admins in Squad's
admin config.
Valid access levels are:
startvote - Not used
changemap - Change the current map or set the next map
pause - Pause server gameplay
cheat - Use server cheat commands
private - Password protect server
balance - Group Ignores server team balance
chat - Admin chat and Server broadcast
kick - Kick players from the server
ban - Ban players from the server
config - Change server config
cameraman - Admin spectate mode
immune - Cannot be kicked / banned
manageserver - Shutdown server
featuretest - Any features added for testing by dev team
reserve - Reserve slot
demos - Record Demos on the server side via admin commands
clientdemos - Record Demos on the client side via commands or the replay UI.
debug - show admin stats command and other debugging info
teamchange - No timer limits on team change
forceteamchange - Can issue the ForceTeamChange command
canseeadminchat - This group can see the admin chat and teamkill/admin-join notifications
'';
};
members = lib.mkOption {
description = ''
Members that are in the group.
'';
default = [ ];
type = lib.types.listOf (lib.types.submodule {
options = {
# TODO: Improve constraints to ensure this is a steam64 id
id = lib.mkOption {
type = lib.types.ints.positive;
apply = (val: builtins.toString val);
description = ''
A user's steam64 id.
'';
};
comment = lib.mkOption {
type = lib.types.nullOr lib.types.singleLineStr;
default = null;
description = ''
Optionally add a comment for the user in the Admin config.
'';
};
};
});
};
};
});
};
bans = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Manual bans to add to the server configuration.
Basic Format: `<banned player steamid>:<unix timestamp of ban expiration>`.
For additional details see
https://squad.fandom.com/wiki/Server_Configuration#Bans_in_Bans.cfg.
'';
};
customOptions = lib.mkOption {
description = ''
Custom options for mods in key-value format. Note that seed settings are considered mod
settings for the purposes of Squad server configuration.
See https://squad.fandom.com/wiki/Server_Configuration#Custom_Options for more
details.
'';
default = { };
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
SeedPlayersThreshold = lib.mkOption {
type = lib.types.ints.positive;
default = 50;
description = ''
Amount of players needed to start the pre-live countdown.
'';
};
SeedMinimumPlayersToLive = lib.mkOption {
type = lib.types.ints.positive;
default = 45;
description = ''
After reaching the SeedPlayersThreshold, if some players disconect, but the current
player count stays at or above this value, don't stop the pre-live countdown. Should
be less than SeedPlayersThreshold to be considered enabled.
'';
};
SeedMatchLengthSeconds = lib.mkOption {
type = lib.types.ints.positive;
default = 21600;
description = ''
Match length of a seed in seconds.
'';
};
SeedInitialTickets = lib.mkOption {
type = lib.types.ints.positive;
default = 100;
description = ''
Initial tickets for both teams.
'';
};
SeedAllKitsAvailable = lib.mkOption {
type = lib.types.bool;
default = true;
apply = (val: if val == true then 1 else 0);
description = ''
Enable or disable availability of all kits during seeding phase.
'';
};
SeedSecondsBeforeLive = lib.mkOption {
type = lib.types.float;
default = 60.0;
description = ''
Length of the pre-live countdown.
'';
};
};
};
};
excludedFactions = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Exlude factions from the rotation.
See https://squad.fandom.com/wiki/Server_Configuration#Excluded_Factions for more
details.
'';
};
excludedFactionSetups = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Exlude specific faction setups from the rotation.
See https://squad.fandom.com/wiki/Server_Configuration#Excluded_Faction_Setups for
more details.
'';
};
excludedLayers = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Exclude layers from loading.
See https://squad.fandom.com/wiki/Server_Configuration#Excluded_Layers for
more details.
'';
};
excludedLevels = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Exclude entire maps/levels from loading.
See https://squad.fandom.com/wiki/Server_Configuration#Excluded_Levels for
more details.
'';
};
levelRotation = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Set rotation of maps/levels allowing any layer on those maps.
See https://squad.fandom.com/wiki/Server_Configuration#Level_Rotation for
more details.
'';
};
layerRotation = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Set rotation of specific layers.
See https://squad.fandom.com/wiki/Server_Configuration#Layer_Rotation for
more details.
'';
};
serverMessages = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
Server messages to show on a rotation based on the ServerMessageInterval.
See
https://squad.fandom.com/wiki/Server_Configuration#Server_Messages_in_ServerMessages.cfg
for more details.
'';
};
motd = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Message to show to all players who join the server.
See https://squad.fandom.com/wiki/Server_Configuration#Message_of_the_day_in_Motd.cfg
for more details.
'';
};
remoteAdminLists = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
The remote admin lists that the server will also pull from for admins.
See
https://squad.fandom.com/wiki/Server_Configuration#Remote_Admin_Lists_in_RemoteAdminListHosts.cfg
for more details.
'';
};
remoteBanLists = lib.mkOption {
type = lib.types.listOf lib.types.str;
apply = lib.concatStringsSep "\n";
default = [ ];
description = ''
The remote ban lists that the server will also pull from for bans.
See
https://squad.fandom.com/wiki/Server_Configuration#Remote_Ban_Lists_in_RemoteBanListHosts.cfg
for more details.
'';
};
server = {
passwordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
The file to read the server password from. If this is set then the server will
require a password. Prefer this option over `ServerPassword`.
'';
};
maxTickRate = lib.mkOption {
type = lib.types.ints.positive;
default = 35;
description = ''
The max tick rate the server will run at. Recommended to use a tick rate of 35 (the
default).
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
ServerName = lib.mkOption {
type = lib.types.str;
default = "${name}";
description = ''
Server name of the server to show in the server browser.
Multiple servers MUST have unique names.
'';
};
ServerPassword = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
The password required to join the server. If this is empty (defualt) then the
server will be joinable without a password. Prefer the
`config.server.passwordFile` option so the password is not copied into the Nix
Store.
'';
};
ShouldAdvertise = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether or not the server should appear in the server browser.
'';
};
IsLANMatch = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Set the server to LAN mode.
'';
};
MaxPlayers = lib.mkOption {
type = lib.types.ints.positive;
# By default most licensed servers allow up to 100 players.
default = 100;
description = ''
Set the player limit for the server.
'';
};
NumReservedSlots = lib.mkOption {
type = lib.types.ints.positive;
default = 2;
description = ''
Set the number of reserved slots for those with `reserve` perms in the admin list.
'';
};
PublicQueueLimit = lib.mkOption {
type = lib.types.addCheck lib.types.int (x: x >= -1);
default = 25;
description = ''
The limit on how many players can be queued to join the server.
If set to -1 then the queue is unlimited.
'';
};
MapRotationMode = lib.mkOption {
type = lib.types.enum [
"LevelList"
"LayerList"
"LevelList_Randomized"
"LayerList_Randomized"
];
default = "LayerList";
description = ''
The map rotation mode to use. If set to LevelList, will use level rotation, if set
to LayerList, will use layer rotation. Suffixing with `_Randomized` will respect
the defined layers/levels, but not their ordering.
See https://squad.fandom.com/wiki/Server_Configuration#Map_Rotation_Modes for more
details.
'';
};
RandomizeAtStart = lib.mkOption {
type = lib.types.bool;
default = false;
readOnly = true;
visible = false;
description = ''
Whether the Map/Layer rotations list should be randomized at start.
According to Squad Configs "DO NOT USE, MODDED WILL NOT WORK".
'';
};
UseVoteFactions = lib.mkOption {
type = lib.types.bool;
default = false;
readOnly = true;
visible = false;
description = ''
Whether the Faction should be voted on at the end of a round.
At the time this was created, Squad's voting system does not work.
'';
};
UseVoteLevel = lib.mkOption {
type = lib.types.bool;
default = false;
readOnly = true;
visible = false;
description = ''
Whether the level should be voted on at the end of a round.
At the time this was created, Squad's voting system does not work.
'';
};
UseVoteLayer = lib.mkOption {
type = lib.types.bool;
default = false;
readOnly = true;
visible = false;
description = ''
Whether the layer should be voted on at the end of a round.
At the time this was created, Squad's voting system does not work.
'';
};
AllowTeamChanges = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Completely Allow or Disallow team changes to all players. Only users in the admin
config with `Level_Balance` can bypass this.
'';
};
PreventTeamChangeIfUnbalanced = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
If disabled, players can always change teams regardless of the balance.
'';
};
NumPlayersDiffForTeamChanges = lib.mkOption {
type = lib.types.ints.unsigned;
default = 2;
description = ''
Maximum allowed difference in player count between teams. This takes into account
the team the player leaves and the team the player joins.
'';
};
RejoinSquadDelayAfterKick = lib.mkOption {
type = lib.types.ints.unsigned;
default = 180;
description = ''
Amount of time before a player kicked from a squad can rejoin that squad.
'';
};
RecordDemos = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow admins with `ClientDemos` permission to record demos. It's recommended to
leave this disabled as it can be used to cheat easily without a way to detect if
cheating is occuring.
'';
};
AllowPublicClientsToRecord = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow any playersto record demos. It's recommended to leave this disabled as it
can be used to cheat easily without a way to detect if cheating is occuring.
'';
};
ServerMessageInterval = lib.mkOption {
type = lib.types.ints.positive;
default = 1200;
description = ''
Interval between showing server messages.
'';
};
TKAutoKickEnabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether or not to kick players who exceed the `AutoTKBanNumberTKs` limit.
NOTE: Licensed servers MUST enable this option.
'';
};
AutoTKBanNumberTKs = lib.mkOption {
type = lib.types.ints.positive;
default = 10;
description = ''
How many TKs a player may have before being kicked.
NOTE: Licensed servers MUST set this option between 7 and 10 inclusive.
'';
};
AutoTKBanTime = lib.mkOption {
type = lib.types.ints.unsigned;
default = 300;
description = ''
How long to reject a player auto kicked for TKs from joining in seconds.
NOTE: Licensed servers MUST set this option to be more than 0.
'';
};
AllowDevProfiling = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to allow Offword Industries Developers to be admins in the server.
NOTE: Licensed servers MUST enable this option.
'';
};
VehicleClaimingDisabled = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to disable vehicle claiming.
NOTE: Licensed servers MUST disable this option.
'';
};
Tags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
apply = lib.concatStringsSep " ";
description = ''
Tags to apply to the server to be shown in the server browser.
See https://squad.fandom.com/wiki/Server_Configuration#Tag_System for more details.
'';
};
Rules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
apply = lib.concatStringsSep " ";
description = ''
Rules to apply to the server to be shown in the server browser.
See https://squad.fandom.com/wiki/Server_Configuration#Tag_System for more details.
'';
};
};
};
default = { };
description = ''
Options to be defined in Server.cfg
See
https://squad.fandom.com/wiki/Server_Configuration#Server_Configuration_Settings_in_Server.cfg
for more details.
'';
};
};
license = {
file = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
A path to a file containing the server license. Prefer this over
`config.license.content` so the license text isn't copied into the Nix
store.
'';
};
content = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
The raw content of the license for the server. Prefer using the
`config.license.file` option over this as the content in this option will be copied
into the Nix store.
'';
};
};
};
extraStartArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Additional arguments to pass to the Squad Server when it is launched.
'';
};
};
}));
};
};
config =
let
# Credit to https://github.com/mkaito/nixos-modded-minecraft-servers/tree/master.
# A fair bit of the handling of the nested servers was based upon the code there.
enabledServers = lib.filterAttrs (_: conf: conf.enable) cfg.servers;
mkServerName = name: replaceNonAlum "-" name;
eachEnabledServer = f: lib.mapAttrs' (name: config: lib.nameValuePair (mkServerName name) (f name config)) enabledServers;
collectPorts = portType: lib.lists.flatten (lib.mapAttrsToList (_: serverConfig: serverConfig.${portType}) enabledServers);
gamePorts = collectPorts "gamePort";
queryPorts = collectPorts "queryPort";
rconPorts = collectPorts "rconPort";
beaconPorts = collectPorts "beaconPort";
allPorts = gamePorts ++ queryPorts ++ rconPorts ++ beaconPorts;
in
{
assertions = [
{
assertion = (lib.unique gamePorts) == gamePorts;
message = ''
Your Squad servers have overlapping game ports. Ensure the game ports are unique.
Reminder: Squad uses the game port you define and `gamePort + 1`.
Game Ports Found:
${builtins.toJSON gamePorts}
'';
}
{
assertion = (lib.unique queryPorts) == queryPorts;
message = ''
Your Squad servers have overlapping query ports. Ensure the query ports are unique.
Reminder: Squad uses the query port you define and `queryPort + 1`.
Query Ports Found:
${builtins.toJSON queryPorts}
'';
}
{
assertion = (lib.unique rconPorts) == rconPorts;
message = ''
Your Squad servers have overlapping rcon ports. Ensure the rcon ports are unique.
Rcon Ports Found:
${builtins.toJSON rconPorts}
'';
}
{
assertion = (lib.unique beaconPorts) == beaconPorts;
message = ''
Your Squad servers have overlapping beacon ports. Ensure the beacon ports are unique.
Rcon Ports Found:
${builtins.toJSON beaconPorts}
'';
}
{
assertion = (lib.unique allPorts) == allPorts;
message = ''
Your Squad servers have overlapping ports among game, query, rcon, and beacon ports.
Ensure all ports are unique among all Squad servers.
All Ports Found:
${builtins.toJSON allPorts}
'';
}
];
networking.firewall = {
allowedUDPPorts = beaconPorts ++ gamePorts ++ queryPorts ++ rconPorts;
allowedTCPPorts = rconPorts ++ queryPorts;
};
systemd.services = (eachEnabledServer (name: cfg:
let
cfgs = {
Admins = pkgs.writeText "Admins.cfg" cfg.config.admins;
Bans = pkgs.writeText "Bans.cfg" cfg.config.bans;
CustomOptions = settingsFormat.generate "CustomOptions.cfg" cfg.config.customOptions;
ExcludedFactionSetups = pkgs.writeText "ExcludedFactionSetups.cfg" cfg.config.excludedFactionSetups;
ExcludedFactions = pkgs.writeText "ExcludedFactions.cfg" cfg.config.excludedFactions;
ExcludedLayers = pkgs.writeText "ExcludedLayers.cfg" cfg.config.excludedLayers;
ExcludedLevels = pkgs.writeText "ExcludedLevels.cfg" cfg.config.excludedLevels;
LayerRotation = pkgs.writeText "LayerRotation.cfg" cfg.config.layerRotation;
LevelRotation = pkgs.writeText "LayerRotation.cfg" cfg.config.levelRotation;
License = pkgs.writeText "License.cfg" cfg.config.license.content;
MOTD = pkgs.writeText "MOTD.cfg" cfg.config.motd;
Rcon = settingsFormat.generate "Rcon.cfg" cfg.config.rcon.settings;
RemoteAdminListHosts = pkgs.writeText "RemoteAdminListHosts.cfg" cfg.config.remoteAdminLists;
RemoteBanListHosts = pkgs.writeText "RemoteBanListHosts.cfg" cfg.config.remoteBanLists;
Server = settingsFormat.generate "Server.cfg" cfg.config.server.settings;
ServerMessages = pkgs.writeText "ServerMessages.cfg" cfg.config.serverMessages;
};
in
{
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
StateDirectory = "${cfg.stateDir}";
CacheDirectory = "${cfg.cacheDir}";
StateDirectoryMode = "0700";
LoadCredential = [ ]
++
lib.optional
(cfg.config.rcon.passwordFile != null)
[ "SQUAD_RCON_PASSWORD_FILE:${cfg.config.rcon.passwordFile}" ]
++
lib.optional
(cfg.config.server.passwordFile != null)
[ "SQUAD_SERVER_PASSWORD_FILE:${cfg.config.server.passwordFile}" ]
++
lib.optional
(cfg.config.license.file != null)
[ "SQUAD_LICENSE_FILE:${cfg.config.license.file}" ];
ExecStart =
let
server_dir = "/var/lib/${cfg.stateDir}";
in
pkgs.writeScript "start-squad-server" ''
#!${pkgs.bash}/bin/bash
set -euo pipefail
# Install or update the server.
cat <<-__EOS__
Installing/Updating Squad Server:
Name -> '${cfg.config.server.settings.ServerName}'
Path -> '${server_dir}'
This may take a while as the server will need to download any required files if they
weren't downloaded previously.
__EOS__
HOME="/var/cache/${cfg.cacheDir}" ${pkgs.steamcmd}/bin/steamcmd \
+force_install_dir "${server_dir}" \
+login anonymous \
+app_update 403240 validate \
+quit
cat <<-__EOS__
Patching Squad Binaries
__EOS__
find "${server_dir}/" \
-type f \
-executable \
-printf "patchelf: Attempting to patch '%p'\n" \
-exec \
${pkgs.patchelf}/bin/patchelf --set-interpreter ${pkgs.glibc}/lib/ld-linux-x86-64.so.2 {} \;
cat <<-__EOS__
Generating Configurations
__EOS__
pushd ./SquadGame/ServerConfig >/dev/null 2>&1
${lib.attrsets.foldlAttrs (acc: name: path: ''
${acc}
# Handle the ${name} configuration
printf "Generating the '%s' configuration file.\n" "${name}.cfg"
cp -f "${path}" ./"${name}.cfg"
'') "" cfgs}
${lib.optionalString (cfg.config.server.passwordFile != null) ''
## Handle secrets for the `Server.cfg` file ##
# Safely load the server password outside of the nix store
sed -i -e 's/^ServerPassword=.*$/ServerPassword='"$(${pkgs.systemd}/bin/systemd-creds cat SQUAD_SERVER_PASSWORD_FILE)"'/g' ./Server.cfg
''}
# Correct the permissions for the Squad Server cfgs. When the Squad Server is first
# installed it will include the configs by default with an overly open CHMOD.
chmod 0400 *.cfg
${lib.optionalString (cfg.config.rcon.passwordFile != null) ''
## Handle secrets for the `Rcon.cfg` file ##
# Safely load the rcon password outside of the nix store
sed -i -e 's/^Password=.*$/Password='"$(${pkgs.systemd}/bin/systemd-creds cat SQUAD_RCON_PASSWORD_FILE)"'/g' ./Rcon.cfg
''}
${lib.optionalString (cfg.config.server.passwordFile != null) ''
## Handle secrets for the `License.cfg` file ##
# Safely load the license outside of the nix store
printf "%s" "$(${pkgs.systemd}/bin/systemd-creds cat SQUAD_LICENSE_FILE)" > ./License.cfg
''}
popd >/dev/null 2>&1
cat <<-__EOS__
Starting Squad Server:
Name -> '${cfg.config.server.settings.ServerName}
Path -> '${server_dir}'
__EOS__
LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib" ./SquadGameServer.sh \
Port=${builtins.toString cfg.gamePort} \
QueryPort=${builtins.toString cfg.queryPort} \
FIXEDMAXTICKRATE=${builtins.toString cfg.config.server.maxTickRate} \
FIXEDMAXPLAYERS=${builtins.toString cfg.config.server.settings.MaxPlayers} \
beaconport=${builtins.toString cfg.beaconPort}
'';
WorkingDirectory = "/var/lib/${cfg.stateDir}";
};
}));
};
}