M CODEOWNERS => CODEOWNERS +2 -0
@@ 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
M doc/guix.texi => doc/guix.texi +85 -0
@@ 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
M etc/teams.scm => etc/teams.scm +2 -0
@@ 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
M gnu/services/games.scm => gnu/services/games.scm +214 -1
@@ 1,6 1,7 @@
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Arun Isaac <arunisaac@systemreboot.net>
;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2025 Maxim Cournoyer <maxim@guixotic.coop>
;;;
;;; 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))
@@ 73,6 90,202 @@ install udev rules required to use the controller as an unprivileged user.")
;;;
+;;; 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 <luanti-configuration> (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
+<luanti-configuration> object."
+ (match-record config <luanti-configuration>
+ (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 <shepherd-service> object of Luanti."
+ (match-record config <luanti-configuration>
+ ( 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-configuration>
+ (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
;;;
A gnu/tests/games.scm => gnu/tests/games.scm +108 -0
@@ 0,0 1,108 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Maxim Cournoyer <maxim@guixotic.coop>
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(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)))))))