summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Thompson <dthompson@vistahigherlearning.com>2022-10-22 20:42:54 -0400
committerDavid Thompson <dthompson@vistahigherlearning.com>2022-10-22 20:55:46 -0400
commit18565edfb75218cee5ad67bd521d33ecc495b6a4 (patch)
tree93c812f97a7910119f13067efe984e4b9609dae5
First commit!
-rw-r--r--.dir-locals.el6
-rw-r--r--.gitignore12
-rw-r--r--COPYING674
-rw-r--r--Makefile.am65
-rwxr-xr-xbootstrap3
-rw-r--r--catbird/asset.scm263
-rw-r--r--catbird/cached-slots.scm109
-rw-r--r--catbird/camera.scm134
-rw-r--r--catbird/config.scm37
-rw-r--r--catbird/inotify.scm218
-rw-r--r--catbird/input-map.scm196
-rw-r--r--catbird/kernel.scm415
-rw-r--r--catbird/line-editor.scm333
-rw-r--r--catbird/minibuffer.scm178
-rw-r--r--catbird/mixins.scm216
-rw-r--r--catbird/mode.scm126
-rw-r--r--catbird/node-2d.scm960
-rw-r--r--catbird/node.scm181
-rw-r--r--catbird/observer.scm58
-rw-r--r--catbird/overlay.scm137
-rw-r--r--catbird/region.scm124
-rw-r--r--catbird/repl.scm371
-rw-r--r--catbird/ring-buffer.scm85
-rw-r--r--catbird/scene.scm169
-rw-r--r--configure.ac18
-rw-r--r--guix.scm180
-rw-r--r--pre-inst-env.in34
-rw-r--r--test-env.in5
-rw-r--r--tests/utils.scm30
29 files changed, 5337 insertions, 0 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..6b30fc9
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,6 @@
+((scheme-mode
+ .
+ ((eval . (put 'run-script 'scheme-indent-function 1))
+ (eval . (put 'test-group 'scheme-indent-function 1))
+ (eval . (put 'with-agenda 'scheme-indent-function 1))
+ (eval . (put 'with-tests 'scheme-indent-function 1)))))
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2891512
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+*.go
+*~
+/Makefile.in
+/aclocal.m4
+/config.log
+/config.status
+/configure
+/pre-inst-env
+/autom4te.cache/
+/build-aux/
+/Makefile
+/test-env
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..94a9ed0
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program 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.
+
+ This program 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..e6f8c2f
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,65 @@
+## Catbird Game Engine
+## Copyright © 2022 David Thompson <davet@gnu.org>
+##
+## Catbird 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.
+##
+## Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+GOBJECTS = $(SOURCES:%.scm=%.go)
+
+nobase_mod_DATA = $(SOURCES) $(NOCOMP_SOURCES)
+nobase_go_DATA = $(GOBJECTS)
+
+# Make sure source files are installed first, so that the mtime of
+# installed compiled files is greater than that of installed source
+# files. See
+# <http://lists.gnu.org/archive/html/guile-devel/2010-07/msg00125.html>
+# for details.
+guile_install_go_files = install-nobase_goDATA
+$(guile_install_go_files): install-nobase_modDATA
+
+CLEANFILES = $(GOBJECTS)
+EXTRA_DIST = $(SOURCES) $(NOCOMP_SOURCES)
+GUILE_WARNINGS = -Wunbound-variable -Warity-mismatch -Wformat
+SUFFIXES = .scm .go
+.scm.go:
+ $(AM_V_GEN)$(top_builddir)/pre-inst-env $(GUILE_TOOLS) compile $(GUILE_WARNINGS) -o "$@" "$<"
+
+moddir=$(prefix)/share/guile/site/$(GUILE_EFFECTIVE_VERSION)
+godir=$(libdir)/guile/$(GUILE_EFFECTIVE_VERSION)/site-ccache
+
+SOURCES = \
+ catbird/config.scm \
+ catbird/inotify.scm \
+ catbird/ring-buffer.scm \
+ catbird/mixins.scm \
+ catbird/cached-slots.scm \
+ catbird/observer.scm \
+ catbird/asset.scm \
+ catbird/input-map.scm \
+ catbird/mode.scm \
+ catbird/camera.scm \
+ catbird/node.scm \
+ catbird/node-2d.scm \
+ catbird/scene.scm \
+ catbird/region.scm \
+ catbird/kernel.scm \
+ catbird/line-editor.scm \
+ catbird/minibuffer.scm \
+ catbird/repl.scm \
+ catbird/overlay.scm
+
+TEST_EXTENSIONS = .scm
+SCM_LOG_COMPILER = $(top_builddir)/test-env $(GUILE)
+
+EXTRA_DIST += \
+ COPYING
diff --git a/bootstrap b/bootstrap
new file mode 100755
index 0000000..e756b42
--- /dev/null
+++ b/bootstrap
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+autoreconf -vif
diff --git a/catbird/asset.scm b/catbird/asset.scm
new file mode 100644
index 0000000..72f46df
--- /dev/null
+++ b/catbird/asset.scm
@@ -0,0 +1,263 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Game data loaded from the file system, such as an image or audio
+;; file.
+;;
+;;; Code:
+(define-module (catbird asset)
+ #:use-module (catbird config)
+ #:use-module (catbird inotify)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:export (<asset>
+ file-names
+ loader
+ artifact
+ subscribers
+ load!
+ ->asset
+ subscribe
+ unsubscribe
+ on-asset-refresh
+ define-asset
+ reload-modified-assets
+
+ <asset-container>))
+
+(define (absolute-file-name file-name)
+ (if (absolute-file-name? file-name)
+ file-name
+ (string-append (getcwd) "/" file-name)))
+
+
+;;;
+;;; Base Asset
+;;;
+
+(define-root-class <asset> ()
+ (file-names #:getter file-names #:init-keyword #:file-names)
+ (loader #:getter loader #:init-keyword #:loader)
+ (artifact #:accessor %artifact #:init-value #f)
+ (subscribers #:getter subscribers #:init-form (make-weak-key-hash-table)))
+
+(define-method (initialize (asset <asset>) initargs)
+ (next-method)
+ ;; Convert relative file names to absolute file names for
+ ;; consistency and ease of use later.
+ (slot-set! asset 'file-names (map absolute-file-name (file-names asset))))
+
+;; Allow any object to be wrapped in an asset.
+(define-method (->asset x)
+ (make <asset>
+ #:file-names '()
+ #:loader (lambda () x)))
+
+(define-method (->asset (asset <asset>))
+ asset)
+
+(define-method (subscribe (asset <asset>) obj context)
+ (let ((subs (subscribers asset)))
+ (hashq-set! subs obj (cons context (hashq-ref subs obj '())))))
+
+(define-method (unsubscribe (asset <asset>) obj context)
+ (let* ((subs (subscribers asset))
+ (contexts (delq context (hashq-ref subs obj '()))))
+ (if (null? contexts)
+ (hashq-remove! subs obj)
+ (hashq-set! subs obj contexts))))
+
+(define-method (on-asset-refresh obj context)
+ #t)
+
+(define-method (notify-refresh (asset <asset>))
+ (hash-for-each (lambda (subscriber contexts)
+ (for-each (lambda (context)
+ (on-asset-refresh subscriber context))
+ contexts))
+ (subscribers asset)))
+
+(define-method (load! (asset <asset>))
+ (let ((value (apply (loader asset) (file-names asset))))
+ (set! (%artifact asset) value)
+ (notify-refresh asset)
+ value))
+
+(define-method (reload! (asset <asset>))
+ (load! asset))
+
+(define-method (unload! (asset <asset>))
+ (set! (%artifact asset) #f))
+
+(define-method (artifact (asset <asset>))
+ (or (%artifact asset)
+ (load! asset)))
+
+
+;;;
+;;; Auto-reloading Asset
+;;;
+
+(define-class <auto-reload-asset> (<asset>)
+ ;; Do not create inotify handle until it is needed.
+ (inotify #:allocation #:class #:init-form (delay (make-inotify)))
+ ;; List of all auto-reloadable assets stored as a weak key hash
+ ;; table
+ (assets #:allocation #:class #:init-thunk make-weak-key-hash-table))
+
+(define (asset-inotify)
+ (force (class-slot-ref <auto-reload-asset> 'inotify)))
+
+(define (auto-reload-assets)
+ (class-slot-ref <auto-reload-asset> 'assets))
+
+(define (register-auto-reload-asset! asset)
+ (hashq-set! (auto-reload-assets) asset #t))
+
+(define-method (load! (asset <auto-reload-asset>))
+ ;; These are both no-ops if the asset and file are already being
+ ;; watched.
+ (register-auto-reload-asset! asset)
+ (for-each (lambda (file-name)
+ (inotify-add-watch! (asset-inotify) file-name '(close-write)))
+ (file-names asset))
+ (next-method))
+
+(define (assets-for-event event)
+ (let ((f (inotify-watch-file-name (inotify-event-watch event))))
+ (hash-fold (lambda (asset dummy-value memo)
+ (if (member f (file-names asset))
+ (cons asset memo)
+ memo))
+ '()
+ (auto-reload-assets))))
+
+;; Needs to be called periodically in the game loop to reload modified
+;; assets.
+(define (reload-modified-assets)
+ "Reload all assets whose files have been modified."
+ (let ((inotify (asset-inotify)))
+ (while (inotify-pending-events? inotify)
+ (let* ((event (inotify-read-event inotify))
+ (assets (assets-for-event event)))
+ (if (null? assets)
+ ;; There are no assets associated with this file anymore
+ ;; (they've been redefined with new file names or GC'd),
+ ;; so remove the watch.
+ (inotify-watch-remove! (inotify-event-watch event))
+ ;; Reload all assets associated with the file.
+ (for-each reload! assets))))))
+
+
+;;;
+;;; Syntax
+;;;
+
+(define-syntax-rule (define-asset name ((var file-name) ...) body ...)
+ (define name
+ (let ((file-names (list file-name ...))
+ (proc (lambda (var ...) body ...)))
+ (if (and (defined? 'name) (is-a? name <asset>))
+ (begin
+ (initialize name
+ #:file-names file-names
+ #:loader proc)
+ name)
+ (make (if developer-mode? <auto-reload-asset> <asset>)
+ #:file-names file-names
+ #:loader proc)))))
+
+
+;;;
+;;; Asset Metaclass
+;;;
+
+(define-class <asset-slot-class> (<catbird-metaclass>))
+
+(define-method (asset-slot? (slot <slot>))
+ (get-keyword #:asset? (slot-definition-options slot)))
+
+(define (slot-ref* obj slot-name)
+ (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name)))
+
+(define-method (compute-getter-method (class <asset-slot-class>) slot)
+ (if (asset-slot? slot)
+ ;; Wrap the original getter procedure with a new procedure that
+ ;; extracts the current value from the asset object.
+ (make <method>
+ #:specializers (list class)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj)
+ (artifact (proc obj)))))
+ (next-method)))
+
+(define-method (compute-setter-method (class <asset-slot-class>) slot)
+ (if (asset-slot? slot)
+ ;; Wrap the original setter procedure with a new procedure that
+ ;; manages asset update notifications.
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj new)
+ (let ((old (slot-ref* obj slot-name))
+ (new* (->asset new)))
+ (unless (eq? old new)
+ (when old
+ (unsubscribe old obj slot-name))
+ (subscribe new* obj slot-name)
+ (proc obj new*))))))
+ (next-method)))
+
+(define (map-initargs proc initargs)
+ (let loop ((initargs initargs))
+ (match initargs
+ (() '())
+ ((slot-name value . rest)
+ (cons* slot-name (proc slot-name value) (loop rest))))))
+
+(define (for-each-initarg proc initargs)
+ (let loop ((initargs initargs))
+ (match initargs
+ (() '())
+ ((slot-name value . rest)
+ (proc slot-name value)
+ (loop rest)))))
+
+(define (coerce-asset obj slot-name)
+ (let ((value (slot-ref* obj slot-name)))
+ (if (is-a? value <asset>)
+ value
+ (let ((asset (->asset value)))
+ (slot-set! obj slot-name asset)
+ asset))))
+
+(define-class <asset-container> ()
+ #:metaclass <asset-slot-class>)
+
+(define-method (initialize (instance <asset-container>) initargs)
+ (next-method)
+ ;; Subscribe for updates to all asset slots.
+ (for-each (lambda (slot)
+ (when (asset-slot? slot)
+ (let* ((slot-name (slot-definition-name slot))
+ (value (coerce-asset instance slot-name)))
+ (subscribe value instance slot-name))))
+ (class-slots (class-of instance))))
diff --git a/catbird/cached-slots.scm b/catbird/cached-slots.scm
new file mode 100644
index 0000000..22ab00d
--- /dev/null
+++ b/catbird/cached-slots.scm
@@ -0,0 +1,109 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Slots whose values can be lazily recomputed.
+;;
+;;; Code:
+(define-module (catbird cached-slots)
+ #:use-module (catbird config)
+ #:use-module (oop goops)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-9)
+ #:export (<cacheable>
+ slot-expired?
+ expire-slot!))
+
+(define-record-type <cached-value>
+ (%make-cached-value value expired? proc)
+ cached-value?
+ (value %cached-value-ref set-cached-value!)
+ (expired? cached-value-expired? set-cached-value-expired!)
+ (proc cached-value-proc))
+
+(define (make-cached-value init proc)
+ (%make-cached-value init #t proc))
+
+(define (refresh-cached-value! cache)
+ (let ((x ((cached-value-proc cache) (%cached-value-ref cache))))
+ (set-cached-value! cache x)
+ (set-cached-value-expired! cache #f)))
+
+(define (cached-value-ref cache)
+ (when (cached-value-expired? cache)
+ (refresh-cached-value! cache))
+ (%cached-value-ref cache))
+
+(define (expire-cached-value! cache)
+ (set-cached-value-expired! cache #t))
+
+(define (expire-slot! obj slot-name)
+ (expire-cached-value! (slot-ref obj slot-name)))
+
+(define (slot-expired? obj slot-name)
+ (cached-value-expired? (slot-ref obj slot-name)))
+
+(define-class <cached-slot-class> (<catbird-metaclass>))
+
+(define (slot-ref* obj slot-name)
+ (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name)))
+
+(define-method (cached-slot? (slot <slot>))
+ (get-keyword #:cached? (slot-definition-options slot)))
+
+(define-method (slot-refresh-proc (slot <slot>))
+ (get-keyword #:refresh (slot-definition-options slot)))
+
+(define-method (compute-getter-method (class <cached-slot-class>) slot)
+ (if (cached-slot? slot)
+ ;; Wrap the original getter procedure with a new procedure that
+ ;; extracts the current value from the cached value, recomputing
+ ;; it if necessary.
+ (make <method>
+ #:specializers (list class)
+ #:procedure (let ((proc (method-procedure (next-method))))
+ (lambda (obj)
+ (cached-value-ref (proc obj)))))
+ (next-method)))
+
+(define-method (compute-setter-method (class <cached-slot-class>) slot)
+ (if (cached-slot? slot)
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (lambda (obj new)
+ (raise-exception
+ (make-exception-with-message "cached slots cannot be set"))))
+ (next-method)))
+
+(define-class <cacheable> ()
+ #:metaclass <cached-slot-class>)
+
+(define-method (initialize (instance <cacheable>) initargs)
+ (next-method)
+ ;; Setup cached values.
+ (for-each (lambda (slot)
+ (when (cached-slot? slot)
+ (let* ((slot-name (slot-definition-name slot))
+ (refresh-proc (slot-refresh-proc slot))
+ (cached-value (make-cached-value
+ (slot-ref* instance slot-name)
+ (lambda (prev)
+ (refresh-proc instance prev)))))
+ (slot-set! instance slot-name cached-value))))
+ (class-slots (class-of instance))))
diff --git a/catbird/camera.scm b/catbird/camera.scm
new file mode 100644
index 0000000..525843f
--- /dev/null
+++ b/catbird/camera.scm
@@ -0,0 +1,134 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Views into a scene.
+;;
+;;; Code:
+(define-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (chickadee math)
+ #:use-module (chickadee math matrix)
+ #:use-module (chickadee math rect)
+ #:use-module (chickadee math vector)
+ #:use-module (oop goops)
+ #:export (<camera>
+ projection-matrix
+ view-matrix
+ width
+ height
+ current-camera
+
+ <camera-2d>
+ view-bounding-box
+ move-to
+ move-by
+
+ <camera-3d>
+ field-of-vision
+ near-clip
+ far-clip
+ direction
+ up))
+
+(define-root-class <camera> ()
+ (width #:accessor width #:init-keyword #:width)
+ (height #:accessor height #:init-keyword #:height)
+ (projection-matrix #:getter projection-matrix #:init-thunk make-identity-matrix4)
+ (view-matrix #:getter view-matrix #:init-thunk make-identity-matrix4))
+
+(define-generic refresh-projection)
+(define-generic refresh-view)
+
+(define-method (initialize (camera <camera>) args)
+ (next-method)
+ (refresh-projection camera)
+ (refresh-view camera))
+
+(define current-camera (make-parameter #f))
+
+
+;;;
+;;; 2D Camera
+;;;
+
+(define-class <camera-2d> (<camera> <movable-2d>)
+ (view-bounding-box #:accessor view-bounding-box #:init-thunk make-null-rect))
+
+(define-method (initialize (camera <camera-2d>) initargs)
+ (next-method)
+ (let ((bb (view-bounding-box camera)))
+ (set-rect-width! bb (width camera))
+ (set-rect-height! bb (height camera))))
+
+(define-method (refresh-projection (camera <camera-2d>))
+ (orthographic-projection! (projection-matrix camera)
+ 0.0 (width camera)
+ (height camera) 0.0
+ 0.0 1.0))
+
+(define-method (refresh-view (camera <camera-2d>))
+ (let ((p (position camera))
+ (bb (view-bounding-box camera)))
+ (matrix4-translate! (view-matrix camera) p)
+ (set-rect-x! bb (vec2-x p))
+ (set-rect-y! bb (vec2-y p))))
+
+(define-method (move-to (camera <camera-2d>) p)
+ (vec2-copy! p (position camera))
+ (refresh-view camera))
+
+(define-method (move-by (camera <camera-2d>) d)
+ (vec2-add! (position camera) d)
+ (refresh-view camera))
+
+
+;;;
+;;; 3D Camera
+;;;
+
+(define-class <camera-3d> (<camera> <movable-3d>)
+ (field-of-vision #:getter field-of-vision #:init-keyword #:field-of-vision
+ #:init-value (degrees->radians 60))
+ (near-clip #:getter near-clip #:init-keyword #:near-clip #:init-value 0.1)
+ (far-clip #:getter far-clip #:init-keyword #:far-clip #:init-value 5.0)
+ (direction #:getter direction #:init-keyword #:direction
+ #:init-form (vec3 0.0 0.0 -1.0))
+ (up #:getter up #:init-keyword #:up
+ #:init-form (vec3 0.0 1.0 0.0)))
+
+(define-method (refresh-projection (camera <camera-3d>))
+ (perspective-projection! (projection-matrix camera)
+ (field-of-vision camera)
+ (/ (width camera) (height camera))
+ (near-clip camera)
+ (far-clip camera)))
+
+(define-method (refresh-view (camera <camera-3d>))
+ (look-at! (view-matrix camera)
+ (position camera)
+ (direction camera)
+ (up camera)))
+
+(define-method (move-to (camera <camera-3d>) p)
+ (vec3-copy! p (position camera))
+ (refresh-view camera))
+
+(define-method (move-by (camera <camera-3d>) d)
+ (vec3-add! (position camera) d)
+ (refresh-view camera))
diff --git a/catbird/config.scm b/catbird/config.scm
new file mode 100644
index 0000000..4a57c52
--- /dev/null
+++ b/catbird/config.scm
@@ -0,0 +1,37 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Engine-wide global configuration.
+;;
+;;; Code:
+(define-module (catbird config)
+ #:use-module (oop goops)
+ #:export (developer-mode?
+ <catbird-metaclass>
+ define-root-class))
+
+(define developer-mode?
+ (equal? (getenv "CATBIRD_DEV_MODE") "1"))
+
+(define <catbird-metaclass>
+ (if developer-mode? <redefinable-class> <class>))
+
+(define-syntax-rule (define-root-class name (supers ...) args ...)
+ (define-class name (supers ...)
+ args ...
+ #:metaclass <catbird-metaclass>))
diff --git a/catbird/inotify.scm b/catbird/inotify.scm
new file mode 100644
index 0000000..fc170f5
--- /dev/null
+++ b/catbird/inotify.scm
@@ -0,0 +1,218 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Linux inotify bindings.
+;;
+;;; Code:
+(define-module (catbird inotify)
+ #:use-module (ice-9 binary-ports)
+ #:use-module (ice-9 format)
+ #:use-module (ice-9 match)
+ #:use-module (rnrs bytevectors)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-9 gnu)
+ #:use-module (system foreign)
+ #:export (make-inotify
+ inotify?
+ inotify-watches
+ inotify-add-watch!
+ inotify-pending-events?
+ inotify-read-event
+ inotify-watch?
+ inotify-watch-id
+ inotify-watch-file-name
+ inotify-watch-remove!
+ inotify-event?
+ inotify-event-watch
+ inotify-event-type
+ inotify-event-cookie
+ inotify-event-file-name))
+
+(define libc (dynamic-link))
+
+(define inotify-init
+ (pointer->procedure int (dynamic-func "inotify_init" libc) '()))
+
+(define inotify-add-watch
+ (pointer->procedure int (dynamic-func "inotify_add_watch" libc)
+ (list int '* uint32)))
+
+(define inotify-rm-watch
+ (pointer->procedure int (dynamic-func "inotify_rm_watch" libc)
+ (list int int)))
+
+(define IN_ACCESS #x00000001) ; file was accessed.
+(define IN_MODIFY #x00000002) ; file was modified.
+(define IN_ATTRIB #x00000004) ; metadata changed
+(define IN_CLOSE_WRITE #x00000008) ; file opened for writing closed
+(define IN_CLOSE_NOWRITE #x00000010) ; file not opened for writing closed
+(define IN_OPEN #x00000020) ; file was opened
+(define IN_MOVED_FROM #x00000040) ; file was moved from X
+(define IN_MOVED_TO #x00000080) ; file was moved to Y
+(define IN_CREATE #x00000100) ; subfile was created
+(define IN_DELETE #x00000200) ; subfile was deleted
+(define IN_DELETE_SELF #x00000400) ; self was deleted
+(define IN_MOVE_SELF #x00000800) ; self was moved
+;; Kernel flags
+(define IN_UNMOUNT #x00002000) ; backing fs was unmounted
+(define IN_Q_OVERFLOW #x00004000) ; event queue overflowed
+(define IN_IGNORED #x00008000) ; file was ignored
+;; Special flags
+(define IN_ONLYDIR #x01000000) ; only watch if directory
+(define IN_DONT_FOLLOW #x02000000) ; do not follow symlink
+(define IN_EXCL_UNLINK #x04000000) ; exclude events on unlinked objects
+(define IN_MASK_ADD #x20000000) ; add to the mask of an existing watch
+(define IN_ISDIR #x40000000) ; event occurred against directory
+(define IN_ONESHOT #x80000000) ; only send event once
+
+(define mask/symbol (make-hash-table))
+(define symbol/mask (make-hash-table))
+
+(for-each (match-lambda
+ ((sym mask)
+ (hashq-set! symbol/mask sym mask)
+ (hashv-set! mask/symbol mask sym)))
+ `((access ,IN_ACCESS)
+ (modify ,IN_MODIFY)
+ (attrib ,IN_ATTRIB)
+ (close-write ,IN_CLOSE_WRITE)
+ (close-no-write ,IN_CLOSE_NOWRITE)
+ (open ,IN_OPEN)
+ (moved-from ,IN_MOVED_FROM)
+ (moved-to ,IN_MOVED_TO)
+ (create ,IN_CREATE)
+ (delete ,IN_DELETE)
+ (delete-self ,IN_DELETE_SELF)
+ (move-self ,IN_MOVE_SELF)
+ (only-dir ,IN_ONLYDIR)
+ (dont-follow ,IN_DONT_FOLLOW)
+ (exclude-unlink ,IN_EXCL_UNLINK)
+ (is-directory ,IN_ISDIR)
+ (once ,IN_ONESHOT)))
+
+(define (symbol->mask sym)
+ (hashq-ref symbol/mask sym))
+
+(define (mask->symbol sym)
+ (hashq-ref mask/symbol sym))
+
+(define-record-type <inotify>
+ (%make-inotify port buffer buffer-pointer watches)
+ inotify?
+ (port inotify-port)
+ (buffer inotify-buffer)
+ (buffer-pointer inotify-buffer-pointer)
+ (watches inotify-watches))
+
+(define-record-type <inotify-watch>
+ (make-inotify-watch id file-name owner)
+ inotify-watch?
+ (id inotify-watch-id)
+ (file-name inotify-watch-file-name)
+ (owner inotify-watch-owner))
+
+(define-record-type <inotify-event>
+ (make-inotify-event watch type cookie file-name)
+ inotify-event?
+ (watch inotify-event-watch)
+ (type inotify-event-type)
+ (cookie inotify-event-cookie)
+ (file-name inotify-event-file-name))
+
+(define (display-inotify inotify port)
+ (format port "#<inotify port: ~a>" (inotify-port inotify)))
+
+(define (display-inotify-watch watch port)
+ (format port "#<inotify-watch id: ~d file-name: ~a>"
+ (inotify-watch-id watch)
+ (inotify-watch-file-name watch)))
+
+(define (display-inotify-event event port)
+ (format port "#<inotify-event type: ~s cookie: ~d file-name: ~a watch: ~a>"
+ (inotify-event-type event)
+ (inotify-event-cookie event)
+ (inotify-event-file-name event)
+ (inotify-event-watch event)))
+
+(set-record-type-printer! <inotify> display-inotify)
+(set-record-type-printer! <inotify-watch> display-inotify-watch)
+(set-record-type-printer! <inotify-event> display-inotify-event)
+
+(define (make-inotify)
+ (let ((fd (inotify-init))
+ (buffer (make-bytevector 4096)))
+ (%make-inotify (fdopen fd "r")
+ buffer
+ (bytevector->pointer buffer)
+ (make-hash-table))))
+
+(define (inotify-fd inotify)
+ (port->fdes (inotify-port inotify)))
+
+(define (absolute-file-name file-name)
+ (if (absolute-file-name? file-name)
+ file-name
+ (string-append (getcwd) "/" file-name)))
+
+(define (inotify-add-watch! inotify file-name modes)
+ (let* ((watches (inotify-watches inotify))
+ (abs-file-name (absolute-file-name file-name))
+ (wd (inotify-add-watch (inotify-fd inotify)
+ (string->pointer abs-file-name)
+ (apply logior (map symbol->mask modes)))))
+ (or (hashv-ref watches wd)
+ (let ((new-watch (make-inotify-watch wd abs-file-name inotify)))
+ (hashv-set! watches wd new-watch)
+ new-watch))))
+
+(define (inotify-watch-remove! watch)
+ (inotify-rm-watch (inotify-fd (inotify-watch-owner watch))
+ (inotify-watch-id watch))
+ (hashv-remove! (inotify-watches (inotify-watch-owner watch))
+ (inotify-watch-id watch)))
+
+(define (inotify-pending-events? inotify)
+ ;; Sometimes an interrupt happens during the char-ready? call and an
+ ;; exception is thrown. Just return #f in that case and move on
+ ;; with life.
+ (false-if-exception (char-ready? (inotify-port inotify))))
+
+(define (read-int port buffer)
+ (get-bytevector-n! port buffer 0 (sizeof int))
+ (bytevector-sint-ref buffer 0 (native-endianness) (sizeof int)))
+
+(define (read-uint32 port buffer)
+ (get-bytevector-n! port buffer 0 (sizeof uint32))
+ (bytevector-uint-ref buffer 0 (native-endianness) (sizeof uint32)))
+
+(define (read-string port buffer buffer-pointer length)
+ (and (> length 0)
+ (begin
+ (get-bytevector-n! port buffer 0 length)
+ (pointer->string buffer-pointer))))
+
+(define (inotify-read-event inotify)
+ (let* ((port (inotify-port inotify))
+ (buffer (inotify-buffer inotify))
+ (wd (read-int port buffer))
+ (event-mask (read-uint32 port buffer))
+ (cookie (read-uint32 port buffer))
+ (len (read-uint32 port buffer))
+ (name (read-string port buffer (inotify-buffer-pointer inotify) len)))
+ (make-inotify-event (hashv-ref (inotify-watches inotify) wd)
+ (mask->symbol event-mask) cookie name)))
diff --git a/catbird/input-map.scm b/catbird/input-map.scm
new file mode 100644
index 0000000..4fdf62c
--- /dev/null
+++ b/catbird/input-map.scm
@@ -0,0 +1,196 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Keyboard, mouse, controller input specification.
+;;
+;;; Code:
+(define-module (catbird input-map)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-1)
+ #:export (make-input-map
+ add-input
+ remove-input
+ key-press
+ key-release
+ text-input
+ mouse-press
+ mouse-release
+ mouse-move
+ mouse-wheel
+ controller-press
+ controller-release
+ controller-move
+ key-press-handler
+ key-release-handler
+ text-input-handler
+ mouse-press-handler
+ mouse-release-handler
+ mouse-move-handler
+ mouse-wheel-handler
+ controller-press-handler
+ controller-release-handler
+ controller-move-handler))
+
+(define (make-input-map)
+ '())
+
+(define (add-input input-map spec handler)
+ (cons (cons spec handler)
+ input-map))
+
+(define (remove-input input-map spec)
+ (delete spec input-map
+ (match-lambda
+ ((s . _) (equal? s spec)))))
+
+(define* (key-press key #:optional (modifiers '()))
+ `(keyboard ,key ,modifiers down))
+
+(define* (key-release key #:optional (modifiers '()))
+ `(keyboard ,key ,modifiers up))
+
+(define (text-input)
+ '(text-input))
+
+(define (mouse-press button)
+ `(mouse button ,button down))
+
+(define (mouse-release button)
+ `(mouse button ,button up))
+
+(define* (mouse-move #:optional (buttons '()))
+ `(mouse move ,buttons))
+
+(define* (mouse-wheel)
+ '(mouse wheel))
+
+(define (controller-press id button)
+ `(controller button ,id ,button down))
+
+(define (controller-release id button)
+ `(controller button ,id ,button up))
+
+(define (controller-move id axis)
+ `(controller axis ,id ,axis))
+
+;; Chickadee is specific about which modifier keys are pressed and
+;; makes distinctions between left and right ctrl, for example. For
+;; convenience, we want users to be able to specify simply 'ctrl' and
+;; it will match both left and right.
+(define (modifiers-match? spec-modifiers modifiers)
+ (every (lambda (k)
+ (case k
+ ;; The specification is looking for a specific modifier
+ ;; key.
+ ((left-ctrl right-ctrl left-alt right-alt left-shift right-shift)
+ (memq k modifiers))
+ ;; The specification is looking for either left/right
+ ;; modifier key.
+ ((ctrl)
+ (or (memq 'left-control modifiers)
+ (memq 'right-control modifiers)))
+ ((alt)
+ (or (memq 'left-alt modifiers)
+ (memq 'right-alt modifiers)))
+ ((shift)
+ (or (memq 'left-shift modifiers)
+ (memq 'right-shift modifiers)))))
+ spec-modifiers))
+
+(define (key-press-handler input-map key modifiers)
+ (any (match-lambda
+ ((('keyboard key* modifiers* 'down) . handler)
+ (and (eq? key key*)
+ (modifiers-match? modifiers* modifiers)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (key-release-handler input-map key modifiers)
+ (any (match-lambda
+ ((('keyboard key* modifiers* 'up) . handler)
+ (and (eq? key key*)
+ (modifiers-match? modifiers modifiers*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (text-input-handler input-map)
+ (any (match-lambda
+ ((('text-input) . handler) handler)
+ (_ #f))
+ input-map))
+
+(define (mouse-press-handler input-map button)
+ (any (match-lambda
+ ((('mouse 'button button* 'down) . handler)
+ (and (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-release-handler input-map button)
+ (any (match-lambda
+ ((('mouse 'button button* 'up) . handler)
+ (and (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-move-handler input-map buttons)
+ (any (match-lambda
+ ((('mouse 'move buttons*) . handler)
+ (and (= (length buttons) (length buttons*))
+ (every (lambda (b) (memq b buttons*)) buttons)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (mouse-wheel-handler input-map)
+ (any (match-lambda
+ ((('mouse 'wheel) . handler)
+ handler)
+ (_ #f))
+ input-map))
+
+(define (controller-press-handler input-map controller-id button)
+ (any (match-lambda
+ ((('controller 'button controller-id* button* 'down) . handler)
+ (and (= controller-id controller-id*)
+ (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (controller-release-handler input-map controller-id button)
+ (any (match-lambda
+ ((('controller 'button controller-id* button* 'up) . handler)
+ (and (= controller-id controller-id*)
+ (eq? button button*)
+ handler))
+ (_ #f))
+ input-map))
+
+(define (controller-move-handler input-map controller-id axis)
+ (any (match-lambda
+ ((('controller 'axis controller-id* axis*) . handler)
+ (and (= controller-id controller-id*)
+ (eq? axis axis*)
+ handler))
+ (_ #f))
+ input-map))
diff --git a/catbird/kernel.scm b/catbird/kernel.scm
new file mode 100644
index 0000000..70d3afb
--- /dev/null
+++ b/catbird/kernel.scm
@@ -0,0 +1,415 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Manager of the Catbird game engine.
+;;
+;;; Code:
+(define-module (catbird kernel)
+ #:use-module (catbird asset)
+ #:use-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird input-map)
+ #:use-module (catbird mixins)
+ #:use-module (catbird mode)
+ #:use-module (catbird region)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee data array-list)
+ #:use-module (chickadee math rect)
+ #:use-module (ice-9 atomic)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:use-module (system repl coop-server)
+ #:export (all-regions
+ create-full-region
+ create-region
+ find-region-by-name
+ frames-per-second
+ kill-region
+ current-keyboard-focus
+ take-keyboard-focus
+ current-controller-focus
+ take-controller-focus
+ bind-input/global
+ unbind-input/global
+ run-catbird
+ exit-catbird))
+
+
+;;;
+;;; Kernel
+;;;
+
+(define-root-class <kernel> ()
+ (controllers #:getter controllers #:init-thunk make-array-list)
+ (regions #:accessor regions #:init-value '())
+ (input-map #:accessor input-map #:init-thunk make-input-map)
+ (keyboard-focus #:accessor keyboard-focus #:init-value #f)
+ (controller-focus #:getter controller-focus #:init-thunk make-hash-table)
+ (repl #:accessor repl #:init-value #f)
+ (frame-start-time #:accessor frame-start-time #:init-value 0.0)
+ (average-frame-time #:accessor average-frame-time #:init-value 0.0))
+
+(define-method (load* (kernel <kernel>))
+ (when developer-mode?
+ (set! (repl kernel) (spawn-coop-repl-server))))
+
+;; Add the system notification and debugging overlay.
+(define-method (add-overlay (kernel <kernel>))
+ (let ((region (create-full-region #:name 'overlay #:rank 9999)))
+ (set! (camera region)
+ (make <camera-2d>
+ #:width (rect-width (area region))
+ #:height (rect-height (area region))))
+ ;; Use resolve-module to avoid a circular dependency.
+ (replace-scene region
+ ((module-ref (resolve-module '(catbird overlay))
+ 'make-overlay)))))
+
+(define-method (overlay-scene (kernel <kernel>))
+ (scene (lookup-region kernel 'overlay)))
+
+(define-method (notify (kernel <kernel>) message)
+ (let ((notify (module-ref (resolve-module '(catbird overlay)) 'notify)))
+ (notify (overlay-scene kernel) message)))
+
+(define-method (update (kernel <kernel>) dt)
+ (when developer-mode?
+ (poll-coop-repl-server (repl kernel))
+ (reload-modified-assets))
+ (for-each (lambda (region) (update/around region dt))
+ (regions kernel)))
+
+(define-method (render (kernel <kernel>) alpha)
+ (for-each (lambda (region)
+ (render/around region alpha))
+ (regions kernel))
+ ;; Compute FPS.
+ (let ((current-time (elapsed-time)))
+ (set! (average-frame-time kernel)
+ (+ (* (- current-time (frame-start-time kernel)) 0.1)
+ (* (average-frame-time kernel) 0.9)))
+ (set! (frame-start-time kernel) current-time)))
+
+(define-method (lookup-region (kernel <kernel>) region-name)
+ (find (lambda (region)
+ (eq? (name region) region-name))
+ (regions kernel)))
+
+(define-method (add-region (kernel <kernel>) (region <region>))
+ (let ((r (regions kernel)))
+ ;; The first region added gets keyboard focus by default.
+ (when (null? r)
+ (set! (keyboard-focus kernel) region))
+ (set! (regions kernel)
+ (sort-by-rank/ascending (cons region (regions kernel))))))
+
+(define-method (bind-input (kernel <kernel>) spec handler)
+ (set! (input-map kernel) (add-input (input-map kernel) spec handler)))
+
+(define-method (unbind-input (kernel <kernel>) spec)
+ (set! (input-map kernel) (remove-input (input-map kernel) spec)))
+
+
+;;;
+;;; Keyboard
+;;;
+
+(define-method (on-key-press (kernel <kernel>) key modifiers)
+ (or (let ((handler (key-press-handler (input-map kernel) key modifiers)))
+ (and handler (handler)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-key-press s key modifiers)))))
+
+(define-method (on-key-release (kernel <kernel>) key modifiers)
+ (or (let ((handler (key-release-handler (input-map kernel) key modifiers)))
+ (and handler (handler)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-key-release s key modifiers)))))
+
+(define-method (on-text-input (kernel <kernel>) text)
+ (or (let ((handler (text-input-handler (input-map kernel))))
+ (and handler (handler text)))
+ (let* ((r (keyboard-focus kernel))
+ (s (and r (scene r))))
+ (and s (on-text-input s text)))))
+
+
+;;;
+;;; Mouse
+;;;
+
+(define (mouse-search kernel proc)
+ (let loop ((regions* (regions kernel)))
+ (match regions*
+ (() #f)
+ ((r . rest)
+ (or (loop rest)
+ (let ((s (scene r)))
+ (and s (proc s))))))))
+
+(define-method (on-mouse-press (kernel <kernel>) button x y)
+ (or (let ((handler (mouse-press-handler (input-map kernel) button)))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-press s button x y)))))
+
+(define-method (on-mouse-release (kernel <kernel>) button x y)
+ (or (let ((handler (mouse-release-handler (input-map kernel) button)))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-release s button x y)))))
+
+(define-method (on-mouse-move (kernel <kernel>) x y x-rel y-rel buttons)
+ (or (let ((handler (mouse-move-handler (input-map kernel) buttons)))
+ (and handler (handler x y x-rel y-rel)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-move s x y x-rel y-rel buttons)))))
+
+(define-method (on-mouse-wheel (kernel <kernel>) x y)
+ (or (let ((handler (mouse-wheel-handler (input-map kernel))))
+ (and handler (handler x y)))
+ (mouse-search kernel
+ (lambda (s)
+ (on-mouse-wheel s x y)))))
+
+
+;;;
+;;; Controllers
+;;;
+
+(define-method (controller-focus (kernel <kernel>) slot)
+ (hashv-ref (controller-focus kernel) (controller-slot-id slot)))
+
+(define (make-controller-slot id)
+ (vector id #f))
+
+(define (controller-slot-id slot)
+ (vector-ref slot 0))
+
+(define (controller-slot-controller slot)
+ (vector-ref slot 1))
+
+(define (controller-slot-empty? slot)
+ (not (controller-slot-controller slot)))
+
+(define (fill-controller-slot! slot controller)
+ (vector-set! slot 1 controller))
+
+(define (clear-controller-slot! slot)
+ (fill-controller-slot! slot #f))
+
+(define-method (empty-controller-slot (kernel <kernel>))
+ (let* ((c (controllers kernel))
+ (n (array-list-size c)))
+ (let loop ((i 0))
+ (if (= i n)
+ (let ((slot (make-controller-slot i)))
+ (array-list-push! c slot)
+ slot)
+ (let ((slot (array-list-ref c i)))
+ (if (controller-slot-empty? slot)
+ slot
+ (loop (+ i 1))))))))
+
+(define-method (find-controller-slot (kernel <kernel>) controller)
+ (let* ((c (controllers kernel))
+ (n (array-list-size c)))
+ (let loop ((i 0))
+ (if (= i n)
+ #f
+ (let ((slot (array-list-ref c i)))
+ (if (eq? (controller-slot-controller slot) controller)
+ slot
+ (loop (+ i 1))))))))
+
+(define-method (on-controller-add (kernel <kernel>) controller)
+ (let ((slot (empty-controller-slot kernel)))
+ (notify kernel (string-append "Controller "
+ (number->string
+ (+ (controller-slot-id slot) 1))
+ " connected: "
+ (controller-name controller)))
+ (fill-controller-slot! slot controller)))
+
+(define-method (on-controller-remove (kernel <kernel>) controller)
+ (let ((slot (find-controller-slot kernel controller)))
+ (notify kernel (string-append "Controller "
+ (number->string
+ (+ (controller-slot-id slot) 1))
+ " disconnected: "
+ (controller-name controller)))
+ (clear-controller-slot! (find-controller-slot kernel controller))))
+
+(define-method (on-controller-press (kernel <kernel>) controller button)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-press-handler (input-map kernel)
+ (controller-slot-id slot)
+ button)))
+ (and handler (handler)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and r (on-controller-press s
+ (controller-slot-id slot)
+ button))))))
+
+(define-method (on-controller-release (kernel <kernel>) controller button)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-release-handler (input-map kernel)
+ (controller-slot-id slot)
+ button)))
+ (and handler (handler)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and s (on-controller-release s
+ (controller-slot-id slot)
+ button))))))
+
+(define-method (on-controller-move (kernel <kernel>) controller axis value)
+ (let ((slot (find-controller-slot kernel controller)))
+ (or (let ((handler (controller-move-handler (input-map kernel)
+ (controller-slot-id slot)
+ axis)))
+ (and handler (handler value)))
+ (let* ((r (controller-focus kernel slot))
+ (s (and r (scene r))))
+ (and s (on-controller-move s
+ (controller-slot-id slot)
+ axis
+ value))))))
+
+
+;;;
+;;; Global kernel API
+;;;
+
+(define current-kernel (make-parameter #f))
+
+(define (unique-region-name)
+ (gensym "region-"))
+
+(define* (create-region area #:key (rank 0) (name (unique-region-name)))
+ (let ((region (make-region area name rank)))
+ (add-region (current-kernel) region)
+ region))
+
+(define* (create-full-region #:key (rank 0) (name (unique-region-name)))
+ (let ((w (window-width (current-window)))
+ (h (window-height (current-window))))
+ (create-region (make-rect 0.0 0.0 w h) #:rank rank #:name name)))
+
+(define (kill-region region)
+ (let ((k (current-kernel)))
+ (set! (regions k) (delq region (regions k)))))
+
+(define (all-regions)
+ (regions (current-kernel)))
+
+(define (find-region-by-name name)
+ (lookup-region (current-kernel) name))
+
+(define (current-keyboard-focus)
+ (keyboard-focus (current-kernel)))
+
+(define (take-keyboard-focus region)
+ (set! (keyboard-focus (current-kernel)) region))
+
+(define (current-controller-focus controller-id)
+ (hashv-ref (controller-focus (current-kernel)) controller-id))
+
+(define (take-controller-focus controller-id region)
+ (hashv-set! (controller-focus (current-kernel)) controller-id region))
+
+(define (bind-input/global spec handler)
+ (bind-input (current-kernel) spec handler))
+
+(define (unbind-input/global spec handler)
+ (unbind-input (current-kernel) spec handler))
+
+(define (frames-per-second)
+ (/ 1.0 (average-frame-time (current-kernel))))
+
+(define* (run-catbird thunk #:key (width 1366) (height 768)
+ (title "^~Catbird~^") (fullscreen? #f)
+ (resizable? #t) (update-hz 60))
+ (let ((kernel (make <kernel>)))
+ (parameterize ((current-kernel kernel))
+ (run-game #:window-title title
+ #:window-width width
+ #:window-height height
+ #:window-fullscreen? fullscreen?
+ #:window-resizable? resizable?
+ #:update-hz update-hz
+ #:load
+ (lambda ()
+ (load* kernel)
+ (thunk)
+ (add-overlay kernel))
+ #:draw
+ (lambda (alpha)
+ (render kernel alpha))
+ #:update
+ (lambda (dt)
+ (update kernel dt))
+ #:key-press
+ (lambda (key modifiers repeat?)
+ (on-key-press kernel key modifiers))
+ #:key-release
+ (lambda (key modifiers)
+ (on-key-release kernel key modifiers))
+ #:text-input
+ (lambda (text)
+ (on-text-input kernel text))
+ #:mouse-press
+ ;; TODO: Handle click counter?
+ (lambda (button clicks x y)
+ (on-mouse-press kernel button x y))
+ #:mouse-release
+ (lambda (button x y)
+ (on-mouse-release kernel button x y))
+ #:mouse-move
+ (lambda (x y x-rel y-rel buttons)
+ (on-mouse-move kernel x y x-rel y-rel buttons))
+ #:mouse-wheel
+ (lambda (x y)
+ (on-mouse-wheel kernel x y))
+ #:controller-add
+ (lambda (controller)
+ (on-controller-add kernel controller))
+ #:controller-remove
+ (lambda (controller)
+ (on-controller-remove kernel controller))
+ #:controller-press
+ (lambda (controller button)
+ (on-controller-press kernel controller button))
+ #:controller-release
+ (lambda (controller button)
+ (on-controller-release kernel controller button))
+ #:controller-move
+ (lambda (controller axis value)
+ (on-controller-move kernel controller axis value))))))
+
+(define (exit-catbird)
+ "Stop the Catbird engine."
+ (abort-game))
diff --git a/catbird/line-editor.scm b/catbird/line-editor.scm
new file mode 100644
index 0000000..463bd0c
--- /dev/null
+++ b/catbird/line-editor.scm
@@ -0,0 +1,333 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Single line text editor with history and Emacs-like keybindings.
+;;
+;;; Code:
+(define-module (catbird line-editor)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird observer)
+ #:use-module (catbird region)
+ #:use-module (catbird ring-buffer)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<line-editor>
+ <line-edit-mode>
+ backward-char
+ backward-delete-char
+ forward-delete-char
+ backward-history
+ beginning-of-line
+ clear-line
+ end-of-line
+ forward-char
+ forward-history
+ get-line
+ history-enabled?
+ insert-char
+ invert-color
+ kill-line
+ overwrite
+ prompt
+ save-to-history)
+ #:re-export (color
+ font))
+
+;; TODO: Matching paren/quote highlighting.
+(define-class <line-editor> (<node-2d>)
+ (chars-before #:accessor chars-before #:init-value '())
+ (chars-after #:accessor chars-after #:init-value '())
+ (cached-line #:accessor cached-line #:init-value #f)
+ (prompt #:accessor prompt #:init-keyword #:prompt #:init-value ""
+ #:observe? #t)
+ ;; TODO: Allow customizable history length.
+ (history #:accessor history #:init-form (make-ring-buffer 128))
+ (history-enabled? #:accessor history-enabled?
+ #:init-keyword #:history-enabled? #:init-value #t)
+ (history-index #:accessor history-index #:init-value 0)
+ (font #:accessor font #:init-keyword #:font #:init-thunk default-font
+ #:asset? #t)
+ (color #:accessor color #:init-keyword #:color #:init-value white)
+ (invert-color #:accessor invert-color #:init-keyword #:invert-color
+ #:init-value black)
+ (accepting-input? #:accessor accepting-input? #:init-value #t))
+
+(define-method (on-change (editor <line-editor>) slot old new)
+ (update-visual editor))
+
+(define-method (on-boot (editor <line-editor>))
+ (attach-to editor
+ (make <label>
+ #:name 'prompt
+ #:font (font editor)
+ #:text (prompt editor)
+ #:color (color editor))
+ (make <label>
+ #:name 'before-cursor
+ #:rank 1
+ #:font (font editor)
+ #:color (color editor))
+ (make <label>
+ #:name 'on-cursor
+ #:rank 1
+ #:font (font editor))
+ (make <label>
+ #:name 'after-cursor
+ #:rank 1
+ #:font (font editor)
+ #:color (color editor))
+ (make <canvas>
+ #:name 'cursor
+ #:painter
+ (with-style ((fill-color (color editor)))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ (font-line-width (font editor) "_")
+ (font-line-height (font editor)))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+;; Whenever a command key sequence is pressed while the line editor is
+;; active we have to stop accepting text input for one tick.
+;; Otherwise, an errant character shows up. For example, if the user
+;; presses alt+p, and that is bound to (backward-history editor 1),
+;; the 'p' character shows up at the end of the line. This is due to
+;; the fact that SDL generates a key event *and* an input event for
+;; the same key press.
+(define-method (discard-next-char (editor <line-editor>))
+ (run-script editor
+ (set! (accepting-input? editor) #f)
+ (sleep (current-timestep))
+ (set! (accepting-input? editor) #t)))
+
+(define-method (update-visual (editor <line-editor>))
+ (let* ((lprompt (& editor prompt))
+ (cursor (& editor cursor))
+ (before (& editor before-cursor))
+ (on (& editor on-cursor))
+ (after (& editor after-cursor)))
+ (set! (cached-line editor) #f)
+ ;; Stop cursor blink. The cursor should only blink when the user
+ ;; is idle.
+ (stop-scripts cursor)
+ ;; Ensure the cursor is visible in case we stopped the script
+ ;; during a time when it was hidden.
+ (show cursor)
+ ;; Put the proper text in the proper labels
+ (set! (text lprompt) (prompt editor))
+ (set! (text before)
+ (list->string (reverse (chars-before editor))))
+ (set! (text on)
+ (match (chars-after editor)
+ (() "")
+ ((c . _)
+ (string c))))
+ (set! (text after)
+ (match (chars-after editor)
+ (() "")
+ ((_ . chars)
+ (list->string chars))))
+ ;; Line everything up.
+ (place-right lprompt before)
+ (place-right before on)
+ (place-right on after)
+ (align-left on cursor)
+ ;; Adjust size
+ (set! (width editor)
+ (+ (width lprompt) (width before) (width on) (width after)))
+ (set! (height editor) (height cursor))
+ ;; Resume blinking cursor after a short idle timeout.
+ (run-script cursor
+ (forever
+ (set! (color on) (invert-color editor))
+ (sleep 0.5)
+ (hide cursor)
+ (set! (color on) (color editor))
+ (sleep 0.5)
+ (show cursor)))))
+
+(define-method (get-line (editor <line-editor>))
+ (or (cached-line editor)
+ (let ((line (list->string
+ (append (reverse (chars-before editor))
+ (chars-after editor)))))
+ (set! (cached-line editor) line)
+ line)))
+
+(define-method (overwrite (editor <line-editor>) str)
+ (set! (chars-before editor) (reverse (string->list str)))
+ (set! (chars-after editor) '())
+ (update-visual editor))
+
+(define-method (clear-line (editor <line-editor>))
+ (discard-next-char editor)
+ (overwrite editor "")
+ (newest-history editor))
+
+(define-method (insert-char (editor <line-editor>) char)
+ (when (accepting-input? editor)
+ (set! (chars-before editor) (cons char (chars-before editor)))
+ (update-visual editor)))
+
+(define-method (backward-delete-char (editor <line-editor>) n)
+ (unless (<= n 0)
+ (set! (chars-before editor)
+ (drop (chars-before editor)
+ (min n (length (chars-before editor))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (forward-delete-char (editor <line-editor>) n)
+ (unless (<= n 0)
+ (set! (chars-after editor)
+ (drop (chars-after editor)
+ (min n (length (chars-after editor))))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (kill-line (editor <line-editor>))
+ (forward-delete-char editor (length (chars-after editor))))
+
+(define-method (backward-char (editor <line-editor>) n)
+ (let loop ((n n)
+ (before (chars-before editor))
+ (after (chars-after editor)))
+ (if (or (<= n 0) (null? before))
+ (begin
+ (set! (chars-before editor) before)
+ (set! (chars-after editor) after))
+ (loop (- n 1)
+ (cdr before)
+ (cons (car before) after))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (forward-char (editor <line-editor>) n)
+ (let loop ((n n)
+ (before (chars-before editor))
+ (after (chars-after editor)))
+ (if (or (<= n 0) (null? after))
+ (begin
+ (set! (chars-before editor) before)
+ (set! (chars-after editor) after))
+ (loop (- n 1)
+ (cons (car after) before)
+ (cdr after))))
+ (discard-next-char editor)
+ (update-visual editor))
+
+(define-method (beginning-of-line (editor <line-editor>))
+ (backward-char editor (length (chars-before editor))))
+
+(define-method (end-of-line (editor <line-editor>))
+ (forward-char editor (length (chars-after editor))))
+
+(define-method (save-to-history (editor <line-editor>))
+ (ring-buffer-put! (history editor) (get-line editor)))
+
+(define-method (history-ref (editor <line-editor>) i)
+ (ring-buffer-ref (history editor) i))
+
+(define-method (go-to-history (editor <line-editor>) i)
+ (when (and (history-enabled? editor)
+ (>= i 0)
+ (< i (ring-buffer-length (history editor))))
+ (set! (history-index editor) i)
+ (overwrite editor (history-ref editor i))))
+
+(define-method (backward-history (editor <line-editor>) n)
+ (discard-next-char editor)
+ (go-to-history editor (max (- (history-index editor) n) 0)))
+
+(define-method (forward-history (editor <line-editor>) n)
+ (discard-next-char editor)
+ (go-to-history editor
+ (min (+ (history-index editor) n)
+ (- (ring-buffer-length (history editor)) 1))))
+
+(define-method (newest-history (editor <line-editor>))
+ (set! (history-index editor) (ring-buffer-length (history editor))))
+
+
+;;;
+;;; Line editing minor mode
+;;;
+
+(define-class <line-edit-mode> (<minor-mode>)
+ (editor #:accessor editor #:init-keyword #:editor))
+
+(define-method (insert-text (mode <line-edit-mode>) new-text)
+ (let ((e (editor mode)))
+ (string-for-each (lambda (char)
+ (insert-char e char))
+ new-text)))
+
+(define-method (backward-delete-char (mode <line-edit-mode>))
+ (backward-delete-char (editor mode) 1))
+
+(define-method (forward-delete-char (mode <line-edit-mode>))
+ (forward-delete-char (editor mode) 1))
+
+(define-method (backward-char (mode <line-edit-mode>))
+ (backward-char (editor mode) 1))
+
+(define-method (forward-char (mode <line-edit-mode>))
+ (forward-char (editor mode) 1))
+
+(define-method (beginning-of-line (mode <line-edit-mode>))
+ (beginning-of-line (editor mode)))
+
+(define-method (end-of-line (mode <line-edit-mode>))
+ (end-of-line (editor mode)))
+
+(define-method (backward-history (mode <line-edit-mode>))
+ (backward-history (editor mode) 1))
+
+(define-method (forward-history (mode <line-edit-mode>))
+ (forward-history (editor mode) 1))
+
+(define-method (kill-line (mode <line-edit-mode>))
+ (kill-line (editor mode)))
+
+(bind-input <line-edit-mode> (key-press 'backspace) backward-delete-char)
+(bind-input <line-edit-mode> (key-press 'delete) forward-delete-char)
+(bind-input <line-edit-mode> (key-press 'd '(ctrl)) forward-delete-char)
+(bind-input <line-edit-mode> (key-press 'left) backward-char)
+(bind-input <line-edit-mode> (key-press 'b '(ctrl)) backward-char)
+(bind-input <line-edit-mode> (key-press 'right) forward-char)
+(bind-input <line-edit-mode> (key-press 'f '(ctrl)) forward-char)
+(bind-input <line-edit-mode> (key-press 'home) beginning-of-line)
+(bind-input <line-edit-mode> (key-press 'a '(ctrl)) beginning-of-line)
+(bind-input <line-edit-mode> (key-press 'end) end-of-line)
+(bind-input <line-edit-mode> (key-press 'e '(ctrl)) end-of-line)
+(bind-input <line-edit-mode> (key-press 'up) backward-history)
+(bind-input <line-edit-mode> (key-press 'p '(alt)) backward-history)
+(bind-input <line-edit-mode> (key-press 'down) forward-history)
+(bind-input <line-edit-mode> (key-press 'n '(alt)) forward-history)
+(bind-input <line-edit-mode> (key-press 'k '(ctrl)) kill-line)
+(bind-input <line-edit-mode> (text-input) insert-text)
diff --git a/catbird/minibuffer.scm b/catbird/minibuffer.scm
new file mode 100644
index 0000000..3bfc4c0
--- /dev/null
+++ b/catbird/minibuffer.scm
@@ -0,0 +1,178 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Emacs-like minibuffer for command entry.
+;;
+;;; Code:
+(define-module (catbird minibuffer)
+ #:use-module (catbird kernel)
+ #:use-module (catbird line-editor)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:export (<minibuffer>
+ <minibuffer-mode>
+ define-minibuffer-command))
+
+(define %background-color (make-color 0.0 0.0 0.0 0.8))
+(define %prompt "> ")
+(define %padding 8.0)
+
+(define-class <minibuffer> (<node-2d>)
+ (commands #:accessor commands #:allocation #:class
+ #:init-thunk make-hash-table))
+
+(define (minibuffer-commands)
+ (class-slot-ref <minibuffer> 'commands))
+
+(define (lookup-minibuffer-command name)
+ (hash-ref (minibuffer-commands) name))
+
+(define (add-minibuffer-command name thunk)
+ (hash-set! (minibuffer-commands) name thunk))
+
+(define-syntax-rule (define-minibuffer-command name body ...)
+ (add-minibuffer-command (symbol->string 'name) (lambda () body ...)))
+
+(define-method (on-boot (minibuffer <minibuffer>))
+ (attach-to minibuffer
+ (make <canvas>
+ #:name 'background)
+ (make <line-editor>
+ #:name 'editor
+ #:rank 1
+ #:position (vec2 %padding %padding)
+ #:prompt %prompt)))
+
+(define-method (resize-minibuffer (minibuffer <minibuffer>) width)
+ (set! (painter (& minibuffer background))
+ (with-style ((fill-color %background-color))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ width
+ (+ (font-line-height (font (& minibuffer editor)))
+ (* %padding 2.0)))))))
+
+(define-method (clear-minibuffer (minibuffer <minibuffer>))
+ (clear-line (& minibuffer editor)))
+
+;; TODO: The line editor should have a generic completion facility.
+(define-method (autocomplete (minibuffer <minibuffer>))
+ (let ((prefix (get-line (& minibuffer editor))))
+ ;; Auto-complete if there is a single command name that starts
+ ;; with the characters the user has already typed.
+ (match (hash-fold (lambda (key value prev)
+ (if (string-prefix? prefix key)
+ (cons key prev)
+ prev))
+ '()
+ (minibuffer-commands))
+ ((name)
+ (overwrite (& minibuffer editor) name))
+ ;; TODO: Display multiple completion options to user.
+ (_ #f))))
+
+(define-method (get-command (minibuffer <minibuffer>))
+ (lookup-minibuffer-command (get-line (& minibuffer editor))))
+
+(define-method (valid-command? (minibuffer <minibuffer>))
+ (procedure? (get-command minibuffer)))
+
+(define-method (run-command (minibuffer <minibuffer>))
+ (let ((thunk (get-command minibuffer)))
+ (save-to-history (& minibuffer editor))
+ (when (procedure? thunk)
+ (thunk))))
+
+
+;;;
+;;; Minibuffer major mode
+;;;
+
+(define-class <minibuffer-mode> (<major-mode>)
+ (prev-keyboard-focus #:accessor prev-keyboard-focus))
+
+(define-method (on-enter (mode <minibuffer-mode>))
+ (let* ((scene (parent mode))
+ (region (car (regions scene)))
+ (minibuffer (or (& scene minibuffer)
+ (make <minibuffer>
+ #:name 'minibuffer
+ #:rank 999))))
+ (if (parent minibuffer)
+ (begin
+ (clear-minibuffer minibuffer)
+ (show (& scene minibuffer)))
+ (attach-to (parent mode) minibuffer))
+ (resize-minibuffer minibuffer (area-width region))
+ (set! (prev-keyboard-focus mode) (current-keyboard-focus))
+ (take-keyboard-focus region)
+ (add-minor-mode scene (make <line-edit-mode>
+ #:editor (& scene minibuffer editor)))))
+
+(define-method (on-exit (mode <minibuffer-mode>))
+ (hide (& (parent mode) minibuffer))
+ (remove-minor-mode (parent mode) <line-edit-mode>)
+ (take-keyboard-focus (prev-keyboard-focus mode)))
+
+(define-method (close-minibuffer (mode <minibuffer-mode>))
+ (pop-major-mode (parent mode)))
+
+(define-method (autocomplete (mode <minibuffer-mode>))
+ (autocomplete (& (parent mode) minibuffer)))
+
+(define-method (run-command (mode <minibuffer-mode>))
+ ;; The minibuffer needs to be closed before running the command so
+ ;; that this mode is no longer active and we've had a chance to
+ ;; clean up the state of the overlay scene.
+ (let ((minibuffer (& (parent mode) minibuffer)))
+ (when (valid-command? minibuffer)
+ (close-minibuffer mode)
+ (run-command minibuffer))))
+
+(bind-input <minibuffer-mode> (key-press 'escape) close-minibuffer)
+(bind-input <minibuffer-mode> (key-press 'g '(ctrl)) close-minibuffer)
+(bind-input <minibuffer-mode> (key-press 'tab) autocomplete)
+(bind-input <minibuffer-mode> (key-press 'return) run-command)
+
+
+;;;
+;;; Basic minibuffer commands
+;;;
+
+(define (for-each-user-scene proc)
+ (for-each (lambda (region)
+ (unless (eq? (name region) 'overlay)
+ (let ((s (scene region)))
+ (and s (proc s)))))
+ (all-regions)))
+
+;; General purpose built-in commands.
+(define-minibuffer-command pause (for-each-user-scene pause))
+(define-minibuffer-command resume (for-each-user-scene resume))
+(define-minibuffer-command quit (exit-catbird))
diff --git a/catbird/mixins.scm b/catbird/mixins.scm
new file mode 100644
index 0000000..5e93690
--- /dev/null
+++ b/catbird/mixins.scm
@@ -0,0 +1,216 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Fundamental mix-in classes.
+;;
+;;; Code:
+(define-module (catbird mixins)
+ #:use-module (catbird config)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 exceptions)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-9)
+ #:export (<nameable>
+ name
+
+ <rankable>
+ rank
+ sort-by-rank/ascending
+
+ <containable>
+ parent
+ attach
+ detach
+ on-enter
+ on-exit
+ on-attach
+ on-detach
+
+ <updatable>
+ update
+ update/around
+
+ <scriptable>
+ agenda
+ on-pause
+ on-resume
+ paused?
+ pause
+ resume
+ run-script
+ stop-scripts
+
+ <renderable>
+ visible?
+ on-show
+ on-hide
+ show
+ hide
+ render
+ render/around
+ render/before
+
+ <movable-2d>
+ <movable-3d>
+ position)
+ #:replace (pause))
+
+(define-class <nameable> ()
+ (name #:accessor name #:init-keyword #:name #:init-value #f))
+
+;; For Z sorting objects and such.
+(define-class <rankable> ()
+ (rank #:accessor rank #:init-keyword #:rank #:init-value 0))
+
+(define (sort-by-rank/ascending lst)
+ (sort lst
+ (lambda (a b)
+ (< (rank a) (rank b)))))
+
+
+;;;
+;;; Containable
+;;;
+
+(define-class <containable> ()
+ (parent #:accessor parent #:init-form #f))
+
+(define-method (on-enter (child <containable>))
+ #t)
+
+(define-method (on-exit (child <containable>))
+ #t)
+
+(define-method (on-attach parent (child <containable>))
+ #t)
+
+(define-method (on-detach parent (child <containable>))
+ #t)
+
+(define-method (attach (obj <containable>) container)
+ (when (parent obj)
+ (raise-exception
+ (make-exception-with-message "object already has a parent")))
+ (set! (parent obj) container)
+ (on-enter obj)
+ (on-attach container obj))
+
+(define-method (detach (obj <containable>))
+ (unless (parent obj)
+ (raise-exception
+ (make-exception-with-message "object has no parent")))
+ (on-detach (parent obj) obj)
+ (on-exit obj)
+ (set! (parent obj) #f))
+
+
+;;;
+;;; Updatable
+;;;
+
+(define-class <updatable> ())
+
+(define-method (update (obj <updatable>) dt)
+ #t)
+
+(define-method (update/around (obj <updatable>) dt)
+ (update obj dt))
+
+
+;;;
+;;; Scriptable
+;;;
+
+(define-class <scriptable> (<updatable>)
+ (paused? #:accessor paused? #:init-form #f #:init-keyword #:paused?)
+ (agenda #:getter agenda #:init-thunk make-agenda))
+
+(define-method (on-pause (obj <scriptable>))
+ #t)
+
+(define-method (on-resume (obj <scriptable>))
+ #t)
+
+(define-method (pause (obj <scriptable>))
+ (unless (paused? obj)
+ (set! (paused? obj) #t)
+ (on-pause obj)))
+
+(define-method (resume (obj <scriptable>))
+ (when (paused? obj)
+ (set! (paused? obj) #f)
+ (on-resume obj)))
+
+(define-method (update/around (obj <scriptable>) dt)
+ (unless (paused? obj)
+ (with-agenda (agenda obj)
+ (update-agenda dt)
+ (next-method))))
+
+(define-syntax-rule (run-script obj body ...)
+ (with-agenda (agenda obj) (script body ...)))
+
+(define-method (stop-scripts obj)
+ (with-agenda (agenda obj) (clear-agenda)))
+
+
+;;;
+;;; Renderable
+;;;
+
+(define-class <renderable> ()
+ (visible? #:accessor visible? #:init-form #t #:init-keyword #:visible?))
+
+(define-method (on-show (obj <renderable>))
+ #t)
+
+(define-method (on-hide (obj <renderable>))
+ #t)
+
+(define-method (show (obj <renderable>))
+ (set! (visible? obj) #t)
+ (on-show obj))
+
+(define-method (hide (obj <renderable>))
+ (set! (visible? obj) #f)
+ (on-hide obj))
+
+(define-method (render (obj <renderable>) alpha)
+ #t)
+
+(define-method (render/before (obj <renderable>) alpha)
+ #t)
+
+(define-method (render/around (obj <renderable>) alpha)
+ (when (visible? obj)
+ (render/before obj alpha)
+ (render obj alpha)))
+
+
+;;;
+;;; Movable
+;;;
+
+(define-class <movable-2d> ()
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec2 0.0 0.0)))
+
+(define-class <movable-3d> ()
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec3 0.0 0.0 0.0)))
diff --git a/catbird/mode.scm b/catbird/mode.scm
new file mode 100644
index 0000000..d0d45ac
--- /dev/null
+++ b/catbird/mode.scm
@@ -0,0 +1,126 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Game state encapsulation.
+;;
+;;; Code:
+(define-module (catbird mode)
+ #:use-module (catbird config)
+ #:use-module (catbird input-map)
+ #:use-module (catbird mixins)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<major-mode>
+ <minor-mode>
+ bind-input
+ unbind-input
+ name-mode
+ clear-inputs
+ input-map
+ on-key-press
+ on-key-release
+ on-text-input
+ on-mouse-press
+ on-mouse-release
+ on-mouse-move
+ on-mouse-wheel
+ on-controller-press
+ on-controller-release
+ on-controller-move
+ <nothing-mode>)
+ #:re-export (controller-move
+ controller-press
+ controller-release
+ key-press
+ key-release
+ text-input
+ mouse-move
+ mouse-press
+ mouse-release
+ mouse-wheel
+ name
+ on-enter
+ on-exit
+ on-pause
+ on-resume
+ update))
+
+(define-root-class <mode> (<scriptable> <containable> <nameable>)
+ (input-map #:accessor input-map #:allocation #:each-subclass
+ #:init-thunk make-input-map))
+
+(define-method (input-map (mode-class <class>))
+ (class-slot-ref mode-class 'input-map))
+
+(define-method (bind-input mode-class spec handler)
+ (class-slot-set! mode-class 'input-map
+ (add-input (input-map mode-class) spec handler)))
+
+(define-method (unbind-input mode-class spec)
+ (class-slot-set! mode-class 'input-map
+ (remove-input (input-map mode-class) spec)))
+
+(define (clear-inputs mode-class)
+ (class-slot-set! mode-class 'input-map '()))
+
+(define-method (on-key-press (mode <mode>) key modifiers)
+ (let ((handler (key-press-handler (input-map mode) key modifiers)))
+ (and handler (handler mode))))
+
+(define-method (on-key-release (mode <mode>) key modifiers)
+ (let ((handler (key-release-handler (input-map mode) key modifiers)))
+ (and handler (handler mode))))
+
+(define-method (on-text-input (mode <mode>) text)
+ (let ((handler (text-input-handler (input-map mode))))
+ (and handler (handler mode text))))
+
+(define-method (on-mouse-press (mode <mode>) button x y)
+ (let ((handler (mouse-press-handler (input-map mode) button)))
+ (and handler (handler mode x y))))
+
+(define-method (on-mouse-release (mode <mode>) button x y)
+ (let ((handler (mouse-release-handler (input-map mode) button)))
+ (and handler (handler mode x y))))
+
+(define-method (on-mouse-move (mode <mode>) x y x-rel y-rel buttons)
+ (let ((handler (mouse-move-handler (input-map mode) buttons)))
+ (and handler (handler mode x y x-rel y-rel))))
+
+(define-method (on-mouse-wheel (mode <mode>) x y)
+ (let ((handler (mouse-wheel-handler (input-map mode))))
+ (and handler (handler mode x y))))
+
+(define-method (on-controller-press (mode <mode>) controller-id button)
+ (let ((handler (controller-press-handler (input-map mode) controller-id button)))
+ (and handler (handler mode))))
+
+(define-method (on-controller-release (mode <mode>) controller-id button)
+ (let ((handler (controller-release-handler (input-map mode) controller-id button)))
+ (and handler (handler mode))))
+
+(define-method (on-controller-move (mode <mode>) controller-id axis value)
+ (let ((handler (controller-move-handler (input-map mode) controller-id axis)))
+ (and handler (handler mode value))))
+
+(define-class <major-mode> (<mode>))
+
+(define-class <minor-mode> (<mode>))
+
+(define-class <nothing-mode> (<major-mode>))
diff --git a/catbird/node-2d.scm b/catbird/node-2d.scm
new file mode 100644
index 0000000..47749cc
--- /dev/null
+++ b/catbird/node-2d.scm
@@ -0,0 +1,960 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; 2D game nodes.
+;;
+;;; Code:
+(define-module (catbird node-2d)
+ #:use-module (catbird asset)
+ #:use-module (catbird camera)
+ #:use-module (catbird cached-slots)
+ #:use-module (catbird mixins)
+ #:use-module (catbird node)
+ #:use-module (catbird observer)
+ #:use-module (chickadee)
+ #:use-module (chickadee math)
+ #:use-module (chickadee math bezier)
+ #:use-module (chickadee math easings)
+ #:use-module (chickadee math matrix)
+ #:use-module (chickadee math rect)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee graphics 9-patch)
+ #:use-module (chickadee graphics blend)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics engine)
+ #:use-module (chickadee graphics framebuffer)
+ #:use-module (chickadee graphics particles)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics sprite)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee graphics texture)
+ #:use-module (chickadee graphics tile-map)
+ #:use-module (chickadee graphics viewport)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (rnrs base)
+ #:export (<node-2d>
+ aggregate-bounding-box
+ align-bottom
+ align-left
+ align-right
+ align-top
+ default-height
+ default-width
+ expire-local-matrix
+ follow-bezier-path
+ local-bounding-box
+ local-matrix
+ move-by
+ move-to
+ on-child-resize
+ origin
+ origin-x
+ origin-y
+ pick
+ place-above
+ place-below
+ place-left
+ place-right
+ position-x
+ position-y
+ resize
+ rotate-by
+ rotate-to
+ rotation
+ scale
+ scale-by
+ scale-to
+ scale-x
+ scale-y
+ shear
+ shear-x
+ shear-y
+ teleport
+ world-bounding-box
+ world-matrix
+
+ <sprite>
+ texture
+ source-rect
+ blend-mode
+ tint
+
+ <atlas-sprite>
+ atlas
+ index
+
+ <animation>
+ frames
+ frame-duration
+
+ <animated-sprite>
+ animations
+ frame-duration
+ current-animation
+ start-time
+ change-animation
+
+ <9-patch>
+ top-margin
+ bottom-margin
+ left-margin
+ right-margin
+
+ <sprite-batch>
+ batch
+
+ <canvas>
+ painter
+
+ <label>
+ font
+ text
+ color
+ align
+ vertical-align
+
+ <tile-map>
+ tile-map
+ layers
+
+ <particles>
+ particles)
+ #:re-export (height
+ position
+ width))
+
+(define (refresh-local-matrix node local)
+ (matrix4-2d-transform! local
+ #:origin (origin node)
+ #:position (render-position node)
+ #:rotation (rotation node)
+ #:scale (scale node)
+ #:shear (shear node))
+ local)
+
+(define (refresh-world-matrix node world)
+ (let ((p (parent node))
+ (local (local-matrix node)))
+ (if (is-a? p <node-2d>)
+ (matrix4-mult! world local (world-matrix (parent node)))
+ (begin
+ (matrix4-identity! world)
+ (matrix4-mult! world world local)))
+ world))
+
+(define (refresh-inverse-world-matrix node inverse)
+ (matrix4-inverse! (world-matrix node) inverse)
+ inverse)
+
+(define (refresh-local-bounding-box node bb)
+ (let ((p (position node))
+ (o (origin node))
+ (r (rotation node))
+ (k (shear node))
+ (s (size node)))
+ (if (and (= r 0.0)
+ (= (vec2-x k) 0.0)
+ (= (vec2-y k) 0.0))
+ ;; Fast path: Node is axis-aligned and bounding box
+ ;; calculation is easy peasy.
+ (let ((s (scale node)))
+ (set-rect-x! bb (- (vec2-x p) (vec2-x o)))
+ (set-rect-y! bb (- (vec2-y p) (vec2-y o)))
+ (set-rect-width! bb (* (rect-width s) (vec2-x s)))
+ (set-rect-height! bb (* (rect-height s) (vec2-y s))))
+ ;; Slow path: Node is rotated, sheared, or both.
+ (let* ((m (local-matrix node))
+ (x0 0.0)
+ (y0 0.0)
+ (x1 (rect-width s))
+ (y1 (rect-height s))
+ (x2 (matrix4-transform-x m x0 y0))
+ (y2 (matrix4-transform-y m x0 y0))
+ (x3 (matrix4-transform-x m x1 y0))
+ (y3 (matrix4-transform-y m x1 y0))
+ (x4 (matrix4-transform-x m x1 y1))
+ (y4 (matrix4-transform-y m x1 y1))
+ (x5 (matrix4-transform-x m x0 y1))
+ (y5 (matrix4-transform-y m x0 y1))
+ (xmin (min x2 x3 x4 x5))
+ (ymin (min y2 y3 y4 y5))
+ (xmax (max x2 x3 x4 x5))
+ (ymax (max y2 y3 y4 y5)))
+ (set-rect-x! bb xmin)
+ (set-rect-y! bb ymin)
+ (set-rect-width! bb (- xmax xmin))
+ (set-rect-height! bb (- ymax ymin))))
+ bb))
+
+(define (refresh-world-bounding-box node bb)
+ (let* ((m (world-matrix node))
+ (s (size node))
+ (x0 0.0)
+ (y0 0.0)
+ (x1 (rect-width s))
+ (y1 (rect-height s))
+ (x2 (matrix4-transform-x m x0 y0))
+ (y2 (matrix4-transform-y m x0 y0))
+ (x3 (matrix4-transform-x m x1 y0))
+ (y3 (matrix4-transform-y m x1 y0))
+ (x4 (matrix4-transform-x m x1 y1))
+ (y4 (matrix4-transform-y m x1 y1))
+ (x5 (matrix4-transform-x m x0 y1))
+ (y5 (matrix4-transform-y m x0 y1))
+ (xmin (min x2 x3 x4 x5))
+ (ymin (min y2 y3 y4 y5))
+ (xmax (max x2 x3 x4 x5))
+ (ymax (max y2 y3 y4 y5)))
+ (set-rect-x! bb xmin)
+ (set-rect-y! bb ymin)
+ (set-rect-width! bb (- xmax xmin))
+ (set-rect-height! bb (- ymax ymin))
+ bb))
+
+(define (refresh-aggregate-bounding-box node bb)
+ ;; If the node has no children then the aggregate bounding box is
+ ;; the same as the world bounding box.
+ (rect-copy! (world-bounding-box node) bb)
+ (for-each-child (lambda (child)
+ (rect-union! bb (aggregate-bounding-box child)))
+ node)
+ bb)
+
+(define-class <node-2d> (<node> <movable-2d> <cacheable>)
+ ;; Translation of the origin. By default, the origin is at the
+ ;; bottom left corner of a node.
+ (origin #:accessor origin #:init-form (vec2 0.0 0.0) #:init-keyword #:origin
+ #:observe? #t)
+ (origin-x #:accessor origin-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (origin node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (origin node) x)
+ (expire-local-matrix node)))
+ (origin-y #:accessor origin-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (origin node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (origin node) y)
+ (expire-local-matrix node)))
+ ;; Translation
+ (position #:accessor position #:init-keyword #:position
+ #:init-form (vec2 0.0 0.0) #:observe? #t)
+ (position-x #:accessor position-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (position node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (position node) x)
+ (expire-local-matrix node)))
+ (position-y #:accessor position-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (position node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (position node) y)
+ (expire-local-matrix node)))
+ ;; Rotation around the Z-axis.
+ (rotation #:accessor rotation #:init-form 0.0 #:init-keyword #:rotation
+ #:observe? #t)
+ ;; Scaling
+ (scale #:accessor scale #:init-form (vec2 1.0 1.0) #:init-keyword #:scale
+ #:observe? #t)
+ (scale-x #:accessor scale-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (scale node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (scale node) x)
+ (expire-local-matrix node)))
+ (scale-y #:accessor scale-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (scale node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (scale node) y)
+ (expire-local-matrix node)))
+ ;; Shearing
+ (shear #:accessor shear #:init-form (vec2 0.0 0.0) #:init-keyword #:shear
+ #:observe? #t)
+ (shear-x #:accessor shear-x #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-x (shear node)))
+ #:slot-set! (lambda (node x)
+ (set-vec2-x! (shear node) x)
+ (expire-local-matrix node)))
+ (shear-y #:accessor shear-y #:allocation #:virtual
+ #:slot-ref (lambda (node) (vec2-y (shear node)))
+ #:slot-set! (lambda (node y)
+ (set-vec2-y! (shear node) y)
+ (expire-local-matrix node)))
+ ;; Some extra position vectors for defeating "temporal aliasing"
+ ;; when rendering.
+ (last-position #:getter last-position #:init-form (vec2 0.0 0.0))
+ (render-position #:getter render-position #:init-form (vec2 0.0 0.0))
+ ;; Transformation matrices:
+ ;;
+ ;; The local matrix incorporates the node-specific translation,
+ ;; rotation, scale, and shear factors.
+ (local-matrix #:getter local-matrix #:init-thunk make-identity-matrix4
+ #:cached? #t #:refresh refresh-local-matrix)
+ ;; The world matrix is defined by the multiplication of the parent's
+ ;; world matrix with the local matrix.
+ (world-matrix #:getter world-matrix #:init-thunk make-identity-matrix4
+ #:cached? #t #:refresh refresh-world-matrix)
+ ;; The inverse world matrix is useful for translating world
+ ;; coordinates into local coordinates. Using this matrix it is
+ ;; possible to detect if the mouse is over a rotated and sheared
+ ;; node, for example.
+ (inverse-world-matrix #:getter inverse-world-matrix
+ #:init-form (make-identity-matrix4)
+ #:cached? #t #:refresh refresh-inverse-world-matrix)
+ ;; Node dimensions. Stored as a rectangle for convenience, so it
+ ;; can be used as a bounding box that doesn't take any
+ ;; transformation matrix into consideration.
+ (size #:getter size #:init-thunk make-null-rect)
+ (width #:accessor width #:init-keyword #:width #:watch? #t #:allocation #:virtual
+ #:slot-ref (lambda (node) (rect-width (size node)))
+ #:slot-set! (lambda (node w)
+ (set-rect-width! (size node) w)
+ (expire-local-bounding-box node)))
+ (height #:accessor height #:init-keyword #:height #:watch? #t #:allocation #:virtual
+ #:slot-ref (lambda (node) (rect-height (size node)))
+ #:slot-set! (lambda (node h)
+ (set-rect-height! (size node) h)
+ (expire-local-bounding-box node)))
+ ;; The local bounding box is the combination of the node's
+ ;; dimensions with the local transformation matrix.
+ (local-bounding-box #:getter local-bounding-box #:init-thunk make-null-rect
+ #:cached? #t #:refresh refresh-local-bounding-box)
+ ;; The world bounding box is the combination of the node's
+ ;; dimensions with the world transformation matrix.
+ (world-bounding-box #:getter world-bounding-box #:init-thunk make-null-rect
+ #:cached? #t #:refresh refresh-world-bounding-box)
+ ;; The aggregate bounding box is the union of the node's world
+ ;; bounding boxes and the aggregate bounding boxes of all its
+ ;; children. This bounding box is used to quickly determine if a
+ ;; point in world space might be within any node in a tree. This
+ ;; bounding box can be used for render culling, mouse selection, and
+ ;; render clipping.
+ (aggregate-bounding-box #:getter aggregate-bounding-box
+ #:init-thunk make-null-rect #:cached? #t
+ #:refresh refresh-aggregate-bounding-box))
+
+(define-method (initialize (node <node-2d>) args)
+ (next-method)
+ ;; If scale is specified as a scalar value, convert it to a vector
+ ;; that applies identical scaling to both axes.
+ (let ((s (scale node)))
+ (when (number? s)
+ (slot-set! node 'scale (vec2 s s))))
+ ;; If caller doesn't specify a custom width and height, let the node
+ ;; pick a reasonable default size.
+ (when (= (width node) 0.0)
+ (set! (width node) (default-width node)))
+ (when (= (height node) 0.0)
+ (set! (height node) (default-height node)))
+ ;; Build an initial bounding box.
+ (vec2-copy! (position node) (render-position node))
+ ;; Set the initial last position to the same as the initial position
+ ;; to avoid a brief flash where the node appears at (0, 0).
+ (remember-position node))
+
+(define (expire-local-matrix node)
+ (expire-slot! node 'local-matrix)
+ (expire-world-matrix node)
+ (expire-local-bounding-box node))
+
+(define (expire-world-matrix node)
+ (unless (slot-expired? node 'world-matrix)
+ (expire-slot! node 'world-matrix)
+ (expire-slot! node 'inverse-world-matrix)
+ (for-each-child (lambda (child)
+ (expire-world-matrix child)
+ (expire-world-bounding-box child))
+ node)))
+
+(define (expire-local-bounding-box node)
+ (expire-slot! node 'local-bounding-box)
+ (expire-world-bounding-box node))
+
+(define (expire-world-bounding-box node)
+ (expire-slot! node 'world-bounding-box)
+ (expire-aggregate-bounding-box node))
+
+(define (expire-aggregate-bounding-box node)
+ (unless (slot-expired? node 'aggregate-bounding-box)
+ (expire-slot! node 'aggregate-bounding-box)
+ (let ((p (parent node)))
+ (when (is-a? p <node-2d>)
+ (expire-aggregate-bounding-box p)))))
+
+
+;;;
+;;; Bounding boxes
+;;;
+
+(define-method (default-width (node <node-2d>)) 0.0)
+
+(define-method (default-height (node <node-2d>)) 0.0)
+
+(define-method (on-child-resize node child)
+ #t)
+
+;; (define-method ((setter origin) (node <node-2d>))
+;; (dirty! node)
+;; (next-method))
+
+(define-method (on-change (node <node-2d>) slot old new)
+ (case slot
+ ((origin position rotation scale shear)
+ (expire-local-matrix node))))
+
+(define-method (resize (node <node-2d>) w h)
+ (set! (width node) w)
+ (set! (height node) h)
+ (expire-local-bounding-box node))
+
+
+;;;
+;;; Animation
+;;;
+
+(define-method (remember-position (node <node-2d>))
+ (vec2-copy! (position node) (last-position node)))
+
+(define-method (remember-position/recursive (node <node-2d>))
+ (remember-position node)
+ (for-each-child remember-position/recursive node))
+
+(define-method (move-to (node <node-2d>) x y)
+ (set! (position-x node) x)
+ (set! (position-y node) y))
+
+(define-method (move-to (node <node-2d>) x y duration ease)
+ (let ((p (position node)))
+ (move-by node (- x (vec2-x p)) (- y (vec2-y p)) duration ease)))
+
+(define-method (move-to (node <node-2d>) x y duration)
+ (move-to node x y duration smoothstep))
+
+(define-method (move-by (node <node-2d>) dx dy)
+ (let ((p (position node)))
+ (move-to node (+ (vec2-x p) dx) (+ (vec2-y p) dy))))
+
+(define-method (move-by (node <node-2d>) dx dy duration ease)
+ (let* ((p (position node))
+ (start-x (vec2-x p))
+ (start-y (vec2-y p)))
+ (tween duration 0.0 1.0
+ (lambda (n)
+ (move-to node
+ (+ start-x (* dx n))
+ (+ start-y (* dy n))))
+ #:ease ease)))
+
+(define-method (move-by (node <node-2d>) dx dy duration)
+ (move-by node dx dy duration smoothstep))
+
+(define-method (teleport (node <node-2d>) x y)
+ ;; When teleporting, we want to avoid position interpolation and odd
+ ;; looking camera jumps.
+ ;;
+ ;; Interpolation is avoided by setting all 3 position vectors to the
+ ;; same values. This prevents a visual artifact where the player
+ ;; sees 1 frame where the node is somewhere in between its former
+ ;; position and the new position.
+ ;;
+ ;; The camera jump problem occurs when a camera has a node as its
+ ;; tracking target and that node teleports. Normally, the camera's
+ ;; view matrix is updated before any nodes are rendered, and thus
+ ;; *before* the node can recompute its world matrix based on the new
+ ;; position. This creates 1 frame where the camera is improperly
+ ;; positioned at the target's old location. This 1 frame lag is not
+ ;; an issue during normal movement, but when teleporting it causes a
+ ;; noticably unsmooth blip. Forcing the matrices to be recomputed
+ ;; immediately solves this issue.
+ (set-vec2! (position node) x y)
+ (set-vec2! (last-position node) x y)
+ (set-vec2! (render-position node) x y)
+ (expire-local-matrix node))
+
+(define-method (rotate-to (node <node-2d>) theta)
+ (set! (rotation node) theta))
+
+(define-method (rotate-to (node <node-2d>) theta duration ease)
+ (tween duration (rotation node) theta
+ (lambda (r)
+ (rotate-to node r))
+ #:ease ease))
+
+(define-method (rotate-to (node <node-2d>) theta duration)
+ (rotate-to node theta duration smoothstep))
+
+(define-method (rotate-by (node <node-2d>) dtheta)
+ (rotate-to node (+ (rotation node) dtheta)))
+
+(define-method (rotate-by (node <node-2d>) dtheta duration ease)
+ (rotate-to node (+ (rotation node) dtheta) duration ease))
+
+(define-method (rotate-by (node <node-2d>) dtheta duration)
+ (rotate-by node dtheta duration smoothstep))
+
+(define-method (scale-to (node <node-2d>) sx sy)
+ (set! (scale-x node) sx)
+ (set! (scale-y node) sy))
+
+(define-method (scale-to (node <node-2d>) s)
+ (scale-to node s s))
+
+(define-method (scale-to (node <node-2d>) sx sy duration ease)
+ (scale-by node (- sx (scale-x node)) (- sy (scale-y node)) duration ease))
+
+(define-method (scale-to (node <node-2d>) sx sy duration)
+ (scale-to node sx sy duration smoothstep))
+
+(define-method (scale-by (node <node-2d>) dsx dsy)
+ (scale-to node (+ (scale-x node) dsx) (+ (scale-y node) dsy)))
+
+(define-method (scale-by (node <node-2d>) ds)
+ (scale-by node ds ds))
+
+(define-method (scale-by (node <node-2d>) dsx dsy duration ease)
+ (let ((start-x (scale-x node))
+ (start-y (scale-y node)))
+ (tween duration 0.0 1.0
+ (lambda (n)
+ (scale-to node
+ (+ start-x (* dsx n))
+ (+ start-y (* dsy n))))
+ #:ease ease)))
+
+(define-method (scale-by (node <node-2d>) dsx dsy duration)
+ (scale-by node dsx dsy duration smoothstep))
+
+(define-method (scale-by (node <node-2d>) ds duration (ease <procedure>))
+ (scale-by node ds ds duration ease))
+
+(define-method (follow-bezier-path (node <node-2d>) path duration forward?)
+ (let ((p (position node))
+ (path (if forward? path (reverse path))))
+ (for-each (lambda (bezier)
+ (tween duration
+ (if forward? 0.0 1.0)
+ (if forward? 1.0 0.0)
+ (lambda (t)
+ (bezier-curve-point-at! p bezier t)
+ (expire-local-matrix node))
+ #:ease linear))
+ path)))
+
+(define-method (follow-bezier-path (node <node-2d>) path duration)
+ (follow-bezier-path node path duration #t))
+
+(define-method (pick (node <node-2d>) p pred)
+ (and (pred node)
+ (let loop ((kids (reverse (children node))))
+ (match kids
+ (()
+ (let* ((m (inverse-world-matrix node))
+ (x (vec2-x p))
+ (y (vec2-y p))
+ (tx (matrix4-transform-x m x y))
+ (ty (matrix4-transform-y m x y)))
+ (and (>= tx 0.0)
+ (< tx (width node))
+ (>= ty 0.0)
+ (< ty (height node))
+ node)))
+ ((child . rest)
+ (let ((o (origin node)))
+ (or (pick child p pred)
+ (loop rest))))))))
+
+
+;;;
+;;; Updating/rendering
+;;;
+
+(define-method (update/around (node <node-2d>) dt)
+ (unless (paused? node)
+ (remember-position node))
+ (next-method))
+
+(define-method (pause (node <node-2d>))
+ ;; We need to set the last position of all objects in the tree to
+ ;; their current position, otherwise any moving objects will
+ ;; experience this weird jitter while paused because the last
+ ;; position will never be updated during the duration of the pause
+ ;; event.
+ (next-method)
+ (remember-position/recursive node))
+
+(define-method (tree-in-view? (node <node-2d>))
+ (rect-intersects? (aggregate-bounding-box node)
+ (view-bounding-box (current-camera))))
+
+(define-method (in-view? (node <node-2d>))
+ (rect-intersects? (world-bounding-box node)
+ (view-bounding-box (current-camera))))
+
+(define-method (render/around (node <node-2d>) alpha)
+ ;; Compute the linearly interpolated rendering position, in the case
+ ;; that node has moved since the last update.
+ (when (visible? node)
+ (let ((p (position node))
+ (lp (last-position node))
+ (rp (render-position node))
+ (beta (- 1.0 alpha)))
+ (unless (and (vec2= rp p) (vec2= lp p))
+ (set-vec2-x! rp (+ (* (vec2-x p) alpha) (* (vec2-x lp) beta)))
+ (set-vec2-y! rp (+ (* (vec2-y p) alpha) (* (vec2-y lp) beta)))
+ (expire-local-matrix node)))
+ (next-method)))
+
+
+;;;
+;;; Relative placement and alignment
+;;;
+
+;; Placement and alignment of nodes is done under the assumption that
+;; the nodes are in the same local coordinate space. If this is not
+;; the case, the results will be garbage.
+
+(define* (place-right a b #:key (padding 0.0))
+ "Adjust B's x position coordinate so that it is PADDING distance to
+the right of A."
+ (set! (position-x b) (+ (position-x a) (width a) padding)))
+
+(define* (place-left a b #:key (padding 0.0))
+ "Adjust B's x position coordinate so that it is PADDING distance to
+the left of A."
+ (set! (position-x b) (- (position-x a) (width b) padding)))
+
+(define* (place-above a b #:key (padding 0.0))
+ "Adjust B's y position coordinate so that it is PADDING distance above
+A."
+ (set! (position-y b) (+ (position-y a) (height a) padding)))
+
+(define* (place-below a b #:key (padding 0.0))
+ "Adjust B's y position coordinate so that it is PADDING distance below
+A."
+ (set! (position-y b) (- (position-y a) (height b) padding)))
+
+(define (align-left a b)
+ "Align the left side of B with the left side of A."
+ (set! (position-x b) (position-x a)))
+
+(define (align-right a b)
+ "Align the right side of B with the right side of A."
+ (set! (position-x b) (+ (position-x a) (width a))))
+
+(define (align-bottom a b)
+ "Align the bottom of B with the bottom of A."
+ (set! (position-y b) (position-y a)))
+
+(define (align-top a b)
+ "Align the top of B with the top of A."
+ (set! (position-y b) (+ (position-y a) (height a))))
+
+
+;;;
+;;; Sprite
+;;;
+
+(define-class <sprite> (<node-2d>)
+ (texture #:accessor texture #:init-keyword #:texture #:asset? #t
+ #:observe? #t)
+ (tint #:accessor tint #:init-keyword #:tint #:init-form white)
+ (blend-mode #:accessor blend-mode #:init-keyword #:blend-mode
+ #:init-form blend:alpha))
+
+(define-method (default-width (sprite <sprite>))
+ (texture-width (texture sprite)))
+
+(define-method (default-height (sprite <sprite>))
+ (texture-height (texture sprite)))
+
+(define-method (on-change (sprite <sprite>) slot-name old new)
+ (case slot-name
+ ((texture)
+ (set! (width sprite) (texture-width new))
+ (set! (height sprite) (texture-height new)))))
+
+(define-method (render (sprite <sprite>) alpha)
+ (let ((t (texture sprite)))
+ (with-graphics-state ((g:blend-mode (blend-mode sprite)))
+ (draw-sprite* t (size sprite) (world-matrix sprite)
+ #:tint (tint sprite)
+ #:texcoords (texture-gl-tex-rect t)))))
+
+
+;;;
+;;; Texture Atlas Sprite
+;;;
+
+(define-class <atlas-sprite> (<sprite>)
+ (atlas #:accessor atlas #:init-keyword #:atlas #:asset? #t #:observe? #t)
+ (index #:accessor index #:init-keyword #:index #:init-value 0 #:observe? #t))
+
+(define-method (sync-texture (sprite <atlas-sprite>))
+ (let ((t (texture-atlas-ref (atlas sprite) (index sprite))))
+ (set! (texture sprite) t)))
+
+(define-method (on-boot (sprite <atlas-sprite>))
+ (sync-texture sprite))
+
+(define-method (on-change (sprite <atlas-sprite>) slot-name old new)
+ (case slot-name
+ ((atlas index)
+ (sync-texture sprite))
+ (else
+ (next-method))))
+
+
+;;;
+;;; Animated Sprite
+;;;
+
+(define-class <animation> ()
+ (frames #:getter frames #:init-keyword #:frames)
+ (frame-duration #:getter frame-duration #:init-keyword #:frame-duration
+ #:init-form 250))
+
+(define-class <animated-sprite> (<atlas-sprite>)
+ (atlas #:accessor atlas #:init-keyword #:atlas #:asset? #t)
+ (animations #:accessor animations #:init-keyword #:animations)
+ (current-animation #:accessor current-animation
+ #:init-keyword #:default-animation
+ #:init-form 'default)
+ (start-time #:accessor start-time #:init-form 0))
+
+(define-method (on-enter (sprite <animated-sprite>))
+ (update sprite 0))
+
+(define-method (update (sprite <animated-sprite>) dt)
+ (let* ((anim (assq-ref (animations sprite) (current-animation sprite)))
+ (frame-duration (frame-duration anim))
+ (frames (frames anim))
+ (anim-duration (* frame-duration (vector-length frames)))
+ (time (mod (- (elapsed-time) (start-time sprite)) anim-duration))
+ (frame (vector-ref frames (inexact->exact
+ (floor (/ time frame-duration))))))
+ (when (not (= frame (index sprite)))
+ (set! (index sprite) frame))))
+
+(define-method (change-animation (sprite <animated-sprite>) name)
+ (set! (current-animation sprite) name)
+ (set! (start-time sprite) (elapsed-time)))
+
+
+;;;
+;;; 9-Patch
+;;;
+
+(define-class <9-patch> (<node-2d>)
+ (texture #:accessor texture #:init-keyword #:texture #:asset? #t)
+ (left-margin #:accessor left-margin #:init-keyword #:left)
+ (right-margin #:accessor right-margin #:init-keyword #:right)
+ (bottom-margin #:accessor bottom-margin #:init-keyword #:bottom)
+ (top-margin #:accessor top-margin #:init-keyword #:top)
+ (mode #:accessor mode #:init-keyword #:mode #:init-value 'stretch)
+ (blend-mode #:accessor blend-mode #:init-keyword #:blend-mode
+ #:init-value blend:alpha)
+ (tint #:accessor tint #:init-keyword #:tint #:init-value white)
+ (render-rect #:getter render-rect #:init-form (make-rect 0.0 0.0 0.0 0.0)))
+
+(define-method (initialize (9-patch <9-patch>) initargs)
+ (let ((default-margin (get-keyword #:margin initargs 0.0)))
+ (slot-set! 9-patch 'left-margin default-margin)
+ (slot-set! 9-patch 'right-margin default-margin)
+ (slot-set! 9-patch 'bottom-margin default-margin)
+ (slot-set! 9-patch 'top-margin default-margin))
+ (next-method)
+ (set-rect-width! (render-rect 9-patch) (width 9-patch))
+ (set-rect-height! (render-rect 9-patch) (height 9-patch)))
+
+;; (define-method (on-change (9-patch <9-patch>) slot-name old new)
+;; (case slot-name
+;; ((width)
+;; (set-rect-width! (render-rect 9-patch) new))
+;; ((height)
+;; (set-rect-height! (render-rect 9-patch) new)))
+;; (next-method))
+
+(define-method (render (9-patch <9-patch>) alpha)
+ (draw-9-patch* (texture 9-patch)
+ (render-rect 9-patch)
+ (world-matrix 9-patch)
+ #:top-margin (top-margin 9-patch)
+ #:bottom-margin (bottom-margin 9-patch)
+ #:left-margin (left-margin 9-patch)
+ #:right-margin (right-margin 9-patch)
+ #:mode (mode 9-patch)
+ #:blend-mode (blend-mode 9-patch)
+ #:tint (tint 9-patch)))
+
+
+;;;
+;;; Sprite Batch
+;;;
+
+(define-class <sprite-batch> (<node-2d>)
+ (batch #:accessor batch #:init-keyword #:batch)
+ (blend-mode #:accessor blend-mode
+ #:init-keyword #:blend-mode
+ #:init-form blend:alpha)
+ (clear-after-draw? #:accessor clear-after-draw?
+ #:init-keyword #:clear-after-draw?
+ #:init-form #t)
+ (batch-matrix #:accessor batch-matrix #:init-thunk make-identity-matrix4))
+
+(define-method (render (sprite-batch <sprite-batch>) alpha)
+ (let ((batch (batch sprite-batch)))
+ (draw-sprite-batch* batch (batch-matrix sprite-batch)
+ #:blend-mode (blend-mode sprite-batch))
+ (when (clear-after-draw? sprite-batch)
+ (sprite-batch-clear! batch))))
+
+
+;;;
+;;; Vector Path
+;;;
+
+(define-class <canvas> (<node-2d>)
+ (painter #:accessor painter #:init-keyword #:painter #:init-value #f
+ #:observe? #t)
+ (canvas #:accessor canvas #:init-thunk make-empty-canvas))
+
+(define-method (refresh-painter (c <canvas>))
+ (let* ((p (painter c)))
+ (when p
+ (let ((bb (painter-bounding-box p)))
+ (set-canvas-painter! (canvas c) p)
+ ;; (set! (origin-x canvas) (- (rect-x bb)))
+ ;; (set! (origin-y canvas) (- (rect-y bb)))
+ (set! (width c) (rect-width bb))
+ (set! (height c) (rect-height bb))))))
+
+(define-method (on-boot (c <canvas>))
+ (refresh-painter c))
+
+(define-method ((setter canvas) (c <canvas>))
+ (next-method)
+ (set-canvas-painter! (canvas c) (painter c)))
+
+(define-method (on-change (c <canvas>) slot-name old new)
+ (case slot-name
+ ((painter)
+ (refresh-painter c))
+ (else
+ (next-method))))
+
+(define-method (render (c <canvas>) alpha)
+ (draw-canvas* (canvas c) (world-matrix c)))
+
+
+;;;
+;;; Label
+;;;
+
+(define-class <label> (<node-2d>)
+ (font #:accessor font #:init-keyword #:font #:init-thunk default-font
+ #:asset? #t #:observe? #t)
+ (text #:accessor text #:init-value "" #:init-keyword #:text #:observe? #t)
+ (compositor #:accessor compositor #:init-thunk make-compositor)
+ (page #:accessor page #:init-thunk make-page)
+ (typeset #:accessor typeset #:init-value typeset-lrtb)
+ (align #:accessor align #:init-value 'left #:init-keyword #:align #:observe? #t)
+ (vertical-align #:accessor vertical-align #:init-value 'bottom
+ #:init-keyword #:vertical-align #:observe? #t)
+ (color #:accessor color #:init-keyword #:color #:init-value white #:observe? #t))
+
+(define-method (realign (label <label>))
+ (set! (origin-x label)
+ (case (align label)
+ ((left) 0.0)
+ ((right) (width label))
+ ((center) (/ (width label) 2.0))))
+ (set! (origin-y label)
+ (case (vertical-align label)
+ ((bottom) 0.0)
+ ((top) (height label))
+ ((center) (+ (/ (height label) 2.0) (font-descent (font label)))))))
+
+(define-method (refresh-label (label <label>))
+ (let ((c (compositor label))
+ (p (page label)))
+ (compositor-reset! c)
+ ((typeset label) c (font label) (text label) (color label))
+ (page-reset! p)
+ (page-write! p c)
+ (let ((bb (page-bounding-box p)))
+ (set! (width label) (rect-width bb))
+ (set! (height label) (rect-height bb)))))
+
+(define-method (on-boot (label <label>))
+ (refresh-label label)
+ (realign label))
+
+(define-method (on-asset-reload (label <label>) slot-name asset)
+ (case slot-name
+ ((font)
+ (refresh-label label))))
+
+(define-method (on-change (label <label>) slot-name old new)
+ (case slot-name
+ ((font text)
+ (refresh-label label)
+ (unless (eq? (align label) 'left)
+ (realign label)))
+ ((color)
+ (refresh-label label))
+ ((align vertical-align)
+ (realign label))
+ (else
+ (next-method))))
+
+(define-method (render (label <label>) alpha)
+ (draw-page (page label) (world-matrix label)))
+
+
+;;;
+;;; Tiled Map
+;;;
+
+(define-class <tile-map> (<node-2d>)
+ (tile-map #:accessor tile-map #:init-keyword #:map #:asset? #t)
+ (layers #:accessor layers #:init-keyword #:layers #:init-form #f))
+
+(define-method (render (node <tile-map>) alpha)
+ (let ((m (tile-map node)))
+ (draw-tile-map* m (world-matrix node) (tile-map-rect m)
+ #:layers (layers node))))
+
+
+;;;
+;;; Particles
+;;;
+
+(define-class <particles> (<node-2d>)
+ (particles #:accessor particles #:init-keyword #:particles))
+
+(define-method (on-boot (particles <particles>))
+ ;; Default bounding box size.
+ (when (zero? (width particles))
+ (set! (width particles) 32.0))
+ (when (zero? (height particles))
+ (set! (height particles) 32.0)))
+
+(define-method (update (node <particles>) dt)
+ (update-particles (particles node)))
+
+(define-method (render (node <particles>) alpha)
+ (draw-particles* (particles node) (world-matrix node)))
diff --git a/catbird/node.scm b/catbird/node.scm
new file mode 100644
index 0000000..262147b
--- /dev/null
+++ b/catbird/node.scm
@@ -0,0 +1,181 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Base game node class.
+;;
+;;; Code:
+(define-module (catbird node)
+ #:use-module (catbird asset)
+ #:use-module (catbird cached-slots)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird observer)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:export (<node>
+ in-view?
+ tree-in-view?
+ children
+ for-each-child
+ on-boot
+ reboot
+ child-ref &
+ attach-to
+ replace
+ blink)
+ #:re-export (agenda
+ detach
+ hide
+ name
+ parent
+ on-enter
+ on-hide
+ on-exit
+ on-pause
+ on-resume
+ on-show
+ pause
+ paused?
+ rank
+ render
+ resume
+ run-script
+ show
+ stop-scripts
+ visible?
+ update))
+
+(define-class <node>
+ (<renderable> <scriptable> <containable> <nameable> <rankable>
+ <observer> <asset-container>)
+ ;; An integer value that determines priority order for
+ ;; updating/rendering.
+ (rank #:getter rank #:init-value 0 #:init-keyword #:rank)
+ ;; List of children, sorted by rank.
+ (children #:accessor children #:init-value '())
+ ;; Children indexed by name for fast lookup.
+ (children-by-name #:getter children-by-name #:init-thunk make-hash-table))
+
+(define-method (initialize (node <node>) initargs)
+ (next-method)
+ (on-boot node))
+
+(define-method (on-boot (node <node>))
+ #t)
+
+(define-method (reboot (node <node>))
+ (for-each-child detach node)
+ (with-agenda (agenda node) (reset-agenda))
+ (on-boot node))
+
+(define-method (write (node <node>) port)
+ (define (strip-angle-brackets str)
+ (let ((start (if (string-prefix? "<" str) 1 0))
+ (end (if (string-suffix? ">" str)
+ (- (string-length str) 1)
+ (string-length str))))
+ (substring str start end)))
+ (format port "#<~a name: ~a>"
+ (strip-angle-brackets
+ (symbol->string
+ (class-name (class-of node))))
+ (name node)))
+
+(define (for-each-child proc node)
+ (for-each proc (children node)))
+
+(define-method (update/around (node <node>) dt)
+ (unless (paused? node)
+ ;; Update children first, recursively.
+ (for-each-child (lambda (child) (update/around child dt)) node)
+ (next-method)))
+
+(define-method (tree-in-view? (node <node>))
+ #t)
+
+(define-method (in-view? (node <node>))
+ #t)
+
+(define-method (render/around (node <node>) alpha)
+ (when (and (visible? node) (tree-in-view? node))
+ (next-method)
+ (for-each-child (lambda (child) (render/around child alpha)) node)))
+
+(define-method (child-ref (parent <node>) name)
+ (hashq-ref (children-by-name parent) name))
+
+(define-syntax &
+ (syntax-rules ()
+ ((_ parent child-name)
+ (child-ref parent 'child-name))
+ ((_ parent child-name . rest)
+ (& (child-ref parent 'child-name) . rest))))
+
+(define-method (attach-to (new-parent <node>) . new-children)
+ ;; Validate all the nodes first. The whole operation will fail if
+ ;; any of them cannot be attached.
+ (for-each (lambda (child)
+ (when (parent child)
+ (raise-exception
+ (make-exception-with-message "node already has a parent")))
+ (when (child-ref new-parent (name child))
+ (raise-exception
+ (make-exception-with-message "node name taken"))))
+ new-children)
+ ;; Add named children to the name index for quick lookup later.
+ (for-each (lambda (child)
+ (when (name child)
+ (hashq-set! (children-by-name new-parent) (name child) child)))
+ new-children)
+ ;; Add the new children and sort them by their rank so that
+ ;; updating/rendering happens in the desired order.
+ (set! (children new-parent)
+ (sort-by-rank/ascending (append new-children (children new-parent))))
+ ;; Attach children to the parent, triggering initial enter/attach
+ ;; hooks.
+ (for-each (lambda (child)
+ (attach child new-parent))
+ new-children))
+
+(define-method (replace (parent-node <node>) . replacements)
+ (for-each (lambda (replacement)
+ (let ((old (child-ref parent-node (name replacement))))
+ (when old
+ (detach old))))
+ replacements)
+ (apply attach-to parent-node replacements))
+
+(define-method (detach (node <node>))
+ (let ((p (parent node)))
+ ;; Remove child from parent.
+ (set! (children p) (delq node (children p)))
+ ;; Remove from name index.
+ (when (name node)
+ (hashq-remove! (children-by-name p) (name node)))
+ (next-method)))
+
+(define-method (blink (node <node>) times interval)
+ (let loop ((i 0))
+ (when (< i times)
+ (set! (visible? node) #f)
+ (sleep interval)
+ (set! (visible? node) #t)
+ (sleep interval)
+ (loop (+ i 1)))))
diff --git a/catbird/observer.scm b/catbird/observer.scm
new file mode 100644
index 0000000..1408fc2
--- /dev/null
+++ b/catbird/observer.scm
@@ -0,0 +1,58 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Slots that notify instances upon modification.
+;;
+;;; Code:
+(define-module (catbird observer)
+ #:use-module (catbird config)
+ #:use-module (oop goops)
+ #:export (<observer>
+ on-change))
+
+;; This is a hack to deal with the fact that specializing GOOPS
+;; accessors does not compose with inheritance.
+;;
+;; See
+;; https://dthompson.us/issues-with-object-oriented-programming-in-guile.html
+;; for details.
+
+(define-class <observer-slot-class> (<catbird-metaclass>))
+
+(define-generic on-change)
+
+(define-method (observer-slot? (slot <slot>))
+ (get-keyword #:observe? (slot-definition-options slot)))
+
+(define-method (compute-setter-method (class <observer-slot-class>) slot)
+ (if (observer-slot? slot)
+ ;; Wrap the original setter procedure with a new procedure that
+ ;; calls the on-change method.
+ (make <method>
+ #:specializers (list class <top>)
+ #:procedure (let ((slot-name (slot-definition-name slot))
+ (proc (method-procedure (next-method))))
+ (lambda (obj new)
+ (let ((old (and (slot-bound? obj slot-name)
+ (slot-ref obj slot-name))))
+ (proc obj new)
+ (on-change obj slot-name old new)))))
+ (next-method)))
+
+(define-class <observer> ()
+ #:metaclass <observer-slot-class>)
diff --git a/catbird/overlay.scm b/catbird/overlay.scm
new file mode 100644
index 0000000..a0563f1
--- /dev/null
+++ b/catbird/overlay.scm
@@ -0,0 +1,137 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; System overlay scene for notifications and developer tools.
+;;
+;;; Code:
+(define-module (catbird overlay)
+ #:use-module (catbird kernel)
+ #:use-module (catbird input-map)
+ #:use-module (catbird minibuffer)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird repl)
+ #:use-module (catbird scene)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (chickadee scripting)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:export (make-overlay
+ notify
+ open-minibuffer))
+
+(define %background-color (make-color 0.2 0.2 0.2 0.8))
+
+(define-class <overlay> (<scene>))
+
+(define (make-overlay)
+ (make <overlay>))
+
+(define-method (notify (scene <overlay>) message)
+ (run-script scene
+ (let* ((padding 8.0)
+ (label (make <label>
+ #:name 'message
+ #:rank 1
+ #:position (vec2 padding padding)
+ #:text message))
+ (region (car (regions scene)))
+ (bg (make <canvas>
+ #:name 'background
+ #:painter
+ (with-style ((fill-color %background-color))
+ (fill
+ (rounded-rectangle (vec2 0.0 0.0)
+ (+ (width label) padding padding)
+ (+ (height label) padding)
+ #:radius 2.0)))))
+ (notification (make <node-2d>
+ #:position (vec2 padding
+ (- (height (camera region))
+ (height bg)
+ padding)))))
+ (attach-to notification bg label)
+ (attach-to scene notification)
+ (sleep 5.0)
+ (detach notification))))
+
+(define-method (open-minibuffer)
+ (let ((r (find-region-by-name 'overlay)))
+ (push-major-mode (scene r) (make <minibuffer-mode>))))
+
+(define-class <fps-display> (<node-2d>))
+
+(define-method (on-boot (fps-display <fps-display>))
+ (let* ((font (default-font))
+ (padding 4.0)
+ (box-width (+ (font-line-width font "999.9")
+ (* padding 2.0)))
+ (box-height (+ (font-line-height font) padding)))
+ (attach-to fps-display
+ (make <canvas>
+ #:name 'background
+ #:painter
+ (with-style ((fill-color (make-color 0 0 0 0.5)))
+ (fill
+ (rectangle (vec2 0.0 0.0)
+ box-width
+ box-height))))
+ (make <label>
+ #:name 'label
+ #:rank 1
+ #:font font
+ #:position (vec2 padding padding)))
+ (set! (width fps-display) box-width)
+ (set! (height fps-display) box-height)
+ (set! (origin-y fps-display) box-height)
+ (update-fps fps-display)
+ (run-script fps-display
+ (forever
+ (sleep 1.0)
+ (update-fps fps-display)))))
+
+(define-method (update-fps (fps-display <fps-display>))
+ (set! (text (& fps-display label))
+ (format #f "~1,1f" (frames-per-second))))
+
+(define-minibuffer-command show-fps
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r))))
+ (when (and s (not (& s fps-display)))
+ (attach-to s (make <fps-display>
+ #:name 'fps-display
+ #:rank 99
+ #:position (vec2 0.0 (area-height r)))))))
+
+(define-minibuffer-command hide-fps
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r)))
+ (f (and s (& s fps-display))))
+ (when f (detach f))))
+
+(define-minibuffer-command repl
+ (let* ((r (find-region-by-name 'overlay))
+ (s (and r (scene r))))
+ (when s
+ (push-major-mode s (make <repl-mode>)))))
+
+(bind-input/global (key-press 'x '(alt)) open-minibuffer)
diff --git a/catbird/region.scm b/catbird/region.scm
new file mode 100644
index 0000000..b666811
--- /dev/null
+++ b/catbird/region.scm
@@ -0,0 +1,124 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Rectangular sub-regions of the game window to which a scene can be
+;; rendered.
+;;
+;;; Code:
+(define-module (catbird region)
+ #:use-module (catbird camera)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird node)
+ #:use-module (catbird scene)
+ #:use-module (chickadee)
+ #:use-module (chickadee data array-list)
+ #:use-module (chickadee graphics engine)
+ #:use-module (chickadee graphics viewport)
+ #:use-module (chickadee math rect)
+ #:use-module (ice-9 exceptions)
+ #:use-module (oop goops)
+ #:export (<region>
+ make-region
+ area
+ area-x
+ area-y
+ area-width
+ area-height
+ camera
+ scene
+ replace-scene
+ push-scene
+ pop-scene)
+ #:re-export (name
+ rank
+ render
+ update))
+
+(define-root-class <region> (<renderable> <updatable> <nameable> <rankable>)
+ (area #:accessor area #:init-keyword #:area)
+ (camera #:accessor camera #:init-keyword #:camera #:init-value #f)
+ (scene #:accessor scene #:init-keyword #:scene #:init-value #f)
+ (scene-stack #:getter scene-stack #:init-thunk make-array-list)
+ (viewport #:accessor viewport))
+
+(define-method (area-x (region <region>))
+ (rect-x (area region)))
+
+(define-method (area-y (region <region>))
+ (rect-y (area region)))
+
+(define-method (area-width (region <region>))
+ (rect-width (area region)))
+
+(define-method (area-height (region <region>))
+ (rect-height (area region)))
+
+(define (float->int x)
+ (inexact->exact (round x)))
+
+(define-method (initialize (region <region>) args)
+ (next-method)
+ (let ((r (area region)))
+ (set! (viewport region)
+ (make-viewport (float->int (rect-x r))
+ (float->int (rect-y r))
+ (float->int (rect-width r))
+ (float->int (rect-height r))))))
+
+(define (make-region area name rank)
+ (let* ((window (current-window))
+ (w (window-width window))
+ (h (window-height window)))
+ (when (or (< (rect-left area) 0.0)
+ (< (rect-bottom area) 0.0)
+ (> (rect-right area) w)
+ (> (rect-top area) h))
+ (raise-exception
+ (make-exception-with-message "region area exceeds window area")))
+ (make <region> #:area area #:name name #:rank rank)))
+
+(define-method (replace-scene (r <region>) (new-scene <scene>))
+ (let ((old-scene (scene r)))
+ (when old-scene (on-exit old-scene))
+ (set! (scene r) new-scene)
+ (set! (regions new-scene) (cons r (regions new-scene)))
+ (on-enter new-scene)))
+
+(define-method (push-scene (region <region>) (new-scene <scene>))
+ (let ((old-scene (scene region)))
+ (when old-scene
+ (array-list-push! (scene-stack region) old-scene))
+ (replace-scene region new-scene)))
+
+(define-method (pop-scene (region <region>))
+ (let ((stack (scene-stack region)))
+ (unless (array-list-empty? stack)
+ (replace-scene (array-list-pop! stack)))))
+
+(define-method (update (region <region>) dt)
+ (let ((s (scene region)))
+ (when s (update/around s dt))))
+
+(define-method (render (region <region>) alpha)
+ (let ((s (scene region))
+ (c (camera region)))
+ (when (and s c)
+ (parameterize ((current-camera c))
+ (with-projection (projection-matrix (camera region))
+ (render/around s alpha))))))
diff --git a/catbird/repl.scm b/catbird/repl.scm
new file mode 100644
index 0000000..965e2d8
--- /dev/null
+++ b/catbird/repl.scm
@@ -0,0 +1,371 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; In-engine asynchronous REPL implementation.
+;;
+;;; Code:
+(define-module (catbird repl)
+ #:use-module (catbird line-editor)
+ #:use-module (catbird kernel)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (catbird node-2d)
+ #:use-module (catbird region)
+ #:use-module (catbird ring-buffer)
+ #:use-module (catbird scene)
+ #:use-module (chickadee graphics color)
+ #:use-module (chickadee graphics path)
+ #:use-module (chickadee graphics text)
+ #:use-module (chickadee math vector)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 match)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (system base compile)
+ #:use-module (system base language)
+ #:export (<repl>
+ <repl-mode>
+ resize-repl))
+
+;; TODO: Multiple values
+;; TODO: Multiple expressions
+;; TODO: Debugger
+;; TODO: Switching languages
+
+(define %background-color (make-color 0.0 0.0 0.0 0.9))
+
+(define (make-user-module)
+ (let ((module (resolve-module '(guile-user) #f)))
+ (beautify-user-module! module)
+ module))
+
+(define-class <repl> (<node-2d>)
+ (language #:accessor language #:init-value (lookup-language 'scheme))
+ (module #:accessor module #:init-thunk make-user-module)
+ (max-line-length #:accessor max-line-length #:init-value 256)
+ (log-lines #:accessor log-lines #:init-form (make-ring-buffer 64)))
+
+(define-method (on-boot (repl <repl>))
+ (attach-to repl
+ (make <canvas>
+ #:name 'background)
+ (make <label>
+ #:name 'log
+ #:rank 1)
+ (make <line-editor>
+ #:name 'editor
+ #:rank 1))
+ (log-append repl "Enter ',help' for help.")
+ (refresh-prompt repl))
+
+(define-method (log-append (repl <repl>) line)
+ (ring-buffer-put! (log-lines repl)
+ ;; Truncate long lines
+ (if (> (string-length line) (max-line-length repl))
+ (substring line 0 (max-line-length repl))
+ line))
+ (refresh-log repl))
+
+(define-method (concatenate-log (repl <repl>))
+ (let ((n (- (inexact->exact
+ (floor
+ (/ (height repl)
+ (font-line-height (font (& repl log))))))
+ 1))
+ (lines (log-lines repl)))
+ (string-join (let loop ((i (max (- (ring-buffer-length lines) n) 0)))
+ (if (< i (ring-buffer-length lines))
+ (cons (ring-buffer-ref lines i)
+ (loop (+ i 1)))
+ '()))
+ "\n")))
+
+(define-method (refresh-log (repl <repl>))
+ (let ((log (& repl log)))
+ (set! (text log) (concatenate-log repl))
+ (set! (position-y log) (- (height repl) (height log)))
+ (place-below log (& repl editor))))
+
+(define-method (refresh-prompt (repl <repl>))
+ (set! (prompt (& repl editor))
+ (format #f "~a@~a> "
+ (language-name (language repl))
+ (module-name (module repl)))))
+
+(define-method (resize-repl (repl <repl>) w h)
+ (set! (width repl) w)
+ (set! (height repl) h)
+ (set! (painter (& repl background))
+ (with-style ((fill-color %background-color))
+ (fill
+ (rectangle (vec2 0.0 0.0) w h))))
+ (refresh-log repl))
+
+(define-method (repl-read-expression (repl <repl>) line)
+ (call-with-input-string line
+ (lambda (port)
+ ((language-reader (language repl)) port (module repl)))))
+
+(define-method (with-output-to-log (repl <repl>) thunk)
+ (let* ((val *unspecified*)
+ (str (with-output-to-string
+ (lambda ()
+ (set! val (thunk))))))
+ (unless (string-null? str)
+ (for-each (lambda (line)
+ (log-append repl line))
+ (string-split str #\newline)))
+ val))
+
+(define-method (with-error-handling (repl <repl>) thunk)
+ (let ((stack #f))
+ (define (handle-error e)
+ (let ((frame (stack-ref stack 0)))
+ (log-append repl
+ (format #f "~a: In procedure: ~a:"
+ (match (frame-source frame)
+ ((_ file-name line . column)
+ (format #f "~a:~a:~a"
+ (if file-name
+ (basename file-name)
+ "unknown file")
+ line column))
+ (_ "unknown"))
+ (or (frame-procedure-name frame)
+ "unknown")))
+ (log-append repl
+ (format #f "In procedure: ~a: ~a"
+ (or (and (exception-with-origin? e)
+ (exception-origin e))
+ "unknown")
+ (if (and (exception-with-message? e)
+ (exception-with-irritants? e))
+ (apply format #f (exception-message e)
+ (exception-irritants e))
+ "")))
+ (with-output-to-log repl
+ (lambda ()
+ (display-backtrace stack
+ (current-output-port))))))
+ (define (pre-unwind-handler . args)
+ (set! stack (make-stack #t 4)))
+ (define (throw-handler)
+ (with-throw-handler #t thunk pre-unwind-handler))
+ (define (exception-handler e)
+ (if (quit-exception? e)
+ (raise-exception e)
+ (handle-error e)))
+ (with-exception-handler exception-handler throw-handler #:unwind? #t)))
+
+(define-method (repl-compile (repl <repl>) line)
+ (define (compile-line)
+ (with-output-to-log repl
+ (lambda ()
+ (compile (repl-read-expression repl line)
+ #:from (language repl)
+ #:env (module repl)))))
+ (with-error-handling repl compile-line))
+
+(define-method (write-value-to-log (repl <repl>) x)
+ (unless (unspecified? x)
+ (with-output-to-log repl (lambda () (write x)))))
+
+(define (skip-whitespace str i)
+ (let loop ((i i))
+ (cond
+ ((= i (string-length str))
+ (- i 1))
+ ((char-whitespace? (string-ref str i))
+ (loop (+ i 1)))
+ (else
+ i))))
+
+(define (find-whitespace str i)
+ (let loop ((i i))
+ (cond
+ ((= i (string-length str))
+ i)
+ ((char-whitespace? (string-ref str i))
+ i)
+ (else
+ (loop (+ i 1))))))
+
+(define (meta-command-string? str)
+ (and (not (string-null? str))
+ (eqv? (string-ref str (skip-whitespace str 0)) #\,)))
+
+(define (parse-meta-command str)
+ (let* ((i (skip-whitespace str 0))
+ (j (find-whitespace str i)))
+ (cons (substring str i j)
+ (call-with-input-string (substring str j)
+ (lambda (port)
+ (let loop ()
+ (let ((exp (read port)))
+ (if (eof-object? exp)
+ '()
+ (cons exp (loop))))))))))
+
+(define-method (meta-command (repl <repl>) line)
+ (match (parse-meta-command line)
+ ((name args ...)
+ (let ((meta (lookup-meta-command name)))
+ (if meta
+ (with-error-handling repl
+ (lambda ()
+ (apply-meta-command meta repl args)))
+ (log-append repl (string-append "Unknown meta-command: " name)))))))
+
+(define-method (repl-eval (repl <repl>))
+ (let* ((editor (& repl editor))
+ (line (get-line editor)))
+ (save-to-history editor)
+ (log-append repl (string-append (prompt editor) line))
+ (if (meta-command-string? line)
+ (meta-command repl line)
+ (write-value-to-log repl (repl-compile repl line)))
+ (clear-line editor)
+ (refresh-log repl)
+ (refresh-prompt repl)))
+
+
+;;;
+;;; Meta commands
+;;;
+
+(define-record-type <meta-command>
+ (make-meta-command name aliases category docstring proc)
+ meta-command?
+ (name meta-command-name)
+ (aliases meta-command-aliases)
+ (category meta-command-category)
+ (docstring meta-command-docstring)
+ (proc meta-command-proc))
+
+(define (apply-meta-command meta repl args)
+ (apply (meta-command-proc meta) repl args))
+
+(define *meta-commands* '())
+
+(define (lookup-meta-command name)
+ (find (lambda (m)
+ (or (string=? (meta-command-name m) name)
+ (any (lambda (alias)
+ (string=? alias name))
+ (meta-command-aliases m))))
+ *meta-commands*))
+
+(define (add-meta-command! name aliases category docstring proc)
+ (set! *meta-commands*
+ (cons (make-meta-command name aliases category docstring proc)
+ *meta-commands*)))
+
+(define (symbol->meta-command sym)
+ (string-append "," (symbol->string sym)))
+
+(define-syntax define-meta-command
+ (syntax-rules ()
+ ((_ ((name aliases ...) category repl args ...) docstring body ...)
+ (add-meta-command! (symbol->meta-command 'name)
+ (map symbol->meta-command '(aliases ...))
+ 'category
+ docstring
+ (lambda* (repl args ...)
+ body ...)))
+ ((_ (name category repl args ...) docstring body ...)
+ (add-meta-command! (symbol->meta-command 'name)
+ '()
+ 'category
+ docstring
+ (lambda* (repl args ...)
+ body ...)))))
+
+(define-meta-command (help help repl)
+ "- Show this help information."
+ (for-each (lambda (m)
+ (match (meta-command-aliases m)
+ (()
+ (log-append repl
+ (format #f "~a ~a"
+ (meta-command-name m)
+ (meta-command-docstring m))))
+ (aliases
+ (log-append repl
+ (format #f "~a ~a ~a"
+ (meta-command-name m)
+ aliases
+ (meta-command-docstring m))))))
+ (sort *meta-commands*
+ (lambda (a b)
+ (string<? (meta-command-name a)
+ (meta-command-name b))))))
+
+(define-meta-command ((quit q) system repl)
+ "- Quit program."
+ (exit-catbird))
+
+(define-meta-command ((import use) module repl module-name)
+ "MODULE - Import a module."
+ (module-use! (module repl) (resolve-module module-name)))
+
+(define-meta-command ((module m) module repl #:optional module-name)
+ "[MODULE] - Change current module or show current module."
+ (if module-name
+ (log-append repl (format #f "~a" (module-name (module repl))))
+ (set! (module repl) (resolve-module module-name))))
+
+
+;;;
+;;; REPL major mode
+;;;
+
+(define-class <repl-mode> (<major-mode>)
+ (prev-keyboard-focus #:accessor prev-keyboard-focus #:init-value #f))
+
+(define (repl mode)
+ (& (parent mode) repl))
+
+(define-method (on-enter (mode <repl-mode>))
+ (let* ((scene (parent mode))
+ (region (car (regions scene)))
+ (repl (or (& (parent mode) repl)
+ (make <repl>
+ #:name 'repl))))
+ (unless (parent repl)
+ (attach-to (parent mode) repl))
+ (show repl)
+ (resize-repl repl (area-width region) (area-height region))
+ (set! (prev-keyboard-focus mode) (current-keyboard-focus))
+ (take-keyboard-focus region)
+ (add-minor-mode scene (make <line-edit-mode>
+ #:editor (& repl editor)))))
+
+(define-method (close-repl (mode <repl-mode>))
+ (let ((scene (parent mode)))
+ (hide (& scene repl))
+ (take-keyboard-focus (prev-keyboard-focus mode))
+ (remove-minor-mode (parent mode) <line-edit-mode>)
+ (pop-major-mode scene)))
+
+(define-method (eval-expression (mode <repl-mode>))
+ (repl-eval (repl mode)))
+
+(bind-input <repl-mode> (key-press 'escape) close-repl)
+(bind-input <repl-mode> (key-press 'g '(ctrl)) close-repl)
+(bind-input <repl-mode> (key-press 'return) eval-expression)
diff --git a/catbird/ring-buffer.scm b/catbird/ring-buffer.scm
new file mode 100644
index 0000000..7632c2c
--- /dev/null
+++ b/catbird/ring-buffer.scm
@@ -0,0 +1,85 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Ring buffer data structure.
+;;
+;;; Code:
+(define-module (catbird ring-buffer)
+ #:use-module (srfi srfi-9)
+ #:export (make-ring-buffer
+ ring-buffer
+ ring-buffer-length
+ ring-buffer-put!
+ ring-buffer-get!
+ ring-buffer-ref
+ ring-buffer-clear!))
+
+(define-record-type <ring-buffer>
+ (%make-ring-buffer vector length head tail)
+ ring-buffer?
+ (vector ring-buffer-vector)
+ (length ring-buffer-length set-ring-buffer-length!)
+ (head ring-buffer-head set-ring-buffer-head!)
+ (tail ring-buffer-tail set-ring-buffer-tail!))
+
+(define (make-ring-buffer size)
+ (%make-ring-buffer (make-vector size #f) 0 0 0))
+
+(define (ring-buffer-empty? ring)
+ (zero? (ring-buffer-length ring)))
+
+(define (ring-buffer-put! ring x)
+ (let* ((head (ring-buffer-head ring))
+ (tail (ring-buffer-tail ring))
+ (l (ring-buffer-length ring))
+ (v (ring-buffer-vector ring))
+ (vl (vector-length v)))
+ (vector-set! v tail x)
+ (set-ring-buffer-length! ring (min (+ l 1) vl))
+ (when (and (> l 0) (= head tail))
+ (set-ring-buffer-head! ring (modulo (+ head 1) vl)))
+ (set-ring-buffer-tail! ring (modulo (+ tail 1) vl))))
+
+(define (ring-buffer-get! ring)
+ (if (ring-buffer-empty? ring)
+ (error "ring buffer empty" ring)
+ (let* ((head (ring-buffer-head ring))
+ (v (ring-buffer-vector ring))
+ (result (vector-ref v head)))
+ (vector-set! v head #f)
+ (set-ring-buffer-head! ring (modulo (+ head 1) (vector-length v)))
+ (set-ring-buffer-length! ring (- (ring-buffer-length ring) 1))
+ result)))
+
+(define (ring-buffer-ref ring i)
+ (let ((l (ring-buffer-length ring))
+ (v (ring-buffer-vector ring)))
+ (if (>= i l)
+ (error "ring buffer index out of bounds" i)
+ (vector-ref v (modulo (+ (ring-buffer-head ring) i)
+ (vector-length v))))))
+
+(define (ring-buffer-clear! ring)
+ (let ((v (ring-buffer-vector ring)))
+ (set-ring-buffer-head! ring 0)
+ (set-ring-buffer-tail! ring 0)
+ (set-ring-buffer-length! ring 0)
+ (let loop ((i 0))
+ (when (< i (vector-length v))
+ (vector-set! v i #f)
+ (loop (+ i 1))))))
diff --git a/catbird/scene.scm b/catbird/scene.scm
new file mode 100644
index 0000000..9197e47
--- /dev/null
+++ b/catbird/scene.scm
@@ -0,0 +1,169 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Game scenes encapsulate a tree of nodes and the state machine
+;; (modes) that manipulate those nodes to create a playable game.
+;;
+;;; Code:
+(define-module (catbird scene)
+ #:use-module (catbird config)
+ #:use-module (catbird mixins)
+ #:use-module (catbird mode)
+ #:use-module (catbird node)
+ #:use-module (chickadee data array-list)
+ #:use-module (ice-9 exceptions)
+ #:use-module (ice-9 format)
+ #:use-module (oop goops)
+ #:use-module (srfi srfi-1)
+ #:export (<scene>
+ regions
+ major-mode
+ minor-modes
+ replace-major-mode
+ push-major-mode
+ pop-major-mode
+ add-minor-mode
+ remove-minor-mode))
+
+(define-root-class <scene> (<node>)
+ (regions #:accessor regions #:init-value '())
+ (major-mode #:accessor major-mode #:init-keyword #:major-mode
+ #:init-form (make <nothing-mode>))
+ (major-mode-stack #:getter major-mode-stack #:init-thunk make-array-list)
+ (minor-modes #:accessor minor-modes #:init-value '())
+ (input-map #:getter input-map #:init-value '()))
+
+(define-method (initialize (scene <scene>) args)
+ (next-method)
+ (attach (major-mode scene) scene))
+
+(define-method (replace-major-mode (scene <scene>) (mode <major-mode>))
+ (let ((old-mode (major-mode scene)))
+ (when old-mode
+ (detach old-mode))
+ (set! (major-mode scene) mode)
+ (attach mode scene)))
+
+(define-method (push-major-mode (scene <scene>) (mode <major-mode>))
+ (let ((old-mode (major-mode scene)))
+ (array-list-push! (major-mode-stack scene) old-mode)
+ (when old-mode
+ (pause old-mode))
+ (set! (major-mode scene) mode)
+ (attach mode scene)))
+
+(define-method (pop-major-mode (scene <scene>))
+ (let ((stack (major-mode-stack scene)))
+ (unless (array-list-empty? stack)
+ (let ((mode (major-mode scene))
+ (prev-mode (array-list-pop! stack)))
+ (when mode
+ (detach mode))
+ (set! (major-mode scene) prev-mode)
+ (resume prev-mode)))))
+
+(define-method (add-minor-mode (scene <scene>) (mode <minor-mode>))
+ (when (parent mode)
+ (raise-exception
+ (make-exception-with-message "mode already attached to a scene")))
+ (set! (minor-modes scene) (cons mode (minor-modes scene)))
+ (attach mode scene))
+
+(define-method (remove-minor-mode (scene <scene>) (mode <minor-mode>))
+ (unless (eq? scene (parent mode))
+ (raise-exception
+ (make-exception-with-message "mode not attached to scene")))
+ (let ((modes (minor-modes scene)))
+ (set! (minor-modes scene) (delq mode modes))
+ (detach mode)))
+
+(define-method (remove-minor-mode (scene <scene>) (mode-class <class>))
+ (let ((mode (find (lambda (mode)
+ (eq? (class-of mode) mode-class))
+ (minor-modes scene))))
+ (when mode
+ (remove-minor-mode scene mode))))
+
+(define-method (search-modes (scene <scene>) proc)
+ (or (proc (major-mode scene))
+ (find (lambda (mode)
+ (proc mode))
+ (minor-modes scene))))
+
+(define-method (on-key-press (scene <scene>) key modifiers)
+ (search-modes scene
+ (lambda (mode)
+ (on-key-press mode key modifiers))))
+
+(define-method (on-key-release (scene <scene>) key modifiers)
+ (search-modes scene
+ (lambda (mode)
+ (on-key-release mode key modifiers))))
+
+(define-method (on-text-input (scene <scene>) text)
+ (search-modes scene
+ (lambda (mode)
+ (on-text-input mode text))))
+
+(define-method (on-mouse-press (scene <scene>) button x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-press mode button x y))))
+
+(define-method (on-mouse-release (scene <scene>) button x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-release mode button x y))))
+
+(define-method (on-mouse-move (scene <scene>) x y x-rel y-rel buttons)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-move mode x y x-rel y-rel buttons))))
+
+(define-method (on-mouse-wheel (scene <scene>) x y)
+ (search-modes scene
+ (lambda (mode)
+ (on-mouse-wheel mode x y))))
+
+(define-method (on-controller-press (scene <scene>) controller-id button)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-press mode controller-id button))))
+
+(define-method (on-controller-release (scene <scene>) controller-id button)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-release mode controller-id button))))
+
+(define-method (on-controller-move (scene <scene>) controller-id axis value)
+ (search-modes scene
+ (lambda (mode)
+ (on-controller-move mode controller-id axis value))))
+
+(define-method (update (scene <scene>) dt)
+ (update (major-mode scene) dt)
+ (for-each (lambda (mode) (update mode dt))
+ (minor-modes scene)))
+
+(define-method (pause (scene <scene>))
+ (for-each-child pause scene)
+ (next-method))
+
+(define-method (resume (scene <scene>))
+ (for-each-child resume scene)
+ (next-method))
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..e8809e4
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,18 @@
+AC_INIT(catbird, 0.1.0)
+AC_CONFIG_SRCDIR(catbird)
+AC_CONFIG_AUX_DIR([build-aux])
+AM_INIT_AUTOMAKE([color-tests -Wall -Wno-portability foreign])
+AM_SILENT_RULES([yes])
+
+AC_PATH_PROG([GUILE], [guile])
+AC_CONFIG_FILES([Makefile])
+AC_CONFIG_FILES([pre-inst-env], [chmod +x pre-inst-env])
+AC_CONFIG_FILES([test-env], [chmod +x test-env])
+
+GUILE_PKG([3.0])
+GUILE_PROGS
+
+GUILE_MODULE_REQUIRED([chickadee])
+GUILE_MODULE_REQUIRED([sdl2])
+
+AC_OUTPUT
diff --git a/guix.scm b/guix.scm
new file mode 100644
index 0000000..33438a1
--- /dev/null
+++ b/guix.scm
@@ -0,0 +1,180 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Development environment for GNU Guix.
+;;
+;; To setup a development environment and build, run:
+;;
+;; guix shell
+;; ./bootstrap
+;; ./configure
+;; make -j${nproc}
+;;
+;; To build the development snapshot, run:
+;;
+;; guix build -f guix.scm
+;;
+;; To install the development snapshot, run:
+;;
+;; guix install -f guix.scm
+;;
+;;; Code:
+(use-modules (ice-9 match)
+ (srfi srfi-1)
+ (guix build-system gnu)
+ (guix download)
+ (guix gexp)
+ (guix git-download)
+ ((guix licenses) #:prefix license:)
+ (guix packages)
+ (guix utils)
+ (gnu packages)
+ (gnu packages audio)
+ (gnu packages autotools)
+ (gnu packages fontutils)
+ (gnu packages gl)
+ (gnu packages guile)
+ (gnu packages image)
+ (gnu packages maths)
+ (gnu packages mp3)
+ (gnu packages pkg-config)
+ (gnu packages readline)
+ (gnu packages sdl)
+ (gnu packages texinfo)
+ (gnu packages xiph))
+
+(define target-guile guile-3.0-latest)
+
+(define guile3.0-opengl
+ (package
+ (inherit guile-opengl)
+ (inputs
+ (modify-inputs (package-inputs guile-opengl)
+ (replace "guile" target-guile)))
+ (native-inputs
+ (modify-inputs (package-native-inputs guile-opengl)
+ (append autoconf automake)))
+ (arguments
+ (substitute-keyword-arguments (package-arguments guile-opengl)
+ ((#:phases phases)
+ `(modify-phases ,phases
+ (delete 'patch-makefile)
+ (add-before 'bootstrap 'patch-configure.ac
+ (lambda _
+ ;; The Guile version check doesn't work for the 3.0
+ ;; pre-release, so just remove it.
+ (substitute* "configure.ac"
+ (("GUILE_PKG\\(\\[2.2 2.0\\]\\)") ""))
+ (substitute* "Makefile.am"
+ (("\\$\\(GUILE_EFFECTIVE_VERSION\\)") "3.0")
+ (("ccache") "site-ccache"))
+ #t))
+ (replace 'bootstrap
+ (lambda _
+ (invoke "autoreconf" "-vfi")))))))))
+
+(define guile-sdl2
+ (let ((commit "e9a7f5e748719ce5b6ccd08ff91861b578034ea6"))
+ (package
+ (name "guile-sdl2")
+ (version (string-append "0.7.0-1." (string-take commit 7)))
+ (source (origin
+ (method git-fetch)
+ (uri (git-reference
+ (url "https://git.dthompson.us/guile-sdl2.git")
+ (commit commit)))
+ (sha256
+ (base32
+ "0ay7mcar8zs0j5rihwlzi0l46vgg9i93piip4v8a3dzwjx3myr7v"))))
+ (build-system gnu-build-system)
+ (arguments
+ '(#:make-flags '("GUILE_AUTO_COMPILE=0")
+ #:phases
+ (modify-phases %standard-phases
+ (add-after 'unpack 'bootstrap
+ (lambda _
+ (invoke "sh" "bootstrap"))))))
+ (native-inputs (list autoconf automake pkg-config texinfo))
+ (inputs (list target-guile sdl2))
+ (synopsis "Guile bindings for SDL2")
+ (description "Guile-sdl2 provides pure Guile Scheme bindings to the
+SDL2 C shared library via the foreign function interface.")
+ (home-page "https://git.dthompson.us/guile-sdl2.git")
+ (license license:lgpl3+))))
+
+(define chickadee
+ (let ((commit "5edce04c698cd92149004ead1cad77c481c682e8"))
+ (package
+ (name "chickadee")
+ (version (string-append "0.8.0-1." (string-take commit 7)))
+ (source (origin
+ (method git-fetch)
+ (uri (git-reference
+ (url "https://git.dthompson.us/chickadee.git")
+ (commit commit)))
+ (sha256
+ (base32
+ "1hckx827aw3af8cbw1mfjy57wdssv1q3bs2hziymxddipwa0d425"))))
+ (build-system gnu-build-system)
+ (arguments
+ '(#:make-flags '("GUILE_AUTO_COMPILE=0")
+ #:phases
+ (modify-phases %standard-phases
+ (add-after 'unpack 'bootstrap
+ (lambda _
+ (invoke "sh" "bootstrap"))))))
+ (native-inputs (list autoconf automake pkg-config texinfo))
+ (inputs (list freetype
+ libjpeg-turbo
+ libpng
+ libvorbis
+ mpg123
+ openal
+ readline
+ target-guile))
+ (propagated-inputs (list guile3.0-opengl guile-sdl2))
+ (synopsis "Game development toolkit for Guile Scheme")
+ (description "Chickadee is a game development toolkit for Guile
+Scheme. It contains all of the basic components needed to develop
+2D/3D video games.")
+ (home-page "https://dthompson.us/projects/chickadee.html")
+ (license license:gpl3+))))
+
+(define %source-dir (dirname (current-filename)))
+
+(package
+ (name "catbird")
+ (version "0.1")
+ (source (local-file %source-dir
+ #:recursive? #t
+ #:select? (git-predicate %source-dir)))
+ (build-system gnu-build-system)
+ (arguments
+ '(#:make-flags '("GUILE_AUTO_COMPILE=0")
+ #:phases
+ (modify-phases %standard-phases
+ (add-after 'unpack 'bootstrap
+ (lambda _
+ (invoke "sh" "bootstrap"))))))
+ (native-inputs (list autoconf automake pkg-config texinfo))
+ (inputs (list target-guile))
+ (propagated-inputs (list chickadee guile-sdl2))
+ (synopsis "Game engine for Scheme programmers")
+ (description "Catbird is a game engine written in Guile Scheme.")
+ (home-page "https://dthompson.us/projects/chickadee.html")
+ (license license:gpl3+))
diff --git a/pre-inst-env.in b/pre-inst-env.in
new file mode 100644
index 0000000..ada1d01
--- /dev/null
+++ b/pre-inst-env.in
@@ -0,0 +1,34 @@
+#!/bin/sh
+
+# Catbird Game Engine
+# Copyright © 2022 David Thompson <davet@gnu.org>
+#
+# Catbird 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.
+#
+# Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+abs_top_srcdir="`cd "@abs_top_srcdir@" > /dev/null; pwd`"
+abs_top_builddir="`cd "@abs_top_builddir@" > /dev/null; pwd`"
+
+export GUILE_LOAD_COMPILED_PATH="$abs_top_builddir${GUILE_LOAD_COMPILED_PATH:+:}$GUILE_LOAD_COMPILED_PATH"
+export GUILE_LOAD_PATH="$abs_top_builddir:$abs_top_srcdir${GUILE_LOAD_PATH:+:}:$GUILE_LOAD_PATH"
+
+if [ -n "$LOCAL_CHICKADEE" ]
+then
+ export GUILE_LOAD_PATH=$LOCAL_CHICKADEE:$GUILE_LOAD_PATH
+ export GUILE_LOAD_COMPILED_PATH=$LOCAL_CHICKADEE:$GUILE_LOAD_COMPILED_PATH
+ export CHICKADEE_DATADIR=$LOCAL_CHICKADEE/data
+fi
+
+export PATH="$abs_top_builddir/scripts:$PATH"
+
+exec "$@"
diff --git a/test-env.in b/test-env.in
new file mode 100644
index 0000000..1ab197a
--- /dev/null
+++ b/test-env.in
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+"@abs_top_builddir@/pre-inst-env" "$@"
+
+exit $?
diff --git a/tests/utils.scm b/tests/utils.scm
new file mode 100644
index 0000000..8346d71
--- /dev/null
+++ b/tests/utils.scm
@@ -0,0 +1,30 @@
+;;; Catbird Game Engine
+;;; Copyright © 2022 David Thompson <davet@gnu.org>
+;;;
+;;; Catbird 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.
+;;;
+;;; Catbird 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 Catbird. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Testing utilities.
+;;
+;;; Code:
+(define-module (tests utils)
+ #:use-module (srfi srfi-64)
+ #:export (with-tests))
+
+(define-syntax-rule (with-tests name body ...)
+ (begin
+ (test-begin name)
+ body ...
+ (exit (zero? (test-runner-fail-count (test-end))))))