summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson2@worcester.edu>2021-09-13 08:21:43 -0400
committerDavid Thompson <dthompson2@worcester.edu>2021-09-21 20:17:36 -0400
commitbe3d45520c1caf84c5db22fd40704f0c25cd01cf (patch)
treedc93e7edfa726e7bf243307846487cbdd13358e4
parent5c9b19b378d80ca3d33cb2ad0bd5465239a0c1f6 (diff)
Add a CLI.
-rw-r--r--Makefile.am4
-rw-r--r--README14
-rw-r--r--chickadee/cli.scm96
-rw-r--r--chickadee/cli/play.scm252
-rw-r--r--doc/api.texi3
-rw-r--r--doc/chickadee.texi184
6 files changed, 540 insertions, 13 deletions
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 <dthompson2@worcester.edu>
+;;;
+;;; 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
+;;; <http://www.gnu.org/licenses/>.
+
+(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 <http://gnu.org/licenses/gpl.html>
+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 <davet@gnu.org>
+;;;
+;;; 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
+;;; <http://www.gnu.org/licenses/>.
+
+(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 <game-debugger>
+ (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