├── Makefile ├── README.md ├── g9x.c └── l9x.c /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # 4 | all: l9x-1 l9x 5 | 6 | # FIXME: add 6809 rules etc 7 | fuzix: l9x-z80 l9x-z80-1 8 | 9 | l9x-1: l9x.c 10 | $(CC) -O2 -Wall -pedantic -DTEXT_VERSION1 l9x.c -o ./l9x-1 11 | 12 | l9x: l9x.c 13 | $(CC) -O2 -Wall -pedantic l9x.c -o ./l9x 14 | 15 | l9x-z80-1: l9x.c 16 | fcc --nostdio -O2 -DVIRTUAL_GAME -DTEXT_VERSION1 l9x.c -c 17 | fcc -o l9x-z80-1 l9x.rel 18 | size.fuzix l9x-z80-1 19 | 20 | l9x-z80: l9x.c 21 | fcc --nostdio -O2 -DVIRTUAL_GAME l9x.c -c 22 | fcc -o l9x-z80 l9x.rel 23 | size.fuzix l9x-z80 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # L9X 2 | 3 | L9X is a very tight implementation of the Level 9 game engine. It is designed 4 | at this point to run version 2 game databases which must be just the database 5 | (look for L9Cut). Set the define according to the game. Erik The Viking has 6 | V1 style messages but a V2 database, the other V2 games are generally using 7 | the later message format with lengths not end markers. 8 | 9 | In theory a V1 game will work providing you put a proper V2 style header on the 10 | front and compile it with the V1 messages. 11 | 12 | # Things To Do 13 | 14 | Double check the parsing logic is correct with regards to unknown words and 15 | word counting 16 | 17 | Once the Fuzix console scrolling support is tweaked add graphics by forking and 18 | running a second graphics process. 19 | 20 | Put a V2 header on Colossal Cave and test it 21 | 22 | Autodetect the text table type somehow. 23 | 24 | # V3 and V4 games 25 | 26 | These are generally bigger and the interpreter also has to provide some rather 27 | uglier and more complicated decompressors and output handlers. Possibly doable 28 | but further down the list. 29 | 30 | -------------------------------------------------------------------------------- /g9x.c: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copright 2015 Alan Cox 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * This code is extensively based upon level9, the Level 9 interpreter 15 | * by Glen Summers, David Kinder, Alan Staniforth, Simon Baldwin, Dieter 16 | * Baron and Andreas Scherrer. 17 | */ 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | /* 27 | * Graphics driver for L9X 28 | * 29 | * This will be forked from the main process and own the upper 30 | * screen area. Right now it only understands V2 graphics format 31 | * and also has no ideas about output, it just generates the four 32 | * colour maps. 33 | * 34 | * When we've got the needed bits in place elsewhere we can then 35 | * fork this off and send it messages over a pipe to manage the 36 | * display. That also means we'll get the fancy behaviour of late 37 | * games for free when the graphics ran asynchronously. We will need 38 | * slightly more logic because we have to avoid getting further and 39 | * further behind, and we also have to handshake with the main game 40 | * when switching in and out of graphics mode, probably two pipes then! 41 | */ 42 | #define GFXSTACK_SIZE 64 43 | 44 | static char pictures[8192]; 45 | 46 | static uint8_t *gfxstack_base[GFXSTACK_SIZE]; 47 | static uint8_t **gfxstack = gfxstack_base; 48 | static uint16_t gfxscale_base[GFXSTACK_SIZE]; /* Check if byte will do */ 49 | static uint16_t *gfxscale = gfxscale_base; 50 | 51 | static const uint8_t scalemap[] = { 52 | 0x00, 0x02, 0x04, 0x06, 0x07, 0x09, 0x0c, 0x10 53 | }; 54 | 55 | static uint16_t scale; 56 | static uint8_t reflect; 57 | static uint8_t option; 58 | static uint8_t ink; 59 | static uint8_t palette[4]; 60 | static int16_t draw_x, draw_y, new_x, new_y; 61 | 62 | static uint8_t display[128 * 160 / 4]; 63 | 64 | /* The following routines will probably want to be in asm for any 8bit 65 | platform to provide sufficient speed */ 66 | 67 | static void mayplot(uint8_t x, uint8_t y) 68 | { 69 | uint8_t *byte; 70 | uint8_t shift = (x & 3) << 1; 71 | uint8_t b = 3 << shift; 72 | uint8_t c = ink << shift; 73 | uint8_t oc = option << shift; 74 | 75 | if (x < 0 || x > 160 || y < 0 || y > 127) 76 | return; 77 | 78 | x >>= 2; 79 | byte = display + (y * 160 / 4) + x; 80 | if ((*byte & b) == oc) { 81 | *byte &= ~b; 82 | *byte |= c; 83 | } 84 | } 85 | 86 | static void plot(uint8_t x, uint8_t y, uint8_t c) 87 | { 88 | uint8_t *byte; 89 | uint8_t shift = (x & 3) << 1; 90 | uint8_t b = 3 << shift; 91 | 92 | if (x < 0 || x > 160 || y < 0 || y > 127) 93 | return; 94 | 95 | c <<= shift;; 96 | 97 | x >>= 2; 98 | 99 | byte = display + (y * 160 / 4) + x; 100 | *byte &= ~b; 101 | *byte |= c; 102 | } 103 | 104 | static uint8_t peek(uint8_t x, uint8_t y) 105 | { 106 | uint8_t shift = (x & 3) << 1; 107 | uint8_t *byte = display + (y * 160 / 4) + (x >> 2); 108 | if (x < 0 || x > 159 || y < 0 || y > 127) 109 | return 255; 110 | return (*byte >> shift) & 3; 111 | } 112 | 113 | /* Likewise this needs a proper byte filling algorithm with minimal recursion 114 | or it'll be slow as molasses and eat the stack 115 | 116 | Basically you probably want to do 117 | 118 | if the byte we are on is entirely the precomputed old colour then set 119 | it to the precomputed new colour and do a left and right sweep. When doing 120 | the sweep add any byte above or below that is solid old colour unless you 121 | saw solid old colour last byte above/below as you sweep. 122 | 123 | when you hit bytes left/right fill the end pixels doing a pixel by pixel 124 | walk. 125 | 126 | During the pixel, and byte sweeps when you hit a byte that is not solid old, 127 | or solid new then add each pixel that doesn't match to the stack 128 | 129 | */ 130 | 131 | static void fill(uint8_t x, uint8_t y, uint8_t c, uint8_t c2) 132 | { 133 | if (peek(x, y) != c2) 134 | return; 135 | plot(x, y, c); 136 | fill(x+1, y, c, c2); 137 | fill(x-1, y, c, c2); 138 | fill(x, y-1, c, c2); 139 | fill(x, y+1, c, c2); 140 | } 141 | 142 | static void line(int16_t x, int16_t y, int16_t x1, int16_t y1) 143 | { 144 | int8_t stepy = 0; 145 | int8_t ydir = 1; 146 | int16_t dx; 147 | int16_t derr; 148 | int16_t acc; 149 | 150 | /* Do we need to draw multiple pixels up or across ? */ 151 | if (abs(y1 - y) > abs(x1 - x)) { 152 | int16_t t = x; 153 | x = y; 154 | y = t; 155 | t = x1; 156 | x1 = y1; 157 | y1 = t; 158 | stepy = 1; 159 | } 160 | /* Which way around ? */ 161 | if (x > x1) { 162 | int16_t t = x1; 163 | x1 = x; 164 | x = t; 165 | t = y1; 166 | y1 = y; 167 | y = t; 168 | } 169 | 170 | /* Work out our step and draw */ 171 | derr = abs(y1 - y); 172 | dx = x1 - x; 173 | if (y1 < y) { 174 | ydir = -1; 175 | } 176 | acc = dx >> 1; 177 | 178 | for (; x <= x1; x++) { 179 | if (stepy) 180 | mayplot(y, x); /* Inverted co-ords y is x x is y */ 181 | else 182 | mayplot(x, y); 183 | acc -= derr; 184 | if (acc < 0) { 185 | acc += dx; 186 | y += ydir; 187 | } 188 | } 189 | } 190 | 191 | static void error(const char *p) 192 | { 193 | fprintf(stderr, "%s\n", p); 194 | exit(1); 195 | } 196 | 197 | static void fill_current(uint8_t i) 198 | { 199 | uint8_t m = i & 3; 200 | if (m == 0) 201 | m = ink; 202 | fill(draw_x >> 6 , 127 - (draw_y >> 7), m, option & 3); 203 | } 204 | 205 | static void draw_line(void) 206 | { 207 | line(draw_x >> 6, 127 - (draw_y >> 7), new_x >> 6, 127 - (new_y >> 7)); 208 | } 209 | 210 | static uint8_t *gfind(uint16_t code) 211 | { 212 | uint8_t *p = pictures; 213 | uint8_t h = code >> 4; 214 | uint8_t l = (code & 0x0F) << 4; 215 | 216 | while(1) { 217 | /* Top 8bits of code */ 218 | uint8_t c = *p++; 219 | /* Next 4 bits of code, then 4 high bits of length */ 220 | uint8_t cl = *p++; 221 | /* Check for end mark */ 222 | if (c & 0x80) 223 | return NULL; 224 | if (c == h) { 225 | if ((cl & 0xF0) == l) 226 | return p + 1; 227 | } 228 | /* Move on to next record */ 229 | p += ((cl & 0x0F) << 8) | *p - 2; 230 | } 231 | } 232 | 233 | static void gfexecute(uint8_t * pc) 234 | { 235 | static int16_t x, y; 236 | 237 | if (pc == NULL) 238 | return; 239 | 240 | while (1) { 241 | uint8_t opcode = *pc++; 242 | uint8_t draw = 0; 243 | 244 | switch (opcode >> 6) { 245 | case 0: 246 | draw = 1; 247 | case 1: 248 | /* 0 - draw 1 - move */ 249 | x = (opcode & 0x18) >> 3; 250 | if (opcode & 0x20) 251 | x = (x | 0xFC) - 0x100; 252 | y = (opcode & 0x03) << 2; 253 | if (opcode & 0x04) 254 | y = (y | 0xF0) - 0x100; 255 | if (reflect & 2) 256 | x = -x; 257 | if (reflect & 1) 258 | y = -y; 259 | new_x = draw_x + ((x * scale) & ~7); 260 | new_y = draw_y + ((y * scale) & ~7); 261 | if (draw) 262 | draw_line(); 263 | draw_x = new_x; 264 | draw_y = new_y; 265 | break; 266 | case 2: 267 | *gfxstack++ = pc; 268 | *gfxscale++ = scale; 269 | pc = gfind(opcode & 0x3F); 270 | break; 271 | case 3: 272 | switch ((opcode >> 3) & 7) { 273 | case 0: 274 | draw = 1; 275 | case 1: 276 | { 277 | uint16_t coord = ((uint16_t)opcode << 8) | *pc++; 278 | x = (coord & 0x3E0) >> 5; 279 | if (coord & 0x400) 280 | x = (x | 0xE0) - 0x100; 281 | y = (coord & 0x0F) << 2; 282 | if (coord & 0x10) 283 | y = (y | 0xC0) - 0x100; 284 | if (reflect & 2) 285 | x = -x; 286 | if (reflect & 1) 287 | y = -y; 288 | new_x = draw_x + ((x * scale) & ~7); 289 | new_y = draw_y + ((y * scale) & ~7); 290 | if (draw) 291 | draw_line(); 292 | draw_x = new_x; 293 | draw_y = new_y; 294 | } 295 | break; 296 | case 2: 297 | ink = opcode & 3; 298 | break; 299 | case 3: 300 | opcode &= 7; 301 | if (!opcode) { 302 | scale = 0x80; 303 | /* Early games only */ 304 | gfxscale = gfxscale_base; 305 | } else { 306 | uint16_t ns = (scale * scalemap[opcode]) >> 3; 307 | if (ns > 0xff) { 308 | printf("SCALE OVERFLOW\n"); 309 | ns = 0xff; 310 | } 311 | scale = ns; 312 | } 313 | break; 314 | case 4: 315 | fill_current(opcode & 7); 316 | break; 317 | case 5: 318 | *gfxstack++ = pc + 1; 319 | *gfxscale++ = scale; 320 | pc = gfind(((((uint16_t)opcode) & 7) << 8) | *pc); 321 | break; 322 | case 6: 323 | if (opcode & 4) { 324 | opcode &= 3; 325 | opcode ^= reflect; 326 | } 327 | reflect = opcode; 328 | break; 329 | case 7: 330 | switch (opcode & 7) { 331 | case 1: 332 | opcode = *pc++; 333 | palette[(opcode >> 3) & 3] = opcode & 7; 334 | break; 335 | case 3: 336 | draw_x = 0x40 * *pc++; 337 | draw_y = 0x40 * *pc++; 338 | break; 339 | case 4: 340 | option = *pc ? ((*pc & 3) | 0x80) : 0; 341 | pc++; 342 | break; 343 | case 7: 344 | if (gfxstack == gfxstack_base) 345 | return; 346 | pc = *--gfxstack; 347 | /* Fall through */ 348 | case 5: 349 | if (gfxscale != gfxscale_base) 350 | scale = *--gfxscale; 351 | break; 352 | default: 353 | error("ILGFX"); 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | static void draw_picture(uint16_t pic) 361 | { 362 | ink = 3; 363 | option = 0; 364 | reflect = 0; 365 | draw_x = 0x1400; 366 | draw_y = 0x1400; 367 | scale = 0x80; 368 | gfxstack = gfxstack_base; 369 | gfxscale = gfxscale_base; 370 | gfexecute(gfind(0)); 371 | gfexecute(gfind(pic)); 372 | } 373 | 374 | static const char *palmap[8] = { 375 | "0 0 0", 376 | "255 0 0", 377 | "0 255 0", /* Some machines have a rather saner green */ 378 | "255 255 0", 379 | "0 0 255", 380 | "255 0 255", /* Magenta or brown, brown is better ! */ 381 | "0 255 255", 382 | "255 255 255" 383 | }; 384 | 385 | static void printrgb(FILE *o, uint8_t c) 386 | { 387 | fprintf(o, "%s ", palmap[palette[c]]); 388 | } 389 | 390 | static void write_ppm(FILE *o) 391 | { 392 | uint8_t c; 393 | uint8_t x, y; 394 | 395 | fprintf(o, "P3\n160 128 3\n"); 396 | 397 | for (y = 0; y < 128; y++) { 398 | for (x = 0; x < 160; x++) { 399 | c = peek(x, y); 400 | printrgb(o, c); 401 | } 402 | fprintf(o, "\n"); 403 | } 404 | } 405 | 406 | int main(int argc, char *argv[]) 407 | { 408 | int fd; 409 | FILE *o; 410 | 411 | if (argc != 3) { 412 | fprintf(stderr, "g9x: picturefile number\n"); 413 | exit(1); 414 | } 415 | fd = open(argv[1], O_RDONLY); 416 | if (fd == -1) { 417 | perror(argv[1]); 418 | exit(1); 419 | } 420 | if (read(fd, pictures, sizeof(pictures)) < 1024) { 421 | fprintf(stderr, "Invalid picture data.\n"); 422 | exit(1); 423 | } 424 | close(fd); 425 | draw_picture(atoi(argv[2])); 426 | 427 | o = fopen("out.ppm", "w"); 428 | if (o == NULL) { 429 | perror("out.ppm"); 430 | exit(1); 431 | } 432 | write_ppm(o); 433 | fclose(o); 434 | return 0; 435 | } 436 | 437 | -------------------------------------------------------------------------------- /l9x.c: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copright 2015 Alan Cox 3 | * 4 | * This program is free software; you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation; either version 2 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * Parts of this code are based upon poking through level9, the Level 9 15 | * interpreter by Glen Summers et al. 16 | */ 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | /* 29 | * Defines 30 | * 31 | * VIRTUAL_GAME : Page the game from disc file 32 | * STACKSIZE : Override stack size default (256) 33 | * LISTSIZE : Override list size default (1024) 34 | * TEXT_VERSION1 : Interpreter for early stype text strings 35 | */ 36 | 37 | #ifndef STACKSIZE 38 | #define STACKSIZE 256 /* Probably needs more for later games */ 39 | #endif 40 | #ifndef LISTSIZE 41 | #define LISTSIZE 1024 /* Later games need more */ 42 | #endif 43 | 44 | #ifdef VIRTUAL_GAME 45 | 46 | static uint8_t game[32]; 47 | #define game_base ((uint8_t *)NULL) 48 | 49 | #else 50 | 51 | /* Running from memory directly */ 52 | #define getb(x) *(x) 53 | static uint8_t game[27000]; 54 | #define game_base game 55 | 56 | #endif 57 | 58 | static int gamefile; 59 | 60 | static uint16_t gamesize; 61 | 62 | static uint8_t *messages; 63 | static uint8_t *worddict; 64 | static uint8_t *dictionary; 65 | static uint8_t *exitmap; 66 | static uint8_t *pc; 67 | static uint8_t *pcbase; 68 | 69 | static uint8_t game_over; 70 | 71 | static uint8_t opcode; 72 | 73 | static struct { 74 | uint16_t c_hash; 75 | uint16_t c_pc; 76 | uint16_t c_sp; 77 | uint16_t c_stackbase[STACKSIZE]; 78 | uint16_t c_variables[256]; 79 | uint8_t c_lists[LISTSIZE]; /* Probably much bigger for later games */ 80 | } context; 81 | 82 | #define variables context.c_variables 83 | #define lists context.c_lists 84 | #define stackbase context.c_stackbase 85 | 86 | uint16_t *stack = stackbase; 87 | 88 | static uint8_t *tables[16]; 89 | static uint8_t ttype[16]; 90 | 91 | static char buffer[80]; 92 | static uint8_t wordbuf[3]; 93 | static uint8_t wordcount; 94 | 95 | static uint16_t seed; /* Random numbers */ 96 | 97 | static void error(const char *p); 98 | 99 | /* 100 | * I/O routines. 101 | */ 102 | 103 | static char wbuf[80]; 104 | static int wbp = 0; 105 | static int xpos = 0; 106 | static uint8_t cols; 107 | 108 | static void display_init(void) 109 | { 110 | char *c; 111 | #ifdef TIOCGWINSZ 112 | struct winsize w; 113 | if (ioctl(0, TIOCGWINSZ, &w) != -1) { 114 | cols = w.ws_col; 115 | return; 116 | } 117 | #elif VTSIZE 118 | int16_t v = ioctl(0, VTSIZE, 0); 119 | if (v != -1) { 120 | cols = v & 0xFF; 121 | return; 122 | } 123 | #endif 124 | c = getenv("COLS"); 125 | cols = c ? atoi(c): 80; 126 | if (cols == 0) 127 | cols = 80; 128 | } 129 | 130 | static void display_exit(void) 131 | { 132 | } 133 | 134 | static void flush_word(void) 135 | { 136 | write(1, wbuf, wbp); 137 | xpos += wbp; 138 | wbp = 0; 139 | } 140 | 141 | static void char_out(char c) 142 | { 143 | if (c == '\n') { 144 | flush_word(); 145 | if (xpos) 146 | write(1, "\n", 1); 147 | xpos = 0; 148 | return; 149 | } 150 | if (c != ' ') { 151 | if (wbp < 80) 152 | wbuf[wbp++] = c; 153 | return; 154 | } 155 | if (xpos + wbp >= cols) { 156 | xpos = 0; 157 | write(1,"\n", 1); 158 | } 159 | flush_word(); 160 | write(1," ", 1); 161 | xpos++; 162 | } 163 | 164 | static void string_out(const char *p) 165 | { 166 | while(*p) 167 | char_out(*p++); 168 | } 169 | 170 | static void print_num(uint16_t v) 171 | { 172 | #ifdef __linux__ 173 | char buf[9]; 174 | snprintf(buf, 8, "%d", v); /* FIXME: avoid expensive snprintf */ 175 | string_out(buf); 176 | #else 177 | string_out(_itoa(v)); 178 | #endif 179 | } 180 | 181 | static void read_line(void) 182 | { 183 | int l = read(0, buffer, sizeof(buffer)); 184 | if (l < 0) 185 | error("read"); 186 | buffer[l] = 0; 187 | if (l && buffer[l-1] == '\n') 188 | buffer[l-1] = 0; 189 | xpos = 0; 190 | } 191 | 192 | static void read_filename(void) 193 | { 194 | string_out("Filename: "); 195 | read_line(); 196 | } 197 | 198 | static void print_char(uint8_t c) 199 | { 200 | if (c == 0x25) 201 | c = '\n'; 202 | else if (c == 0x5F) 203 | c = ' '; 204 | char_out(c); 205 | } 206 | 207 | static void error(const char *p) 208 | { 209 | display_exit(); 210 | write(2, p, strlen(p)); 211 | write(2, "\n", 1); 212 | exit(1); 213 | } 214 | 215 | #ifdef VIRTUAL_GAME 216 | 217 | #define NUM_PAGES 64 /* 16K */ 218 | 219 | static uint8_t last_ah; 220 | static uint8_t *last_base; 221 | 222 | #ifdef STATISTICS 223 | static unsigned long slow; 224 | static unsigned long miss; 225 | static unsigned long fast; 226 | #define STAT(x) ((x)++) 227 | #else 228 | #define STAT(x) 229 | #endif 230 | 231 | static uint8_t page_cache[NUM_PAGES][256]; 232 | static uint8_t page_addr[NUM_PAGES]; 233 | static uint8_t page_pri[NUM_PAGES]; /* 0 = unused , 1+ is use count */ 234 | 235 | static uint8_t page_alloc(void) 236 | { 237 | uint8_t low = 255; 238 | uint8_t i, lnum = 0; 239 | for (i = 0; i < NUM_PAGES; i++) { 240 | if (page_pri[i] == 0) 241 | return i; 242 | if (page_pri[i] < low) { 243 | lnum = i; 244 | low = page_pri[i]; 245 | } 246 | } 247 | return lnum; 248 | } 249 | 250 | static void page_sweep(void) 251 | { 252 | uint8_t i; 253 | for (i = 0; i < NUM_PAGES; i++) 254 | if (page_pri[i] > 1) 255 | page_pri[i] /= 2; 256 | } 257 | 258 | static void page_load(uint8_t slot, uint8_t ah) 259 | { 260 | page_addr[slot] = ah; 261 | page_pri[slot] = 0x80; 262 | /* Caution - last page is not packed so a short read isn't 263 | always an error */ 264 | if (lseek(gamefile, (ah << 8), SEEK_SET) < 0 || 265 | read(gamefile, page_cache[slot], 256) < 0) { 266 | error("pageload"); 267 | } 268 | /* Quick hack if you want to simulate disk loading on smaller 269 | devices */ 270 | /* usleep(10000);*/ /* DEBUG */ 271 | } 272 | 273 | static uint8_t page_find(uint8_t ah) 274 | { 275 | uint8_t i; 276 | for (i = 0; i < NUM_PAGES; i++) { 277 | if (page_addr[i] == ah) { 278 | STAT(slow); 279 | page_pri[i] |= 0x80; 280 | return i; 281 | } 282 | } 283 | page_sweep(); 284 | i = page_alloc(); 285 | page_load(i, ah); 286 | STAT(miss); 287 | return i; 288 | } 289 | 290 | static uint8_t getb(uint8_t *p) 291 | { 292 | uint16_t addr = (uint16_t)p; 293 | uint8_t ah = addr >> 8; 294 | uint8_t c; 295 | 296 | if (ah == last_ah) { 297 | STAT(fast); 298 | return last_base[addr&0xff]; 299 | } 300 | 301 | /* Find the right buffer */ 302 | c = page_find(ah); 303 | last_ah = ah; 304 | last_base = page_cache[c]; 305 | return last_base[addr & 0xff]; 306 | } 307 | 308 | #endif 309 | 310 | #ifdef TEXT_VERSION1 311 | /* Call initially with the messages as the pointer. Each message contains 312 | a mix of character codes and dictionary numbers giving pieces of text 313 | to substitute. The dictionary is permitted to self reference giving an 314 | extremely elegant compressor that beats the Infocom and AdventureSoft / 315 | Adveture International compressors while being smaller ! */ 316 | 317 | /* FIXME: for version 2 games they swapped the 1 markers for length bytes 318 | with an odd hack where a 0 length means 255 + nextbyte (unless 0 if so 319 | repeat */ 320 | static void decompress(uint8_t *p, uint16_t m) 321 | { 322 | uint8_t d; 323 | /* Walk the table looking for 1 bytes and counting off our 324 | input */ 325 | while(m--) 326 | while(getb(p++) != 1); 327 | while((d = getb(p++)) > 2) { 328 | if (d < 0x5E) 329 | print_char(d + 0x1d); 330 | else 331 | decompress(worddict, d - 0x5E); 332 | } 333 | } 334 | #else 335 | 336 | static uint8_t *msglen(uint8_t *p, uint16_t *l) 337 | { 338 | *l = 0; 339 | while(!getb(p)) { 340 | *l += 255; 341 | p++; 342 | } 343 | *l += getb(p++); 344 | return p; 345 | } 346 | 347 | static void decompress(uint8_t *p, uint16_t m) 348 | { 349 | uint8_t d; 350 | uint16_t l; 351 | /* Walk the table skipping messages */ 352 | if (m == 0) 353 | return; 354 | while(--m) { 355 | p = msglen(p, &l); 356 | p += l - 1; 357 | } 358 | p = msglen(p, &l); 359 | /* A 1 byte message means its 0 text chars long */ 360 | while(--l) { 361 | d = getb(p++); 362 | if (d < 3) 363 | return; 364 | if (d < 0x5E) 365 | print_char(d + 0x1d); 366 | else 367 | decompress(worddict - 1, d - 0x5d); 368 | } 369 | } 370 | #endif 371 | 372 | static void print_message(uint16_t m) 373 | { 374 | decompress(messages, m); 375 | } 376 | 377 | /* 378 | * More complex bits the engine has methods for 379 | */ 380 | 381 | static uint8_t reverse[] = { 382 | 0x10, 0x14, 0x16, 0x17, 383 | 0x11, 0x18, 0x12, 0x13, 384 | 0x15, 0x1a, 0x19, 0x1c, 385 | 0x1b 386 | }; 387 | 388 | static void lookup_exit(void) 389 | { 390 | uint8_t l = variables[getb(pc++)]; 391 | uint8_t d = variables[getb(pc++)]; 392 | uint8_t *p = exitmap; 393 | uint8_t v; 394 | uint8_t ls = l; 395 | 396 | /* Scan through the table finding 0x80 end markers */ 397 | l--; /* No entry 0 */ 398 | while (l--) { 399 | do { 400 | v = getb(p); 401 | p += 2; 402 | } while (!(v & 0x80)); 403 | } 404 | /* Now find our exit */ 405 | /* Basically each entry is a word in the form 406 | [Last.1][BiDir.1][Flags.2][Exit.4][Target.8] */ 407 | do { 408 | v = getb(p); 409 | if ((v & 0x0F) == d) { 410 | variables[getb(pc++)] = ((getb(p++)) >> 4) & 7; /* Flag bits */ 411 | variables[getb(pc++)] = getb(p++); 412 | return; 413 | } 414 | p+=2; 415 | } while(!(v & 0x80)); 416 | /* Exits can be bidirectional - we have to now sweep the whole table looking 417 | for a backlinked exit */ 418 | if (d <= 12) { 419 | d = reverse[d]; 420 | p = exitmap; 421 | l = 1; 422 | do { 423 | v = getb(p++); 424 | if (getb(p++) == ls && ((v & 0x1f) == d)) { 425 | variables[getb(pc++)] = (v >> 4) & 7; 426 | variables[getb(pc++)] = l; 427 | return; 428 | } 429 | if (v & 0x80) 430 | l++; 431 | } while(getb(p)); 432 | } 433 | variables[getb(pc++)] = 0; 434 | variables[getb(pc++)] = 0; 435 | } 436 | 437 | static uint8_t wordcmp(char *s, uint8_t *p, uint8_t *v) 438 | { 439 | do { 440 | if (*s != 0 && toupper(*s++) != (getb(p) & 0x7F)) 441 | return 0; 442 | } while(!(getb(p++) & 0x80)); 443 | *v = getb(p); 444 | return 1; 445 | } 446 | 447 | static uint8_t matchword(char *s) 448 | { 449 | uint8_t *p = dictionary; 450 | uint8_t v; 451 | 452 | do { 453 | /* outword(p); */ 454 | if (wordcmp(s, p, &v) == 1) 455 | return v; 456 | /* Find the next word */ 457 | while(getb(p) && !(getb(p) & 0x80)) 458 | p++; 459 | p++; 460 | p++; 461 | } while ((getb(p) & 0x80) == 0); 462 | /* FIXME: correct code for non match check */ 463 | return 0xFF; 464 | } 465 | 466 | static void do_input(void) 467 | { 468 | uint8_t *w = wordbuf; 469 | char *p = buffer; 470 | char *s; 471 | 472 | wordcount = 0; 473 | read_line(); 474 | 475 | while(*p) { 476 | while (isspace(*p)) 477 | p++; 478 | /* Now at word start */ 479 | wordcount++; 480 | /* Check - do we count unknown words */ 481 | s = p; 482 | while(*p && !isspace(*p)) 483 | p++; 484 | /* The text between s and p-1 is now the word */ 485 | *p++ = 0; 486 | if (w < wordbuf + sizeof(wordbuf)) 487 | *w++ = matchword(s); 488 | } 489 | 490 | /* Finally put the first 3 words and the count into variables */ 491 | w = wordbuf; 492 | variables[getb(pc++)] = *w++; 493 | variables[getb(pc++)] = *w++; 494 | variables[getb(pc++)] = *w++; 495 | variables[getb(pc++)] = wordcount; 496 | } 497 | 498 | /* This is fairly mindless but will do for now */ 499 | static uint16_t hash(void) 500 | { 501 | uint8_t h = 0; 502 | uint8_t *gp = game; 503 | while(gp < game + 32) 504 | h += *gp++; 505 | return h; 506 | } 507 | 508 | static char savefail[] = "Save failed\n"; 509 | static char loadfail[] = "Load failed\n"; 510 | 511 | static void save_game(void) 512 | { 513 | int fd; 514 | read_filename(); 515 | if (!*buffer) 516 | return; 517 | fd = open(buffer, O_WRONLY|O_TRUNC|O_CREAT, 0600); 518 | if (fd == -1) { 519 | string_out(savefail); 520 | return; 521 | } 522 | context.c_pc = pc - pcbase; 523 | context.c_sp = stack - stackbase; 524 | context.c_hash = hash(); 525 | if (write(fd, &context, sizeof(context)) != sizeof(context)) 526 | string_out(savefail); 527 | close(fd); 528 | } 529 | 530 | static void load_game(void) 531 | { 532 | int fd; 533 | 534 | read_filename(); 535 | if (!*buffer) 536 | return; 537 | fd = open(buffer, O_RDONLY); 538 | if (fd == -1) { 539 | string_out(loadfail); 540 | return; 541 | } 542 | if (read(fd, &context, sizeof(context)) != sizeof(context) || 543 | context.c_hash != hash()) { 544 | string_out(loadfail); 545 | memset(lists, 0, sizeof(lists)); 546 | memset(variables, 0, sizeof(variables)); 547 | pc = pcbase; 548 | } else { 549 | pc = pcbase + context.c_pc; 550 | stack = stackbase + context.c_sp; 551 | } 552 | close(fd); 553 | } 554 | 555 | /* 556 | * Implement the core Level 9 machine (for version 1 and 2 anyway) 557 | * 558 | * This is an extremely elegant and very compact bytecode with 559 | * various helpers for "game" things. 560 | */ 561 | 562 | static uint16_t constant(void) 563 | { 564 | uint16_t r = getb(pc++); 565 | if (!(opcode & 0x40)) 566 | r |= (getb(pc++)) << 8; 567 | return r; 568 | } 569 | 570 | static uint8_t *address(void) 571 | { 572 | if (opcode & 0x20) { 573 | int8_t s = (int8_t)getb(pc++); 574 | return pc + s - 1; 575 | } 576 | pc += 2; 577 | return pcbase + getb(pc-2) + (getb(pc-1) << 8); 578 | } 579 | 580 | static void skipaddress(void) 581 | { 582 | if (!(opcode & 0x20)) 583 | pc++; 584 | pc++; 585 | } 586 | 587 | /* List ops access a small fixed number of tables */ 588 | static void listop(void) 589 | { 590 | uint8_t t = (opcode & 0x1F) + 1; 591 | uint8_t *base = tables[t]; 592 | if (base == NULL) 593 | error("BADL"); 594 | if (opcode & 0x20) 595 | base += variables[getb(pc++)]; 596 | else 597 | base += getb(pc++); 598 | if ((base >= game_base && base < game_base + gamesize) || 599 | (base >= lists && base < lists + sizeof(lists))) { 600 | if (!(opcode & 0x40)) { 601 | if (ttype[t]) 602 | variables[getb(pc++)] = *base; 603 | else 604 | variables[getb(pc++)] = getb(base); 605 | } else { 606 | if (ttype[t] == 0) 607 | error("WFLT"); 608 | *base = variables[getb(pc++)]; 609 | } 610 | } else { 611 | error("LFLT"); 612 | } 613 | } 614 | 615 | static void execute(void) 616 | { 617 | uint8_t *base; 618 | uint8_t tmp; 619 | uint16_t tmp16; 620 | 621 | 622 | while(!game_over) { 623 | opcode = getb(pc++); 624 | if (opcode & 0x80) 625 | listop(); 626 | else switch(opcode & 0x1f) { 627 | case 0: 628 | pc = address(); 629 | break; 630 | case 1: { 631 | uint8_t *newpc = address(); 632 | if (stack == stackbase + sizeof(stackbase)) 633 | error("stack overflow"); 634 | *stack++ = pc - pcbase; 635 | pc = newpc; 636 | } 637 | break; 638 | case 2: 639 | if (stack == stackbase) 640 | error("stack underflow"); 641 | pc = pcbase + *--stack; 642 | break; 643 | case 3: 644 | print_num(variables[getb(pc++)]); 645 | break; 646 | case 4: 647 | print_message(variables[getb(pc++)]); 648 | break; 649 | case 5: 650 | print_message(constant()); 651 | break; 652 | case 6: 653 | switch(getb(pc++)) { 654 | case 1: 655 | game_over = 1; 656 | /* FIXME: call driver in later game engines */ 657 | break; 658 | case 2: 659 | /* Emulate the random number algorithm in the original */ 660 | seed = (((seed << 8) + 0x0A - seed) << 2) + seed + 1; 661 | variables[getb(pc++)] = seed & 0xff; 662 | break; 663 | case 3: 664 | save_game(); 665 | break; 666 | case 4: 667 | load_game(); 668 | break; 669 | case 5: 670 | memset(variables, 0, sizeof(variables)); 671 | break; 672 | case 6: 673 | stack = stackbase; 674 | break; 675 | default: 676 | /* fprintf(stderr, "Unknown driver function %d\n", pc[-1]); */ 677 | error("unkndriv"); 678 | } 679 | break; 680 | case 7: 681 | do_input(); 682 | break; 683 | case 8: 684 | tmp16 = constant(); 685 | variables[getb(pc++)] = tmp16; 686 | break; 687 | case 9: 688 | variables[getb(pc + 1)] = variables[getb(pc)]; 689 | pc += 2; 690 | break; 691 | case 10: 692 | variables[getb(pc + 1)] += variables[getb(pc)]; 693 | pc += 2; 694 | break; 695 | case 11: 696 | variables[getb(pc + 1)] -= variables[getb(pc)]; 697 | pc += 2; 698 | break; 699 | case 14: /* This looks weird, but its basically a jump table */ 700 | base = pcbase + (getb(pc) + (getb(pc + 1) << 8)); 701 | base += 2 * variables[getb(pc + 2)]; /* 16bit entries * */ 702 | pc = pcbase + getb(base) + (getb(base + 1) << 8); 703 | break; 704 | case 15: 705 | lookup_exit(); 706 | break; 707 | case 16: 708 | /* These two are defined despite gcc whining. It doesn't matter 709 | which way around they get evaluated */ 710 | if (variables[getb(pc++)] == variables[getb(pc++)]) 711 | pc = address(); 712 | else 713 | skipaddress(); 714 | break; 715 | case 17: 716 | if (variables[getb(pc++)] != variables[getb(pc++)]) 717 | pc = address(); 718 | else 719 | skipaddress(); 720 | break; 721 | case 18: 722 | tmp = getb(pc++); 723 | if (variables[tmp] < variables[getb(pc++)]) 724 | pc = address(); 725 | else 726 | skipaddress(); 727 | break; 728 | case 19: 729 | tmp = getb(pc++); 730 | if (variables[tmp] > variables[getb(pc++)]) 731 | pc = address(); 732 | else 733 | skipaddress(); 734 | break; 735 | case 24: 736 | if (variables[getb(pc++)] == constant()) 737 | pc = address(); 738 | else 739 | skipaddress(); 740 | break; 741 | case 25: 742 | if (variables[getb(pc++)] != constant()) 743 | pc = address(); 744 | else 745 | skipaddress(); 746 | break; 747 | case 26: 748 | if (variables[getb(pc++)] < constant()) 749 | pc = address(); 750 | else 751 | skipaddress(); 752 | break; 753 | case 27: 754 | if (variables[getb(pc++)] > constant()) 755 | pc = address(); 756 | else 757 | skipaddress(); 758 | break; 759 | case 21: 760 | /* clear screen */ 761 | pc++; /* value indicates screen to clear */ 762 | break; 763 | case 22: 764 | /* picture */ 765 | pc++; 766 | break; 767 | case 20: 768 | /* graphics mode */ 769 | case 23: 770 | /* getnextobject */ 771 | case 28: 772 | /* print input */ 773 | default: 774 | /* fprintf(stderr, "bad op %d\n", opcode); */ 775 | error("badop"); 776 | } 777 | } 778 | } 779 | 780 | 781 | int main(int argc, char *argv[]) 782 | { 783 | uint8_t off = 4; 784 | int i; 785 | 786 | if (argc == 1) 787 | error("l9x [game.dat]\n"); 788 | 789 | gamefile = open(argv[1], O_RDONLY); 790 | if (gamefile == -1) { 791 | perror(argv[1]); 792 | exit(1); 793 | } 794 | /* FIXME: allocate via sbrk once removed stdio usage */ 795 | if ((gamesize = read(gamefile, game, sizeof(game))) < 32) 796 | error("l9x: not a valid game\n"); 797 | #ifdef VIRTUAL_GAME 798 | gamesize = 0xff00; 799 | memset(page_addr, 0xff, sizeof(page_addr)); 800 | #else 801 | close(gamefile); 802 | #endif 803 | 804 | /* Header starts with message and decompression dictionary */ 805 | messages = game_base + (game[0] | (game[1] << 8)); 806 | worddict = game_base + (game[2] | (game[3] << 8)); 807 | /* Then the tables for list ops */ 808 | for (i = 0; i < 12; i++) { 809 | uint16_t v = game[off] | (game[off + 1] << 8); 810 | if (i != 11 && (v & 0x8000)) { 811 | tables[i] = lists + (v & 0x7FFF); 812 | ttype[i] = 1; 813 | } else 814 | tables[i] = game_base + v; 815 | off += 2; 816 | } 817 | /* Some of which have hard coded uses and always point into game */ 818 | exitmap = tables[0]; 819 | dictionary = tables[1]; 820 | pcbase = pc = tables[11]; 821 | /* 3 and 4 are used for getnextobject and friends on later games, 822 | 9 is used for driver magic and ramsave stuff */ 823 | 824 | display_init(); 825 | 826 | seed = time(NULL); 827 | 828 | execute(); 829 | 830 | #ifdef STATISTICS 831 | printf("Fast %d Slow %d Miss %d\n", fast, slow, miss); 832 | #endif 833 | } 834 | --------------------------------------------------------------------------------