├── .gitattributes ├── .gitignore ├── .gitmodules ├── README ├── data.py ├── dcode ├── header.py └── optables.py ├── fonts ├── bzork.ttf ├── font.py └── noto │ ├── mono │ ├── NotoSansMono-Bold.ttf │ └── NotoSansMono-Regular.ttf │ └── serif │ ├── NotoSerif-Bold.ttf │ ├── NotoSerif-BoldItalic.ttf │ ├── NotoSerif-Italic.ttf │ └── NotoSerif-Regular.ttf ├── settings.py ├── setup.py ├── sounds ├── beep.aiff └── boop.aiff ├── vio ├── __init__.py └── zcode.py ├── viola.py └── zcode ├── __init__.py ├── constants.py ├── dictionary.py ├── error.py ├── game.py ├── header.py ├── input.py ├── instructions.py ├── memory.py ├── numbers.py ├── objects.py ├── opcodes.py ├── optables.py ├── output.py ├── routines.py ├── screen.py ├── sounds.py └── text.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | 46 | [Dd]ebug/ 47 | [Rr]elease/ 48 | x64/ 49 | build/ 50 | [Bb]in/ 51 | [Oo]bj/ 52 | 53 | # MSTest test Results 54 | [Tt]est[Rr]esult*/ 55 | [Bb]uild[Ll]og.* 56 | 57 | *_i.c 58 | *_p.c 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.log 79 | *.scc 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | 94 | # Guidance Automation Toolkit 95 | *.gpState 96 | 97 | # ReSharper is a .NET coding add-in 98 | _ReSharper*/ 99 | *.[Rr]e[Ss]harper 100 | 101 | # TeamCity is a build add-in 102 | _TeamCity* 103 | 104 | # DotCover is a Code Coverage Tool 105 | *.dotCover 106 | 107 | # NCrunch 108 | *.ncrunch* 109 | .*crunch*.local.xml 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.Publish.xml 129 | *.pubxml 130 | 131 | # NuGet Packages Directory 132 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 133 | #packages/ 134 | 135 | # Windows Azure Build Output 136 | csx 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | sql/ 144 | *.Cache 145 | ClientBin/ 146 | [Ss]tyle[Cc]op.* 147 | ~$* 148 | *~ 149 | *.dbmdl 150 | *.[Pp]ublish.xml 151 | *.pfx 152 | *.publishsettings 153 | 154 | # RIA/Silverlight projects 155 | Generated_Code/ 156 | 157 | # Backup & report files from converting an old project file to a newer 158 | # Visual Studio version. Backup files are not needed, because we have git ;-) 159 | _UpgradeReport_Files/ 160 | Backup*/ 161 | UpgradeLog*.XML 162 | UpgradeLog*.htm 163 | 164 | # SQL Server files 165 | App_Data/*.mdf 166 | App_Data/*.ldf 167 | 168 | ############# 169 | ## Windows detritus 170 | ############# 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | 182 | # Mac crap 183 | .DS_Store 184 | 185 | 186 | ############# 187 | ## Python 188 | ############# 189 | 190 | *.py[co] 191 | 192 | # Packages 193 | *.egg 194 | *.egg-info 195 | dist/ 196 | build/ 197 | eggs/ 198 | parts/ 199 | var/ 200 | sdist/ 201 | develop-eggs/ 202 | .installed.cfg 203 | 204 | # Installer logs 205 | pip-log.txt 206 | 207 | # Unit test / coverage reports 208 | .coverage 209 | .tox 210 | 211 | #Translations 212 | *.mo 213 | 214 | #Mr Developer 215 | .mr.developer.cfg 216 | 217 | #PyCharm 218 | venv/ 219 | .idea/ 220 | 221 | # Viola 222 | glk/ 223 | glulx/ 224 | games/ 225 | misc/ 226 | future/ 227 | zcode/lgop.txt 228 | icons/ 229 | 230 | *.txt 231 | 232 | *.zblorb 233 | *.z1 234 | *.z2 235 | *.z3 236 | *.z4 237 | *.z5 238 | *.z6 239 | *.z7 240 | *.z8 241 | *.blorb 242 | *.blb 243 | *.zblb 244 | 245 | viola 246 | 247 | dcode/details 248 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ififf"] 2 | path = ififf 3 | url = https://github.com/DFillmore/ififf 4 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | VIOLA 0.8.1 2 | 3 | Written by David Fillmore. 4 | 5 | Viola is a Z-Machine Interpreter. That is, it interprets programs written for the virtual 6 | machine known as the Z-Machine. There's a brief history of the Z-Machine at the bottom of 7 | this file, for some reason. 8 | 9 | The latest code for Viola is available at https://github.com/DFillmore/viola. 10 | 11 | FEATURES 12 | 13 | * Z-Machine Versions 1-8 support 14 | * Z-Machine Standards Document 1.1 compliance 15 | * Blorb Resource Format 2.0 compliance 16 | * Quetzal Saved Game Format 1.4 compliance 17 | * Support (via Blorb) for sounds and, in Version 6, images. 18 | 19 | 20 | INSTALLATION 21 | 22 | Some brief notes on getting the program up and running. 23 | 24 | You will need Python 3 (I use version 3.3.5), which you can get at http://www.python.org, and 25 | pygame (I use version 1.9.2), which you can get at http://www.pygame.org/. I genuinely don't 26 | know if the setup.py file does anything useful. 27 | 28 | BRIEF HISTORY OF THE Z-MACHINE FOR SOME REASON 29 | 30 | The Z-Machine was created in the early Eighties by Infocom in order to run 31 | text adventures. 32 | 33 | Infocom evolved the design of the Z-Machine gradually, and created six distinct versions, 34 | each with more capabilites than the previous. Versions 1, 2 and 3 are almost identical, 35 | with slight changes to certain parts of the design that make them incompatible. Most of 36 | Infocom's games were released for version 3. Version 3 was a fairly simple design, but a 37 | seperate upper window was introduced for one game, and support for sound effects (which 38 | was really part of the Version 5 design, but a scaled down version was stuck into Version 39 | 3). 40 | Version 4 introduced text styles, better upper window support and a few other features, 41 | as well as allowing games to be larger. Version 5 allowed even larger games, with several 42 | new features including better sound support. Version 6 allowed for graphics, up to eight 43 | windows which could be moved and resized, bigger games, and other less obvious features. 44 | 45 | Then Infocom went out of business, more or less. 46 | 47 | Several years later, Graham Nelson created Inform, which allowed anyone to create 48 | Z-Machine games. Two new Versions were created, numbered 7 and 8. These have none of the 49 | extra features of z6, but instead are identical to the z5 model, except that they both 50 | allow larger games. z7 was never really used. 51 | 52 | Most games created for the Z-Machine since Infocom stopped business are z5 games. 53 | -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import re 16 | from urllib.request import urlopen 17 | 18 | 19 | ifdb_url = "https://ifdb.org/search?searchgo=Search+Games&searchfor=ifid:" 20 | 21 | legacy_serials = ("00", "01", "02", "03", "04", "05") 22 | 23 | 24 | def getcode(gamedata): 25 | release = (gamedata[2] << 8) + gamedata[3] 26 | serial = gamedata[0x12:0x18].decode('latin-1') 27 | return str(release) + '.' + serial 28 | 29 | 30 | def getifid(gamedata): 31 | serial: list[str] = list(gamedata[0x12:0x18].decode('latin-1')) 32 | if serial[0] == "8" or serial[0] == "9" or ''.join(serial[0:2]) in legacy_serials: 33 | pass 34 | else: 35 | expr = r"(?<=UUID:\/\/)[a-zA-z\d\-]+(?=\/\/)" 36 | r = re.compile(expr, re.M | re.S) 37 | match = r.search(gamedata.decode('latin-1')) 38 | if match is not None: 39 | return match.string[match.start():match.end()] 40 | for a in range(len(serial)): 41 | if not serial[a].isalnum(): 42 | serial[a] = "-" 43 | release = int.from_bytes(gamedata[2:4], byteorder="big") 44 | checksum = int.from_bytes(gamedata[0x1c:0x1e], byteorder="big") 45 | i = "ZCODE-" + str(release) + "-" + ''.join(serial) 46 | 47 | if serial[0].isnumeric() and serial[0] != "8" and ''.join(serial) not in ("000000", "999999", "------"): 48 | i += "-" + hex(checksum)[2:] 49 | return i 50 | 51 | 52 | def getpage(ifid): 53 | response = urlopen(ifdb_url+ifid) 54 | page = response.read().decode('latin-1') 55 | if 'No results were found.' in page and 'Can\'t find the game you\'re looking for?' in page: 56 | page = None 57 | return page 58 | 59 | 60 | def gettitle(ifdb_page): 61 | expr = r"(?<=\).*(?=\<\/h1\>)" 62 | r = re.compile(expr, re.M | re.S) 63 | match = r.search(ifdb_page) 64 | if match is not None: 65 | return match.string[match.start():match.end()] 66 | return None 67 | 68 | 69 | def getauthor(ifdb_page): 70 | expr1 = r"(?<=\)" 71 | expr2 = r"(?<=\">).*" 72 | r = re.compile(expr1, re.M | re.S) 73 | match = r.search(ifdb_page) 74 | if match is not None: 75 | m = match.string[match.start():match.end()] 76 | r = re.compile(expr2, re.M | re.S) 77 | match = r.search(m) 78 | if match is not None: 79 | return match.string[match.start():match.end()] 80 | return None 81 | 82 | 83 | if __name__ == '__main__': 84 | f = open('games/zork1.z3', 'rb') 85 | d = f.read() 86 | f.close() 87 | p = getpage(getifid(d)) 88 | if p: 89 | print(getifid(d), gettitle(p), getauthor(p)) 90 | -------------------------------------------------------------------------------- /dcode/header.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import string 16 | 17 | 18 | # /***** FILE HEADER STRUCTURE (PROGRAM AND IMAGE) *****/ 19 | 20 | # 0x0 /* D-MACHINE VERSION BYTE */ 21 | # 0x1 /* MODE BYTE */ 22 | # 0x2 /* GAME ID */ 23 | # 0x4 /* END OF GAME FILE PRELOAD (WORD PTR) */ 24 | # 0x6 /* EXECUTION START ADDR (WORD PTR "ODD") */ 25 | # 0x8 /* BEGINNING OF PURE CODE (WORD PTR) */ 26 | # 0xA /* GLOBAL TABLE (WORD PTR) */ 27 | # 0xC /* 6 BYTE SERIAL NUMBER (DATE) */ 28 | # 0x12 /* LENGTH OF GAME FILE, IN WORDS */ 29 | # 0x14 /* CHECKSUM OF GAME FILE, EXCLUDING HEADER */ 30 | # 0x16 /* INTERPRETER ID, LETTER & NUMBER */ 31 | 32 | # 0x40 /* LENGTH OF HEADER, PROGRAM FILE */ 33 | 34 | # 0x0 /* length of image file, in words */ 35 | # 0x2 /* end of preload, word ptr */ 36 | # 0x4 /* checksum of image file */ 37 | # 0x6 /* total number of blocksets in the file */ 38 | # 0x7 /* total number of icons in the file */ 39 | 40 | # 0x8 /* LENGTH OF HEADER, IMAGE FILE */ 41 | 42 | # 0x1 /* padding between code and image files, in words */ 43 | 44 | 45 | def setup(): # set all the relevant bits and bytes and words in the header 46 | # Flags 1 47 | if zversion() == 3: 48 | setflag(1, 4, 0) # status line is available 49 | setflag(1, 5, 1) # screen splitting is available 50 | setflag(1, 6, 0) # The default font is not fixed-pitch 51 | elif zversion() < 9: 52 | setflag(1, 2, zcode.screen.supportedstyles(2)) # Boldface 53 | setflag(1, 3, zcode.screen.supportedstyles(4)) # Italic 54 | setflag(1, 4, zcode.screen.supportedstyles(8)) # Fixed-pitch style 55 | if zcode.use_standard >= STANDARD_02: # from 0.2 onward 56 | setflag(1, 7, 1) # Timed input 57 | if zversion() > 4: 58 | setflag(1, 0, zcode.screen.supportedgraphics(0)) # Colours 59 | if zversion() == 6: 60 | setflag(1, 1, zcode.screen.supportedgraphics(3)) # Picture displaying 61 | if zcode.sounds.availablechannels(0) + zcode.sounds.availablechannels(1) > 0: # if any effect or music channels are available, sound effects are available 62 | setflag(1, 5, 1) # sound effects 63 | else: 64 | setflag(1, 5, 0) 65 | 66 | 67 | # Flags 2 - If unset by the game, the terp should leave them like that. 68 | if zversion() > 4: 69 | if getflag(2, 3): # pictures 70 | setflag(2, 3, zcode.screen.supportedgraphics(3)) 71 | if getflag(2, 4): # undo 72 | setflag(2, 4, 1) 73 | if getflag(2, 5): # mouse 74 | setflag(2, 5, 1) 75 | if getflag(2, 6): # colours 76 | setflag(2, 6, zcode.screen.supportedgraphics(0)) 77 | if getflag(2, 7): # sound effects 78 | if zcode.screen.supportedgraphics(0) + zcode.screen.supportedgraphics(1) > 0: 79 | setflag(2, 7, 1) 80 | else: 81 | setflag(2, 7, 0) 82 | if getflag(2, 8): # menus 83 | setflag(2, 8, 0) 84 | 85 | # Flags 3 - If unset by the game, the terp should leave them like that. All unknown bits should be set to 0. 86 | if zversion() > 4: 87 | if getflag(3, 0): # transparency 88 | setflag(3, 0, zcode.screen.supportedgraphics(2)) 89 | for a in range(1, 16): # set all the other bits to 0, because we don't know what they do 90 | setflag(3, a, 0) 91 | 92 | if zversion() > 3: 93 | # Interpreter number 94 | setterpnum(6) 95 | # Interpreter version 96 | setterpversion(ord('V')) 97 | 98 | updateSizes() 99 | 100 | if zversion() > 4: 101 | # Default foreground colour 102 | setdeffgcolour(zcode.screen.DEFFOREGROUND) 103 | # Default background colour 104 | setdefbgcolour(zcode.screen.DEFBACKGROUND) 105 | settruedefaultforeground(zcode.screen.spectrum[2]) 106 | settruedefaultbackground(zcode.screen.spectrum[9]) 107 | # Z-machine Standard number 108 | m = standards[zcode.use_standard][0] 109 | n = standards[zcode.use_standard][1] 110 | setstandardnum(m, n) 111 | 112 | def updateFontSize(): 113 | if zversion() > 4: 114 | # Font width 115 | if zcode.screen.graphics_mode == 1: 116 | setfontwidth(zcode.screen.currentWindow.getFont().getWidth()) 117 | else: 118 | setfontwidth(1) 119 | # Font height 120 | if zcode.screen.graphics_mode == 1: 121 | setfontheight(zcode.screen.currentWindow.getFont().getHeight()) 122 | else: 123 | setfontheight(1) 124 | 125 | 126 | def updateSizes(): 127 | if zversion() > 3: 128 | columns = int(zcode.screen.ioScreen.getWidth() // zcode.screen.getWindow(1).getFont().getWidth()) 129 | # Screen height (lines) 130 | setscreenheightlines(int(zcode.screen.ioScreen.getHeight() // zcode.screen.getWindow(1).getFont().getHeight())) 131 | # Screen width (chars) 132 | setscreenwidthchars(columns) 133 | 134 | if zversion() > 4: 135 | # Screen width (units) 136 | if zcode.screen.graphics_mode == 1: 137 | setscreenwidth(zcode.screen.ioScreen.getWidth()) 138 | else: 139 | setscreenwidth(columns) 140 | # Screen height (units) 141 | if zcode.screen.graphics_mode == 1: 142 | setscreenheight(zcode.screen.ioScreen.getHeight()) 143 | else: 144 | setscreenheight(int(zcode.screen.ioScreen.getHeight() // zcode.screen.getWindow(1).getFont().getHeight())) 145 | updateFontSize() 146 | 147 | 148 | def release(): 149 | return zcode.memory.getword(0x2) 150 | 151 | def serial(): 152 | x = zcode.memory.getarray(0x12,6) 153 | return ''.join([chr(b) for b in x]) 154 | 155 | def identifier(): 156 | return str(release()) + "." + serial() 157 | 158 | zmachine_version = None 159 | 160 | def zversion(): 161 | global zmachine_version 162 | if zmachine_version == None: 163 | zmachine_version = zcode.memory.getbyte(0) 164 | return zmachine_version 165 | 166 | 167 | def getflag(bitmap, bit): # bitmap is the set of flags to look in, such as flags 1, bit is the bit number to check, such as bit 1 for z3 status line type checking 168 | if bitmap == 1: 169 | flag = 1 170 | for a in range(bit): 171 | flag = flag * 2 172 | 173 | if zcode.memory.getbyte(1) & flag == flag: 174 | return 1 175 | else: 176 | return 0 177 | elif bitmap == 2: 178 | flag = 1 179 | for a in range(bit): 180 | flag = flag * 2 181 | if zcode.memory.getword(0x10) & flag == flag: 182 | return 1 183 | else: 184 | return 0 185 | elif bitmap == 3: 186 | if headerextsize() < 4: 187 | return 0 188 | else: 189 | flag = 1 190 | for a in range(bit): 191 | flag = flag * 2 192 | if zcode.memory.getword(headerextloc() + 4) & flag == flag: 193 | return 1 194 | else: 195 | return 0 196 | 197 | 198 | def setflag(bitmap, bit, value): 199 | # bitmap is the set of flags to look in, bit is the bit number to change, value is either 1 for on or 0 for off 200 | global flags1, flags2 201 | if bitmap == 1: 202 | flag = 1 203 | for a in range(bit): 204 | flag = flag * 2 205 | if value: 206 | zcode.memory.setbyte(1, zcode.memory.getbyte(1) | flag) 207 | else: 208 | zcode.memory.setbyte(1, zcode.memory.getbyte(1) & ~flag) 209 | elif bitmap == 2: 210 | flag = 1 211 | for a in range(bit): 212 | flag = flag * 2 213 | if value: 214 | zcode.memory.setword(0x10, zcode.memory.getword(0x10) | flag) 215 | else: 216 | zcode.memory.setword(0x10, zcode.memory.getword(0x10) & ~flag) 217 | elif bitmap == 3: 218 | if headerextsize() >= 4: 219 | flag = 1 220 | for a in range(bit): 221 | flag = flag * 2 222 | if value: 223 | zcode.memory.setword(headerextloc() + 8, zcode.memory.getword(headerextloc() + 8) | flag) 224 | else: 225 | zcode.memory.setword(headerextloc() + 8, zcode.memory.getword(headerextloc() + 8) & ~flag) 226 | 227 | high_memory = None 228 | 229 | def highmembase(): 230 | global high_memory 231 | if not high_memory: 232 | high_memory = zcode.memory.getword(0x4) 233 | return high_memory 234 | 235 | initial_PC = None 236 | 237 | def initialPC(): # for non z6 238 | global initial_PC 239 | if not initial_PC: 240 | initial_PC = zcode.memory.getword(0x6) 241 | return initial_PC 242 | 243 | main_routine = None 244 | 245 | def mainroutine(): # for z6 246 | global main_routine 247 | if not main_routine: 248 | main_routine = zcode.memory.getword(0x6) 249 | return main_routine 250 | 251 | dictionary_location = None 252 | 253 | def dictionaryloc(): 254 | global dictionary_location 255 | if not dictionary_location: 256 | dictionary_location = zcode.memory.getword(0x8) 257 | return dictionary_location 258 | 259 | object_table = None 260 | 261 | def objtableloc(): 262 | global object_table 263 | if not object_table: 264 | object_table = zcode.memory.getword(0xA) 265 | return object_table 266 | 267 | globals_location = None 268 | 269 | def globalsloc(): 270 | global globals_location 271 | if not globals_location: 272 | globals_location = zcode.memory.getword(0xa) 273 | return globals_location 274 | 275 | static_memory = None 276 | 277 | def statmembase(): 278 | global static_memory 279 | if not static_memory: 280 | static_memory = zcode.memory.getword(0x8) 281 | return static_memory 282 | 283 | abbreviations_table = None 284 | 285 | def abbrevtableloc(): 286 | global abbreviations_table 287 | if not abbreviations_table: 288 | abbreviations_table = zcode.memory.getword(0x18) 289 | return abbreviations_table 290 | 291 | file_length = None 292 | 293 | def filelen(): # in the header, this may be 0, in which case this routine should figure it out manually. But it doesn't. 294 | global file_length 295 | if not file_length: 296 | l = (zcode.memory.getbyte(0x1a) << 8) + zcode.memory.getbyte(0x1b) 297 | if l == 0: 298 | file_length = len(memory.data) 299 | elif zversion() < 4: # versions 1 to 3 300 | file_length = l * 2 301 | elif zversion() < 6: # versions 4 and 5 302 | file_length = l * 4 303 | else: # versions 6, 7 and 8 304 | file_length = l * 8 305 | return file_length 306 | 307 | checksum = None 308 | 309 | def getchecksum(): 310 | global checksum 311 | if not checksum: 312 | checksum = zcode.memory.getword(0x1C) 313 | return checksum 314 | 315 | def setterpnum(number): 316 | zcode.memory.setbyte(0x1E, number) 317 | 318 | def setterpversion(number): 319 | zcode.memory.setbyte(0x1F, number) 320 | 321 | def getterpnum(): 322 | return zcode.memory.getbyte(0x1E) 323 | 324 | def getterpversion(): # I doubt this will be needed 325 | return zcode.memory.getbyte(0x1F) 326 | 327 | def getscreenheightlines(): 328 | return zcode.memory.getbyte(0x20) 329 | 330 | def getscreenwidthchars(): 331 | return zcode.memory.getbyte(0x21) 332 | 333 | def getscreenwidth(): # screen width in units 334 | return zcode.memory.getword(0x22) 335 | 336 | def getscreenheight(): # screen height in units 337 | return zcode.memory.getword(0x24) 338 | 339 | def getfontwidth(): 340 | if zversion() == 6: 341 | return zcode.memory.getbyte(0x27) 342 | else: 343 | return zcode.memory.getbyte(0x26) 344 | 345 | def getfontheight(): 346 | if zversion() == 6: 347 | return zcode.memory.getbyte(0x26) 348 | else: 349 | return zcode.memory.getbyte(0x27) 350 | 351 | 352 | 353 | def setscreenheightlines(lines): 354 | zcode.memory.setbyte(0x20, lines) 355 | 356 | def setscreenwidthchars(chars): 357 | zcode.memory.setbyte(0x21, chars) 358 | 359 | def setscreenwidth(units): # screen width in units 360 | zcode.memory.setword(0x22, units) 361 | 362 | def setscreenheight(units): # screen height in units 363 | zcode.memory.setword(0x24, units) 364 | 365 | def setfontwidth(units): 366 | if zversion() == 6: 367 | zcode.memory.setbyte(0x27, units) 368 | else: 369 | zcode.memory.setbyte(0x26, units) 370 | 371 | def setfontheight(units): 372 | if zversion() == 6: 373 | zcode.memory.setbyte(0x26, units) 374 | else: 375 | zcode.memory.setbyte(0x27, units) 376 | 377 | routines_offset = None 378 | def routineoffset(): # z6 only 379 | global routines_offset 380 | if not routines_offset: 381 | routines_offset = zcode.memory.getword(0x28) * 8 382 | return routines_offset 383 | 384 | strings_offset = None 385 | 386 | def stringoffset(): # z6 only 387 | global strings_offset 388 | if not strings_offset: 389 | strings_offset = zcode.memory.getword(0x2A) * 8 390 | return strings_offset 391 | 392 | def setdefbgcolour(colour): 393 | zcode.memory.setbyte(0x2c, colour) 394 | 395 | def setdeffgcolour(colour): 396 | zcode.memory.setbyte(0x2D, colour) 397 | 398 | def getdefbgcolour(): 399 | return zcode.memory.getbyte(0x2C) 400 | 401 | def getdeffgcolour(): 402 | return zcode.memory.getbyte(0x2D) 403 | 404 | terminating_characters = None 405 | 406 | def termcharloc(): 407 | global terminating_characters 408 | if terminating_characters == None: 409 | terminating_characters = zcode.memory.getword(0x2E) 410 | return terminating_characters 411 | 412 | def settextwidth(len): # total width in units of text sent to output stream 3 413 | zcode.memory.setword(0x30, len) 414 | 415 | def setstandardnum(n, m): # for standard 1.0, setstandardnum(1, 0) would be sent. 416 | zcode.memory.setbyte(0x32, n) 417 | zcode.memory.setbyte(0x33, m) 418 | 419 | def getstandardnum(): 420 | n = zcode.memory.getbyte(0x32) 421 | m = zcode.memory.getbyte(0x33) 422 | return (n,m) 423 | 424 | alphabet_table = None 425 | 426 | def alphatableloc(): 427 | global alphabet_table 428 | if alphabet_table == None: 429 | alphabet_table = zcode.memory.getword(0x34) 430 | return alphabet_table 431 | 432 | header_extension = None 433 | 434 | def headerextloc(): 435 | global header_extension 436 | if header_extension == None: 437 | header_extension = zcode.memory.getword(0x36) 438 | return header_extension 439 | 440 | 441 | 442 | # header extension stuff 443 | 444 | header_extension_size = None 445 | 446 | def headerextsize(): 447 | global header_extension_size 448 | if header_extension_size == None: 449 | if headerextloc() == 0: 450 | header_extension_size = 0 451 | else: 452 | header_extension_size = zcode.memory.getword(headerextloc()) 453 | return header_extension_size 454 | 455 | def setHeaderExtWord(word, value): 456 | if headerextsize() < word: 457 | return 0 458 | zcode.memory.setword(headerextloc() + (word*2), value) 459 | 460 | def getHeaderExtWord(word): 461 | if headerextsize() < word: 462 | return 0 463 | else: 464 | return zcode.memory.getword(headerextloc() + (word*2)) 465 | 466 | 467 | def setmousex(xpos): 468 | setHeaderExtWord(1, xpos) 469 | 470 | def setmousey(ypos): 471 | setHeaderExtWord(2, ypos) 472 | 473 | unicode_table = None 474 | 475 | def unicodetableloc(): 476 | global unicode_table 477 | if unicode_table == None: 478 | unicode_table = getHeaderExtWord(3) 479 | return unicode_table 480 | 481 | 482 | 483 | 484 | # Standard 1.1 stuff below 485 | 486 | def settruedefaultforeground(colour): 487 | setHeaderExtWord(5, colour) 488 | 489 | def settruedefaultbackground(colour): 490 | setHeaderExtWord(6, colour) 491 | 492 | def gettruedefaultforeground(): 493 | return getHeaderExtWord(5) 494 | 495 | def gettruedefaultbackground(): 496 | return getHeaderExtWord(6) 497 | -------------------------------------------------------------------------------- /dcode/optables.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | from zcode.constants import * 17 | #define EXT_OP 192 18 | #define XQEQU EXT_OP + OPQEQU 19 | #define OPCALL 224 20 | #define OPPUT 225 21 | #define OPPUTB 226 22 | #define OPINPUT 227 23 | #define OPSHOWI 228 24 | #define OPSETI 229 25 | #define OPSWAPI 230 26 | #define OPSOUND 231 27 | #define OPRAND 232 28 | #define OPCLEAR 233 29 | #define OPSHOWN 234 30 | #define OPWIND 235 31 | #define OPITER 236 32 | #define OPLOAD 237 33 | #define OPDUMP 238 34 | #define OPREST 239 35 | #define OPSAVE 240 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | op2 = [ zcode.opcodes.z_badop, #define OPUNDF 0 56 | zcode.opcodes.z_add, #define OPADD 1 57 | zcode.opcodes.z_sub, #define OPSUB 2 58 | zcode.opcodes.z_mul, #define OPMUL 3 59 | zcode.opcodes.z_div, #define OPDIV 4 60 | zcode.opcodes.z_mod, #define OPMOD 5 61 | zcode.opcodes.z_and, #define OPBAND 6 62 | zcode.opcodes.z_or, #define OPBOR 7 63 | zcode.opcodes.z_badop, #zcode.opcodes.z_BXOR, #define OPBXOR 8 64 | zcode.opcodes.z_test, #define OPBTST 9 65 | zcode.opcodes.z_je, #define OPQEQU 10 66 | zcode.opcodes.z_jl, #define OPQLES 11 67 | zcode.opcodes.z_dec_chk, #define OPQDLE 12 68 | zcode.opcodes.z_jg, #define OPQGRT 13 69 | zcode.opcodes.z_inc_chk, #define OPQIGR 14 70 | zcode.opcodes.z_store, #define OPSET 15 71 | zcode.opcodes.z_loadw, #define OPGET 16 72 | zcode.opcodes.z_loadb, #define OPGETB 17 73 | ] 74 | 75 | op1 = [ zcode.opcodes.z_push, #define OPPUSH 128 76 | zcode.opcodes.z_pull, #define OPPOP 129 77 | zcode.opcodes.z_load, #define OPVALU 130 78 | zcode.opcodes.z_inc, #define OPINC 131 79 | zcode.opcodes.z_dec, #define OPDEC 132 80 | zcode.opcodes.z_jz, #define OPQZER 133 81 | zcode.opcodes.z_not, #define OPBCOM 134 /* "BNOT" */ 82 | zcode.opcodes.z_jump, #define OPJUMP 135 83 | zcode.opcodes.z_ret, #define OPRETU 136 84 | ] 85 | 86 | op0 = [ zcode.opcodes.z_nop, #define OPNOOP 176 87 | zcode.opcodes.z_rtrue, #define OPRTRU 177 88 | zcode.opcodes.z_rfalse, #define OPRFAL 178 89 | zcode.opcodes.z_ret_popped, #define OPRSTA 179 90 | zcode.opcodes.z_pop, #define OPFSTA 180 91 | zcode.opcodes.z_quit, #define OPQUIT 181 92 | zcode.opcodes.z_piracy, #define OPCOPY 182 93 | zcode.opcodes.z_verify, #define OPVERI 183 94 | ] 95 | 96 | opvar = [ zcode.opcodes.z_call_vs,#define OPCALL 224 97 | zcode.opcodes.z_storew,#define OPPUT 225 98 | zcode.opcodes.z_storeb,#define OPPUTB 226 99 | zcode.opcodes.z_read_char,#define OPINPUT 227 100 | zcode.opcodes.z_badop,#zcode.opcodes.SHOWI#define OPSHOWI 228 101 | zcode.opcodes.z_badop,#zcode.opcodes.SETI #define OPSETI 229 102 | zcode.opcodes.z_badop,#zcode.opcodes.SWAPI #define OPSWAPI 230 103 | zcode.opcodes.z_sound_effect,#define OPSOUND 231 104 | zcode.opcodes.z_random,#define OPRAND 232 105 | zcode.opcodes.z_erase_window,#define OPCLEAR 233 106 | zcode.opcodes.z_badop,#zcode.opcodes.SHOWN,#define OPSHOWN 234 107 | zcode.opcodes.z_badop,#zcode.opcodes.OPWIND,#define OPWIND 235 108 | zcode.opcodes.z_badop,# zcode.opcodes.OPITER,#define OPITER 236 109 | zcode.opcodes.z_badop,#zcode.opcodes.LOAD,#define OPLOAD 237 110 | zcode.opcodes.z_badop,#zcode.opcodes.DUMP,#define OPDUMP 238 111 | zcode.opcodes.z_save,#define OPREST 239 112 | zcode.opcodes.z_restore,#define OPSAVE 240 113 | ] 114 | 115 | opext = [ zcode.opcodes.z_restore, #define XQEQU EXT_OP + OPQEQU 116 | ] 117 | 118 | def setup(): 119 | pass 120 | 121 | -------------------------------------------------------------------------------- /fonts/bzork.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/bzork.ttf -------------------------------------------------------------------------------- /fonts/font.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | tables = {} 16 | 17 | def readTableDirectory(fontFile): 18 | #offset subtable 19 | 20 | #Type Name Description 21 | #uint32 scaler type A tag to indicate the OFA scaler to be used to rasterize this font; see the note on the scaler type below for more information. 22 | #uint16 numTables number of tables 23 | #uint16 searchRange (maximum power of 2 <= numTables)*16 24 | #uint16 entrySelector log2(maximum power of 2 <= numTables) 25 | #uint16 rangeShift numTables*16-searchRange 26 | b = fontFile.read(4) 27 | scaler = int.from_bytes(b, byteorder='big') 28 | 29 | b = fontFile.read(2) 30 | numTables = int.from_bytes(b, byteorder='big') 31 | 32 | b = fontFile.read(2) 33 | searchRange = int.from_bytes(b, byteorder='big') 34 | 35 | b = fontFile.read(2) 36 | entrySelector = int.from_bytes(b, byteorder='big') 37 | 38 | b = fontFile.read(2) 39 | rangeShift = int.from_bytes(b, byteorder='big') 40 | 41 | 42 | #The table directory 43 | 44 | #Type Name Description 45 | #uint32 tag 4-byte identifier 46 | #uint32 checkSum checksum for this table 47 | #uint32 offset offset from beginning of sfnt 48 | #uint32 length length of this table in bytes (actual length not padded length) 49 | 50 | for a in range(numTables): 51 | table = {} 52 | 53 | b = fontFile.read(4) 54 | t = b.decode('utf-8') 55 | 56 | b = fontFile.read(4) 57 | table['checkSum'] = int.from_bytes(b, byteorder='big') 58 | 59 | b = fontFile.read(4) 60 | table['offset'] = int.from_bytes(b, byteorder='big') 61 | 62 | b = fontFile.read(4) 63 | table['length'] = int.from_bytes(b, byteorder='big') 64 | 65 | tables[t] = table 66 | 67 | 68 | def subtable(fontFile, tableLoc): 69 | fontFile.seek(tableLoc) 70 | 71 | #'cmap' format 4 72 | #Type Name Description 73 | #UInt16 format Format number is set to 4 74 | #UInt16 length Length of subtable in bytes 75 | #UInt16 language Language code (see above) 76 | #UInt16 segCountX2 2 * segCount 77 | #UInt16 searchRange 2 * (2**FLOOR(log2(segCount))) 78 | #UInt16 entrySelector log2(searchRange/2) 79 | #UInt16 rangeShift (2 * segCount) - searchRange 80 | #UInt16 endCode[segCount] Ending character code for each segment, last = 0xFFFF. 81 | #UInt16 reservedPad This value should be zero 82 | #UInt16 startCode[segCount] Starting character code for each segment 83 | #UInt16 idDelta[segCount] Delta for all character codes in segment 84 | #UInt16 idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 85 | #UInt16 glyphIndexArray[variable] Glyph index array 86 | endCodes = [] 87 | startCodes = [] 88 | idDeltas = [] 89 | idRangeOffsets = [] 90 | glyphIndexArrays = [] 91 | codes = set() 92 | b = fontFile.read(2) 93 | form = int.from_bytes(b, byteorder='big') 94 | if form == 4: 95 | b = fontFile.read(2) 96 | leng = int.from_bytes(b, byteorder='big') 97 | b = fontFile.read(2) 98 | language = int.from_bytes(b, byteorder='big') 99 | b = fontFile.read(2) 100 | segCount = int(int.from_bytes(b, byteorder='big') / 2) 101 | b = fontFile.read(2) 102 | searchRange = int.from_bytes(b, byteorder='big') 103 | b = fontFile.read(2) 104 | entrySelector = int.from_bytes(b, byteorder='big') 105 | b = fontFile.read(2) 106 | rangeShift = int.from_bytes(b, byteorder='big') 107 | 108 | for a in range(segCount): 109 | b = fontFile.read(2) 110 | endCodes.append(int.from_bytes(b, byteorder='big')) 111 | b = fontFile.read(2) 112 | reservedPad = int.from_bytes(b, byteorder='big') 113 | 114 | for a in range(segCount): 115 | b = fontFile.read(2) 116 | startCodes.append(int.from_bytes(b, byteorder='big')) 117 | 118 | for a in range(segCount): 119 | b = fontFile.read(2) 120 | idDeltas.append(int.from_bytes(b, byteorder='big')) 121 | 122 | for a in range(segCount): 123 | b = fontFile.read(2) 124 | idRangeOffsets.append(int.from_bytes(b, byteorder='big')) 125 | 126 | b = fontFile.read(2) 127 | glyphIndexArray = int.from_bytes(b, byteorder='big') 128 | 129 | 130 | #to find if a glyph is available, search endCodes for the first number >= the character number we want. If the corresponding startCode is <= the character number, there is a glyph. If not, not. 131 | 132 | for s in range(segCount): 133 | endCode = endCodes[s] 134 | startCode = startCodes[s] 135 | idRangeOffset = idRangeOffsets[s] 136 | idDelta = idDeltas[s] 137 | #print('r', startCode, endCode) 138 | #for c in range(startCode, endCode+1): 139 | # if c not in codes: 140 | # if idRangeOffset == 0: 141 | # glyphId = idDelta + c 142 | # if c == 0x0370: 143 | # print('g', glyphId) 144 | 145 | # codes.add(c) 146 | # else: 147 | # glyphId = idRangeOffset + 2 * (c - startCode) + l 148 | # code. 149 | 150 | codes.update(range(startCode, endCode+1)) 151 | elif form == 12: 152 | # format 12 header 153 | # UInt16 format Subtable format; set to 12 154 | #UInt16 reserved Set to 0. 155 | #UInt32 length Byte length of this subtable (including the header) 156 | #UInt32 language Language code 157 | #UInt32 nGroups Number of groupings which follow 158 | b = fontFile.read(2) 159 | reserved = int.from_bytes(b, byteorder='big') 160 | b = fontFile.read(4) 161 | tlength = int.from_bytes(b, byteorder='big') 162 | b = fontFile.read(4) 163 | language = int.from_bytes(b, byteorder='big') 164 | b = fontFile.read(4) 165 | numGroups = int.from_bytes(b, byteorder='big') 166 | 167 | # format 12 group 168 | #UInt32 startCharCode First character code in this group 169 | #UInt32 endCharCode Last character code in this group 170 | #UInt32 startGlyphCode Glyph index corresponding to the starting character code; subsequent charcters are mapped to sequential glyphs 171 | for a in range(numGroups): 172 | b = fontFile.read(4) 173 | startCharCode = int.from_bytes(b, byteorder='big') 174 | if startCharCode > 0xffff: 175 | continue 176 | b = fontFile.read(4) 177 | endCharCode = int.from_bytes(b, byteorder='big') 178 | b = fontFile.read(4) 179 | startGlyphCode = int.from_bytes(b, byteorder='big') 180 | codes.update(range(startCharCode, endCharCode+1)) 181 | 182 | return codes 183 | 184 | def readcmap(fontFile): 185 | o = tables['cmap']['offset'] 186 | fontFile.seek(o) 187 | #The 'cmap' index 188 | #Type Name Description 189 | #UInt16 version Version number (Set to zero) 190 | #UInt16 numberSubtables Number of encoding subtables 191 | strt = fontFile.tell() 192 | b = fontFile.read(2) 193 | vers = int.from_bytes(b, byteorder='big') 194 | b = fontFile.read(2) 195 | numberSubtables = int.from_bytes(b, byteorder='big') 196 | 197 | #The 'cmap' encoding subtable 198 | #Type Name Description 199 | #UInt16 platformID Platform identifier 200 | #UInt16 platformSpecificID Platform-specific encoding identifier 201 | #UInt32 offset Offset of the mapping table 202 | 203 | codes = set() 204 | for a in range(numberSubtables): 205 | b = fontFile.read(2) 206 | platformID = int.from_bytes(b, byteorder='big') 207 | b = fontFile.read(2) 208 | platformSpecificID = int.from_bytes(b, byteorder='big') 209 | b = fontFile.read(4) 210 | mapOffset = int.from_bytes(b, byteorder='big') 211 | if platformID == 0: # Unicode 212 | codes.update(subtable(fontFile, mapOffset+o)) 213 | return codes 214 | 215 | 216 | 217 | 218 | 219 | def getCodes(fontFileName): 220 | if fontFileName: 221 | fontFile = open(fontFileName, 'rb') 222 | readTableDirectory(fontFile) 223 | codes = readcmap(fontFile) 224 | fontFile.close() 225 | else: 226 | codes = [] 227 | return codes 228 | 229 | 230 | -------------------------------------------------------------------------------- /fonts/noto/mono/NotoSansMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/mono/NotoSansMono-Bold.ttf -------------------------------------------------------------------------------- /fonts/noto/mono/NotoSansMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/mono/NotoSansMono-Regular.ttf -------------------------------------------------------------------------------- /fonts/noto/serif/NotoSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/serif/NotoSerif-Bold.ttf -------------------------------------------------------------------------------- /fonts/noto/serif/NotoSerif-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/serif/NotoSerif-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/noto/serif/NotoSerif-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/serif/NotoSerif-Italic.ttf -------------------------------------------------------------------------------- /fonts/noto/serif/NotoSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/fonts/noto/serif/NotoSerif-Regular.ttf -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2004 - 2019 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # reads settings from the .violarc file 16 | 17 | from __future__ import annotations 18 | 19 | import copy 20 | import re 21 | import os 22 | import data 23 | 24 | locations = ["$HOME", "$USERPROFILE"] 25 | 26 | base_expression = r"(\\#|.)*?(?=$|#)" 27 | 28 | 29 | def findgame(gamecode): 30 | expr = r"(?:code: | ifid:)[\s\w.]*" + gamecode + r"(?:.|\n)*?(?=code:|ifid:|\Z)" 31 | r = re.compile(expr, re.M | re.S) 32 | match = r.search(filetext) 33 | if match is None: 34 | return None 35 | return match.string[match.start():match.end()] 36 | 37 | 38 | def getdefaults(): 39 | expr = r"(?:.*?(\n|\Z))*?(?=code:|ifid:|\Z)" 40 | r = re.compile(expr, re.M | re.S) 41 | match = r.search(filetext) 42 | return match.string[match.start():match.end()] 43 | 44 | 45 | def gettitle(gamesettings): 46 | expr = r"title:" + base_expression 47 | r = re.compile(expr, re.M) 48 | match = r.search(gamesettings) 49 | if match is None: 50 | return None 51 | else: 52 | return match.string[match.start() + 6:match.end()].strip() 53 | 54 | 55 | def getheadline(gamesettings): 56 | expr = r"headline:" + base_expression 57 | r = re.compile(expr, re.M) 58 | match = r.search(gamesettings) 59 | if match is None: 60 | return None 61 | else: 62 | return match.string[match.start() + 6:match.end()].strip() 63 | 64 | 65 | def getauthor(gamesettings): 66 | expr = r"author:" + base_expression 67 | r = re.compile(expr, re.M) 68 | match = r.search(gamesettings) 69 | if match is None: 70 | return None 71 | else: 72 | return match.string[match.start() + 6:match.end()].strip() 73 | 74 | 75 | def getblorb(gamesettings): 76 | expr = r"blorb:" + base_expression 77 | r = re.compile(expr, re.M) 78 | match = r.search(gamesettings) 79 | if match is None: 80 | return None 81 | else: 82 | return match.string[match.start() + 6:match.end()].strip() 83 | 84 | 85 | def getheight(gamesettings): 86 | expr = r"height:" + base_expression 87 | r = re.compile(expr, re.M) 88 | match = r.search(gamesettings) 89 | if match is None: 90 | return None 91 | else: 92 | return int(match.string[match.start() + 7:match.end()].strip()) 93 | 94 | 95 | def getwidth(gamesettings): 96 | expr = r"width:" + base_expression 97 | r = re.compile(expr, re.M) 98 | match = r.search(gamesettings) 99 | if match is None: 100 | return None 101 | else: 102 | return int(match.string[match.start() + 6:match.end()].strip()) 103 | 104 | 105 | def getterpnum(gamesettings): 106 | expr = r"terpnum:" + base_expression 107 | r = re.compile(expr, re.M) 108 | match = r.search(gamesettings) 109 | if match is None: 110 | return None 111 | else: 112 | return match.string[match.start() + 8:match.end()].strip() 113 | 114 | 115 | def getforeground(gamesettings): 116 | expr = r"foreground:" + base_expression 117 | r = re.compile(expr, re.M) 118 | match = r.search(gamesettings) 119 | if match is None: 120 | return None 121 | else: 122 | return match.string[match.start() + 11:match.end()].strip() 123 | 124 | 125 | def getbackground(gamesettings): 126 | expr = r"background:" + base_expression 127 | r = re.compile(expr, re.M) 128 | match = r.search(gamesettings) 129 | if match is None: 130 | return None 131 | else: 132 | return match.string[match.start() + 11:match.end()].strip() 133 | 134 | 135 | # priority of settings: command line > game-specific settings > data from ifdb > default settings > viola defaults 136 | 137 | class gameset: 138 | priority = 0 139 | title = None 140 | width = None 141 | height = None 142 | blorb = None 143 | terp_number = None 144 | foreground = None 145 | background = None 146 | headline = None 147 | author = None 148 | 149 | def __init__(self, *, priority=0, width=None, height=None, blorb=None, terp_number=None, 150 | foreground=None, background=None, title=None, headline=None, author=None): 151 | self.priority = priority 152 | self.width = width 153 | self.height = height 154 | self.blorb = blorb 155 | self.terp_number = terp_number 156 | self.foreground = foreground 157 | self.background = background 158 | self.title = title 159 | self.headline = headline 160 | self.author = author 161 | 162 | def merge(self, gset: gameset) -> gameset: 163 | setA = copy.deepcopy(self) 164 | setB = copy.deepcopy(gset) 165 | if self.priority < gset.priority: 166 | setA = copy.deepcopy(gset) 167 | setB = copy.deepcopy(self) 168 | 169 | if setA.width: 170 | setB.width = setA.width 171 | if setA.height: 172 | setB.height = setA.height 173 | if setA.blorb: 174 | setB.blorb = setA.blorb 175 | if setA.terp_number: 176 | setB.terp_number = setA.terp_number 177 | if setA.foreground: 178 | setB.foreground = setA.foreground 179 | if setA.background: 180 | setB.background = setA.background 181 | if setA.title: 182 | setB.title = setA.title 183 | if setA.headline: 184 | setB.headline = setA.headline 185 | if setA.author: 186 | setB.author = setA.author 187 | 188 | return setB 189 | 190 | 191 | def getsettings(gamecode=None): 192 | if not gamecode: 193 | gamesettings = getdefaults() 194 | priority = 1 195 | else: 196 | gamesettings = findgame(gamecode) 197 | priority = 3 198 | 199 | if gamesettings is None: 200 | gset = gameset(priority=priority, width=None, height=None, blorb=None, terp_number=None, 201 | foreground=None, background=None, headline=None, author=None) 202 | else: 203 | gset = gameset(priority=priority, 204 | title=gettitle(gamesettings), 205 | headline=getheadline(gamesettings), 206 | author=getauthor(gamesettings), 207 | width=getwidth(gamesettings), 208 | height=getheight(gamesettings), 209 | blorb=getblorb(gamesettings), 210 | terp_number=getterpnum(gamesettings), 211 | foreground=getforeground(gamesettings), 212 | background=getbackground(gamesettings) 213 | ) 214 | return gset 215 | 216 | 217 | def setup(): 218 | global file, filesize, filetext, defaults 219 | 220 | filename: str | None = None 221 | for loc in locations: 222 | filename = os.path.join(os.path.expandvars(loc), ".violarc") 223 | if os.path.exists(filename) == 1: 224 | break 225 | 226 | if filename is None: 227 | filetext = '' 228 | else: 229 | file = open(filename, "r") 230 | filesize = os.stat(filename).st_size 231 | filetext = file.read(filesize) 232 | file.close() 233 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='Viola', 4 | version='0.8', 5 | description='Z-Machine interpreter written in Python', 6 | author='David Fillmore', 7 | author_email='marvin@frobnitz.co.uk', 8 | packages=["vio", "zcode"], 9 | scripts=['viola.py',], 10 | py_modules=["babel", "blorb", "iff", "quetzal", "settings"], 11 | data_files=[('fonts', ['fonts/bzork.ttf', 12 | 'fonts/noto/mono/NotoSansMono-Bold.ttf', 13 | 'fonts/noto/mono/NotoSansMono-Regular.ttf', 14 | 'fonts/noto/mono/OFL.txt', 15 | 'fonts/noto/serif/NotoSerif-Regular.ttf', 16 | 'fonts/noto/serif/NotoSerif-Bold.ttf', 17 | 'fonts/noto/serif/NotoSerif-Italic.ttf', 18 | 'fonts/noto/serif/NotoSerif-BoldItalic.ttf', 19 | 'fonts/noto/serif/OFL.txt' 20 | ] 21 | ) 22 | ], 23 | install_requires=['pygame>=2.5'] 24 | ) 25 | -------------------------------------------------------------------------------- /sounds/beep.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/sounds/beep.aiff -------------------------------------------------------------------------------- /sounds/boop.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DFillmore/viola/87f47a0331ef89b3b4bcbb2b6a6c6cee02321723/sounds/boop.aiff -------------------------------------------------------------------------------- /vio/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | __all__ = ["zcode"] 16 | from vio import * 17 | -------------------------------------------------------------------------------- /viola.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (C) 2001 - 2019 David Fillmore 4 | # 5 | # This file is part of Viola. 6 | # 7 | # Viola is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Viola is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | 17 | from __future__ import annotations 18 | 19 | import data 20 | import vio.zcode as io 21 | import sys 22 | import getopt 23 | import settings 24 | import zcode 25 | from ififf import blorb 26 | from ififf import babel 27 | from zcode.constants import specs 28 | 29 | blorbs = [] 30 | height = None 31 | width = None 32 | title = None 33 | terpnum: None | int = None 34 | gamecode = None 35 | 36 | # priority of settings: command line > game-specific settings > data from ifdb > default settings > viola defaults 37 | 38 | command_settings = settings.gameset(priority=4) 39 | ifdb_settings = settings.gameset(priority=2) 40 | base_settings = settings.gameset(width=1024, 41 | height=768, 42 | terp_number=zcode.header.TERP_IBM, 43 | foreground='black', 44 | background='white' 45 | ) 46 | 47 | 48 | def checkgamefile(gamefile): 49 | gamefile.seek(0) 50 | gameid = gamefile.read(4) 51 | if gameid.decode('latin-1') == 'FORM': # The file is an IFF FORM file 52 | gamefile.seek(8) 53 | if gamefile.read(4).decode('latin-1') == 'IFRS': # The file is a Blorb resource file 54 | return 'blorb' 55 | else: 56 | return 'unknown' 57 | elif gameid.decode('latin-1') == 'GLUL': 58 | return 'glulx' 59 | elif 1 <= gameid[0] <= 8: 60 | return 'zcode' 61 | else: 62 | return 'unknown' 63 | 64 | 65 | class UnknownGameType(Exception): 66 | def __init__(self, value): 67 | self.value = value 68 | 69 | def __str__(self): 70 | return repr(self.value) 71 | 72 | 73 | class UnsupportedGameType(Exception): 74 | def __init__(self, value): 75 | self.value = value 76 | 77 | def __str__(self): 78 | return repr(self.value) 79 | 80 | 81 | def getgame(filename): 82 | global blorbs 83 | f = io.findfile(filename, gamefile=True) 84 | if not f: 85 | print("Error opening game file", file=sys.stderr) 86 | sys.exit() 87 | gamefile = open(f, 'rb') 88 | gamefile.seek(0) 89 | 90 | # check to see if it's actually a blorb file 91 | 92 | gametype = checkgamefile(gamefile) 93 | gamefile.seek(0) 94 | if gametype == 'blorb': 95 | blorbs = [blorb.blorb(blorb.blorb_chunk(gamefile.read()))] 96 | game = blorbs[0].getExec(0) 97 | elif gametype == 'unknown': 98 | raise UnknownGameType("Viola does not recognise the format of the game file.") 99 | else: 100 | game = gamefile.read() 101 | 102 | if game[:4] == 'GLUL': 103 | raise UnsupportedGameType("Viola does not support Glulx games at this time.") 104 | 105 | return game 106 | 107 | 108 | def handle_parameters(argv): # handles command line parameters 109 | global blorbfiles 110 | global height, width, title, transcriptfile, usespec, recordfile, playbackfile, gamecode 111 | global command_settings, ifdb_settings 112 | # viola [options] gamefile [resourcefile] 113 | if len(argv) <= 1: 114 | print('Syntax: viola [options] game-file [resource-file]\n' 115 | ' -d print debug messages\n' 116 | ' -w screen width\n' 117 | ' -h screen height\n' 118 | ' -T output transcript file\n' 119 | ' -t milliseconds between timer calls (default 100)\n' 120 | ' -R record input commands to file\n' 121 | ' -P playback input commands from file\n' 122 | ' -B force blorb file to work even if it does not match the game\n' 123 | ' -D download game information (title and author)' 124 | ) 125 | sys.exit() 126 | 127 | if len(argv) <= 1: 128 | return None 129 | 130 | args = getopt.getopt(argv[1:], 'BdDh:w:T:t:R:P:', ['zspec=']) 131 | 132 | options = args[0] 133 | args = args[1] 134 | transcriptfile = False 135 | recordfile = False 136 | playbackfile = False 137 | usespec = 3 138 | datagrab: bool = False 139 | 140 | for a in options: 141 | if a[0] == '-d': 142 | zcode.debug = True 143 | elif a[0] == '-h': 144 | command_settings.height = int(a[1]) 145 | elif a[0] == '-w': 146 | command_settings.width = int(a[1]) 147 | elif a[0] == '-T': 148 | transcriptfile = a[1] 149 | elif a[0] == '-t': 150 | io.timer_period = int(a[1]) 151 | elif a[0] == '-R': 152 | recordfile = a[1] 153 | elif a[0] == '-P': 154 | playbackfile = a[1] 155 | elif a[0] == '-B': 156 | blorb.forceblorb = True 157 | elif a[0] == '-D': 158 | datagrab = True 159 | 160 | elif a[0] == '--zspec': 161 | specversion = a[1] 162 | if specversion not in specs: 163 | print("The specification selected must be one of:") 164 | for a in specs: 165 | print(a) 166 | sys.exit() 167 | usespec = specs.index(specversion) 168 | 169 | if playbackfile and recordfile: 170 | print('Cannot record commands and playback commands at the same time (-P and -R).') 171 | sys.exit() 172 | 173 | gamedata = getgame(args[0]) 174 | gamecode = data.getcode(gamedata) 175 | if datagrab: 176 | ifid = data.getifid(gamedata) 177 | ifdb_page = data.getpage(ifid) 178 | if ifdb_page: 179 | command_settings.title = data.gettitle(ifdb_page) 180 | command_settings.author = data.getauthor(ifdb_page) 181 | 182 | for a in args[1:]: 183 | f = open(a, 'rb') 184 | blorb_data = f.read() 185 | f.close() 186 | blorbs.append(blorb.blorb(blorb.blorb_chunk(blorb_data))) 187 | 188 | return gamedata 189 | 190 | 191 | def setupmodules(gamefile): 192 | global terpnum, title, transcriptfile 193 | 194 | realForeground = zcode.screen.convertBasicToRealColour(foreground) 195 | realBackground = zcode.screen.convertBasicToRealColour(background) 196 | 197 | io.setup(width, height, blorbs, title, realForeground, realBackground) 198 | zcode.use_standard = usespec 199 | if not zcode.memory.setup(gamefile): 200 | return False 201 | 202 | # set up the various modules 203 | zcode.game.setup() 204 | zcode.routines.setup() 205 | zcode.screen.setup() 206 | zcode.input.setup(playbackfile) 207 | zcode.output.setup((False, True, transcriptfile, False, recordfile)) 208 | zcode.optables.setup() 209 | zcode.sounds.setup(blorbs) 210 | zcode.header.setup() 211 | zcode.objects.setup() 212 | zcode.text.setup(gamecode) 213 | if terpnum is not None: 214 | zcode.header.setterpnum(int(terpnum)) 215 | 216 | return True 217 | 218 | 219 | def rungame(gamedata): 220 | global height, width, title, terpnum, foreground, background 221 | settings.setup() 222 | defset = settings.getsettings() 223 | gameset = settings.getsettings(data.getcode(gamedata)) 224 | if not gameset: 225 | gameset = settings.getsettings(data.getifid(gamedata)) 226 | 227 | gameset = gameset.merge(defset) 228 | gameset = gameset.merge(ifdb_settings) 229 | gameset = gameset.merge(command_settings) 230 | gameset = gameset.merge(base_settings) 231 | 232 | height = gameset.height 233 | width = gameset.width 234 | 235 | if gameset.foreground and gameset.foreground.lower() in zcode.screen.colours: 236 | foreground = zcode.screen.colours[gameset.foreground.lower()] 237 | else: 238 | try: 239 | foreground = int(gameset.foreground) 240 | except TypeError: 241 | foreground = 2 242 | 243 | if gameset.background and gameset.background.lower() in zcode.screen.colours: 244 | background = zcode.screen.colours[gameset.background.lower()] 245 | else: 246 | try: 247 | background = int(gameset.background) 248 | except TypeError: 249 | background = 9 250 | 251 | if gameset.blorb is not None: 252 | blorbs.append(io.findfile(gameset.blorb)) 253 | 254 | for a in range(len(blorbs)): 255 | if not blorbs[a]: 256 | blorbs.pop(a) 257 | 258 | bwidth = 0 259 | bheight = 0 260 | for a in blorbs: 261 | bwidth, bheight = a.getWinSizes()[:2] 262 | 263 | if bwidth == 0: 264 | wrat = 1 265 | bwidth = width 266 | else: 267 | wrat = width / bwidth 268 | 269 | if bheight == 0: 270 | hrat = 1 271 | bheight = height 272 | else: 273 | hrat = height / bheight 274 | 275 | if wrat < hrat: 276 | rat = wrat 277 | else: 278 | rat = hrat 279 | 280 | width = round(bwidth * rat) 281 | height = round(bheight * rat) 282 | 283 | terpnum = gameset.terp_number 284 | 285 | # title, headline and author in settings file take precedence over information from blorb metadata 286 | 287 | try: 288 | iFiction = blorbs[0].getMetaData() 289 | except IndexError: 290 | iFiction = None 291 | 292 | if iFiction: 293 | blorb_settings = settings.gameset(priority=gameset.priority-1, title=babel.getTitle(iFiction), 294 | author=babel.getAuthor(iFiction), headline=babel.getHeadline(iFiction) 295 | ) 296 | gameset = gameset.merge(blorb_settings) 297 | 298 | title = gameset.title 299 | headline = gameset.headline 300 | author = gameset.author 301 | 302 | if title is None: 303 | title = '' 304 | if headline is not None: 305 | title = title + ' (' + headline + ')' 306 | if author is not None: 307 | title += ' by ' + author 308 | 309 | if title == '' or title is None: 310 | title = 'Viola' 311 | else: 312 | title = 'Viola - ' + title 313 | 314 | if not setupmodules(gamedata): 315 | zcode.error.fatal('Couldn\'t open gamefile ' + sys.argv[1]) 316 | 317 | zcode.routines.execstart() 318 | return 1 319 | 320 | 321 | if __name__ == '__main__': 322 | gamedata = handle_parameters(sys.argv) 323 | 324 | if zcode.profile: 325 | import cProfile 326 | import pstats 327 | 328 | with cProfile.Profile() as pr: 329 | rungame(gamedata) 330 | 331 | stats = pstats.Stats(pr) 332 | stats.sort_stats(pstats.SortKey.TIME) 333 | stats.dump_stats(filename='viola.prof') 334 | else: 335 | rungame(gamedata) 336 | -------------------------------------------------------------------------------- /zcode/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | 10 | __all__ = ["dictionary", 11 | "error", 12 | "game", 13 | "header", 14 | "input", 15 | "instructions", 16 | "memory", 17 | "numbers", 18 | "objects", 19 | "opcodes", 20 | "optables", 21 | "output", 22 | "routines", 23 | "screen", 24 | "sounds", 25 | "text", 26 | "constants" 27 | ] 28 | 29 | from zcode import * 30 | 31 | debug = False 32 | profile = False 33 | use_standard = 3 34 | -------------------------------------------------------------------------------- /zcode/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | STANDARD_00 = 0 16 | STANDARD_02 = 1 17 | STANDARD_10 = 2 18 | STANDARD_11 = 3 19 | 20 | specs = ['0', '0.2', '1.0', '1.1'] 21 | 22 | 23 | beyond_zork_codes = ["1.870412","1.870715","47.870915","49.870917","51.870923","57.871221","60.880610"] 24 | -------------------------------------------------------------------------------- /zcode/dictionary.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import re 16 | 17 | import zcode 18 | 19 | 20 | def importdict(address): 21 | numcodes = zcode.memory.getbyte(address) 22 | entrylength = zcode.memory.getbyte(address + numcodes + 1) 23 | numofentries = zcode.memory.getword(address + numcodes + 2) 24 | address += numcodes + 4 25 | entries = [] 26 | for a in range(numofentries): 27 | offset = address + (a * entrylength) 28 | if zcode.header.zversion < 4: 29 | entries.append(list(zcode.memory.getarray(offset, 4))) 30 | else: 31 | entries.append(list(zcode.memory.getarray(offset, 6))) 32 | return entries 33 | 34 | 35 | def getseperators(address): 36 | numcodes = zcode.memory.getbyte(address) 37 | seperators = [chr(a) for a in zcode.memory.getarray(address + 1, numcodes)] 38 | return seperators 39 | 40 | 41 | def splitinput(text, seperators): 42 | for count, value in enumerate(seperators): 43 | newvalue = value.center(3) 44 | text = text.replace(value, newvalue) 45 | text = text.split() 46 | return text 47 | 48 | 49 | def findword(word, dictionary): # attempts to find a word in a dictionary, returning the address if found, or 0 50 | if dictionary == 0: 51 | dictionary = zcode.header.dictionaryloc 52 | dictlist = importdict(dictionary) 53 | numcodes = zcode.memory.getbyte(dictionary) 54 | entrylength = zcode.memory.getbyte(dictionary + numcodes + 1) 55 | if dictlist.count(zcode.text.encodetext(word)) != 0: # found the word 56 | return (dictlist.index(zcode.text.encodetext(word)) * entrylength) + dictionary + numcodes + 4 57 | else: 58 | return 0 59 | 60 | 61 | def findstarts(textarray, seperators): 62 | if zcode.header.zversion < 5: 63 | offset = 1 64 | else: 65 | offset = 2 66 | s = '[' + ''.join(seperators) + ']| +' 67 | p = re.compile(s) 68 | 69 | wordstarts = [] 70 | for count, value in enumerate(textarray): 71 | if (textarray[count - 1]) == ' ' and (value != ' '): # if the previous character was a space and this one isn't 72 | wordstarts.append(count + offset) # this byte is the start of a new word 73 | 74 | elif seperators.count(value) != 0: # if the current character is a seperator 75 | wordstarts.append(count + offset) 76 | 77 | elif (count == 0) and (value != ' '): # if this is the first character, and it's not a space 78 | wordstarts.append(count + offset) 79 | 80 | elif seperators.count(textarray[count - 1]) != 0: # if the *previous* character is a seperator 81 | wordstarts.append(count + offset) 82 | 83 | return wordstarts 84 | 85 | 86 | def tokenise(intext, parseaddress=0, dictionary=0, flag=0): 87 | if zcode.header.zversion > 4 and parseaddress == 0: 88 | pass 89 | else: 90 | if dictionary == 0: # default dictionary 91 | dictionary = zcode.header.dictionaryloc 92 | seperators = getseperators(dictionary) 93 | words = splitinput(intext, seperators) 94 | maxparse = zcode.memory.getbyte(parseaddress) 95 | while len(words) > maxparse: 96 | words.pop() 97 | zcode.memory.setbyte(parseaddress + 1, len(words)) 98 | address = parseaddress + 2 99 | starts = findstarts(intext, seperators) 100 | for count, value in enumerate(words): 101 | dictloc = findword(value, dictionary) 102 | if dictloc == 0 and flag == 1: 103 | pass 104 | else: 105 | zcode.memory.setword(address, dictloc) 106 | zcode.memory.setbyte(address + 2, len(value)) 107 | zcode.memory.setbyte(address + 3, starts[count]) 108 | address += 4 109 | -------------------------------------------------------------------------------- /zcode/error.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import sys 16 | import zcode 17 | 18 | strictzlevel = 1 19 | 20 | errors = [] 21 | 22 | 23 | def fatal(message): 24 | try: 25 | for a in range(1, 3): 26 | zcode.output.streams[a].write('Fatal Error: ' + str(message)) 27 | except: 28 | pass 29 | print('Fatal Error:', message, file=sys.stderr) 30 | sys.exit() 31 | 32 | 33 | def strictz(message): 34 | global errors, strictzlevel 35 | if strictzlevel == 0: # ignore all levels 36 | pass 37 | elif strictzlevel == 1: # report first error 38 | if message not in errors: 39 | for a in range(1, 3): 40 | zcode.output.streams[a].write('Warning: ' + str(message) + ' (will ignore further occurences)\r') 41 | errors.append(message) 42 | elif strictzlevel == 2: # report all errors 43 | for a in range(1, 3): 44 | zcode.output.streams[a].write('Warning: ' + str(message) + '\r') 45 | else: # exit after any error 46 | for a in range(1, 3): 47 | zcode.output.streams[a].write('Fatal Error: ' + str(message) + '\r') 48 | zcode.routines.quit = 1 49 | 50 | 51 | def warning(message): 52 | for a in range(1, 3): 53 | zcode.output.streams[a].write('Warning: ' + str(message) + '\r') 54 | -------------------------------------------------------------------------------- /zcode/game.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import copy 16 | 17 | from ififf import quetzal 18 | import vio.zcode as io 19 | import zcode 20 | 21 | returning = False 22 | 23 | undolist = [] 24 | 25 | LARGEST_STACK = 0 26 | 27 | interruptstack = [] 28 | 29 | INT_INPUT = 1 30 | INT_NEWLINE = 2 31 | INT_SOUND = 3 32 | 33 | 34 | class interruptdata: 35 | def __init__(self, itype, routine): 36 | self.routine = routine 37 | self.type = itype 38 | 39 | routine = None 40 | type = 0 41 | 42 | 43 | def setup(): 44 | global PC, evalstack, callstack, lvars, retPC, numargs, interruptroutine, currentframe, timerroutine, timer 45 | PC = 0 46 | callstack = [] 47 | if zcode.memory.data[0] != 6: 48 | currentframe = frame() 49 | currentframe.lvars = [] 50 | currentframe.evalstack = [] 51 | 52 | interruptroutine = 0 53 | timerroutine = 0 54 | timer = None 55 | 56 | 57 | class frame: 58 | retPC = 0 59 | flags = 0 60 | varnum = 0 61 | numargs = 0 62 | evalstacksize = 0 63 | lvars = [] 64 | evalstack = [] 65 | interrupt = False 66 | 67 | 68 | class undoframe: 69 | memory = [] 70 | callstack = [] 71 | currentframe = 0 72 | PC = 0 73 | 74 | 75 | def save(): 76 | f = io.openfile(zcode.screen.currentWindow, 'w') 77 | sd = quetzal.qdata() 78 | sd.release = zcode.header.release 79 | sd.serial = zcode.header.serial 80 | sd.checksum = zcode.header.checksum 81 | sd.PC = PC 82 | sd.memory = zcode.memory.data[:zcode.header.statmembase] 83 | sd.omemory = zcode.memory.originaldata[:zcode.header.statmembase] 84 | sd.callstack = copy.deepcopy(callstack) 85 | sd.currentframe = copy.deepcopy(currentframe) 86 | return quetzal.save(f, sd) 87 | 88 | 89 | def save_undo(): 90 | undodata = undoframe() 91 | undodata.memory = zcode.memory.data[:] 92 | undodata.callstack = copy.deepcopy(callstack) 93 | undodata.currentframe = copy.deepcopy(currentframe) 94 | undodata.PC = PC 95 | undolist.append(undodata) 96 | return 1 97 | 98 | 99 | def restore(filename=None, prompt=1): 100 | global PC, callstack, currentframe, interruptstack 101 | zcode.sounds.stopall() 102 | bis = interruptstack[:] 103 | interruptstack = [] # clear the interrupt stack, or it may do weird things after the game is restored 104 | f = io.openfile(zcode.screen.currentWindow, 'r') 105 | sd = quetzal.qdata() 106 | sd.release = zcode.header.release 107 | sd.serial = zcode.header.serial 108 | sd.checksum = zcode.header.checksum 109 | sd.omemory = zcode.memory.originaldata[:zcode.header.statmembase] 110 | 111 | sd = quetzal.restore(f.read(), sd) 112 | if sd == False: 113 | interruptstack = bis[:] # if the restore failed, put the interrupt stack back how it was 114 | return 0 115 | preflags = zcode.memory.getword(zcode.header.FLAGS2_ADDRESS) & 3 116 | zcode.memory.setarray(0, sd.memory) 117 | postflags = zcode.memory.getword(zcode.header.FLAGS2_ADDRESS) & 0xfffc 118 | zcode.memory.setword(zcode.header.FLAGS2_ADDRESS, preflags + postflags) 119 | PC = sd.PC 120 | callstack = copy.deepcopy(sd.callstack) 121 | currentframe = copy.deepcopy(sd.currentframe) 122 | 123 | return 1 124 | 125 | 126 | def restore_undo(): 127 | global callstack, currentframe, PC 128 | if undolist == []: 129 | return 3 130 | undodata = undolist.pop() 131 | preflags = zcode.memory.getword(zcode.header.FLAGS2_ADDRESS) & 3 132 | zcode.memory.data = undodata.memory[:] 133 | postflags = zcode.memory.getword(zcode.header.FLAGS2_ADDRESS) & 0xfffc 134 | zcode.memory.setword(zcode.header.FLAGS2_ADDRESS, preflags + postflags) 135 | callstack = copy.deepcopy(undodata.callstack) 136 | currentframe = copy.deepcopy(undodata.currentframe) 137 | PC = undodata.PC 138 | 139 | return 2 140 | 141 | 142 | def getvar(varnum, indirect=False): 143 | if varnum == 0: 144 | return getstack(indirect) 145 | elif varnum < 0x10: 146 | return getlocal(varnum - 1) 147 | else: 148 | return getglobal(varnum - 0x10) 149 | 150 | 151 | def setvar(varnum, value, indirect=False): 152 | if varnum == 0: 153 | putstack(value, indirect) 154 | elif varnum < 0x10: 155 | setlocal(varnum - 1, value) 156 | else: 157 | setglobal(varnum - 0x10, value) 158 | 159 | 160 | def getstack(indirect=False): 161 | global currentframe 162 | try: 163 | if indirect == True: 164 | return currentframe.evalstack[-1] 165 | else: 166 | return currentframe.evalstack.pop() 167 | except: 168 | zcode.error.strictz("Tried to get a value from an empty stack.") 169 | return 0 170 | 171 | 172 | def putstack(value, indirect=False): 173 | global currentframe 174 | value = zcode.numbers.unsigned(value) 175 | if indirect: 176 | currentframe.evalstack[-1] = value 177 | else: 178 | currentframe.evalstack.append(value) 179 | 180 | 181 | def getlocal(varnum): 182 | global currentframe 183 | return currentframe.lvars[varnum] 184 | 185 | 186 | def getglobal(varnum): 187 | table = zcode.header.globalsloc 188 | return zcode.memory.getword(table + (varnum * zcode.memory.WORDSIZE)) 189 | 190 | 191 | def setlocal(varnum, value): 192 | global currentframe 193 | value = zcode.numbers.unsigned(value) 194 | currentframe.lvars[varnum] = value 195 | 196 | 197 | def setglobal(varnum, value): 198 | value = zcode.numbers.unsigned(value) 199 | table = zcode.header.globalsloc 200 | zcode.memory.setword(table + (varnum * zcode.memory.WORDSIZE), value) 201 | 202 | 203 | def interrupt_call(): 204 | global PC 205 | if len(interruptstack) > 0 and not returning and not zcode.routines.quit: # if there are calls on the interrupt stack 206 | oldPC = PC 207 | i = interruptstack.pop() 208 | if zcode.instructions.inputInstruction: 209 | PC = zcode.routines.oldpc 210 | address = i.routine 211 | call(address, [], 0, 1) 212 | zcode.routines.execloop() 213 | PC = oldPC 214 | 215 | 216 | def call(address, args, useret, introutine=0, initial=0): # initial is for the initial routine call by z6 games 217 | global LARGEST_STACK 218 | global callstack 219 | global currentframe 220 | global PC 221 | if len(callstack) > LARGEST_STACK: 222 | LARGEST_STACK = len(callstack) 223 | 224 | if address == 0: 225 | if useret == 1: 226 | zcode.instructions.store(0) 227 | else: 228 | # okay, there is always a current frame (except for the initial call in z6 games) 229 | # the current frame holds the current lvars and the current evalstack 230 | # when a new routine is called, the current frame is merely shoved onto the call stack 231 | # then a new frame is created 232 | # the new frame is given a fresh evalstack and lvars 233 | # then the flag for whether a value should be returned is set, and the return variable is set 234 | # then the return PC is set 235 | # then the lvars are setup and the new PC is set 236 | try: 237 | if currentframe.interrupt: 238 | introutine = 1 239 | except: 240 | pass 241 | if initial == 0: 242 | newframe = frame() 243 | newframe.lvars = currentframe.lvars[:] 244 | newframe.flags = currentframe.flags 245 | newframe.retPC = currentframe.retPC 246 | newframe.varnum = currentframe.varnum 247 | newframe.numargs = currentframe.numargs 248 | newframe.evalstack = currentframe.evalstack[:] 249 | newframe.evalstacksize = len(currentframe.evalstack) 250 | newframe.interrupt = currentframe.interrupt 251 | callstack.append(newframe) 252 | 253 | currentframe = frame() 254 | currentframe.evalstack = [] 255 | currentframe.lvars = [] 256 | 257 | if useret == 0: 258 | currentframe.flags = (1 << 4) 259 | currentframe.varnum = 0 260 | else: 261 | retvar = zcode.memory.getbyte(PC) 262 | PC += 1 263 | currentframe.flags = 0 264 | currentframe.varnum = retvar 265 | currentframe.retPC = PC 266 | currentframe.numargs = len(args) 267 | currentframe.evalstacksize = 0 268 | if initial == 1: 269 | currentframe.retPC = 0 270 | if introutine: 271 | currentframe.interrupt = True 272 | 273 | # then we find out where the new routine is 274 | address = zcode.memory.unpackaddress(address, 1) 275 | # then we set up said routine 276 | PC = zcode.routines.setuproutine(address) 277 | currentframe.flags += len(currentframe.lvars) 278 | if len(args) > len(currentframe.lvars): # now we throw away any arguments that won't fit 279 | args = args[:len(currentframe.lvars)] 280 | 281 | args = list(map(zcode.numbers.unsigned, args)) # make sure the numbers are unsigned 282 | 283 | currentframe.lvars[:len(args)] = args[:] # overlay the local variables with the arguments 284 | 285 | if zcode.debug: 286 | print(' [', end='') 287 | if currentframe.lvars != []: 288 | for a in currentframe.lvars[:-1]: 289 | print(a, end=', ') 290 | print(currentframe.lvars[-1], end=']') 291 | else: 292 | print(']', end='') 293 | 294 | 295 | def ret(value): 296 | global PC 297 | global retPC 298 | global numargs 299 | global callstack 300 | global evalstack 301 | global lvars 302 | global interruptroutine 303 | global currentframe 304 | global timer 305 | global timervalue 306 | PC = currentframe.retPC 307 | varnum = currentframe.varnum 308 | if currentframe.flags & 16 == 16: 309 | useret = 0 310 | else: 311 | useret = 1 312 | currentframe = callstack.pop() 313 | if useret == 1: 314 | setvar(varnum, value) 315 | 316 | if timer and currentframe.interrupt == False: 317 | timer = False 318 | zcode.routines.timerreturn = True 319 | if value != 0: 320 | timervalue = True 321 | io.stoptimer() 322 | # all right, we need to check here if the program has printed anything 323 | # not sure how, but anyway, if the program has printed, we reprint the 324 | # input text so far. If the program has not printed, we do not reprint 325 | # the input text. Yay. 326 | if zcode.output.streams[1].interruptprinted: 327 | inp = [chr(a) for a in zcode.input.instring] 328 | inp = ''.join(inp) 329 | inp = inp.lower() 330 | zcode.output.streams[1].write(inp) 331 | zcode.output.streams[1].interruptprinted = False 332 | zcode.screen.currentWindow.flushTextBuffer() 333 | 334 | 335 | timervalue = False 336 | 337 | 338 | # user stacks 339 | 340 | def pushuserstack(address, value): 341 | # I'm assuming that if the stack overflows, we don't branch, and don't push the value 342 | slots = zcode.memory.getword(address) 343 | if slots == 0: 344 | return 0 345 | else: 346 | zcode.memory.setword(address + (slots * zcode.memory.WORDSIZE), value) 347 | zcode.memory.setword(address, slots - 1) 348 | return 1 349 | 350 | 351 | def pulluserstack(address): 352 | slots = zcode.memory.getword(address) 353 | topvalue = slots + 1 354 | value = zcode.memory.getword(address + (topvalue * zcode.memory.WORDSIZE)) 355 | slots += 1 356 | zcode.memory.setword(address, slots) 357 | return value 358 | 359 | 360 | def popuserstack(address, items): 361 | slots = zcode.memory.getword(address) 362 | slots += items 363 | zcode.memory.setword(address, slots) 364 | 365 | 366 | def firetimer(): 367 | global timerreturned 368 | global timer 369 | 370 | zcode.screen.currentWindow.flushTextBuffer() 371 | zcode.screen.currentWindow.screen.update() 372 | timerreturned = 0 373 | timer = True 374 | i = interruptdata(INT_INPUT, timerroutine) 375 | interruptstack.append(i) 376 | interrupt_call() 377 | zcode.routines.timerreturn = False 378 | -------------------------------------------------------------------------------- /zcode/header.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | from zcode.constants import * 17 | 18 | standards = [(0,0), (0,2), (1,0), (1,1)] 19 | 20 | ZVERSION_ADDRESS = 0x0 21 | FLAGS1_ADDRESS = 0x1 22 | RELEASE_ADDRESS = 0x2 23 | HIGHMEM_ADDRESS = 0x4 24 | INITIALPC_ADDRESS = 0x6 25 | DICTIONARY_ADDRESS = 0x8 26 | OBJECTS_ADDRESS = 0xA 27 | GLOBALS_ADDRESS = 0xC 28 | STATICMEM_ADDRESS = 0xE 29 | FLAGS2_ADDRESS = 0x10 30 | SERIAL_ADDRESS = 0x12 31 | ABBREVS_ADDRESS = 0x18 32 | FILELEN_ADDRESS = 0x1A 33 | CHECKSUM_ADDRESS = 0x1C 34 | TERPNUM_ADDRESS = 0x1E 35 | TERPVER_ADDRESS = 0x1F 36 | SLINES_ADDRESS = 0x20 37 | SCHARS_ADDRESS = 0x21 38 | SWIDTH_ADDRESS = 0x22 39 | SHEIGHT_ADDRESS = 0x24 40 | FWIDTH_ADDRESS = 0x26 41 | FHEIGHT_ADDRESS = 0x27 42 | ROFFSET_ADDRESS = 0x28 43 | SOFFSET_ADDRESS = 0x2A 44 | BG_ADDRESS = 0x2C 45 | FG_ADDRESS = 0x2D 46 | TERMCHARS_ADDRESS = 0x2E 47 | TEXTWIDTH_ADDRESS = 0x30 48 | STANDARD_ADDRESS = 0x32 49 | ALPHATABLE_ADDRESS = 0x34 50 | HEADEREXT_ADDRESS = 0x36 51 | 52 | TERP_DEC = 1 # DECSystem-20 53 | TERP_APPLEE = 2 # Apple IIe 54 | TERP_MAC = 3 # Macintosh 55 | TERP_AMIGA = 4 # Amiga 56 | TERP_ATARI = 5 # Atari ST 57 | TERP_IBM = 6 # IBM PC 58 | TERP_C128 = 7 # Commodore 128 59 | TERP_C64 = 8 # Commodore 64 60 | TERP_APPLEC = 9 # Apple IIc 61 | TERP_APPLEGS = 10 # Apple IIgs 62 | TERP_TANDY = 11 # Tandy Color 63 | 64 | TERP_NUMBER = TERP_IBM 65 | 66 | 67 | def setup(): # set all the relevant bits and bytes and words in the header 68 | global zversion, release, highmembase, startat, dictionaryloc, objtableloc, globalsloc, statmembase, serial, abbrevtableloc 69 | global checksum, roffset, soffset, termcharloc, alphatableloc, headerextloc 70 | global FWIDTH_ADDRESS, FHEIGHT_ADDRESS 71 | 72 | statmembase = int.from_bytes(zcode.memory.data[STATICMEM_ADDRESS:STATICMEM_ADDRESS+zcode.memory.WORDSIZE], byteorder='big') 73 | 74 | 75 | zversion = zcode.memory.getbyte(0) 76 | release = zcode.memory.getword(RELEASE_ADDRESS) 77 | highmembase = zcode.memory.getword(HIGHMEM_ADDRESS) 78 | startat = zcode.memory.getword(INITIALPC_ADDRESS) 79 | dictionaryloc = zcode.memory.getword(DICTIONARY_ADDRESS) 80 | objtableloc = zcode.memory.getword(OBJECTS_ADDRESS) 81 | globalsloc = zcode.memory.getword(GLOBALS_ADDRESS) 82 | 83 | serial = ''.join([chr(c) for c in zcode.memory.getarray(SERIAL_ADDRESS, 6)]) 84 | abbrevtableloc = zcode.memory.getword(ABBREVS_ADDRESS) 85 | 86 | # filelen is set and used in the memory module 87 | 88 | checksum = zcode.memory.getword(CHECKSUM_ADDRESS) 89 | 90 | if zversion == 6: 91 | roffset = zcode.memory.getword(ROFFSET_ADDRESS) * 8 92 | soffset = zcode.memory.getword(SOFFSET_ADDRESS) * 8 93 | 94 | termcharloc = zcode.memory.getword(TERMCHARS_ADDRESS) 95 | alphatableloc = zcode.memory.getword(ALPHATABLE_ADDRESS) 96 | headerextloc = zcode.memory.getword(HEADEREXT_ADDRESS) 97 | 98 | if zversion == 6: 99 | FWIDTH_ADDRESS = 0x27 100 | FHEIGHT_ADDRESS = 0x26 101 | 102 | # Flags 1 103 | if zversion == 3: 104 | setflag(1, 4, 0) # status line is available 105 | setflag(1, 5, 1) # screen splitting is available 106 | setflag(1, 6, 0) # The default font is not fixed-pitch 107 | elif zversion < 9: 108 | setflag(1, 2, zcode.screen.supportedstyles(2)) # Boldface 109 | setflag(1, 3, zcode.screen.supportedstyles(4)) # Italic 110 | setflag(1, 4, zcode.screen.supportedstyles(8)) # Fixed-pitch style 111 | if zcode.use_standard >= STANDARD_02: # from 0.2 onward 112 | setflag(1, 7, 1) # Timed input 113 | if zversion > 4: 114 | setflag(1, 0, zcode.screen.supportedgraphics(0)) # Colours 115 | if zversion == 6: 116 | setflag(1, 1, zcode.screen.supportedgraphics(3)) # Picture displaying 117 | if zcode.sounds.availablechannels(0) + zcode.sounds.availablechannels(1) > 0: # if any effect or music channels are available, sound effects are available 118 | setflag(1, 5, 1) # sound effects 119 | else: 120 | setflag(1, 5, 0) 121 | 122 | # Flags 2 - If unset by the game, the terp should leave them like that. 123 | if zversion > 4: 124 | if getflag(2, 3): # pictures 125 | setflag(2, 3, zcode.screen.supportedgraphics(3)) 126 | if getflag(2, 4): # undo 127 | setflag(2, 4, 1) 128 | if getflag(2, 5): # mouse 129 | setflag(2, 5, 1) 130 | if getflag(2, 6): # colours 131 | setflag(2, 6, zcode.screen.supportedgraphics(0)) 132 | if getflag(2, 7): # sound effects 133 | if zcode.screen.supportedgraphics(0) + zcode.screen.supportedgraphics(1) > 0: 134 | setflag(2, 7, 1) 135 | else: 136 | setflag(2, 7, 0) 137 | if getflag(2, 8): # menus 138 | setflag(2, 8, 0) 139 | 140 | # Flags 3 - If unset by the game, the terp should leave them like that. All unknown bits should be set to 0. 141 | if zversion > 4: 142 | if getflag(3, 0): # transparency 143 | setflag(3, 0, zcode.screen.supportedgraphics(2)) 144 | for a in range(1, 16): # set all the other bits to 0, because we don't know what they do 145 | setflag(3, a, 0) 146 | 147 | if zversion > 3: 148 | # Interpreter number 149 | setterpnum(TERP_NUMBER) 150 | # Interpreter version 151 | setterpversion(ord('V')) 152 | 153 | updateSizes() 154 | 155 | if zversion > 4: 156 | # Default foreground colour 157 | setdeffgcolour(zcode.screen.DEFFOREGROUND) 158 | # Default background colour 159 | setdefbgcolour(zcode.screen.DEFBACKGROUND) 160 | settruedefaultforeground(zcode.screen.spectrum[2]) 161 | settruedefaultbackground(zcode.screen.spectrum[9]) 162 | # Z-machine Standard number 163 | m = standards[zcode.use_standard][0] 164 | n = standards[zcode.use_standard][1] 165 | setstandardnum(m, n) 166 | 167 | 168 | def updateFontSize(): 169 | if zversion > 4: 170 | # Font width 171 | if zcode.screen.graphics_mode == 1: 172 | setfontwidth(zcode.screen.currentWindow.getFont().getWidth()) 173 | else: 174 | setfontwidth(1) 175 | # Font height 176 | if zcode.screen.graphics_mode == 1: 177 | setfontheight(zcode.screen.currentWindow.getFont().getHeight()) 178 | else: 179 | setfontheight(1) 180 | 181 | 182 | def updateSizes(): 183 | if zversion > 3: 184 | columns = int(zcode.screen.ioScreen.getWidth() // zcode.screen.getWindow(1).getFont().getWidth()) 185 | # Screen height (lines) 186 | setscreenheightlines(int(zcode.screen.ioScreen.getHeight() // zcode.screen.getWindow(1).getFont().getHeight())) 187 | # Screen width (chars) 188 | setscreenwidthchars(columns) 189 | 190 | if zversion > 4: 191 | # Screen width (units) 192 | if zcode.screen.graphics_mode == 1: 193 | setscreenwidth(zcode.screen.ioScreen.getWidth()) 194 | else: 195 | setscreenwidth(columns) 196 | # Screen height (units) 197 | if zcode.screen.graphics_mode == 1: 198 | setscreenheight(zcode.screen.ioScreen.getHeight()) 199 | else: 200 | setscreenheight(int(zcode.screen.ioScreen.getHeight() // zcode.screen.getWindow(1).getFont().getHeight())) 201 | updateFontSize() 202 | 203 | 204 | def identifier(): 205 | return str(release) + "." + serial 206 | 207 | 208 | def getflag(bitmap, 209 | bit): # bitmap is the set of flags to look in, such as flags 1, bit is the bit number to check, such as bit 1 for z3 status line type checking 210 | if bitmap == 1: 211 | flag = pow(2, bit) 212 | if zcode.memory.getbyte(FLAGS1_ADDRESS) & flag == flag: 213 | return 1 214 | else: 215 | return 0 216 | elif bitmap == 2: 217 | flag = pow(2, bit) 218 | if zcode.memory.getword(FLAGS2_ADDRESS) & flag == flag: 219 | return 1 220 | else: 221 | return 0 222 | elif bitmap == 3: 223 | if headerextsize() < 4: 224 | return 0 225 | else: 226 | flag = pow(2, bit) 227 | if zcode.memory.getword(headerextloc() + 4) & flag == flag: 228 | return 1 229 | else: 230 | return 0 231 | 232 | 233 | def setflag(bitmap, bit, value): 234 | # bitmap is the set of flags to look in, bit is the bit number to change, value is either 1 for on or 0 for off 235 | global flags1, flags2 236 | if bitmap == 1: 237 | flag = pow(2, bit) 238 | if value: 239 | zcode.memory.setbyte(FLAGS1_ADDRESS, zcode.memory.getbyte(FLAGS1_ADDRESS) | flag) 240 | else: 241 | zcode.memory.setbyte(FLAGS1_ADDRESS, zcode.memory.getbyte(FLAGS1_ADDRESS) & ~flag) 242 | elif bitmap == 2: 243 | flag = pow(2, bit) 244 | if value: 245 | zcode.memory.setword(FLAGS2_ADDRESS, zcode.memory.getword(FLAGS2_ADDRESS) | flag) 246 | else: 247 | zcode.memory.setword(FLAGS2_ADDRESS, zcode.memory.getword(FLAGS2_ADDRESS) & ~flag) 248 | elif bitmap == 3: 249 | if headerextsize() >= 4: 250 | flag = pow(2, bit) 251 | if value: 252 | zcode.memory.setword(headerextloc() + 8, zcode.memory.getword(headerextloc() + 8) | flag) 253 | else: 254 | zcode.memory.setword(headerextloc() + 8, zcode.memory.getword(headerextloc() + 8) & ~flag) 255 | 256 | 257 | def setterpnum(number): 258 | zcode.memory.setbyte(TERPNUM_ADDRESS, number) 259 | 260 | 261 | def setterpversion(number): 262 | zcode.memory.setbyte(TERPVER_ADDRESS, number) 263 | 264 | 265 | def getterpnum(): 266 | return zcode.memory.getbyte(TERPNUM_ADDRESS) 267 | 268 | 269 | def getterpversion(): # I doubt this will be needed 270 | return zcode.memory.getbyte(TERPVER_ADDRESS) 271 | 272 | 273 | def getscreenheightlines(): 274 | return zcode.memory.getbyte(SLINES_ADDRESS) 275 | 276 | 277 | def getscreenwidthchars(): 278 | return zcode.memory.getbyte(SCHARS_ADDRESS) 279 | 280 | 281 | def getscreenwidth(): # screen width in units 282 | return zcode.memory.getword(SWIDTH_ADDRESS) 283 | 284 | 285 | def getscreenheight(): # screen height in units 286 | return zcode.memory.getword(SHEIGHT_ADDRESS) 287 | 288 | 289 | def getfontwidth(): 290 | return zcode.memory.getbyte(FWIDTH_ADDRESS) 291 | 292 | 293 | def getfontheight(): 294 | return zcode.memory.getbyte(FHEIGHT_ADDRESS) 295 | 296 | 297 | def setscreenheightlines(lines): 298 | zcode.memory.setbyte(SLINES_ADDRESS, lines) 299 | 300 | 301 | def setscreenwidthchars(chars): 302 | zcode.memory.setbyte(SCHARS_ADDRESS, chars) 303 | 304 | 305 | def setscreenwidth(units): # screen width in units 306 | zcode.memory.setword(SWIDTH_ADDRESS, units) 307 | 308 | 309 | def setscreenheight(units): # screen height in units 310 | zcode.memory.setword(SHEIGHT_ADDRESS, units) 311 | 312 | 313 | def setfontwidth(units): 314 | zcode.memory.setbyte(FWIDTH_ADDRESS, units) 315 | 316 | 317 | def setfontheight(units): 318 | zcode.memory.setbyte(FHEIGHT_ADDRESS, units) 319 | 320 | 321 | def setdefbgcolour(colour): 322 | zcode.memory.setbyte(BG_ADDRESS, colour) 323 | 324 | 325 | def setdeffgcolour(colour): 326 | zcode.memory.setbyte(FG_ADDRESS, colour) 327 | 328 | 329 | def getdefbgcolour(): 330 | return zcode.memory.getbyte(BG_ADDRESS) 331 | 332 | 333 | def getdeffgcolour(): 334 | return zcode.memory.getbyte(FG_ADDRESS) 335 | 336 | 337 | def settextwidth(len): # total width in units of text sent to output stream 3 338 | zcode.memory.setword(TEXTWIDTH_ADDRESS, len) 339 | 340 | 341 | def setstandardnum(n, m): # for standard 1.0, setstandardnum(1, 0) would be sent. 342 | zcode.memory.setbyte(STANDARD_ADDRESS, n) 343 | zcode.memory.setbyte(STANDARD_ADDRESS + 1, m) 344 | 345 | 346 | def getstandardnum(): 347 | n = zcode.memory.getbyte(STANDARD_ADDRESS) 348 | m = zcode.memory.getbyte(STANDARD_ADDRESS + 1) 349 | return (n, m) 350 | 351 | 352 | # header extension stuff 353 | 354 | header_extension_size = None 355 | 356 | 357 | def headerextsize(): 358 | global header_extension_size 359 | if header_extension_size == None: 360 | if headerextloc == 0: 361 | header_extension_size = 0 362 | else: 363 | header_extension_size = zcode.memory.getword(headerextloc) 364 | return header_extension_size 365 | 366 | 367 | def setHeaderExtWord(word, value): 368 | if headerextsize() < word: 369 | return 0 370 | zcode.memory.setword(headerextloc() + (word * 2), value) 371 | 372 | 373 | def getHeaderExtWord(word): 374 | if headerextsize() < word: 375 | return 0 376 | else: 377 | return zcode.memory.getword(headerextloc + (word * 2)) 378 | 379 | 380 | def setmousex(xpos): 381 | setHeaderExtWord(1, xpos) 382 | 383 | 384 | def setmousey(ypos): 385 | setHeaderExtWord(2, ypos) 386 | 387 | 388 | unicode_table = None 389 | 390 | 391 | def unicodetableloc(): 392 | global unicode_table 393 | if unicode_table == None: 394 | unicode_table = getHeaderExtWord(3) 395 | return unicode_table 396 | 397 | 398 | # Standard 1.1 stuff below 399 | 400 | def settruedefaultforeground(colour): 401 | setHeaderExtWord(5, colour) 402 | 403 | 404 | def settruedefaultbackground(colour): 405 | setHeaderExtWord(6, colour) 406 | 407 | 408 | def gettruedefaultforeground(): 409 | return getHeaderExtWord(5) 410 | 411 | 412 | def gettruedefaultbackground(): 413 | return getHeaderExtWord(6) 414 | 415 | -------------------------------------------------------------------------------- /zcode/input.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import vio.zcode as io 16 | import zcode 17 | 18 | stream = 0 19 | file_commands = [] 20 | instring = [] 21 | 22 | command_history = [] 23 | chplace = -1 24 | streamfile = None 25 | 26 | 27 | def setup(playbackfile=False): 28 | global ioInput 29 | global mouse 30 | mouse = mouseTracker() 31 | ioInput = io.input(zcode.screen.ioScreen) 32 | if playbackfile: 33 | setStream(1, playbackfile) 34 | 35 | 36 | def setStream(number, filename=None): 37 | global stream 38 | global filecommands 39 | global streamfile 40 | stream = number 41 | if number == 0 and streamfile: 42 | streamfile = None 43 | elif number == 1: 44 | prompt = False 45 | if not filename: 46 | filename = 'COMMANDS.REC' 47 | prompt = True 48 | streamfile = readFile(-1, filename=filename, prompt=prompt).decode('utf-8') 49 | if streamfile: 50 | filecommands = streamfile.split('\n') 51 | while filecommands[-1].strip() == '': 52 | filecommands.pop() 53 | filecommands.reverse() 54 | stream = 1 55 | 56 | 57 | def getTerminatingCharacters(): 58 | if zcode.header.zversion < 5: 59 | return [] 60 | location = zcode.header.termcharloc 61 | chars = [] 62 | x = 1 63 | while x != 0: 64 | x = zcode.memory.getbyte(location) 65 | location += 1 66 | chars.append(x) 67 | chars.pop() 68 | if chars.count(255) != 0: # if 255 is one of the terminating characters, make every 'function character' terminating 69 | chars = [] 70 | for a in range(129, 155): 71 | chars.append(a) 72 | for a in range(252, 255): 73 | chars.append(a) 74 | return chars 75 | 76 | 77 | class mouseTracker: 78 | xpos = 1 79 | ypos = 1 80 | buttons = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 81 | 82 | 83 | def convertInputToZSCII(char): 84 | if char == 273: 85 | return 129 86 | if char == 274: 87 | return 130 88 | if char == 275: 89 | return 132 90 | if char == 276: 91 | return 131 92 | if char >= 282 and char <= 293: 93 | char -= 149 94 | return char 95 | if char >= 256 and char <= 265: 96 | char -= 111 97 | return char 98 | if char in zcode.text.reverseunitable: 99 | char = zcode.text.reverseunitable[char] 100 | return char 101 | return None 102 | 103 | 104 | def getInput(display=True, ignore=False, chistory=True): 105 | global mouse 106 | global stream 107 | global instring 108 | global chplace 109 | termchar = False 110 | if zcode.routines.quit: 111 | return None 112 | zcode.game.interrupt_call() 113 | if stream == 0: 114 | input = ioInput.getinput() 115 | zcode.screen.currentWindow.hideCursor() 116 | if ignore: 117 | if isinstance(input, io.keypress): 118 | return input.value 119 | return None 120 | zsciivalue = None 121 | 122 | if isinstance(input, io.keypress): 123 | if chistory and input.value == 273: # pressed up key 124 | if chplace < len(command_history) - 1: 125 | chplace += 1 126 | 127 | inp = [chr(a) for a in zcode.input.instring] 128 | inp = ''.join(inp) 129 | w = zcode.screen.currentWindow.getStringLength(inp) 130 | h = zcode.screen.currentWindow.getStringHeight(inp) 131 | x = zcode.screen.currentWindow.getCursor()[0] - w 132 | y = zcode.screen.currentWindow.getCursor()[1] 133 | zcode.screen.currentWindow.eraseArea(x, y, w, h) 134 | zcode.screen.currentWindow.setCursor(x, y) 135 | 136 | instring = command_history[chplace] 137 | for c in instring: 138 | zcode.output.streams[1].write(chr(c)) 139 | zcode.screen.currentWindow.flushTextBuffer() 140 | if zcode.screen.cursor: 141 | zcode.screen.currentWindow.showCursor() 142 | return None 143 | if chistory and input.value == 274: # pressed down key 144 | if chplace >= 0: 145 | if chplace >= 0: 146 | chplace -= 1 147 | newstring = command_history[chplace] 148 | if chplace == -1: 149 | newstring = [] 150 | 151 | inp = [chr(a) for a in zcode.input.instring] 152 | inp = ''.join(inp) 153 | w = zcode.screen.currentWindow.getStringLength(inp) 154 | h = zcode.screen.currentWindow.getStringHeight(inp) 155 | x = zcode.screen.currentWindow.getCursor()[0] - w 156 | y = zcode.screen.currentWindow.getCursor()[1] 157 | zcode.screen.currentWindow.eraseArea(x, y, w, h) 158 | zcode.screen.currentWindow.setCursor(x, y) 159 | 160 | instring = newstring 161 | for c in instring: 162 | zcode.output.streams[1].write(chr(c)) 163 | zcode.screen.currentWindow.flushTextBuffer() 164 | if zcode.screen.cursor: 165 | zcode.screen.currentWindow.showCursor() 166 | return None 167 | 168 | if len(input.character) == 1: 169 | zsciivalue = ord(input.character) 170 | else: 171 | zsciivalue = input.value 172 | if zsciivalue > 126: 173 | zsciivalue = convertInputToZSCII(zsciivalue) 174 | 175 | if isinstance(input, io.mousedown): 176 | if input.button != None: 177 | mouse.buttons[input.button] = 1 178 | zsciivalue = 254 # mouse down == single click 179 | zcode.header.setmousex(mouse.xpos) 180 | zcode.header.setmousey(mouse.ypos) 181 | 182 | if isinstance(input, io.mouseup): 183 | if input.button != None: 184 | mouse.buttons[input.button] = 0 185 | 186 | if isinstance(input, io.mousemove): 187 | mouse.xpos = zcode.screen.pix2units(input.xpos, horizontal=True, coord=True) 188 | mouse.ypos = zcode.screen.pix2units(input.ypos, horizontal=False, coord=True) 189 | 190 | if isinstance(input, io.keypress): 191 | if zsciivalue in zcode.text.inputvalues: 192 | if zsciivalue not in getTerminatingCharacters() and display and zsciivalue in zcode.text.outputvalues: 193 | if zsciivalue == 13: 194 | zcode.screen.currentWindow.hideCursor() 195 | 196 | zcode.output.streams[1].write(zcode.text.getZSCIIchar(zsciivalue)) 197 | 198 | zcode.screen.currentWindow.flushTextBuffer() 199 | #if zcode.header.zversion != 6: 200 | # zcode.output.streams[2].write(zcode.text.getZSCIIchar(zsciivalue)) 201 | 202 | else: 203 | if zcode.screen.cursor: 204 | zcode.screen.currentWindow.showCursor() 205 | return None 206 | zcode.screen.currentWindow.showCursor() 207 | return zsciivalue 208 | else: 209 | currentcommand = filecommands.pop() 210 | if len(currentcommand) > 0: 211 | c = currentcommand[0] 212 | if len(c) == 1: 213 | zsciivalue = ord(c) 214 | else: 215 | zsciivalue = convertInputToZSCII(c) 216 | currentcommand = currentcommand[1:] 217 | filecommands.append(currentcommand) 218 | 219 | if zsciivalue not in getTerminatingCharacters() and display: 220 | zcode.output.streams[1].write(chr(zsciivalue)) 221 | return zsciivalue 222 | else: 223 | if len(filecommands) == 0: 224 | stream = 0 225 | zcode.output.streams[1].write('\r') 226 | return 13 227 | 228 | 229 | def readFile(length, filename=None, prompt=False, seek=0): 230 | f = io.openfile(zcode.screen.currentWindow, 'r', filename, prompt) 231 | if f == None: 232 | return False 233 | f.seek(seek) 234 | if length == -1: 235 | data = f.read() 236 | else: 237 | data = f.read(length) 238 | f.close() 239 | return data 240 | -------------------------------------------------------------------------------- /zcode/instructions.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | 17 | operands = [] 18 | 19 | instructions = {} 20 | branches = {} 21 | 22 | 23 | def decode(address): 24 | global operands 25 | global instructions 26 | inaddress = address 27 | if zcode.debug: 28 | print(hex(address), end=' ') 29 | operands = [] 30 | 31 | if address in instructions: 32 | operands = instructions[address]['operands'] 33 | for a in operands: 34 | if a['type'] == 2: # variable 35 | varnum = a['varnum'] 36 | a['value'] = zcode.game.getvar(varnum) 37 | 38 | return instructions[address]['raddress'] 39 | optype = zcode.memory.getbyte(address) 40 | if optype < 0x20: # long 2OP, small constant, small constant 41 | op1 = {'type': 1, 'varnum': None, 'value': zcode.memory.getbyte(address + 1)} 42 | op2 = {'type': 1, 'varnum': None, 'value': zcode.memory.getbyte(address + 2)} 43 | operands.extend([op1, op2]) 44 | address += 3 45 | elif optype < 0x40: # long 2OP, small constant, variable 46 | op1 = {'type': 1, 'varnum': None, 'value': zcode.memory.getbyte(address + 1)} 47 | varnum = zcode.memory.getbyte(address + 2) 48 | op2 = {'type': 2, 'varnum': varnum, 'value': zcode.game.getvar(varnum)} 49 | operands.extend([op1, op2]) 50 | address += 3 51 | elif optype < 0x60: # long 2OP, variable, small constant 52 | varnum = zcode.memory.getbyte(address + 1) 53 | op1 = {'type': 2, 'varnum': varnum, 'value': zcode.game.getvar(varnum)} 54 | op2 = {'type': 1, 'varnum': None, 'value': zcode.memory.getbyte(address + 2)} 55 | operands.extend([op1, op2]) 56 | address += 3 57 | elif optype < 0x80: # long 2OP, variable, variable 58 | varnum1 = zcode.memory.getbyte(address + 1) 59 | varnum2 = zcode.memory.getbyte(address + 2) 60 | op1 = {'type': 2, 'varnum': varnum1, 'value': zcode.game.getvar(varnum1)} 61 | op2 = {'type': 2, 'varnum': varnum2, 'value': zcode.game.getvar(varnum2)} 62 | operands.extend([op1, op2]) 63 | address += 3 64 | elif optype < 0x90: # short 1OP, large constant 65 | op1 = {'type': 0, 'varnum': None, 'value': zcode.memory.getword(address + 1)} 66 | operands.append(op1) 67 | address += 3 68 | elif optype < 0xA0: # short 1OP, small constant 69 | op1 = {'type': 1, 'varnum': None, 'value': zcode.memory.getbyte(address + 1)} 70 | operands.append(op1) 71 | address += 2 72 | elif optype < 0xB0: # short 1OP, variable 73 | varnum = zcode.memory.getbyte(address + 1) 74 | op1 = {'type': 2, 'varnum': varnum, 'value': zcode.game.getvar(varnum)} 75 | operands.append(op1) 76 | address += 2 77 | elif optype < 0xC0: # short 0OP 78 | address += 1 79 | elif optype < 0xE0: # variable 2OP 80 | address += 1 81 | operandtypes = zcode.memory.getbyte(address) 82 | for x in range(6, -2, -2): 83 | otype = (operandtypes >> x) & 3 84 | if otype == 0: # large constant 85 | address += 1 86 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getword(address)}) 87 | address += 1 88 | elif otype == 1: # small constant 89 | address += 1 90 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getbyte(address)}) 91 | elif otype == 2: # variable 92 | address += 1 93 | varnum = zcode.memory.getbyte(address) 94 | operands.append({'type': otype, 'varnum': varnum, 'value': zcode.game.getvar(varnum)}) 95 | else: 96 | pass 97 | address += 1 98 | elif optype < 0x100: # variable VAR 99 | address += 1 100 | if (optype & 0x1f == 12) or (optype & 0x1f == 26): # if the opcode is call_vs2 or call_vn2, there can be up to 8 operands 101 | operandtypes = zcode.memory.getword(address) 102 | address += 1 103 | for x in range(14, -2, -2): 104 | otype = (operandtypes >> x) & 3 105 | if otype == 0: # large constant 106 | address += 1 107 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getword(address)}) 108 | address += 1 109 | elif otype == 1: # small constant 110 | address += 1 111 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getbyte(address)}) 112 | elif otype == 2: # variable 113 | address += 1 114 | varnum = zcode.memory.getbyte(address) 115 | operands.append({'type': otype, 'varnum': varnum, 'value': zcode.game.getvar(varnum)}) 116 | else: 117 | pass 118 | else: 119 | operandtypes = zcode.memory.getbyte(address) 120 | for x in range(6, -2, -2): 121 | otype = (operandtypes >> x) & 3 122 | if otype == 0: # large constant 123 | address += 1 124 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getword(address)}) 125 | address += 1 126 | elif otype == 1: # small constant 127 | address += 1 128 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getbyte(address)}) 129 | elif otype == 2: # variable 130 | address += 1 131 | varnum = zcode.memory.getbyte(address) 132 | operands.append({'type': otype, 'varnum': varnum, 'value': zcode.game.getvar(varnum)}) 133 | else: 134 | pass 135 | address += 1 136 | if address > zcode.header.statmembase: 137 | instructions[inaddress] = {'operands': operands, 'raddress': address} 138 | return address 139 | 140 | 141 | def decodeextended(address): 142 | global operands, instructions 143 | if address in instructions: 144 | operands = instructions[address]['operands'] 145 | for a in operands: 146 | if a['type'] == 2: # variable 147 | varnum = a['varnum'] 148 | a['value'] = zcode.game.getvar(varnum) 149 | return instructions[address]['raddress'] 150 | 151 | inaddress = address 152 | address += 1 153 | operandtypes = zcode.memory.getbyte(address) 154 | for x in range(6, -2, -2): 155 | otype = (operandtypes >> x) & 3 156 | if otype == 0: # large constant 157 | address += 1 158 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getword(address)}) 159 | address += 1 160 | elif otype == 1: # small constant 161 | address += 1 162 | operands.append({'type': otype, 'varnum': None, 'value': zcode.memory.getbyte(address)}) 163 | elif otype == 2: # variable 164 | address += 1 165 | varnum = zcode.memory.getbyte(address) 166 | operands.append({'type': otype, 'varnum': varnum, 'value': zcode.game.getvar(varnum)}) 167 | else: 168 | pass 169 | address += 1 170 | if address > zcode.header.statmembase: 171 | instructions[inaddress] = {'operands': operands, 'raddress': address} 172 | return address 173 | 174 | 175 | inputInstruction = False 176 | 177 | 178 | def printoperands(): 179 | for op in operands: 180 | if op['type'] == 2: 181 | if op['varnum'] == 0: 182 | print('stack(', end='') 183 | elif op['varnum'] < 0x10: 184 | print('l' + str(op['varnum'] - 1), end='(') 185 | else: 186 | print('g' + str(op['varnum'] - 0x10), end='(') 187 | print(op['value'], end=') ') 188 | elif op['type'] == 1: 189 | print('#', end='') 190 | print(f"{op['value']:02d}", end=' ') 191 | elif op['type'] == 0: 192 | print('#', end='') 193 | print(f"{op['value']:04d}", end=' ') 194 | 195 | 196 | def runops(address): 197 | global inputInstruction, instructions 198 | if address in instructions: 199 | try: 200 | optable = instructions[address]['optable'] 201 | opcode_num = instructions[address]['opcode'] 202 | opcode = optable[opcode_num] 203 | if zcode.debug: 204 | if not opcode == zcode.opcodes.z_extended: 205 | print(opcode.__name__.replace('z_', '@'), end=' ') 206 | printoperands() 207 | opcode() 208 | if zcode.debug: 209 | print() 210 | return None 211 | except: 212 | pass 213 | 214 | optype = zcode.memory.getbyte(address) 215 | optable = None 216 | mask = 0x1f 217 | if optype < 0x80: 218 | optable = zcode.optables.op2 219 | elif optype < 0xb0: 220 | optable = zcode.optables.op1 221 | mask = 0xf 222 | elif optype < 0xc0: 223 | optable = zcode.optables.op0 224 | mask = 0xf 225 | elif optype < 0xe0: 226 | optable = zcode.optables.op2 227 | elif optype < 0x100: 228 | if optype & 0x1f == 0x4 or optype & 0x1f == 0x16: 229 | inputInstruction = True 230 | else: 231 | inputInstruction = False 232 | optable = zcode.optables.opvar 233 | opcode_num = optype & mask 234 | opcode = optable[opcode_num] 235 | if zcode.debug: 236 | if not opcode == zcode.opcodes.z_extended: 237 | print(opcode.__name__.replace('z_', '@'), end=' ') 238 | printoperands() 239 | 240 | if address > zcode.header.statmembase: 241 | instructions[address]['optable'] = optable 242 | instructions[address]['opcode'] = opcode_num 243 | 244 | opcode() 245 | if zcode.debug: 246 | print() 247 | 248 | 249 | def store(value): 250 | value = zcode.numbers.unsigned(value) 251 | var = zcode.memory.getbyte(zcode.game.PC) 252 | zcode.game.PC += 1 253 | zcode.game.setvar(var, value) 254 | if zcode.debug: 255 | if var == 0: 256 | print('-> stack =', zcode.game.currentframe.evalstack, end=' ') 257 | else: 258 | if var < 0x10: 259 | varname = 'local' + str(var - 1) 260 | else: 261 | varname = 'global' + str(var - 0x10) 262 | print('->', varname, '=', value, end=' ') 263 | 264 | 265 | def branch(condition): 266 | global branches 267 | if zcode.debug: 268 | print('?', end='') 269 | inaddress = zcode.game.PC 270 | if inaddress in branches: 271 | bdata = branches[inaddress] 272 | offset = bdata['offset'] 273 | byte1 = bdata['mode'] 274 | zcode.game.PC = bdata['branchfrom'] 275 | else: 276 | byte1 = zcode.memory.getbyte(zcode.game.PC) 277 | zcode.game.PC += 1 278 | if byte1 & 64: # if bit 6 is set, branch information only occupies 1 byte 279 | offset = byte1 & 63 280 | else: # if bit 6 is clear, branch information occupies 2 bytes 281 | byte2 = zcode.memory.getbyte(zcode.game.PC) 282 | offset = ((byte1 & 63) << 8) + byte2 283 | zcode.game.PC += 1 284 | 285 | # the offset is a 14-bit signed number, so we have to convert it a bit. 286 | 287 | offset -= ((offset & 0x2000) * 2) 288 | 289 | if zcode.debug and not byte1 & 128: 290 | print('~', end='') 291 | 292 | # if the top bit is set, branch on true 293 | # if it isn't set, branch on false 294 | dobranch = (byte1 & 128 == condition << 7) 295 | 296 | if zcode.debug: 297 | if offset == 0: 298 | print('rfalse', end=' ') 299 | elif offset == 1: 300 | print('rtrue', end=' ') 301 | else: 302 | print(hex(zcode.game.PC + offset - 2), end=' ') 303 | branchfrom = zcode.game.PC 304 | if dobranch: 305 | if zcode.debug: 306 | print('(success)', end=' ') 307 | if offset == 0: 308 | zcode.game.ret(0) 309 | elif offset == 1: 310 | zcode.game.ret(1) 311 | else: 312 | zcode.game.PC = zcode.game.PC + offset - 2 313 | elif zcode.debug: 314 | print('(fail)', end=' ') 315 | if inaddress > zcode.header.statmembase: 316 | branches[inaddress] = {'offset': offset, 'mode': byte1, 'branchfrom': branchfrom} 317 | -------------------------------------------------------------------------------- /zcode/memory.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | from __future__ import annotations 16 | import functools 17 | import sys 18 | 19 | import zcode 20 | from zcode.constants import * 21 | 22 | data: bytearray | None = None 23 | originaldata = None 24 | WORDSIZE = 2 25 | memory_size = 0 26 | 27 | 28 | def setup(gamedata): 29 | global data 30 | global originaldata 31 | global filelen 32 | global WORDSIZE 33 | global memory_size 34 | 35 | originaldata = gamedata[:] 36 | data = bytearray(gamedata) 37 | 38 | memory_size = len(data) 39 | 40 | version = data[0] 41 | if version > 6 and zcode.use_standard == STANDARD_00: 42 | print('Versions 7 and 8 of the Z-Machine are not available before Standard 0.2') 43 | sys.exit() 44 | if version < 1 or version > 8: 45 | return False 46 | 47 | filelen = int.from_bytes(data[0x1a:0x1c], byteorder='big') 48 | # in the header, the file lenth may be 0, in which case it is figured it out manually. 49 | if filelen == 0: 50 | filelen = len(data) 51 | elif version < 4: # versions 1 to 3 52 | filelen *= 2 53 | elif version < 6: # versions 4 and 5 54 | filelen *= 4 55 | elif version < 9: # versions 6, 7 and 8 56 | filelen *= 8 57 | 58 | return True 59 | 60 | 61 | def verify(): 62 | global originaldata 63 | checksum = sum(originaldata[0x40:zcode.header.filelen]) % 0x10000 64 | if checksum == zcode.header.getchecksum(): 65 | return True 66 | return False 67 | 68 | 69 | def getbyte(offset): 70 | global data 71 | 72 | offset = zcode.numbers.unsigned(offset) 73 | 74 | if offset == 0x26 or offset == 0x27: 75 | zcode.header.updateFontSize() 76 | 77 | if offset >= memory_size: 78 | zcode.error.fatal("Tried to read a byte beyond available memory at " + hex(offset) + ".") 79 | 80 | return data[offset] 81 | 82 | 83 | def setbyte(offset, byte): 84 | global data 85 | offset = zcode.numbers.unsigned(offset) 86 | 87 | if offset == 0x11: 88 | # if the transcription bit is being set, start transcription 89 | if byte & 1 and not zcode.output.streams[2].active: 90 | zcode.output.openstream(2) 91 | elif byte & 1 == 0 and zcode.output.streams[2].active: # if however it has just been unset, stop transcription 92 | zcode.output.closestream(2) 93 | 94 | if not (byte & 2 and zcode.screen.fixedpitchbit): 95 | zcode.screen.currentWindow.flushTextBuffer() 96 | zcode.screen.fixedpitchbit = True 97 | elif byte & 2 == 0 and zcode.screen.fixedpitchbit: 98 | zcode.screen.currentWindow.flushTextBuffer() 99 | zcode.screen.fixedpitchbit = False 100 | 101 | if offset >= zcode.header.statmembase: 102 | zcode.error.fatal("Tried to write a byte beyond dynamic memory at " + hex(offset) + ".") 103 | 104 | byte = zcode.numbers.unsigned(byte) & 0xFF 105 | data[offset] = int(byte) 106 | 107 | 108 | static_words = {} 109 | 110 | 111 | def getword(offset): 112 | global data 113 | global static_words 114 | if offset in static_words: 115 | return static_words[offset] 116 | 117 | offset = zcode.numbers.unsigned(offset) 118 | 119 | if offset == 0x26: 120 | zcode.header.updateFontSize() 121 | 122 | if offset >= memory_size: 123 | zcode.error.fatal("Tried to read a word beyond available memory at " + hex(offset) + ".") 124 | 125 | value = int.from_bytes(data[offset:offset + WORDSIZE], byteorder='big') 126 | 127 | if offset > zcode.header.statmembase: 128 | static_words[offset] = value 129 | 130 | return value 131 | 132 | 133 | def setword(offset, word): 134 | global data 135 | 136 | offset = zcode.numbers.unsigned(offset) 137 | 138 | if offset == 0x10: 139 | # if the transcription bit is being set, start transcription 140 | if word & 1 and (zcode.output.streams[2].active == False): 141 | zcode.output.openstream(2) 142 | elif word & 1 == 0 and zcode.output.streams[2].active: # if however it is being unset, stop transcription 143 | zcode.output.closestream(2) 144 | if word & 2 and zcode.screen.fixedpitchbit == False: 145 | zcode.screen.currentWindow.flushTextBuffer() 146 | zcode.screen.fixedpitchbit = True 147 | elif word & 2 == 0 and zcode.screen.fixedpitchbit: 148 | zcode.screen.currentWindow.flushTextBuffer() 149 | zcode.screen.fixedpitchbit = False 150 | 151 | if offset >= zcode.header.statmembase: 152 | zcode.error.fatal("Tried to write a word beyond dynamic memory at " + hex(offset) + ".") 153 | 154 | word = zcode.numbers.unsigned(word) 155 | data[offset:offset + WORDSIZE] = int.to_bytes(word, WORDSIZE, byteorder='big') 156 | 157 | 158 | def getarray(offset: int, length: int) -> bytearray: 159 | offset = zcode.numbers.unsigned(offset) 160 | return data[offset:offset + length] 161 | 162 | 163 | def getwordarray(offset, length): 164 | offset = zcode.numbers.unsigned(offset) 165 | return [int.from_bytes(a, byteorder='big') for a in zip(*(iter(getarray(offset, length * WORDSIZE)),) * WORDSIZE)] 166 | 167 | 168 | def setarray(offset, newdata): 169 | global data 170 | offset = zcode.numbers.unsigned(offset) 171 | if len(newdata) + offset > zcode.header.statmembase: 172 | zcode.error.fatal("Tried to write a word beyond dynamic memory at " + hex(offset + len(newdata)) + ".") 173 | data[offset:offset + len(newdata)] = newdata[:] 174 | 175 | 176 | @functools.lru_cache(maxsize=128) 177 | def wordaddress(address): # this is so simple, and so rare, it seems kinda pointless having it here. 178 | if address * WORDSIZE >= len(data): 179 | zcode.error.fatal("Tried to access data beyond available memory at " + hex(address) + ".") 180 | return address * WORDSIZE 181 | 182 | 183 | @functools.lru_cache(maxsize=128) 184 | def unpackaddress(address, type=0): 185 | if zcode.header.zversion < 4: # zversions 1, 2 and 3 186 | return address * 2 187 | elif zcode.header.zversion < 6: # zversion 4 and 5 188 | return address * 4 189 | elif zcode.header.zversion < 8: # zversions 6 and 7 190 | if type == 1: # routine calls 191 | return (address * 4) + zcode.header.roffset 192 | elif type == 2: # print_paddr 193 | return (address * 4) + zcode.header.soffset 194 | elif zcode.header.zversion == 8: # zversion 8 195 | return address * 8 196 | -------------------------------------------------------------------------------- /zcode/numbers.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import random 16 | 17 | import zcode 18 | 19 | 20 | # Due to the fact that the Z-machine is generally considered to round numbers 21 | # towards 0, and Python always rounds numbers towards minus infinity, we need 22 | # to mess around a bit. This code is stolen from old Viola, because it works 23 | # and I couldn't be bothered to figure it all out again. As such it is probably 24 | # the only code surviving from the original, very messy version of Viola. 25 | 26 | def div(a, b): # divide a by b 27 | if b == 0: 28 | zcode.error.fatal('Tried to divide by zero') 29 | else: 30 | x = a / b 31 | y = round(x) 32 | if (y < 0) and (y < x): 33 | y += 1 34 | if (y > 0) and (y > x): 35 | y -= 1 36 | z = int(y) 37 | return z 38 | 39 | 40 | def mod(a, b): # divide a by b and return the remainder 41 | if b == 0: 42 | zcode.error.fatal('Tried to divide by zero') 43 | else: 44 | x = a / b 45 | y = round(x) 46 | if (y < 0) and (y < x): 47 | y += 1 48 | if (y > 0) and (y > x): 49 | y = y - 1 50 | y = int(y) 51 | z = int((0 - ((y * b) - a))) 52 | return z 53 | 54 | 55 | def reduce(num): # reduces out of range numbers 56 | num = unsigned(num) 57 | if num > 0xFFFF: 58 | num = num % 0x10000 59 | num = signed(num) 60 | return num 61 | 62 | 63 | mode = 0 # 0 is random mode. 1 is predictable mode. 64 | seed = 0 65 | sequence = 1 66 | 67 | 68 | # since the z-machine uses 16-bit numbers and python uses 32-bit numbers, 69 | # we have to convert back and forth a bit. If we want to do signed maths with 70 | # numbers from memory, we have to convert them using signed(). If we want to store 71 | # the result of a calculation in memory, we have to convert it using unsigned(). 72 | 73 | def signed(negnum): 74 | return negnum - ((negnum & 0x8000) * 2) 75 | 76 | 77 | def unsigned(negnum): 78 | if (negnum < 0): 79 | negnum += 0x10000 80 | return negnum 81 | 82 | 83 | def randomize(zseed): # seeds the z-machine random number generator 84 | global seed 85 | global mode 86 | global sequence 87 | if zseed == 0: 88 | random.seed() 89 | elif zseed < 1000: 90 | seed = zseed 91 | mode = 1 92 | sequence = 1 93 | else: 94 | seed = zseed 95 | mode = 1 96 | random.seed(seed) 97 | 98 | 99 | def getrandom(max): # returns a number from the z-machine random number generator. Max should not be more than 32767 100 | global seed 101 | global sequence 102 | if mode == 0: 103 | if max == 1: 104 | value = 1 105 | else: 106 | value = random.randrange(1, max + 1) 107 | elif seed < 1000: # if the mode is predictable and the seed is less than 1000, max is ignored (it should be the same as seed in any case) 108 | value = sequence 109 | sequence += 1 110 | if sequence > seed: 111 | sequence = 1 112 | else: 113 | value = random.randrange(1, max + 1) 114 | return value 115 | -------------------------------------------------------------------------------- /zcode/objects.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | from zcode.constants import * 17 | 18 | object_location = {} 19 | object_properties_address = {} 20 | object_properties_data_address = {} 21 | object_property_address = {} 22 | object_data = [] 23 | largest_object_number = 0 24 | object_data_length = 0 25 | 26 | object_list = {} # not a list 27 | 28 | 29 | class z_object: 30 | location = False 31 | properties_address = False 32 | parent = False 33 | sibling = False 34 | child = False 35 | attributes = False 36 | elder_sibling = False 37 | 38 | 39 | def setup(): 40 | global propdef 41 | global objstart 42 | propdef = zcode.header.objtableloc 43 | if zcode.header.zversion < 4: 44 | objstart = propdef + 62 45 | else: 46 | objstart = propdef + 126 47 | 48 | 49 | def getDefaultProperty(propnum): 50 | address = ((propnum - 1) * 2) + propdef 51 | return zcode.memory.getword(address) 52 | 53 | 54 | def updateObjectData(wipe=False): 55 | global object_data 56 | global object_data_length 57 | 58 | if zcode.header.zversion < 4: 59 | object_data_length = (9 * largest_object_number) - 9 60 | else: 61 | object_data_length = (12 * largest_object_number) - 12 62 | 63 | object_data = zcode.memory.data[objstart:objstart + object_data_length] 64 | 65 | if wipe: # the object table in the game's memory has been altered unexpectedly, so all our cached data is useless 66 | object_list = {} 67 | object_properties_address = {} 68 | object_properties_data_address = {} 69 | object_property_address = {} 70 | 71 | 72 | def findObject(obj): 73 | """returns the address in memory of the object with the number given""" 74 | global object_list 75 | global largest_object_number 76 | if obj > largest_object_number: 77 | largest_object_number = obj 78 | updateObjectData() 79 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 80 | if obj in object_list and object_list[obj].location: 81 | return object_list[obj].location 82 | else: 83 | updateObjectData(wipe=True) 84 | if zcode.header.zversion < 4: 85 | address = objstart + (9 * obj) - 9 86 | else: 87 | address = objstart + (14 * obj) - 14 88 | if obj in object_list: 89 | o = object_list[obj] 90 | else: 91 | o = z_object() 92 | o.location = address 93 | object_list[obj] = o 94 | return address 95 | 96 | 97 | def getParent(obj): 98 | """returns the number of the parent object of the object with the number given""" 99 | global object_list 100 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 101 | if obj in object_list and object_list[obj].parent: 102 | return object_list[obj].parent 103 | else: 104 | updateObjectData(wipe=True) 105 | address = findObject(obj) 106 | if zcode.header.zversion < 4: 107 | parent = zcode.memory.getbyte(address + 4) 108 | else: 109 | parent = zcode.memory.getword(address + 6) 110 | if obj in object_list: 111 | o = object_list[obj] 112 | else: 113 | o = z_object() 114 | o.parent = parent 115 | object_list[obj] = o 116 | return parent 117 | 118 | 119 | def getSibling(obj): 120 | """returns the number of the sibling object of the object with the number given""" 121 | global object_list 122 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 123 | if obj in object_list and object_list[obj].sibling: 124 | return object_list[obj].sibling 125 | else: 126 | updateObjectData(wipe=True) 127 | address = findObject(obj) 128 | if zcode.header.zversion < 4: 129 | sibling = zcode.memory.getbyte(address + 5) 130 | else: 131 | sibling = zcode.memory.getword(address + 8) 132 | if obj in object_list: 133 | o = object_list[obj] 134 | else: 135 | o = z_object() 136 | o.sibling = sibling 137 | object_list[obj] = o 138 | return sibling 139 | 140 | 141 | def getChild(obj): 142 | """returns the number of the child object of the object with the number given""" 143 | global object_list 144 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 145 | if obj in object_list and object_list[obj].child: 146 | return object_list[obj].child 147 | else: 148 | updateObjectData(wipe=True) 149 | address = findObject(obj) 150 | if zcode.header.zversion < 4: 151 | child = zcode.memory.getbyte(address + 6) 152 | else: 153 | child = zcode.memory.getword(address + 10) 154 | if obj in object_list: 155 | o = object_list[obj] 156 | else: 157 | o = z_object() 158 | o.child = child 159 | object_list[obj] = o 160 | return child 161 | 162 | 163 | def getElderSibling(obj): 164 | """returns the number of the object to which the object with the number given is the sibling""" 165 | global object_list 166 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 167 | if obj in object_list and object_list[obj].elder_sibling: 168 | return object_list[obj].elder_sibling 169 | else: 170 | updateObjectData(wipe=True) 171 | 172 | parent = getParent(obj) 173 | if parent == 0: # there is no parent, so there is no elder sibling 174 | eldersibling = 0 175 | else: 176 | eldestchild = getChild(parent) 177 | if eldestchild == obj: # there is no elder sibling 178 | eldersibling = 0 179 | else: 180 | sibling = getSibling(eldestchild) 181 | eldersibling = eldestchild 182 | while sibling != obj: 183 | eldersibling = getSibling(eldersibling) 184 | sibling = getSibling(eldersibling) 185 | if obj in object_list: 186 | o = object_list[obj] 187 | else: 188 | o = z_object() 189 | o.elder_sibling = eldersibling 190 | object_list[obj] = o 191 | return eldersibling 192 | 193 | 194 | def setParent(obj, parent): # sets obj's parent to parent 195 | global object_list 196 | address = findObject(obj) 197 | if zcode.header.zversion < 4: 198 | zcode.memory.setbyte(address + 4, parent) 199 | else: 200 | zcode.memory.setword(address + 6, parent) 201 | if obj in object_list: 202 | o = object_list[obj] 203 | else: 204 | o = z_object() 205 | o.elder_sibling = False 206 | o.parent = parent 207 | object_list[obj] = o 208 | updateObjectData() 209 | 210 | 211 | def setSibling(obj, sibling): # sets obj's sibling to sibling 212 | global object_list 213 | address = findObject(obj) 214 | if zcode.header.zversion < 4: 215 | zcode.memory.setbyte(address + 5, sibling) 216 | else: 217 | zcode.memory.setword(address + 8, sibling) 218 | if obj in object_list: 219 | o = object_list[obj] 220 | else: 221 | o = z_object() 222 | o.elder_sibling = False 223 | o.sibling = sibling 224 | object_list[obj] = o 225 | updateObjectData() 226 | 227 | 228 | def setChild(obj, child): # sets obj's child to child 229 | global object_list 230 | address = findObject(obj) 231 | if zcode.header.zversion < 4: 232 | zcode.memory.setbyte(address + 6, child) 233 | else: 234 | zcode.memory.setword(address + 10, child) 235 | if obj in object_list: 236 | o = object_list[obj] 237 | else: 238 | o = z_object() 239 | o.child = child 240 | object_list[obj] = o 241 | updateObjectData() 242 | 243 | 244 | def getPropertiesAddress(obj): # returns the address of the properties table for the object numbered obj 245 | global object_list 246 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 247 | if obj in object_list and object_list[obj].properties_address: 248 | return object_list[obj].properties_address 249 | else: 250 | updateObjectData(wipe=True) 251 | 252 | address = findObject(obj) 253 | if zcode.header.zversion < 4: 254 | address = zcode.memory.getword(address + 7) 255 | else: 256 | address = zcode.memory.getword(address + 12) 257 | if obj in object_list: 258 | o = object_list[obj] 259 | else: 260 | o = z_object() 261 | o.properties_address = address 262 | object_list[obj] = o 263 | updateObjectData() 264 | 265 | return address 266 | 267 | 268 | def setAttribute(obj, attr): # sets attribute number attr in object number obj 269 | address = findObject(obj) 270 | if attr < 16: 271 | flags = zcode.memory.getword(address) 272 | shift = attr 273 | elif attr < 32: 274 | flags = zcode.memory.getword(address + 2) 275 | shift = attr - 16 276 | elif attr < 48 and zcode.header.zversion > 3: 277 | flags = zcode.memory.getword(address + 4) 278 | shift = attr - 32 279 | else: # this is an error condition 280 | zcode.error.strictz('Tried to set attribute ' + str(attr)) 281 | return None 282 | flagset = (0x8000 >> shift) 283 | flags = flags | flagset 284 | if attr < 16: 285 | zcode.memory.setbyte(address, (flags >> 8) & 255) 286 | zcode.memory.setbyte(address + 1, flags & 255) 287 | elif attr < 32: 288 | zcode.memory.setbyte(address + 2, (flags >> 8) & 255) 289 | zcode.memory.setbyte(address + 3, flags & 255) 290 | elif attr < 48: 291 | zcode.memory.setword(address + 4, flags) 292 | 293 | 294 | def clearAttribute(obj, attr): # clears attribute number attr in object number obj 295 | address = findObject(obj) 296 | if attr < 16: 297 | flags = zcode.memory.getword(address) 298 | shift = attr 299 | elif attr < 32: 300 | flags = zcode.memory.getword(address + 2) 301 | shift = attr - 16 302 | elif attr < 48 and zcode.header.zversion > 3: 303 | flags = zcode.memory.getword(address + 4) 304 | shift = attr - 32 305 | else: # this is an error condition 306 | zcode.error.strictz('Tried to clear attribute ' + str(attr)) 307 | return None 308 | flagclear = (0x8000 >> shift) 309 | flagclear = ~flagclear 310 | flags = flags & flagclear 311 | if attr < 16: 312 | zcode.memory.setbyte(address, (flags >> 8) & 255) 313 | zcode.memory.setbyte(address + 1, flags & 255) 314 | elif attr < 32: 315 | zcode.memory.setbyte(address + 2, (flags >> 8) & 255) 316 | zcode.memory.setbyte(address + 3, flags & 255) 317 | elif attr < 48: 318 | zcode.memory.setword(address + 4, flags) 319 | 320 | 321 | def testAttribute(obj, attr): # tests if attribute number attr is on in object number obj 322 | address = findObject(obj) 323 | if attr < 16: 324 | flags = zcode.memory.getword(address) 325 | shift = attr 326 | elif attr < 32: 327 | flags = zcode.memory.getword(address + 2) 328 | shift = attr - 16 329 | elif zcode.header.zversion > 3 and attr < 48: 330 | flags = zcode.memory.getword(address + 4) 331 | shift = attr - 32 332 | else: 333 | zcode.error.strictz('Tried to test attribute ' + str(attr)) 334 | return 0 335 | flagtest = (0x8000 >> shift) 336 | if flagtest & flags == flagtest: 337 | return 1 338 | else: 339 | return 0 340 | 341 | 342 | def getShortName(obj): # returns the decoded short name of the object 343 | if obj == 0: 344 | return 0 345 | address = getPropertiesAddress(obj) + 1 346 | if zcode.memory.getbyte(address - 1) == 0: 347 | return 'NoName' 348 | return zcode.text.decodetext(address) 349 | 350 | 351 | # now we get to the properties themselves. This is a bit more difficult 352 | 353 | 354 | def getPropertySize(address): # given the address of the size byte of a property, returns the size 355 | if zcode.header.zversion < 4: 356 | size = (zcode.memory.getbyte(address) >> 5) + 1 357 | else: 358 | if (zcode.memory.getbyte(address) >> 7) & 1 == 1: 359 | size = (zcode.memory.getbyte(address + 1) & 0x3f) 360 | if size == 0: 361 | if zcode.use_standard >= STANDARD_10: # Standard 1.0 and above 362 | size = 64 363 | else: 364 | zcode.error.warning('Property length of 0 (in Standard 1.0 and higher this would mean a length of 64)') 365 | else: 366 | if (zcode.memory.getbyte(address) >> 6) & 1 == 1: 367 | size = 2 368 | else: 369 | size = 1 370 | return size 371 | 372 | 373 | def getPropertyNumber(address): # given the address of the size byte of a property, returns the property number 374 | if zcode.header.zversion < 4: 375 | size = zcode.memory.getbyte(address) & 0x1F 376 | else: 377 | size = zcode.memory.getbyte(address) & 0x3f 378 | return size 379 | 380 | 381 | def getNextProperty(address): 382 | if zcode.header.zversion < 4: 383 | sizelen = 1 # for z3 and earlier games, there is only one size byte 384 | else: # in later versions of the z-machine there may be either one or two bytes 385 | if zcode.memory.getbyte(address) & 128 == 128: # if the top bit of the first size byte is set, there are two size bytes 386 | sizelen = 2 387 | else: # if it is unset, there is only one size byte 388 | sizelen = 1 389 | 390 | newaddress = address + sizelen + getPropertySize(address) 391 | return newaddress 392 | 393 | 394 | def getFirstProperty(obj): # returns the address of obj's first property 395 | address = getPropertiesAddress(obj) # find the start of the properties header 396 | headerlen = zcode.memory.getbyte(address) * 2 # find the length of the short name 397 | address = address + 1 + headerlen # find the first byte after the short name 398 | return address 399 | 400 | 401 | def getPropertyAddress(obj, prop): # returns the address of the property prop of the object obj (this returns the address of the first byte of the size info, not the first byte of the data) 402 | global object_property_address 403 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 404 | if obj in object_property_address: 405 | if prop in object_property_address[obj]: 406 | return object_property_address[obj][prop] 407 | else: 408 | updateObjectData(wipe=True) 409 | 410 | address = getFirstProperty(obj) 411 | num = getPropertyNumber(address) 412 | if num == 0: 413 | return 0 414 | while num != prop: 415 | address = getNextProperty(address) 416 | num = getPropertyNumber(address) # find out what property this is 417 | if num == 0: 418 | prop = 0 419 | address = 0 420 | if obj not in object_property_address: 421 | object_property_address[obj] = {} 422 | object_property_address[obj][prop] = address 423 | return address 424 | 425 | 426 | def getPropertyDataAddress(obj, prop): # returns the address of the first byte of the data of the given property of the given object. No, really. 427 | if object_data == zcode.memory.data[objstart:objstart + object_data_length]: 428 | if obj in object_properties_data_address: 429 | if prop in object_properties_data_address[obj]: 430 | return object_properties_data_address[obj][prop] 431 | else: 432 | updateObjectData(wipe=True) 433 | 434 | sizeaddr = getPropertyAddress(obj, prop) 435 | if sizeaddr != 0: 436 | if zcode.header.zversion < 4: 437 | sizeaddr += 1 438 | elif (zcode.memory.getbyte(sizeaddr) >> 7) & 1 == 1: 439 | sizeaddr += 2 440 | else: 441 | sizeaddr += 1 442 | if obj not in object_properties_data_address: 443 | object_properties_data_address[obj] = {} 444 | object_properties_data_address[obj][prop] = sizeaddr 445 | return sizeaddr 446 | -------------------------------------------------------------------------------- /zcode/optables.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | from zcode.constants import * 17 | 18 | op2 = [ zcode.opcodes.z_badop, 19 | zcode.opcodes.z_je, 20 | zcode.opcodes.z_jl, 21 | zcode.opcodes.z_jg, 22 | zcode.opcodes.z_dec_chk, 23 | zcode.opcodes.z_inc_chk, 24 | zcode.opcodes.z_jin, 25 | zcode.opcodes.z_test, 26 | zcode.opcodes.z_or, 27 | zcode.opcodes.z_and, 28 | zcode.opcodes.z_test_attr, 29 | zcode.opcodes.z_set_attr, 30 | zcode.opcodes.z_clear_attr, 31 | zcode.opcodes.z_store, 32 | zcode.opcodes.z_insert_obj, 33 | zcode.opcodes.z_loadw, 34 | zcode.opcodes.z_loadb, 35 | zcode.opcodes.z_get_prop, 36 | zcode.opcodes.z_get_prop_addr, 37 | zcode.opcodes.z_get_next_prop, 38 | zcode.opcodes.z_add, 39 | zcode.opcodes.z_sub, 40 | zcode.opcodes.z_mul, 41 | zcode.opcodes.z_div, 42 | zcode.opcodes.z_mod, 43 | zcode.opcodes.z_badop, 44 | zcode.opcodes.z_badop, 45 | zcode.opcodes.z_badop, 46 | zcode.opcodes.z_badop, 47 | zcode.opcodes.z_badop, 48 | zcode.opcodes.z_badop, 49 | zcode.opcodes.z_badop 50 | ] 51 | 52 | op1 = [ zcode.opcodes.z_jz, 53 | zcode.opcodes.z_get_sibling, 54 | zcode.opcodes.z_get_child, 55 | zcode.opcodes.z_get_parent, 56 | zcode.opcodes.z_get_prop_len, 57 | zcode.opcodes.z_inc, 58 | zcode.opcodes.z_dec, 59 | zcode.opcodes.z_print_addr, 60 | zcode.opcodes.z_badop, 61 | zcode.opcodes.z_remove_obj, 62 | zcode.opcodes.z_print_obj, 63 | zcode.opcodes.z_ret, 64 | zcode.opcodes.z_jump, 65 | zcode.opcodes.z_print_paddr, 66 | zcode.opcodes.z_load, 67 | zcode.opcodes.z_not 68 | ] 69 | 70 | op0 = [ zcode.opcodes.z_rtrue, 71 | zcode.opcodes.z_rfalse, 72 | zcode.opcodes.z_print, 73 | zcode.opcodes.z_print_ret, 74 | zcode.opcodes.z_nop, 75 | zcode.opcodes.z_save, 76 | zcode.opcodes.z_restore, 77 | zcode.opcodes.z_restart, 78 | zcode.opcodes.z_ret_popped, 79 | zcode.opcodes.z_pop, 80 | zcode.opcodes.z_quit, 81 | zcode.opcodes.z_new_line, 82 | zcode.opcodes.z_badop, 83 | zcode.opcodes.z_badop, 84 | zcode.opcodes.z_badop, 85 | zcode.opcodes.z_badop 86 | ] 87 | 88 | opvar = [ zcode.opcodes.z_call_vs, 89 | zcode.opcodes.z_storew, 90 | zcode.opcodes.z_storeb, 91 | zcode.opcodes.z_put_prop, 92 | zcode.opcodes.z_read, 93 | zcode.opcodes.z_print_char, 94 | zcode.opcodes.z_print_num, 95 | zcode.opcodes.z_random, 96 | zcode.opcodes.z_push, 97 | zcode.opcodes.z_pull, 98 | zcode.opcodes.z_badop, 99 | zcode.opcodes.z_badop, 100 | zcode.opcodes.z_badop, 101 | zcode.opcodes.z_badop, 102 | zcode.opcodes.z_badop, 103 | zcode.opcodes.z_badop, 104 | zcode.opcodes.z_badop, 105 | zcode.opcodes.z_badop, 106 | zcode.opcodes.z_badop, 107 | zcode.opcodes.z_badop, 108 | zcode.opcodes.z_badop, 109 | zcode.opcodes.z_badop, 110 | zcode.opcodes.z_badop, 111 | zcode.opcodes.z_badop, 112 | zcode.opcodes.z_badop, 113 | zcode.opcodes.z_badop, 114 | zcode.opcodes.z_badop, 115 | zcode.opcodes.z_badop, 116 | zcode.opcodes.z_badop, 117 | zcode.opcodes.z_badop, 118 | zcode.opcodes.z_badop, 119 | zcode.opcodes.z_badop 120 | ] 121 | 122 | opext = [zcode.opcodes.z_badop] * 256 123 | 124 | 125 | def setup(): 126 | global op2, op1, op0, opvar, opext 127 | 128 | if zcode.memory.data[0] >= 3: 129 | op0[0xc] = zcode.opcodes.z_show_status 130 | op0[0xd] = zcode.opcodes.z_verify 131 | opvar[0xa] = zcode.opcodes.z_split_window 132 | opvar[0xb] = zcode.opcodes.z_set_window 133 | opvar[0x13] = zcode.opcodes.z_output_stream 134 | opvar[0x14] = zcode.opcodes.z_input_stream 135 | opvar[0x15] = zcode.opcodes.z_sound_effect 136 | 137 | if zcode.memory.data[0] >= 4: 138 | op2[0x19] = zcode.opcodes.z_call_2s 139 | op1[0x8] = zcode.opcodes.z_call_1s 140 | op0[0xc] = zcode.opcodes.z_badop 141 | opvar[0xc] = zcode.opcodes.z_call_vs2 142 | opvar[0xd] = zcode.opcodes.z_erase_window 143 | opvar[0xe] = zcode.opcodes.z_erase_line 144 | opvar[0xf] = zcode.opcodes.z_set_cursor 145 | opvar[0x10] = zcode.opcodes.z_get_cursor 146 | opvar[0x11] = zcode.opcodes.z_set_text_style 147 | opvar[0x12] = zcode.opcodes.z_buffer_mode 148 | opvar[0x16] = zcode.opcodes.z_read_char 149 | opvar[0x17] = zcode.opcodes.z_scan_table 150 | 151 | if zcode.memory.data[0] >= 5: 152 | op2[0x1a] = zcode.opcodes.z_call_2n 153 | op2[0x1b] = zcode.opcodes.z_set_colour 154 | op2[0x1c] = zcode.opcodes.z_throw 155 | op1[0xf] = zcode.opcodes.z_call_1n 156 | op0[0x5] = zcode.opcodes.z_badop 157 | op0[0x6] = zcode.opcodes.z_badop 158 | op0[0x9] = zcode.opcodes.z_catch 159 | op0[0xe] = zcode.opcodes.z_extended 160 | op0[0xf] = zcode.opcodes.z_piracy 161 | opvar[0x18] = zcode.opcodes.z_not 162 | opvar[0x19] = zcode.opcodes.z_call_vn 163 | opvar[0x1a] = zcode.opcodes.z_call_vn2 164 | opvar[0x1b] = zcode.opcodes.z_tokenise 165 | opvar[0x1c] = zcode.opcodes.z_encode_text 166 | opvar[0x1d] = zcode.opcodes.z_copy_table 167 | opvar[0x1e] = zcode.opcodes.z_print_table 168 | opvar[0x1f] = zcode.opcodes.z_check_arg_count 169 | opext[0x0] = zcode.opcodes.z_save 170 | opext[0x1] = zcode.opcodes.z_restore 171 | opext[0x2] = zcode.opcodes.z_log_shift 172 | opext[0x3] = zcode.opcodes.z_art_shift 173 | opext[0x4] = zcode.opcodes.z_set_font 174 | opext[0x9] = zcode.opcodes.z_save_undo 175 | opext[0xa] = zcode.opcodes.z_restore_undo 176 | if zcode.use_standard >= STANDARD_10: # Standard 1.0 or later 177 | opext[0xb] = zcode.opcodes.z_print_unicode 178 | opext[0xc] = zcode.opcodes.z_check_unicode 179 | if zcode.use_standard >= STANDARD_11: # Standard 1.1 or later 180 | opext[0xd] = zcode.opcodes.z_set_true_colour 181 | 182 | if zcode.memory.data[0] == 6: 183 | opext[0x5] = zcode.opcodes.z_draw_picture 184 | opext[0x6] = zcode.opcodes.z_picture_data 185 | opext[0x7] = zcode.opcodes.z_erase_picture 186 | opext[0x8] = zcode.opcodes.z_set_margins 187 | opext[0x10] = zcode.opcodes.z_move_window 188 | opext[0x11] = zcode.opcodes.z_window_size 189 | opext[0x12] = zcode.opcodes.z_window_style 190 | opext[0x13] = zcode.opcodes.z_get_wind_prop 191 | opext[0x14] = zcode.opcodes.z_scroll_window 192 | opext[0x15] = zcode.opcodes.z_pop_stack 193 | opext[0x16] = zcode.opcodes.z_read_mouse 194 | opext[0x17] = zcode.opcodes.z_mouse_window 195 | opext[0x18] = zcode.opcodes.z_push_stack 196 | opext[0x19] = zcode.opcodes.z_put_wind_prop 197 | opext[0x1a] = zcode.opcodes.z_print_form 198 | opext[0x1b] = zcode.opcodes.z_make_menu 199 | opext[0x1c] = zcode.opcodes.z_picture_table 200 | if zcode.use_standard >= STANDARD_11: # Standard 1.1 or later 201 | opext[0x1d] = zcode.opcodes.z_buffer_screen 202 | -------------------------------------------------------------------------------- /zcode/output.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import string 16 | import vio.zcode as io 17 | import zcode 18 | from zcode.constants import * 19 | 20 | 21 | def setup(startstreams=(False, True, False, False, False)): 22 | global streams 23 | streams = [None, screenstream(), transcriptstream(), [], commandstream(), interpreterstream()] 24 | if startstreams[2]: 25 | streams[2].filename = startstreams[2] 26 | if startstreams[4]: 27 | streams[4].filename = startstreams[4] 28 | for stream, start in enumerate(startstreams): 29 | if start: 30 | streams[stream].open() 31 | 32 | 33 | def savefilename(location): 34 | alphanumeric = string.digits + string.ascii_letters 35 | try: 36 | nameloc = zcode.instructions.operands[2]['value'] 37 | namelen = zcode.memory.getbyte(nameloc) 38 | name = bytearray(zcode.memory.getarray(nameloc + 1, namelen)).decode('latin-1') 39 | ext = name.find('.') 40 | if ext >= 0: 41 | name = name[:ext] 42 | filename = ''.join([i for i in name if i in alphanumeric]) 43 | except: 44 | filename = 'NULL' 45 | if filename == '': 46 | filename = 'NULL' 47 | 48 | return filename 49 | 50 | 51 | class outputstream: 52 | active = False 53 | quiet = False 54 | 55 | def open(self): 56 | self.active = True 57 | 58 | def close(self): 59 | self.active = False 60 | 61 | def write(self, data): 62 | if self.active and self.quiet == False: 63 | self.output(data) 64 | 65 | def output(self, data): 66 | pass 67 | 68 | 69 | combining = [chr(a) for a in range(0x0300, 0x0370)] 70 | nbsp = chr(0x00A0) 71 | 72 | 73 | class screenstream(outputstream): 74 | active = True 75 | interruptprinted = False 76 | 77 | def output(self, data): 78 | if zcode.debug: 79 | print('"', end='') 80 | print(data.replace('\r', '\n'), end='') 81 | print('"', end=' ') 82 | for a in combining: 83 | data = data.replace(a, nbsp + a) 84 | if zcode.game.currentframe.interrupt: 85 | self.interruptprinted = True 86 | zcode.screen.printtext(data) 87 | 88 | 89 | class transcriptstream(outputstream): 90 | filename = None 91 | 92 | def open(self): 93 | if self.filename == None: 94 | self.filename = writefile(b"", filename="TRANSCRIPT.LOG", prompt=True, append=False) 95 | self.active = True 96 | zcode.header.setflag(2, 0, 1) 97 | 98 | def output(self, data): 99 | data = data.replace('\r', '\n') 100 | if zcode.screen.currentWindow.testattribute(4): 101 | file = io.openfile(zcode.screen.currentWindow, 'a', self.filename) 102 | writefile(data.encode('utf-8'), filename=self.filename, prompt=False, append=True) 103 | 104 | def close(self): 105 | self.active = False 106 | zcode.header.setflag(2, 0, 0) # make sure the transcripting bit reflects the current state of transcription 107 | 108 | 109 | class memorystream(outputstream): 110 | location = None 111 | width = None 112 | data = "" 113 | 114 | def open(self, location, width=None): 115 | global stream 116 | self.location = location 117 | self.width = width 118 | self.active = True 119 | self.data = [] 120 | streams[1].quiet = True 121 | streams[2].quiet = True 122 | streams[4].quiet = True 123 | 124 | def close(self): 125 | global stream 126 | self.active = False 127 | if self.width == None: 128 | zcode.memory.setword(self.location, len(self.data)) 129 | OFFSET = 2 130 | else: 131 | OFFSET = 0 132 | lines = [] 133 | if self.width != None: # formatted text for z6 134 | text = ''.join(self.data) 135 | while len(text) > 0: 136 | x = zcode.screen.currentWindow.fitText(text, self.width) 137 | if text[:x].find('\r') != -1: 138 | x = text[:x].find('\r') 139 | line = (len(text[:x]), text[:x]) 140 | lines.append(line) 141 | text = text[line[0]:] 142 | if zcode.header.zversion == 6: 143 | zcode.header.settextwidth(zcode.screen.currentWindow.getFont().getStringLength(''.join(self.data))) 144 | 145 | c = 0 146 | data = [] 147 | if self.width == None: 148 | for a in self.data: # make absolutely certain each value in data fits in a byte 149 | b = ord(a) 150 | d = [] 151 | while b > 255: 152 | d.append(b & 255) 153 | b = b >> 8 154 | data.append(b) 155 | d.reverse() 156 | for b in d: 157 | data.append(b) 158 | 159 | 160 | else: 161 | for l in lines: 162 | linelength = l[0] 163 | line = l[1] 164 | data.append((linelength >> 8) & 0xff) 165 | data.append(linelength & 0xff) 166 | for c in line: 167 | data.append(ord(c)) 168 | 169 | for count, value in enumerate(data): 170 | zcode.memory.setbyte(self.location + OFFSET + count, value) 171 | 172 | if self.width != None: # if a width operand was passed to output stream 3, we need to add a 0 word on the end of the text 173 | zcode.memory.setword(self.location + len(data), 0) 174 | self.data = [] 175 | 176 | def output(self, data): 177 | data = zcode.text.unicodetozscii(data) 178 | self.data += data 179 | 180 | 181 | class commandstream(outputstream): 182 | filename = None 183 | 184 | def open(self): 185 | if self.filename == None: 186 | self.filename = writefile(b"", filename="COMMANDS.REC", prompt=True, append=False) 187 | self.active = True 188 | 189 | def output(self, data): 190 | file = io.openfile(zcode.screen.currentWindow, 'a', self.filename) 191 | writefile(data.encode('utf-8'), filename=self.filename, prompt=False, append=True) 192 | 193 | 194 | class istreamdata(): 195 | data = [] 196 | 197 | 198 | class gibberize(istreamdata): 199 | ident = 'GIBBERIZE:' 200 | 201 | def processdata(self, data): 202 | c = data.index(':') 203 | data = data[c + 1:] 204 | t = ''.join(data).replace('S', '5').replace('A', '4').replace('E', '3').replace('I', '1').replace('O', '0') 205 | streams[5].outputdata = t 206 | 207 | 208 | istreamidents = {'GIBBERIZE:': gibberize()} 209 | 210 | 211 | class interpreterstream(outputstream): 212 | data = [] 213 | tempdata = [] 214 | location = None 215 | outputdata = [] 216 | 217 | def open(self, location): 218 | self.tempdata = [] 219 | self.active = True 220 | self.location = location 221 | 222 | def output(self, data): 223 | self.tempdata.extend(data) 224 | 225 | def close(self): 226 | self.active = False 227 | # here we should actually process the data 228 | # first, search the tempdata stream for a : 229 | c = self.tempdata.index(':') 230 | 231 | # then, use all data up to and including the : as an identifier string 232 | identdata = self.tempdata[:c+1] 233 | ident = ''.join(identdata) 234 | # check the identifier string against istreamidents dictionary, to see if we recognize it 235 | if ident not in istreamidents: 236 | # if we don't, do nothing, throw the tempdata away 237 | self.tempdata = [] 238 | else: 239 | # if we do, send the tempdata to the class object for the ident type, and let that deal with what to do next 240 | istreamidents[ident].processdata(self.tempdata) # this should set up the output data for us if it needs to 241 | self.tempdata = [] 242 | 243 | # check to see if there's any output data to write back to the game memory 244 | if len(self.outputdata) > 0 and self.location: 245 | tablelen = zcode.memory.getword(self.location) 246 | l = self.location + 2 247 | d = list(self.outputdata[:]) 248 | z = 0 249 | d2 = [] 250 | for a in d: # make absolutely certain each value in data fits in a byte 251 | z += 1 252 | try: 253 | b = ord(a) 254 | except: 255 | pass 256 | while b > 255: 257 | d2.append(b & 255) 258 | b = b >> 8 259 | d2.append(b) 260 | d = d2[:tablelen] # lose any data that will not fit (we should have checked before this to make sure this won't happen, though) 261 | zcode.memory.setarray(self.location+2, d) 262 | zcode.memory.setarray(self.location + 2, d) 263 | self.outputdata = [] 264 | 265 | 266 | def checkident(address): 267 | i = [] 268 | a = address 269 | lastchar = None 270 | while lastchar != ':': 271 | i.append(chr(zcode.memory.getbyte(a))) 272 | lastchar = i[-1] 273 | a += 1 274 | ident = ''.join(i) 275 | if ident in istreamidents: 276 | return 1 277 | else: 278 | return 0 279 | 280 | 281 | def numopenstreams(stream): 282 | try: 283 | if streams[stream].active: # should work for most streams (any that aren't lists like stream 3) 284 | return 1 285 | else: 286 | return 0 287 | except: 288 | try: 289 | return len(streams[stream]) # should work for any streams that are lists (like stream 3) 290 | except: 291 | return 0 # just in case 292 | 293 | 294 | def openstream(stream, location=None, width=None): # area is only used for stream 3, width only for z6 stream 3 295 | global streams 296 | if stream == 3: 297 | if zcode.use_standard <= STANDARD_02: 298 | if len(streams[3]) == 0: 299 | m = memorystream() 300 | streams[3].append(m) 301 | try: 302 | streams[3][0].close() 303 | except: 304 | pass 305 | streams[3][0].open(location, width) 306 | else: 307 | if len(streams[3]) < 16: 308 | m = memorystream() 309 | m.open(location, width) 310 | streams[3].append(m) 311 | else: 312 | zcode.error.fatal('Tried to open too many memory streams.') 313 | else: 314 | try: 315 | streams[stream].open() 316 | except: 317 | zcode.error.strictz("Tried to open stream ' + str(stream) + ', which doesn't exist") 318 | 319 | 320 | def closestream(stream): 321 | global streams 322 | if stream != 3: 323 | streams[stream].close() 324 | else: 325 | if zcode.use_standard <= STANDARD_02: 326 | try: 327 | streams[3][0].close() 328 | except: 329 | pass 330 | else: 331 | m = streams[3].pop() 332 | m.close() 333 | if len(streams[3]) == 0: 334 | streams[1].quiet = False 335 | streams[2].quiet = False 336 | streams[4].quiet = False 337 | 338 | 339 | def printtext(text, special=False): # All text to be printed anywhere should be printed here. It will then be sorted out. 340 | streams[1].write(text) 341 | if len(streams[3]) > 0: 342 | streams[3][-1].write(text) 343 | text = text.replace('\r', '\n') 344 | streams[2].write(text) 345 | if special: 346 | streams[4].write(text) 347 | 348 | 349 | def writefile(data, filename=None, prompt=False, append=False): 350 | """Opens a file, writes data to it, and returns the filename""" 351 | if append: 352 | f = io.openfile(zcode.screen.currentWindow, 'a', filename, prompt) 353 | else: 354 | f = io.openfile(zcode.screen.currentWindow, 'w', filename, prompt) 355 | f.write(data) 356 | filename = f.name 357 | f.close() 358 | return filename 359 | -------------------------------------------------------------------------------- /zcode/routines.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import zcode 16 | 17 | quit = 0 18 | input = 0 19 | restart = 0 20 | timerreturn = False 21 | 22 | 23 | def setup(): 24 | global quit, input, restart 25 | quit = 0 26 | input = 0 27 | restart = 0 28 | 29 | 30 | static_routines = {} 31 | 32 | 33 | def setuproutine(address): 34 | """set up the local variables and returns the address of the first instruction""" 35 | global static_routines 36 | if address in static_routines: 37 | zcode.game.currentframe.lvars = static_routines[address]['vars'][:] 38 | return static_routines[address]['address'] 39 | inaddress = address 40 | vars = [] 41 | varnum = zcode.memory.getbyte(address) 42 | address += 1 43 | if zcode.header.zversion < 5: 44 | for a in range(varnum): 45 | vars.append(zcode.memory.getword(address)) 46 | address += 2 47 | else: 48 | for a in range(varnum): 49 | vars.append(0) 50 | zcode.game.currentframe.lvars = vars 51 | if zcode.debug: 52 | print() 53 | print(varnum, 'local variables', end='') 54 | if address > zcode.header.statmembase: 55 | routine = {'address': address, 'vars': vars[:]} 56 | static_routines[inaddress] = routine 57 | return address 58 | 59 | 60 | def execloop(): 61 | global input 62 | global restart 63 | global oldpc 64 | global timerreturn 65 | global quit 66 | 67 | while (restart == 0) and (quit == 0) and (timerreturn == False): 68 | try: 69 | if zcode.screen.ioScreen.resized: 70 | zcode.screen.ioScreen.resized = False 71 | zcode.screen.resize() 72 | except: 73 | pass 74 | 75 | if len(zcode.game.interruptstack) > 0: 76 | zcode.game.interrupt_call() 77 | oldpc = zcode.game.PC 78 | zcode.game.PC = zcode.instructions.decode(zcode.game.PC) 79 | zcode.instructions.runops(oldpc) 80 | timerreturn = False 81 | 82 | 83 | def execstart(): 84 | """set up the Z-Machine to start executing instructions""" 85 | global quit # if set to 1, game ends 86 | global restart 87 | if zcode.debug: 88 | print('start at', hex(zcode.header.startat)) 89 | if zcode.header.zversion != 6: 90 | zcode.game.PC = zcode.header.startat 91 | else: 92 | zcode.game.call(zcode.header.startat, [], 0, 0, 1) 93 | execloop() 94 | while restart: 95 | if zcode.header.zversion != 6: 96 | zcode.game.PC = zcode.header.startat 97 | else: 98 | zcode.game.call(zcode.header.startat, [], 0, 0, 1) 99 | restart = 0 100 | execloop() 101 | -------------------------------------------------------------------------------- /zcode/screen.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | 16 | import settings 17 | import data 18 | import vio.zcode as io 19 | import zcode 20 | from zcode.constants import * 21 | 22 | screen_buffer_mode = 0 23 | 24 | graphics_mode = 0 25 | 26 | cursor = True 27 | 28 | DEFFOREGROUND = 2 29 | DEFBACKGROUND = 9 30 | 31 | 32 | def setup(restarted=False): 33 | global zwindow 34 | global statusline 35 | global currentWindow 36 | global ioScreen 37 | global fonts 38 | global unitHeight 39 | global unitWidth 40 | global spectrum 41 | global DEFFOREGROUND 42 | global DEFBACKGROUND 43 | global graphics_mode 44 | 45 | colours.update(special_colours) 46 | 47 | if zcode.memory.data[0] == 6: # if we're running a Version 6 game 48 | graphics_mode = 1 # set to graphics mode (units == pixels, not units == characters) 49 | colours.update(v6_colours) 50 | 51 | if zcode.use_standard < STANDARD_11: 52 | spectrum.pop(15) 53 | 54 | if restarted == False: 55 | ioScreen = io.zApp.screen 56 | 57 | foreground, background = ioScreen.defaultForeground, ioScreen.defaultBackground 58 | DEFFOREGROUND, DEFBACKGROUND = convertRealToBasicColour(foreground), convertRealToBasicColour(background) 59 | 60 | width = ioScreen.width 61 | height = ioScreen.height 62 | 63 | zwindow = [] 64 | 65 | # create all the windows 66 | zwindow.append(window(ioScreen, fontlist[1])) # lower window (window 0) 67 | if zcode.memory.data[0] < 4: 68 | statusline = window(ioScreen, fontlist[1]) # statusline 69 | statusline.window_id = "statusline" 70 | if zcode.memory.data[0] > 2: 71 | zwindow.append(window(ioScreen, fontlist[1])) # upper window (window 1) 72 | if zcode.memory.data[0] == 6: 73 | for a in range(6): 74 | zwindow.append(window(ioScreen, fontlist[1])) # windows 2 to 7 75 | for count, value in enumerate(zwindow): 76 | getWindow(count).window_id = str(count) 77 | 78 | if graphics_mode == 1: # units are pixels 79 | unitHeight = 1 80 | unitWidth = 1 81 | else: # units are characters 82 | unitHeight = getWindow(0).getFont().getHeight() 83 | unitWidth = getWindow(0).getFont().getWidth() 84 | 85 | # set the current window 86 | 87 | currentWindow = getWindow(0) 88 | 89 | # set up fonts 90 | 91 | if zcode.memory.data[0] != 6 and zcode.memory.data[0] >= 3: 92 | getWindow(1).fontlist[1] = fontlist[4] 93 | if zcode.memory.data[0] < 4: 94 | statusline.fontlist[1] = fontlist[4] 95 | 96 | for a in zwindow: 97 | a.setFontByNumber(1) 98 | if zcode.memory.data[0] < 4: 99 | statusline.setFontByNumber(1) 100 | 101 | # position and size the windows 102 | 103 | if zcode.memory.data[0] == 6: # version 6 104 | for count, value in enumerate(zwindow): 105 | getWindow(count).setPosition(1, 1) 106 | getWindow(count).setSize(0,0) 107 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight()) 108 | getWindow(1).setSize(ioScreen.getWidth(), 0) 109 | elif zcode.memory.data[0] < 4: # version 1, 2 and 3 110 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight() - getWindow(0).getFont().getHeight()) 111 | getWindow(0).setPosition(1, getWindow(0).getFont().getHeight() + 1) 112 | statusline.setSize(ioScreen.getWidth(), statusline.getFont().getHeight()) 113 | statusline.setPosition(1, 1) 114 | if zcode.memory.data[0] == 3: # version 3 115 | getWindow(1).setSize(ioScreen.getWidth(), 0) 116 | getWindow(1).setPosition(1, getWindow(1).getFont().getHeight() + 1) 117 | else: # version 4, 5, 7 and 8 118 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight()) 119 | getWindow(0).setPosition(1, 1) 120 | getWindow(1).setSize(ioScreen.getWidth(), 0) 121 | getWindow(1).setPosition(1, 1) 122 | 123 | # set the cursor in the windows 124 | 125 | if zcode.memory.data[0] == 6: 126 | for count, value in enumerate(zwindow): 127 | getWindow(count).setCursor(1, 1) 128 | elif zcode.memory.data[0] < 5: 129 | getWindow(0).setCursor(1, getWindow(0).y_size - getWindow(0).getFont().getHeight() + 1) 130 | else: 131 | getWindow(0).setCursor(1, 1) 132 | 133 | # set up window attributes 134 | 135 | if zcode.memory.data[0] == 6: 136 | for count, value in enumerate(zwindow): 137 | getWindow(count).setattributes(8, 0) 138 | getWindow(0).setattributes(15, 0) 139 | elif zcode.memory.data[0] < 4: 140 | getWindow(0).setattributes(15, 0) 141 | statusline.setattributes(0, 0) 142 | if zcode.memory.data[0] == 3: 143 | getWindow(1).setattributes(0, 0) 144 | else: 145 | getWindow(0).setattributes(15, 0) 146 | getWindow(1).setattributes(0, 0) 147 | 148 | # set other default window properties 149 | 150 | for count, value in enumerate(zwindow): 151 | getWindow(count).setRealColours(foreground, background) 152 | getWindow(count).font_size = (getWindow(count).getFont().getHeight() << 8) + getWindow(count).getFont().getWidth() 153 | if zcode.memory.data[0] < 4: 154 | statusline.setRealColours(background, foreground) 155 | statusline.font_size = (statusline.getFont().getHeight() << 8) + statusline.getFont().getWidth() 156 | 157 | # give the lower window in versions other than 6 a margin 158 | if zcode.memory.data[0] != 6: 159 | getWindow(0).left_margin = 5 160 | getWindow(0).right_margin = 5 161 | 162 | 163 | def supportedgraphics(arg): # should really tie this into the io module 164 | if arg == 0: # colour 165 | return 1 166 | if arg == 1: # true colour 167 | return 1 168 | if arg == 2: # transparent background colour 169 | if zcode.memory.data[0] == 6: 170 | return 1 171 | return 0 172 | if arg == 3: # picture displaying 173 | if zcode.memory.data[0] == 6: 174 | return 1 175 | return 0 176 | return 0 177 | 178 | 179 | def supportedstyles(arg): 180 | # probably we should change this depending on the current font chosen? Font 3 has no bold or italic variations. 181 | if arg >= 0x10: # we don't recognize any styles above 8 182 | return 0 183 | return 1 # all other styles and combinations are supported 184 | 185 | 186 | # font 3 needs to be the same size as fixed width font 187 | 188 | fixedpitchbit = False 189 | 190 | 191 | def pix2units(pix, horizontal, coord=False): 192 | """converts a number of pixels into a number of z-machine units""" 193 | if graphics_mode == 1: 194 | return pix 195 | if not horizontal: 196 | value = ((pix - 1) // currentWindow.getFont().getHeight()) + 1 197 | else: 198 | value = ((pix - 1) // currentWindow.getFont().getWidth()) + 1 199 | return value 200 | 201 | 202 | def units2pix(units, horizontal, coord=False): 203 | """converts a number of z-machine units into a number of pixels""" 204 | if graphics_mode == 1: 205 | return units 206 | if coord: 207 | units -= 1 208 | if not horizontal: 209 | value = units * currentWindow.getFont().getHeight() 210 | else: 211 | value = units * currentWindow.getFont().getWidth() 212 | if coord: 213 | value += 1 214 | return value 215 | 216 | 217 | def chars2units(chars, horizontal, coord=False): 218 | """converts a height/width size in number of characters into a number of z-machine units""" 219 | 220 | if graphics_mode == 1: 221 | return chars 222 | if not horizontal: 223 | value = ((chars - 1) // currentWindow.getFont().getHeight()) + 1 224 | else: 225 | value = ((chars - 1) // currentWindow.getFont().getWidth()) + 1 226 | return value 227 | 228 | 229 | def units2chars(units, horizontal, coord=False): 230 | """converts a number of z-machine units into a height/width size in number of characters""" 231 | if graphics_mode == 1: 232 | return units 233 | if coord: 234 | units -= 1 235 | if not horizontal: 236 | value = units * currentWindow.getFont().getHeight() 237 | else: 238 | value = units * currentWindow.getFont().getWidth() 239 | if coord: 240 | value += 1 241 | return value 242 | 243 | 244 | class runeFont(io.font): 245 | def prerender(self, text): 246 | self.antialiase = 0 247 | # The version of font 3 used on the Mac has 6 characters for the 'bar filling up' 248 | # Other versions have 8 characters. This hack fixes the visual by changing 249 | # characters to the correct ones. 250 | if (zcode.header.TERP_NUMBER == zcode.header.TERP_MAC) and (settings.code in zcode.constants.beyond_zork_codes): 251 | text = text.replace('U', 'W') 252 | text = text.replace('T', 'V') 253 | text = text.replace('S', 'T') 254 | text = text.replace('R', 'S') 255 | text = text.replace('Q', 'R') 256 | return text 257 | 258 | 259 | font3 = runeFont(io.getBaseDir() + "//fonts//bzork.ttf", name="Runic Font") 260 | 261 | fontlist = [None, 262 | io.defaultFont, 263 | None, # picture font. Unspecified, should always return 0 264 | font3, # Beyond Zork font. Going to require some hacking. 265 | io.fixedFont 266 | ] 267 | 268 | 269 | def specialfont3(): 270 | for w in zwindow: 271 | w.fontlist[3] = io.fixedFont 272 | 273 | 274 | def resize(): 275 | if zcode.header.zversion != 6: 276 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight()) 277 | getWindow(1).setSize(ioScreen.getWidth(), getWindow(1).getSize()[1]) 278 | if zcode.memory.data[0] < 4: 279 | statusline.setSize(ioScreen.getWidth(), statusline.getSize()[1]) 280 | 281 | # Screen height (lines) 282 | zcode.header.setscreenheightlines(ioScreen.getHeight() // getWindow(1).getFont().getHeight()) 283 | # Screen width (chars) 284 | zcode.header.setscreenwidthchars(ioScreen.getWidth() // getWindow(1).getFont().getWidth()) 285 | 286 | if zcode.memory.data[0] > 4: 287 | # Screen width (units) 288 | if zcode.memory.data[0] == 6: 289 | zcode.header.setscreenwidth(ioScreen.getWidth()) 290 | else: 291 | zcode.header.setscreenwidth(ioScreen.getWidth() // getWindow(1).getFont().getWidth()) 292 | # Screen height (units) 293 | if zcode.memory.data[0] == 6: 294 | zcode.header.setscreenheight(ioScreen.getHeight()) 295 | else: 296 | zcode.header.setscreenheight(ioScreen.getHeight() // getWindow(1).getFont().getHeight()) 297 | 298 | if zcode.memory.data[0] < 4: # version 1, 2 and 3 299 | statusline.setSize(ioScreen.getWidth(), statusline.getFont().getHeight()) 300 | 301 | if zcode.memory.data[0] == 3: # version 3 302 | getWindow(1).setSize(ioScreen.getWidth(), getWindow(1).getSize()[1]) 303 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight() - (statusline.getSize()[1] + getWindow(1).getSize()[1])) 304 | else: # versions 1 and 2 305 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight() - statusline.getSize()[1]) 306 | else: # version 4, 5, 7 and 8 307 | getWindow(1).setSize(ioScreen.getWidth(), getWindow(1).getSize()[1]) 308 | getWindow(0).setSize(ioScreen.getWidth(), ioScreen.getHeight() - getWindow(1).getSize()[1]) 309 | 310 | if zcode.memory.data[0] == 6: 311 | zcode.header.setflag(2, 2, 1) 312 | 313 | 314 | def getWindow(winnum): 315 | winnum = zcode.numbers.signed(winnum) 316 | if winnum == -3: 317 | return currentWindow 318 | return zwindow[winnum] 319 | 320 | 321 | def updatestatusline(): # updates the status line for z-machine versions 1 to 3 322 | global currentWindow 323 | prevWindow = currentWindow 324 | currentWindow = statusline 325 | statusline.erase() 326 | if zcode.header.getflag(1, 1) == 1 and zcode.memory.data[0] == 3: 327 | type = 1 328 | else: 329 | type = 0 330 | statusline.setCursor(2 * statusline.getFont().getWidth() + 1, 1) 331 | location = zcode.objects.getShortName(zcode.game.getglobal(0)) 332 | if location == 0: 333 | zcode.error.strictz('Tried to print short name of object 0') 334 | else: 335 | statusline.printText(location) 336 | statusline.flushTextBuffer() 337 | if type == 0: 338 | statusline.setCursor(statusline.getSize()[0] - (23 * statusline.getFont().getWidth()) + 1, 1) 339 | score = str(zcode.game.getglobal(1)) 340 | statusline.printText('Score: ' + score) 341 | statusline.flushTextBuffer() 342 | statusline.setCursor(statusline.getSize()[0] - (12 * statusline.getFont().getWidth()) + 1, 1) 343 | turns = str(zcode.game.getglobal(2)) 344 | statusline.printText('Turns: ' + turns) 345 | statusline.flushTextBuffer() 346 | else: 347 | statusline.setCursor(statusline.getSize()[0] - (12 * statusline.getFont().getWidth()) + 1, 1) 348 | hours = str(zcode.game.getglobal(1)) 349 | minutes = str(zcode.game.getglobal(2)) 350 | if zcode.game.getglobal(2) < 10: 351 | minutes = '0' + minutes 352 | statusline.printText('Time: ' + hours + ':' + minutes) 353 | statusline.flushTextBuffer() 354 | statusline.flushTextBuffer() 355 | # Score: 999 Turns: 9999 # 356 | # Time: 20:52 # 357 | currentWindow = prevWindow 358 | 359 | 360 | def printtext(text): 361 | currentWindow.printText(text) 362 | 363 | 364 | mouse_window = 1 365 | 366 | # Styles definitions 367 | 368 | styles = { 'roman': 0, # Selecting this style unselects all other styles. 369 | 'bold': 1, # This style should make the text bold, or otherwise emphasized. 370 | 'italic': 2, # This one should make it italic. Some interpreters make it underlined. 371 | 'reverse': 4, # Selecting this style reverses the text colours. 372 | 'fixed': 8, # One of three ways to get fixed-pitch text. 373 | } 374 | 375 | # Colour definitions 376 | 377 | colours = { 'black': 2, 378 | 'red': 3, 379 | 'green': 4, 380 | 'yellow': 5, 381 | 'blue': 6, 382 | 'magenta': 7, 383 | 'cyan': 8, 384 | 'white': 9 385 | } 386 | 387 | special_colours = { 'current': 0, 388 | 'default': 1 389 | } 390 | v6_colours = { 'under_cursor': -1, 391 | 'light_grey': 10, 392 | 'medium_grey': 11, 393 | 'dark_grey': 12, 394 | 'transparent': 15 395 | } 396 | 397 | 398 | def checkcolours(): 399 | """Returns 1 if colours are available, 0 if unavailable.""" 400 | return io.checkcolour() 401 | 402 | 403 | # colours 404 | 405 | spectrum = { 2: 0x0000, # Black 406 | 3: 0x001D, # Red 407 | 4: 0x0340, # Green 408 | 5: 0x03BD, # Yellow 409 | 6: 0x59A0, # Blue 410 | 7: 0x7C1F, # Magenta 411 | 8: 0x77A0, # Cyan 412 | 9: 0x7FFF, # White 413 | 10: 0x5AD6, # Light Grey 414 | 11: 0x4631, # Medium Grey 415 | 12: 0x2D6B, # Dark Grey 416 | 15: -4 # Transparent 417 | } 418 | 419 | last_spectrum_colour = 0 420 | 421 | 422 | def convertBasicToRealColour(basic_colour): 423 | true_colour = convertBasicToTrueColour(basic_colour) 424 | if true_colour != None: 425 | return convertTrueToRealColour(true_colour) 426 | return None 427 | 428 | 429 | def convertRealToBasicColour(real_colour): 430 | true_colour = convertRealToTrueColour(real_colour) 431 | basic_colour = convertTrueToBasicColour(true_colour) 432 | return basic_colour 433 | 434 | 435 | def convertBasicToTrueColour(basic_colour): 436 | if basic_colour in spectrum: 437 | return spectrum[basic_colour] 438 | else: 439 | return None 440 | 441 | 442 | def convertTrueToBasicColour(true_colour): 443 | global last_spectrum_colour 444 | global spectrum 445 | highest_spectrum = 0 446 | for a in spectrum: 447 | if spectrum[a] == true_colour: 448 | return a 449 | 450 | if last_spectrum_colour == 0 or last_spectrum_colour == 255: 451 | last_spectrum_colour = 16 452 | else: 453 | last_spectrum_colour += 1 454 | spectrum[last_spectrum_colour] = true_colour 455 | return last_spectrum_colour 456 | 457 | 458 | def convertTrueToRealColour(true_colour): 459 | if true_colour == -4: 460 | return (0, 0, 0, 0) # transparent 461 | b = true_colour >> 10 462 | g = (true_colour >> 5) & 31 463 | r = true_colour & 31 464 | r = (r << 3) | (r >> 2) 465 | b = (b << 3) | (b >> 2) 466 | g = (g << 3) | (g >> 2) 467 | a = 0xff # alpha = fully opaque 468 | real_colour = (r, g, b, a) 469 | return real_colour 470 | 471 | 472 | def convertRealToTrueColour(colour): 473 | if colour[3] == 0: 474 | return -4 475 | r = colour[0] // 8 476 | g = colour[1] // 8 477 | b = colour[2] // 8 478 | c = (((b << 5) + g) << 5) + r 479 | return c 480 | 481 | 482 | # windows 483 | 484 | class window(io.window): 485 | def __str__(self): 486 | return 'window ' + str(self.window_id) 487 | 488 | # window properties (window coordinates are measured from 1,1) 489 | newline_routine = 0 490 | interrupt_countdown = 0 491 | basic_foreground_colour = 0 492 | basic_background_colour = 0 493 | font_number = 1 494 | font_size = 0 495 | attributes = 0 496 | true_foreground_colour = 0 497 | true_background_colour = 0 498 | 499 | relative_font_size = 0 500 | old_relative_font_size = 0 501 | window_id = None 502 | last_x_cursor = 1 503 | 504 | # styles 505 | reverse = False 506 | bold = False 507 | italic = False 508 | fixed = False 509 | 510 | def __init__(self, screen, font): 511 | self.screen = screen 512 | self.fontlist = fontlist[:] 513 | self.setFont(font) 514 | foreground = convertBasicToRealColour(2) 515 | background = convertBasicToRealColour(9) 516 | self.setRealColours(foreground, background) 517 | self.getFont().resetSize() 518 | 519 | def preFlush(self): 520 | self.font = self.getFont() 521 | self.font.setReverse(self.reverse) 522 | self.font.setBold(self.bold) 523 | self.font.setItalic(self.italic) 524 | 525 | def testfont(self, font): 526 | """Checks to see if the givenfont is available for use. Returns 1 if available, 0 if unavailable.""" 527 | if font > len(self.fontlist) - 1: 528 | return False 529 | if self.fontlist[font] == None: 530 | return False 531 | else: 532 | return True 533 | 534 | def setFontSize(self, newsize): 535 | self.flushTextBuffer() 536 | if self.getFont().defaultSize() + newsize < 1: 537 | return False 538 | if newsize > 10: 539 | return False 540 | self.old_relative_font_size = self.relative_font_size 541 | self.relative_font_size += newsize 542 | self.getFont().resize(newsize) 543 | if self.getFont().getHeight() > self.maxfontheight: 544 | self.maxfontheight = self.getFont().getHeight() 545 | ascent = self.getFont().getAscent() 546 | descent = self.getFont().getDescent() 547 | self.font_metrics = ((ascent & 255) << 8) + (descent & 255) 548 | return True 549 | 550 | def getFont(self): 551 | if (fixedpitchbit or self.fixed) and self.font_number == 1: 552 | return self.fontlist[4] 553 | return self.fontlist[self.font_number] 554 | 555 | def resetfontsize(self): 556 | s = self.old_relative_font_size - self.relative_font_size 557 | self.setfontsize(s) 558 | 559 | def atMargin(self): 560 | """Checks to see if the cursor is at the left margin""" 561 | if self.getCursor()[0] == self.left_margin + 1: 562 | return True 563 | else: 564 | return False 565 | 566 | def setBasicColours(self, foreground, background): 567 | self.basic_foreground_colour = foreground 568 | self.basic_background_colour = background 569 | 570 | def getBasicColours(self): 571 | return (self.basic_foreground_colour, self.basic_background_colour) 572 | 573 | def setTrueColours(self, foreground, background): 574 | basic_foreground = convertTrueToBasicColour(foreground) 575 | basic_background = convertTrueToBasicColour(background) 576 | self.true_foreground_colour = foreground 577 | self.true_background_colour = background 578 | self.setBasicColours(basic_foreground, basic_background) 579 | 580 | def getTrueColours(self): 581 | return (self.true_foreground_colour, self.true_background_colour) 582 | 583 | def setRealColours(self, foreground, background): 584 | true_foreground = convertRealToTrueColour(foreground) 585 | true_background = convertRealToTrueColour(background) 586 | self.setTrueColours(true_foreground, true_background) 587 | self.setColours(foreground, background) 588 | 589 | def setMargins(self, left, right): 590 | self.left_margin = left 591 | self.right_margin = right 592 | self.setCursorToMargin() 593 | 594 | def setStyle(self, style): 595 | self.flushTextBuffer() 596 | if style == 0: 597 | self.reverse = False 598 | self.bold = False 599 | self.italic = False 600 | self.fixed = False 601 | else: 602 | if style & 1: 603 | self.reverse = True 604 | if style & 2: 605 | self.bold = True 606 | if style & 4: 607 | self.italic = True 608 | if style & 8: 609 | self.fixed = True 610 | 611 | def getStyle(self): 612 | s = 0 613 | if self.reverse: 614 | s += 1 615 | if self.bold: 616 | s += 2 617 | if self.italic: 618 | s += 4 619 | if self.fixed: 620 | s += 8 621 | return s 622 | 623 | def setPosition(self, x, y): 624 | self.x_coord = x 625 | self.y_coord = y 626 | 627 | def getPosition(self): 628 | return (self.x_coord, self.y_coord) 629 | 630 | def getCursor(self): 631 | return (self.x_cursor, self.y_cursor) 632 | 633 | def setCursorToMargin(self): # makes sure the cursor is inside the margins 634 | if (self.getCursor()[0] <= self.left_margin) or (self.getCursor()[0] >= (self.getSize()[0] - self.right_margin)): 635 | self.setCursor(self.left_margin + 1, self.getCursor()[1]) 636 | 637 | def setprops(self, zproperty, value): 638 | """General purpose routine to set the value of a window property by property number. Generally not used.""" 639 | if zproperty == 0: 640 | self.setPosition(self.getPosition()[0], value) 641 | elif zproperty == 1: 642 | self.setPosition(value, self.getPosition()[1]) 643 | elif zproperty == 2: 644 | self.setSize(self.getSize()[0], value) 645 | elif zproperty == 3: 646 | self.setSize(value, self.getSize()[1]) 647 | elif zproperty == 4: 648 | self.setCursor(self.getCursor()[0], value) 649 | elif zproperty == 5: 650 | self.setCursor(value, self.getCursor()[1]) 651 | elif zproperty == 6: 652 | self.left_margin = value 653 | elif zproperty == 7: 654 | self.right_margin = value 655 | elif zproperty == 8: 656 | self.newline_routine = value 657 | elif zproperty == 9: 658 | self.interrupt_countdown = value 659 | elif zproperty == 10: 660 | self.setStyle(value) 661 | elif zproperty == 11: 662 | fg = convertBasicToRealColour(value & 255) 663 | bg = convertBasicToRealColour(value >> 8) 664 | self.setRealColours(fg, bg) 665 | elif zproperty == 12: 666 | self.setFontByNumber(value) 667 | elif zproperty == 13: 668 | pass 669 | elif zproperty == 14: 670 | self.attributes = value 671 | elif zproperty == 15: 672 | self.line_count = value 673 | elif zproperty == 16: 674 | pass 675 | elif zproperty == 17: 676 | pass 677 | elif zproperty == 18: 678 | pass 679 | else: 680 | return False 681 | 682 | def getprops(self, zproperty): 683 | """General purpose routine to retrieve the value of a window property, by property number. Also generally not used.""" 684 | if zproperty == 0: 685 | return self.getPosition()[1] 686 | elif zproperty == 1: 687 | return self.getPosition()[0] 688 | elif zproperty == 2: 689 | return self.getSize()[1] 690 | elif zproperty == 3: 691 | return self.getSize()[0] 692 | elif zproperty == 4: 693 | return self.getCursor()[1] 694 | elif zproperty == 5: 695 | return self.getCursor()[0] 696 | elif zproperty == 6: 697 | return self.left_margin 698 | elif zproperty == 7: 699 | return self.right_margin 700 | elif zproperty == 8: 701 | return self.newline_routine 702 | elif zproperty == 9: 703 | return self.interrupt_countdown 704 | elif zproperty == 10: 705 | return self.getStyle() 706 | elif zproperty == 11: 707 | fg, bg = self.getBasicColours() 708 | return (bg << 8) + fg 709 | elif zproperty == 12: 710 | return self.font_number 711 | elif zproperty == 13: 712 | return self.font_size 713 | elif zproperty == 14: 714 | return self.attributes 715 | elif zproperty == 15: 716 | return self.line_count 717 | elif zproperty == 16: 718 | return self.true_foreground_colour 719 | elif zproperty == 17: 720 | return self.true_background_colour 721 | elif zproperty == 18: 722 | return self.font_metrics 723 | else: 724 | return False 725 | 726 | # functions to retrieve more meaningful info from properties 727 | 728 | def setattributes(self, flags, operation): 729 | if operation == 0: # set attributes to match flags exactly 730 | self.attributes = flags 731 | elif operation == 1: # set attributes in flags, leave others as is 732 | self.attributes = self.attributes | flags 733 | elif operation == 2: # unset attributes in flags, leave others as is 734 | self.attributes = self.attributes & (flags ^ 15) 735 | elif operation == 3: # reverse attributes set in flags, leave others 736 | self.attributes = self.attributes ^ flags 737 | 738 | def testattribute(self, attribute): 739 | result = self.attributes & attribute 740 | if result == attribute: 741 | return True 742 | else: 743 | return False 744 | 745 | textbuffer = '' 746 | linetextbuffer = [] 747 | stylebuffer = [] 748 | linestylebuffer = [] 749 | 750 | def updatestylebuffer(self): 751 | pass 752 | 753 | def alterTabs(self): # changes the tab character to various spaces 754 | x = list(self.textbuffer) 755 | if len(x) > 0 and x[0] == '\t' and self.atMargin(): 756 | x[0] = ' ' 757 | self.textbuffer = ''.join(x) 758 | self.textbuffer = self.textbuffer.replace('\t', ' ') 759 | 760 | maxfontheight = 0 761 | 762 | def preNewline(self): 763 | # call the interrupt routine (if it's time to do so) 764 | if zcode.header.getterpnum() != 6 or data.getcode(zcode.memory.originaldata) != '393.890714': 765 | self.cdown = self.countdown() 766 | 767 | def postNewline(self): 768 | if zcode.header.getterpnum() == 6 and data.getcode(zcode.memory.originaldata) == '393.890714': 769 | self.cdown = self.countdown() 770 | 771 | def newline(self): 772 | self.preNewline() 773 | # move to the new line 774 | self.setCursor(self.getCursor()[0], self.getCursor()[1] + self.getFont().getHeight()) 775 | if self.getCursor()[1] >= self.getSize()[1] - self.getFont().getHeight(): 776 | if self.testattribute(2): 777 | self.scroll(self.getFont().getHeight()) # scroll the window region up 778 | self.setCursor(self.getCursor()[0], self.getCursor()[1] - self.getFont().getHeight()) 779 | if self.line_count != -999 and zcode.header.getscreenheightlines() != 255: 780 | self.line_count += 1 781 | 782 | # put the cursor at the current left margin 783 | self.setCursor(self.left_margin + 1, self.getCursor()[1]) 784 | if self.line_count >= (self.getSize()[1] // self.getFont().getHeight()) - 1 and self.testattribute(2): 785 | self.line_count = 0 786 | pfont = self.setFontByNumber(4) 787 | morestring = '[MORE]' 788 | self.drawText(morestring) 789 | self.setFontByNumber(pfont) 790 | 791 | while zcode.input.getInput(ignore=True) != 32: 792 | pass 793 | x = self.getCursor()[0] # + self.getPosition()[0] - 1 794 | y = self.getCursor()[1] # + self.getPosition()[1] - 1 795 | w = zcode.screen.currentWindow.getStringLength(morestring) 796 | h = zcode.screen.currentWindow.getStringHeight(morestring) 797 | zcode.screen.currentWindow.eraseArea(x, y, w, h) 798 | currentWindow.flushTextBuffer() 799 | 800 | self.postNewline() 801 | 802 | def countdown(self): 803 | if self.interrupt_countdown != 0: 804 | self.interrupt_countdown -= 1 805 | if self.interrupt_countdown == 0: 806 | i = zcode.game.interruptdata(zcode.game.INT_NEWLINE, self.newline_routine) 807 | zcode.game.interruptstack.append(i) 808 | zcode.game.interrupt_call() 809 | return 1 810 | 811 | def backspace(self, char): 812 | charwidth = self.getStringLength(char) 813 | charheight = self.getStringHeight(char) 814 | self.setCursor(self.getCursor()[0] - charwidth, self.getCursor()[1]) 815 | area = ((self.getPosition()[0] + self.getCursor()[0] - 1), (self.getPosition()[1] + self.getCursor()[1] - 1), charwidth, charheight) 816 | ioScreen.erase(self.getColours()[1], area) 817 | self.screen.update() 818 | 819 | def setFontByNumber(self, newfont): 820 | """Attempts to set the current font to newfont. Returns previous font number if successful, 0 if 821 | unsuccesful""" 822 | if self.testfont(newfont): 823 | self.flushTextBuffer() 824 | prevfont = self.getFontNumber() 825 | self.font_number = newfont 826 | self.setFont(self.fontlist[newfont]) 827 | self.font_size = (self.getFont().getHeight() << 8) + self.getFont().getWidth() 828 | return prevfont 829 | else: 830 | return 0 831 | 832 | def getFontNumber(self): 833 | """Returns the number of the current font.""" 834 | return self.font_number 835 | 836 | def getpic(self, picture_number): 837 | return io.getpic(ioScreen, picture_number) 838 | 839 | def drawpic(self, picture_number, x, y): 840 | pic = self.getpic(picture_number) 841 | if pic: 842 | pic.draw(self, (self.getPosition()[0] + x - 1), (self.getPosition()[1] + y - 1)) 843 | 844 | def erasepic(self, picture_data, x, y, scale=1): 845 | pic = io.image(picture_data) 846 | newwidth = pic.getWidth() * scale 847 | newheight = pic.getHeight() * scale 848 | 849 | if self.getColours()[1] != -4: 850 | area = ((self.getPosition()[0] + x - 1), (self.getPosition()[1] + y - 1), newwidth, newheight) 851 | ioScreen.erase(self.getColours()[1], area) 852 | 853 | def eraseline(self, length): 854 | if self.getColours()[1] != -4: 855 | ioScreen.erase(self.getColours()[1], (self.getPosition()[0] + self.getCursor()[0] - 1, self.getPosition()[1] + self.getCursor()[1] - 1, length, self.getFont().getHeight())) 856 | 857 | fontlist = [] 858 | 859 | 860 | def eraseWindow(winnum): 861 | if zcode.numbers.signed(winnum) < 0 and currentWindow.getColours()[1][3] == 0: 862 | pass 863 | elif zcode.numbers.signed(winnum) == -1: # this should unsplit the screen, too. And move the cursor. 864 | ioScreen.erase(currentWindow.getColours()[1]) 865 | split(0) 866 | for count, value in enumerate(zwindow): 867 | getWindow(count).setCursor(1, 1) 868 | getWindow(count).line_count = 0 869 | elif zcode.numbers.signed(winnum) == -2: # doesn't unsplit the screen, doesn't move the cursor 870 | ioScreen.erase(currentWindow.getColours()[1]) 871 | elif getWindow(winnum).getColours()[1][3] != 0: 872 | getWindow(winnum).erase() 873 | if zcode.memory.data[0] < 5 and winnum == 0: 874 | getWindow(0).setCursor(getWindow(0).getCursor()[0], getWindow(0).getSize()[1] - getWindow(0).getFont().getHeight()) 875 | 876 | 877 | def split(size): 878 | oldycoord = getWindow(0).getPosition()[1] 879 | 880 | getWindow(0).setPosition(1, size + 1) 881 | getWindow(0).setSize(getWindow(0).getSize()[0], ioScreen.getHeight() - size) 882 | 883 | getWindow(1).setPosition(1, 1) 884 | getWindow(1).setSize(getWindow(1).getSize()[0], size) 885 | 886 | if zcode.memory.data[0] == 3: 887 | eraseWindow(1) 888 | 889 | # move the window's cursor to the same absolute position it was in before the split 890 | 891 | difference = getWindow(0).getPosition()[1] - oldycoord 892 | getWindow(0).setCursor(getWindow(0).getCursor()[0], getWindow(0).getCursor()[1] - difference) 893 | 894 | # if the cursor now lies outside the window, move it to the window origin 895 | 896 | for a in (0, 1): 897 | if (getWindow(a).getCursor()[1] > getWindow(a).getSize()[1]) or (getWindow(a).getCursor()[1] < 1): 898 | getWindow(a).setCursor(1, 1) 899 | -------------------------------------------------------------------------------- /zcode/sounds.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | from ififf import blorb 16 | import vio.zcode as io 17 | import zcode 18 | from vio.zcode import soundchannels 19 | from zcode.constants import * 20 | 21 | AVAILABLE = False 22 | 23 | 24 | def setup(b): 25 | global device, channel, currentchannel, x, sounds, effectschannel, musicchannel 26 | global AVAILABLE, blorbs 27 | try: 28 | io.initsound() 29 | soundchannels[0].append(effectsChannel(0)) 30 | soundchannels[1].append(musicChannel(0)) 31 | AVAILABLE = True 32 | except: 33 | pass 34 | blorbs = b 35 | 36 | 37 | def availablechannels(arg): 38 | if AVAILABLE: 39 | return len(soundchannels[arg]) 40 | return 0 # no sound capablities, no sound channels available 41 | 42 | 43 | def bleep(type): # Either a high or a low bleep. 1 is high, 2 is low 44 | if type == 1: 45 | io.beep() 46 | if type == 2: 47 | io.boop() 48 | 49 | 50 | # It might be a good idea to check the relevant flag in Flags 2 to see 51 | # if the game wants to use sounds. If it does, various sound setting-up stuff can be done (such 52 | # as loading the relevant data from a blorb file) 53 | 54 | 55 | #SOUND 56 | 57 | 58 | class Channel(io.soundChannel): 59 | sound = None 60 | routine = None 61 | 62 | def getbusy(self): 63 | return False 64 | 65 | def Notify(self): 66 | if self.getbusy() == False and self.routine != None and self.routine != 0: 67 | self.stop() 68 | i = zcode.game.interruptdata(zcode.game.INT_SOUND, self.routine) 69 | zcode.game.interruptstack.append(i) 70 | zcode.game.interrupt_call() 71 | zcode.routines.execloop() 72 | 73 | 74 | class musicChannel(io.musicChannel): 75 | type = 1 76 | 77 | 78 | class effectsChannel(io.effectsChannel): 79 | type = 0 80 | 81 | def Notify(self): 82 | self.getbusy() 83 | if self.getbusy() == False and self.routine != None and self.routine != 0: 84 | #self.stop() 85 | i = zcode.game.interruptdata(zcode.game.INT_SOUND, self.routine) 86 | zcode.game.interruptstack.append(i) 87 | zcode.game.interrupt_call() 88 | zcode.routines.execloop() 89 | 90 | 91 | class Sound: 92 | def __init__(self, sound_number): 93 | self.number = sound_number 94 | for a in blorbs: 95 | sound_data = a.getSnd(sound_number) 96 | self.type = a.getSndType(sound_number) 97 | # Standards below 1.1 do not support seperate channels for different sound types, so we 98 | # just don't support music in that case 99 | if zcode.use_standard < STANDARD_11 and self.type != 0: 100 | self.sound = None 101 | elif sound_data: 102 | self.sound = io.sound(sound_data, self.type) 103 | else: 104 | self.sound = None 105 | 106 | def play(self, volume, repeats): 107 | try: 108 | self.sound.set_volume(volume) 109 | self.sound.play(loops=repeats) 110 | except: 111 | pass 112 | 113 | def stop(self): 114 | self.sound.stop() 115 | 116 | type = 0 117 | routine = 0 118 | repeats = 1 119 | playing = 1 120 | number = 0 121 | 122 | 123 | currentchannel = [1, 1] 124 | 125 | 126 | def playsound(sound, effect, volume, repeats, routine): 127 | """plays, prepares, stops or finishes with a sound.""" 128 | # the 'volume' data from the opcode 'sound_effect' contains both the volume and repeats data needed here 129 | if repeats == 255: 130 | repeats = -1 131 | else: 132 | repeats -= 1 133 | 134 | if effect == 1 or effect == 4: 135 | return False 136 | elif effect == 2: 137 | try: 138 | s = Sound(sound) 139 | soundchannels[s.type][currentchannel[s.type]-1].play(s, volume, repeats, routine) 140 | return True 141 | except: 142 | return False 143 | 144 | elif effect == 3: 145 | try: 146 | s = Sound(sound) 147 | soundchannels[s.type][currentchannel[s.type]-1].stop(s) 148 | return True 149 | except: 150 | return False 151 | 152 | 153 | def stopall(): 154 | for a in soundchannels: 155 | for b in a: 156 | b.stop(b.sound) 157 | -------------------------------------------------------------------------------- /zcode/text.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2001 - 2024 David Fillmore 2 | # 3 | # This file is part of Viola. 4 | # 5 | # Viola is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Viola is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | import settings 16 | import zcode 17 | from zcode.constants import * 18 | 19 | ibm_graphics_chars = { 191:0x2510, 192:0x2514, 217:0x2518, 218:0x250C, 196:0x2500, 179:0x2502, 24:0x2191, 25:0x2193 } 20 | appleiic_mousetext_chars = { 95:0x258f, 90:0x2595, 76:0x2594, 75:0x2191, 74:0x2193 } 21 | 22 | beyondzork = False 23 | 24 | 25 | def setup(gamecode): 26 | setupunitable() 27 | setupreverseunitable() 28 | setupalphatable() 29 | if gamecode in zcode.constants.beyond_zork_codes: 30 | beyondzork = True 31 | if zcode.header.getterpnum() == 9 or (zcode.header.getterpnum() == 6 and not zcode.header.getflag(2, 3)): 32 | zcode.screen.specialfont3() 33 | 34 | 35 | addrwibble = 0 36 | A0 = ' abcdefghijklmnopqrstuvwxyz' 37 | A1 = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ' 38 | A2 = ' \r0123456789.,!?_#\'"/\\-:()' 39 | 40 | unitable = { 155: 0xe4, 41 | 156: 0xf6, 42 | 157: 0xfc, 43 | 158: 0xc4, 44 | 159: 0xd6, 45 | 160: 0xdc, 46 | 161: 0xdf, 47 | 162: 0xbb, 48 | 163: 0xab, 49 | 164: 0xeb, 50 | 165: 0xef, 51 | 166: 0xff, 52 | 167: 0xcb, 53 | 168: 0xcf, 54 | 169: 0xe1, 55 | 170: 0xe9, 56 | 171: 0xed, 57 | 172: 0xf3, 58 | 173: 0xfa, 59 | 174: 0xfd, 60 | 175: 0xc1, 61 | 176: 0xc9, 62 | 177: 0xcd, 63 | 178: 0xd3, 64 | 179: 0xda, 65 | 180: 0xdd, 66 | 181: 0xe0, 67 | 182: 0xe8, 68 | 183: 0xec, 69 | 184: 0xf2, 70 | 185: 0xf9, 71 | 186: 0xc0, 72 | 187: 0xc8, 73 | 188: 0xcc, 74 | 189: 0xd2, 75 | 190: 0xd9, 76 | 191: 0xe2, 77 | 192: 0xea, 78 | 193: 0xee, 79 | 194: 0xf4, 80 | 195: 0xfb, 81 | 196: 0xc2, 82 | 197: 0xca, 83 | 198: 0xce, 84 | 199: 0xd4, 85 | 200: 0xdb, 86 | 201: 0xe5, 87 | 202: 0xc5, 88 | 203: 0xf8, 89 | 204: 0xd8, 90 | 205: 0xe3, 91 | 206: 0xf1, 92 | 207: 0xf5, 93 | 208: 0xc3, 94 | 209: 0xd1, 95 | 210: 0xd5, 96 | 211: 0xe6, 97 | 212: 0xc6, 98 | 213: 0xe7, 99 | 214: 0xc7, 100 | 215: 0xfe, 101 | 216: 0xf0, 102 | 217: 0xde, 103 | 218: 0xd0, 104 | 219: 0xa3, 105 | 220: 0x153, 106 | 221: 0x152, 107 | 222: 0xa1, 108 | 223: 0xbf 109 | } 110 | 111 | outputvalues = [0, 9, 11, 13] 112 | outputvalues.extend(list(range(32, 127))) 113 | #outputvalues.extend(list(range(129, 133))) 114 | #outputvalues.extend(list(range(133, 145))) 115 | #outputvalues.extend(list(range(145, 155))) 116 | outputvalues.extend(list(range(155, 252))) 117 | 118 | inputvalues = [8, 13, 27] 119 | inputvalues.extend(list(range(32, 127))) 120 | inputvalues.extend(list(range(129, 133))) 121 | inputvalues.extend(list(range(133, 145))) 122 | inputvalues.extend(list(range(145, 155))) 123 | inputvalues.extend(list(range(155, 252))) 124 | inputvalues.extend(list(range(252, 255))) 125 | 126 | 127 | def setupunitable(): 128 | global unitable 129 | if zcode.use_standard >= STANDARD_10: 130 | if zcode.header.zversion > 4: 131 | loc = zcode.header.unicodetableloc() 132 | if loc != 0: 133 | size = zcode.memory.getbyte(loc) 134 | loc += 1 135 | for a in range(size): 136 | unitable[155+a] = zcode.memory.getword(loc+(a*2)) 137 | elif zcode.use_standard == STANDARD_00: 138 | unitable = { 155: 0xe4, 139 | 156: 0xf6, 140 | 157: 0xfc, 141 | 158: 0xc4, 142 | 159: 0xd6, 143 | 160: 0xdc, 144 | 161: 0xdf, 145 | 162: 0xbb, 146 | 163: 0xab, 147 | } 148 | 149 | 150 | def setupreverseunitable(): 151 | global reverseunitable 152 | reverseunitable = {} 153 | for key in list(unitable.keys()): 154 | val = unitable[key] 155 | reverseunitable[val] = key 156 | 157 | 158 | def setupalphatable(): 159 | global A0, A1, A2 160 | if zcode.header.zversion == 1: 161 | A2 = ' 0123456789.,!?_#\'"/\\<-:()' 162 | if (zcode.header.zversion >= 5) and (zcode.header.alphatableloc != 0): 163 | loc = zcode.header.alphatableloc 164 | A0 = ' ' + zcode.memory.getarray(loc, 26).decode('latin-1') 165 | A1 = ' ' + zcode.memory.getarray(loc + 26, 26).decode('latin-1') 166 | A2 = ' ' + zcode.memory.getarray(loc + 52, 26).decode('latin-1') 167 | A2 = A2.replace(A2[7], '\r') 168 | 169 | 170 | def splitwords(word): 171 | chars = (word >> 10 & 31, word >> 5 & 31, word & 31) 172 | return chars 173 | 174 | 175 | def gettextlength(address): 176 | """determines how much space an encoded string takes up""" 177 | loc = address 178 | endbit = 0 179 | a = 1 180 | while endbit == 0: 181 | word = zcode.memory.getword(loc) 182 | if word & 0x8000: 183 | endbit = 1 184 | loc += 2 185 | a += 1 186 | 187 | return loc - address 188 | 189 | 190 | def undefinedChars(text): 191 | undefined = [] 192 | undefined.extend(range(0x0020)) 193 | undefined.extend(range(0x007f, 0x0100)) 194 | for c in undefined: 195 | text = text.replace(chr(c), chr(zcode.screen.currentWindow.font.missingGlyph)) 196 | return text 197 | 198 | 199 | def convertBZorkCode(code): 200 | if zcode.screen.currentWindow.getFontNumber() == 3 and zcode.header.getterpnum() == zcode.header.TERP_APPLEC: # apple iic 201 | try: 202 | return appleiic_mousetext_chars[code] 203 | except: 204 | return code 205 | elif zcode.header.getterpnum() == zcode.header.TERP_IBM and not zcode.header.getflag(2, 3): # ibm pc, and graphics not available 206 | try: 207 | return ibm_graphics_chars[code] 208 | except: 209 | return code 210 | return code 211 | 212 | 213 | def getZSCIIchar(code): 214 | if beyondzork: 215 | newcode = convertBZorkCode(code) 216 | if newcode != code: 217 | return chr(newcode) 218 | 219 | if code == 0: 220 | return '' 221 | if code == 1 and zcode.header.zversion == 1: 222 | return '\r' 223 | elif code == 9 and zcode.header.zversion == 6: # tab (needs to be smaller when using fixed pitch) 224 | return '\t' 225 | elif code == 11 and zcode.header.zversion == 6: # sentence space (needs to be smaller when using fixed pitch) 226 | return chr(0x2001) 227 | elif code == 13: 228 | return '\r' 229 | elif code > 31 and code < 127: 230 | return chr(code) 231 | elif code >= 155 and code <= 251: # extra chars 232 | try: 233 | char = unitable[code] 234 | char = chr(char) 235 | char = undefinedChars(char) 236 | except: 237 | zcode.error.warning(f'ZSCII character {code} undefined for output.') 238 | return '' 239 | 240 | return char 241 | else: 242 | zcode.error.warning(f'ZSCII character {code} undefined for output.') 243 | return '' 244 | 245 | 246 | def printabbrev(table, code): 247 | address = zcode.header.abbrevtableloc 248 | loc = 32 * (table-1) + code 249 | infoaddress = address + (loc * 2) 250 | wordaddress = zcode.memory.getword(infoaddress) 251 | textaddress = zcode.memory.wordaddress(wordaddress) 252 | chars = zcharstozscii(textaddress) 253 | return chars 254 | 255 | 256 | def zcharstozscii(address): 257 | global A0, A1, A2 258 | codes = [] 259 | loc = address 260 | finished = False 261 | while not finished: 262 | w = zcode.memory.getword(loc) 263 | c = splitwords(w) 264 | if w & 0x8000 == 0x8000: 265 | finished = True 266 | 267 | for a in c: 268 | codes.append(a) 269 | 270 | loc += 2 271 | 272 | # codes should now be a list of the z-characters, with Unicode string bytes 273 | # Need to convert this to ZSCII (and Unicode strings bytes) 274 | ZSCII = [] 275 | twocode = 0 276 | abbrev = 0 277 | 278 | temporary = False 279 | currentalpha = A0 280 | previousalpha = A0 281 | 282 | for a in codes: 283 | if abbrev != 0: 284 | ZSCII.extend(printabbrev(abbrev, a)) 285 | abbrev = 0 286 | elif twocode == 1: # first 5 bits of a 10-byte zscii code 287 | ZSCII.append(a << 5) 288 | twocode = 2 289 | elif twocode == 2: # last 5 bits of a 10-byte zscii code 290 | ZSCII[-1] = chr(ZSCII[-1] + a) 291 | twocode = 0 292 | elif a < 4 and a > 0 and zcode.header.zversion >= 3: 293 | abbrev = a 294 | elif a == 1 and zcode.header.zversion == 2: 295 | abbrev = a 296 | elif a == 4 and zcode.header.zversion >= 3: 297 | previousalpha = currentalpha 298 | currentalpha = A1 299 | temporary = True 300 | elif a == 5 and zcode.header.zversion >= 3: 301 | previousalpha = currentalpha 302 | currentalpha = A2 303 | temporary = True 304 | elif (a == 2 or a == 4) and zcode.header.zversion < 3: 305 | previousalpha = currentalpha 306 | if currentalpha == A0: 307 | currentalpha = A1 308 | elif currentalpha == A1: 309 | currentalpha = A2 310 | elif currentalpha == A2: 311 | currentalpha = A0 312 | if a == 2: 313 | temporary = True 314 | elif (a == 3 or a == 5) and zcode.header.zversion < 3: 315 | previousalpha = currentalpha 316 | if currentalpha == A0: 317 | currentalpha = A2 318 | elif currentalpha == A1: 319 | currentalpha = A0 320 | elif currentalpha == A2: 321 | currentalpha = A1 322 | if a == 3: 323 | temporary = True 324 | 325 | elif currentalpha == A2 and a == 6: # start a ten-byte zscii code 326 | twocode = 1 327 | if temporary == True: 328 | currentalpha = previousalpha 329 | temporary = False 330 | elif a == 1 and zcode.header.zversion == 1: 331 | ZSCII.append('\r') 332 | else: 333 | ZSCII.append(currentalpha[a]) 334 | if temporary == True: 335 | currentalpha = previousalpha 336 | temporary = False 337 | return ZSCII 338 | 339 | 340 | def zsciitounicode(zscii): 341 | text = [] 342 | unitext = [] 343 | for a in zscii: 344 | text.append(getZSCIIchar(ord(a)).encode('utf-8')) 345 | return b''.join(text) 346 | 347 | 348 | def decodetext(address, antirecurse=0): 349 | z = zcharstozscii(address) 350 | u = zsciitounicode(z) 351 | return u.decode('utf-8') 352 | 353 | 354 | def unicodetozscii(text): 355 | # needs to turn characters into '?' if we can't encode them 356 | w = [] 357 | for a in text: 358 | if ord(a) in list(reverseunitable.keys()): 359 | w.append(chr(reverseunitable[ord(a)])) 360 | else: 361 | if ord(a) not in outputvalues: 362 | w.append('?') 363 | else: 364 | w.append(a) 365 | return ''.join(w) 366 | 367 | 368 | def zsciitozchars(text, maxlen=-1): 369 | encoded = [] 370 | for a in text: 371 | if A0.find(a) != -1: 372 | encoded.append(A0.find(a)) 373 | elif A1.find(a) != -1: 374 | encoded.append(4) 375 | encoded.append(A1.find(a)) 376 | elif A2.find(a) != -1: 377 | encoded.append(5) 378 | encoded.append(A2.find(a)) 379 | else: # The letter is not in any of the three alphabets 380 | encoded.append(5) 381 | encoded.append(6) 382 | if ord(a) in outputvalues: 383 | encoded.append(ord(a) >> 5) 384 | encoded.append(ord(a) & 0x1f) 385 | 386 | if maxlen != -1: 387 | while len(encoded) > maxlen: 388 | encoded.pop() 389 | while len(encoded) < maxlen: 390 | encoded.append(5) 391 | 392 | words = [] 393 | words.append((encoded[0] << 10) + (encoded[1] << 5) + encoded[2]) 394 | words.append((encoded[3] << 10) + (encoded[4] << 5) + encoded[5]) 395 | if zcode.header.zversion > 3: 396 | words.append((encoded[6] << 10) + (encoded[7] << 5) + encoded[8]) 397 | words[-1] = words[-1] | 0x8000 398 | chars = [] 399 | for a in words: 400 | chars.append(a >> 8) 401 | chars.append(a & 0xff) 402 | return chars 403 | 404 | 405 | def encodetext(word): # encodes words for matching against the dictionary 406 | word = unicodetozscii(word) 407 | if zcode.header.zversion < 4: 408 | maxlen = 6 409 | else: 410 | maxlen = 9 411 | chars = zsciitozchars(word, maxlen) 412 | return chars 413 | --------------------------------------------------------------------------------