Add Bocfel interpreter
authorPhilip Chimento <philip.chimento@gmail.com>
Sun, 16 Sep 2012 17:52:07 +0000 (19:52 +0200)
committerPhilip Chimento <philip.chimento@gmail.com>
Sun, 16 Sep 2012 17:52:07 +0000 (19:52 +0200)
The Bocfel interpreter is now available for playing Z-machine files.

47 files changed:
configure.ac
interpreters/Makefile.am
interpreters/bocfel/BUILDING [new file with mode: 0644]
interpreters/bocfel/COPYING.GPLv2 [new file with mode: 0644]
interpreters/bocfel/COPYING.GPLv3 [new file with mode: 0644]
interpreters/bocfel/Makefile.am [new file with mode: 0644]
interpreters/bocfel/README [new file with mode: 0644]
interpreters/bocfel/blorb.c [new file with mode: 0644]
interpreters/bocfel/blorb.h [new file with mode: 0644]
interpreters/bocfel/branch.c [new file with mode: 0644]
interpreters/bocfel/branch.h [new file with mode: 0644]
interpreters/bocfel/dict.c [new file with mode: 0644]
interpreters/bocfel/dict.h [new file with mode: 0644]
interpreters/bocfel/glkstart.c [new file with mode: 0644]
interpreters/bocfel/iff.c [new file with mode: 0644]
interpreters/bocfel/iff.h [new file with mode: 0644]
interpreters/bocfel/io.c [new file with mode: 0644]
interpreters/bocfel/io.h [new file with mode: 0644]
interpreters/bocfel/math.c [new file with mode: 0644]
interpreters/bocfel/math.h [new file with mode: 0644]
interpreters/bocfel/memory.c [new file with mode: 0644]
interpreters/bocfel/memory.h [new file with mode: 0644]
interpreters/bocfel/objects.c [new file with mode: 0644]
interpreters/bocfel/objects.h [new file with mode: 0644]
interpreters/bocfel/osdep.c [new file with mode: 0644]
interpreters/bocfel/osdep.h [new file with mode: 0644]
interpreters/bocfel/process.c [new file with mode: 0644]
interpreters/bocfel/process.h [new file with mode: 0644]
interpreters/bocfel/random.c [new file with mode: 0644]
interpreters/bocfel/random.h [new file with mode: 0644]
interpreters/bocfel/screen.c [new file with mode: 0644]
interpreters/bocfel/screen.h [new file with mode: 0644]
interpreters/bocfel/stack.c [new file with mode: 0644]
interpreters/bocfel/stack.h [new file with mode: 0644]
interpreters/bocfel/table.c [new file with mode: 0644]
interpreters/bocfel/table.h [new file with mode: 0644]
interpreters/bocfel/unicode.c [new file with mode: 0644]
interpreters/bocfel/unicode.h [new file with mode: 0644]
interpreters/bocfel/util.c [new file with mode: 0644]
interpreters/bocfel/util.h [new file with mode: 0644]
interpreters/bocfel/zoom.c [new file with mode: 0644]
interpreters/bocfel/zoom.h [new file with mode: 0644]
interpreters/bocfel/zterp.c [new file with mode: 0644]
interpreters/bocfel/zterp.h [new file with mode: 0644]
libchimara/chimara-if.c
libchimara/chimara-if.h
player/preferences.c

index 391b91f4e3c0b7bb6deec17b50e23fe4a09bf909..54b947f25e9ddbf4a68d3992fd15afff55322b93 100644 (file)
@@ -169,6 +169,7 @@ interpreters/frotz/Makefile
 interpreters/nitfol/Makefile
 interpreters/glulxe/Makefile
 interpreters/git/Makefile
 interpreters/nitfol/Makefile
 interpreters/glulxe/Makefile
 interpreters/git/Makefile
+interpreters/bocfel/Makefile
 tests/Makefile
 player/Makefile
 player/config.py
 tests/Makefile
 player/Makefile
 player/config.py
index 0b1cb2e11122dd9bc68e7b9ccc42d5333c5db126..cd8e930fdbc0e1bf02914c6d46d32cf4c13b36bb 100644 (file)
@@ -1,3 +1,3 @@
-SUBDIRS=nitfol frotz glulxe git
+SUBDIRS=nitfol frotz glulxe git bocfel
 
 -include $(top_srcdir)/git.mk
 
 -include $(top_srcdir)/git.mk
