From be3d45520c1caf84c5db22fd40704f0c25cd01cf Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 13 Sep 2021 08:21:43 -0400 Subject: Add a CLI. --- Makefile.am | 4 +- README | 14 +-- chickadee/cli.scm | 96 +++++++++++++++++++ chickadee/cli/play.scm | 252 +++++++++++++++++++++++++++++++++++++++++++++++++ doc/api.texi | 3 + doc/chickadee.texi | 184 ++++++++++++++++++++++++++++++++++++ 6 files changed, 540 insertions(+), 13 deletions(-) create mode 100644 chickadee/cli.scm create mode 100644 chickadee/cli/play.scm diff --git a/Makefile.am b/Makefile.am index f7c959d..330e55d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -93,7 +93,9 @@ SOURCES = \ chickadee/scripting/script.scm \ chickadee/scripting/channel.scm \ chickadee/scripting.scm \ - chickadee.scm + chickadee.scm \ + chickadee/cli.scm \ + chickadee/cli/play.scm TESTS = \ tests/math/vector.scm diff --git a/README b/README index 0886740..dfba2d1 100644 --- a/README +++ b/README @@ -13,20 +13,10 @@ Here's what rendering a sprite looks like: #+BEGIN_SRC scheme - (use-modules (chickadee) - (chickadee math vector) - (chickadee graphics sprite) - (chickadee graphics texture)) - - (define sprite #f) - - (define (load) - (set! sprite (load-image "images/chickadee.png"))) + (define sprite (load-image "images/chickadee.png")) (define (draw alpha) - (draw-sprite sprite #v(256.0 176.0))) - - (run-game #:load load #:draw draw) + (draw-sprite sprite (vec2 256.0 176.0))) #+END_SRC * Features diff --git a/chickadee/cli.scm b/chickadee/cli.scm new file mode 100644 index 0000000..66f2093 --- /dev/null +++ b/chickadee/cli.scm @@ -0,0 +1,96 @@ +;;; Chickadee Game Toolkit +;;; Copyright © 2021 David Thompson +;;; +;;; Chickadee 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. +;;; +;;; Chickadee 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 this program. If not, see +;;; . + +(define-module (chickadee cli) + #:use-module (chickadee) + #:use-module (chickadee config) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-37) + #:export (launch-chickadee + display-version-and-exit + leave + simple-args-fold + operands)) + +(define (display-version-and-exit) + (format #t "Chickadee ~a +Copyright (C) 2021 David Thompson and Chickadee contributors +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the extent permitted by law.~%" + %chickadee-version) + (exit 0)) + +(define (display-help-and-exit) + (format #t "Usage: chickadee SUBCOMMAND ARGS ...~% +Run SUBCOMMAND with ARGS + +Valid subcommands: +* play~%") + (exit 1)) + +(define (leave format-string . args) + "Display error message and exist." + (apply format (current-error-port) format-string args) + (newline) + (exit 1)) + +(define (simple-args-fold args options defaults) + (args-fold args options + (lambda (opt name arg result) + (leave "unrecognized option: ~A" name)) + (lambda (arg result) + (alist-cons 'operand arg result)) + defaults)) + +(define (operands opts) + (filter-map (match-lambda + (('operand . arg) arg) + (_ #f)) + opts)) + +(define (run-chickadee-command command . args) + (define (invalid-command) + (format (current-error-port) "invalid subcommand: ~a~%~%" command) + (display-help-and-exit)) + (let* ((module + (catch 'misc-error + (lambda () + (resolve-interface `(chickadee cli ,command))) + (lambda args + (invalid-command)))) + (proc-name (symbol-append 'chickadee- command)) + (command-proc (false-if-exception (module-ref module proc-name)))) + (if (procedure? command-proc) + (apply command-proc args) + (invalid-command)))) + +(define (subcommand? arg) + (not (string-prefix? "-" arg))) + +(define (launch-chickadee . args) + (match args + ((program (or "--verison" "-v")) + (display-version-and-exit)) + ((or (program) (program (or "--help" "-h"))) + (display-help-and-exit)) + ((program (? subcommand? subcommand) . args*) + (apply run-chickadee-command (string->symbol subcommand) args*)) + ((program invalid-subcommand . args*) + (leave "invalid subcommand: ~A" invalid-subcommand)))) diff --git a/chickadee/cli/play.scm b/chickadee/cli/play.scm new file mode 100644 index 0000000..c893c27 --- /dev/null +++ b/chickadee/cli/play.scm @@ -0,0 +1,252 @@ +;;; Chickadee Game Toolkit +;;; Copyright © 2021 David Thompson +;;; +;;; Chickadee 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. +;;; +;;; Chickadee 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 this program. If not, see +;;; . + +(define-module (chickadee cli play) + #:declarative? #f + #:use-module (chickadee) + #:use-module (chickadee async-repl) + #:use-module (chickadee cli) + #:use-module (chickadee config) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-9) + #:use-module (srfi srfi-37) + #:use-module (system repl command) + #:use-module (system repl debug) + #:use-module (system repl coop-server) + #:use-module (system repl server) + #:export (chickadee-play)) + +(define (display-help-and-exit) + (format #t "Usage: chickadee play [OPTIONS] FILE~% +Play the game defined in FILE.~%") + (display " + --help display this help and exit") + (display " + -t, --title=TITLE set window title to TITLE") + (display " + -w, --width=WIDTH set window width to WIDTH") + (display " + -h, --height=HEIGHT set window height to HEIGHT") + (display " + -f, --fullscreen fullscreen mode") + (display " + -r, --resizable allow window to be resized") + (display " + -u, --update-hz=N set update rate to N times per second") + (display " + --repl start REPL in this terminal") + (display " + --repl-server=[PORT] start REPL server on PORT or 37146 by default") + (newline) + (exit 1)) + +(define %options + (list (option '("help") #f #f + (lambda (opt name arg result) + (display-help-and-exit))) + (option '(#\t "title") #t #f + (lambda (opt name arg result) + (alist-cons 'title arg result))) + (option '(#\w "width") #t #f + (lambda (opt name arg result) + (alist-cons 'width (string->number arg) result))) + (option '(#\h "height") #t #f + (lambda (opt name arg result) + (alist-cons 'height (string->number arg) result))) + (option '(#\f "fullscreen") #f #f + (lambda (opt name arg result) + (alist-cons 'fullscreen? #t result))) + (option '(#\r "resizable") #f #f + (lambda (opt name arg result) + (alist-cons 'resizable? #f result))) + (option '(#\u "update-hz") #t #f + (lambda (opt name arg result) + (alist-cons 'update-hz (string->number arg) result))) + (option '("repl") #f #f + (lambda (opt name arg result) + (alist-cons 'repl #t result))) + (option '("repl-server") #f #t + (lambda (opt name arg result) + (alist-cons 'repl + (if arg + (string->number arg) + 37146) + result))))) + +(define %default-options + '((title . "chickadee") + (width . 640) + (height . 480) + (fullscreen? . #f) + (resizable? . #f) + (update-hz . 60) + (repl . #f))) + +(define (make-user-module) + (let ((module (resolve-module '(chickadee-user) #f))) + (beautify-user-module! module) + (for-each (lambda (name) + (module-use! module (resolve-interface name))) + ;; Automatically load commonly used modules for + ;; maximum convenience. + '((chickadee) + (chickadee graphics color) + (chickadee graphics engine) + (chickadee graphics font) + (chickadee graphics texture) + (chickadee math) + (chickadee math matrix) + (chickadee math rect) + (chickadee math vector) + (chickadee scripting))) + (module-define! module 'quit-game (lambda () (abort-game))) + module)) + +(define-record-type + (make-game-debugger) + game-debugger? + (debug game-debugger-debug set-game-debugger-debug!) + (debugging? game-debugger-debugging? set-game-debugger-debugging!)) + +(define *debugger* (make-game-debugger)) + +(define (launch-game file-name opts) + (let ((module (make-user-module)) + (repl #f) + (debug #f)) + (define-meta-command ((debug-game chickadee) r) + "debug-game +Enter a debugger for the current game loop error." + (if debug + (begin + (set-async-repl-debug! repl debug)) + (begin + (display "nothing to debug") + (newline)))) + (define-meta-command ((resume-game chickadee) r) + "resume-game +Resume the game loop without entering a debugger." + (if debug + (set! debug #f) + (begin + (display "not currently debugging") + (newline)))) + (define-syntax-rule (trampoline name args ...) + (lambda (args ...) + (let ((proc (false-if-exception (module-ref module 'name)))) + (when (procedure? proc) + (proc args ...))))) + (define (load-game) + (save-module-excursion + (lambda () + (let ((dir (dirname file-name))) + (set-current-module module) + (chdir dir) + (add-to-load-path dir) + (primitive-load (basename file-name)) + (let ((repl-opt (assq-ref opts 'repl))) + (cond + ((number? repl-opt) + (set! repl (spawn-coop-repl-server + (make-tcp-server-socket #:port repl-opt)))) + (repl-opt + (set! repl (make-async-repl)) + (start-async-repl repl abort-game)))))))) + (define (handle-error stack key args) + ;; Setup the REPL debug object. + (let* ((tag (and (pair? (fluid-ref %stacks)) + (cdr (fluid-ref %stacks)))) + (stack (narrow-stack->vector + stack + ;; Take the stack from the given frame, cutting 0 + ;; frames. + 0 + ;; Narrow the end of the stack to the most recent + ;; start-stack. + ;;tag + ;; And one more frame, because %start-stack + ;; invoking the start-stack thunk has its own frame + ;; too. + 0 (and tag 1) + )) + (error-string (call-with-output-string + (lambda (port) + (let ((frame (and (< 0 (vector-length stack)) + (vector-ref stack 0)))) + (print-exception port frame key args)))))) + (set! debug (make-debug stack 0 error-string)) + ;; Just update the REPL endlessly until the developer says to + ;; resume. + (let* ((window (current-window)) + (old-title (window-title (current-window)))) + (set-window-title! window + "*** ERROR: Run ,debug-game in REPL for details ***") + (while debug + (update-repl) + (usleep 1000)) + (set-window-title! window old-title)))) + (define (update-repl) + (cond + ((async-repl? repl) + (update-async-repl repl)) + (repl + (poll-coop-repl-server repl)))) + ;; Run game loop, deferring all event handlers to those defined + ;; in the user's Scheme file. + (run-game #:window-title (assq-ref opts 'title) + #:window-width (assq-ref opts 'width) + #:window-height (assq-ref opts 'height) + #:window-fullscreen? (assq-ref opts 'fullscreen?) + #:window-resizable? (assq-ref opts 'resizable?) + #:update-hz (assq-ref opts 'update-hz) + #:load load-game + #:update (let ((update* (trampoline update dt))) + (lambda (dt) + (update-repl) + (update* dt))) + #:draw (trampoline draw alpha) + #:quit (trampoline quit-game) + #:key-press (trampoline key-press key modifiers repeat?) + #:key-release (trampoline key-release key modifiers) + #:text-input (trampoline text-input text) + #:mouse-press (trampoline mouse-press button clicks x y) + #:mouse-release (trampoline mouse-release button x y) + #:mouse-move (trampoline mouse-move x y x-rel y-rel buttons) + #:mouse-wheel (trampoline mouse-wheel x y) + #:controller-add (trampoline controller-add controller) + #:controller-remove (trampoline controller-remove controller) + #:controller-press (trampoline controller-press controller + button) + #:controller-release (trampoline controller-release controller + button) + #:controller-move (trampoline controller-move controller axis + value) + #:error (if repl handle-error #f)) + (when (async-repl? repl) + (close-async-repl repl)))) + +(define (chickadee-play . args) + (let ((opts (simple-args-fold args %options %default-options))) + (match (operands opts) + (() + (leave "you must specify a Scheme file to load")) + ((file-name) + (launch-game file-name opts)) + (_ + (leave "too many arguments specified. just pass a Scheme file name."))))) diff --git a/doc/api.texi b/doc/api.texi index 56df86f..b22fd57 100644 --- a/doc/api.texi +++ b/doc/api.texi @@ -33,6 +33,9 @@ styles of game loops. The appropriately named @code{run-game} and @code{abort-game} procedures are the entry and exit points to the Chickadee game loop. +If you are using @command{chickadee play} to launch your game, then +calling @code{run-game} is already taken care of for you. + @deffn {Procedure} run-game [#:window-title "Chickadee!"] @ [#:window-width 640] [#:window-height 480] @ [#:window-fullscreen? @code{#f}] @ diff --git a/doc/chickadee.texi b/doc/chickadee.texi index db2ebc6..06535c9 100644 --- a/doc/chickadee.texi +++ b/doc/chickadee.texi @@ -51,6 +51,8 @@ The document was typeset with @c Generate the nodes for this menu with `C-c C-u C-m'. @menu * Installation:: Installing Chickadee. +* Getting Started:: Writing your first Chickadee program. +* Command Line Interface:: Run Chickadee programs from the terminal. * API Reference:: Chickadee API reference. * Copying This Manual:: The GNU Free Documentation License and you! @@ -93,6 +95,188 @@ Additionally, Chickadee depends on being able to create an OpenGL 3.3 context at runtime, which means that some older computers may not be able to run games written with Chickadee. +@node Getting Started +@chapter Getting Started + +One of the simplest programs we can make with Chickadee is rendering +the text ``Hello, world'' on screen. Here's what that looks like: + +@example +(define (draw alpha) + (draw-text "Hello, world!" (vec2 64.0 240.0))) +@end example + +The @code{draw} procedure is called frequently to draw the game scene. +For the sake of simplicity, we will ignore the @code{alpha} variable +in this tutorial. + +To run this program, we'll use the @command{chickadee play} command: + +@example +chickadee play hello.scm +@end example + +This is a good start, but it's boring. Let's make the text move! + +@example +(define position (vec2 0.0 240.0)) + +(define (draw alpha) + (draw-text "Hello, world!" position)) + +(define (update dt) + (set-vec2-x! position (+ (vec2-x position) (* 100.0 dt)))) +@end example + +The @code{vec2} type is used to store 2D coordinates +(@pxref{Vectors}.) A variable named @code{position} contains the +position where the text should be rendered. A new hook called +@code{update} has been added to handle the animation. This hook is +called frequently to update the state of the game. The variable +@code{dt} (short for ``delta-time'') contains the amount of time that +has passed since the last update, in seconds. Putting it all +together, this update procedure is incrementing the x coordinate of +the position by 100 pixels per second. + +This is neat, but after a few seconds the text moves off the screen +completely, never to be seen again. It would be better if the text +bounced back and forth against the sides of the window. + +@example +(define position (vec2 0.0 240.0)) + +(define (draw alpha) + (draw-text "Hello, world!" position)) + +(define (update dt) + (update-agenda dt)) + +(define (update-x x) + (set-vec2-x! position x)) + +(let ((start 0.0) + (end 536.0) + (duration 4.0)) + (script + (while #t + (tween duration start end update-x) + (tween duration end start update-x)))) +@end example + +This final example uses Chickadee's scripting features +(@pxref{Scripting}) to bounce the text between the edges of the window +indefinitely using the handy @code{tween} procedure. The only thing +the @code{update} procedure needs to do now is advance the clock of +the ``agenda'' (the thing that runs scripts.) The script takes care +of the rest. + +This quick tutorial has hopefully given you a taste of what you can do +with Chickadee. The rest of this manual gets into all of the details +that were glossed over, and much more. Try rendering a sprite, +playing a sound effect, or handling keyboard input. But most +importantly: Have fun! + +@node Command Line Interface +@chapter Command Line Interface + +While Chickadee is a library at heart, it also comes with a command +line utility to make it easier to get started. + +@menu +* Invoking chickadee play:: Run Chickadee programs +@end menu + +@node Invoking chickadee play +@section Invoking @command{chickadee play} + +The @command{chickadee play} command is used to open a window and run +the Chickadee game contained within a Scheme source file. + +@example +chickadee play the-legend-of-emacs.scm +@end example + +In this file, special procedures may be defined to handle various +events from the game loop: + +@itemize +@item load-game +@item quit-game +@item draw +@item update +@item key-press +@item key-release +@item text-input +@item mouse-press +@item mouse-release +@item mouse-move +@item mouse-wheel +@item controller-add +@item controller-remove +@item controller-press +@item controller-release +@item controller-move +@end itemize + +See @ref{The Game Loop} for complete information on all of these +hooks, such as the arguments that each procedure receives. + +In additional to evaluating the specified source file, the directory +containing that file is added to Guile's load path so that games can +easily be divided into many different files. Furthermore, that +directory is entered prior to evaluating the file so that data files +(images, sounds, etc.) can be loaded relative to the main source file, +regardless of what the current directory was when @command{chickadee +play} was invoked. + +Many aspects of the initial game window and environment can be +controlled via the following options: + +@table @code +@item --title=@var{title} +@itemx -t @var{title} + +Set the window title to @var{title}. + +@item --width=@var{width} +@itemx -w @var{width} + +Set the window width to @var{width} pixels. + +@item --height=@var{height} +@itemx -h @var{height} + +Set the window height to @var{height} pixels. + +@item --fullscreen +@itemx -f + +Open window in fullscreen mode. + +@item --resizable +@itemx -r + +Make window resizable. + +@item --update-hz=@var{n} +@itemx -u @var{n} + +Update the game @var{n} times per second. + +@item --repl + +Launch a REPL in the terminal. This will allow the game environment +to debugged and modified without having to stop and restart the game +after each change. + +@item --repl-server[=@var{port}] + +Launch a REPL server on port @var{port}, or 37146 by default. +Especially useful when paired with the +@url{https://www.nongnu.org/geiser/, Geiser} extension for Emacs. + +@end table + @node API Reference @chapter API Reference -- cgit v1.2.3