ZXGate Visual Novel Engine for ZX Spectrum

With "Steins;Gate Space Octet" port


I don't own the Steins;Gate Space Octet game script. It is copyright to the original authors and right holders. If you do not own a copy of this game, it is illegal for you to download and play this port. The port is provided only as an example of usage of the engine. It is fine for you to use the ZXGate engine itself, though, as it is completely original content.

System requirements

  • ZX Spectrum 128K
  • TR-DOS 5.01 or later with Beta Disk Interface
  • AY-3-8910 or YM2149F sound generator
  • Color monitor
  • 640K Floppy Drive

How to play

  1. Load HENIKUUKAN.trd into A: drive
  2. Enter TR-DOS from menu or using the command RANDOMIZE USR 15616 in BASIC
  3. If game won't autostart, input RUN at the A> prompt and hit Enter

How to use ZXGate Engine

  1. Place any necessary binary files into bin folder (e.g. the Vortex Tracker music player resides there as VT.C).
  2. Place your graphics in scr format into graphics folder.
  3. Place your compiled Vortex Tracker music files into music folder.
  4. Write the script and place it into script folder.
  5. Write the system script and save it as system_script.txt.
  6. Write the main script and save it as main_script.txt.
  7. Edit main.asm, line STRT_SCPT db "SGBOOT C",#00 to match your main menu script file name (the engine loads it after a Game Over).
  8. Run to build the game.
  9. The resulting disk images will be in out/disk.


The system script is a script containing commands available inside the whole game. It is embedded into the engine during build and is not overwritten during the game process. It's a good idea to place i.e. Save and Load commands here.

The main script is a script embedded into the engine during build, that is executed when the engine is started. It's a good place for explanation on how to play your game, and a directive to load the first file of your novel. It will be replaced in memory once the next script is loaded and there is no way to go back to it afterwards.

Text script representation

The text scripts are compiled into binary during build time using If you need to eliminate the EOF mark at the end of script (e.g. compiling a System Script — having a EOF mark in it renders the game unplayable!), use the --no-eof flag. Then specify all your text files as one command line, otherwise the flag names won't be matched correctly and the game won't work. Example: ./ myscene1.txt myscene2a.txt myscene2b.txt myscene3.txt.

You can write any text inside a text script. That text will be displayed line-by-line to the player while they are playing your game. However there are some special cases.

If a line begins with ;, then it is considered a comment. It will not be included in the game, so this is handy to quickly explain about what is happening in that place of the script.

If a line begins with @, it is considered a command. The following commands are supported: (all file name arguments must be supplied without extension)

  • @stop — stops the background music from playing
  • @draw file=filename, @redraw file=filename — draws a picture on the screen
  • @clear — clears the screen
  • @wait time=N — wait N seconds. If N > 1000, considered to be in milliseconds.
  • @play file=filename — play music loaded from the specified file
  • @goto file=filename — load the script from the specified file name and start executing it
  • @gameover — game over, return to the starting script
  • @ifflag name="my flag" value="2" — condition to check a flag's value. There can't be more than 245 different flags, and any flag's value cannot exceed 255. If you need to check multiple at once, combine them like that: @ifflag name="first_flag" value="0" name="another_flag" value="2". Should the flags match the specified values, the script continues. Otherwise the script will continue from the matching @else or @endif command.
  • @iftemp name="my temp flag" — same as @ifflag, but checks for a temp flag. Temp flags don't have a value, they can be only set or reset.
  • @ifsys name="my sys flag" — same as @iftemp, but checks for a systemwide flag.
  • @flag name="my flag" value="10" — sets a value for a flag.
  • @tf name="my temp flag" — sets a temp flag.
  • @sf name="my sys flag" — sets a systemwide flag.
  • @load file=filename — loads game progress from a save file with the specified file name
  • @save file=filename — saves game progress into a save file with the specified file name
  • @input — gives the user a command line to input a command.

If the line starts with a *, it is considered a user-command. This means, when the user is given input via @input, if they enter that line, the script will continue executing right below it.


Consider the following script:

@play file=whatislove


    You lost it.

@ifflag name="whatislove" value="0"
    Baby don't hurt me.
    @flag name="whatislove" value="1"
    @ifflag name="whatislove" value="1"
        Don't hurt me.
        @flag name="whatislove" value="2"
        No more.

When executed, it will start playing music from file named "WHATISLOVE.C" on the floppy, and give user a command prompt.

If the user types "help", the line "You lost it." will be displayed and the game is considered over.

However, if the user types "whatislove", the text "Baby don't hurt me." is presented, and then, again, a command prompt.

If the user types "whatislove" again, the first @ifflag won't match, so execution continues from the first @else. The second @ifflag will match, so the line "Don't hurt me" is presented.

If the user types "whatislove" once more, then neither of the @ifflag's match, and the line "No more." is presented, after which, the game is over.