diff --git a/interpreters/bocfel/BUILDING b/interpreters/bocfel/BUILDING
new file mode 100644 (file)
index 0000000..30411e8
--- /dev/null
@@ -0,0 +1,135 @@
+GNU make is required, and all of the examples herein assume that GNU make is
+being used.  If your native make is not GNU, substitute gmake for make.
+
+A C99 compiler is required to build Bocfel; or at least a compiler that supports
+the C99 features that are used.  These include, but are probably not limited to,
+VLAs, snprintf(), mixed declarations and code, fixed-width integers, and
+compound literals.
+
+Officially supported compilers are handled in compiler.mk.  If you get Bocfel
+working with another compiler, I would be happy to add it to compiler.mk.  Note
+that Bocfel has a few requirements (beyond C99) from its compilers.  Compilers
+are expected to support the following rather standard Unix command-line
+arguments, and are expected to work as linkers, as well:
+-D - define a macro
+-L - define a library search path
+-l - specify a library to link
+-o - specify the output file name
+-g - build with debugging symbols
+-c - compile but don't link, producing a .o file from a .c file
+-O - Optimize (but this can be changed via the $OPT variable)
+
+Compilers are also expected to understand optimization flags while they are
+linking; modern compilers can do link-time optimization, and generally require
+the same flags when compiling and linking (the system linker is usually not
+smart enough to handle the link-time optimized object files).
+
+The character set used is required to be compatible with ASCII.  This is not a
+particularly onerous requirement; but if I ever have access to a system that
+supports a wildly different character set, I would be willing to think about
+extending support beyond ASCII.  For now, though, it is much easier to assume
+ASCII, because almost everybody uses a character set that is compatible with it,
+including the Z-machine.
+
+A 32-bit (or greater) system is probably a requirement.  I have tried to avoid
+the assumption that "int" is 32 bits, and it is entirely possible that I have
+succeeded and a system with 16-bit ints would work properly.  However, there are
+some lookup tables that are 65K each, which would probably cause fits for 16-bit
+systems.  As with the ASCII requirement, if I ever happen upon a 16-bit system
+with a C99 compiler, I will probably try to get Bocfel working with it. 
+
+I make use of fixed-width types such as uint8_t and uint16_t.  As a practical
+side-effect, Bocfel requires a system with 8-, 16-, and 32-bit 2's complement
+integers.  This is likely not a problem.
+
+-------------------------------------------------------------------------------
+
+There are two main types of build: Glk and non-Glk.  Glk builds use libraries
+based on Andrew Plotkin's Glk standard for I/O.  Glk builds can be full-
+featured, including timed input and cursor control, allowing games like Border
+Zone and Seastalker to run as intended.  Non-Glk builds use nothing but standard
+C functions.  This has the advantage of running on systems where Glk has not
+been ported, but the disadvantage of missing important features.  Non-Glk builds
+are generally not useful.
+
+The first thing to do is edit config.mk and set $PLATFORM to the proper value.
+See the comments in that file for an explanation.  There are other variables
+that may be set through config.mk, each of which has comments explaining its
+use.
+
+To build a non-Glk interpreter, simply run:
+make GLK=
+
+The $GLK variable may also be set through config.mk.
+
+Glk builds are slightly more involved.  For most Glk libraries (e.g. glkterm(w),
+xglk, cheapglk, Windows Glk), you will want to unpack the source distribution
+into the current directory (the one containing Bocfel's source), and then build
+it.  After this is done, simply run
+make GLK=glktermw
+
+to build a Glk-enabled interpreter (where glktermw is the name of the directory
+into which the Glk library was unpacked).  Note that the Windows Glk
+distribution does not unpack into its own directory, so you will want to change
+into the winglk directory before unpacking it.
+
+The presence of a file called Make.<glk_library_name> in the Glk library
+directory is required.  Most Glk libraries will include this, but at least
+Windows Glk does not.  I have included a Make.winglk that should be sufficient
+to build a Windows Glk-enabled interpreter, at least with MinGW.
+
+Bocfel can also be built against Gargoyle's Glk implementation, taking full
+advantage of extra features it provides.  Ben Cressey, Gargoyle's maintainer,
+has imported Bocfel into the Gargoyle source repository, so the easiest way to
+obtain Gargoyle support is to check out the latest version of Gargoyle's source
+code; see http://code.google.com/p/garglk/source/checkout.  If you would prefer
+to use Bocfel's build system to build against Gargoyle as you would any other
+Glk library, read on.
+
+The build process for Gargoyle is slightly more involved.  Gargoyle includes a
+full-featured Glk library, but it is not designed for external use.  However,
+getting a Gargoyle- (or rather, a garglk-) enabled build is not too difficult.
+
+The first step is to get and install the Jam build tool from
+http://www.perforce.com/jam/jam.html.  Your vendor may provide a jam package or
+port; consult its documentation.
+
+Next, get a copy of the Gargoyle source code from http://garglk.googlecode.com/.
+Extract it somewhere (it need not be in the current directory).  Enter the
+directory and run jam.  From the build/<youros>.release/garglk directory, copy
+the files libgarglk.so and libgarglkmain.a to the directory "gargoyle" (which
+contains a Make.gargoyle file) in the Bocfel source distribution.  Then copy the
+files garglk/glk.h, garglk/glkstart.h, and garglk/gi_blorb.h from Gargoyle to
+Bocfel's gargoyle directory.  You can now build a garglk-enabled interpreter:
+make GLK=gargoyle
+
+If you do not already have Gargoyle installed, you will need to install the
+library libgarglk.so somewhere in your library search path.  I would, however,
+recommend just installing Gargoyle.  If you do this, you will want to build the
+source code of the same version that you install.  If you don't, it's possible
+(although unlikely) that ABI or API changes in Gargoyle will cause problems.
+
+Bocfel is built and tested against the development version of Gargoyle, so
+ideally this would be the version to build against.  However, Gargoyle does, by
+and large, implement a standard API, so the version should not make a large
+difference.  There are a few Gargoyle-specific extensions that can possibly
+change, but they have been stable apart from one change over the past couple of
+years.  The current (2010.1) release of Gargoyle should work, but the only
+officially supported version is whatever was current as of this version of
+Bocfel's release.
+
+-------------------------------------------------------------------------------
+
+Glk is very portable, and there is very little system-specific code that is
+needed.  However, there is a small amount.  Some libraries are, conventionally,
+considered to be Unix (cheapglk, xglk, and glkterm(w)); one Windows (Windows
+Glk, naturally).  Bocfel needs to know what kind of platform it is running on in
+order to provide a few system-specific functions, and it uses this information
+to decide what kind of Glk library is in use.  While it is theoretically
+possible (for example) for a Unix-based Glk library to use a non Unix-style Glk
+startup, I have no desire to require users to set both their OS and Glk
+platforms.  Thus a Windows user who desires to use cheapglk (assuming it builds
+on Windows) is out of luck, for example.  I consider this to be acceptable
+because I expect garglk to be the preferred Glk implementation, and even if it's
+not, most Glk uses will work.  When building with garglk, Unix-style Glk startup
+is forced, because this is what garglk uses, regardless of platform.
diff --git a/interpreters/bocfel/COPYING.GPLv2 b/interpreters/bocfel/COPYING.GPLv2
new file mode 100644 (file)
index 0000000..a3f6b12
--- /dev/null
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                          59 Temple Place - Suite 330, Boston, MA
+                          02111-1307, USA.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  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.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+                    END OF TERMS AND CONDITIONS
+\f
+       Appendix: 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
+convey 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) 19yy  <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 2 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, write to the Free Software
+    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision 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, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This 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 Library General
+Public License instead of this License.
diff --git a/interpreters/bocfel/COPYING.GPLv3 b/interpreters/bocfel/COPYING.GPLv3
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
@@ -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/interpreters/bocfel/Makefile.am b/interpreters/bocfel/Makefile.am
new file mode 100644 (file)
index 0000000..ecb779e
--- /dev/null
@@ -0,0 +1,15 @@
+pkglib_LTLIBRARIES = bocfel.la
+bocfel_la_SOURCES = blorb.c blorb.h branch.c branch.h dict.c dict.h glkstart.c \
+       iff.c iff.h io.c io.h math.c math.h memory.c memory.h objects.c objects.h \
+       osdep.c osdep.h process.c process.h random.c random.h screen.c screen.h \
+       stack.c stack.h table.c table.h unicode.c unicode.h util.c util.h zoom.c \
+       zoom.h zterp.c zterp.h
+bocfel_la_CPPFLAGS = -DZTERP_GLK -DZTERP_UNIX \
+       -I$(top_srcdir) -I$(top_srcdir)/libchimara
+bocfel_la_CFLAGS = -std=c99 $(AM_CFLAGS)
+bocfel_la_LDFLAGS = -module $(PLUGIN_LIBTOOL_FLAGS)
+
+bocfeldocdir = $(datadir)/doc/$(PACKAGE)/bocfel
+dist_bocfeldoc_DATA = BUILDING COPYING.GPLv2 COPYING.GPLv3 README
+
+-include $(top_srcdir)/git.mk
diff --git a/interpreters/bocfel/README b/interpreters/bocfel/README
new file mode 100644 (file)
index 0000000..1d22cd3
--- /dev/null
@@ -0,0 +1,18 @@
+Bocfel is an interpreter for the Z-machine, which means that it plays text
+adventure games.  For more information on the Z-machine and interpreters, please
+see http://en.wikipedia.org/wiki/Z-machine.
+
+For building instructions, please consult the BUILDING file.
+
+If you are poking around the source code, you might notice a lot of identifiers
+containing "zterp".  When I first started this project, I put it in my
+Subversion repository, but I wasn't sure it would ever go anywhere.  I quickly
+picked a rather generic name--zterp--because a name was needed for the
+repository path.  It turns out, however, that long ago somebody released a
+Z-machine interpreter called zterp, so I had to find something new.  By the time
+I realized this, however, the name "zterp" had become entrenched in the source.
+I've changed all user-visible uses of "zterp" to "bocfel", but have kept the old
+identifiers in the source.
+
+Chris Spiegel <cspiegel@gmail.com>
+http://bocfel.googlecode.com/
diff --git a/interpreters/bocfel/blorb.c b/interpreters/bocfel/blorb.c
new file mode 100644 (file)
index 0000000..456e976
--- /dev/null
@@ -0,0 +1,121 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+#include "blorb.h"
+#include "iff.h"
+#include "io.h"
+
+struct zterp_blorb
+{
+  struct zterp_io *io;
+  struct zterp_iff *iff;
+
+  size_t nchunks;
+  zterp_blorb_chunk *chunks;
+};
+
+struct zterp_blorb *zterp_blorb_parse(zterp_io *io)
+{
+  uint32_t size;
+  uint32_t nresources;
+  zterp_iff *iff;
+  struct zterp_blorb *blorb = NULL;
+
+  iff = zterp_iff_parse(io, "IFRS");
+  if(!zterp_iff_find(iff, "RIdx", &size)) goto err;
+  zterp_iff_free(iff);
+
+  if(!zterp_io_read32(io, &nresources)) goto err;
+
+  if((nresources * 12) + 4 != size) goto err;
+
+  blorb = malloc(sizeof *blorb);
+  if(blorb == NULL) goto err;
+
+  blorb->io = io;
+  blorb->nchunks = 0;
+  blorb->chunks = NULL;
+
+  for(uint32_t i = 0; i < nresources; i++)
+  {
+    uint32_t usage, number, start, type;
+    zterp_blorb_chunk *new;
+    long saved;
+    uint32_t idx;
+
+    if(!zterp_io_read32(io, &usage) || !zterp_io_read32(io, &number) || !zterp_io_read32(io, &start)) goto err;
+
+    if(usage != BLORB_PICT && usage != BLORB_SND && usage != BLORB_EXEC) goto err;
+
+    saved = zterp_io_tell(io);
+    if(saved == -1) goto err;
+
+    if(zterp_io_seek(io, start, SEEK_SET) == -1) goto err;
+
+    if(!zterp_io_read32(io, &type) || !zterp_io_read32(io, &size)) goto err;
+
+    if(zterp_io_seek(io, saved, SEEK_SET) == -1) goto err;
+
+    if(type == STRID("FORM"))
+    {
+      start -= 8;
+      size += 8;
+    }
+
+    /* Not really efficient, but does it matter? */
+    new = realloc(blorb->chunks, sizeof *new * ++blorb->nchunks);
+    if(new == NULL) goto err;
+    blorb->chunks = new;
+
+    idx = blorb->nchunks - 1;
+
+    new[idx].usage = usage;
+    new[idx].number = number;
+    new[idx].type = type;
+    memcpy(new[idx].name, IDSTR(type), 5);
+    new[idx].offset = start + 8;
+    new[idx].size = size;
+  }
+
+  return blorb;
+
+err:
+  zterp_blorb_free(blorb);
+
+  return NULL;
+}
+
+void zterp_blorb_free(struct zterp_blorb *blorb)
+{
+  if(blorb != NULL) free(blorb->chunks);
+  free(blorb);
+}
+
+const zterp_blorb_chunk *zterp_blorb_find(struct zterp_blorb *blorb, uint32_t usage, int number)
+{
+  for(size_t i = 0; i < blorb->nchunks; i++)
+  {
+    if(blorb->chunks[i].usage == usage && blorb->chunks[i].number == number) return &blorb->chunks[i];
+  }
+
+  return NULL;
+}
diff --git a/interpreters/bocfel/blorb.h b/interpreters/bocfel/blorb.h
new file mode 100644 (file)
index 0000000..654becc
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef ZTERP_BLORB_H
+#define ZTERP_BLORB_H
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "io.h"
+
+#define BLORB_PICT     0x50696374
+#define BLORB_SND      0x536e6420
+#define BLORB_EXEC     0x45786563
+
+typedef struct zterp_blorb zterp_blorb;
+
+typedef struct
+{
+  uint32_t usage;
+  int number;
+  uint32_t type;
+  char name[5];
+  uint32_t offset;
+  uint32_t size;
+} zterp_blorb_chunk;
+
+zterp_blorb *zterp_blorb_parse(zterp_io *);
+void zterp_blorb_free(zterp_blorb *);
+const zterp_blorb_chunk *zterp_blorb_find(zterp_blorb *, uint32_t, int);
+
+#endif
diff --git a/interpreters/bocfel/branch.c b/interpreters/bocfel/branch.c
new file mode 100644 (file)
index 0000000..2af3d29
--- /dev/null
@@ -0,0 +1,91 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+
+#include "branch.h"
+#include "memory.h"
+#include "process.h"
+#include "stack.h"
+#include "util.h"
+#include "zterp.h"
+
+void branch_if(int do_branch)
+{
+  uint8_t branch;
+  uint16_t offset;
+
+  branch = BYTE(pc++);
+
+  if(!do_branch) branch ^= 0x80;
+
+  offset = branch & 0x3f;
+
+  if((branch & 0x40) == 0)
+  {
+    offset = (offset << 8) | BYTE(pc++);
+
+    /* Get the sign right. */
+    if(offset & 0x2000) offset |= 0xc000;
+  }
+
+  if(branch & 0x80)
+  {
+    if(offset > 1)
+    {
+      pc += (int16_t)offset - 2;
+      ZASSERT(pc < memory_size, "branch to invalid address 0x%lx", (unsigned long)pc);
+    }
+    else
+    {
+      do_return(offset);
+    }
+  }
+}
+
+void zjump(void)
+{
+  /* -= 2 because pc has been advanced past the jump instruction. */
+  pc += (int16_t)zargs[0];
+  pc -= 2;
+
+  ZASSERT(pc < memory_size, "@jump to invalid address 0x%lx", (unsigned long)pc);
+}
+
+void zjz(void)
+{
+  branch_if(zargs[0] == 0);
+}
+
+void zje(void)
+{
+  if     (znargs == 1) branch_if(0);
+  else if(znargs == 2) branch_if(zargs[0] == zargs[1]);
+  else if(znargs == 3) branch_if(zargs[0] == zargs[1] || zargs[0] == zargs[2]);
+  else                 branch_if(zargs[0] == zargs[1] || zargs[0] == zargs[2] || zargs[0] == zargs[3]);
+}
+
+void zjl(void)
+{
+  branch_if((int16_t)zargs[0] < (int16_t)zargs[1]);
+}
+
+void zjg(void)
+{
+  branch_if((int16_t)zargs[0] > (int16_t)zargs[1]);
+}
diff --git a/interpreters/bocfel/branch.h b/interpreters/bocfel/branch.h
new file mode 100644 (file)
index 0000000..709860c
--- /dev/null
@@ -0,0 +1,12 @@
+#ifndef ZTERP_BRANCH_H
+#define ZTERP_BRANCH_H
+
+void branch_if(int);
+
+void zjump(void);
+void zjz(void);
+void zje(void);
+void zjl(void);
+void zjg(void);
+
+#endif
diff --git a/interpreters/bocfel/dict.c b/interpreters/bocfel/dict.c
new file mode 100644 (file)
index 0000000..f184ddb
--- /dev/null
@@ -0,0 +1,319 @@
+/*-
+ * Copyright 2009-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "dict.h"
+#include "memory.h"
+#include "process.h"
+#include "unicode.h"
+#include "util.h"
+#include "zterp.h"
+
+static uint16_t separators;
+static uint8_t num_separators;
+
+static uint16_t GET_WORD(uint8_t *base)
+{
+  return (base[0] << 8) | base[1];
+}
+static void MAKE_WORD(uint8_t *base, uint16_t val)
+{
+  base[0] = val >> 8;
+  base[1] = val & 0xff;
+}
+
+/* Add the character c to the nth position of the encoded text.  c is a
+ * 5-bit value (either a shift character, which selects an alphabet, or
+ * the index into the current alphabet).
+ */
+static void add_zchar(int c, int n, uint8_t *encoded)
+{
+  uint16_t w = GET_WORD(&encoded[2 * (n / 3)]);
+
+  /* From §3.2:
+   * --first byte-------   --second byte---
+   * 7    6 5 4 3 2  1 0   7 6 5  4 3 2 1 0
+   * bit  --first--  --second---  --third--
+   *
+   * So to figure out which third of the word to store to:
+   * If n is 0, 3, 6, ... then store to the first (left shift 10).
+   * If n is 1, 4, 7, ... then store to the second (left shift 5).
+   * If n is 2, 5, 8, ... then store to the third (left shift 0).
+   * “Or” into the previous value because, unless this is the first
+   * character, there are already values we’ve stored there.
+   */
+  w |= (c & 0x1f) << (5 * (2 - (n % 3)));
+
+  MAKE_WORD(&encoded[2 * (n / 3)], w);
+}
+
+/* Encode the text at “s”, of length “len” (there is not necessarily a
+ * terminating null character) into the buffer “encoded”.
+ *
+ * For V3 the encoded text is 6 Z-characters (4 bytes); for V4 and above
+ * it’s 9 characters (6 bytes).  Due to the nature of the loop here,
+ * it’s possible to encode too many bytes.  For example, if the string
+ * given is "aaa<" in a V3 game, the three 'a' characters will take up a
+ * word (all three being packed into one), but the single '<' character
+ * will take up two words (one full word and a third of the next) due to
+ * the fact that '<' is not in the alphabet table.  Thus the encoded
+ * text will be 7 characters. This is OK because routines that use the
+ * encoded string are smart enough to only pay attention to the first 6
+ * or 9 Z-characters; and partial Z-characters are OK per §3.6.1.
+ *
+ * 1.1 of the standard revises the encoding for V1 and V2 games.  I am
+ * not implementing the new rules for two basic reasons:
+ * 1) It apparently only affects three (unnecessary) dictionary words in
+ *    the known V1-2 games.
+ * 2) Because of 1, it is not worth the effort to peek ahead and see
+ *    what the next character is to determine whether to shift once or
+ *    to lock.
+ *
+ * Z-character 0 is a space (§3.5.1), so theoretically a space should be
+ * encoded simply with a zero.  However, Inform 6.32 encodes space
+ * (which has the value 32) as a 10-bit ZSCII code, which is the
+ * Z-characters 5, 6, 1, 0.  Assume this is correct.
+ */
+static void encode_string(const uint8_t *s, size_t len, uint8_t encoded[8])
+{
+  int n = 0;
+  const int res = zversion <= 3 ? 6 : 9;
+  const int shiftbase = zversion <= 2 ? 1 : 3;
+
+  memset(encoded, 0, 8);
+
+  for(size_t i = 0; i < len && n < res; i++)
+  {
+    int pos;
+
+    pos = atable_pos[s[i]];
+    if(pos >= 0)
+    {
+      int shift = pos / 26;
+      int c = pos % 26;
+
+      if(shift) add_zchar(shiftbase + shift, n++, encoded);
+      add_zchar(c + 6, n++, encoded);
+    }
+    else
+    {
+      add_zchar(shiftbase + 2, n++, encoded);
+      add_zchar(6, n++, encoded);
+      add_zchar(s[i] >> 5, n++, encoded);
+      add_zchar(s[i] & 0x1f, n++, encoded);
+    }
+  }
+
+  while(n < res)
+  {
+    add_zchar(5, n++, encoded);
+  }
+
+  /* §3.2: the MSB of the last encoded word must be set. */
+  if(zversion <= 3) encoded[2] |= 0x80;
+  else              encoded[4] |= 0x80;
+}
+
+static int dict_compar(const void *a, const void *b)
+{
+  return memcmp(a, b, zversion <= 3 ? 4 : 6);
+}
+static uint16_t dict_find(const uint8_t *token, size_t len, uint16_t dictionary)
+{
+  uint8_t elength;
+  uint16_t base;
+  long nentries;
+  uint8_t *ret = NULL;
+  uint8_t encoded[8];
+
+  encode_string(token, len, encoded);
+
+  elength = user_byte(dictionary + num_separators + 1);
+  nentries = (int16_t)user_word(dictionary + num_separators + 2);
+  base = dictionary + num_separators + 2 + 2;
+
+  ZASSERT(elength >= (zversion <= 3 ? 4 : 6), "dictionary entry length (%d) too small", elength);
+  ZASSERT(base + (labs(nentries) * elength) < memory_size, "reported dictionary length extends beyond memory size");
+
+  if(nentries > 0)
+  {
+    ret = bsearch(encoded, &memory[base], nentries, elength, dict_compar);
+  }
+  else
+  {
+    for(long i = 0; i < -nentries; i++)
+    {
+      uint8_t *entry = &memory[base + (i * elength)];
+
+      if(dict_compar(encoded, entry) == 0)
+      {
+        ret = entry;
+        break;
+      }
+    }
+  }
+
+  if(ret == NULL) return 0;
+
+  return base + (ret - &memory[base]);
+}
+
+static int is_sep(uint8_t c)
+{
+  if(c == ZSCII_SPACE) return 1;
+
+  for(uint16_t i = 0; i < num_separators; i++) if(user_byte(separators + i) == c) return 1;
+
+  return 0;
+}
+
+static void handle_token(const uint8_t *base, const uint8_t *token, int len, uint16_t parse, uint16_t dictionary, int found, int flag)
+{
+  uint16_t d;
+  const uint8_t examine[] = { 'e', 'x', 'a', 'm', 'i', 'n', 'e' };
+  const uint8_t again[] = { 'a', 'g', 'a', 'i', 'n' };
+  const uint8_t wait[] = { 'w', 'a', 'i', 't' };
+
+  d = dict_find(token, len, dictionary);
+
+  if(!options.disable_abbreviations && base == token && len == 1)
+  {
+    if     (*token == 'x') d = dict_find(examine, sizeof examine, dictionary);
+    else if(*token == 'g') d = dict_find(again, sizeof again, dictionary);
+    else if(*token == 'z') d = dict_find(wait, sizeof wait, dictionary);
+  }
+
+  if(flag && d == 0) return;
+
+  parse = parse + 2 + (found * 4);
+
+  user_store_word(parse, d);
+
+  user_store_byte(parse + 2, len);
+
+  if(zversion <= 4) user_store_byte(parse + 3, token - base + 1);
+  else              user_store_byte(parse + 3, token - base + 2);
+}
+
+/* The behavior of tokenize is described in §15 (under the read opcode)
+ * and §13.
+ *
+ * For the text buffer, byte 0 is ignored in both V3/4 and V5+.
+ * Byte 1 of V3/4 is the start of the string, while in V5+ it is the
+ * length of the string.
+ * Byte 2 of V5+ is the start of the string.  V3/4 strings have a null
+ * terminator, while V5+ do not.
+ *
+ * For the parse buffer, byte 0 contains the maximum number of tokens
+ * that can be read.
+ * The number of tokens found is stored in byte 1.
+ * Each token is then represented by a 4-byte chunk with the following
+ * information:
+ * • The first two bytes are the byte address of the dictionary entry
+ *   for the token, or 0 if the token was not found in the dictionary.
+ * • The next byte is the length of the token.
+ * • The final byte is the offset in the string of the token.
+ */
+void tokenize(uint16_t text, uint16_t parse, uint16_t dictionary, int flag)
+{
+  const uint8_t *p, *lastp;
+  uint8_t *string;
+  uint32_t text_len = 0;
+  const int maxwords = user_byte(parse);
+  int in_word = 0;
+  int found = 0;
+
+  if(dictionary == 0) dictionary = header.dictionary;
+
+  ZASSERT(dictionary != 0, "attempt to tokenize without a valid dictionary");
+
+  num_separators = user_byte(dictionary);
+  separators = dictionary + 1;
+
+  if(zversion >= 5) text_len = user_byte(text + 1);
+  else              while(user_byte(text + 1 + text_len) != 0) text_len++;
+
+  ZASSERT(text + 1 + (zversion >= 5) + text_len < memory_size, "attempt to tokenize out-of-bounds string");
+
+  string = &memory[text + 1 + (zversion >= 5)];
+
+  for(p = string; p - string < text_len && *p == ZSCII_SPACE; p++);
+  lastp = p;
+
+  text_len -= (p - string);
+
+  do
+  {
+    if(!in_word && text_len != 0 && !is_sep(*p))
+    {
+      in_word = 1;
+      lastp = p;
+    }
+
+    if(text_len == 0 || is_sep(*p))
+    {
+      if(in_word)
+      {
+        handle_token(string, lastp, p - lastp, parse, dictionary, found++, flag);
+      }
+
+      /* §13.6.1: Separators (apart from a space) are tokens too. */
+      if(text_len != 0 && *p != ZSCII_SPACE)
+      {
+        handle_token(string, p, 1, parse, dictionary, found++, flag);
+      }
+
+      if(found == maxwords) break;
+
+      in_word = 0;
+    }
+
+    p++;
+
+  } while(text_len--);
+
+  user_store_byte(parse + 1, found);
+}
+
+static void encode_text(uint32_t text, uint16_t len, uint16_t coded)
+{
+  uint8_t encoded[8];
+
+  ZASSERT(text + len < memory_size, "reported text length extends beyond memory size");
+
+  encode_string(&memory[text], len, encoded);
+
+  for(int i = 0; i < 6; i++) user_store_byte(coded + i, encoded[i]);
+}
+
+void ztokenise(void)
+{
+  if(znargs < 3) zargs[2] = 0;
+  if(znargs < 4) zargs[3] = 0;
+
+  tokenize(zargs[0], zargs[1], zargs[2], zargs[3]);
+}
+
+void zencode_text(void)
+{
+  encode_text(zargs[0] + zargs[2], zargs[1], zargs[3]);
+}
diff --git a/interpreters/bocfel/dict.h b/interpreters/bocfel/dict.h
new file mode 100644 (file)
index 0000000..e1c8c02
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef DICTIONARY_H
+#define DICTIONARY_H
+
+#include <stdint.h>
+
+void tokenize(uint16_t, uint16_t, uint16_t, int);
+
+void ztokenise(void);
+void zencode_text(void);
+
+#endif
diff --git a/interpreters/bocfel/glkstart.c b/interpreters/bocfel/glkstart.c
new file mode 100644 (file)
index 0000000..89cfb27
--- /dev/null
@@ -0,0 +1,120 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glk.h>
+#include <libchimara/garglk.h>
+
+/* Even on Win32, Gargoyle provides a glkunix startup. */
+#if defined(ZTERP_UNIX) || defined(GARGLK)
+#include <string.h>
+
+#include <glkstart.h>
+
+#include "util.h"
+#include "zterp.h"
+
+zexternally_visible
+glkunix_argumentlist_t glkunix_arguments[] =
+{
+  { "-a",      glkunix_arg_NumberValue,        "-a N           set the size of the evaluation stack" },
+  { "-A",      glkunix_arg_NumberValue,        "-A N           set the size of the call stack" },
+  { "-c",      glkunix_arg_NoValue,            "-c             disable color" },
+  { "-C",      glkunix_arg_NoValue,            "-C             disable the use of a config file" },
+  { "-d",      glkunix_arg_NoValue,            "-d             disable timed input" },
+  { "-D",      glkunix_arg_NoValue,            "-D             disable sound effects" },
+  { "-e",      glkunix_arg_NoValue,            "-e             enable ANSI escapes in the transcript" },
+  { "-E",      glkunix_arg_ValueFollows,       "-E string      set the escape string for -e" },
+  { "-f",      glkunix_arg_NoValue,            "-f             disable fixed-width fonts" },
+  { "-F",      glkunix_arg_NoValue,            "-F             assume font is fixed-width" },
+  { "-g",      glkunix_arg_NoValue,            "-g             disable the character graphics font" },
+  { "-G",      glkunix_arg_NoValue,            "-G             enable alternative box-drawing character graphics" },
+  { "-i",      glkunix_arg_NoValue,            "-i             display the id of the story file and exit" },
+  { "-k",      glkunix_arg_NoValue,            "-k             disable the use of terminating keys (notably used in Beyond Zork)" },
+  { "-l",      glkunix_arg_NoValue,            "-l             disable utf-8 transcripts" },
+  { "-L",      glkunix_arg_NoValue,            "-L             force utf-8 transcrips" },
+  { "-m",      glkunix_arg_NoValue,            "-m             disable meta commands" },
+  { "-n",      glkunix_arg_NumberValue,        "-n N           set the interpreter number (see 11.1.3 in The Z-machine Standards Document 1.0)" },
+  { "-N",      glkunix_arg_NumberValue,        "-N N           set the interpreter version (see 11.1.3.1 in The Z-machine Standards Document 1.0)" },
+  { "-r",      glkunix_arg_NoValue,            "-r             start the story by replaying a command record" },
+  { "-R",      glkunix_arg_NoValue,            "-R filename    set the filename to be used if replaying a command record" },
+  { "-s",      glkunix_arg_NoValue,            "-s             start the story with command recording on" },
+  { "-S",      glkunix_arg_NoValue,            "-S filename    set the filename to be used if command recording is turned on" },
+  { "-t",      glkunix_arg_NoValue,            "-t             start the story with transcripting on" },
+  { "-T",      glkunix_arg_ValueFollows,       "-T filename    set the filename to be used if transcription is turned on" },
+  { "-u",      glkunix_arg_NumberValue,        "-u N           set the maximum number of undo slots" },
+  { "-U",      glkunix_arg_NoValue,            "-U             disable compression in undo slots" },
+  { "-v",      glkunix_arg_NoValue,            "-v             display version information" },
+  { "-x",      glkunix_arg_NoValue,            "-x             disable expansion of abbreviations" },
+  { "-X",      glkunix_arg_NoValue,            "-X             enable tandy censorship" },
+  { "-y",      glkunix_arg_NoValue,            "-y             when opening a transcript, overwrite rather than append to an existing file" },
+  { "-z",      glkunix_arg_NumberValue,        "-z N           set initial random seed" },
+  { "-Z",      glkunix_arg_ValueFollows,       "-Z device      read initial random seed from device" },
+  { "",                glkunix_arg_ValueFollows,       "filename       file to load" },
+
+  { NULL, glkunix_arg_End, NULL }
+};
+
+zexternally_visible
+int glkunix_startup_code(glkunix_startup_t *data)
+{
+  if(!process_arguments(data->argc, data->argv)) return 0;
+
+#ifdef GARGLK
+  garglk_set_program_name("Bocfel");
+  if(game_file != NULL)
+  {
+    char *p = strrchr(game_file, '/');
+    garglk_set_story_name(p == NULL ? game_file : p + 1);
+  }
+#endif
+
+  return 1;
+}
+#elif defined(ZTERP_WIN32)
+#include <windows.h>
+
+#include <WinGlk.h>
+
+#include "util.h"
+
+int InitGlk(unsigned int);
+
+zexternally_visible
+int WINAPI WinMain(HINSTANCE instance, HINSTANCE previnstance, LPSTR cmdline, int cmdshow)
+{
+  /* This works (with a linker message) under MinGW, but I don’t
+   * know if it’s supposed to; I am unfamiliar with how Windows
+   * handles command-line arguments.
+   */
+  extern int __argc;
+  extern char **__argv;
+
+  if(!InitGlk(0x00000700)) exit(EXIT_FAILURE);
+
+  if(!process_arguments(__argc, __argv)) exit(EXIT_FAILURE);
+
+  winglk_app_set_name("Bocfel");
+
+  glk_main();
+  glk_exit();
+
+  return 0;
+}
+#else
+#error Glk on this platform is not supported.
+#endif
diff --git a/interpreters/bocfel/iff.c b/interpreters/bocfel/iff.c
new file mode 100644 (file)
index 0000000..e7fe4e1
--- /dev/null
@@ -0,0 +1,112 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "iff.h"
+#include "io.h"
+
+struct zterp_iff
+{
+  zterp_io *io;
+  uint32_t tag;
+  long offset;
+  uint32_t size;
+
+  struct zterp_iff *next;
+};
+
+void zterp_iff_free(zterp_iff *iff)
+{
+  while(iff != NULL)
+  {
+    zterp_iff *tmp = iff->next;
+    free(iff);
+    iff = tmp;
+  }
+}
+
+zterp_iff *zterp_iff_parse(zterp_io *io, const char type[4])
+{
+  uint32_t tag;
+
+  zterp_iff *iff = NULL, *tail = NULL;
+
+  if(zterp_io_seek(io, 0, SEEK_SET) == -1) goto err;
+
+  if(!zterp_io_read32(io, &tag) || tag != STRID("FORM")) goto err;
+
+  if(zterp_io_seek(io, 4, SEEK_CUR) == -1) goto err;
+
+  if(!zterp_io_read32(io, &tag) || tag != STRID(type)) goto err;
+
+  while(zterp_io_read32(io, &tag))
+  {
+    uint32_t size;
+    zterp_iff *new;
+
+    if(!zterp_io_read32(io, &size)) goto err;
+
+    new = malloc(sizeof *new);
+    if(new == NULL) goto err;
+
+    new->tag = tag;
+    new->io = io;
+    new->offset = zterp_io_tell(io);
+    new->size = size;
+    new->next = NULL;
+
+    if(iff == NULL) iff = new;
+    else            tail->next = new;
+
+    tail = new;
+
+    if(new->offset == -1) goto err;
+
+    if(size & 1) size++;
+
+    if(zterp_io_seek(io, size, SEEK_CUR) == -1) goto err;
+  }
+
+  return iff;
+
+err:
+  zterp_iff_free(iff);
+
+  return NULL;
+}
+
+int zterp_iff_find(zterp_iff *iff, const char tag[4], uint32_t *size)
+{
+  while(iff != NULL)
+  {
+    if(iff->tag == STRID(tag))
+    {
+      if(zterp_io_seek(iff->io, iff->offset, SEEK_SET) == -1) return 0;
+      *size = iff->size;
+
+      return 1;
+    }
+
+    iff = iff->next;
+  }
+
+  return 0;
+}
diff --git a/interpreters/bocfel/iff.h b/interpreters/bocfel/iff.h
new file mode 100644 (file)
index 0000000..fd63d26
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef ZTERP_IFF_H
+#define ZTERP_IFF_H
+
+#include <stdint.h>
+
+#include "io.h"
+
+typedef struct zterp_iff zterp_iff;
+
+/* Translate an IFF tag into the corresponding 32-bit integer. */
+#define STRID(s) ( \
+    (((uint32_t)(s)[0]) << 24) | \
+    (((uint32_t)(s)[1]) << 16) | \
+    (((uint32_t)(s)[2]) <<  8) | \
+    (((uint32_t)(s)[3]) <<  0)   \
+    )
+
+/* Reverse of above. */
+#define IDSTR(n) ((char[5]){ \
+    ((uint32_t)n >> 24) & 0xff, \
+    ((uint32_t)n >> 16) & 0xff, \
+    ((uint32_t)n >>  8) & 0xff, \
+    ((uint32_t)n >>  0) & 0xff, \
+    })
+
+
+void zterp_iff_free(zterp_iff *);
+zterp_iff *zterp_iff_parse(zterp_io *, const char [4]);
+int zterp_iff_find(zterp_iff *, const char [4], uint32_t *);
+
+#endif
diff --git a/interpreters/bocfel/io.c b/interpreters/bocfel/io.c
new file mode 100644 (file)
index 0000000..d51e65a
--- /dev/null
@@ -0,0 +1,493 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#endif
+
+#include "io.h"
+#include "osdep.h"
+#include "unicode.h"
+
+#define MAX_PATH       4096
+
+int use_utf8_io;
+
+/* Generally speaking, UNICODE_LINEFEED (10) is used as a newline.  Glk
+ * requires this (Glk API 0.7.0 §2.2), and when Unicode is available, we
+ * write characters out by hand even with stdio, so no translation can
+ * be done.  However, when stdio is being used, Unicode is not
+ * available, and the file usage will be for a transcript or
+ * command-script, use '\n' as a newline so translation can be done;
+ * this is the only case where streams are opened in text mode.
+ *
+ * zterp_io_stdio() and zterp_io_stdout() are considered text-mode if
+ * Unicode is not available, binary otherwise.
+ */
+#define textmode(io)   (!use_utf8_io && ((io->mode) & (ZTERP_IO_TRANS | ZTERP_IO_INPUT)))
+
+struct zterp_io
+{
+  enum type { IO_STDIO, IO_GLK } type;
+
+  FILE *fp;
+  int mode;
+#ifdef ZTERP_GLK
+  strid_t file;
+#endif
+};
+
+/* Glk does not like you to be able to pass a full filename to
+ * glk_fileref_create_by_name(); this means that Glk cannot be used to
+ * open arbitrary files.  However, Glk is still required to prompt for
+ * files, such as in a save game situation.  To allow zterp_io to work
+ * for opening files both with and without a prompt, it will use stdio
+ * when either Glk is not available, or when Glk is available but
+ * prompting is not necessary.
+ *
+ * This is needed because the IFF parser is required for both opening
+ * games (zblorb files) and for saving/restoring.  The former needs to
+ * be able to access any file on the filesystem, and the latter needs to
+ * prompt.  This is a headache.
+ *
+ * Prompting is assumed to be necessary if “filename” is NULL.
+ */
+zterp_io *zterp_io_open(const char *filename, int mode)
+{
+  zterp_io *io;
+  char smode[] = "wb";
+
+  fprintf(stderr, "zterp_io_open: '%s'\n", filename);
+
+  io = malloc(sizeof *io);
+  if(io == NULL) goto err;
+  io->mode = mode;
+
+  if     (mode & ZTERP_IO_RDONLY) smode[0] = 'r';
+  else if(mode & ZTERP_IO_APPEND) smode[0] = 'a';
+
+  if(textmode(io)) smode[1] = 0;
+
+#ifdef ZTERP_GLK
+  int usage = fileusage_BinaryMode, filemode;
+
+  if     (mode & ZTERP_IO_SAVE)  usage |= fileusage_SavedGame;
+  else if(mode & ZTERP_IO_TRANS) usage |= fileusage_Transcript;
+  else if(mode & ZTERP_IO_INPUT) usage |= fileusage_InputRecord;
+  else                           usage |= fileusage_Data;
+
+  if     (mode & ZTERP_IO_RDONLY) filemode = filemode_Read;
+  else if(mode & ZTERP_IO_WRONLY) filemode = filemode_Write;
+  else if(mode & ZTERP_IO_APPEND) filemode = filemode_WriteAppend;
+
+  else goto err;
+#else
+  const char *prompt;
+
+  if     (mode & ZTERP_IO_SAVE)  prompt = "Enter filename for save game: ";
+  else if(mode & ZTERP_IO_TRANS) prompt = "Enter filename for transcript: ";
+  else if(mode & ZTERP_IO_INPUT) prompt = "Enter filename for command record: ";
+  else                           prompt = "Enter filename for data: ";
+#endif
+
+  /* No need to prompt. */
+  if(filename != NULL)
+  {
+    io->type = IO_STDIO;
+    io->fp = fopen(filename, smode);
+    if(io->fp == NULL) goto err;
+  }
+  /* Prompt. */
+  else
+  {
+#ifdef ZTERP_GLK
+    frefid_t ref;
+
+    ref = glk_fileref_create_by_prompt(usage, filemode, 0);
+    if(ref == NULL) goto err;
+
+    io->type = IO_GLK;
+    io->file = glk_stream_open_file(ref, filemode, 0);
+    glk_fileref_destroy(ref);
+    if(io->file == NULL) goto err;
+#else
+    char fn[MAX_PATH], *p;
+
+    printf("\n%s", prompt);
+    fflush(stdout);
+    if(fgets(fn, sizeof fn, stdin) == NULL || fn[0] == '\n') goto err;
+    p = strchr(fn, '\n');
+    if(p != NULL) *p = 0;
+
+    io->type = IO_STDIO;
+    io->fp = fopen(fn, smode);
+    if(io->fp == NULL) goto err;
+#endif
+  }
+
+  return io;
+
+err:
+  free(io);
+
+  return NULL;
+}
+
+/* The zterp_os_reopen_binary() calls attempt to reopen stdin/stdout as
+ * binary streams so that reading/writing UTF-8 doesn’t cause unwanted
+ * translations.  The mode of ZTERP_IO_TRANS is set when Unicode is
+ * unavailable as a way to signal that these are text streams.
+ */
+const zterp_io *zterp_io_stdin(void)
+{
+  static zterp_io io;
+
+  if(io.fp == NULL)
+  {
+    io.type = IO_STDIO;
+    io.mode = ZTERP_IO_RDONLY;
+    if(use_utf8_io) zterp_os_reopen_binary(stdin);
+    else            io.mode |= ZTERP_IO_TRANS;
+    io.fp = stdin;
+  }
+
+  return &io;
+}
+
+const zterp_io *zterp_io_stdout(void)
+{
+  static zterp_io io;
+
+  if(io.fp == NULL)
+  {
+    io.type = IO_STDIO;
+    io.mode = ZTERP_IO_WRONLY;
+    if(use_utf8_io) zterp_os_reopen_binary(stdout);
+    else            io.mode |= ZTERP_IO_TRANS;
+    io.fp = stdout;
+  }
+
+  return &io;
+}
+
+void zterp_io_close(zterp_io *io)
+{
+#ifdef ZTERP_GLK
+  if(io->type == IO_GLK)
+  {
+    glk_stream_close(io->file, NULL);
+  }
+  else
+#endif
+  {
+    fclose(io->fp);
+  }
+
+  free(io);
+}
+
+int zterp_io_seek(const zterp_io *io, long offset, int whence)
+{
+  /* To smooth over differences between Glk and standard I/O, don’t
+   * allow seeking in append-only streams.
+   */
+  if(io->mode & ZTERP_IO_APPEND) return -1;
+
+#ifdef ZTERP_GLK
+  if(io->type == IO_GLK)
+  {
+    glk_stream_set_position(io->file, offset, whence == SEEK_SET ? seekmode_Start : whence == SEEK_CUR ? seekmode_Current : seekmode_End);
+    return 0; /* dammit */
+  }
+  else
+#endif
+  {
+    return fseek(io->fp, offset, whence);
+  }
+}
+
+long zterp_io_tell(const zterp_io *io)
+{
+#ifdef ZTERP_GLK
+  if(io->type == IO_GLK)
+  {
+    return glk_stream_get_position(io->file);
+  }
+  else
+#endif
+  {
+    return ftell(io->fp);
+  }
+}
+
+/* zterp_io_read() and zterp_io_write() always operate in terms of
+ * bytes, whether or not Unicode is available.
+ */
+size_t zterp_io_read(const zterp_io *io, void *buf, size_t n)
+{
+#ifdef ZTERP_GLK
+  if(io->type == IO_GLK)
+  {
+    glui32 s = glk_get_buffer_stream(io->file, buf, n);
+    /* This should only happen if io->file is invalid. */
+    if(s == (glui32)-1) s = 0;
+    return s;
+  }
+  else
+#endif
+  {
+    return fread(buf, 1, n, io->fp);
+  }
+}
+
+size_t zterp_io_write(const zterp_io *io, const void *buf, size_t n)
+{
+#ifdef ZTERP_GLK
+  if(io->type == IO_GLK)
+  {
+    glk_put_buffer_stream(io->file, (char *)buf, n);
+    return n; /* dammit */
+  }
+  else
+#endif
+  {
+    return fwrite(buf, 1, n, io->fp);
+  }
+}
+
+int zterp_io_read16(const zterp_io *io, uint16_t *v)
+{
+  uint8_t buf[2];
+
+  if(zterp_io_read(io, buf, sizeof buf) != sizeof buf) return 0;
+
+  *v = (buf[0] << 8) | buf[1];
+
+  return 1;
+}
+
+int zterp_io_read32(const zterp_io *io, uint32_t *v)
+{
+  uint8_t buf[4];
+
+  if(zterp_io_read(io, buf, sizeof buf) != sizeof buf) return 0;
+
+  *v = (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
+
+  return 1;
+}
+
+/* Read a byte and make sure it’s part of a valid UTF-8 sequence. */
+static int read_byte(const zterp_io *io, uint8_t *c)
+{
+  if(zterp_io_read(io, c, sizeof *c) != sizeof *c) return 0;
+  if((*c & 0x80) != 0x80) return 0;
+
+  return 1;
+}
+
+/* zterp_io_getc() and zterp_io_putc() are meant to operate in terms of
+ * characters, not bytes.  That is, unlike C, bytes and characters are
+ * not equivalent as far as Zterp’s I/O system is concerned.
+ */
+
+/* Read a UTF-8 character, returning it.
+ * -1 is returned on EOF.
+ *
+ * If there is a problem reading the UTF-8 (either from an invalid
+ * sequence or from a too-large value), a question mark is returned.
+ *
+ * If Unicode is not available, read a single byte (assumed to be
+ * Latin-1).
+ * If Unicode is not available, IO_STDIO is in use, and text mode is
+ * set, do newline translation.  Text mode is likely to always be
+ * set—this function really shouldn’t be used in binary mode.
+ */
+long zterp_io_getc(const zterp_io *io)
+{
+  long ret;
+
+  if(!use_utf8_io)
+  {
+#ifdef ZTERP_GLK
+    if(io->type == IO_GLK)
+    {
+      ret = glk_get_char_stream(io->file);
+    }
+    else
+#endif
+    {
+      int c;
+
+      c = getc(io->fp);
+      if(c == EOF) ret = -1;
+      else         ret = c;
+
+      if(textmode(io) && c == '\n') ret = UNICODE_LINEFEED;
+    }
+  }
+  else
+  {
+    uint8_t c;
+
+    if(zterp_io_read(io, &c, sizeof c) != sizeof c)
+    {
+      ret = -1;
+    }
+    else if((c & 0x80) == 0) /* One byte. */
+    {
+      ret = c;
+    }
+    else if((c & 0xe0) == 0xc0) /* Two bytes. */
+    {
+      ret = (c & 0x1f) << 6;
+
+      if(!read_byte(io, &c)) return UNICODE_QUESTIONMARK;
+
+      ret |= (c & 0x3f);
+    }
+    else if((c & 0xf0) == 0xe0) /* Three bytes. */
+    {
+      ret = (c & 0x0f) << 12;
+
+      if(!read_byte(io, &c)) return UNICODE_QUESTIONMARK;
+
+      ret |= ((c & 0x3f) << 6);
+
+      if(!read_byte(io, &c)) return UNICODE_QUESTIONMARK;
+
+      ret |= (c & 0x3f);
+    }
+    else if((c & 0xf8) == 0xf0) /* Four bytes. */
+    {
+      /* The Z-machine doesn’t support Unicode this large, but at
+       * least try not to leave a partial character in the stream.
+       */
+      zterp_io_seek(io, 3, SEEK_CUR);
+
+      ret = UNICODE_QUESTIONMARK;
+    }
+    else /* Invalid value. */
+    {
+      ret = UNICODE_QUESTIONMARK;
+    }
+  }
+
+  if(ret > UINT16_MAX) ret = UNICODE_QUESTIONMARK;
+
+  return ret;
+}
+
+/* Write a Unicode character as UTF-8.
+ *
+ * If Unicode is not available, write the value out as a single Latin-1
+ * byte.  If it is too large for a byte, write out a question mark.
+ *
+ * If Unicode is not available, IO_STDIO is in use, and text mode is
+ * set, do newline translation.
+ *
+ * Text mode is likely to always be set—this function really shouldn’t
+ * be used in binary mode.
+ */
+void zterp_io_putc(const zterp_io *io, uint16_t c)
+{
+  if(!use_utf8_io)
+  {
+    if(c > UINT8_MAX) c = UNICODE_QUESTIONMARK;
+#ifdef ZTERP_GLK
+    if(io->type == IO_GLK)
+    {
+      glk_put_char_stream(io->file, c);
+    }
+    else
+#endif
+    {
+      if(textmode(io) && c == UNICODE_LINEFEED) c = '\n';
+      putc(c, io->fp);
+    }
+  }
+  else
+  {
+    uint8_t hi = c >> 8, lo = c & 0xff;
+
+#define WRITE(c)       zterp_io_write(io, &(uint8_t){ c }, sizeof (uint8_t))
+    if(c < 128)
+    {
+      WRITE(c);
+    }
+    else if(c < 2048)
+    {
+      WRITE(0xc0 | (hi << 2) | (lo >> 6));
+      WRITE(0x80 | (lo & 0x3f));
+    }
+    else
+    {
+      WRITE(0xe0 | (hi >> 4));
+      WRITE(0x80 | ((hi << 2) & 0x3f) | (lo >> 6));
+      WRITE(0x80 | (lo & 0x3f));
+    }
+#undef WRITE
+  }
+}
+
+long zterp_io_readline(const zterp_io *io, uint16_t *buf, size_t len)
+{
+  long ret;
+
+  if(len > LONG_MAX) return -1;
+
+  for(ret = 0; ret < len; ret++)
+  {
+    long c = zterp_io_getc(io);
+
+    /* EOF before newline means there was a problem. */
+    if(c == -1) return -1;
+
+    /* Don’t count the newline. */
+    if(c == UNICODE_LINEFEED) break;
+
+    buf[ret] = c;
+  }
+
+  return ret;
+}
+
+long zterp_io_filesize(const zterp_io *io)
+{
+  if(io->type == IO_STDIO && !textmode(io))
+  {
+    return zterp_os_filesize(io->fp);
+  }
+  else
+  {
+    return -1;
+  }
+}
+
+void zterp_io_flush(const zterp_io *io)
+{
+  if(io == NULL || io->type != IO_STDIO || !(io->mode & (ZTERP_IO_WRONLY | ZTERP_IO_APPEND))) return;
+
+  fflush(io->fp);
+}
diff --git a/interpreters/bocfel/io.h b/interpreters/bocfel/io.h
new file mode 100644 (file)
index 0000000..c3b9945
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef ZTERP_IO_H
+#define ZTERP_IO_H
+
+#include <stdio.h>
+#include <stdint.h>
+
+typedef struct zterp_io zterp_io;
+
+#define ZTERP_IO_DATA          0x00
+#define ZTERP_IO_SAVE          0x01
+#define ZTERP_IO_TRANS         0x02
+#define ZTERP_IO_INPUT         0x04
+#define ZTERP_IO_RDONLY                0x08
+#define ZTERP_IO_WRONLY                0x10
+#define ZTERP_IO_APPEND                0x20
+
+/* This variable controls whether the IO system writes UTF-8 or
+ * Latin-1; it is distinct from Glk’s Unicode setting.
+ * If this is set, transcripts will be written in UTF-8, and if
+ * Glk is not being used, screen output will be written in UTF-8.
+ */
+extern int use_utf8_io;
+
+zterp_io *zterp_io_open(const char *, int);
+const zterp_io *zterp_io_stdin(void);
+const zterp_io *zterp_io_stdout(void);
+void zterp_io_close(zterp_io *);
+int zterp_io_seek(const zterp_io *, long, int);
+long zterp_io_tell(const zterp_io *);
+size_t zterp_io_read(const zterp_io *, void *, size_t);
+size_t zterp_io_write(const zterp_io *, const void *, size_t);
+int zterp_io_read16(const zterp_io *, uint16_t *);
+int zterp_io_read32(const zterp_io *, uint32_t *);
+long zterp_io_getc(const zterp_io *);
+void zterp_io_putc(const zterp_io *, uint16_t);
+long zterp_io_readline(const zterp_io *, uint16_t *, size_t);
+long zterp_io_filesize(const zterp_io *);
+void zterp_io_flush(const zterp_io *);
+
+#endif
diff --git a/interpreters/bocfel/math.c b/interpreters/bocfel/math.c
new file mode 100644 (file)
index 0000000..925391d
--- /dev/null
@@ -0,0 +1,166 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+
+#include "math.h"
+#include "branch.h"
+#include "process.h"
+#include "stack.h"
+#include "util.h"
+#include "zterp.h"
+
+void zinc(void)
+{
+  store_variable(zargs[0], variable(zargs[0]) + 1);
+}
+
+void zdec(void)
+{
+  store_variable(zargs[0], variable(zargs[0]) - 1);
+}
+
+void znot(void)
+{
+  store(~zargs[0]);
+}
+
+void zdec_chk(void)
+{
+  int16_t new;
+  int16_t val = zargs[1];
+
+  zdec();
+
+  /* The z-spec 1.1 requires indirect variable references to the stack not to push/pop */
+  if(zargs[0] == 0) new = *stack_top_element();
+  else              new = variable(zargs[0]);
+
+  branch_if(new < val);
+}
+
+void zinc_chk(void)
+{
+  int16_t new;
+  int16_t val = zargs[1];
+
+  zinc();
+
+  /* The z-spec 1.1 requires indirect variable references to the stack not to push/pop */
+  if(zargs[0] == 0) new = *stack_top_element();
+  else              new = variable(zargs[0]);
+
+  branch_if(new > val);
+}
+
+void ztest(void)
+{
+  branch_if( (zargs[0] & zargs[1]) == zargs[1] );
+}
+
+void zor(void)
+{
+  store(zargs[0] | zargs[1]);
+}
+
+void zand(void)
+{
+  store(zargs[0] & zargs[1]);
+}
+
+void zadd(void)
+{
+  store(zargs[0] + zargs[1]);
+}
+
+void zsub(void)
+{
+  store(zargs[0] - zargs[1]);
+}
+
+void zmul(void)
+{
+  store(zargs[0] * zargs[1]);
+}
+
+void zdiv(void)
+{
+  ZASSERT(zargs[1] != 0, "divide by zero");
+  store((int16_t)zargs[0] / (int16_t)zargs[1]);
+}
+
+void zmod(void)
+{
+  ZASSERT(zargs[1] != 0, "divide by zero");
+  store((int16_t)zargs[0] % (int16_t)zargs[1]);
+}
+
+void zlog_shift(void)
+{
+  int16_t places = zargs[1];
+
+  /* Shifting more than 15 bits is undefined (as of Standard 1.1), but
+   * do the most sensible thing.
+   */
+  if(places < -15 || places > 15)
+  {
+    store(0);
+    return;
+  }
+
+  if(places < 0) store(zargs[0] >> -places);
+  else           store(zargs[0] <<  places);
+}
+
+void zart_shift(void)
+{
+  int16_t number = zargs[0], places = zargs[1];
+
+  /* Shifting more than 15 bits is undefined (as of Standard 1.1), but
+   * do the most sensible thing.
+   */
+  if(places < -15 || places > 15)
+  {
+    store(number < 0 ? -1 : 0);
+    return;
+  }
+
+  /* Shifting a negative value in C has some consequences:
+   * • Shifting a negative value left is undefined.
+   * • Shifting a negative value right is implementation defined.
+   *
+   * Thus these are done by hand.  The Z-machine requires a right-shift
+   * of a negative value to propagate the sign bit.  This is easily
+   * accomplished by complementing the value (yielding a positive
+   * number), shifting it right (zero filling), and complementing again
+   * (flip the shifted-in zeroes to ones).
+   *
+   * For a left-shift, the result should presumably be the same as a
+   * logical shift, so do that.
+   */
+  if(number < 0)
+  {
+    if(places < 0) store(~(~number >> -places));
+    else           store(zargs[0]  <<  places);
+  }
+  else
+  {
+    if(places < 0) store(zargs[0] >> -places);
+    else           store(zargs[0] <<  places);
+  }
+}
diff --git a/interpreters/bocfel/math.h b/interpreters/bocfel/math.h
new file mode 100644 (file)
index 0000000..94c427a
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef ZTERP_MATH_H
+#define ZTERP_MATH_H
+
+void zinc(void);
+void zdec(void);
+void znot(void);
+void zdec_chk(void);
+void zinc_chk(void);
+void ztest(void);
+void zor(void);
+void zand(void);
+void zadd(void);
+void zsub(void);
+void zmul(void);
+void zdiv(void);
+void zmod(void);
+void zlog_shift(void);
+void zart_shift(void);
+
+#endif
diff --git a/interpreters/bocfel/memory.c b/interpreters/bocfel/memory.c
new file mode 100644 (file)
index 0000000..e234b88
--- /dev/null
@@ -0,0 +1,76 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdint.h>
+
+#include "memory.h"
+#include "screen.h"
+#include "util.h"
+#include "zterp.h"
+
+uint8_t *memory, *dynamic_memory;
+uint32_t memory_size;
+
+void user_store_byte(uint16_t addr, uint8_t v)
+{
+  /* If safety checks are off, there’s no point in checking these
+   * special cases. */
+#ifndef ZTERP_NO_SAFETY_CHECKS
+#ifdef ZTERP_TANDY
+  if(addr == 0x01)
+  {
+    ZASSERT(v == BYTE(addr) || (BYTE(addr) ^ v) == 8, "not allowed to modify any bits but 3 at 0x0001");
+  }
+  else
+#endif
+
+  /* 0x10 can’t be modified, but let it slide if the story is storing
+   * the same value that’s already there.  This is useful because the
+   * flags at 0x10 are stored in a word, so the story possibly could use
+   * @storew at 0x10 to modify the bits in 0x11.
+   */
+  if(addr == 0x10 && BYTE(addr) == v)
+  {
+    return;
+  }
+  else
+#endif
+
+  if(addr == 0x11)
+  {
+    ZASSERT((BYTE(addr) ^ v) < 8, "not allowed to modify bits 3-7 at 0x0011");
+
+    if(!output_stream((v & FLAGS2_TRANSCRIPT) ? OSTREAM_SCRIPT : -OSTREAM_SCRIPT, 0)) v &= ~FLAGS2_TRANSCRIPT;
+
+    header_fixed_font = v & FLAGS2_FIXED;
+    set_current_style();
+  }
+
+  else
+  {
+    ZASSERT(addr >= 0x40 && addr < header.static_start, "attempt to write to read-only address 0x%lx", (unsigned long)addr);
+  }
+
+  STORE_BYTE(addr, v);
+}
+
+void user_store_word(uint16_t addr, uint16_t v)
+{
+  user_store_byte(addr + 0, v >> 8);
+  user_store_byte(addr + 1, v & 0xff);
+}
diff --git a/interpreters/bocfel/memory.h b/interpreters/bocfel/memory.h
new file mode 100644 (file)
index 0000000..a53dee3
--- /dev/null
@@ -0,0 +1,55 @@
+#ifndef ZTERP_MEMORY_H
+#define ZTERP_MEMORY_H
+
+#include <stdint.h>
+
+#include "util.h"
+#include "zterp.h"
+
+/* Story files do not have access to memory beyond 64K.  If they do
+ * something that would cause such access, wrap appropriately.  This is
+ * the approach Frotz uses (at least for @loadw/@loadb), and is endorsed
+ * by Andrew Plotkin (see http://www.intfiction.org/forum/viewtopic.php?f=38&t=2052).
+ * The standard isn’t exactly clear on the issue, and this appears to be
+ * the most sensible way to deal with the problem.
+ */
+
+extern uint8_t *memory, *dynamic_memory;
+extern uint32_t memory_size;
+
+#define BYTE(addr)             (memory[addr])
+#define STORE_BYTE(addr, val)  ((void)(memory[addr] = (val)))
+
+static inline uint16_t WORD(uint32_t addr)
+{
+#ifndef ZTERP_NO_CHEAT
+  uint16_t cheat_val;
+  if(cheat_find_freezew(addr, &cheat_val)) return cheat_val;
+#endif
+  return (memory[addr] << 8) | memory[addr + 1];
+}
+
+static inline void STORE_WORD(uint32_t addr, uint16_t val)
+{
+  memory[addr + 0] = val >> 8;
+  memory[addr + 1] = val & 0xff;
+}
+
+static inline uint8_t user_byte(uint16_t addr)
+{
+  ZASSERT(addr < header.static_end, "attempt to read out-of-bounds address 0x%lx", (unsigned long)addr);
+
+  return BYTE(addr);
+}
+
+static inline uint16_t user_word(uint16_t addr)
+{
+  ZASSERT(addr < header.static_end - 1, "attempt to read out-of-bounds address 0x%lx", (unsigned long)addr);
+
+  return WORD(addr);
+}
+
+void user_store_byte(uint16_t, uint8_t);
+void user_store_word(uint16_t, uint16_t);
+
+#endif
diff --git a/interpreters/bocfel/objects.c b/interpreters/bocfel/objects.c
new file mode 100644 (file)
index 0000000..ae68839
--- /dev/null
@@ -0,0 +1,436 @@
+/*-
+ * Copyright 2009-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "objects.h"
+#include "branch.h"
+#include "memory.h"
+#include "process.h"
+#include "screen.h"
+#include "util.h"
+#include "zterp.h"
+
+static uint16_t OBJECT(uint16_t n)
+{
+  /* Use 32-bit arithmetic to detect 16-bit overflow. */
+  uint32_t base = header.objects, obj = n, addr;
+  int objsize;
+
+  if(zversion <= 3)
+  {
+    ZASSERT(n <= 255, "illegal object %u referenced", (unsigned)n);
+    addr = base + (31 * 2) + (9 * (obj - 1));
+    objsize = 9;
+  }
+  else
+  {
+    addr = base + (63 * 2) + (14 * (obj - 1));
+    objsize = 14;
+  }
+
+  ZASSERT(addr + objsize < header.static_start, "object %u out of range", (unsigned)n);
+
+  return addr;
+}
+
+#define OFFSET_PARENT  (zversion <= 3 ? 4 :  6)
+#define OFFSET_SIBLING (zversion <= 3 ? 5 :  8)
+#define OFFSET_CHILD   (zversion <= 3 ? 6 : 10)
+#define OFFSET_PROP    (zversion <= 3 ? 7 : 12)
+
+#define PARENT(object)         RELATION(object, OFFSET_PARENT)
+#define SIBLING(object)                RELATION(object, OFFSET_SIBLING)
+#define CHILD(object)          RELATION(object, OFFSET_CHILD)
+
+#define SET_PARENT(obj1, obj2) SET_OBJECT(obj1, obj2, OFFSET_PARENT)
+#define SET_SIBLING(obj1, obj2)        SET_OBJECT(obj1, obj2, OFFSET_SIBLING)
+#define SET_CHILD(obj1, obj2)  SET_OBJECT(obj1, obj2, OFFSET_CHILD)
+
+static uint16_t PROPADDR(uint16_t n)
+{
+  return WORD(OBJECT(n) + OFFSET_PROP);
+}
+
+static uint16_t RELATION(uint16_t object, int offset)
+{
+  return zversion <= 3 ? BYTE(OBJECT(object) + offset) : WORD(OBJECT(object) + offset);
+}
+
+/*
+ * the 32 attribute flags     parent     sibling     child   properties
+ * ---32 bits in 4 bytes---   ---3 bytes------------------  ---2 bytes--
+ *
+ * the 48 attribute flags     parent    sibling   child     properties
+ * ---48 bits in 6 bytes---   ---3 words, i.e. 6 bytes----  ---2 bytes--
+ */
+static void SET_OBJECT(uint16_t obj1, uint16_t obj2, int offset)
+{
+  if(zversion <= 3) STORE_BYTE(OBJECT(obj1) + offset, obj2);
+  else              STORE_WORD(OBJECT(obj1) + offset, obj2);
+}
+
+static void remove_obj(uint16_t object)
+{
+  uint16_t parent = PARENT(object);
+
+  if(parent != 0)
+  {
+    uint16_t child = CHILD(parent);
+
+    /* Direct child */
+    if(child == object)
+    {
+      /* parent->child = parent->child->sibling */
+      SET_CHILD(parent, SIBLING(child));
+    }
+    else
+    {
+      while(SIBLING(child) != object)
+      {
+        /* child = child->sibling */
+        child = SIBLING(child);
+      }
+
+      /* Now the sibling of child is the object to remove. */
+
+      /* child->sibling = child->sibling->sibling */
+      SET_SIBLING(child, SIBLING(SIBLING(child)));
+    }
+
+    /* object->parent = 0 */
+    SET_PARENT(object, 0);
+
+    /* object->sibling = 0 */
+    SET_SIBLING(object, 0);
+  }
+}
+
+static uint16_t property_length(uint16_t propaddr)
+{
+  uint16_t length;
+  /* The address is to the data; the size byte is right before. */
+  uint8_t byte = user_byte(propaddr - 1);
+
+  if(zversion <= 3)
+  {
+    length = (byte >> 5) + 1;
+  }
+  else
+  {
+    if(byte & 0x80)
+    {
+      length = byte & 0x3f;
+      if(length == 0) length = 64;
+    }
+    else
+    {
+      length = (byte & 0x40) ? 2 : 1;
+    }
+  }
+
+  return length;
+}
+
+static uint8_t PROPERTY(uint16_t addr)
+{
+  uint8_t propnum;
+
+  if(zversion <= 3)
+  {
+    propnum = user_byte(addr - 1) & 0x1f;
+  }
+  else
+  {
+    if(user_byte(addr - 1) & 0x80) propnum = user_byte(addr - 2) & 0x3f;
+    else                           propnum = user_byte(addr - 1) & 0x3f;
+  }
+
+  return propnum;
+}
+
+static uint16_t advance_prop_addr(uint16_t propaddr)
+{
+  uint8_t size;
+
+  size = user_byte(propaddr++);
+
+  if(size == 0) return 0;
+
+  if(zversion >= 4 && (size & 0x80)) propaddr++;
+
+  return propaddr;
+}
+
+static uint16_t first_property(uint16_t object)
+{
+  uint16_t propaddr = PROPADDR(object);
+
+  propaddr += (2 * user_byte(propaddr)) + 1;
+
+  return advance_prop_addr(propaddr);
+}
+
+static uint16_t next_property(uint16_t propaddr)
+{
+  propaddr += property_length(propaddr);
+
+  return advance_prop_addr(propaddr);
+}
+
+#define FOR_EACH_PROPERTY(object, addr) for(uint16_t addr = first_property(object); addr != 0; addr = next_property(addr))
+
+static int find_prop(uint16_t object, uint16_t property, uint16_t *propaddr, uint16_t *length)
+{
+  FOR_EACH_PROPERTY(object, addr)
+  {
+    if(PROPERTY(addr) == property)
+    {
+      *propaddr = addr;
+      *length = property_length(addr);
+      return 1;
+    }
+  }
+
+  return 0;
+}
+
+static void check_attr(uint16_t attr)
+{
+  ZASSERT(attr <= (zversion <= 3 ? 31 : 47), "invalid attribute: %u", (unsigned)attr);
+}
+
+static int is_zero(int is_store, int is_jump)
+{
+  if(zargs[0] == 0)
+  {
+    if(is_store) store(0);
+    if(is_jump)  branch_if(0);
+
+    return 1;
+  }
+
+  return 0;
+}
+
+#define check_zero(store, jump)        do { if(is_zero(store, jump)) return; } while(0)
+
+/* Attributes are stored at the very beginning of an object, so the
+ * address OBJECT() returns refers directly to the attributes.  The
+ * leftmost bit is attribute 0.  Thus these attribute functions need to
+ * find out first which byte of the attributes to look at; this is done
+ * by dividing by 8.  Attributes 0-7 will be in byte 0, 8-15 in byte 1,
+ * and so on.  Then the particular bit is found.  Attributes 0..7 are
+ * bits 7..0, attributes 8..15 are 7..0, and so on.  Taking the
+ * remainder of the attribute divided by 8 gives the bit position,
+ * counting from the left, of the attribute.
+ */
+#define ATTR_BIT(num)          (0x80U >> ((num) % 8))
+void ztest_attr(void)
+{
+  check_zero(0, 1);
+  check_attr(zargs[1]);
+
+  uint16_t addr = OBJECT(zargs[0]) + (zargs[1] / 8);
+
+  branch_if(BYTE(addr) & ATTR_BIT(zargs[1]));
+}
+
+void zset_attr(void)
+{
+  check_zero(0, 0);
+  check_attr(zargs[1]);
+
+  uint16_t addr = OBJECT(zargs[0]) + (zargs[1] / 8);
+
+  STORE_BYTE(addr, BYTE(addr) | ATTR_BIT(zargs[1]));
+}
+
+void zclear_attr(void)
+{
+  check_zero(0, 0);
+  check_attr(zargs[1]);
+
+  uint16_t addr = OBJECT(zargs[0]) + (zargs[1] / 8);
+
+  STORE_BYTE(addr, BYTE(addr) & ~ATTR_BIT(zargs[1]));
+}
+#undef ATTR_BIT
+
+void zremove_obj(void)
+{
+  check_zero(0, 0);
+
+  remove_obj(zargs[0]);
+}
+
+void zinsert_obj(void)
+{
+  check_zero(0, 0);
+
+  remove_obj(zargs[0]);
+
+  SET_SIBLING(zargs[0], CHILD(zargs[1]));
+  SET_CHILD(zargs[1], zargs[0]);
+  SET_PARENT(zargs[0], zargs[1]);
+}
+
+void zget_sibling(void)
+{
+  check_zero(1, 1);
+
+  uint16_t sibling = SIBLING(zargs[0]);
+
+  store(sibling);
+  branch_if(sibling != 0);
+}
+
+void zget_child(void)
+{
+  check_zero(1, 1);
+
+  uint16_t child = CHILD(zargs[0]);
+
+  store(child);
+  branch_if(child != 0);
+}
+
+void zget_parent(void)
+{
+  check_zero(1, 0);
+
+  store(PARENT(zargs[0]));
+}
+
+void zput_prop(void)
+{
+  check_zero(0, 0);
+
+  uint16_t propaddr, length;
+  int found;
+
+  found = find_prop(zargs[0], zargs[1], &propaddr, &length);
+
+  ZASSERT(found, "broken story: no prop");
+  ZASSERT(length == 1 || length == 2, "broken story: property too long: %u", (unsigned)length);
+
+  if(length == 1) user_store_byte(propaddr, zargs[2] & 0xff);
+  else            user_store_word(propaddr, zargs[2]);
+}
+
+void zget_prop(void)
+{
+  check_zero(1, 0);
+
+  uint16_t propaddr, length;
+
+  if(find_prop(zargs[0], zargs[1], &propaddr, &length))
+  {
+    if     (length == 1) store(user_byte(propaddr));
+    else if(length == 2) store(user_word(propaddr));
+
+    /* If the length is > 2, the story file is misbehaving.  At least
+     * Christminster does this, and Frotz and Nitfol allow it, reading a
+     * word, so do that here.
+     */
+    else                 store(user_word(propaddr));
+  }
+  else
+  {
+    uint32_t i;
+
+    ZASSERT(zargs[1] < (zversion <= 3 ? 32 : 64), "invalid property: %u", (unsigned)zargs[1]);
+
+    i = header.objects + (2 * (zargs[1] - 1));
+    store(WORD(i));
+  }
+}
+
+void zget_prop_len(void)
+{
+  /* Z-spec 1.1 says @get_prop_len 0 must yield 0. */
+  if(zargs[0] == 0) store(0);
+  else              store(property_length(zargs[0]));
+}
+
+void zget_prop_addr(void)
+{
+  check_zero(1, 0);
+
+  uint16_t propaddr, length;
+
+  if(find_prop(zargs[0], zargs[1], &propaddr, &length)) store(propaddr);
+  else store(0);
+}
+
+void zget_next_prop(void)
+{
+  check_zero(1, 0);
+
+  uint16_t object = zargs[0], property = zargs[1];
+  int next = 0;
+  int found_prop = 0;
+
+  FOR_EACH_PROPERTY(object, propaddr)
+  {
+    uint8_t propnum = PROPERTY(propaddr);
+
+    if(property == 0 || next)
+    {
+      found_prop = propnum;
+      break;
+    }
+
+    if(propnum == property) next = 1;
+  }
+
+  store(found_prop);
+}
+
+void zjin(void)
+{
+  /* @jin 0 0 is not defined, since @jin requires an object (§15) and
+   * object 0 is not actually an object (§12.3).  However, many
+   * interpreters yield a true value for this, and Torbjorn Andersson’s
+   * strictz tester expects it to be true, so go with the flow.
+   */
+  if(zargs[0] == 0 && zargs[1] == 0)
+  {
+    branch_if(1);
+    return;
+  }
+
+  check_zero(0, 1);
+
+  branch_if(PARENT(zargs[0]) == zargs[1]);
+}
+
+void print_object(uint16_t obj, void (*outc)(uint8_t))
+{
+  if(obj == 0) return;
+
+  print_handler(PROPADDR(obj) + 1, outc);
+}
+
+void zprint_obj(void)
+{
+  check_zero(0, 0);
+
+  print_object(zargs[0], NULL);
+}
diff --git a/interpreters/bocfel/objects.h b/interpreters/bocfel/objects.h
new file mode 100644 (file)
index 0000000..eb532ab
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef OBJECTS_H
+#define OBJECTS_H
+
+#include <stdint.h>
+
+void print_object(uint16_t, void (*)(uint8_t));
+
+void zget_sibling(void);
+void zget_child(void);
+void zget_parent(void);
+void zremove_obj(void);
+void ztest_attr(void);
+void zset_attr(void);
+void zclear_attr(void);
+void zinsert_obj(void);
+void zget_prop_len(void);
+void zget_prop_addr(void);
+void zget_next_prop(void);
+void zput_prop(void);
+void zget_prop(void);
+void zjin(void);
+void zprint_obj(void);
+
+#endif
diff --git a/interpreters/bocfel/osdep.c b/interpreters/bocfel/osdep.c
new file mode 100644 (file)
index 0000000..454c1b2
--- /dev/null
@@ -0,0 +1,312 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef ZTERP_UNIX
+#define _XOPEN_SOURCE  600
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <limits.h>
+
+#include "osdep.h"
+#include "screen.h"
+
+/* OS-specific functions should all be collected in this file for
+ * convenience.  A sort of poor-man’s “reverse” inheritance is used: for
+ * each function that a particular operating system provides, it should
+ * #define a macro of the same name.  At the end of the file, a generic
+ * function is provided for each function that has no associated macro
+ * definition.
+ *
+ * The functions required are as follows:
+ *
+ * long zterp_os_filesize(FILE *fp)
+ *
+ * Return the size of the file referred to by fp.  It is safe to assume
+ * that the file is opened in binary mode.  The file position indicator
+ * need not be maintained.  If the size of the file is larger than
+ * LONG_MAX, -1 should be returned.
+ *
+ * int zterp_os_have_unicode(void)
+ *
+ * The main purpose behind this function is to indicate whether
+ * transcripts will be written in UTF-8 or Latin-1.  This is, of course,
+ * not necessarily an OS matter, but I’ve run into some issues with
+ * UTF-8 and Windows (at least through Wine), so I want to be as
+ * sensible as I can with the defaults.  The user is able to override
+ * this value if he so desires.
+ * If a Glk build is not being used, this function also serves to
+ * indicate whether all I/O, not just transcripts, should be UTF-8 or
+ * not.  Glk libraries are able to be queried as to their support for
+ * Unicode so there is no need to make assumptions in that case.
+ *
+ * void zterp_os_rcfile(char *s, size_t n)
+ *
+ * Different operating systems have different ideas about where
+ * configuration data should be stored; this function will copy a
+ * suitable value for the bocfel configuration file into the buffer s
+ * which is n bytes long.
+ *
+ * void zterp_os_reopen_binary(FILE *fp)
+ *
+ * Writing UTF-8 requires that no mangling be done, such as might happen
+ * when a stream is opened in text mode.  This function should, if
+ * necessary, set the mode on the file pointer in fp to be binary.
+ *
+ * The following functions are useful for non-Glk builds only.  They
+ * provide for some handling of screen functions that is normally taken
+ * care of by Glk.
+ *
+ * void zterp_os_get_screen_size(unsigned *w, unsigned *h)
+ *
+ * The size of the terminal, if known, is written into *w (width) and *h
+ * (height).  If terminal size is unavalable, nothing should be written.
+ *
+ * void zterp_os_init_term(void)
+ *
+ * If something special needs to be done to prepare the terminal for
+ * output, it should be done here.  This function is called once at
+ * program startup.
+ *
+ * int zterp_os_have_style(int style)
+ *
+ * This should return true if the provided style (see style.h for valid
+ * STYLE_ values) is available.  It is safe to assume that styles will
+ * not be combined; e.g. this will not be called as:
+ * zterp_os_have_style(STYLE_BOLD | STYLE_ITALIC);
+ *
+ * int zterp_os_have_colors(void)
+ *
+ * Returns true if the terminal supports colors.
+ *
+ * void zterp_os_set_style(int style, int fg, int bg)
+ *
+ * Set both a style and foreground/background color.  Any previous
+ * settings should be ignored; for example, if the last call to
+ * zterp_os_set_style() turned on italics and the current call sets
+ * bold, the result should be bold, not bold italic.
+ * Unlike in zterp_os_have_style(), here styles may be combined.  See
+ * the Unix implementation for a reference.
+ * The colors are Z-machine colors (see §8.3.1), with the following
+ * note: the only color values that will ever be passed in are 1–9.
+ */
+
+/******************
+ * Unix functions *
+ ******************/
+#ifdef ZTERP_UNIX
+#include <sys/stat.h>
+
+long zterp_os_filesize(FILE *fp)
+{
+  struct stat buf;
+  int fd = fileno(fp);
+
+  if(fd == -1 || fstat(fd, &buf) == -1 || !S_ISREG(buf.st_mode) || buf.st_size > LONG_MAX) return -1;
+
+  return buf.st_size;
+}
+#define zterp_os_filesize
+
+int zterp_os_have_unicode(void)
+{
+  return 1;
+}
+#define zterp_os_have_unicode
+
+void zterp_os_rcfile(char *s, size_t n)
+{
+  snprintf(s, n, "%s/.bocfelrc", getenv("HOME") != NULL ? getenv("HOME") : ".");
+}
+#define zterp_os_rcfile
+
+#ifndef ZTERP_GLK
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <curses.h>
+#include <term.h>
+#ifdef TIOCGWINSZ
+void zterp_os_get_screen_size(unsigned *w, unsigned *h)
+{
+  struct winsize winsize;
+
+  if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) == 0)
+  {
+    *w = winsize.ws_col;
+    *h = winsize.ws_row;
+  }
+}
+#define zterp_os_get_screen_size
+#endif
+
+static const char *ital = NULL, *rev = NULL, *bold = NULL, *none = NULL;
+static char *fg_string = NULL, *bg_string = NULL;
+static int have_colors = 0;
+void zterp_os_init_term(void)
+{
+  if(setupterm(NULL, STDIN_FILENO, NULL) != OK) return;
+
+  /* prefer italics over underline for emphasized text */
+  ital = tgetstr("ZH", NULL);
+  if(ital == NULL) ital = tgetstr("us", NULL);
+  rev  = tgetstr("mr", NULL);
+  bold = tgetstr("md", NULL);
+  none = tgetstr("me", NULL);
+
+  fg_string = tgetstr("AF", NULL);
+  bg_string = tgetstr("AB", NULL);
+
+  have_colors = none != NULL && fg_string != NULL && bg_string != NULL;
+}
+#define zterp_os_init_term
+
+int zterp_os_have_style(int style)
+{
+  if(none == NULL) return 0;
+
+  if     (style == STYLE_ITALIC)  return ital != NULL;
+  else if(style == STYLE_REVERSE) return rev  != NULL;
+  else if(style == STYLE_BOLD)    return bold != NULL;
+  else if(style == STYLE_NONE)    return none != NULL;
+
+  return 0;
+}
+#define zterp_os_have_style
+
+int zterp_os_have_colors(void)
+{
+  return have_colors;
+}
+#define zterp_os_have_colors
+
+void zterp_os_set_style(int style, int fg, int bg)
+{
+  /* If the terminal cannot be reset, nothing can be used. */
+  if(none == NULL) return;
+
+  putp(none);
+
+  if((style & STYLE_ITALIC)  && ital != NULL) putp(ital);
+  if((style & STYLE_REVERSE) && rev  != NULL) putp(rev);
+  if((style & STYLE_BOLD)    && bold != NULL) putp(bold);
+
+  if(have_colors)
+  {
+    if(fg > 1) putp(tparm(fg_string, fg - 2, 0, 0, 0, 0, 0, 0, 0, 0));
+    if(bg > 1) putp(tparm(bg_string, bg - 2, 0, 0, 0, 0, 0, 0, 0, 0));
+  }
+}
+#define zterp_os_set_style
+#endif
+
+/*********************
+ * Windows functions *
+ *********************/
+#elif defined(ZTERP_WIN32)
+void zterp_os_rcfile(char *s, size_t n)
+{
+  char *p;
+
+  p = getenv("APPDATA");
+  if(p == NULL) p = getenv("LOCALAPPDATA");
+  if(p == NULL) p = ".";
+
+  snprintf(s, n, "%s\\bocfel.ini", p);
+}
+#define zterp_os_rcfile
+
+#endif
+
+/*********************
+ * Generic functions *
+ *********************/
+#ifndef zterp_os_filesize
+long zterp_os_filesize(FILE *fp)
+{
+  /* Assume fseek() can seek to the end of binary streams. */
+  if(fseek(fp, 0, SEEK_END) == -1) return -1;
+
+  return ftell(fp);
+}
+#endif
+
+#ifndef zterp_os_have_unicode
+int zterp_os_have_unicode(void)
+{
+  return 0;
+}
+#endif
+
+#ifndef zterp_os_rcfile
+void zterp_os_rcfile(char *s, size_t n)
+{
+  snprintf(s, n, "bocfelrc");
+}
+#endif
+
+/* When UTF-8 output is enabled, special translation of characters (e.g.
+ * newline) should not be done.  Theoretically this function exists to
+ * set stdin/stdout to binary mode, if necessary.  Unix makes no
+ * text/binary distinction, but Windows does.  I’m under the impression
+ * that there is a setmode() function that should be able to do this,
+ * but my knowledge of Windows is so small that I do not want to do much
+ * more than I have, lest I completely break Windows support—assuming it
+ * even works.
+ * freopen() should be able to do this, but with my testing under Wine,
+ * no text gets output in such a case.
+ */
+#ifndef zterp_os_reopen_binary
+void zterp_os_reopen_binary(FILE *fp)
+{
+}
+#endif
+
+#ifndef ZTERP_GLK
+#ifndef zterp_os_get_screen_size
+void zterp_os_get_screen_size(unsigned *w, unsigned *h)
+{
+}
+#endif
+
+#ifndef zterp_os_init_term
+void zterp_os_init_term(void)
+{
+}
+#endif
+
+#ifndef zterp_os_have_style
+int zterp_os_have_style(int style)
+{
+  return 0;
+}
+#endif
+
+#ifndef zterp_os_have_colors
+int zterp_os_have_colors(void)
+{
+  return 0;
+}
+#endif
+
+#ifndef zterp_os_set_style
+void zterp_os_set_style(int style, int fg, int bg)
+{
+}
+#endif
+#endif
diff --git a/interpreters/bocfel/osdep.h b/interpreters/bocfel/osdep.h
new file mode 100644 (file)
index 0000000..de8f062
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef ZTERP_OSDEP_H
+#define ZTERP_OSDEP_H
+
+#include <stdio.h>
+#include <stddef.h>
+
+long zterp_os_filesize(FILE *);
+int zterp_os_have_unicode(void);
+void zterp_os_rcfile(char *, size_t);
+void zterp_os_reopen_binary(FILE *);
+
+#ifndef ZTERP_GLK
+void zterp_os_get_screen_size(unsigned *, unsigned *);
+void zterp_os_init_term(void);
+int zterp_os_have_style(int);
+int zterp_os_have_colors(void);
+void zterp_os_set_style(int, int, int);
+#endif
+
+#endif
diff --git a/interpreters/bocfel/process.c b/interpreters/bocfel/process.c
new file mode 100644 (file)
index 0000000..385aee3
--- /dev/null
@@ -0,0 +1,416 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <setjmp.h>
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#endif
+
+#include "process.h"
+#include "branch.h"
+#include "dict.h"
+#include "math.h"
+#include "memory.h"
+#include "objects.h"
+#include "random.h"
+#include "screen.h"
+#include "stack.h"
+#include "table.h"
+#include "util.h"
+#include "zoom.h"
+#include "zterp.h"
+
+uint16_t zargs[8];
+int znargs;
+
+static jmp_buf *jumps;
+static size_t njumps;
+
+/* Each time an interrupt happens, process_instructions() is called
+ * (effectively starting a whole new round of interpreting).  This
+ * variable holds the current level of interpreting: 0 for no
+ * interrupts, 1 if one interrupt has been called, 2 if an interrupt was
+ * called inside of an interrupt, and so on.
+ */
+static long ilevel = -1;
+
+long interrupt_level(void)
+{
+  return ilevel;
+}
+
+/* When this is called, the interrupt at level “level” will stop
+ * running: if a single interrupt is running, then break_from(1) will
+ * stop the interrupt, going back to the main program.  Breaking from
+ * interrupt level 0 (which is not actually an interrupt) will end the
+ * program.  This is how @quit is implemented.
+ */
+void break_from(long level)
+{
+  ilevel = level - 1;
+  longjmp(jumps[level], 1);
+}
+
+/* If a restore happens inside of an interrupt, the level needs to be
+ * set back to 0, but without a longjmp(), so break_from() cannot be
+ * used.
+ */
+void reset_level(void)
+{
+  ilevel = 0;
+}
+
+/* To signal a restart, longjmp() is called with 2; this advises
+ * process_instructions() to restart the story file and then continue
+ * execution, whereas a value of 1 tells it to return immediately.
+ */
+static void zrestart(void)
+{
+  ilevel = 0;
+  longjmp(jumps[0], 2);
+}
+
+/* Returns 1 if decoded, 0 otherwise (omitted) */
+static int decode_base(uint8_t type, uint16_t *loc)
+{
+  if     (type == 0) *loc = WORD(pc++);                /* Large constant. */
+  else if(type == 1) *loc = BYTE(pc);          /* Small constant. */
+  else if(type == 2) *loc = variable(BYTE(pc));        /* Variable. */
+  else               return 0;                 /* Omitted. */
+
+  pc++;
+
+  return 1;
+}
+
+static void decode_var(uint8_t types)
+{
+  uint16_t ret;
+
+  if(!decode_base((types >> 6) & 0x03, &ret)) return;
+  zargs[znargs++] = ret;
+  if(!decode_base((types >> 4) & 0x03, &ret)) return;
+  zargs[znargs++] = ret;
+  if(!decode_base((types >> 2) & 0x03, &ret)) return;
+  zargs[znargs++] = ret;
+  if(!decode_base((types >> 0) & 0x03, &ret)) return;
+  zargs[znargs++] = ret;
+}
+
+/* op[0] is 0OP, op[1] is 1OP, etc */
+static void (*op[5][256])(void);
+static const char *opnames[5][256];
+enum { ZERO, ONE, TWO, VAR, EXT };
+
+#define op_call(opt, opnumber) (op[opt][opnumber]())
+
+/* This nifty trick is from Frotz. */
+static void zextended(void)
+{
+  uint8_t opnumber = BYTE(pc++);
+
+  decode_var(BYTE(pc++));
+
+  /* §14.2.1
+   * The exception for 0x80–0x83 is for the Zoom extensions.
+   * Standard 1.1 implicitly updates §14.2.1 to recommend ignoring
+   * opcodes in the range EXT:30 to EXT:255, due to the fact that
+   * @buffer_screen is EXT:29.
+   */
+  if(opnumber > 0x1d && (opnumber < 0x80 || opnumber > 0x83)) return;
+
+  op_call(EXT, opnumber);
+}
+
+static void illegal_opcode(void)
+{
+#ifndef ZTERP_NO_SAFETY_CHECKS
+  die("illegal opcode (pc = 0x%lx)", zassert_pc);
+#else
+  die("illegal opcode");
+#endif
+}
+
+void setup_opcodes(void)
+{
+  for(int i = 0; i < 5; i++)
+  {
+    for(int j = 0; j < 256; j++)
+    {
+      op[i][j] = illegal_opcode;
+    }
+  }
+#define OP(args, opcode, fn) do { op[args][opcode] = fn; opnames[args][opcode] = #fn; } while(0)
+  OP(ZERO, 0x00, zrtrue);
+  OP(ZERO, 0x01, zrfalse);
+  OP(ZERO, 0x02, zprint);
+  OP(ZERO, 0x03, zprint_ret);
+  OP(ZERO, 0x04, znop);
+  if(zversion <= 4) OP(ZERO, 0x05, zsave);
+  if(zversion <= 4) OP(ZERO, 0x06, zrestore);
+  OP(ZERO, 0x07, zrestart);
+  OP(ZERO, 0x08, zret_popped);
+  if(zversion <= 4) OP(ZERO, 0x09, zpop);
+  else              OP(ZERO, 0x09, zcatch);
+  OP(ZERO, 0x0a, zquit);
+  OP(ZERO, 0x0b, znew_line);
+  if     (zversion == 3) OP(ZERO, 0x0c, zshow_status);
+  else if(zversion >= 4) OP(ZERO, 0x0c, znop); /* §15: Technically illegal in V4+, but a V5 Wishbringer accidentally uses this opcode. */
+  if(zversion >= 3) OP(ZERO, 0x0d, zverify);
+  if(zversion >= 5) OP(ZERO, 0x0e, zextended);
+  if(zversion >= 5) OP(ZERO, 0x0f, zpiracy);
+
+  OP(ONE, 0x00, zjz);
+  OP(ONE, 0x01, zget_sibling);
+  OP(ONE, 0x02, zget_child);
+  OP(ONE, 0x03, zget_parent);
+  OP(ONE, 0x04, zget_prop_len);
+  OP(ONE, 0x05, zinc);
+  OP(ONE, 0x06, zdec);
+  OP(ONE, 0x07, zprint_addr);
+  if(zversion >= 4) OP(ONE, 0x08, zcall_1s);
+  OP(ONE, 0x09, zremove_obj);
+  OP(ONE, 0x0a, zprint_obj);
+  OP(ONE, 0x0b, zret);
+  OP(ONE, 0x0c, zjump);
+  OP(ONE, 0x0d, zprint_paddr);
+  OP(ONE, 0x0e, zload);
+  if(zversion <= 4) OP(ONE, 0x0f, znot);
+  else              OP(ONE, 0x0f, zcall_1n);
+
+  OP(TWO, 0x01, zje);
+  OP(TWO, 0x02, zjl);
+  OP(TWO, 0x03, zjg);
+  OP(TWO, 0x04, zdec_chk);
+  OP(TWO, 0x05, zinc_chk);
+  OP(TWO, 0x06, zjin);
+  OP(TWO, 0x07, ztest);
+  OP(TWO, 0x08, zor);
+  OP(TWO, 0x09, zand);
+  OP(TWO, 0x0a, ztest_attr);
+  OP(TWO, 0x0b, zset_attr);
+  OP(TWO, 0x0c, zclear_attr);
+  OP(TWO, 0x0d, zstore);
+  OP(TWO, 0x0e, zinsert_obj);
+  OP(TWO, 0x0f, zloadw);
+  OP(TWO, 0x10, zloadb);
+  OP(TWO, 0x11, zget_prop);
+  OP(TWO, 0x12, zget_prop_addr);
+  OP(TWO, 0x13, zget_next_prop);
+  OP(TWO, 0x14, zadd);
+  OP(TWO, 0x15, zsub);
+  OP(TWO, 0x16, zmul);
+  OP(TWO, 0x17, zdiv);
+  OP(TWO, 0x18, zmod);
+  if(zversion >= 4) OP(TWO, 0x19, zcall_2s);
+  if(zversion >= 5) OP(TWO, 0x1a, zcall_2n);
+  if(zversion >= 5) OP(TWO, 0x1b, zset_colour);
+  if(zversion >= 5) OP(TWO, 0x1c, zthrow);
+
+  OP(VAR, 0x00, zcall);
+  OP(VAR, 0x01, zstorew);
+  OP(VAR, 0x02, zstoreb);
+  OP(VAR, 0x03, zput_prop);
+  OP(VAR, 0x04, zread);
+  OP(VAR, 0x05, zprint_char);
+  OP(VAR, 0x06, zprint_num);
+  OP(VAR, 0x07, zrandom);
+  OP(VAR, 0x08, zpush);
+  OP(VAR, 0x09, zpull);
+  if(zversion >= 3) OP(VAR, 0x0a, zsplit_window);
+  if(zversion >= 3) OP(VAR, 0x0b, zset_window);
+  if(zversion >= 4) OP(VAR, 0x0c, zcall_vs2);
+  if(zversion >= 4) OP(VAR, 0x0d, zerase_window);
+  if(zversion >= 4) OP(VAR, 0x0e, zerase_line);
+  if(zversion >= 4) OP(VAR, 0x0f, zset_cursor);
+  if(zversion >= 4) OP(VAR, 0x10, zget_cursor);
+  if(zversion >= 4) OP(VAR, 0x11, zset_text_style);
+  if(zversion >= 4) OP(VAR, 0x12, znop); /* XXX buffer_mode */
+  if(zversion >= 3) OP(VAR, 0x13, zoutput_stream);
+  if(zversion >= 3) OP(VAR, 0x14, zinput_stream);
+  if(zversion >= 3) OP(VAR, 0x15, zsound_effect);
+  if(zversion >= 4) OP(VAR, 0x16, zread_char);
+  if(zversion >= 4) OP(VAR, 0x17, zscan_table);
+  if(zversion >= 5) OP(VAR, 0x18, znot);
+  if(zversion >= 5) OP(VAR, 0x19, zcall_vn);
+  if(zversion >= 5) OP(VAR, 0x1a, zcall_vn2);
+  if(zversion >= 5) OP(VAR, 0x1b, ztokenise);
+  if(zversion >= 5) OP(VAR, 0x1c, zencode_text);
+  if(zversion >= 5) OP(VAR, 0x1d, zcopy_table);
+  if(zversion >= 5) OP(VAR, 0x1e, zprint_table);
+  if(zversion >= 5) OP(VAR, 0x1f, zcheck_arg_count);
+
+  if(zversion >= 5) OP(EXT, 0x00, zsave5);
+  if(zversion >= 5) OP(EXT, 0x01, zrestore5);
+  if(zversion >= 5) OP(EXT, 0x02, zlog_shift);
+  if(zversion >= 5) OP(EXT, 0x03, zart_shift);
+  if(zversion >= 5) OP(EXT, 0x04, zset_font);
+  if(zversion >= 6) OP(EXT, 0x05, znop); /* XXX draw_picture */
+  if(zversion >= 6) OP(EXT, 0x06, zpicture_data);
+  if(zversion >= 6) OP(EXT, 0x07, znop); /* XXX erase_picture */
+  if(zversion >= 6) OP(EXT, 0x08, znop); /* XXX set_margins */
+  if(zversion >= 5) OP(EXT, 0x09, zsave_undo);
+  if(zversion >= 5) OP(EXT, 0x0a, zrestore_undo);
+  if(zversion >= 5) OP(EXT, 0x0b, zprint_unicode);
+  if(zversion >= 5) OP(EXT, 0x0c, zcheck_unicode);
+  if(zversion >= 5) OP(EXT, 0x0d, zset_true_colour);
+  if(zversion >= 6) OP(EXT, 0x10, znop); /* XXX move_window */
+  if(zversion >= 6) OP(EXT, 0x11, znop); /* XXX window_size */
+  if(zversion >= 6) OP(EXT, 0x12, znop); /* XXX window_style */
+  if(zversion >= 6) OP(EXT, 0x13, zget_wind_prop);
+  if(zversion >= 6) OP(EXT, 0x14, znop); /* XXX scroll_window */
+  if(zversion >= 6) OP(EXT, 0x15, zpop_stack);
+  if(zversion >= 6) OP(EXT, 0x16, znop); /* XXX read_mouse */
+  if(zversion >= 6) OP(EXT, 0x17, znop); /* XXX mouse_window */
+  if(zversion >= 6) OP(EXT, 0x18, zpush_stack);
+  if(zversion >= 6) OP(EXT, 0x19, znop); /* XXX put_wind_prop */
+  if(zversion >= 6) OP(EXT, 0x1a, zprint_form);
+  if(zversion >= 6) OP(EXT, 0x1b, zmake_menu);
+  if(zversion >= 6) OP(EXT, 0x1c, znop); /* XXX picture_table */
+  if(zversion >= 6) OP(EXT, 0x1d, zbuffer_screen);
+
+  /* Zoom extensions. */
+  OP(EXT, 0x80, zstart_timer);
+  OP(EXT, 0x81, zstop_timer);
+  OP(EXT, 0x82, zread_timer);
+  OP(EXT, 0x83, zprint_timer);
+#undef OP
+}
+
+void process_instructions(void)
+{
+  if(njumps <= ++ilevel)
+  {
+    jumps = realloc(jumps, ++njumps * sizeof *jumps);
+    if(jumps == NULL) die("unable to allocate memory for jump buffer");
+  }
+
+  switch(setjmp(jumps[ilevel]))
+  {
+    case 1: /* Normal break from interrupt. */
+      return;
+    case 2: /* Special break: a restart was requested. */
+      {
+        /* §6.1.3: Flags2 is preserved on a restart. */
+        uint16_t flags2 = WORD(0x10);
+
+        process_story();
+
+        STORE_WORD(0x10, flags2);
+      }
+      break;
+  }
+
+  while(1)
+  {
+    uint8_t opcode;
+
+#if defined(ZTERP_GLK) && defined(ZTERP_GLK_TICK)
+    glk_tick();
+#endif
+
+    ZPC(pc);
+
+    opcode = BYTE(pc++);
+
+    /* long 2OP */
+    if(opcode < 0x80)
+    {
+      znargs = 2;
+
+      if(opcode & 0x40) zargs[0] = variable(BYTE(pc++));
+      else              zargs[0] = BYTE(pc++);
+
+      if(opcode & 0x20) zargs[1] = variable(BYTE(pc++));
+      else              zargs[1] = BYTE(pc++);
+
+      op_call(TWO, opcode & 0x1f);
+    }
+
+    /* short 1OP */
+    else if(opcode < 0xb0)
+    {
+      znargs = 1;
+
+      if(opcode < 0x90) /* large constant */
+      {
+        zargs[0] = WORD(pc);
+        pc += 2;
+      }
+      else if(opcode < 0xa0) /* small constant */
+      {
+        zargs[0] = BYTE(pc++);
+      }
+      else /* variable */
+      {
+        zargs[0] = variable(BYTE(pc++));
+      }
+
+      op_call(ONE, opcode & 0x0f);
+    }
+
+    /* short 0OP (plus EXT) */
+    else if(opcode < 0xc0)
+    {
+      znargs = 0;
+
+      op_call(ZERO, opcode & 0x0f);
+    }
+
+    /* variable 2OP */
+    else if(opcode < 0xe0)
+    {
+      znargs = 0;
+
+      decode_var(BYTE(pc++));
+
+      op_call(TWO, opcode & 0x1f);
+    }
+
+    /* Double variable VAR */
+    else if(opcode == 0xec || opcode == 0xfa)
+    {
+      uint8_t types1, types2;
+
+      znargs = 0;
+
+      types1 = BYTE(pc++);
+      types2 = BYTE(pc++);
+      decode_var(types1);
+      decode_var(types2);
+
+      op_call(VAR, opcode & 0x1f);
+    }
+
+    /* variable VAR */
+    else
+    {
+      znargs = 0;
+
+      read_pc = pc - 1;
+
+      decode_var(BYTE(pc++));
+
+      op_call(VAR, opcode & 0x1f);
+    }
+  }
+}
diff --git a/interpreters/bocfel/process.h b/interpreters/bocfel/process.h
new file mode 100644 (file)
index 0000000..e98b2c8
--- /dev/null
@@ -0,0 +1,16 @@
+#ifndef ZTERP_PROCESS_h
+#define ZTERP_PROCESS_H
+
+#include <stdint.h>
+
+extern uint16_t zargs[];
+extern int znargs;
+
+void break_from(long);
+void reset_level(void);
+long interrupt_level(void);
+
+void setup_opcodes(void);
+void process_instructions(void);
+
+#endif
diff --git a/interpreters/bocfel/random.c b/interpreters/bocfel/random.c
new file mode 100644 (file)
index 0000000..2d15347
--- /dev/null
@@ -0,0 +1,162 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <limits.h>
+#include <time.h>
+
+#include "random.h"
+#include "process.h"
+#include "zterp.h"
+
+/* Mersenne Twister. */
+static uint32_t mt[624];
+static uint32_t mt_idx = 0;
+
+static void zterp_srand(uint32_t s)
+{
+  mt[0] = s;
+  for(int i = 1; i < 624; i++)
+  {
+    mt[i] = 1812433253UL * (mt[i - 1] ^ (mt[i - 1] >> 30)) + i;
+  }
+  mt_idx = 0;
+}
+
+static void mt_gen_num(void)
+{
+  for(int i = 0; i < 624; i++)
+  {
+    uint32_t y;
+
+    y = mt[i] & 0x80000000UL;
+    y |= (mt[(i + 1) % 624]) & 0x7fffffffUL;
+
+    mt[i] = mt[(i + 397) % 624] ^ (y >> 1);
+
+    if(y % 2 == 1) mt[i] ^= 2567483615UL;
+  }
+}
+
+static uint32_t zterp_rand(void)
+{
+  uint32_t y;
+
+  if(mt_idx == 0) mt_gen_num();
+
+  y = mt[mt_idx];
+  y ^= (y >> 11);
+  y ^= (y << 7) & 2636928640UL;
+  y ^= (y << 15) & 4022730752UL;
+  y ^= (y >> 18);
+
+  mt_idx = (mt_idx + 1) % 624;
+
+  return y;
+}
+
+static int rng_interval = 0;
+static int rng_counter  = 0;
+
+/* Called with 0, seed the PRNG with either
+ * a) a user-provided seed (via -z) if available, or
+ * b) a seed read from a user-provided file/device (via -Z) if
+ *    available, or
+ * c) a seed derived from a hash of the constituent bytes of the value
+ *    returned by time(NULL)
+ *
+ * Called with a value 0 < S < 1000, generate a string of numbers 1, 2,
+ * 3, ..., S, 1, 2, 3, ... S, ... as recommended in the remarks to §2.
+ *
+ * Called with a value >= 1000, use that value as a normal seed.
+ */
+void seed_random(long value)
+{
+  if(value == 0)
+  {
+    if(options.random_seed == -1)
+    {
+      time_t t = time(NULL);
+      unsigned char *p = (unsigned char *)&t;
+      uint32_t s = 0;
+
+      /* time_t hashing based on code by Lawrence Kirby. */
+      for(size_t i = 0; i < sizeof t; i++) s = s * (UCHAR_MAX + 2U) + p[i];
+
+      if(options.random_device != NULL)
+      {
+        FILE *fp;
+        uint32_t temp;
+
+        fp = fopen(options.random_device, "r");
+        if(fp != NULL)
+        {
+          if(fread(&temp, sizeof temp, 1, fp) == 1) s = temp;
+          fclose(fp);
+        }
+      }
+
+      zterp_srand(s);
+    }
+    else
+    {
+      zterp_srand(options.random_seed);
+    }
+
+    rng_interval = 0;
+  }
+  else if(value < 1000)
+  {
+    rng_counter = 0;
+    rng_interval = value;
+  }
+  else
+  {
+    zterp_srand(value);
+    rng_interval = 0;
+  }
+}
+
+void zrandom(void)
+{
+  long v = (int16_t)zargs[0];
+
+  if(v <= 0)
+  {
+    seed_random(-v);
+    store(0);
+  }
+  else
+  {
+    uint32_t res;
+
+    if(rng_interval != 0)
+    {
+      res = rng_counter++;
+      if(rng_counter == rng_interval) rng_counter = 0;
+    }
+    else
+    {
+      res = zterp_rand();
+    }
+
+    store(res % zargs[0] + 1);
+  }
+}
diff --git a/interpreters/bocfel/random.h b/interpreters/bocfel/random.h
new file mode 100644 (file)
index 0000000..5968432
--- /dev/null
@@ -0,0 +1,8 @@
+#ifndef ZTERP_MT_H
+#define ZTERP_MT_H
+
+void seed_random(long);
+
+void zrandom(void);
+
+#endif
diff --git a/interpreters/bocfel/screen.c b/interpreters/bocfel/screen.c
new file mode 100644 (file)
index 0000000..35feb56
--- /dev/null
@@ -0,0 +1,2475 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <stdarg.h>
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#include <libchimara/garglk.h>
+#endif
+
+#include "screen.h"
+#include "branch.h"
+#include "dict.h"
+#include "io.h"
+#include "memory.h"
+#include "objects.h"
+#include "osdep.h"
+#include "process.h"
+#include "stack.h"
+#include "unicode.h"
+#include "util.h"
+#include "zterp.h"
+
+static struct window
+{
+  unsigned style;
+
+  enum font { FONT_NONE = -1, FONT_PREVIOUS, FONT_NORMAL, FONT_PICTURE, FONT_CHARACTER, FONT_FIXED } font;
+  enum font prev_font;
+
+#ifdef ZTERP_GLK
+  winid_t id;
+  long x, y; /* Only meaningful for window 1 */
+  int pending_read;
+  union line
+  {
+    char latin1[256];
+    glui32 unicode[256];
+  } *line;
+  int has_echo;
+#endif
+} windows[8], *mainwin = &windows[0], *curwin = &windows[0];
+#ifdef ZTERP_GLK
+static struct window *upperwin = &windows[1];
+static struct window statuswin;
+static long upper_window_height = 0;
+static long upper_window_width = 0;
+static winid_t errorwin;
+#endif
+
+/* In all versions but 6, styles are global and stored in mainwin.  For
+ * V6, styles are tracked per window and thus stored in each individual
+ * window.  For convenience, this macro expands to the “style window”
+ * for any version.
+ */
+#define style_window   (zversion == 6 ? curwin : mainwin)
+
+/* If the window needs to be temporarily switched (@show_status and
+ * @print_form print to specific windows, and window_change() might
+ * temporarily need to switch to the upper window), the code that
+ * requires a specific window can be wrapped in these macros.
+ */
+#ifdef ZTERP_GLK
+#define SWITCH_WINDOW_START(win)       { struct window *saved_ = curwin; curwin = (win); glk_set_window((win)->id);
+#define SWITCH_WINDOW_END()            curwin = saved_; glk_set_window(curwin->id); }
+#else
+#define SWITCH_WINDOW_START(win)       { struct window *saved_ = curwin; curwin = (win);
+#define SWITCH_WINDOW_END()            curwin = saved_; }
+#endif
+
+/* Output stream bits. */
+#define STREAM_SCREEN          (1U << 1)
+#define STREAM_TRANS           (1U << 2)
+#define STREAM_MEMORY          (1U << 3)
+#define STREAM_SCRIPT          (1U << 4)
+
+static unsigned int streams = STREAM_SCREEN;
+static zterp_io *transio, *scriptio;
+
+static struct
+{
+  uint16_t table;
+  uint16_t i;
+} stables[16];
+static int stablei = -1;
+
+static int istream = ISTREAM_KEYBOARD;
+static zterp_io *istreamio;
+
+struct input
+{
+  enum { INPUT_CHAR, INPUT_LINE } type;
+
+  /* ZSCII value of key read for @read_char. */
+  uint8_t key;
+
+  /* Unicode line of chars read for @read. */
+  uint32_t *line;
+  uint8_t maxlen;
+  uint8_t len;
+  uint8_t preloaded;
+
+  /* Character used to terminate input.  If terminating keys are not
+   * supported by the Glk implementation being used (or if Glk is not
+   * used at all) this will be ZSCII_NEWLINE; or in the case of
+   * cancellation, 0.
+   */
+  uint8_t term;
+};
+
+/* This macro makes it so that code elsewhere needn’t check have_unicode before printing. */
+#define GLK_PUT_CHAR(c)                do { if(!have_unicode) glk_put_char(unicode_to_latin1[c]); else glk_put_char_uni(c); } while(0)
+
+void show_message(const char *fmt, ...)
+{
+  va_list ap;
+  char message[1024];
+
+  va_start(ap, fmt);
+  vsnprintf(message, sizeof message, fmt, ap);
+  va_end(ap);
+
+#ifdef ZTERP_GLK
+  static int error_lines = 0;
+
+  if(errorwin != NULL)
+  {
+    glui32 w, h;
+
+    /* Allow multiple messages to stack, but force at least 5 lines to
+     * always be visible in the main window.  This is less than perfect
+     * because it assumes that each message will be less than the width
+     * of the screen, but it’s not a huge deal, really; even if the
+     * lines are too long, at least Gargoyle and glktermw are graceful
+     * enough.
+     */
+    glk_window_get_size(mainwin->id, &w, &h);
+
+    if(h > 5) glk_window_set_arrangement(glk_window_get_parent(errorwin), winmethod_Below | winmethod_Fixed, ++error_lines, errorwin);
+    glk_put_char_stream(glk_window_get_stream(errorwin), UNICODE_LINEFEED);
+  }
+  else
+  {
+    errorwin = glk_window_open(mainwin->id, winmethod_Below | winmethod_Fixed, error_lines = 2, wintype_TextBuffer, 0);
+  }
+
+  /* If windows are not supported (e.g. in cheapglk), messages will not
+   * get displayed.  If this is the case, print to the main window.
+   */
+  if(errorwin != NULL)
+  {
+    glk_set_style_stream(glk_window_get_stream(errorwin), style_Alert);
+    glk_put_string_stream(glk_window_get_stream(errorwin), message);
+  }
+  else
+  {
+    SWITCH_WINDOW_START(mainwin);
+    glk_put_string("\12[");
+    glk_put_string(message);
+    glk_put_string("]\12");
+    SWITCH_WINDOW_END();
+  }
+#else
+  /* In Glk messages go to a separate window, but they're interleaved in
+   * non-Glk.  Put brackets around the message in an attempt to offset
+   * it from the game a bit.
+   */
+  fprintf(stderr, "\n[%s]\n", message);
+#endif
+}
+
+/* See §7.
+ * This returns true if the stream was successfully selected.
+ * Deselecting a stream is always successful.
+ */
+int output_stream(int16_t number, uint16_t table)
+{
+  if(number > 0)
+  {
+    streams |= 1U << number;
+  }
+  else if(number < 0)
+  {
+    if(number != -3 || stablei == 0) streams &= ~(1U << -number);
+  }
+
+  if(number == 2)
+  {
+    STORE_WORD(0x10, WORD(0x10) | FLAGS2_TRANSCRIPT);
+    if(transio == NULL)
+    {
+      transio = zterp_io_open(options.transcript_name, ZTERP_IO_TRANS | (options.overwrite_transcript ? ZTERP_IO_WRONLY : ZTERP_IO_APPEND));
+      if(transio == NULL)
+      {
+        STORE_WORD(0x10, WORD(0x10) & ~FLAGS2_TRANSCRIPT);
+        streams &= ~STREAM_TRANS;
+        warning("unable to open the transcript");
+      }
+    }
+  }
+  else if(number == -2)
+  {
+    STORE_WORD(0x10, WORD(0x10) & ~FLAGS2_TRANSCRIPT);
+  }
+
+  if(number == 3)
+  {
+    stablei++;
+    ZASSERT(stablei < 16, "too many stream tables");
+
+    stables[stablei].table = table;
+    user_store_word(stables[stablei].table, 0);
+    stables[stablei].i = 2;
+  }
+  else if(number == -3 && stablei >= 0)
+  {
+    user_store_word(stables[stablei].table, stables[stablei].i - 2);
+    stablei--;
+  }
+
+  if(number == 4)
+  {
+    if(scriptio == NULL)
+    {
+      scriptio = zterp_io_open(options.record_name, ZTERP_IO_WRONLY | ZTERP_IO_INPUT);
+      if(scriptio == NULL)
+      {
+        streams &= ~STREAM_SCRIPT;
+        warning("unable to open the script");
+      }
+    }
+  }
+  /* XXX v6 has even more handling */
+
+  return number < 0 || (streams & (1U << number));
+}
+
+void zoutput_stream(void)
+{
+  output_stream(zargs[0], zargs[1]);
+}
+
+/* See §10.
+ * This returns true if the stream was successfully selected.
+ */
+int input_stream(int which)
+{
+  istream = which;
+
+  if(istream == ISTREAM_KEYBOARD)
+  {
+    if(istreamio != NULL)
+    {
+      zterp_io_close(istreamio);
+      istreamio = NULL;
+    }
+  }
+  else if(istream == ISTREAM_FILE)
+  {
+    if(istreamio == NULL)
+    {
+      istreamio = zterp_io_open(options.replay_name, ZTERP_IO_INPUT | ZTERP_IO_RDONLY);
+      if(istreamio == NULL)
+      {
+        warning("unable to open the command script");
+        istream = ISTREAM_KEYBOARD;
+      }
+    }
+  }
+  else
+  {
+    ZASSERT(0, "invalid input stream: %d", istream);
+  }
+
+  return istream == which;
+}
+
+void zinput_stream(void)
+{
+  input_stream(zargs[0]);
+}
+
+/* This does not even pretend to understand V6 windows. */
+static void set_current_window(struct window *window)
+{
+  curwin = window;
+
+#ifdef ZTERP_GLK
+  if(curwin == upperwin && upperwin->id != NULL)
+  {
+    upperwin->x = upperwin->y = 0;
+    glk_window_move_cursor(upperwin->id, 0, 0);
+  }
+
+  glk_set_window(curwin->id);
+#endif
+
+  set_current_style();
+}
+
+/* Find and validate a window.  If window is -3 and the story is V6,
+ * return the current window.
+ */
+static struct window *find_window(uint16_t window)
+{
+  int16_t w = window;
+
+  ZASSERT(zversion == 6 ? w == -3 || (w >= 0 && w < 8) : w == 0 || w == 1, "invalid window selected: %d", w);
+
+  if(w == -3) return curwin;
+
+  return &windows[w];
+}
+
+#ifdef ZTERP_GLK
+/* When resizing the upper window, the screen’s contents should not
+ * change (§8.6.1); however, the way windows are handled with Glk makes
+ * this slightly impossible.  When an Inform game tries to display
+ * something with “box”, it expands the upper window, displays the quote
+ * box, and immediately shrinks the window down again.  This is a
+ * problem under Glk because the window immediately disappears.  Other
+ * games, such as Bureaucracy, expect the upper window to shrink as soon
+ * as it has been requested.  Thus the following system is used:
+ *
+ * If a request is made to shrink the upper window, it is granted
+ * immediately if there has been user input since the last window resize
+ * request.  If there has not been user input, the request is delayed
+ * until after the next user input is read.
+ */
+static long delayed_window_shrink = -1;
+static int saw_input;
+
+static void update_delayed(void)
+{
+  glui32 height;
+
+  if(delayed_window_shrink == -1 || upperwin->id == NULL) return;
+
+  glk_window_set_arrangement(glk_window_get_parent(upperwin->id), winmethod_Above | winmethod_Fixed, delayed_window_shrink, upperwin->id);
+  upper_window_height = delayed_window_shrink;
+
+  /* Glk might resize the window to a smaller height than was requested,
+   * so track the actual height, not the requested height.
+   */
+  glk_window_get_size(upperwin->id, NULL, &height);
+  if(height != upper_window_height)
+  {
+    /* This message probably won’t be seen in a window since the upper
+     * window is likely covering everything, but try anyway.
+     */
+    show_message("Unable to fulfill window size request: wanted %ld, got %lu", delayed_window_shrink, (unsigned long)height);
+    upper_window_height = height;
+  }
+
+  delayed_window_shrink = -1;
+}
+
+/* Both the upper and lower windows have their own issues to deal with
+ * when there is line input.  This function ensures that the cursor
+ * position is properly tracked in the upper window, and if possible,
+ * aids in the suppression of newline printing on input cancellation in
+ * the lower window.
+ */
+static void cleanup_screen(struct input *input)
+{
+  if(input->type != INPUT_LINE) return;
+
+  /* If the current window is the upper window, the position of the
+   * cursor needs to be tracked, so after a line has successfully been
+   * read, advance the cursor to the initial position of the next line,
+   * or if a terminating key was used or input was canceled, to the end
+   * of the input.
+   */
+  if(curwin == upperwin)
+  {
+    if(input->term != ZSCII_NEWLINE) upperwin->x += input->len;
+
+    if(input->term == ZSCII_NEWLINE || upperwin->x >= upper_window_width)
+    {
+      upperwin->x = 0;
+      if(upperwin->y < upper_window_height) upperwin->y++;
+    }
+
+    glk_window_move_cursor(upperwin->id, upperwin->x, upperwin->y);
+  }
+
+  /* If line input echoing is turned off, newlines will not be printed
+   * when input is canceled, but neither will the input line.  Fix that.
+   */
+  if(curwin->has_echo)
+  {
+    glk_set_style(style_Input);
+    for(int i = 0; i < input->len; i++) GLK_PUT_CHAR(input->line[i]);
+    if(input->term == ZSCII_NEWLINE) glk_put_char(UNICODE_LINEFEED);
+    set_current_style();
+  }
+}
+
+/* In an interrupt, if the story tries to read or write, the previous
+ * read event (which triggered the interrupt) needs to be canceled.
+ * This function does the cancellation.
+ */
+static void cancel_read_events(struct window *window)
+{
+  if(window->pending_read)
+  {
+    event_t ev;
+
+    glk_cancel_char_event(window->id);
+    glk_cancel_line_event(window->id, &ev);
+
+    /* If the pending read was a line input, zero terminate the string
+     * so when it’s re-requested the length of the already-loaded
+     * portion can be discovered.  Also deal with cursor positioning in
+     * the upper window, and line echoing in the lower window.
+     */
+    if(ev.type == evtype_LineInput && window->line != NULL)
+    {
+      uint32_t line[ev.val1];
+      struct input input = { .type = INPUT_LINE, .line = line, .term = 0, .len = ev.val1 };
+
+      if(have_unicode) window->line->unicode[ev.val1] = 0;
+      else             window->line->latin1 [ev.val1] = 0;
+
+      for(int i = 0; i < input.len; i++)
+      {
+        if(have_unicode) line[i] = window->line->unicode[i];
+        else             line[i] = window->line->latin1 [i];
+      }
+
+      cleanup_screen(&input);
+    }
+
+    window->pending_read = 0;
+    window->line = NULL;
+  }
+}
+
+static void clear_window(struct window *window)
+{
+  if(window->id == NULL) return;
+
+  /* glk_window_clear() cannot be used while there are pending read requests. */
+  cancel_read_events(window);
+
+  glk_window_clear(window->id);
+
+  window->x = window->y = 0;
+}
+#endif
+
+/* If restoring from an interrupt (which is a bad idea to begin with),
+ * it’s entirely possible that there will be pending read events that
+ * need to be canceled, so allow that.
+ */
+void cancel_all_events(void)
+{
+#ifdef ZTERP_GLK
+  for(int i = 0; i < 8; i++) cancel_read_events(&windows[i]);
+#endif
+}
+
+static void resize_upper_window(long nlines)
+{
+#ifdef ZTERP_GLK
+  if(upperwin->id == NULL) return;
+
+  /* To avoid code duplication, put all window resizing code in
+   * update_delayed() and, if necessary, call it from here.
+   */
+  delayed_window_shrink = nlines;
+  if(upper_window_height <= nlines || saw_input) update_delayed();
+
+  saw_input = 0;
+
+  /* §8.6.1.1.2 */
+  if(zversion == 3) clear_window(upperwin);
+
+  /* As in a few other areas, changing the upper window causes reverse
+   * video to be deactivated, so reapply the current style.
+   */
+  set_current_style();
+#endif
+}
+
+void close_upper_window(void)
+{
+  /* The upper window is never destroyed; rather, when it’s closed, it
+   * shrinks to zero height.
+   */
+  resize_upper_window(0);
+
+#ifdef ZTERP_GLK
+  delayed_window_shrink = -1;
+  saw_input = 0;
+#endif
+
+  set_current_window(mainwin);
+}
+
+void get_screen_size(unsigned int *width, unsigned int *height)
+{
+  *width  = 80;
+  *height = 24;
+
+#ifdef ZTERP_GLK
+  glui32 w, h;
+
+  /* The main window can be proportional, and if so, its width is not
+   * generally useful because games tend to care about width with a
+   * fixed font.  If a status window is available, or if an upper window
+   * is available, use that to calculate the width, because these
+   * windows will have a fixed-width font.  The height is the combined
+   * height of all windows.
+   */
+  glk_window_get_size(mainwin->id, &w, &h);
+  *height = h;
+  if(statuswin.id != NULL)
+  {
+    glk_window_get_size(statuswin.id, &w, &h);
+    *height += h;
+  }
+  if(upperwin->id != NULL)
+  {
+    glk_window_get_size(upperwin->id, &w, &h);
+    *height += h;
+  }
+  *width = w;
+#else
+  zterp_os_get_screen_size(width, height);
+#endif
+
+  /* XGlk does not report the size of textbuffer windows, so here’s a safety net. */
+  if(*width  == 0) *width  = 80;
+  if(*height == 0) *height = 24;
+
+  /* Terrible hack: Because V6 is not properly supported, the window to
+   * which Journey writes its story is completely covered up by window
+   * 1.  For the same reason, only the bottom 6 lines of window 1 are
+   * actually useful, even though the game expands it to cover the whole
+   * screen.  By pretending that the screen height is only 6, the main
+   * window, where text is actually sent, becomes visible.
+   */
+  if(is_story("83-890706") && *height > 6) *height = 6;
+}
+
+#ifdef GLK_MODULE_LINE_TERMINATORS
+static uint32_t *term_keys, term_size, term_nkeys;
+
+void term_keys_reset(void)
+{
+  free(term_keys);
+  term_keys = NULL;
+  term_size = 0;
+  term_nkeys = 0;
+}
+
+static void insert_key(uint32_t key)
+{
+  if(term_nkeys == term_size)
+  {
+    term_size += 32;
+
+    term_keys = realloc(term_keys, term_size * sizeof *term_keys);
+    if(term_keys == NULL) die("unable to allocate memory for terminating keys");
+  }
+
+  term_keys[term_nkeys++] = key;
+}
+
+void term_keys_add(uint8_t key)
+{
+  switch(key)
+  {
+    case 129: insert_key(keycode_Up); break;
+    case 130: insert_key(keycode_Down); break;
+    case 131: insert_key(keycode_Left); break;
+    case 132: insert_key(keycode_Right); break;
+    case 133: insert_key(keycode_Func1); break;
+    case 134: insert_key(keycode_Func2); break;
+    case 135: insert_key(keycode_Func3); break;
+    case 136: insert_key(keycode_Func4); break;
+    case 137: insert_key(keycode_Func5); break;
+    case 138: insert_key(keycode_Func6); break;
+    case 139: insert_key(keycode_Func7); break;
+    case 140: insert_key(keycode_Func8); break;
+    case 141: insert_key(keycode_Func9); break;
+    case 142: insert_key(keycode_Func10); break;
+    case 143: insert_key(keycode_Func11); break;
+    case 144: insert_key(keycode_Func12); break;
+
+    /* Keypad 0–9 should be here, but Glk doesn’t support that. */
+    case 145: case 146: case 147: case 148: case 149:
+    case 150: case 151: case 152: case 153: case 154:
+      break;
+
+    /* Mouse clicks would go here if I supported them. */
+    case 252: case 253: case 254:
+      break;
+
+    case 255:
+      for(int i = 129; i <= 144; i++) term_keys_add(i);
+      break;
+
+    default:
+      ZASSERT(0, "invalid terminating key: %u", (unsigned)key);
+      break;
+  }
+}
+#endif
+
+/* Print out a character.  The character is in “c” and is either Unicode
+ * or ZSCII; if the former, “unicode” is true.
+ */
+static void put_char_base(uint16_t c, int unicode)
+{
+  if(c == 0) return;
+
+  if(streams & STREAM_MEMORY)
+  {
+    ZASSERT(stablei != -1, "invalid stream table");
+
+    /* When writing to memory, ZSCII should always be used (§7.5.3). */
+    if(unicode) c = unicode_to_zscii_q[c];
+
+    user_store_byte(stables[stablei].table + stables[stablei].i++, c);
+  }
+  else
+  {
+    /* For screen and transcription, always prefer Unicode. */
+    if(!unicode) c = zscii_to_unicode[c];
+
+    if(c != 0)
+    {
+      uint8_t zscii = 0;
+
+      /* §16 makes no mention of what a newline in font 3 should map to.
+       * Other interpreters that implement font 3 assume it stays a
+       * newline, and this makes the most sense, so don’t do any
+       * translation in that case.
+       */
+      if(curwin->font == FONT_CHARACTER && !options.disable_graphics_font && c != UNICODE_LINEFEED)
+      {
+        zscii = unicode_to_zscii[c];
+
+        /* These four characters have a “built-in” reverse video (see §16). */
+        if(zscii >= 123 && zscii <= 126)
+        {
+          style_window->style ^= STYLE_REVERSE;
+          set_current_style();
+        }
+
+        c = zscii_to_font3[zscii];
+      }
+#ifdef ZTERP_GLK
+      if((streams & STREAM_SCREEN) && curwin->id != NULL)
+      {
+        cancel_read_events(curwin);
+
+        if(curwin == upperwin)
+        {
+          /* Interpreters seem to have differing ideas about what
+           * happens when the cursor reaches the end of a line in the
+           * upper window.  Some wrap, some let it run off the edge (or,
+           * at least, stop the text at the edge).  The standard, from
+           * what I can see, says nothing on this issue.  Follow Windows
+           * Frotz and don’t wrap.
+           */
+
+          if(c == UNICODE_LINEFEED)
+          {
+            if(upperwin->y < upper_window_height)
+            {
+              /* Glk wraps, so printing a newline when the cursor has
+               * already reached the edge of the screen will produce two
+               * newlines.
+               */
+              if(upperwin->x < upper_window_width) GLK_PUT_CHAR(c);
+
+              /* Even if a newline isn’t explicitly printed here
+               * (because the cursor is at the edge), setting
+               * upperwin->x to 0 will cause the next character to be on
+               * the next line because the text will have wrapped.
+               */
+              upperwin->x = 0;
+              upperwin->y++;
+            }
+          }
+          else if(upperwin->x < upper_window_width && upperwin->y < upper_window_height)
+          {
+            upperwin->x++;
+            GLK_PUT_CHAR(c);
+          }
+        }
+        else
+        {
+          GLK_PUT_CHAR(c);
+        }
+      }
+#else
+      if((streams & STREAM_SCREEN) && curwin == mainwin) zterp_io_putc(zterp_io_stdout(), c);
+#endif
+
+      /* If the reverse video bit was flipped (for the character font), flip it back. */
+      if(zscii >= 123 && zscii <= 126)
+      {
+        style_window->style ^= STYLE_REVERSE;
+        set_current_style();
+      }
+
+      if((streams & STREAM_TRANS) && curwin == mainwin) zterp_io_putc(transio, c);
+    }
+  }
+}
+
+void put_char_u(uint16_t c)
+{
+  put_char_base(c, 1);
+}
+
+void put_char(uint8_t c)
+{
+  put_char_base(c, 0);
+}
+
+static void put_string(const char *s)
+{
+  for(; *s != 0; s++)
+  {
+    if(*s == '\n') put_char(ZSCII_NEWLINE);
+    else           put_char(*s);
+  }
+}
+
+/* Decode and print a zcode string at address “addr”.  This can be
+ * called recursively thanks to abbreviations; the initial call should
+ * have “in_abbr” set to 0.
+ * Each time a character is decoded, it is passed to the function
+ * “outc”.
+ */
+static int print_zcode(uint32_t addr, int in_abbr, void (*outc)(uint8_t))
+{
+  int abbrev = 0, shift = 0, special = 0;
+  int c, lastc = 0; /* Initialize lastc to shut gcc up */
+  uint16_t w;
+  uint32_t counter = addr;
+  int current_alphabet = 0;
+
+  do
+  {
+    ZASSERT(counter < memory_size - 1, "string runs beyond the end of memory");
+
+    w = WORD(counter);
+
+    for(int i = 10; i >= 0; i -= 5)
+    {
+      c = (w >> i) & 0x1f;
+
+      if(special)
+      {
+        if(special == 2) lastc = c;
+        else             outc((lastc << 5) | c);
+
+        special--;
+      }
+
+      else if(abbrev)
+      {
+        uint32_t new_addr;
+
+        new_addr = user_word(header.abbr + 64 * (abbrev - 1) + 2 * c);
+
+        /* new_addr is a word address, so multiply by 2 */
+        print_zcode(new_addr * 2, 1, outc);
+
+        abbrev = 0;
+      }
+
+      else switch(c)
+      {
+        case 0:
+          outc(ZSCII_SPACE);
+          shift = 0;
+          break;
+        case 1:
+          if(zversion == 1)
+          {
+            outc(ZSCII_NEWLINE);
+            shift = 0;
+            break;
+          }
+          /* fallthrough */
+        case 2: case 3:
+          if(zversion >= 3 || (zversion == 2 && c == 1))
+          {
+            ZASSERT(!in_abbr, "abbreviation being used recursively");
+            abbrev = c;
+            shift = 0;
+          }
+          else
+          {
+            shift = c - 1;
+          }
+          break;
+        case 4: case 5:
+          if(zversion <= 2)
+          {
+            current_alphabet = (current_alphabet + (c - 3)) % 3;
+            shift = 0;
+          }
+          else
+          {
+            shift = c - 3;
+          }
+          break;
+        case 6:
+          if(zversion <= 2) shift = (current_alphabet + shift) % 3;
+
+          if(shift == 2)
+          {
+            shift = 0;
+            special = 2;
+            break;
+          }
+          /* fallthrough */
+        default:
+          if(zversion <= 2 && c != 6) shift = (current_alphabet + shift) % 3;
+
+          outc(atable[(26 * shift) + (c - 6)]);
+          shift = 0;
+          break;
+      }
+    }
+
+    counter += 2;
+  } while((w & 0x8000) == 0);
+
+  return counter - addr;
+}
+
+/* Prints the string at addr “addr”.
+ *
+ * Returns the number of bytes the string took up.  “outc” is passed as
+ * the character-print function to print_zcode(); if it is NULL,
+ * put_char is used.
+ */
+int print_handler(uint32_t addr, void (*outc)(uint8_t))
+{
+  return print_zcode(addr, 0, outc != NULL ? outc : put_char);
+}
+
+void zprint(void)
+{
+  pc += print_handler(pc, NULL);
+}
+
+void zprint_ret(void)
+{
+  zprint();
+  put_char(ZSCII_NEWLINE);
+  zrtrue();
+}
+
+void znew_line(void)
+{
+  put_char(ZSCII_NEWLINE);
+}
+
+void zerase_window(void)
+{
+#ifdef ZTERP_GLK
+  switch((int16_t)zargs[0])
+  {
+    case -2:
+      for(int i = 0; i < 8; i++) clear_window(&windows[i]);
+      break;
+    case -1:
+      close_upper_window();
+      /* fallthrough */
+    case 0:
+      /* 8.7.3.2.1 says V5+ should have the cursor set to 1, 1 of the
+       * erased window; V4 the lower window goes bottom left, the upper
+       * to 1, 1.  Glk doesn’t give control over the cursor when
+       * clearing, and that doesn’t really seem to be an issue; so just
+       * call glk_window_clear().
+       */
+      clear_window(mainwin);
+      break;
+    case 1:
+      clear_window(upperwin);
+      break;
+    default:
+      show_message("@erase_window: unhandled window: %d", (int16_t)zargs[0]);
+      break;
+  }
+
+  /* glk_window_clear() kills reverse video in Gargoyle.  Reapply style. */
+  set_current_style();
+#endif
+}
+
+void zerase_line(void)
+{
+#ifdef ZTERP_GLK
+  /* XXX V6 does pixel handling here. */
+  if(zargs[0] != 1 || curwin != upperwin || upperwin->id == NULL) return;
+
+  for(long i = upperwin->x; i < upper_window_width; i++) GLK_PUT_CHAR(UNICODE_SPACE);
+
+  glk_window_move_cursor(upperwin->id, upperwin->x, upperwin->y);
+#endif
+}
+
+/* XXX This is more complex in V6 and needs to be updated when V6 windowing is implemented. */
+static void set_cursor(uint16_t y, uint16_t x)
+{
+#ifdef ZTERP_GLK
+  /* All the windows in V6 can have their cursor positioned; if V6 ever
+   * comes about this should be fixed.
+   */
+  if(curwin != upperwin) return;
+
+  /* -1 and -2 are V6 only, but at least Zracer passes -1 (or it’s
+   * trying to position the cursor to line 65535; unlikely!)
+   */
+  if((int16_t)y == -1 || (int16_t)y == -2) return;
+
+  /* §8.7.2.3 says 1,1 is the top-left, but at least one program (Paint
+   * and Corners) uses @set_cursor 0 0 to go to the top-left; so
+   * special-case it.
+   */
+  if(y == 0) y = 1;
+  if(x == 0) x = 1;
+
+  /* This is actually illegal, but some games (e.g. Beyond Zork) expect it to work. */
+  if(y > upper_window_height) resize_upper_window(y);
+
+  if(upperwin->id != NULL)
+  {
+    upperwin->x = x - 1;
+    upperwin->y = y - 1;
+
+    glk_window_move_cursor(upperwin->id, x - 1, y - 1);
+  }
+#endif
+}
+
+void zset_cursor(void)
+{
+  set_cursor(zargs[0], zargs[1]);
+}
+
+void zget_cursor(void)
+{
+#ifdef ZTERP_GLK
+  user_store_word(zargs[0] + 0, upperwin->y + 1);
+  user_store_word(zargs[0] + 2, upperwin->x + 1);
+#else
+  user_store_word(zargs[0] + 0, 1);
+  user_store_word(zargs[0] + 2, 1);
+#endif
+}
+
+#ifndef ZTERP_GLK
+static int16_t fg_color = 1, bg_color = 1;
+#elif defined(GARGLK)
+static glui32 zcolor_map[] = {
+  zcolor_Default,
+
+  0x000000,    /* Black */
+  0xef0000,    /* Red */
+  0x00d600,    /* Green */
+  0xefef00,    /* Yellow */
+  0x006bb5,    /* Blue */
+  0xff00ff,    /* Magenta */
+  0x00efef,    /* Cyan */
+  0xffffff,    /* White */
+  0xb5b5b5,    /* Light grey */
+  0x8c8c8c,    /* Medium grey */
+  0x5a5a5a,    /* Dark grey */
+};
+static glui32 fg_color = zcolor_Default, bg_color = zcolor_Default;
+
+void update_color(int which, unsigned long color)
+{
+  if(which < 2 || which > 12) return;
+
+  zcolor_map[which - 1] = color;
+}
+#endif
+
+/* A window argument may be supplied in V6, and this needs to be implemented. */
+void zset_colour(void)
+{
+  /* Glk (apart from Gargoyle) has no color support. */
+#if !defined(ZTERP_GLK) || defined(GARGLK)
+  int16_t fg = zargs[0], bg = zargs[1];
+
+  /* In V6, each window has its own color settings.  Since multiple
+   * windows are not supported, simply ignore all color requests except
+   * those in the main window.
+   */
+  if(zversion == 6 && curwin != mainwin) return;
+
+  if(options.disable_color) return;
+
+  /* XXX -1 is a valid color in V6. */
+#ifdef GARGLK
+  if(fg >= 1 && fg <= (zversion >= 5 ? 12 : 9)) fg_color = zcolor_map[fg - 1];
+  if(bg >= 1 && bg <= (zversion >= 5 ? 12 : 9)) bg_color = zcolor_map[bg - 1];
+
+#else
+  if(fg >= 1 && fg <= 9) fg_color = fg;
+  if(bg >= 1 && bg <= 9) bg_color = bg;
+#endif
+
+  set_current_style();
+#endif
+}
+
+#ifdef GARGLK
+/* Convert a 15-bit color to a 24-bit color. */
+static glui32 convert_color(unsigned long color)
+{
+  /* Map 5-bit color values to 8-bit. */
+  const uint8_t table[] = {
+    0x00, 0x08, 0x10, 0x19, 0x21, 0x29, 0x31, 0x3a,
+    0x42, 0x4a, 0x52, 0x5a, 0x63, 0x6b, 0x73, 0x7b,
+    0x84, 0x8c, 0x94, 0x9c, 0xa5, 0xad, 0xb5, 0xbd,
+    0xc5, 0xce, 0xd6, 0xde, 0xe6, 0xef, 0xf7, 0xff
+  };
+
+  return table[(color & 0x001f) >>  0] << 16 |
+         table[(color & 0x03e0) >>  5] << 8  |
+         table[(color & 0x7c00) >> 10] << 0;
+}
+#endif
+
+void zset_true_colour(void)
+{
+#ifdef GARGLK
+  long fg = (int16_t)zargs[0], bg = (int16_t)zargs[1];
+
+  if     (fg >=  0) fg_color = convert_color(fg);
+  else if(fg == -1) fg_color = zcolor_Default;
+
+  if     (bg >=  0) bg_color = convert_color(bg);
+  else if(bg == -1) bg_color = zcolor_Default;
+
+  set_current_style();
+#endif
+}
+
+int header_fixed_font;
+
+#ifdef GARGLK
+/* Idea from Nitfol. */
+static const int style_map[] =
+{
+  style_Normal,
+  style_Normal,
+
+  style_Subheader,     /* Bold */
+  style_Subheader,     /* Bold */
+  style_Emphasized,    /* Italic */
+  style_Emphasized,    /* Italic */
+  style_Alert,         /* Bold Italic */
+  style_Alert,         /* Bold Italic */
+  style_Preformatted,  /* Fixed */
+  style_Preformatted,  /* Fixed */
+  style_User1,         /* Bold Fixed */
+  style_User1,         /* Bold Fixed */
+  style_User2,         /* Italic Fixed */
+  style_User2,         /* Italic Fixed */
+  style_Note,          /* Bold Italic Fixed */
+  style_Note,          /* Bold Italic Fixed */
+};
+#endif
+
+/* Yes, there are three ways to indicate that a fixed-width font should be used. */
+#define use_fixed_font() (header_fixed_font || curwin->font == FONT_FIXED || (style & STYLE_FIXED))
+
+void set_current_style(void)
+{
+  unsigned style = style_window->style;
+#ifdef ZTERP_GLK
+  if(curwin->id == NULL) return;
+
+#ifdef GARGLK
+  if(use_fixed_font()) style |= STYLE_FIXED;
+
+  if(options.disable_fixed) style &= ~STYLE_FIXED;
+
+  ZASSERT(style < 16, "invalid style selected: %x", (unsigned)style);
+
+  glk_set_style(style_map[style]);
+
+  garglk_set_reversevideo(style & STYLE_REVERSE);
+
+  garglk_set_zcolors(fg_color, bg_color);
+#else
+  /* Glk can’t mix other styles with fixed-width, but the upper window
+   * is always fixed, so if it is selected, there is no need to
+   * explicitly request it here.  In addition, the user can disable
+   * fixed-width fonts or tell Bocfel to assume that the output font is
+   * already fixed (e.g. in an xterm); in either case, there is no need
+   * to request a fixed font.
+   * This means that another style can also be applied if applicable.
+   */
+  if(use_fixed_font() &&
+     !options.disable_fixed &&
+     !options.assume_fixed &&
+     curwin != upperwin)
+  {
+    glk_set_style(style_Preformatted);
+    return;
+  }
+
+  /* According to standard 1.1, if mixed styles aren't available, the
+   * priority is Fixed, Italic, Bold, Reverse.
+   */
+  if     (style & STYLE_ITALIC)  glk_set_style(style_Emphasized);
+  else if(style & STYLE_BOLD)    glk_set_style(style_Subheader);
+  else if(style & STYLE_REVERSE) glk_set_style(style_Alert);
+  else                           glk_set_style(style_Normal);
+#endif
+#else
+  zterp_os_set_style(style, fg_color, bg_color);
+#endif
+}
+
+#undef use_fixed_font
+
+/* V6 has per-window styles, but all others have a global style; in this
+ * case, track styles via the main window.
+ */
+void zset_text_style(void)
+{
+  /* A style of 0 means all others go off. */
+  if(zargs[0] == 0) style_window->style = STYLE_NONE;
+  else              style_window->style |= zargs[0];
+
+  set_current_style();
+}
+
+/* Interpreters seem to disagree on @set_font.  Given the code
+
+   @set_font 4 -> i;
+   @set_font 1 -> j;
+   @set_font 0 -> k;
+   @set_font 1 -> l;
+
+ * the following values are returned:
+ * Frotz 2.43:         0, 1, 1, 1
+ * Gargoyle r384:      1, 4, 4, 4
+ * Fizmo 0.6.5:        1, 4, 1, 0
+ * Nitfol 0.5:         1, 4, 0, 1
+ * Filfre .987:        1, 4, 0, 1
+ * Zoom 1.1.4:         1, 1, 0, 1
+ * ZLR 0.07:           0, 1, 0, 1
+ * Windows Frotz 1.15: 1, 4, 1, 1
+ * XZip 1.8.2:         0, 4, 0, 0
+ *
+ * The standard says that “ID 0 means ‘the previous font’.” (§8.1.2).
+ * The Frotz 2.43 source code says that “zargs[0] = number of font or 0
+ * to keep current font”.
+ *
+ * How to implement @set_font turns on the meaning of “previous”.  Does
+ * it mean the previous font _after_ the @set_font call, meaning Frotz
+ * is right?  Or is it the previous font _before_ the @set_font call,
+ * meaning the identity of two fonts needs to be tracked?
+ *
+ * Currently I do the latter.  That yields the following:
+ * 1, 4, 1, 4
+ * Almost comically, no interpreters agree with each other.
+ */
+void zset_font(void)
+{
+  struct window *win = curwin;
+
+  if(zversion == 6 && znargs == 2 && (int16_t)zargs[1] != -3)
+  {
+    ZASSERT(zargs[1] < 8, "invalid window selected: %d", (int16_t)zargs[1]);
+    win = &windows[zargs[1]];
+  }
+
+  /* If no previous font has been stored, consider that an error. */
+  if(zargs[0] == FONT_PREVIOUS && win->prev_font != FONT_NONE)
+  {
+    zargs[0] = win->prev_font;
+    zset_font();
+  }
+  else if(zargs[0] == FONT_NORMAL ||
+         (zargs[0] == FONT_CHARACTER && !options.disable_graphics_font) ||
+         (zargs[0] == FONT_FIXED     && !options.disable_fixed))
+  {
+    store(win->font);
+    win->prev_font = win->font;
+    win->font = zargs[0];
+  }
+  else
+  {
+    store(0);
+  }
+
+  set_current_style();
+}
+
+void zprint_table(void)
+{
+  uint16_t text = zargs[0], width = zargs[1], height = zargs[2], skip = zargs[3];
+  uint16_t n = 0;
+
+#ifdef ZTERP_GLK
+  uint16_t start = 0; /* initialize to appease gcc */
+
+  if(curwin == upperwin) start = upperwin->x + 1;
+#endif
+
+  if(znargs < 3) height = 1;
+  if(znargs < 4) skip = 0;
+
+  for(uint16_t i = 0; i < height; i++)
+  {
+    for(uint16_t j = 0; j < width; j++)
+    {
+      put_char(user_byte(text + n++));
+    }
+
+    if(i + 1 != height)
+    {
+      n += skip;
+#ifdef ZTERP_GLK
+      if(curwin == upperwin)
+      {
+        set_cursor(upperwin->y + 2, start);
+      }
+      else
+#endif
+      {
+        put_char(ZSCII_NEWLINE);
+      }
+    }
+  }
+}
+
+void zprint_char(void)
+{
+  /* Check 32 (space) first: a cursory examination of story files
+   * indicates that this is the most common value passed to @print_char.
+   * This appears to be due to V4+ games blanking the upper window.
+   */
+#define valid_zscii_output(c)  ((c) == 32 || (c) == 0 || (c) == 9 || (c) == 11 || (c) == 13 || ((c) > 32 && (c) <= 126) || ((c) >= 155 && (c) <= 251))
+  ZASSERT(valid_zscii_output(zargs[0]), "@print_char called with invalid character: %u", (unsigned)zargs[0]);
+#undef valid_zscii_output
+
+  put_char(zargs[0]);
+}
+
+void zprint_num(void)
+{
+  char buf[7];
+  int i = 0;
+  long v = (int16_t)zargs[0];
+
+  if(v < 0) v = -v;
+
+  do
+  {
+    buf[i++] = '0' + (v % 10);
+  } while(v /= 10);
+
+  if((int16_t)zargs[0] < 0) buf[i++] = '-';
+
+  while(i--) put_char(buf[i]);
+}
+
+void zprint_addr(void)
+{
+  print_handler(zargs[0], NULL);
+}
+
+void zprint_paddr(void)
+{
+  print_handler(unpack(zargs[0], 1), NULL);
+}
+
+/* XXX This is more complex in V6 and needs to be updated when V6 windowing is implemented. */
+void zsplit_window(void)
+{
+  if(zargs[0] == 0) close_upper_window();
+  else              resize_upper_window(zargs[0]);
+}
+
+void zset_window(void)
+{
+  set_current_window(find_window(zargs[0]));
+}
+
+#ifdef ZTERP_GLK
+static void window_change(void)
+{
+  /* When a textgrid (the upper window) in Gargoyle is rearranged, it
+   * forgets about reverse video settings, so reapply any styles to the
+   * current window (it doesn’t hurt if the window is a textbuffer).  If
+   * the current window is not the upper window that’s OK, because
+   * set_current_style() is called when a @set_window is requested.
+   */
+  set_current_style();
+
+  /* If the new window is smaller, the cursor of the upper window might
+   * be out of bounds.  Pull it back in if so.
+   */
+  if(zversion >= 3 && upperwin->id != NULL && upper_window_height > 0)
+  {
+    long x = upperwin->x, y = upperwin->y;
+    glui32 w, h;
+
+    glk_window_get_size(upperwin->id, &w, &h);
+
+    upper_window_width = w;
+    upper_window_height = h;
+
+    if(x > w) x = w;
+    if(y > h) y = h;
+
+    SWITCH_WINDOW_START(upperwin);
+    set_cursor(y + 1, x + 1);
+    SWITCH_WINDOW_END();
+  }
+
+  /* §8.4
+   * Only 0x20 and 0x21 are mentioned; what of 0x22 and 0x24?  Zoom and
+   * Windows Frotz both update the V5 header entries, so do that here,
+   * too.
+   *
+   * Also, no version restrictions are given, but assume V4+ per §11.1.
+   */
+  if(zversion >= 4)
+  {
+    unsigned width, height;
+
+    get_screen_size(&width, &height);
+
+    STORE_BYTE(0x20, height > 254 ? 254 : height);
+    STORE_BYTE(0x21, width > 255 ? 255 : width);
+
+    if(zversion >= 5)
+    {
+      STORE_WORD(0x22, width > UINT16_MAX ? UINT16_MAX : width);
+      STORE_WORD(0x24, height > UINT16_MAX ? UINT16_MAX : height);
+    }
+  }
+  else
+  {
+    zshow_status();
+  }
+}
+#endif
+
+#ifdef ZTERP_GLK
+static int timer_running;
+
+static void start_timer(uint16_t n)
+{
+  if(!TIMER_AVAILABLE()) return;
+
+  if(timer_running) die("nested timers unsupported");
+  glk_request_timer_events(n * 100);
+  timer_running = 1;
+}
+
+static void stop_timer(void)
+{
+  if(!TIMER_AVAILABLE()) return;
+
+  glk_request_timer_events(0);
+  timer_running = 0;
+}
+
+static void request_char(void)
+{
+  if(have_unicode) glk_request_char_event_uni(curwin->id);
+  else             glk_request_char_event(curwin->id);
+
+  curwin->pending_read = 1;
+}
+
+static void request_line(union line *line, glui32 maxlen, glui32 initlen)
+{
+  if(have_unicode) glk_request_line_event_uni(curwin->id, line->unicode, maxlen, initlen);
+  else             glk_request_line_event(curwin->id, line->latin1, maxlen, initlen);
+
+  curwin->pending_read = 1;
+  curwin->line = line;
+}
+#endif
+
+#define special_zscii(c) ((c) >= 129 && (c) <= 154)
+
+/* This is called when input stream 1 (read from file) is selected.  If
+ * it succefully reads a character/line from the file, it fills the
+ * struct at “input” with the appropriate information and returns true.
+ * If it fails to read (likely due to EOF) then it sets the input stream
+ * back to the keyboard and returns false.
+ */
+static int istream_read_from_file(struct input *input)
+{
+  if(input->type == INPUT_CHAR)
+  {
+    long c;
+
+    c = zterp_io_getc(istreamio);
+    if(c == -1)
+    {
+      input_stream(ISTREAM_KEYBOARD);
+      return 0;
+    }
+
+    /* Don’t translate special ZSCII characters (cursor keys, function keys, keypad). */
+    if(special_zscii(c)) input->key = c;
+    else                 input->key = unicode_to_zscii_q[c];
+  }
+  else
+  {
+    long n;
+    uint16_t line[1024];
+
+    n = zterp_io_readline(istreamio, line, sizeof line / sizeof *line);
+    if(n == -1)
+    {
+      input_stream(ISTREAM_KEYBOARD);
+      return 0;
+    }
+
+    if(n > input->maxlen) n = input->maxlen;
+
+    input->len = n;
+
+#ifdef ZTERP_GLK
+    if(curwin->id != NULL)
+    {
+      glk_set_style(style_Input);
+      for(long i = 0; i < n; i++) GLK_PUT_CHAR(line[i]);
+      GLK_PUT_CHAR(UNICODE_LINEFEED);
+      set_current_style();
+    }
+#else
+    for(long i = 0; i < n; i++) zterp_io_putc(zterp_io_stdout(), line[i]);
+    zterp_io_putc(zterp_io_stdout(), UNICODE_LINEFEED);
+#endif
+
+    for(long i = 0; i < n; i++) input->line[i] = line[i];
+  }
+
+#ifdef ZTERP_GLK
+  event_t ev;
+
+  /* It’s possible that output is buffered, meaning that until
+   * glk_select() is called, output will not be displayed.  When reading
+   * from a command-script, flush on each command so that output is
+   * visible while the script is being replayed.
+   */
+  glk_select_poll(&ev);
+  switch(ev.type)
+  {
+    case evtype_None:
+      break;
+    case evtype_Arrange:
+      window_change();
+      break;
+    default:
+      /* No other events should arrive.  Timers are only started in
+       * get_input() and are stopped before that function returns.
+       * Input events will not happen with glk_select_poll(), and no
+       * other event type is expected to be raised.
+       */
+      break;
+  }
+
+  saw_input = 1;
+#endif
+
+  return 1;
+}
+
+#ifdef GLK_MODULE_LINE_TERMINATORS
+/* Glk returns terminating characters as keycode_*, but we need them as
+ * ZSCII.  This should only ever be called with values that are matched
+ * in the switch, because those are the only ones that Glk was told are
+ * terminating characters.  In the event that another keycode comes
+ * through, though, treat it as Enter.
+ */
+static uint8_t zscii_from_glk(glui32 key)
+{
+  switch(key)
+  {
+    case 13:             return ZSCII_NEWLINE;
+    case keycode_Up:     return 129;
+    case keycode_Down:   return 130;
+    case keycode_Left:   return 131;
+    case keycode_Right:  return 131;
+    case keycode_Func1:  return 133;
+    case keycode_Func2:  return 134;
+    case keycode_Func3:  return 135;
+    case keycode_Func4:  return 136;
+    case keycode_Func5:  return 137;
+    case keycode_Func6:  return 138;
+    case keycode_Func7:  return 139;
+    case keycode_Func8:  return 140;
+    case keycode_Func9:  return 141;
+    case keycode_Func10: return 142;
+    case keycode_Func11: return 143;
+    case keycode_Func12: return 144;
+  }
+
+  return ZSCII_NEWLINE;
+}
+#endif
+
+#ifdef ZTERP_GLK
+/* This is like strlen() but in addition to C strings it can find the
+ * length of a Unicode string (which is assumed to be zero terminated)
+ * if Unicode is being used.
+ */
+static size_t line_len(const union line *line)
+{
+  size_t i;
+
+  if(!have_unicode) return strlen(line->latin1);
+
+  for(i = 0; line->unicode[i] != 0; i++)
+  {
+  }
+
+  return i;
+}
+#endif
+
+/* Attempt to read input from the user.  The input type can be either a
+ * single character or a full line.  If “timer” is not zero, a timer is
+ * started that fires off every “timer” tenths of a second (if the value
+ * is 1, it will timeout 10 times a second, etc.).  Each time the timer
+ * times out the routine at address “routine” is called.  If the routine
+ * returns true, the input is canceled.
+ *
+ * The function returns 1 if input was stored, 0 if there was a
+ * cancellation as described above.
+ */
+static int get_input(int16_t timer, int16_t routine, struct input *input)
+{
+  /* If either of these is zero, no timeout should happen. */
+  if(timer   == 0) routine = 0;
+  if(routine == 0) timer   = 0;
+
+  /* Flush all streams when input is requested. */
+#ifndef ZTERP_GLK
+  zterp_io_flush(zterp_io_stdout());
+#endif
+  zterp_io_flush(scriptio);
+  zterp_io_flush(transio);
+
+  /* Generally speaking, newline will be the reason the line input
+   * stopped, so set it by default.  It will be overridden where
+   * necessary.
+   */
+  input->term = ZSCII_NEWLINE;
+
+  if(istream == ISTREAM_FILE && istream_read_from_file(input)) return 1;
+#ifdef ZTERP_GLK
+  int status = 0;
+  union line line;
+  struct window *saved = NULL;
+
+  /* In V6, input might be requested on an unsupported window.  If so,
+   * switch to the main window temporarily.
+   */
+  if(curwin->id == NULL)
+  {
+    saved = curwin;
+    curwin = mainwin;
+    glk_set_window(curwin->id);
+  }
+
+  if(input->type == INPUT_CHAR)
+  {
+    request_char();
+  }
+  else
+  {
+    for(int i = 0; i < input->preloaded; i++)
+    {
+      if(have_unicode) line.unicode[i] = input->line[i];
+      else             line.latin1 [i] = input->line[i];
+    }
+
+    request_line(&line, input->maxlen, input->preloaded);
+  }
+
+  if(timer != 0) start_timer(timer);
+
+  while(status == 0)
+  {
+    event_t ev;
+
+    glk_select(&ev);
+
+    switch(ev.type)
+    {
+      case evtype_Arrange:
+        window_change();
+        break;
+
+      case evtype_Timer:
+        {
+          ZASSERT(timer != 0, "got unexpected evtype_Timer");
+
+          struct window *saved2 = curwin;
+          int ret;
+
+          stop_timer();
+
+          ret = direct_call(routine);
+
+          /* It’s possible for an interrupt to switch windows; if it
+           * does, simply switch back.  This is the easiest way to deal
+           * with an undefined bit of the Z-machine.
+           */
+          if(curwin != saved2) set_current_window(saved2);
+
+          if(ret)
+          {
+            status = 2;
+          }
+          else
+          {
+            /* If this got reset to 0, that means an interrupt had to
+             * cancel the read event in order to either read or write.
+             */
+            if(!curwin->pending_read)
+            {
+              if(input->type == INPUT_CHAR) request_char();
+              else                          request_line(&line, input->maxlen, line_len(&line));
+            }
+
+            start_timer(timer);
+          }
+        }
+
+        break;
+
+      case evtype_CharInput:
+        ZASSERT(input->type == INPUT_CHAR, "got unexpected evtype_CharInput");
+        ZASSERT(ev.win == curwin->id, "got evtype_CharInput on unexpected window");
+
+        status = 1;
+
+        switch(ev.val1)
+        {
+          case keycode_Delete: input->key =   8; break;
+          case keycode_Return: input->key =  13; break;
+          case keycode_Escape: input->key =  27; break;
+          case keycode_Up:     input->key = 129; break;
+          case keycode_Down:   input->key = 130; break;
+          case keycode_Left:   input->key = 131; break;
+          case keycode_Right:  input->key = 132; break;
+          case keycode_Func1:  input->key = 133; break;
+          case keycode_Func2:  input->key = 134; break;
+          case keycode_Func3:  input->key = 135; break;
+          case keycode_Func4:  input->key = 136; break;
+          case keycode_Func5:  input->key = 137; break;
+          case keycode_Func6:  input->key = 138; break;
+          case keycode_Func7:  input->key = 139; break;
+          case keycode_Func8:  input->key = 140; break;
+          case keycode_Func9:  input->key = 141; break;
+          case keycode_Func10: input->key = 142; break;
+          case keycode_Func11: input->key = 143; break;
+          case keycode_Func12: input->key = 144; break;
+
+          default:
+            input->key = ZSCII_QUESTIONMARK;
+
+            if(ev.val1 <= UINT16_MAX)
+            {
+              uint8_t c = unicode_to_zscii[ev.val1];
+
+              if(c != 0) input->key = c;
+            }
+
+            break;
+        }
+
+        break;
+
+      case evtype_LineInput:
+        ZASSERT(input->type == INPUT_LINE, "got unexpected evtype_LineInput");
+        ZASSERT(ev.win == curwin->id, "got evtype_LineInput on unexpected window");
+        input->len = ev.val1;
+#ifdef GLK_MODULE_LINE_TERMINATORS
+        if(zversion >= 5) input->term = zscii_from_glk(ev.val2);
+#endif
+        status = 1;
+        break;
+    }
+  }
+
+  stop_timer();
+
+  if(input->type == INPUT_CHAR)
+  {
+    glk_cancel_char_event(curwin->id);
+  }
+  else
+  {
+    /* On cancellation, the buffer still needs to be filled, because
+     * it’s possible that line input echoing has been turned off and the
+     * contents will need to be written out.
+     */
+    if(status == 2)
+    {
+      event_t ev;
+
+      glk_cancel_line_event(curwin->id, &ev);
+      input->len = ev.val1;
+      input->term = 0;
+    }
+
+    for(glui32 i = 0; i < input->len; i++)
+    {
+      if(have_unicode) input->line[i] = line.unicode[i] > UINT16_MAX ? UNICODE_QUESTIONMARK : line.unicode[i];
+      else             input->line[i] = (uint8_t)line.latin1[i];
+    }
+  }
+
+  curwin->pending_read = 0;
+  curwin->line = NULL;
+
+  if(status == 1) saw_input = 1;
+
+  if(errorwin != NULL)
+  {
+    glk_window_close(errorwin, NULL);
+    errorwin = NULL;
+  }
+
+  if(saved != NULL)
+  {
+    curwin = saved;
+    glk_set_window(curwin->id);
+  }
+
+  return status != 2;
+#else
+  if(input->type == INPUT_CHAR)
+  {
+    long n;
+    uint16_t line[64];
+
+    n = zterp_io_readline(zterp_io_stdin(), line, sizeof line / sizeof *line);
+
+    /* On error/eof, or if an invalid key was typed, pretend “Enter” was hit. */
+    if(n <= 0)
+    {
+      input->key = ZSCII_NEWLINE;
+    }
+    else
+    {
+      input->key = unicode_to_zscii[line[0]];
+      if(input->key == 0) input->key = ZSCII_NEWLINE;
+    }
+  }
+  else
+  {
+    input->len = input->preloaded;
+
+    if(input->maxlen > input->preloaded)
+    {
+      long n;
+      uint16_t line[1024];
+
+      n = zterp_io_readline(zterp_io_stdin(), line, sizeof line / sizeof *line);
+      if(n != -1)
+      {
+        if(n > input->maxlen - input->preloaded) n = input->maxlen - input->preloaded;
+        for(long i = 0; i < n; i++) input->line[i + input->preloaded] = line[i];
+        input->len += n;
+      }
+    }
+  }
+
+  return 1;
+#endif
+}
+
+void zread_char(void)
+{
+  uint16_t timer = 0;
+  uint16_t routine = zargs[2];
+  struct input input = { .type = INPUT_CHAR };
+
+#ifdef ZTERP_GLK
+  cancel_read_events(curwin);
+#endif
+
+  if(zversion >= 4 && znargs > 1) timer = zargs[1];
+
+  if(!get_input(timer, routine, &input))
+  {
+    store(0);
+    return;
+  }
+
+#ifdef ZTERP_GLK
+  update_delayed();
+#endif
+
+  if(streams & STREAM_SCRIPT)
+  {
+    /* Values 127 to 159 are not valid Unicode, and these just happen to
+     * match up to the values needed for special ZSCII keys, so store
+     * them as-is.
+     */
+    if(special_zscii(input.key)) zterp_io_putc(scriptio, input.key);
+    else                         zterp_io_putc(scriptio, zscii_to_unicode[input.key]);
+  }
+
+  store(input.key);
+}
+
+#ifdef ZTERP_GLK
+static void status_putc(uint8_t c)
+{
+  glk_put_char(zscii_to_unicode[c]);
+}
+#endif
+
+void zshow_status(void)
+{
+#ifdef ZTERP_GLK
+  glui32 width, height;
+  char rhs[64];
+  int first = variable(0x11), second = variable(0x12);
+
+  if(statuswin.id == NULL) return;
+
+  glk_window_clear(statuswin.id);
+
+  SWITCH_WINDOW_START(&statuswin);
+
+  glk_window_get_size(statuswin.id, &width, &height);
+
+#ifdef GARGLK
+  garglk_set_reversevideo(1);
+#else
+  glk_set_style(style_Alert);
+#endif
+  for(glui32 i = 0; i < width; i++) glk_put_char(ZSCII_SPACE);
+
+  glk_window_move_cursor(statuswin.id, 1, 0);
+
+  /* Variable 0x10 is global variable 1. */
+  print_object(variable(0x10), status_putc);
+
+  if(STATUS_IS_TIME())
+  {
+    snprintf(rhs, sizeof rhs, "Time: %d:%02d%s ", (first + 11) % 12 + 1, second, first < 12 ? "am" : "pm");
+    if(strlen(rhs) > width) snprintf(rhs, sizeof rhs, "%02d:%02d", first, second);
+  }
+  else
+  {
+    snprintf(rhs, sizeof rhs, "Score: %d  Moves: %d ", first, second);
+    if(strlen(rhs) > width) snprintf(rhs, sizeof rhs, "%d/%d", first, second);
+  }
+
+  if(strlen(rhs) <= width)
+  {
+    glk_window_move_cursor(statuswin.id, width - strlen(rhs), 0);
+    glk_put_string(rhs);
+  }
+
+  SWITCH_WINDOW_END();
+#endif
+}
+
+/* This is strcmp() except that the first string is Unicode. */
+static int unicmp(const uint32_t *s1, const char *s2)
+{
+  while(*s1 != 0 && *s2 == *s1)
+  {
+    s1++;
+    s2++;
+  }
+
+  return *s1 - *s2;
+}
+
+uint32_t read_pc;
+
+/* Try to parse a meta command.  Returns true if input should be
+ * restarted, false to indicate no more input is required.  In most
+ * cases input will be required because the game has requested it, but
+ * /undo and /restore jump to different locations, so the current @read
+ * no longer exists.
+ */
+static int handle_meta_command(const uint32_t *string)
+{
+  if(unicmp(string, "undo") == 0)
+  {
+    uint16_t flags2 = WORD(0x10);
+    int success = pop_save();
+
+    if(success != 0)
+    {
+      /* §6.1.2. */
+      STORE_WORD(0x10, flags2);
+
+      if(zversion >= 5) store(success);
+      else              put_string("[undone]\n\n>");
+
+      return 0;
+    }
+    else
+    {
+      put_string("[no save found, unable to undo]");
+    }
+  }
+  else if(unicmp(string, "scripton") == 0)
+  {
+    if(output_stream(OSTREAM_SCRIPT, 0)) put_string("[transcripting on]");
+    else                                 put_string("[transcripting failed]");
+  }
+  else if(unicmp(string, "scriptoff") == 0)
+  {
+    output_stream(-OSTREAM_SCRIPT, 0);
+    put_string("[transcripting off]");
+  }
+  else if(unicmp(string, "recon") == 0)
+  {
+    if(output_stream(OSTREAM_RECORD, 0)) put_string("[command recording on]");
+    else                                 put_string("[command recording failed]");
+  }
+  else if(unicmp(string, "recoff") == 0)
+  {
+    output_stream(-OSTREAM_RECORD, 0);
+    put_string("[command recording off]");
+  }
+  else if(unicmp(string, "replay") == 0)
+  {
+    if(input_stream(ISTREAM_FILE)) put_string("[replaying commands]");
+    else                           put_string("[replaying commands failed]");
+  }
+  else if(unicmp(string, "save") == 0)
+  {
+    if(interrupt_level() != 0)
+    {
+      put_string("[cannot call /save while in an interrupt]");
+    }
+    else
+    {
+      uint32_t tmp = pc;
+
+      /* pc is currently set to the next instruction, but the restore
+       * needs to come back to *this* instruction; so temporarily set
+       * pc back before saving.
+       */
+      pc = read_pc;
+      if(do_save(1)) put_string("[saved]");
+      else           put_string("[save failed]");
+      pc = tmp;
+    }
+  }
+  else if(unicmp(string, "restore") == 0)
+  {
+    if(do_restore(1))
+    {
+      put_string("[restored]\n\n>");
+      return 0;
+    }
+    else
+    {
+      put_string("[restore failed]");
+    }
+  }
+  else if(unicmp(string, "help") == 0)
+  {
+    put_string(
+        "/undo: undo a turn\n"
+        "/scripton: start a transcript\n"
+        "/scriptoff: stop a transcript\n"
+        "/recon: start a command record\n"
+        "/recoff: stop a command record\n"
+        "/replay: replay a command record\n"
+        "/save: save the game\n"
+        "/restore: restore a game saved by /save"
+        );
+  }
+  else
+  {
+    put_string("[unknown command]");
+  }
+
+  return 1;
+}
+
+void zread(void)
+{
+  uint16_t text = zargs[0], parse = zargs[1];
+  uint8_t maxchars = zversion >= 5 ? user_byte(text) : user_byte(text) - 1;
+  uint8_t zscii_string[maxchars];
+  uint32_t string[maxchars + 1];
+  struct input input = { .type = INPUT_LINE, .line = string, .maxlen = maxchars };
+  uint16_t timer = 0;
+  uint16_t routine = zargs[3];
+
+#ifdef ZTERP_GLK
+  cancel_read_events(curwin);
+#endif
+
+  if(zversion <= 3) zshow_status();
+
+  if(zversion >= 4 && znargs > 2) timer = zargs[2];
+
+  if(zversion >= 5)
+  {
+    int i;
+
+    input.preloaded = user_byte(text + 1);
+    ZASSERT(input.preloaded <= maxchars, "too many preloaded characters: %d when max is %d", input.preloaded, maxchars);
+
+    for(i = 0; i < input.preloaded; i++) string[i] = zscii_to_unicode[user_byte(text + i + 2)];
+    string[i] = 0;
+
+    /* Under garglk, preloaded input works as it’s supposed to.
+     * Under Glk, it can fail one of two ways:
+     * 1. The preloaded text is printed out once, but is not editable.
+     * 2. The preloaded text is printed out twice, the second being editable.
+     * I have chosen option #2.  For non-Glk, option #1 is done by necessity.
+     */
+#ifdef GARGLK
+    if(curwin->id != NULL) garglk_unput_string_uni(string);
+#endif
+  }
+
+  if(!get_input(timer, routine, &input))
+  {
+#ifdef ZTERP_GLK
+    cleanup_screen(&input);
+#endif
+    if(zversion >= 5) store(0);
+    return;
+  }
+
+#ifdef ZTERP_GLK
+  cleanup_screen(&input);
+#endif
+
+#ifdef ZTERP_GLK
+  update_delayed();
+#endif
+
+  if(options.enable_escape && (streams & STREAM_TRANS))
+  {
+    zterp_io_putc(transio, 033);
+    zterp_io_putc(transio, '[');
+    for(int i = 0; options.escape_string[i] != 0; i++) zterp_io_putc(transio, options.escape_string[i]);
+  }
+
+  for(int i = 0; i < input.len; i++)
+  {
+    zscii_string[i] = unicode_to_zscii_q[unicode_tolower(string[i])];
+    if(streams & STREAM_TRANS)  zterp_io_putc(transio, string[i]);
+    if(streams & STREAM_SCRIPT) zterp_io_putc(scriptio, string[i]);
+  }
+
+  if(options.enable_escape && (streams & STREAM_TRANS))
+  {
+    zterp_io_putc(transio, 033);
+    zterp_io_putc(transio, '[');
+    zterp_io_putc(transio, '0');
+    zterp_io_putc(transio, 'm');
+  }
+
+  if(streams & STREAM_TRANS)  zterp_io_putc(transio, UNICODE_LINEFEED);
+  if(streams & STREAM_SCRIPT) zterp_io_putc(scriptio, UNICODE_LINEFEED);
+
+  if(!options.disable_meta_commands)
+  {
+    string[input.len] = 0;
+
+    if(string[0] == '/')
+    {
+      if(handle_meta_command(string + 1))
+      {
+        /* The game still wants input, so try again. */
+        put_string("\n\n>");
+        zread();
+      }
+
+      return;
+    }
+
+    /* V1–4 do not have @save_undo, so simulate one each time @read is
+     * called.
+     *
+     * pc is currently set to the next instruction, but the undo needs
+     * to come back to *this* instruction; so temporarily set pc back
+     * before pushing the save.
+     */
+    if(zversion <= 4)
+    {
+      uint32_t tmp_pc = pc;
+
+      pc = read_pc;
+      push_save();
+      pc = tmp_pc;
+    }
+  }
+
+  if(zversion >= 5)
+  {
+    user_store_byte(text + 1, input.len); /* number of characters read */
+
+    for(int i = 0; i < input.len; i++)
+    {
+      user_store_byte(text + i + 2, zscii_string[i]);
+    }
+
+    if(parse != 0) tokenize(text, parse, 0, 0);
+
+    store(input.term);
+  }
+  else
+  {
+    for(int i = 0; i < input.len; i++)
+    {
+      user_store_byte(text + i + 1, zscii_string[i]);
+    }
+
+    user_store_byte(text + input.len + 1, 0);
+
+    tokenize(text, parse, 0, 0);
+  }
+}
+
+void zprint_unicode(void)
+{
+  if(valid_unicode(zargs[0])) put_char_u(zargs[0]);
+  else                        put_char_u(UNICODE_QUESTIONMARK);
+}
+
+void zcheck_unicode(void)
+{
+  uint16_t res = 0;
+
+  /* valid_unicode() will tell which Unicode characters can be printed;
+   * and if the Unicode character is in the Unicode input table, it can
+   * also be read.  If Unicode is not available, then any character >255
+   * is invalid for both reading and writing.
+   */
+  if(have_unicode || zargs[0] < 256)
+  {
+    if(valid_unicode(zargs[0]))         res |= 0x01;
+    if(unicode_to_zscii[zargs[0]] != 0) res |= 0x02;
+  }
+
+  store(res);
+}
+
+/* Should picture_data and get_wind_prop be moved to a V6 source file? */
+void zpicture_data(void)
+{
+  if(zargs[0] == 0)
+  {
+    user_store_word(zargs[1] + 0, 0);
+    user_store_word(zargs[1] + 2, 0);
+  }
+
+  /* No pictures means no valid pictures, so never branch. */
+  branch_if(0);
+}
+
+void zget_wind_prop(void)
+{
+  uint16_t val;
+  struct window *win;
+
+  win = find_window(zargs[0]);
+
+  /* These are mostly bald-faced lies. */
+  switch(zargs[1])
+  {
+    case 0: /* y coordinate */
+      val = 0;
+      break;
+    case 1: /* x coordinate */
+      val = 0;
+      break;
+    case 2:  /* y size */
+      val = 100;
+      break;
+    case 3:  /* x size */
+      val = 100;
+      break;
+    case 4:  /* y cursor */
+      val = 0;
+      break;
+    case 5:  /* x cursor */
+      val = 0;
+      break;
+    case 6: /* left margin size */
+      val = 0;
+      break;
+    case 7: /* right margin size */
+      val = 0;
+      break;
+    case 8: /* newline interrupt routine */
+      val = 0;
+      break;
+    case 9: /* interrupt countdown */
+      val = 0;
+      break;
+    case 10: /* text style */
+      val = win->style;
+      break;
+    case 11: /* colour data */
+      val = (9 << 8) | 2;
+      break;
+    case 12: /* font number */
+      val = win->font;
+      break;
+    case 13: /* font size */
+      val = (10 << 8) | 10;
+      break;
+    case 14: /* attributes */
+      val = 0;
+      break;
+    case 15: /* line count */
+      val = 0;
+      break;
+    case 16: /* true foreground colour */
+      val = 0;
+      break;
+    case 17: /* true background colour */
+      val = 0;
+      break;
+    default:
+      die("unknown window property: %u", (unsigned)zargs[1]);
+  }
+
+  store(val);
+}
+
+/* This is not correct, because @output_stream does not work as it
+ * should with a width argument; however, this does print out the
+ * contents of a table that was sent to stream 3, so it’s at least
+ * somewhat useful.
+ *
+ * Output should be to the currently-selected window, but since V6 is
+ * only marginally supported, other windows are not active.  Send to the
+ * main window for the time being.
+ */
+void zprint_form(void)
+{
+  SWITCH_WINDOW_START(mainwin);
+
+  for(uint16_t i = 0; i < user_word(zargs[0]); i++)
+  {
+    put_char(user_byte(zargs[0] + 2 + i));
+  }
+
+  put_char(ZSCII_NEWLINE);
+
+  SWITCH_WINDOW_END();
+}
+
+void zmake_menu(void)
+{
+  branch_if(0);
+}
+
+void zbuffer_screen(void)
+{
+  store(0);
+}
+
+#ifdef GARGLK
+/* Glk does not guarantee great control over how various styles are
+ * going to look, but Gargoyle does.  Abusing the Glk “style hints”
+ * functions allows for quite fine-grained control over style
+ * appearance.  First, clear the (important) attributes for each style,
+ * and then recreate each in whatever mold is necessary.  Re-use some
+ * that are expected to be correct (emphasized for italic, subheader for
+ * bold, and so on).
+ */
+static void set_default_styles(void)
+{
+  int styles[] = { style_Subheader, style_Emphasized, style_Alert, style_Preformatted, style_User1, style_User2, style_Note };
+
+  for(int i = 0; i < 7; i++)
+  {
+    glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Size, 0);
+    glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Weight, 0);
+    glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Oblique, 0);
+
+    /* This sets wintype_TextGrid to be proportional, which of course is
+     * wrong; but text grids are required to be fixed, so Gargoyle
+     * simply ignores this hint for those windows.
+     */
+    glk_stylehint_set(wintype_AllTypes, styles[i], stylehint_Proportional, 1);
+  }
+}
+#endif
+
+int create_mainwin(void)
+{
+#ifdef ZTERP_GLK
+
+#ifdef GARGLK
+  set_default_styles();
+
+  /* Bold */
+  glk_stylehint_set(wintype_AllTypes, style_Subheader, stylehint_Weight, 1);
+
+  /* Italic */
+  glk_stylehint_set(wintype_AllTypes, style_Emphasized, stylehint_Oblique, 1);
+
+  /* Bold Italic */
+  glk_stylehint_set(wintype_AllTypes, style_Alert, stylehint_Weight, 1);
+  glk_stylehint_set(wintype_AllTypes, style_Alert, stylehint_Oblique, 1);
+
+  /* Fixed */
+  glk_stylehint_set(wintype_AllTypes, style_Preformatted, stylehint_Proportional, 0);
+
+  /* Bold Fixed */
+  glk_stylehint_set(wintype_AllTypes, style_User1, stylehint_Weight, 1);
+  glk_stylehint_set(wintype_AllTypes, style_User1, stylehint_Proportional, 0);
+
+  /* Italic Fixed */
+  glk_stylehint_set(wintype_AllTypes, style_User2, stylehint_Oblique, 1);
+  glk_stylehint_set(wintype_AllTypes, style_User2, stylehint_Proportional, 0);
+
+  /* Bold Italic Fixed */
+  glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Weight, 1);
+  glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Oblique, 1);
+  glk_stylehint_set(wintype_AllTypes, style_Note, stylehint_Proportional, 0);
+#endif
+
+  mainwin->id = glk_window_open(0, 0, 0, wintype_TextBuffer, 1);
+  if(mainwin->id == NULL) return 0;
+  glk_set_window(mainwin->id);
+
+#ifdef GLK_MODULE_LINE_ECHO
+  mainwin->has_echo = glk_gestalt(gestalt_LineInputEcho, 0);
+  if(mainwin->has_echo) glk_set_echo_line_event(mainwin->id, 0);
+#endif
+
+  return 1;
+#else
+  return 1;
+#endif
+}
+
+int create_statuswin(void)
+{
+#ifdef ZTERP_GLK
+  if(statuswin.id == NULL) statuswin.id = glk_window_open(mainwin->id, winmethod_Above | winmethod_Fixed, 1, wintype_TextGrid, 0);
+  return statuswin.id != NULL;
+#else
+  return 0;
+#endif
+}
+
+int create_upperwin(void)
+{
+#ifdef ZTERP_GLK
+  /* On a restart, this function will get called again.  It would be
+   * possible to try to resize the upper window to 0 if it already
+   * exists, but it’s easier to just destroy and recreate it.
+   */
+  if(upperwin->id != NULL) glk_window_close(upperwin->id, NULL);
+
+  /* The upper window appeared in V3. */
+  if(zversion >= 3)
+  {
+    upperwin->id = glk_window_open(mainwin->id, winmethod_Above | winmethod_Fixed, 0, wintype_TextGrid, 0);
+    upperwin->x = upperwin->y = 0;
+    upper_window_height = 0;
+
+    if(upperwin->id != NULL)
+    {
+      glui32 w, h;
+
+      glk_window_get_size(upperwin->id, &w, &h);
+      upper_window_width = w;
+
+      if(h != 0 || upper_window_width == 0)
+      {
+        glk_window_close(upperwin->id, NULL);
+        upperwin->id = NULL;
+      }
+    }
+  }
+
+  return upperwin->id != NULL;
+#else
+  return 0;
+#endif
+}
+
+void init_screen(void)
+{
+  for(int i = 0; i < 8; i++)
+  {
+    windows[i].style = STYLE_NONE;
+    windows[i].font = FONT_NORMAL;
+    windows[i].prev_font = FONT_NONE;
+
+#ifdef ZTERP_GLK
+    clear_window(&windows[i]);
+#ifdef GLK_MODULE_LINE_TERMINATORS
+    if(windows[i].id != NULL && term_nkeys != 0 && glk_gestalt(gestalt_LineTerminators, 0)) glk_set_terminators_line_event(windows[i].id, term_keys, term_nkeys);
+#endif
+#endif
+  }
+
+  close_upper_window();
+
+#ifdef ZTERP_GLK
+  if(statuswin.id != NULL) glk_window_clear(statuswin.id);
+
+  if(errorwin != NULL)
+  {
+    glk_window_close(errorwin, NULL);
+    errorwin = NULL;
+  }
+
+  stop_timer();
+
+#ifdef GARGLK
+  fg_color = zcolor_Default;
+  bg_color = zcolor_Default;
+#endif
+
+#else
+  fg_color = 1;
+  bg_color = 1;
+#endif
+
+  if(scriptio != NULL) zterp_io_close(scriptio);
+  scriptio = NULL;
+
+  input_stream(ISTREAM_KEYBOARD);
+
+  streams = STREAM_SCREEN;
+  stablei = -1;
+  set_current_window(mainwin);
+}
diff --git a/interpreters/bocfel/screen.h b/interpreters/bocfel/screen.h
new file mode 100644 (file)
index 0000000..4e11101
--- /dev/null
@@ -0,0 +1,92 @@
+#ifndef ZTERP_SCREEN_H
+#define ZTERP_SCREEN_H
+
+#include <stdint.h>
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#endif
+
+/* Boolean flag describing whether the header bit meaning “fixed font” is set. */
+extern int header_fixed_font;
+
+extern uint32_t read_pc;
+
+void init_screen(void);
+
+int create_mainwin(void);
+int create_statuswin(void);
+int create_upperwin(void);
+void get_screen_size(unsigned int *, unsigned int *);
+void close_upper_window(void);
+void cancel_all_events(void);
+
+/* Text styles. */
+#define STYLE_NONE     (0U     )
+#define STYLE_REVERSE  (1U << 0)
+#define STYLE_BOLD     (1U << 1)
+#define STYLE_ITALIC   (1U << 2)
+#define STYLE_FIXED    (1U << 3)
+
+void show_message(const char *, ...);
+
+#ifdef GLK_MODULE_LINE_TERMINATORS
+void term_keys_reset(void);
+void term_keys_add(uint8_t);
+#endif
+
+#ifdef GARGLK
+void update_color(int, unsigned long);
+#endif
+
+/* Output streams. */
+#define OSTREAM_SCREEN         1
+#define OSTREAM_SCRIPT         2
+#define OSTREAM_MEMORY         3
+#define OSTREAM_RECORD         4
+
+/* Input streams. */
+#define ISTREAM_KEYBOARD       0
+#define ISTREAM_FILE           1
+
+int output_stream(int16_t, uint16_t);
+int input_stream(int);
+
+void set_current_style(void);
+
+int print_handler(uint32_t, void (*)(uint8_t));
+void put_char_u(uint16_t);
+void put_char(uint8_t);
+
+void zoutput_stream(void);
+void zinput_stream(void);
+void zprint(void);
+void zprint_ret(void);
+void znew_line(void);
+void zerase_window(void);
+void zerase_line(void);
+void zset_cursor(void);
+void zget_cursor(void);
+void zset_colour(void);
+void zset_true_colour(void);
+void zset_text_style(void);
+void zset_font(void);
+void zprint_table(void);
+void zprint_char(void);
+void zprint_num(void);
+void zprint_addr(void);
+void zprint_paddr(void);
+void zsplit_window(void);
+void zset_window(void);
+void zread_char(void);
+void zshow_status(void);
+void zread(void);
+void zprint_unicode(void);
+void zcheck_unicode(void);
+void zpicture_data(void);
+void zget_wind_prop(void);
+void zprint_form(void);
+void zmake_menu(void);
+void zbuffer_screen(void);
+
+#endif
diff --git a/interpreters/bocfel/stack.c b/interpreters/bocfel/stack.c
new file mode 100644 (file)
index 0000000..493dd36
--- /dev/null
@@ -0,0 +1,1077 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <setjmp.h>
+
+#include "stack.h"
+#include "branch.h"
+#include "iff.h"
+#include "io.h"
+#include "memory.h"
+#include "process.h"
+#include "screen.h"
+#include "util.h"
+#include "zterp.h"
+
+struct call_frame
+{
+  uint32_t pc;
+  uint16_t *sp;
+  uint8_t nlocals;
+  uint8_t nargs;
+  uint16_t where;
+  uint16_t locals[15];
+};
+
+static struct call_frame *frames;
+static struct call_frame *fp;
+
+#define BASE_OF_FRAMES frames
+static struct call_frame *TOP_OF_FRAMES;
+#define CURRENT_FRAME  (fp - 1)
+#define NFRAMES                ((long)(fp - frames))
+
+static uint16_t *stack;
+static uint16_t *sp;
+
+#define BASE_OF_STACK  stack
+static uint16_t *TOP_OF_STACK;
+
+static void PUSH_STACK(uint16_t n) { ZASSERT(sp != TOP_OF_STACK, "stack overflow"); *sp++ = n; }
+static uint16_t POP_STACK(void) { ZASSERT(sp > CURRENT_FRAME->sp, "stack underflow"); return *--sp; }
+
+static struct save_state
+{
+  uint32_t pc;
+
+  uint32_t memsize;
+  uint8_t *memory;
+
+  uint32_t stack_size;
+  uint16_t *stack;
+
+  long nframes;
+  struct call_frame *frames;
+
+  struct save_state *prev, *next;
+} *saves_head, *saves_tail;
+
+static long nsaves;
+
+static void add_frame(uint32_t pc_, uint16_t *sp_, uint8_t nlocals, uint8_t nargs, uint16_t where)
+{
+  ZASSERT(fp != TOP_OF_FRAMES, "call stack too deep: %ld", NFRAMES + 1);
+
+  fp->pc = pc_;
+  fp->sp = sp_;
+  fp->nlocals = nlocals;
+  fp->nargs = nargs;
+  fp->where = where;
+
+  fp++;
+}
+
+void init_stack(void)
+{
+  /* Allocate space for the evaluation and call stacks.
+   * Clamp the size between 1 and the largest value that will not
+   * produce an overflow of size_t when multiplied by the size of the
+   * type.
+   * Also, the call stack can be no larger than 0xffff so that the
+   * result of a @catch will fit into a 16-bit integer.
+   */
+  if(stack == NULL)
+  {
+#define MIN(a, b)      ((a) < (b) ? (a) : (b))
+#define CLAMP(n, a, b) ((n) < (a) ? (a) : (n) > (b) ? (b): (n))
+    options.eval_stack_size = CLAMP(options.eval_stack_size, 1, SIZE_MAX / sizeof *stack);
+    stack = malloc(options.eval_stack_size * sizeof *stack);
+    if(stack == NULL) die("unable to allocate %lu bytes for the evaluation stack", options.eval_stack_size * (unsigned long)sizeof *stack);
+    TOP_OF_STACK = &stack[options.eval_stack_size];
+
+    options.call_stack_size = CLAMP(options.call_stack_size, 1, MIN(0xffff, (SIZE_MAX / sizeof *frames) - sizeof *frames));
+    /* One extra to help with saving (thus the subtraction of sizeof *frames above). */
+    frames = malloc((options.call_stack_size + 1) * sizeof *frames);
+    if(frames == NULL) die("unable to allocate %lu bytes for the call stack", (options.call_stack_size + 1) * (unsigned long)sizeof *frames);
+    TOP_OF_FRAMES = &frames[options.call_stack_size];
+#undef MIN
+#undef CLAMP
+  }
+
+  sp = BASE_OF_STACK;
+  fp = BASE_OF_FRAMES;
+
+  /* Quetzal requires a dummy frame in non-V6 games, so do that here. */
+  if(zversion != 6) add_frame(0, sp, 0, 0, 0);
+
+  /* Free all previous save states (from @save_undo). */
+  while(saves_head != NULL)
+  {
+    struct save_state *tmp = saves_head;
+    saves_head = saves_head->next;
+    free(tmp->stack);
+    free(tmp->frames);
+    free(tmp->memory);
+    free(tmp);
+  }
+  saves_tail = NULL;
+  nsaves = 0;
+}
+
+uint16_t variable(uint16_t var)
+{
+  ZASSERT(var < 0x100, "unable to decode variable %u", (unsigned)var);
+
+  /* Stack */
+  if(var == 0)
+  {
+    return POP_STACK();
+  }
+
+  /* Locals */
+  else if(var <= 0x0f)
+  {
+    ZASSERT(var <= CURRENT_FRAME->nlocals, "attempting to read from nonexistent local variable %d: routine has %d", (int)var, CURRENT_FRAME->nlocals);
+    return CURRENT_FRAME->locals[var - 1];
+  }
+
+  /* Globals */
+  else if(var <= 0xff)
+  {
+    var -= 0x10;
+    return WORD(header.globals + (var * 2));
+  }
+
+  /* This is an “impossible” situation (ie, the game did something wrong).
+   * It will be caught above if safety checks are turned on, but if they
+   * are not, do what we can: lie.
+   */
+  return -1;
+}
+
+void store_variable(uint16_t var, uint16_t n)
+{
+  ZASSERT(var < 0x100, "unable to decode variable %u", (unsigned)var);
+
+  /* Stack. */
+  if(var == 0)
+  {
+    PUSH_STACK(n);
+  }
+
+  /* Local variables. */
+  else if(var <= 0x0f)
+  {
+    ZASSERT(var <= CURRENT_FRAME->nlocals, "attempting to store to nonexistent local variable %d: routine has %d", (int)var, CURRENT_FRAME->nlocals);
+    CURRENT_FRAME->locals[var - 1] = n;
+  }
+
+  /* Global variables. */
+  else if(var <= 0xff)
+  {
+    var -= 0x10;
+    STORE_WORD(header.globals + (var * 2), n);
+  }
+}
+
+uint16_t *stack_top_element(void)
+{
+  ZASSERT(sp > CURRENT_FRAME->sp, "stack underflow");
+
+  return sp - 1;
+}
+
+void zpush(void)
+{
+  PUSH_STACK(zargs[0]);
+}
+
+void zpull(void)
+{
+  uint16_t v;
+
+  if(zversion != 6)
+  {
+    v = POP_STACK();
+
+    /* The z-spec 1.1 requires indirect variable references to the stack not to push/pop */
+    if(zargs[0] == 0) *stack_top_element() = v;
+    else              store_variable(zargs[0], v);
+  }
+  else
+  {
+    if(znargs == 0)
+    {
+      v = POP_STACK();
+    }
+    else
+    {
+      uint16_t slots = user_word(zargs[0]) + 1;
+
+      v = user_word(zargs[0] + (2 * slots));
+
+      user_store_word(zargs[0], slots);
+    }
+
+    store(v);
+  }
+}
+
+void zload(void)
+{
+  /* The z-spec 1.1 requires indirect variable references to the stack not to push/pop */
+  if(zargs[0] == 0) store(*stack_top_element());
+  else              store(variable(zargs[0]));
+}
+
+void zstore(void)
+{
+  /* The z-spec 1.1 requires indirect variable references to the stack not to push/pop */
+  if(zargs[0] == 0) *stack_top_element() = zargs[1];
+  else              store_variable(zargs[0], zargs[1]);
+}
+
+void call(int do_store)
+{
+  uint32_t jmp_to;
+  uint8_t nlocals;
+  uint16_t where;
+
+  if(zargs[0] == 0)
+  {
+    /* call(2) should never happen if zargs[0] is 0. */
+    if(do_store) store(0);
+    return;
+  }
+
+  jmp_to = unpack(zargs[0], 0);
+  ZASSERT(jmp_to < memory_size - 1, "call to invalid address 0x%lx", (unsigned long)jmp_to);
+
+  nlocals = BYTE(jmp_to++);
+  ZASSERT(nlocals <= 15, "too many (%d) locals at 0x%lx", nlocals, (unsigned long)jmp_to - 1);
+
+  if(zversion <= 4) ZASSERT(jmp_to + (nlocals * 2) < memory_size, "call to invalid address 0x%lx", (unsigned long)jmp_to);
+
+  switch(do_store)
+  {
+    case 1:  where = BYTE(pc++); break; /* Where to store return value */
+    case 0:  where = 0xff + 1;   break; /* Or a tag meaning no return value */
+    default: where = 0xff + 2;   break; /* Or a tag meaning push the return value */
+  }
+
+  add_frame(pc, sp, nlocals, znargs - 1, where);
+
+  for(int i = 0; i < nlocals; i++)
+  {
+    if(i < znargs - 1)
+    {
+      CURRENT_FRAME->locals[i] = zargs[i + 1];
+    }
+    else
+    {
+      if(zversion <= 4) CURRENT_FRAME->locals[i] = WORD(jmp_to + (2 * i));
+      else              CURRENT_FRAME->locals[i] = 0;
+    }
+  }
+
+  /* Take care of locals! */
+  if(zversion <= 4) jmp_to += nlocals * 2;
+
+  pc = jmp_to;
+}
+
+#ifdef ZTERP_GLK
+uint16_t direct_call(uint16_t routine)
+{
+  uint16_t saved_args[znargs];
+  uint16_t saved_nargs;
+
+  memcpy(saved_args, zargs, sizeof saved_args);
+  saved_nargs = znargs;
+
+  znargs = 1;
+  zargs[0] = routine;
+  call(2);
+
+  process_instructions();
+
+  memcpy(zargs, saved_args, sizeof saved_args);
+  znargs = saved_nargs;
+
+  return POP_STACK();
+}
+#endif
+
+void zcall_store(void)
+{
+  call(1);
+}
+void zcall_nostore(void)
+{
+  call(0);
+}
+
+void do_return(uint16_t retval)
+{
+  uint16_t where;
+
+  ZASSERT(NFRAMES > 1, "return attempted outside of a function");
+
+  pc = CURRENT_FRAME->pc;
+  sp = CURRENT_FRAME->sp;
+  where = CURRENT_FRAME->where;
+  fp--;
+
+  if(where <= 0xff)
+  {
+    store_variable(where, retval);
+  }
+  else if(where == 0xff + 2)
+  {
+    PUSH_STACK(retval);
+    break_from(interrupt_level());
+  }
+}
+
+void zret_popped(void)
+{
+  do_return(POP_STACK());
+}
+
+void zpop(void)
+{
+  POP_STACK();
+}
+
+void zcatch(void)
+{
+  ZASSERT(zversion == 6 || NFRAMES > 1, "@catch called outside of a function");
+
+  /* Must account for the dummy frame in non-V6 stories. */
+  store(zversion == 6 ? NFRAMES : NFRAMES - 1);
+}
+
+void zthrow(void)
+{
+  /* As with @catch, account for the dummy frame. */
+  if(zversion != 6) zargs[1]++;
+
+  ZASSERT(zversion == 6 || NFRAMES > 1, "@throw called outside of a function");
+  ZASSERT(zargs[1] <= NFRAMES, "unwinding too far");
+
+  fp = BASE_OF_FRAMES + zargs[1];
+
+  do_return(zargs[0]);
+}
+
+void zret(void)
+{
+  do_return(zargs[0]);
+}
+
+void zrtrue(void)
+{
+  do_return(1);
+}
+
+void zrfalse(void)
+{
+  do_return(0);
+}
+
+void zcheck_arg_count(void)
+{
+  ZASSERT(zversion == 6 || NFRAMES > 1, "@check_arg_count called outside of a function");
+
+  branch_if(zargs[0] <= CURRENT_FRAME->nargs);
+}
+
+void zpop_stack(void)
+{
+  if(znargs == 1)
+  {
+    for(uint16_t i = 0; i < zargs[0]; i++) POP_STACK();
+  }
+  else
+  {
+    user_store_word(zargs[1], user_word(zargs[1]) + zargs[0]);
+  }
+}
+
+void zpush_stack(void)
+{
+  uint16_t slots = user_word(zargs[1]);
+
+  if(slots == 0)
+  {
+    branch_if(0);
+    return;
+  }
+
+  user_store_word(zargs[1] + (2 * slots), zargs[0]);
+  user_store_word(zargs[1], slots - 1);
+
+  branch_if(1);
+}
+
+/* Compress dynamic memory according to Quetzal.  Memory is allocated
+ * for the passed-in pointer, and must be freed by the caller.  The
+ * return value is the size of compressed memory, or 0 on failure.
+ */
+static uint32_t compress_memory(uint8_t **compressed)
+{
+  uint32_t ret = 0;
+  long i = 0;
+  uint8_t *tmp;
+
+  /* The output buffer needs to be 1.5× the size of dynamic memory for
+   * the worst-case scenario: every other byte in memory differs from
+   * the story file.  This will cause every other byte to take up two
+   * bytes in the output, thus creating 3 bytes of output for every 2 of
+   * input.  This should round up for the extreme case of alternating
+   * zero/non-zero bytes with zeroes at the beginning and end, but due
+   * to the fact that trailing zeroes are not stored, it does not need
+   * to.
+   */
+  tmp = malloc((3 * header.static_start) / 2);
+  if(tmp == NULL) return 0;
+
+  while(1)
+  {
+    long run = i;
+
+    /* Count zeroes.  Stop counting when:
+     * • The end of dynamic memory is reached
+     * • A non-zero value is found
+     */
+    while(i < header.static_start && (BYTE(i) ^ dynamic_memory[i]) == 0)
+    {
+      i++;
+    }
+
+    run = i - run;
+
+    /* A run of zeroes at the end need not be written. */
+    if(i == header.static_start) break;
+
+    /* If there has been a run of zeroes, write them out
+     * 256 at a time.
+     */
+    while(run > 0)
+    {
+      tmp[ret++] = 0;
+      tmp[ret++] = (run > 256 ? 255 : run - 1);
+      run -= 256;
+    }
+
+    /* The current byte differs from the story, so write it. */
+    tmp[ret++] = BYTE(i) ^ dynamic_memory[i];
+
+    i++;
+  }
+
+  *compressed = realloc(tmp, ret);
+  if(*compressed == NULL) *compressed = tmp;
+
+  return ret;
+}
+
+/* Reverse of the above function. */
+static int uncompress_memory(const uint8_t *compressed, uint32_t size)
+{
+  uint32_t memory_index = 0;
+
+  memcpy(memory, dynamic_memory, header.static_start);
+
+  for(uint32_t i = 0; i < size; i++)
+  {
+    if(compressed[i] != 0)
+    {
+      if(memory_index == header.static_start) return -1;
+      STORE_BYTE(memory_index, BYTE(memory_index) ^ compressed[i]);
+      memory_index++;
+    }
+    else
+    {
+      if(++i == size) return -1;
+
+      if(memory_index + (compressed[i] + 1) > header.static_start) return -1;
+      memory_index += (compressed[i] + 1);
+    }
+  }
+
+  return 0;
+}
+
+/* Push the current game state onto the game-state stack. */
+int push_save(void)
+{
+  struct save_state *new;
+
+  if(options.max_saves == 0) return -1;
+
+  new = malloc(sizeof *new);
+  if(new == NULL) goto err;
+  new->stack = NULL;
+  new->frames = NULL;
+
+  new->pc = pc;
+
+  new->stack_size = sp - BASE_OF_STACK;
+  new->stack = malloc(new->stack_size * sizeof *new->stack);
+  if(new->stack == NULL) goto err;
+  memcpy(new->stack, BASE_OF_STACK, new->stack_size * sizeof *new->stack);
+
+  new->nframes = NFRAMES;
+  new->frames = malloc(new->nframes * sizeof *new->frames);
+  if(new->frames == NULL) goto err;
+  memcpy(new->frames, BASE_OF_FRAMES, new->nframes * sizeof *new->frames);
+
+  if(options.disable_undo_compression)
+  {
+    new->memory = malloc(header.static_start);
+    if(new->memory == NULL) goto err;
+    memcpy(new->memory, memory, header.static_start);
+  }
+  else
+  {
+    new->memsize = compress_memory(&new->memory);
+    if(new->memsize == 0) goto err;
+  }
+
+  /* If the maximum number has been reached, drop the last element.
+   * A negative value for max_saves means there is no maximum.
+   */
+  if(options.max_saves > 0 && nsaves == options.max_saves)
+  {
+    struct save_state *tmp = saves_tail;
+    saves_tail = saves_tail->prev;
+    if(saves_tail == NULL) saves_head = NULL;
+    else                   saves_tail->next = NULL;
+    free(tmp->stack);
+    free(tmp->frames);
+    free(tmp->memory);
+    free(tmp);
+    nsaves--;
+  }
+
+  new->next = saves_head;
+  new->prev = NULL;
+  if(new->next != NULL) new->next->prev = new;
+  saves_head = new;
+  if(saves_tail == NULL) saves_tail = new;
+
+  nsaves++;
+
+  return 1;
+
+err:
+  if(new != NULL)
+  {
+    free(new->stack);
+    free(new->frames);
+    free(new);
+  }
+
+  return 0;
+}
+
+/* Pop the last-stored game state and jump to it. */
+int pop_save(void)
+{
+  struct save_state *p;
+
+  if(nsaves == 0) return 0;
+
+  p = saves_head;
+
+  pc = p->pc;
+
+  if(options.disable_undo_compression)
+  {
+    memcpy(memory, p->memory, header.static_start);
+  }
+  else
+  {
+    /* If this fails it’s a bug: unlike Quetzal files, the contents of
+     * p->memory are known to be good, because the compression was done
+     * by us with no chance for corruption (apart, again, from bugs).
+     */
+    if(uncompress_memory(p->memory, p->memsize) == -1) die("error uncompressing memory: unable to continue");
+  }
+
+  sp = BASE_OF_STACK + p->stack_size;
+  memcpy(BASE_OF_STACK, p->stack, sizeof *sp * p->stack_size);
+
+  fp = BASE_OF_FRAMES + p->nframes;
+  memcpy(BASE_OF_FRAMES, p->frames, sizeof *p->frames * p->nframes);
+
+  /* Never pop off the last state.  A story has every right to call
+   * @restore_undo as many times as it called @save_undo.  However, if
+   * there aren’t enough save slots, popping off the last state would
+   * cause @restore_undo to return failure when it should not.
+   */
+  if(nsaves > 1)
+  {
+    saves_head = saves_head->next;
+    saves_head->prev = NULL;
+
+    free(p->stack);
+    free(p->frames);
+    free(p->memory);
+    free(p);
+
+    nsaves--;
+  }
+
+  return 2;
+}
+
+void zsave_undo(void)
+{
+  if(interrupt_level() != 0) die("@save_undo called inside of an interrupt");
+
+  store(push_save());
+}
+
+void zrestore_undo(void)
+{
+  uint16_t flags2;
+
+  /* §6.1.2: Flags 2 should be preserved. */
+  flags2 = WORD(0x10);
+  store(pop_save());
+  STORE_WORD(0x10, flags2);
+}
+
+/* Quetzal save/restore functions. */
+static jmp_buf exception;
+#define WRITE8(v)  do { uint8_t  v_ = (v); if(zterp_io_write(savefile, &v_, sizeof v_) != sizeof v_) longjmp(exception, 1); local_written += 1; } while(0)
+#define WRITE16(v) do { uint16_t w_ = (v); WRITE8(w_ >>  8); WRITE8(w_ & 0xff); } while(0)
+#define WRITE32(v) do { uint32_t x_ = (v); WRITE8(x_ >> 24); WRITE8((x_ >> 16) & 0xff); WRITE8((x_ >> 8) & 0xff); WRITE8(x_ & 0xff); } while(0)
+#define WRITEID(v) WRITE32(STRID(v))
+
+static size_t quetzal_write_stack(zterp_io *savefile)
+{
+  size_t local_written = 0;
+
+  /* Add one more “fake” call frame with just enough information to
+   * calculate the evaluation stack used by the current routine.
+   */
+  fp->sp = sp;
+  for(struct call_frame *p = BASE_OF_FRAMES; p != fp; p++)
+  {
+    uint8_t temp;
+
+    WRITE8((p->pc >> 16) & 0xff);
+    WRITE8((p->pc >>  8) & 0xff);
+    WRITE8((p->pc >>  0) & 0xff);
+
+    temp = p->nlocals;
+    if(p->where > 0xff) temp |= 0x10;
+    WRITE8(temp);
+
+    if(p->where > 0xff) WRITE8(0);
+    else                WRITE8(p->where);
+
+    WRITE8((1U << p->nargs) - 1);
+
+    /* number of words of evaluation stack used */
+    WRITE16((p + 1)->sp - p->sp);
+
+    /* local variables */
+    for(int i = 0; i < p->nlocals; i++) WRITE16(p->locals[i]);
+
+    /* evaluation stack */
+    for(ptrdiff_t i = 0; i < (p + 1)->sp - p->sp; i++) WRITE16(p->sp[i]);
+  }
+
+  return local_written;
+}
+
+int save_quetzal(zterp_io *savefile, int is_meta)
+{
+  if(setjmp(exception) != 0) return 0;
+
+  size_t local_written = 0;
+  size_t game_len;
+  uint32_t memsize;
+  uint8_t *compressed;
+  uint8_t *mem = memory;
+  long stks_pos;
+  size_t stack_size;
+
+  WRITEID("FORM");
+  WRITEID("    "); /* to be filled in */
+  WRITEID(is_meta ? "BFMS" : "IFZS");
+
+  WRITEID("IFhd");
+  WRITE32(13);
+  WRITE16(header.release);
+  zterp_io_write(savefile, header.serial, 6);
+  local_written += 6;
+  WRITE16(header.checksum);
+  WRITE8(pc >> 16);
+  WRITE8(pc >> 8);
+  WRITE8(pc & 0xff);
+  WRITE8(0); /* padding */
+
+  /* Store the filename in an IntD chunk. */
+  game_len = 12 + strlen(game_file);
+  WRITEID("IntD");
+  WRITE32(game_len);
+  WRITEID("UNIX");
+  WRITE8(0x02);
+  WRITE8(0);
+  WRITE16(0);
+  WRITEID("    ");
+  zterp_io_write(savefile, game_file, game_len - 12);
+  local_written += (game_len - 12);
+  if(game_len & 1) WRITE8(0);
+
+  memsize = compress_memory(&compressed);
+
+  /* It is possible for the compressed memory size to be larger than
+   * uncompressed; in this case, just store the uncompressed memory.
+   */
+  if(memsize > 0 && memsize < header.static_start)
+  {
+    mem = compressed;
+    WRITEID("CMem");
+  }
+  else
+  {
+    memsize = header.static_start;
+    WRITEID("UMem");
+  }
+  WRITE32(memsize);
+  zterp_io_write(savefile, mem, memsize);
+  local_written += memsize;
+  if(memsize & 1) WRITE8(0); /* padding */
+  free(compressed);
+
+  WRITEID("Stks");
+  stks_pos = zterp_io_tell(savefile);
+  WRITEID("    "); /* to be filled in */
+  stack_size = quetzal_write_stack(savefile);
+  local_written += stack_size;
+  if(stack_size & 1) WRITE8(0); /* padding */
+
+  zterp_io_seek(savefile, 4, SEEK_SET);
+  WRITE32(local_written - 8); /* entire file size minus 8 (FORM + size) */
+
+  zterp_io_seek(savefile, stks_pos, SEEK_SET);
+  WRITE32(stack_size); /* size of the stacks chunk */
+
+  return 1;
+}
+
+/* Restoring can put the system in an inconsistent state by restoring
+ * only part of memory: the save file may be corrupt and cause failure
+ * part way through updating memory, for example.  This set of functions
+ * takes a snapshot of the current state of dynamic memory and the
+ * stacks so they can be restored on failure.
+ */
+static uint8_t *memory_backup;
+static uint16_t *stack_backup;
+static int stack_backup_size;
+static struct call_frame *frames_backup;
+static int frames_backup_size;
+
+static void memory_snapshot_free(void)
+{
+  free(memory_backup);
+  free(stack_backup);
+  free(frames_backup);
+
+  memory_backup = NULL;
+  stack_backup = NULL;
+  frames_backup = NULL;
+}
+
+static void memory_snapshot(void)
+{
+  memory_snapshot_free();
+
+  memory_backup = malloc(header.static_start);
+  if(memory_backup == NULL) goto err;
+
+  memcpy(memory_backup, memory, header.static_start);
+
+  stack_backup_size = sp - stack;
+  if(stack_backup_size != 0)
+  {
+    stack_backup = malloc(stack_backup_size * sizeof *stack);
+    if(stack_backup == NULL) goto err;
+    memcpy(stack_backup, stack, stack_backup_size * sizeof *stack);
+  }
+
+  frames_backup_size = fp - frames;
+  if(frames_backup_size != 0)
+  {
+    frames_backup = malloc(frames_backup_size * sizeof *frames);
+    if(frames_backup == NULL) goto err;
+    memcpy(frames_backup, frames, frames_backup_size * sizeof *frames);
+  }
+
+  return;
+
+err:
+  memory_snapshot_free();
+
+  return;
+}
+
+static int memory_restore(void)
+{
+  /* stack_backup and frames_backup will be NULL if the stacks were
+   * empty, so use memory_backup to determine if a snapshot has been
+   * taken.
+   */
+  if(memory_backup == NULL) return 0;
+
+  memcpy(memory, memory_backup, header.static_start);
+  if(stack_backup != NULL) memcpy(stack, stack_backup, stack_backup_size * sizeof *stack);
+  sp = stack + stack_backup_size;
+  if(frames_backup != NULL) memcpy(frames, frames_backup, frames_backup_size * sizeof *frames);
+  fp = frames + frames_backup_size;
+
+  memory_snapshot_free();
+
+  return 1;
+}
+
+#define goto_err(...)  do { show_message("save file error: " __VA_ARGS__); goto err; } while(0)
+#define goto_death(...)        do { show_message("save file error: " __VA_ARGS__); goto death; } while(0)
+
+int restore_quetzal(zterp_io *savefile, int is_meta)
+{
+  zterp_iff *iff;
+  uint32_t size;
+  uint32_t n = 0;
+  uint8_t ifhd[13];
+
+  iff = zterp_iff_parse(savefile, is_meta ? "BFMS" : "IFZS");
+
+  if(iff == NULL ||
+     !zterp_iff_find(iff, "IFhd", &size) ||
+     size != 13 ||
+     zterp_io_read(savefile, ifhd, sizeof ifhd) != sizeof ifhd)
+  {
+    goto_err("corrupted save file or not a save file at all");
+  }
+
+  if(((ifhd[0] << 8) | ifhd[1]) != header.release ||
+     memcmp(&ifhd[2], header.serial, sizeof header.serial) != 0 ||
+     ((ifhd[8] << 8) | ifhd[9]) != header.checksum)
+  {
+    goto_err("wrong game or version");
+  }
+
+  memory_snapshot();
+
+  if(zterp_iff_find(iff, "CMem", &size))
+  {
+    uint8_t buf[size]; /* Too big for the stack? */
+
+    if(zterp_io_read(savefile, buf, size) != size) goto_err("unexpected eof reading compressed memory");
+
+    if(uncompress_memory(buf, size) == -1) goto_death("memory cannot be uncompressed");
+  }
+  else if(zterp_iff_find(iff, "UMem", &size))
+  {
+    if(size != header.static_start) goto_err("memory size mismatch");
+    if(zterp_io_read(savefile, memory, header.static_start) != header.static_start) goto_death("unexpected eof reading memory");
+  }
+  else
+  {
+    goto_err("no memory chunk found");
+  }
+
+  if(!zterp_iff_find(iff, "Stks", &size)) goto_death("no stacks chunk found");
+
+  sp = BASE_OF_STACK;
+  fp = BASE_OF_FRAMES;
+
+  while(n < size)
+  {
+    uint8_t frame[8];
+    uint8_t nlocals;
+    uint16_t nstack;
+    uint8_t nargs = 0;
+
+    if(zterp_io_read(savefile, frame, sizeof frame) != sizeof frame) goto_death("unexpected eof reading stack frame");
+    n += sizeof frame;
+
+    nlocals = frame[3] & 0xf;
+    nstack = (frame[6] << 8) | frame[7];
+    frame[5]++;
+    while(frame[5] >>= 1) nargs++;
+
+    add_frame((frame[0] << 16) | (frame[1] << 8) | frame[2], sp, nlocals, nargs, (frame[3] & 0x10) ? 0xff + 1 : frame[4]);
+
+    for(int i = 0; i < nlocals; i++)
+    {
+      uint16_t l;
+
+      if(!zterp_io_read16(savefile, &l)) goto_death("unexpected eof reading local variable");
+      CURRENT_FRAME->locals[i] = l;
+
+      n += sizeof l;
+    }
+
+    for(uint16_t i = 0; i < nstack; i++)
+    {
+      uint16_t s;
+
+      if(!zterp_io_read16(savefile, &s)) goto_death("unexpected eof reading stack entry");
+      PUSH_STACK(s);
+
+      n += sizeof s;
+    }
+  }
+
+  if(n != size) goto_death("stack size mismatch");
+
+  zterp_iff_free(iff);
+  memory_snapshot_free();
+
+  pc = (ifhd[10] << 16) | (ifhd[11] << 8) | ifhd[12];
+
+  return 1;
+
+death:
+  /* At this point, something vital (memory and/or the stacks) has been
+   * scribbed upon; if there was a successful backup, restore it.
+   * Otherwise the only course of action is to exit.
+   */
+  if(!memory_restore()) die("the system is likely in an inconsistent state");
+
+err:
+  /* A snapshot may have been taken, but neither memory nor the stacks
+   * have been overwritten, so just free the snapshot.
+   */
+  memory_snapshot_free();
+  zterp_iff_free(iff);
+  return 0;
+}
+
+#undef goto_err
+#undef goto_death
+
+/* Perform all aspects of a save, apart from storing/branching.
+ * Returns true if the save was success, false if not.
+ * “is_meta” is true if this save file is from a meta-save.
+ */
+int do_save(int is_meta)
+{
+  zterp_io *savefile;
+  int success;
+
+  savefile = zterp_io_open(NULL, ZTERP_IO_WRONLY | ZTERP_IO_SAVE);
+  if(savefile == NULL)
+  {
+    warning("unable to open save file");
+    return 0;
+  }
+
+  success = save_quetzal(savefile, is_meta);
+
+  zterp_io_close(savefile);
+
+  return success;
+}
+
+/* The suggested filename is ignored, because Glk and, at least as of
+ * right now, zterp_io_open(), do not provide a method to do this.
+ * The “prompt” argument added by standard 1.1 is thus also ignored.
+ */
+void zsave(void)
+{
+  if(interrupt_level() != 0) die("@save called inside of an interrupt");
+
+  int success = do_save(0);
+
+  if(zversion <= 3) branch_if(success);
+  else              store(success);
+}
+
+/* Perform all aspects of a restore, apart from storing/branching.
+ * Returns true if the restore was success, false if not.
+ * “is_meta” is true if this save file is expected to be from a
+ * meta-save.
+ */
+int do_restore(int is_meta)
+{
+  zterp_io *savefile;
+  uint16_t flags2;
+  int success;
+
+  savefile = zterp_io_open(NULL, ZTERP_IO_RDONLY | ZTERP_IO_SAVE);
+  if(savefile == NULL)
+  {
+    warning("unable to open save file");
+    return 0;
+  }
+
+  flags2 = WORD(0x10);
+
+  success = restore_quetzal(savefile, is_meta);
+
+  zterp_io_close(savefile);
+
+  if(success)
+  {
+    /* On a successful restore, we are outside of any interrupt (since
+     * @save cannot be called inside an interrupt), so reset the level
+     * back to zero.  In addition, there may be pending read events that
+     * need to be canceled, so do that, too.
+     */
+    reset_level();
+    cancel_all_events();
+
+    /* §8.6.1.3 */
+    if(zversion == 3) close_upper_window();
+
+    /* The save might be from a different interpreter with different
+     * capabilities, so update the header to indicate what the current
+     * capabilities are...
+     */
+    write_header();
+
+    /* ...except that flags2 should be preserved (§6.1.2). */
+    STORE_WORD(0x10, flags2);
+
+    /* Redraw the status line in games that use one. */
+    if(zversion <= 3) zshow_status();
+  }
+
+  return success;
+}
+
+void zrestore(void)
+{
+  int success = do_restore(0);
+
+  if(zversion <= 3) branch_if(success);
+  else              store(success ? 2 : 0);
+}
diff --git a/interpreters/bocfel/stack.h b/interpreters/bocfel/stack.h
new file mode 100644 (file)
index 0000000..ab87413
--- /dev/null
@@ -0,0 +1,63 @@
+#ifndef ZTERP_STACK_H
+#define ZTERP_STACK_H
+
+#include <stdint.h>
+
+#include "io.h"
+
+#define DEFAULT_STACK_SIZE     0x4000
+#define DEFAULT_CALL_DEPTH     0x400
+
+void init_stack(void);
+
+uint16_t variable(uint16_t);
+void store_variable(uint16_t, uint16_t);
+uint16_t *stack_top_element(void);
+
+void call(int);
+#ifdef ZTERP_GLK
+uint16_t direct_call(uint16_t);
+#endif
+void do_return(uint16_t);
+
+int save_quetzal(zterp_io *, int);
+int restore_quetzal(zterp_io *, int);
+
+int do_save(int);
+int do_restore(int);
+
+int push_save(void);
+int pop_save(void);
+
+void zpush(void);
+void zpull(void);
+void zload(void);
+void zstore(void);
+void zret_popped(void);
+void zpop(void);
+void zcatch(void);
+void zthrow(void);
+void zret(void);
+void zrtrue(void);
+void zrfalse(void);
+void zcheck_arg_count(void);
+void zpop_stack(void);
+void zpush_stack(void);
+void zsave_undo(void);
+void zrestore_undo(void);
+void zsave(void);
+void zrestore(void);
+
+void zcall_store(void);
+void zcall_nostore(void);
+
+#define zcall          zcall_store
+#define zcall_1n       zcall_nostore
+#define zcall_1s       zcall_store
+#define zcall_2n       zcall_nostore
+#define zcall_2s       zcall_store
+#define zcall_vn       zcall_nostore
+#define zcall_vn2      zcall_nostore
+#define zcall_vs2      zcall_store
+
+#endif
diff --git a/interpreters/bocfel/table.c b/interpreters/bocfel/table.c
new file mode 100644 (file)
index 0000000..1de5e9e
--- /dev/null
@@ -0,0 +1,90 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "table.h"
+#include "branch.h"
+#include "memory.h"
+#include "process.h"
+#include "zterp.h"
+
+void zcopy_table(void)
+{
+  uint16_t first = zargs[0], second = zargs[1], size = zargs[2];
+
+  if(second == 0)
+  {
+    for(uint16_t i = 0; i < size; i++) user_store_byte(first + i, 0);
+  }
+  else if( (first > second) || (int16_t)size < 0 )
+  {
+    long n = labs((int16_t)size);
+    for(long i = 0; i < n; i++) user_store_byte(second + i, user_byte(first + i));
+  }
+  else
+  {
+    for(uint16_t i = 0; i < size; i++) user_store_byte(second + size - i - 1, user_byte(first + size - i - 1));
+  }
+}
+
+void zscan_table(void)
+{
+  uint16_t addr = zargs[1];
+
+  if(znargs < 4) zargs[3] = 0x82;
+
+  for(uint16_t i = 0; i < zargs[2]; i++)
+  {
+    if(
+        ( (zargs[3] & 0x80) && (user_word(addr) == zargs[0])) ||
+        (!(zargs[3] & 0x80) && (user_byte(addr) == zargs[0]))
+      )
+    {
+      store(addr);
+      branch_if(1);
+      return;
+    }
+
+    addr += zargs[3] & 0x7f;
+  }
+
+  store(0);
+  branch_if(0);
+}
+
+void zloadw(void)
+{
+  store(user_word(zargs[0] + (2 * zargs[1])));
+}
+
+void zloadb(void)
+{
+  store(user_byte(zargs[0] + zargs[1]));
+}
+
+void zstoreb(void)
+{
+  user_store_byte(zargs[0] + zargs[1], zargs[2]);
+}
+
+void zstorew(void)
+{
+  user_store_word(zargs[0] + (2 * zargs[1]), zargs[2]);
+}
diff --git a/interpreters/bocfel/table.h b/interpreters/bocfel/table.h
new file mode 100644 (file)
index 0000000..0855541
--- /dev/null
@@ -0,0 +1,11 @@
+#ifndef ZTERP_TABLE_H
+#define ZTERP_TABLE_H
+
+void zcopy_table(void);
+void zscan_table(void);
+void zloadw(void);
+void zloadb(void);
+void zstoreb(void);
+void zstorew(void);
+
+#endif
diff --git a/interpreters/bocfel/unicode.c b/interpreters/bocfel/unicode.c
new file mode 100644 (file)
index 0000000..78f610d
--- /dev/null
@@ -0,0 +1,357 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <stdint.h>
+
+#include "unicode.h"
+#include "memory.h"
+#include "util.h"
+#include "zterp.h"
+
+int have_unicode;
+
+/*
+ * The index is the ZSCII value, minus 155 (so entry 0 refers to ZSCII
+ * value 155); and the value at the index is the Unicode character that
+ * the ZSCII value maps to.  Because Latin-1 and Unicode are equivalent
+ * for 0–255, this table maps to both Unicode and Latin1, with the
+ * caveat that values greater than 255 should be considered invalid in
+ * Latin-1, and are translated as a question mark below in
+ * setup_tables() where appropriate.
+ */
+#define UNICODE_TABLE_SIZE     97
+static int unicode_entries = 69;
+static uint16_t unicode_table[UNICODE_TABLE_SIZE] = {
+0x00e4, 0x00f6, 0x00fc, 0x00c4, 0x00d6, 0x00dc, 0x00df, 0x00bb, 0x00ab,
+0x00eb, 0x00ef, 0x00ff, 0x00cb, 0x00cf, 0x00e1, 0x00e9, 0x00ed, 0x00f3,
+0x00fa, 0x00fd, 0x00c1, 0x00c9, 0x00cd, 0x00d3, 0x00da, 0x00dd, 0x00e0,
+0x00e8, 0x00ec, 0x00f2, 0x00f9, 0x00c0, 0x00c8, 0x00cc, 0x00d2, 0x00d9,
+0x00e2, 0x00ea, 0x00ee, 0x00f4, 0x00fb, 0x00c2, 0x00ca, 0x00ce, 0x00d4,
+0x00db, 0x00e5, 0x00c5, 0x00f8, 0x00d8, 0x00e3, 0x00f1, 0x00f5, 0x00c3,
+0x00d1, 0x00d5, 0x00e6, 0x00c6, 0x00e7, 0x00c7, 0x00fe, 0x00f0, 0x00de,
+0x00d0, 0x00a3, 0x0153, 0x0152, 0x00a1, 0x00bf,
+};
+
+/* If a non-default Unicode table is found, this function is called with
+ * its address; it updates the Unicode table above.
+ */
+void parse_unicode_table(uint16_t utable)
+{
+  if(utable >= memory_size) die("corrupted story: unicode table out of range");
+
+  unicode_entries = BYTE(utable++);
+
+  if(unicode_entries > UNICODE_TABLE_SIZE) die("corrupted story: too many entries in the unicode table");
+  if(utable + (2 * unicode_entries) >= memory_size) die("corrupted story: unicode table out of range");
+
+  for(int i = 0; i < unicode_entries; i++)
+  {
+    unicode_table[i] = WORD(utable + (2 * i));
+  }
+}
+
+/* Table used to convert a ZSCII value to Unicode; and since this is
+ * only used for output, non-output values will be returned as a
+ * question mark.
+ */
+uint16_t zscii_to_unicode[UINT8_MAX + 1];
+
+/* These tables translate a Unicode or (Latin-1) character into its
+ * ZSCII equivalent.  Only valid Unicode characters are translated (that
+ * is, those in the range 32–126, or 160 and above).
+ *
+ * The first table will translate invalid Unicode characters to zero;
+ * the second, to a question mark.
+ */
+uint8_t unicode_to_zscii  [UINT16_MAX + 1];
+uint8_t unicode_to_zscii_q[UINT16_MAX + 1];
+
+/* Convenience table: pass through all values 0–255, but yield a question mark
+ * for others. */
+uint8_t unicode_to_latin1[UINT16_MAX + 1];
+
+/* Convert ZSCII to Unicode line-drawing/rune characters. */
+uint16_t zscii_to_font3[UINT8_MAX + 1];
+
+/* Lookup table to see if a character is in the alphabet table.  Key is
+ * the character, value is the index in the alphabet table, or -1.
+ */
+int atable_pos[UINT8_MAX + 1];
+
+/* Not all fonts provide all characters, so there
+ * may well be a lot of question marks.
+ */
+static void build_font3_table(void)
+{
+  for(int i = 0; i < UINT8_MAX; i++) zscii_to_font3[i] = UNICODE_QUESTIONMARK;
+
+  zscii_to_font3[ 32] = UNICODE_SPACE;
+  zscii_to_font3[ 33] = 0x2190; /* ← */
+  zscii_to_font3[ 34] = 0x2192; /* → */
+  zscii_to_font3[ 35] = 0x2571; /* ╱ */
+  zscii_to_font3[ 36] = 0x2572; /* ╲ */
+  zscii_to_font3[ 37] = UNICODE_SPACE;
+  zscii_to_font3[ 38] = 0x2500; /* ─ */
+  zscii_to_font3[ 39] = 0x2500; /* ─ */
+  zscii_to_font3[ 40] = 0x2502; /* │ */
+  zscii_to_font3[ 41] = 0x2502; /* │ (this should be slightly offset, but whatever) */
+  zscii_to_font3[ 42] = 0x2534; /* ┴ */
+  zscii_to_font3[ 43] = 0x252c; /* ┬ */
+  zscii_to_font3[ 44] = 0x251c; /* ├ */
+  zscii_to_font3[ 45] = 0x2524; /* ┤ */
+  zscii_to_font3[ 46] = 0x2514; /* └ */
+  zscii_to_font3[ 47] = 0x250c; /* ┌ */
+  zscii_to_font3[ 48] = 0x2510; /* ┐ */
+  zscii_to_font3[ 49] = 0x2518; /* ┘ */
+
+  /* There are a few characters that have no box-drawing equivalents.
+   * These are the pieces that have connections sticking out of them,
+   * used to link rooms together.  There are two options: have filled
+   * boxes with no connections which makes the rooms look nice but the
+   * connections look bad, or unfilled boxes with connections which
+   * results in bad looking rooms but attached connections.  The end
+   * result is something like this:
+   *
+   * No connections:         Connections:
+   * ╲       ╱               ╲       ╱
+   *  ┌─┐ ▗▄▖                 ╲─┐ ▗▄╱
+   *  │ ├─▐█▌─                │ ├─┤█├─
+   *  └─┘ ▝▀▘                 └─┘ ▝┬▘
+   *       │                       │
+   *
+   * By default the former is done, but the latter can be chosen.
+   */
+  zscii_to_font3[ 50] = options.enable_alt_graphics ? 0x2571 : 0x2514; /* ╱ or └ */
+  zscii_to_font3[ 51] = options.enable_alt_graphics ? 0x2572 : 0x250c; /* ╲ or ┌ */
+  zscii_to_font3[ 52] = options.enable_alt_graphics ? 0x2571 : 0x2510; /* ╱ or ┐ */
+  zscii_to_font3[ 53] = options.enable_alt_graphics ? 0x2572 : 0x2518; /* ╲ or ┘ */
+
+  zscii_to_font3[ 54] = 0x2588; /* █ */
+  zscii_to_font3[ 56] = 0x2584; /* ▄ */
+  zscii_to_font3[ 55] = 0x2580; /* ▀ */
+  zscii_to_font3[ 57] = 0x258c; /* ▌ */
+  zscii_to_font3[ 58] = 0x2590; /* ▐ */
+
+  zscii_to_font3[ 59] = options.enable_alt_graphics ? 0x2534 : 0x2584; /* ┴ or ▄ */
+  zscii_to_font3[ 60] = options.enable_alt_graphics ? 0x252c : 0x2580; /* ┬ or ▀ */
+  zscii_to_font3[ 61] = options.enable_alt_graphics ? 0x251c : 0x258c; /* ├ or ▌ */
+  zscii_to_font3[ 62] = options.enable_alt_graphics ? 0x2524 : 0x2590; /* ┤ or ▐ */
+
+  zscii_to_font3[ 63] = 0x259d; /* ▝ */
+  zscii_to_font3[ 64] = 0x2597; /* ▗ */
+  zscii_to_font3[ 65] = 0x2596; /* ▖ */
+  zscii_to_font3[ 66] = 0x2598; /* ▘ */
+
+  zscii_to_font3[ 67] = options.enable_alt_graphics ? 0x2571 : 0x259d; /* ╱ or ▝ */
+  zscii_to_font3[ 68] = options.enable_alt_graphics ? 0x2572 : 0x2597; /* ╲ or ▗ */
+  zscii_to_font3[ 69] = options.enable_alt_graphics ? 0x2571 : 0x2596; /* ╱ or ▖ */
+  zscii_to_font3[ 70] = options.enable_alt_graphics ? 0x2572 : 0x2598; /* ╲ or ▘ */
+
+  zscii_to_font3[ 75] = 0x2594; /* ▔ */
+  zscii_to_font3[ 76] = 0x2581; /* ▁ */
+  zscii_to_font3[ 77] = 0x258f; /* ▏ */
+  zscii_to_font3[ 78] = 0x2595; /* ▕ */
+
+  zscii_to_font3[ 79] = UNICODE_SPACE;
+  zscii_to_font3[ 80] = 0x258f; /* ▏ */
+  zscii_to_font3[ 81] = 0x258e; /* ▎ */
+  zscii_to_font3[ 82] = 0x258d; /* ▍ */
+  zscii_to_font3[ 83] = 0x258c; /* ▌ */
+  zscii_to_font3[ 84] = 0x258b; /* ▋ */
+  zscii_to_font3[ 85] = 0x258a; /* ▊ */
+  zscii_to_font3[ 86] = 0x2589; /* ▉ */
+  zscii_to_font3[ 87] = 0x2588; /* █ */
+  zscii_to_font3[ 88] = 0x2595; /* ▕ */
+  zscii_to_font3[ 89] = 0x258f; /* ▏ */
+
+  zscii_to_font3[ 90] = 0x2573; /* ╳ */
+  zscii_to_font3[ 91] = 0x253c; /* ┼ */
+  zscii_to_font3[ 92] = 0x2191; /* ↑ */
+  zscii_to_font3[ 93] = 0x2193; /* ↓ */
+  zscii_to_font3[ 94] = 0x2195; /* ↕ */
+  zscii_to_font3[ 95] = 0x2b1c; /* ⬜ */
+  zscii_to_font3[ 96] = UNICODE_QUESTIONMARK;
+  zscii_to_font3[ 97] = 0x16aa; /* ᚪ */
+  zscii_to_font3[ 98] = 0x16d2; /* ᛒ */
+  zscii_to_font3[ 99] = 0x16c7; /* ᛇ */
+  zscii_to_font3[100] = 0x16de; /* ᛞ */
+  zscii_to_font3[101] = 0x16d6; /* ᛖ */
+  zscii_to_font3[102] = 0x16a0; /* ᚠ */
+  zscii_to_font3[103] = 0x16b7; /* ᚷ */
+  zscii_to_font3[104] = 0x16bb; /* ᚻ */
+  zscii_to_font3[105] = 0x16c1; /* ᛁ */
+  zscii_to_font3[106] = 0x16e8; /* ᛨ */
+  zscii_to_font3[107] = 0x16e6; /* ᛦ */
+  zscii_to_font3[108] = 0x16da; /* ᛚ */
+  zscii_to_font3[109] = 0x16d7; /* ᛗ */
+  zscii_to_font3[110] = 0x16be; /* ᚾ */
+  zscii_to_font3[111] = 0x16a9; /* ᚩ */
+  zscii_to_font3[112] = UNICODE_QUESTIONMARK; /* no good symbol */
+  zscii_to_font3[113] = 0x0068; /* Unicode 'h'; close to the rune. */
+  zscii_to_font3[114] = 0x16b1; /* ᚱ */
+  zscii_to_font3[115] = 0x16cb; /* ᛋ */
+  zscii_to_font3[116] = 0x16cf; /* ᛏ */
+  zscii_to_font3[117] = 0x16a2; /* ᚢ */
+  zscii_to_font3[118] = 0x16e0; /* ᛠ */
+  zscii_to_font3[119] = 0x16b9; /* ᚹ */
+  zscii_to_font3[120] = 0x16c9; /* ᛉ */
+  zscii_to_font3[121] = 0x16a5; /* ᚥ */
+  zscii_to_font3[122] = 0x16df; /* ᛟ */
+
+  /* These are reversed (see §16); a slightly ugly hack in screen.c is
+   * used to accomplish this.
+   */
+  zscii_to_font3[123] = 0x2191; /* ↑ */
+  zscii_to_font3[124] = 0x2193; /* ↓ */
+  zscii_to_font3[125] = 0x2195; /* ↕ */
+  zscii_to_font3[126] = UNICODE_QUESTIONMARK;
+}
+
+void setup_tables(void)
+{
+  /*** ZSCII to Unicode table. ***/
+
+  for(int i = 0; i < UINT8_MAX + 1; i++) zscii_to_unicode[i] = UNICODE_QUESTIONMARK;
+  zscii_to_unicode[0] = 0;
+  zscii_to_unicode[ZSCII_NEWLINE] = UNICODE_LINEFEED;
+
+  if(zversion == 6) zscii_to_unicode[ 9] = UNICODE_SPACE; /* Tab. */
+  if(zversion == 6) zscii_to_unicode[11] = UNICODE_SPACE; /* Sentence space. */
+
+  for(int i = 32; i < 127; i++) zscii_to_unicode[i] = i;
+  for(int i = 0; i < unicode_entries; i++)
+  {
+    uint16_t c = unicode_table[i];
+
+    if(!valid_unicode(c)) c = UNICODE_QUESTIONMARK;
+
+    /* If Unicode is not available, then any values > 255 are invalid. */
+    else if(!have_unicode && c > 255) c = UNICODE_QUESTIONMARK;
+
+    zscii_to_unicode[i + 155] = c;
+  }
+
+  /*** Unicode to ZSCII tables. ***/
+
+  /* Default values. */
+  memset(unicode_to_zscii, 0, sizeof unicode_to_zscii);
+  memset(unicode_to_zscii_q, ZSCII_QUESTIONMARK, sizeof unicode_to_zscii_q);
+
+  /* First fill up the entries found in the Unicode table. */
+  for(int i = 0; i < unicode_entries; i++)
+  {
+    uint16_t c = unicode_table[i];
+
+    if(valid_unicode(c))
+    {
+      unicode_to_zscii  [c] = i + 155;
+      unicode_to_zscii_q[c] = i + 155;
+    }
+  }
+
+  /* Now the values that are equivalent in ZSCII and Unicode. */
+  for(int i = 32; i < 127; i++)
+  {
+    unicode_to_zscii  [i] = i;
+    unicode_to_zscii_q[i] = i;
+  }
+
+  /* Properly translate a newline. */
+  unicode_to_zscii_q[UNICODE_LINEFEED] = ZSCII_NEWLINE;
+
+  /*** Unicode to Latin1 table. ***/
+
+  memset(unicode_to_latin1, UNICODE_QUESTIONMARK, sizeof unicode_to_latin1);
+  for(int i = 0; i < 256; i++) unicode_to_latin1[i] = i;
+
+  /*** ZSCII to character graphics table. ***/
+
+  build_font3_table();
+
+  /*** Alphabet table. ***/
+
+  for(int i = 0; i < 256; i++) atable_pos[i] = -1;
+
+  /* 52 is A2 character 6, which is special and should not
+   * be matched, so skip over it.
+   */
+  for(int i = 0;  i < 52    ; i++) atable_pos[atable[i]] = i;
+  for(int i = 53; i < 26 * 3; i++) atable_pos[atable[i]] = i;
+}
+
+/* This is adapted from Zip2000 (Copyright 2001 Kevin Bracey). */
+uint16_t unicode_tolower(uint16_t c)
+{
+  static const unsigned char basic_latin[0x100] =
+  {
+    0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+    0x40, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
+    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+    0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
+    0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
+    0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
+    0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
+    0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
+    0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+    0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
+    0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xd7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xdf,
+    0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
+    0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
+  };
+  static const unsigned char latin_extended_a[0x80] =
+  {
+    0x01, 0x01, 0x03, 0x03, 0x05, 0x05, 0x07, 0x07, 0x09, 0x09, 0x0b, 0x0b, 0x0d, 0x0d, 0x0f, 0x0f,
+    0x11, 0x11, 0x13, 0x13, 0x15, 0x15, 0x17, 0x17, 0x19, 0x19, 0x1b, 0x1b, 0x1d, 0x1d, 0x1f, 0x1f,
+    0x21, 0x21, 0x23, 0x23, 0x25, 0x25, 0x27, 0x27, 0x29, 0x29, 0x2b, 0x2b, 0x2d, 0x2d, 0x2f, 0x2f,
+    0x00, 0x31, 0x33, 0x33, 0x35, 0x35, 0x37, 0x37, 0x38, 0x3a, 0x3a, 0x3c, 0x3c, 0x3e, 0x3e, 0x40,
+    0x40, 0x42, 0x42, 0x44, 0x44, 0x46, 0x46, 0x48, 0x48, 0x49, 0x4b, 0x4b, 0x4d, 0x4d, 0x4f, 0x4f,
+    0x51, 0x51, 0x53, 0x53, 0x55, 0x55, 0x57, 0x57, 0x59, 0x59, 0x5b, 0x5b, 0x5d, 0x5d, 0x5f, 0x5f,
+    0x61, 0x61, 0x63, 0x63, 0x65, 0x65, 0x67, 0x67, 0x69, 0x69, 0x6b, 0x6b, 0x6d, 0x6d, 0x6f, 0x6f,
+    0x71, 0x71, 0x73, 0x73, 0x75, 0x75, 0x77, 0x77, 0x00, 0x7a, 0x7a, 0x7c, 0x7c, 0x7e, 0x7e, 0x7f
+  };
+  static const unsigned char greek[0x50] =
+  {
+    0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0xac, 0x87, 0xad, 0xae, 0xaf, 0x8b, 0xcc, 0x8d, 0xcd, 0xce,
+    0x90, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+    0xc0, 0xc1, 0xa2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xac, 0xad, 0xae, 0xaf,
+    0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
+    0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf
+  };
+  static const unsigned char cyrillic[0x60] =
+  {
+    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
+    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+    0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
+    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+    0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
+    0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f
+  };
+
+  if     (c  < 0x0100)             c = basic_latin[c];
+  else if(c == 0x0130)             c = 0x0069; /* capital i with dot -> lower case i */
+  else if(c == 0x0178)             c = 0x00ff; /* capital y diaeresis -> lower case y diaeresis */
+  else if(c <  0x0180)             c = latin_extended_a[c - 0x100] + 0x100;
+  else if(c >= 0x380 && c < 0x3d0) c = greek           [c - 0x380] + 0x300;
+  else if(c >= 0x400 && c < 0x460) c = cyrillic        [c - 0x400] + 0x400;
+
+  return c;
+}
diff --git a/interpreters/bocfel/unicode.h b/interpreters/bocfel/unicode.h
new file mode 100644 (file)
index 0000000..b421fef
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef ZTERP_TABLES_H
+#define ZTERP_TABLES_H
+
+#include <stdint.h>
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#endif
+
+#define UNICODE_LINEFEED       10
+#define UNICODE_SPACE          32
+#define UNICODE_QUESTIONMARK   63
+
+#define ZSCII_NEWLINE          13
+#define ZSCII_SPACE            32
+#define ZSCII_QUESTIONMARK     63
+
+/* This variable controls whether Unicode is used for screen
+ * output.  This affects @check_unicode as well as the ZSCII to
+ * Unicode table.  With Glk it is set based on whether the Glk
+ * implementation supports Unicode (checked with the Unicode
+ * gestalt), and determines whether Unicode IO functions should
+ * be used; otherwise, it is kept in parallel with use_utf8_io.
+ */
+extern int have_unicode;
+
+extern uint16_t zscii_to_unicode[];
+extern uint8_t unicode_to_zscii[];
+extern uint8_t unicode_to_zscii_q[];
+extern uint8_t unicode_to_latin1[];
+extern uint16_t zscii_to_font3[];
+extern int atable_pos[];
+
+void parse_unicode_table(uint16_t);
+void setup_tables(void);
+
+uint16_t unicode_tolower(uint16_t);
+
+/* Standard 1.1 notes that Unicode characters 0–31 and 127–159
+ * are invalid due to the fact that they’re control codes.
+ */
+static inline int valid_unicode(uint16_t c) { return (c >= 32 && c <= 126) || c >= 160; }
+
+#endif
diff --git a/interpreters/bocfel/util.c b/interpreters/bocfel/util.c
new file mode 100644 (file)
index 0000000..6cd3d9a
--- /dev/null
@@ -0,0 +1,260 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "util.h"
+#include "screen.h"
+#include "unicode.h"
+#include "zterp.h"
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#include <libchimara/garglk.h>
+#endif
+
+#ifndef ZTERP_NO_SAFETY_CHECKS
+unsigned long zassert_pc;
+
+void assert_fail(const char *fmt, ...)
+{
+  va_list ap;
+  char str[1024];
+
+  va_start(ap, fmt);
+  vsnprintf(str, sizeof str, fmt, ap);
+  va_end(ap);
+
+  snprintf(str + strlen(str), sizeof str - strlen(str), " (pc = 0x%lx)", zassert_pc);
+
+  die("%s", str);
+}
+#endif
+
+void warning(const char *fmt, ...)
+{
+  va_list ap;
+  char str[1024];
+
+  va_start(ap, fmt);
+  vsnprintf(str, sizeof str, fmt, ap);
+  va_end(ap);
+
+  show_message("WARNING: %s", str);
+}
+
+void die(const char *fmt, ...)
+{
+  va_list ap;
+  char str[1024];
+
+  va_start(ap, fmt);
+  vsnprintf(str, sizeof str, fmt, ap);
+  va_end(ap);
+
+  show_message("fatal error: %s", str);
+
+#ifdef ZTERP_GLK
+#ifdef GARGLK
+  fprintf(stderr, "%s\n", str);
+#endif
+  glk_exit();
+#endif
+
+  exit(EXIT_FAILURE);
+}
+
+/* This is not POSIX compliant, but it gets the job done.
+ * It should not be called more than once.
+ */
+static int zoptind = 0;
+static const char *zoptarg;
+static int zgetopt(int argc, char **argv, const char *optstring)
+{
+  static const char *p = "";
+  const char *optp;
+  int c;
+
+  if(*p == 0)
+  {
+    /* No more arguments. */
+    if(++zoptind >= argc) return -1;
+
+    p = argv[zoptind];
+
+    /* No more options. */
+    if(p[0] != '-' || p[1] == 0) return -1;
+
+    /* Handle “--” */
+    if(*++p == '-')
+    {
+      zoptind++;
+      return -1;
+    }
+  }
+
+  c = *p++;
+
+  optp = strchr(optstring, c);
+  if(optp == NULL) return '?';
+
+  if(optp[1] == ':')
+  {
+    if(*p != 0) zoptarg = p;
+    else        zoptarg = argv[++zoptind];
+
+    p = "";
+    if(zoptarg == NULL) return '?';
+  }
+
+  return c;
+}
+
+char *xstrdup(const char *s)
+{
+  size_t n;
+  char *r;
+
+  n = strlen(s) + 1;
+
+  r = malloc(n);
+  if(r != NULL) memcpy(r, s, n);
+
+  return r;
+}
+
+int process_arguments(int argc, char **argv)
+{
+  int c;
+
+  while( (c = zgetopt(argc, argv, "a:A:cCdDeE:fFgGiklLmn:N:rR:sS:tT:u:UvxXyz:Z:")) != -1 )
+  {
+    switch(c)
+    {
+      case 'a':
+        options.eval_stack_size = strtol(zoptarg, NULL, 10);
+        break;
+      case 'A':
+        options.call_stack_size = strtol(zoptarg, NULL, 10);
+        break;
+      case 'c':
+        options.disable_color = 1;
+        break;
+      case 'C':
+        options.disable_config = 1;
+        break;
+      case 'd':
+        options.disable_timed = 1;
+        break;
+      case 'D':
+        options.disable_sound = 1;
+        break;
+      case 'e':
+        options.enable_escape = 1;
+        break;
+      case 'E':
+        options.escape_string = xstrdup(zoptarg);
+        break;
+      case 'f':
+        options.disable_fixed = 1;
+        break;
+      case 'F':
+        options.assume_fixed = 1;
+        break;
+      case 'g':
+        options.disable_graphics_font = 1;
+        break;
+      case 'G':
+        options.enable_alt_graphics = 1;
+        break;
+      case 'i':
+        options.show_id = 1;
+        break;
+      case 'k':
+        options.disable_term_keys = 1;
+        break;
+      case 'l':
+        options.disable_utf8 = 1;
+        break;
+      case 'L':
+        options.force_utf8 = 1;
+        break;
+      case 'm':
+        options.disable_meta_commands = 1;
+        break;
+      case 'n':
+        options.int_number = strtol(zoptarg, NULL, 10);
+        break;
+      case 'N':
+        options.int_version = zoptarg[0];
+        break;
+      case 'r':
+        options.replay_on = 1;
+        break;
+      case 'R':
+        options.replay_name = xstrdup(zoptarg);
+        break;
+      case 's':
+        options.record_on = 1;
+        break;
+      case 'S':
+        options.record_name = xstrdup(zoptarg);
+        break;
+      case 't':
+        options.transcript_on = 1;
+        break;
+      case 'T':
+        options.transcript_name = xstrdup(zoptarg);
+        break;
+      case 'u':
+        options.max_saves = strtol(zoptarg, NULL, 10);
+        break;
+      case 'U':
+        options.disable_undo_compression = 1;
+        break;
+      case 'v':
+        options.show_version = 1;
+        break;
+      case 'x':
+        options.disable_abbreviations = 1;
+        break;
+      case 'X':
+        options.enable_censorship = 1;
+        break;
+      case 'y':
+        options.overwrite_transcript = 1;
+        break;
+      case 'z':
+        options.random_seed = strtol(zoptarg, NULL, 10);
+        break;
+      case 'Z':
+        options.random_device = xstrdup(zoptarg);
+        break;
+      default:
+        return 0;
+    }
+  }
+
+  /* Just ignore excess stories for now. */
+  if(zoptind < argc) game_file = argv[zoptind];
+
+  return 1;
+}
diff --git a/interpreters/bocfel/util.h b/interpreters/bocfel/util.h
new file mode 100644 (file)
index 0000000..a3047a7
--- /dev/null
@@ -0,0 +1,64 @@
+#ifndef ZTERP_UTIL_H
+#define ZTERP_UTIL_H
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#endif
+
+#if defined(__GNUC__) && (__GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 7))
+#define znoreturn              __attribute__((__noreturn__))
+#define zprintflike(f, a)      __attribute__((__format__(__printf__, f, a)))
+#else
+#define znoreturn
+#define zprintflike(f, a)
+#endif
+
+#if defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1))
+#define zexternally_visible    __attribute__((__externally_visible__))
+#else
+#define zexternally_visible
+#endif
+
+#ifndef ZTERP_NO_SAFETY_CHECKS
+extern unsigned long zassert_pc;
+#define ZPC(pc)                do { zassert_pc = pc; } while(0)
+
+zprintflike(1, 2)
+znoreturn
+void assert_fail(const char *, ...);
+#define ZASSERT(expr, ...) do { if(!(expr)) assert_fail(__VA_ARGS__); } while(0)
+#else
+#define ZPC(pc)                        ((void)0)
+#define ZASSERT(expr, ...)     ((void)0)
+#endif
+
+zprintflike(1, 2)
+void warning(const char *, ...);
+
+zprintflike(1, 2)
+znoreturn
+void die(const char *, ...);
+
+char *xstrdup(const char *);
+int process_arguments(int, char **);
+
+/* Somewhat ugly hack to get around the fact that some Glk functions may
+ * not exist.  These function calls should all be guarded (e.g.
+ * if(have_unicode), with have_unicode being set iff GLK_MODULE_UNICODE
+ * is defined) so they will never be called if the Glk implementation
+ * being used does not support them, but they must at least exist to
+ * prevent link errors.
+ */
+#ifdef ZTERP_GLK
+#ifndef GLK_MODULE_UNICODE
+#define glk_put_char_uni(...)          die("bug %s:%d: glk_put_char_uni() called with no unicode", __FILE__, __LINE__)
+#define glk_put_string_uni(...)                die("bug %s:%d: glk_put_string_uni() called with no unicode", __FILE__, __LINE__)
+#define glk_request_char_event_uni(...)        die("bug %s:%d: glk_request_char_event_uni() called with no unicode", __FILE__, __LINE__)
+#define glk_request_line_event_uni(...)        die("bug %s:%d: glk_request_line_event_uni() called with no unicode", __FILE__, __LINE__)
+#endif
+#ifndef GLK_MODULE_LINE_ECHO
+#define glk_set_echo_line_event(...)   die("bug: %s %d: glk_set_echo_line_event() called with no line echo", __FILE__, __LINE__)
+#endif
+#endif
+
+#endif
diff --git a/interpreters/bocfel/zoom.c b/interpreters/bocfel/zoom.c
new file mode 100644 (file)
index 0000000..e805762
--- /dev/null
@@ -0,0 +1,48 @@
+/*-
+ * Copyright 2010-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <time.h>
+
+#include "zoom.h"
+#include "screen.h"
+#include "zterp.h"
+
+static clock_t start_clock, end_clock;
+
+void zstart_timer(void)
+{
+  start_clock = clock();
+}
+
+void zstop_timer(void)
+{
+  end_clock = clock();
+}
+
+void zread_timer(void)
+{
+  store(100 * (end_clock - start_clock) / CLOCKS_PER_SEC);
+}
+
+void zprint_timer(void)
+{
+  char buf[32];
+  snprintf(buf, sizeof buf, "%.2f seconds", (end_clock - start_clock) / (double)CLOCKS_PER_SEC);
+  for(int i = 0; buf[i] != 0; i++) put_char(buf[i]);
+}
diff --git a/interpreters/bocfel/zoom.h b/interpreters/bocfel/zoom.h
new file mode 100644 (file)
index 0000000..9611b83
--- /dev/null
@@ -0,0 +1,9 @@
+#ifndef ZTERP_ZOOM_H
+#define ZTERP_ZOOM_H
+
+void zstart_timer(void);
+void zstop_timer(void);
+void zread_timer(void);
+void zprint_timer(void);
+
+#endif
diff --git a/interpreters/bocfel/zterp.c b/interpreters/bocfel/zterp.c
new file mode 100644 (file)
index 0000000..49b84a0
--- /dev/null
@@ -0,0 +1,1053 @@
+/*-
+ * Copyright 2009-2012 Chris Spiegel.
+ *
+ * This file is part of Bocfel.
+ *
+ * Bocfel is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version
+ * 2 or 3, as published by the Free Software Foundation.
+ *
+ * Bocfel 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 Bocfel.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <ctype.h>
+#include <signal.h>
+#include <limits.h>
+
+#include "zterp.h"
+#include "blorb.h"
+#include "branch.h"
+#include "io.h"
+#include "memory.h"
+#include "osdep.h"
+#include "process.h"
+#include "random.h"
+#include "screen.h"
+#include "stack.h"
+#include "unicode.h"
+#include "util.h"
+
+#ifdef ZTERP_GLK
+#include <glk.h>
+#include <libchimara/garglk.h>
+#ifdef GARGLK
+#include <glkstart.h>
+#include <gi_blorb.h>
+
+static schanid_t sound_channel = NULL;
+#endif
+#endif
+
+#define MAX_LINE       2048
+#define MAX_PATH       4096
+
+#define ZTERP_VERSION  "0.6.1"
+
+const char *game_file;
+struct options options = {
+  .eval_stack_size = DEFAULT_STACK_SIZE,
+  .call_stack_size = DEFAULT_CALL_DEPTH,
+  .disable_color = 0,
+  .disable_config = 0,
+  .disable_sound = 0,
+  .disable_timed = 0,
+  .enable_escape = 0,
+  .escape_string = NULL,
+  .disable_fixed = 0,
+  .assume_fixed = 0,
+  .disable_graphics_font = 0,
+  .enable_alt_graphics = 0,
+  .show_id = 0,
+  .disable_term_keys = 0,
+  .disable_utf8 = 0,
+  .force_utf8 = 0,
+  .disable_meta_commands = 0,
+  .int_number = 1, /* DEC */
+  .int_version = 'C',
+  .replay_on = 0,
+  .replay_name = NULL,
+  .record_on = 0,
+  .record_name = NULL,
+  .transcript_on = 0,
+  .transcript_name = NULL,
+  .max_saves = 10,
+  .disable_undo_compression = 0,
+  .show_version = 0,
+  .disable_abbreviations = 0,
+  .enable_censorship = 0,
+  .overwrite_transcript = 0,
+  .random_seed = -1,
+  .random_device = NULL,
+};
+
+static char story_id[64];
+
+uint32_t pc;
+
+/* zversion stores the Z-machine version of the story: 1–6.
+ *
+ * Z-machine versions 7 and 8 are identical to version 5 but for a
+ * couple of tiny details.  They are thus classified as version 5.
+ *
+ * zwhich stores the actual version (1–8) for the few rare times where
+ * this knowledge is necessary.
+ */
+int zversion;
+static int zwhich;
+
+struct header header;
+
+static struct
+{
+  zterp_io *io;
+  long offset;
+} story;
+
+/* The null character in the alphabet table does not actually signify a
+ * null character: character 6 from A2 is special in that it specifies
+ * that the next two characters form a 10-bit ZSCII character (§3.4).
+ * The code that uses the alphabet table will step around this character
+ * when necessary, so it’s safe to use a null character here to mean
+ * “nothing”.
+ */
+uint8_t atable[26 * 3] =
+{
+  /* A0 */
+  'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+  'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+
+  /* A1 */
+  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+  'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+
+  /* A2 */
+  0x0, 0xd, '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.',
+  ',', '!', '?', '_', '#', '\'','"', '/', '\\','-', ':', '(', ')',
+};
+
+void znop(void)
+{
+}
+
+void zquit(void)
+{
+  break_from(0);
+}
+
+void zverify(void)
+{
+  uint16_t checksum = 0;
+  uint32_t remaining = header.file_length - 0x40;
+
+  if(zterp_io_seek(story.io, story.offset + 0x40, SEEK_SET) == -1)
+  {
+    branch_if(0);
+    return;
+  }
+
+  while(remaining != 0)
+  {
+    uint8_t buf[8192];
+    uint32_t to_read = remaining < sizeof buf ? remaining : sizeof buf;
+
+    if(zterp_io_read(story.io, buf, to_read) != to_read)
+    {
+      branch_if(0);
+      return;
+    }
+
+    for(uint32_t i = 0; i < to_read; i++) checksum += buf[i];
+
+    remaining -= to_read;
+  }
+
+  branch_if(checksum == header.checksum);
+}
+
+uint32_t unpack(uint16_t addr, int string)
+{
+  switch(zwhich)
+  {
+    case 1: case 2: case 3:
+      return addr * 2UL;
+    case 4: case 5:
+      return addr * 4UL;
+    case 6: case 7:
+      return (addr * 4UL) + (string ? header.S_O : header.R_O);
+    case 8:
+      return addr * 8UL;
+    default:
+      die("unhandled z-machine version: %d", zwhich);
+  }
+}
+
+void store(uint16_t v)
+{
+  store_variable(BYTE(pc++), v);
+}
+
+void zsave5(void)
+{
+  zterp_io *savefile;
+  size_t n;
+
+  if(znargs == 0)
+  {
+    zsave();
+    return;
+  }
+
+  /* This should be able to suggest a filename, but Glk doesn’t support that. */
+  savefile = zterp_io_open(NULL, ZTERP_IO_WRONLY | ZTERP_IO_SAVE);
+  if(savefile == NULL)
+  {
+    store(0);
+    return;
+  }
+
+  ZASSERT(zargs[0] + zargs[1] < memory_size, "attempt to save beyond the end of memory");
+  n = zterp_io_write(savefile, &memory[zargs[0]], zargs[1]);
+
+  zterp_io_close(savefile);
+
+  store(n == zargs[1]);
+}
+
+void zrestore5(void)
+{
+  zterp_io *savefile;
+  uint8_t *buf;
+  size_t n;
+
+  if(znargs == 0)
+  {
+    zrestore();
+    return;
+  }
+
+  savefile = zterp_io_open(NULL, ZTERP_IO_RDONLY | ZTERP_IO_SAVE);
+  if(savefile == NULL)
+  {
+    store(0);
+    return;
+  }
+
+  buf = malloc(zargs[1]);
+  if(buf == NULL)
+  {
+    store(0);
+    return;
+  }
+
+  n = zterp_io_read(savefile, buf, zargs[1]);
+  for(size_t i = 0; i < n; i++) user_store_byte(zargs[0] + i, buf[i]);
+
+  free(buf);
+
+  zterp_io_close(savefile);
+
+  store(n);
+}
+
+void zpiracy(void)
+{
+  branch_if(1);
+}
+
+void zsound_effect(void)
+{
+#ifdef GARGLK
+  uint8_t repeats, volume;
+  static uint32_t vols[8] = {
+    0x02000, 0x04000, 0x06000, 0x08000,
+    0x0a000, 0x0c000, 0x0e000, 0x10000
+  };
+
+  if(sound_channel == NULL || zargs[0] < 3) return;
+
+  repeats = zargs[2] >> 8;
+  volume = zargs[2] & 0xff;
+
+  if(volume == 0) volume = 1;
+  if(volume  > 8) volume = 8;
+
+  glk_schannel_set_volume(sound_channel, vols[volume - 1]);
+
+  switch(zargs[1])
+  {
+    case 1: /* prepare */
+      glk_sound_load_hint(zargs[0], 1);
+      break;
+    case 2: /* start */
+      glk_schannel_play_ext(sound_channel, zargs[0], repeats == 255 ? -1 : repeats, 0);
+      break;
+    case 3: /* stop */
+      glk_schannel_stop(sound_channel);
+      break;
+    case 4: /* finish with */
+      glk_sound_load_hint(zargs[0], 0);
+      break;
+  }
+#endif
+}
+
+/* Find a story ID roughly in the form of an IFID according to §2.2.2.1
+ * of draft 7 of the Treaty of Babel.
+ *
+ * This does not add a ZCODE- prefix, and will not search for a manually
+ * created IFID.
+ */
+static void find_id(void)
+{
+  char serial[] = "------";
+
+  for(int i = 0; i < 6; i++)
+  {
+    /* isalnum() cannot be used because it is locale-aware, and this
+     * must only check for A–Z, a–z, and 0–9.  Because ASCII (or a
+     * compatible charset) is required, testing against 'a', 'z', etc.
+     * is OK.
+     */
+#define ALNUM(c) ( ((c) >= 'a' && (c) <= 'z') || ((c) >= 'A' && (c) <= 'Z') || ((c) >= '0' && (c) <= '9') )
+    if(ALNUM(header.serial[i])) serial[i] = header.serial[i];
+#undef ALNUM
+  }
+
+  if(strchr("012345679", serial[0]) != NULL && strcmp(serial, "000000") != 0)
+  {
+    snprintf(story_id, sizeof story_id, "%d-%s-%04x", header.release, serial, (unsigned)header.checksum);
+  }
+  else
+  {
+    snprintf(story_id, sizeof story_id, "%d-%s", header.release, serial);
+  }
+}
+
+int is_story(const char *id)
+{
+  return strcmp(story_id, id) == 0;
+}
+
+#ifndef ZTERP_NO_CHEAT
+/* The index into these arrays is the address to freeze.
+ * The first array tracks whether the address is frozen, while the
+ * second holds the frozen value.
+ */
+static char     freezew_cheat[UINT16_MAX + 1];
+static uint16_t freezew_val  [UINT16_MAX + 1];
+
+static void cheat(char *how)
+{
+  char *p;
+
+  p = strtok(how, ":");
+  if(p == NULL) return;
+
+  if(strcmp(p, "freezew") == 0)
+  {
+    uint16_t addr;
+
+    p = strtok(NULL, ":");
+    if(p == NULL) return;
+
+    if(*p == 'G')
+    {
+      addr = strtoul(p + 1, NULL, 16);
+      if(addr > 239) return;
+
+      addr = header.globals + (addr * 2);
+    }
+    else
+    {
+      addr = strtoul(p, NULL, 16);
+    }
+
+    p = strtok(NULL, ":");
+    if(p == NULL) return;
+
+    freezew_cheat[addr] = 1;
+    freezew_val  [addr] = strtoul(p, NULL, 0);
+  }
+}
+
+int cheat_find_freezew(uint32_t addr, uint16_t *val)
+{
+  if(addr > UINT16_MAX || !freezew_cheat[addr]) return 0;
+
+  *val = freezew_val[addr];
+
+  return 1;
+}
+#endif
+
+static void read_config(void)
+{
+  FILE *fp;
+  char file[MAX_PATH + 1];
+  char line[MAX_LINE];
+  char *key, *val, *p;
+  long n;
+  int story_matches = 1;
+
+  zterp_os_rcfile(file, sizeof file);
+
+  fp = fopen(file, "r");
+  if(fp == NULL) return;
+
+  while(fgets(line, sizeof line, fp) != NULL)
+  {
+    line[strcspn(line, "#\n")] = 0;
+    if(line[0] == 0) continue;
+
+    if(line[0] == '[')
+    {
+      p = strrchr(line, ']');
+      if(p != NULL && p[1] == 0)
+      {
+        *p = 0;
+
+        story_matches = 0;
+        for(p = strtok(line + 1, " ,"); p != NULL; p = strtok(NULL, " ,"))
+        {
+          if(is_story(p)) story_matches = 1;
+        }
+      }
+
+      continue;
+    }
+
+    if(!story_matches) continue;
+
+    key = strtok(line, " \t=");
+    if(key == NULL) continue;
+    val = strtok(NULL, "=");
+    if(val == NULL) continue;
+
+    /* Trim whitespace. */
+    while(isspace((unsigned char)*val)) val++;
+    if(*val == 0) continue;
+    p = val + strlen(val) - 1;
+    while(isspace((unsigned char)*p)) *p-- = 0;
+
+    n = strtol(val, NULL, 10);
+
+#define BOOL(name)     else if(strcmp(key, #name) == 0) options.name = (n != 0)
+#define NUMBER(name)   else if(strcmp(key, #name) == 0) options.name = n
+#define STRING(name)   else if(strcmp(key, #name) == 0) do { free(options.name); options.name = xstrdup(val); } while(0)
+#define CHAR(name)     else if(strcmp(key, #name) == 0) options.name = val[0]
+#ifdef GARGLK
+#define COLOR(name, num)else if(strcmp(key, "color_" #name) == 0) update_color(num, strtol(val, NULL, 16))
+#else
+#define COLOR(name, num)else if(0)
+#endif
+
+    if(0);
+
+    NUMBER(eval_stack_size);
+    NUMBER(call_stack_size);
+    BOOL  (disable_color);
+    BOOL  (disable_timed);
+    BOOL  (disable_sound);
+    BOOL  (enable_escape);
+    STRING(escape_string);
+    BOOL  (disable_fixed);
+    BOOL  (assume_fixed);
+    BOOL  (disable_graphics_font);
+    BOOL  (enable_alt_graphics);
+    BOOL  (disable_term_keys);
+    BOOL  (disable_utf8);
+    BOOL  (force_utf8);
+    BOOL  (disable_meta_commands);
+    NUMBER(max_saves);
+    BOOL  (disable_undo_compression);
+    NUMBER(int_number);
+    CHAR  (int_version);
+    BOOL  (replay_on);
+    STRING(replay_name);
+    BOOL  (record_on);
+    STRING(record_name);
+    BOOL  (transcript_on);
+    STRING(transcript_name);
+    BOOL  (disable_abbreviations);
+    BOOL  (enable_censorship);
+    BOOL  (overwrite_transcript);
+    NUMBER(random_seed);
+    STRING(random_device);
+
+    COLOR(black,   2);
+    COLOR(red,     3);
+    COLOR(green,   4);
+    COLOR(yellow,  5);
+    COLOR(blue,    6);
+    COLOR(magenta, 7);
+    COLOR(cyan,    8);
+    COLOR(white,   9);
+
+#ifndef ZTERP_NO_CHEAT
+    else if(strcmp(key, "cheat") == 0) cheat(val);
+#endif
+
+#undef BOOL
+#undef NUMBER
+#undef STRING
+#undef CHAR
+#undef COLOR
+  }
+
+  fclose(fp);
+}
+
+static int have_statuswin = 0;
+static int have_upperwin  = 0;
+
+/* Various parts of the header (those marked “Rst” in §11) should be
+ * updated by the interpreter.  This function does that.  This is also
+ * used when restoring, because the save file might have come from an
+ * interpreter with vastly different settings.
+ */
+void write_header(void)
+{
+  uint8_t flags1;
+
+  flags1 = BYTE(0x01);
+
+  if(zversion == 3)
+  {
+    flags1 |= FLAGS1_NOSTATUS;
+    flags1 &= ~(FLAGS1_SCREENSPLIT | FLAGS1_VARIABLE);
+
+#ifdef GARGLK
+    /* Assume that if Gargoyle is being used, the default font is not fixed. */
+    flags1 |= FLAGS1_VARIABLE;
+#endif
+
+    if(have_statuswin)            flags1 &= ~FLAGS1_NOSTATUS;
+    if(have_upperwin)             flags1 |= FLAGS1_SCREENSPLIT;
+    if(options.enable_censorship) flags1 |= FLAGS1_CENSOR;
+  }
+  else if(zversion >= 4)
+  {
+    flags1 |= (FLAGS1_BOLD | FLAGS1_ITALIC | FLAGS1_FIXED);
+    flags1 &= ~FLAGS1_TIMED;
+
+    if(zversion >= 5) flags1 &= ~FLAGS1_COLORS;
+
+    if(zversion == 6)
+    {
+      flags1 &= ~(FLAGS1_PICTURES | FLAGS1_SOUND);
+#ifdef GARGLK
+      if(sound_channel != NULL) flags1 |= FLAGS1_SOUND;
+#endif
+    }
+
+#ifdef ZTERP_GLK
+    if(glk_gestalt(gestalt_Timer, 0)) flags1 |= FLAGS1_TIMED;
+#ifdef GARGLK
+    if(zversion >= 5) flags1 |= FLAGS1_COLORS;
+#endif
+#else
+    if(!zterp_os_have_style(STYLE_BOLD)) flags1 &= ~FLAGS1_BOLD;
+    if(!zterp_os_have_style(STYLE_ITALIC)) flags1 &= ~FLAGS1_ITALIC;
+    if(zversion >= 5 && zterp_os_have_colors()) flags1 |= FLAGS1_COLORS;
+#endif
+
+    if(zversion >= 5 && options.disable_color) flags1 &= ~FLAGS1_COLORS;
+    if(options.disable_timed) flags1 &= ~FLAGS1_TIMED;
+    if(options.disable_fixed) flags1 &= ~FLAGS1_FIXED;
+  }
+
+  STORE_BYTE(0x01, flags1);
+
+  if(zversion >= 5)
+  {
+    uint16_t flags2 = WORD(0x10);
+
+    flags2 &= ~FLAGS2_MOUSE;
+#ifdef GARGLK
+    if(sound_channel == NULL) flags2 &= ~FLAGS2_SOUND;
+#else
+    flags2 &= ~FLAGS2_SOUND;
+#endif
+    if(zversion >= 6) flags2 &= ~FLAGS2_MENUS;
+
+    if(options.disable_graphics_font) flags2 &= ~FLAGS2_PICTURES;
+
+    if(options.max_saves == 0) flags2 &= ~FLAGS2_UNDO;
+
+    STORE_WORD(0x10, flags2);
+  }
+
+  if(zversion >= 4)
+  {
+    unsigned int width, height;
+
+    /* Interpreter number & version. */
+    if(options.int_number < 1 || options.int_number > 11) options.int_number = 1; /* DEC */
+    STORE_BYTE(0x1e, options.int_number);
+    STORE_BYTE(0x1f, options.int_version);
+
+    get_screen_size(&width, &height);
+
+    /* Screen height and width.
+     * A height of 255 means infinite, so cap at 254.
+     */
+    STORE_BYTE(0x20, height > 254 ? 254 : height);
+    STORE_BYTE(0x21, width > 255 ? 255 : width);
+
+    if(zversion >= 5)
+    {
+      /* Screen width and height in units. */
+      STORE_WORD(0x22, width > UINT16_MAX ? UINT16_MAX : width);
+      STORE_WORD(0x24, height > UINT16_MAX ? UINT16_MAX : height);
+
+      /* Font width and height in units. */
+      STORE_BYTE(0x26, 1);
+      STORE_BYTE(0x27, 1);
+
+      /* Default background and foreground colors. */
+      STORE_BYTE(0x2c, 1);
+      STORE_BYTE(0x2d, 1);
+    }
+  }
+
+  /* Standard revision # */
+  STORE_BYTE(0x32, 1);
+  STORE_BYTE(0x33, 1);
+}
+
+void process_story(void)
+{
+  if(zterp_io_seek(story.io, story.offset, SEEK_SET) == -1) die("unable to rewind story");
+
+  if(zterp_io_read(story.io, memory, memory_size) != memory_size) die("unable to read from story file");
+
+  zversion =           BYTE(0x00);
+  if(zversion < 1 || zversion > 8) die("only z-code versions 1-8 are supported");
+
+  zwhich = zversion;
+  if(zversion == 7 || zversion == 8) zversion = 5;
+
+  pc =                 WORD(0x06);
+  if(pc >= memory_size) die("corrupted story: initial pc out of range");
+
+  header.release =     WORD(0x02);
+  header.dictionary =  WORD(0x08);
+  header.objects =     WORD(0x0a);
+  header.globals =     WORD(0x0c);
+  header.static_start =        WORD(0x0e);
+  header.abbr =                WORD(0x18);
+
+  memcpy(header.serial, &memory[0x12], sizeof header.serial);
+
+  /* There is no explicit “end of static” tag; but it must end by 0xffff
+   * or the end of the story file, whichever is smaller.
+   */
+  header.static_end = memory_size < 0xffff ? memory_size : 0xffff;
+
+#define PROPSIZE       (zversion <= 3 ? 62L : 126L)
+
+  /* There must be at least enough room in dynamic memory for the header
+   * (64 bytes), the global variables table (480 bytes), and the
+   * property defaults table (62 or 126 bytes).
+   */
+  if(header.static_start < 64 + 480 + PROPSIZE)   die("corrupted story: dynamic memory too small (%d bytes)", (int)header.static_start);
+  if(header.static_start >= memory_size)          die("corrupted story: static memory out of range");
+
+  if(header.dictionary != 0 &&
+     header.dictionary < header.static_start)     die("corrupted story: dictionary is not in static memory");
+
+  if(header.objects < 64 ||
+     header.objects + PROPSIZE > header.static_start)
+                                                  die("corrupted story: object table is not in dynamic memory");
+
+#undef PROPSIZE
+
+  if(header.globals < 64 ||
+     header.globals + 480L > header.static_start) die("corrupted story: global variables are not in dynamic memory");
+
+  if(header.abbr >= memory_size)                  die("corrupted story: abbreviation table out of range");
+
+  header.file_length = WORD(0x1a) * (zwhich <= 3 ? 2UL : zwhich <= 5 ? 4UL : 8UL);
+  if(header.file_length > memory_size)            die("story's reported size (%lu) greater than file size (%lu)", (unsigned long)header.file_length, (unsigned long)memory_size);
+
+  header.checksum = WORD(0x1c);
+
+  if(zwhich == 6 || zwhich == 7)
+  {
+    header.R_O = WORD(0x28) * 8UL;
+    header.S_O = WORD(0x2a) * 8UL;
+  }
+
+  if(dynamic_memory == NULL)
+  {
+    dynamic_memory = malloc(header.static_start);
+    if(dynamic_memory == NULL) die("unable to allocate memory for dynamic memory");
+    memcpy(dynamic_memory, memory, header.static_start);
+  }
+
+#ifdef GLK_MODULE_LINE_TERMINATORS
+  if(!options.disable_term_keys)
+  {
+    if(zversion >= 5 && WORD(0x2e) != 0)
+    {
+      term_keys_reset();
+
+      for(uint32_t i = WORD(0x2e); i < memory_size && memory[i] != 0; i++)
+      {
+        term_keys_add(memory[i]);
+      }
+    }
+  }
+#endif
+
+  if(zversion == 1)
+  {
+    memcpy(&atable[26 * 2], " 0123456789.,!?_#'\"/\\<-:()", 26);
+  }
+  else if(zversion >= 5 && WORD(0x34) != 0)
+  {
+    if(WORD(0x34) + 26 * 3 >= memory_size) die("corrupted story: alphabet table out of range");
+
+    memcpy(atable, &memory[WORD(0x34)], 26 * 3);
+
+    /* Even with a new alphabet table, characters 6 and 7 from A2 must
+     * remain the same (§3.5.5.1).
+     */
+    atable[52] = 0x00;
+    atable[53] = 0x0d;
+  }
+
+  /* Check for a header extension table. */
+  if(zversion >= 5)
+  {
+    uint16_t etable = WORD(0x36);
+
+    if(etable != 0)
+    {
+      uint16_t nentries = user_word(etable);
+
+      if(etable + (2 * nentries) >= memory_size) die("corrupted story: header extension table out of range");
+
+      /* Unicode table. */
+      if(nentries >= 3 && WORD(etable + (2 * 3)) != 0)
+      {
+        uint16_t utable = WORD(etable + (2 * 3));
+
+        parse_unicode_table(utable);
+      }
+
+      /* Flags3. */
+      if(nentries >= 4) STORE_WORD(etable + (2 * 4), 0);
+      /* True default foreground color. */
+      if(nentries >= 5) STORE_WORD(etable + (2 * 5), 0x0000);
+      /* True default background color. */
+      if(nentries >= 6) STORE_WORD(etable + (2 * 6), 0x7fff);
+    }
+  }
+
+  /* The configuration file cannot be read until the ID of the current
+   * story is known, and the ID of the current story is not known until
+   * the file has been processed; so do both of those here.
+   */
+  find_id();
+  if(!options.disable_config) read_config();
+
+  /* Prevent the configuration file from unexpectedly being reread after
+   * @restart or @restore.
+   */
+  options.disable_config = 1;
+
+  /* Most options directly set their respective variables, but a few
+   * require intervention.  Delay that intervention until here so that
+   * the configuration file is taken into account.
+   */
+  if(options.disable_utf8)
+  {
+#ifndef ZTERP_GLK
+    /* If Glk is not being used, the ZSCII to Unicode table needs to be
+     * aligned with the IO character set.
+     */
+    have_unicode = 0;
+#endif
+    use_utf8_io = 0;
+  }
+  if(options.force_utf8)
+  {
+#ifndef ZTERP_GLK
+    /* See above. */
+    have_unicode = 1;
+#endif
+    use_utf8_io = 1;
+  }
+  if(options.escape_string == NULL) options.escape_string = xstrdup("1m");
+
+#ifdef GARGLK
+  if(options.disable_sound && sound_channel != NULL)
+  {
+    glk_schannel_destroy(sound_channel);
+    sound_channel = NULL;
+  }
+#endif
+
+  /* Now that we have a Unicode table and the user’s Unicode
+   * preferences, build the ZSCII to Unicode and Unicode to ZSCII
+   * tables.
+   */
+  setup_tables();
+
+  if(zversion <= 3) have_statuswin = create_statuswin();
+  if(zversion >= 3) have_upperwin  = create_upperwin();
+
+  write_header();
+  /* Put everything in a clean state. */
+  seed_random(0);
+  init_stack();
+  init_screen();
+
+  /* Unfortunately, Beyond Zork behaves badly when the interpreter
+   * number is set to DOS: it assumes that it can print out IBM PC
+   * character codes and get useful results (e.g. it writes out 0x18
+   * expecting an up arrow); however, if the pictures bit is set, it
+   * uses the character graphics font like a good citizen.  Thus turn
+   * that bit on when Beyond Zork is being used and the interpreter is
+   * set to DOS.  It might make sense to do this generally, not just for
+   * Beyond Zork; but this is such a minor corner of the Z-machine that
+   * it probably doesn’t matter.  For now, peg this to Beyond Zork.
+   */
+  if(options.int_number == 6 &&
+      (is_story("47-870915") || is_story("49-870917") ||
+       is_story("51-870923") || is_story("57-871221")))
+  {
+    STORE_WORD(0x10, WORD(0x10) | FLAGS2_PICTURES);
+  }
+
+  if(options.transcript_on)
+  {
+    STORE_WORD(0x10, WORD(0x10) | FLAGS2_TRANSCRIPT);
+    options.transcript_on = 0;
+  }
+
+  if(options.record_on)
+  {
+    output_stream(OSTREAM_RECORD, 0);
+    options.record_on = 0;
+  }
+
+  if(options.replay_on)
+  {
+    input_stream(ISTREAM_FILE);
+    options.replay_on = 0;
+  }
+
+  if(zversion == 6)
+  {
+    zargs[0] = pc;
+    call(0);
+  }
+}
+
+#ifdef ZTERP_GLK
+zexternally_visible
+void glk_main(void)
+#else
+int main(int argc, char **argv)
+#endif
+{
+  zterp_blorb *blorb;
+
+#ifdef ZTERP_GLK
+  if(!create_mainwin()) return;
+#ifdef GLK_MODULE_UNICODE
+  have_unicode = glk_gestalt(gestalt_Unicode, 0);
+#endif
+#else
+  have_unicode = zterp_os_have_unicode();
+#endif
+
+  use_utf8_io = zterp_os_have_unicode();
+
+#ifndef ZTERP_GLK
+  if(!process_arguments(argc, argv)) exit(EXIT_FAILURE);
+
+  zterp_os_init_term();
+#endif
+
+#ifdef ZTERP_GLK
+#define PRINT(s)       do { glk_put_string(s); glk_put_char(UNICODE_LINEFEED); } while(0)
+#else
+#define PRINT(s)       puts(s)
+#endif
+
+  if(options.show_version)
+  {
+    char config[MAX_PATH] = "Configuration file: ";
+
+    PRINT("Bocfel " ZTERP_VERSION);
+#ifdef ZTERP_NO_SAFETY_CHECKS
+    PRINT("Runtime assertions disabled");
+#else
+    PRINT("Runtime assertions enabled");
+#endif
+#ifdef ZTERP_NO_CHEAT
+    PRINT("Cheat support disabled");
+#else
+    PRINT("Cheat support enabled");
+#endif
+#ifdef ZTERP_TANDY
+    PRINT("The Tandy bit can be set");
+#else
+    PRINT("The Tandy bit cannot be set");
+#endif
+
+    zterp_os_rcfile(config + strlen(config), sizeof config - strlen(config));
+    PRINT(config);
+
+#ifdef ZTERP_GLK
+    glk_exit();
+#else
+    exit(0);
+#endif
+  }
+
+#undef PRINT
+
+#ifdef GARGLK
+  if(game_file == NULL)
+  {
+    frefid_t ref;
+
+    ref = glk_fileref_create_by_prompt(fileusage_Data | fileusage_BinaryMode, filemode_Read, 0);
+    if(ref != NULL)
+    {
+      game_file = xstrdup(garglk_fileref_get_name(ref));
+      glk_fileref_destroy(ref);
+    }
+  }
+#endif
+
+  if(game_file == NULL) die("no story provided");
+
+  story.io = zterp_io_open(game_file, ZTERP_IO_RDONLY);
+  if(story.io == NULL) die("cannot open file %s", game_file);
+
+  blorb = zterp_blorb_parse(story.io);
+  if(blorb != NULL)
+  {
+    const zterp_blorb_chunk *chunk;
+
+    chunk = zterp_blorb_find(blorb, BLORB_EXEC, 0);
+    if(chunk == NULL) die("no EXEC resource found");
+    if(strcmp(chunk->name, "ZCOD") != 0)
+    {
+      if(strcmp(chunk->name, "GLUL") == 0) die("Glulx stories are not supported (try git or glulxe)");
+
+      die("unknown story type: %s", chunk->name);
+    }
+
+    if(chunk->offset > LONG_MAX) die("zcode offset too large");
+
+    memory_size = chunk->size;
+    story.offset = chunk->offset;
+
+    zterp_blorb_free(blorb);
+  }
+  else
+  {
+    long size = zterp_io_filesize(story.io);
+
+    if(size == -1) die("unable to determine file size");
+    if(size > UINT32_MAX) die("file too large");
+
+    memory_size = size;
+    story.offset = 0;
+  }
+
+#ifdef GARGLK
+  if(glk_gestalt(gestalt_Sound, 0))
+  {
+    /* 5 for the worst case of needing to add .blb to the end plus the
+     * null character.
+     */
+    char *blorb_file = malloc(strlen(game_file) + 5);
+    if(blorb_file != NULL)
+    {
+      char *p;
+      strid_t file;
+
+      strcpy(blorb_file, game_file);
+      p = strrchr(blorb_file, '.');
+      if(p != NULL) *p = 0;
+      strcat(blorb_file, ".blb");
+
+      file = glkunix_stream_open_pathname(blorb_file, 0, 0);
+      if(file != NULL)
+      {
+        giblorb_set_resource_map(file);
+        sound_channel = glk_schannel_create(0);
+      }
+
+      free(blorb_file);
+    }
+  }
+#endif
+
+  if(memory_size < 64) die("story file too small");
+  if(memory_size > SIZE_MAX - 22) die("story file too large");
+
+  /* It’s possible for a story to be cut short in the middle of an
+   * instruction.  If so, the processing loop will run past the end of
+   * memory.  Either pc needs to be checked each and every time it is
+   * incremented, or a small guard needs to be placed at the end of
+   * memory that will trigger an illegal instruction error.  The latter
+   * is done by filling the end of memory with zeroes, which do not
+   * represent a valid instruction.
+   *
+   * There need to be at least 22 bytes for the worst case: 0xec
+   * (call_vs2) as the last byte in memory.  The next two bytes, which
+   * will be zeroes, indicate that 8 large constants, or 16 bytes, will
+   * be next.  This is a store instruction, so one more byte will be
+   * read to determine where to store.  Another byte is read to
+   * determine the next opcode; this will be zero, which is nominally a
+   * 2OP, requiring two more bytes to be read.  At this point the opcode
+   * will be looked up, resulting in an illegal instruction error.
+   */
+  memory = malloc(memory_size + 22);
+  if(memory == NULL) die("unable to allocate memory for story file");
+  memset(memory + memory_size, 0, 22);
+
+  process_story();
+
+  /* If header transcript/fixed bits have been set, either by the
+   * story or by the user, this will activate them.
+   */
+  user_store_word(0x10, WORD(0x10));
+
+  if(options.show_id)
+  {
+#ifdef ZTERP_GLK
+    glk_put_string(story_id);
+    glk_exit();
+#else
+    puts(story_id);
+    exit(0);
+#endif
+  }
+
+  setup_opcodes();
+
+  process_instructions();
+
+#ifndef ZTERP_GLK
+  return 0;
+#endif
+}
diff --git a/interpreters/bocfel/zterp.h b/interpreters/bocfel/zterp.h
new file mode 100644 (file)
index 0000000..2280ef0
--- /dev/null
@@ -0,0 +1,117 @@
+#ifndef ZTERP_H
+#define ZTERP_H
+
+#include <stdint.h>
+
+struct options
+{
+  long eval_stack_size;
+  long call_stack_size;
+  int disable_color;
+  int disable_config;
+  int disable_timed;
+  int disable_sound;
+  int enable_escape;
+  char *escape_string;
+  int disable_fixed;
+  int assume_fixed;
+  int disable_graphics_font;
+  int enable_alt_graphics;
+  int show_id;
+  int disable_term_keys;
+  int disable_utf8;
+  int force_utf8;
+  int disable_meta_commands;
+  long int_number;
+  int int_version;
+  int replay_on;
+  char *replay_name;
+  int record_on;
+  char *record_name;
+  int transcript_on;
+  char *transcript_name;
+  long max_saves;
+  int disable_undo_compression;
+  int show_version;
+  int disable_abbreviations;
+  int enable_censorship;
+  int overwrite_transcript;
+  long random_seed;
+  char *random_device;
+};
+
+extern const char *game_file;
+extern struct options options;
+
+/* v3 */
+#define FLAGS1_STATUSTYPE      (1U << 1)
+#define FLAGS1_STORYSPLIT      (1U << 2)
+#define FLAGS1_CENSOR          (1U << 3)
+#define FLAGS1_NOSTATUS                (1U << 4)
+#define FLAGS1_SCREENSPLIT     (1U << 5)
+#define FLAGS1_VARIABLE                (1U << 6)
+
+/* v4 */
+#define FLAGS1_COLORS          (1U << 0)
+#define FLAGS1_PICTURES                (1U << 1)
+#define FLAGS1_BOLD            (1U << 2)
+#define FLAGS1_ITALIC          (1U << 3)
+#define FLAGS1_FIXED           (1U << 4)
+#define FLAGS1_SOUND           (1U << 5)
+#define FLAGS1_TIMED           (1U << 7)
+
+#define FLAGS2_TRANSCRIPT      (1U << 0)
+#define FLAGS2_FIXED           (1U << 1)
+#define FLAGS2_STATUS          (1U << 2)
+#define FLAGS2_PICTURES                (1U << 3)
+#define FLAGS2_UNDO            (1U << 4)
+#define FLAGS2_MOUSE           (1U << 5)
+#define FLAGS2_COLORS          (1U << 6)
+#define FLAGS2_SOUND           (1U << 7)
+#define FLAGS2_MENUS           (1U << 8)
+
+#define STATUS_IS_TIME()       (zversion == 3 && (BYTE(0x01) & FLAGS1_STATUSTYPE))
+#define TIMER_AVAILABLE()      (zversion >= 4 && (BYTE(0x01) & FLAGS1_TIMED))
+
+struct header
+{
+  uint16_t release;
+  uint16_t dictionary;
+  uint16_t objects;
+  uint16_t globals;
+  uint16_t static_start;
+  uint16_t static_end;
+  uint16_t abbr;
+  uint32_t file_length;
+  uint8_t  serial[6];
+  uint16_t checksum;
+  uint32_t R_O;
+  uint32_t S_O;
+};
+
+extern uint32_t pc;
+extern int zversion;
+extern struct header header;
+extern uint8_t atable[];
+
+int is_story(const char *);
+
+void write_header(void);
+
+uint32_t unpack(uint16_t, int);
+void store(uint16_t);
+void process_story(void);
+
+#ifndef ZTERP_NO_CHEAT
+int cheat_find_freezew(uint32_t, uint16_t *);
+#endif
+
+void znop(void);
+void zquit(void);
+void zverify(void);
+void zpiracy(void);
+void zsave5(void);
+void zrestore5(void);
+void zsound_effect(void);
+
+#endif
index 64c32e30211b8dd1ba0ce7dee6dfec7bd9ae0d5c..0554d93fc3530a6ea190034e972de3cdafcbfbf3 100644 (file)
  */
 
 static gboolean supported_formats[CHIMARA_IF_NUM_FORMATS][CHIMARA_IF_NUM_INTERPRETERS] = {
  */
 
 static gboolean supported_formats[CHIMARA_IF_NUM_FORMATS][CHIMARA_IF_NUM_INTERPRETERS] = {
-       /* Frotz Nitfol Glulxe Git */
-       { TRUE,  TRUE,  FALSE, FALSE }, /* Z5 */
-       { TRUE,  TRUE,  FALSE, FALSE }, /* Z6 */
-       { TRUE,  TRUE,  FALSE, FALSE }, /* Z8 */
-       { TRUE,  TRUE,  FALSE, FALSE }, /* Zblorb */
-       { FALSE, FALSE, TRUE,  TRUE  }, /* Glulx */
-       { FALSE, FALSE, TRUE,  TRUE  }  /* Gblorb */
+       /* Frotz Nitfol Glulxe Git    Bocfel */
+       { TRUE,  TRUE,  FALSE, FALSE, TRUE  }, /* Z5 */
+       { TRUE,  TRUE,  FALSE, FALSE, TRUE  }, /* Z6 */
+       { TRUE,  TRUE,  FALSE, FALSE, TRUE  }, /* Z8 */
+       { TRUE,  TRUE,  FALSE, FALSE, TRUE  }, /* Zblorb */
+       { FALSE, FALSE, TRUE,  TRUE,  FALSE }, /* Glulx */
+       { FALSE, FALSE, TRUE,  TRUE,  FALSE }  /* Gblorb */
 };
 static gchar *format_names[CHIMARA_IF_NUM_FORMATS] = {
        N_("Z-code version 5"),
 };
 static gchar *format_names[CHIMARA_IF_NUM_FORMATS] = {
        N_("Z-code version 5"),
@@ -48,10 +48,10 @@ static gchar *format_names[CHIMARA_IF_NUM_FORMATS] = {
        N_("Blorbed Glulx")
 };
 static gchar *interpreter_names[CHIMARA_IF_NUM_INTERPRETERS] = {
        N_("Blorbed Glulx")
 };
 static gchar *interpreter_names[CHIMARA_IF_NUM_INTERPRETERS] = {
-       N_("Frotz"), N_("Nitfol"), N_("Glulxe"), N_("Git")
+       N_("Frotz"), N_("Nitfol"), N_("Glulxe"), N_("Git"), N_("Bocfel")
 };
 static gchar *plugin_names[CHIMARA_IF_NUM_INTERPRETERS] = {
 };
 static gchar *plugin_names[CHIMARA_IF_NUM_INTERPRETERS] = {
-       "frotz", "nitfol", "glulxe", "git"
+       "frotz", "nitfol", "glulxe", "git", "bocfel"
 };
 
 typedef enum _ChimaraIFFlags {
 };
 
 typedef enum _ChimaraIFFlags {
index 72d64610346542555c9b68407b1d04f39cb833ea..f8ec757dfd3948676bd15db0d2e84959698f794d 100644 (file)
@@ -44,6 +44,7 @@ typedef enum {
  * @CHIMARA_IF_INTERPRETER_NITFOL: Nitfol
  * @CHIMARA_IF_INTERPRETER_GLULXE: Glulxe
  * @CHIMARA_IF_INTERPRETER_GIT: Git
  * @CHIMARA_IF_INTERPRETER_NITFOL: Nitfol
  * @CHIMARA_IF_INTERPRETER_GLULXE: Glulxe
  * @CHIMARA_IF_INTERPRETER_GIT: Git
+ * @CHIMARA_IF_INTERPRETER_BOCFEL: Bocfel
  * 
  * Constants representing the available interpreter plugins.
  */
  * 
  * Constants representing the available interpreter plugins.
  */
@@ -55,6 +56,7 @@ typedef enum {
        CHIMARA_IF_INTERPRETER_NITFOL,
        CHIMARA_IF_INTERPRETER_GLULXE,
        CHIMARA_IF_INTERPRETER_GIT,
        CHIMARA_IF_INTERPRETER_NITFOL,
        CHIMARA_IF_INTERPRETER_GLULXE,
        CHIMARA_IF_INTERPRETER_GIT,
+       CHIMARA_IF_INTERPRETER_BOCFEL,
        /*< private >*/
        CHIMARA_IF_NUM_INTERPRETERS
 } ChimaraIFInterpreter;
        /*< private >*/
        CHIMARA_IF_NUM_INTERPRETERS
 } ChimaraIFInterpreter;
index 1f39927082e077073dc8b0aa978a3434521e4595..a9f5f43b661634c56fc116e67c20c04ac9908d9b 100644 (file)
@@ -104,11 +104,13 @@ parse_interpreter(const char *interp)
                return CHIMARA_IF_INTERPRETER_GLULXE;
        if(strcmp(interp, "git") == 0)
                return CHIMARA_IF_INTERPRETER_GIT;
                return CHIMARA_IF_INTERPRETER_GLULXE;
        if(strcmp(interp, "git") == 0)
                return CHIMARA_IF_INTERPRETER_GIT;
+       if(strcmp(interp, "bocfel") == 0)
+               return CHIMARA_IF_INTERPRETER_BOCFEL;
        return CHIMARA_IF_INTERPRETER_NONE;
 }
 
 static const char *interpreter_strings[CHIMARA_IF_NUM_INTERPRETERS] = {
        return CHIMARA_IF_INTERPRETER_NONE;
 }
 
 static const char *interpreter_strings[CHIMARA_IF_NUM_INTERPRETERS] = {
-       "frotz", "nitfol", "glulxe", "git"
+       "frotz", "nitfol", "glulxe", "git", "bocfel"
 };
 
 static const char *
 };
 
 static const char *
@@ -123,7 +125,8 @@ static const char *interpreter_display_strings[CHIMARA_IF_NUM_INTERPRETERS] = {
        N_("Frotz"),
        N_("Nitfol"),
        N_("Glulxe"),
        N_("Frotz"),
        N_("Nitfol"),
        N_("Glulxe"),
-       N_("Git")
+       N_("Git"),
+       N_("Bocfel")
 };
 
 static const char *
 };
 
 static const char *