diff --git a/CODEOWNERS b/CODEOWNERS index da3e644b5a0..35b29b2607d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,8 @@ gnu/packages/game-development\.scm @guix/games gnu/packages/luanti\.scm @guix/games gnu/packages/esolangs\.scm @guix/games gnu/packages/motti\.scm @guix/games +gnu/services/games\.scm @guix/games +gnu/tests/games\.scm @guix/games guix/build/luanti-build-system\.scm @guix/games etc/teams/gnome @guix/gnome diff --git a/doc/guix.texi b/doc/guix.texi index c504ec06cd7..bd3c73824da 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -43209,6 +43209,91 @@ the @code{joycond-configuration} configuration), so that joycond controllers can be detected and used by an unprivileged user. @end defvar +@subsubheading Luanti service +@cindex luanti +@cindex voxel-based games +@uref{https://www.luanti.org/en/, Luanti} is a voxel game engine that +powers many games. This service is for hosting a Luanti server. The +various options can be configured via the @code{luanti-configuration} +record, documented below: + +@c %start of fragment + +@deftp {Data Type} luanti-configuration +Available @code{luanti-configuration} fields are: + +@table @asis +@item @code{luanti} (default: @code{luanti-server}) (type: file-like) +The Luanti package to use. + +@item @code{game} (default: @code{luanti-mineclonia}) (type: file-like) +The Luanti game package to serve. + +@item @code{game-configuration} (type: maybe-file-like) +A configuration file to use for the selected Luanti game, which +corresponds to the @file{minetest.conf} file. + +@item @code{mods} (type: maybe-list-of-file-likes) +A list of Luanti mod packages to use. Note that using mods is +complicated by the requirements of Luanti to 1) manually enable the mod +and any of its dependent mods in the @file{world.rt} file of the world +used and 2) to register the mod names and those of its dependents via a +@samp{secure.trusted_mods} @code{game-configuration} directive. Consult +the example below for more precise directions. + +@item @code{log-file} (default: @code{"/var/log/luanti.log"}) (type: maybe-string) +The log file to log to. To disable logging, set this to +@code{%unset-value}. + +@item @code{verbose?} (default: @code{#f}) (type: boolean) +Print more detailed information. + +@item @code{port} (default: @code{30000}) (type: port) +The UDP port the server should listen to. + +@item @code{world} (type: maybe-string) +An existing Luanti world directory to serve. If omitted, a new world is +created under the @file{/var/lib/luanti/.minetest/worlds/world} +directory. If an absolute file name is provided, it is used directly. +Otherwise, it is expected to be a directory under +@file{/var/lib/luanti/.minetest/worlds/}. + +@end table + +@end deftp + + +@c %end of fragment + +Here's the simplest example of a Luanti server, which in its default +configuration serves the @code{luanti-mineclonia} game. + +@lisp +(service luanti-service-type) +@end lisp + +Here's a slightly more elaborate one, which adds the +@code{luanti-whitelist} mod. Embedded are comments explaining extra +needed steps when using mods. Failing to do these steps will cause the +service to fail to start. + +@lisp +(service luanti-service-type + (luanti-configuration + (game luanti-mineclonia) + (game-configuration + (plain-file + "minetest.conf" + ;; lib_chatcmdbuilder is a dependency of the whitelist mod + "secure.trusted_mods = whitelist,lib_chatcmdbuilder\n")) + ;; The + ;; '/var/lib/luanti/.minetest/worlds/world/world.mt' + ;; file needs to be hand-edited to add: + ;; load_mod_whitelist = true + ;; load_mod_lib_chatcmdbuilder = true + (mods (list luanti-whitelist)))) +@end lisp + @subsubheading The Battle for Wesnoth Service @cindex wesnothd @uref{https://wesnoth.org, The Battle for Wesnoth} is a fantasy, turn diff --git a/etc/teams.scm b/etc/teams.scm index a5cdbf3e296..30739f34669 100755 --- a/etc/teams.scm +++ b/etc/teams.scm @@ -666,6 +666,8 @@ ecosystem." "gnu/packages/luanti.scm" "gnu/packages/esolangs.scm" ; granted, rather niche "gnu/packages/motti.scm" + "gnu/services/games.scm" + "gnu/tests/games.scm" "guix/build/luanti-build-system.scm"))) (define-team gnome diff --git a/gnu/services/games.scm b/gnu/services/games.scm index cea442fd3ae..9f78a3bc9b5 100644 --- a/gnu/services/games.scm +++ b/gnu/services/games.scm @@ -1,6 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2018 Arun Isaac ;;; Copyright © 2022 Ludovic Courtès +;;; Copyright © 2025 Maxim Cournoyer ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,20 +24,36 @@ #:use-module (gnu services shepherd) #:use-module (gnu packages admin) #:use-module (gnu packages games) + #:use-module (gnu packages luanti) #:use-module ((gnu services base) #:select (udev-service-type)) #:use-module (gnu system shadow) #:use-module ((gnu system file-systems) #:select (file-system-mapping)) #:use-module (gnu build linux-container) - #:autoload (guix least-authority) (least-authority-wrapper) + #:use-module (guix build utils) + #:autoload (guix least-authority) (%default-preserved-environment-variables + least-authority-wrapper) #:use-module (guix gexp) #:use-module (guix modules) #:use-module (guix packages) #:use-module (guix records) #:use-module (ice-9 match) + #:use-module (srfi srfi-1) #:export (joycond-configuration joycond-configuration? joycond-service-type + luanti-configuration + luanti-configuration? + luanti-configuration-game-configuration + luanti-configuration-game + luanti-configuration-mods + luanti-configuration-log-file + luanti-configuration-luanti + luanti-configuration-port + luanti-configuration-verbose? + luanti-configuration-world + luanti-service-type + wesnothd-configuration wesnothd-configuration? wesnothd-service-type)) @@ -71,6 +88,202 @@ install udev rules required to use the controller as an unprivileged user.") (compose list joycond-configuration-package)))) (default-value (joycond-configuration)))) + +;;; +;;; Luanti. +;;; + +(define list-of-file-likes? + (list-of file-like?)) + +(define-maybe/no-serialization list-of-file-likes) + +(define-maybe/no-serialization file-like) + +(define-maybe/no-serialization string) + +(define (port? x) + (and (number? x) + (and (>= 0) (<= x 65535)))) + +(define-configuration/no-serialization luanti-configuration + (luanti + (file-like luanti-server) + "The Luanti package to use.") + (game + (file-like luanti-mineclonia) + "The Luanti game package to serve.") + (game-configuration + maybe-file-like + "A configuration file to use for the selected Luanti game, which +corresponds to the @file{minetest.conf} file.") + (mods + maybe-list-of-file-likes + "A list of Luanti mod packages to use. Note that using mods is complicated +by the requirements of Luanti to 1) manually enable the mod and any of its +dependent mods in the @file{world.rt} file of the world used and 2) to +register the mod names and those of its dependents via a +@samp{secure.trusted_mods} @code{game-configuration} directive. Consult the +example below for more precise directions.") + (log-file + (maybe-string "/var/log/luanti.log") + "The log file to log to. To disable logging, set this to +@code{%unset-value}.") + (verbose? + (boolean #f) + "Print more detailed information.") + (port + (port 30000) + "The UDP port the server should listen to.") + (world + maybe-string + "An existing Luanti world directory to serve. If omitted, a new world is +created under the @file{/var/lib/luanti/.minetest/worlds/world} directory. If +an absolute file name is provided, it is used directly. Otherwise, it is +expected to be a directory under @file{/var/lib/luanti/.minetest/worlds/}.")) + +(define %luanti-account + (list (user-group + (name "luanti") + (system? #t)) + (user-account + (name "luanti") + (group "luanti") + (system? #t) + (comment "Luanti server user") + (home-directory "/var/lib/luanti")))) + +(define (luanti-activation config) + "Activation script for the Luanti server." + (match-record config (world) + #~(begin + (use-modules (guix build utils) + (srfi srfi-34)) + + (define user (getpwnam "luanti")) + (define* (sanitize-permissions file #:optional (mode #o400)) + (guard (c (#t #t)) + (chown file (passwd:uid user) (passwd:gid user)) + (chmod file mode))) + + (mkdir-p/perms "/var/lib/luanti" (getpwnam "luanti") #o755) + + ;; Sanitize the permissions of a provided pre-populated world + ;; directory. + (when #$(and (maybe-value-set? world) + (absolute-file-name? world)) + (for-each sanitize-permissions + (find-files #$world #:directories? #t)))))) + +(define (transitive-mods mods) + "Return the transitive list of mods in MODS, these included." + (append-map (lambda (m) + (if (package? m) + (cons m (map second ;drop label + (package-transitive-propagated-inputs m))) + (list m))) + (if (maybe-value-set? mods) + mods + '()))) + +(define (luanti-wrapper config) + "Return a least-authority wrapper for 'luantiserver', based on CONFIG, a + object." + (match-record config + (luanti game game-configuration log-file mods world) + (let ((mods (transitive-mods mods))) + (least-authority-wrapper + (file-append luanti "/bin/luantiserver") + #:name "luantiserver-pola-wrapper" + #:mappings + (let ((readable (filter maybe-value-set? + (append (list luanti game game-configuration) + mods))) + (writable (filter maybe-value-set? + (append (list "/var/lib/luanti" log-file) + (if (and (maybe-value-set? world) + (absolute-file-name? world)) + (list world) + '()))))) + (append (map (lambda (r) + (file-system-mapping + (source r) + (target source))) + readable) + (map (lambda (w) + (file-system-mapping + (source w) + (target source) + (writable? #t))) + writable))) + #:user "luanti" + #:group "luanti" + ;; XXX: The user namespace must be shared otherwise the UID is different + ;; in the container and Luanti fails to create its data directory. + #:namespaces (fold delq %namespaces '(user net)) + #:preserved-environment-variables + (cons* "LUANTI_GAME_PATH" "LUANTI_MOD_PATH" + %default-preserved-environment-variables))))) + +(define (luanti-shepherd-service config) + "Return the object of Luanti." + (match-record config + ( luanti game game-configuration log-file mods verbose? + port world) + ;; Some mods have dependencies on other mods; we need to ensure these gets + ;; added to the LUANTI_MOD_PATH as well. + (let ((mods (transitive-mods mods))) + (list (shepherd-service + (provision '(luanti)) + (requirement '(user-processes)) + (start #~(make-forkexec-constructor + (append (list #$(luanti-wrapper config) + "--port" (number->string #$port)) + (if #$(maybe-value-set? game-configuration) + '("--config" #$game-configuration) + '()) + (if #$verbose? + '("--verbose") + '()) + (if #$(maybe-value-set? world) + (if (absolute-file-name? #$world) + '("--world" #$world) + '("--worldname" #$world)) + '())) + #:environment-variables + (append + (list "HOME=/var/lib/luanti" + (string-append "LUANTI_GAME_PATH=" + #$game "/share/luanti/games") + (string-append + "LUANTI_MOD_PATH=" + (list->search-path-as-string + (search-path-as-list '("share/luanti/mods") + '#$mods) + ":")))) + #:log-file #$(and (maybe-value-set? log-file) + log-file))) + (stop #~(make-kill-destructor))))))) + +(define luanti-service-type + (service-type + (name 'luanti) + (extensions + (list (service-extension shepherd-root-service-type + luanti-shepherd-service) + (service-extension profile-service-type + (match-record-lambda + (luanti game) + (list luanti game))) + (service-extension account-service-type + (const %luanti-account)) + (service-extension activation-service-type + luanti-activation))) + (default-value (luanti-configuration)) + (description + "Run @url{https://www.luanti.org/en/, Luanti}, the voxel game engine, as a +server."))) + ;;; ;;; The Battle for Wesnoth server diff --git a/gnu/tests/games.scm b/gnu/tests/games.scm new file mode 100644 index 00000000000..52391f0caa9 --- /dev/null +++ b/gnu/tests/games.scm @@ -0,0 +1,108 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2025 Maxim Cournoyer +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix is distributed in the hope that it will be useful, but +;;; WITHOUT ANY WARRANTY; without even the implied warranty of +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;;; GNU General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (gnu tests games) + #:use-module (gnu packages luanti) + #:use-module (gnu tests) + #:use-module (gnu services) + #:use-module (gnu services games) + #:use-module (gnu system) + #:use-module (gnu system vm) + #:use-module (guix gexp) + #:use-module (guix modules) + #:export (%test-luanti)) + +(define (run-luanti-test name config) + "Run a test of an OS running LUANTI-SERVICE." + (define os + (marionette-operating-system + (simple-operating-system + (service luanti-service-type config)) + #:imported-modules '((gnu build dbus-service) + (gnu services herd)))) + + (define vm (virtual-machine + (operating-system os) + (memory-size 1024))) + + (define test + (with-imported-modules (source-module-closure + '((gnu build marionette))) + #~(begin + (use-modules (gnu build marionette) + (srfi srfi-64)) + + (define marionette + (make-marionette (list #$vm))) + + (test-runner-current (system-test-runner #$output)) + (test-begin "luanti") + + (test-assert "luanti service can be stopped" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (stop-service 'luanti)) + marionette)) + + (test-assert "luanti service can be started" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (start-service 'luanti)) + marionette)) + + (test-assert "luanti server is responding on configured port" + ;; This is based on the Python script example in doc/protocol.txt. + (marionette-eval + `(begin + (use-modules ((gnu build dbus-service) #:select (with-retries)) + (gnu services herd) + (ice-9 match) + (rnrs bytevectors) + (rnrs bytevectors gnu)) + + (define sock (socket PF_INET SOCK_DGRAM 0)) + (define addr (make-socket-address AF_INET INADDR_LOOPBACK + ,#$(luanti-configuration-port + config))) + (define probe #vu8(#x4f #x45 #x74 #x03 #x00 #x00 #x00 #x01)) + (define buf (make-bytevector 1000)) + + (with-retries 25 1 + (sendto sock probe addr) + (match (select (list sock) '() '() 2) ;limit time to block + (((sock) _ _) + (match (recvfrom! sock buf) + ((byte-count . _) + (and (>= (pk 'byte-count byte-count) 14) + (pk 'peer-id (bytevector-slice buf 12 2))))))))) + marionette)) + + (test-end)))) + + (gexp->derivation name test)) + +(define %test-luanti + (system-test + (name "luanti") + (description "Connect to a running Luanti server.") + (value (run-luanti-test name (luanti-configuration + (game luanti-mineclonia) + ;; To test some extra code paths. + (mods (list luanti-whitelist)))))))