Inline ASM-scripts

Since recently, you can also write scripts inline in the assembly code, using the definitions in engine/commandset.asm

The above example would look like so in an inline ASM representation:


    defb SCMD_BGM,"WHATISLOV",0 ; mind the TRDOS file name length!
    defb SCMD_INPUT

        defb "help",0
    defb SCMD_PTREND
        defb "You lost it.",0
        defb SCMD_GAMEOVER

        defb "whatislove",0
    defb SCMD_PTREND
            defb "Baby don't hurt me.",0
        defb SCMD_ELSE
            defb SCMD_GETFLAG,FLAG_WHATISLOVE,#01,#00
                defb "Don't hurt me.",0
                defb SCMD_SETFLAG,FLAG_WHATISLOVE,#02
            defb SCMD_ELSE
                defb "No more.",0
                defb SCMD_GAMEOVER
            defb SCMD_ENDIF
        defb SCMD_ENDIF
    defb SCMD_INPUT

So, as you can see, it's a very monstrous construction, and is good mostly for writing small scripts only.

Binary script representation

For reference only! Always refer to main.asm comments and engine/commandset.asm code. Or better yet, write text-scripts and build them into compiled binary scripts using, or write inline ASM-scripts :-)

; ------------ Conditionals
; D1 aa bb aa ... 00        Check GAMEFLAGS (aa is offset, bb is value, logic AND: read as "if aa is bb AND aa2 is bb2 AND ...")
; D0 aa bb                  Set GAMEFLAGS (aa is offset, bb is value)
; D2 aa                     Set TEMPFLAGS (logic OR, every bit is one flag)
; D3 aa                     Check TEMPFLAGS (logic AND)
; D4 aa                     Set SYSFLAG (logic OR, every bit is one flag)
; D5 aa                     Check SYSFLAG
; F2                        Else (if Check not matched, script continues from there)
; F3                        End Check

; ------------ Multi-media
; E0 xx xx xx ... 00        Load Script File
; E1 xx xx xx ... 00        Load Music File
; E2 xx xx xx ... 00        Load Image File
; E3 xx xx xx ... 00        Reserved
; F4                        Stop Music

; ------------ Gameplay
; F0 xx xx xx ... 00        Save to file
; F1 xx xx xx ... 00        Load from file
; E4                        Game Over
; F5 xx xx xx ... 00        Reserved
; E5 xx                     Wait xx seconds
; F6                        Clear screen
; F7                        EOF (used for pointer parsing)
; FF                        User Input Pointer
; FD xx xx xx ... 00
;    xx xx xx ... 00
;    xx xx xx ... 00 FE     Pointer List
; xx xx xx xx ... 00        Print string

Here is how the above example would look like when compiled into binary (markup added for easier reading):

    E1 57 48 41 54 49 53 4C 4F 43 00

    68 65 6C 70 00 
    59 6F 75 20 6C 6F 73 74 20 69 74 2E 00

    77 68 61 74 69 73 6C 6F 76 65 00

    D1 01 00 00
        42 61 62 79 20 64 6F 6E 27 74 20 68 75 72 74 20 6D 65 2E 00 
        D0 01 01 
        D1 01 01 00
            44 6F 6E 27 74 20 68 75 72 74 20 6D 65 2E 00
            D0 01 02 
            4E 6F 20 6D 6F 72 65 2E 00


State of the game

  • Bad endings work
  • Good ending works
  • Save/load works
  • Easter eggs work
  • look badge doesn't work

State of the engine

Builds and runs as long as the build environment matches:

  • FISH shell
  • zx7 compressor
  • mctrd
  • SjASMPlus v1.07 RC8
  • Python 2.7
  • Linux or OSX

Technical details

The engine uses standard TR-DOS procedures to load and write files, and the lower screen ROM procedure to output the text. So in theory it's possible to get 4 Out of Screen when displaying a long text or typing a long command, but it isn't the engine's problem :-)

The music player and music files are loaded in the Page 0 starting #C000. The music is played using an interrupt service routine (to make code much easier).

There are custom characters defined for text output while the engine is running:

  • α — binary #90, text #83#BF
  • β — binary #91, text #83#C0
  • Enter key symbol — binary #92
  • Floppy disk — binary #93
  • ♫ — binary #94, text #81#F4
  • Book — binary #95
  • Picture frame — binary #96
  • Joystick — binary #97
  • Clock — binary #98
  • ★ — binary #99, text #81#99
  • Flag — binary #9A

Todos of the game

  • Fix "look badge" (implement in system_script.txt)
  • Support wildcard command matching (using the EOF mark?)


English translation: Fuwanovel ZX Spectrum port, music re-arrangement in Vortex Tracker: Akasaka

Greetz and Thanks

  • nyuk
  • g0blinish
  • Fuwanovel
  • Nakata Keigo