Information wants to be free...

CP/M-68K for Motorola 68332

I have created a CP/M BIOS to be able to run CP/M-68K on an embedded system with a Motorola 68332 MCU. The 68332 contains a "Queued Serial Module" that the BIOS interacts with to provide a console for CP/M over a RS-232 serial port. There is only support for one disk, which is a RAM disk implementation, also handled by the BIOS. During development I relied heavily on the BDM functionality which I made an adapter for, but this can also be used to load the CP/M system and RAM disk image directly into memory through the use of S-records.

Since CP/M-68K programs are written for pure Motorola 68000 CPUs there is a known incompatibility with one particular "MOVE SR,xx" instruction in Motorola 68010 and later CPUs including the 68332 core. It is possible to patch this in software, so this BIOS implements the DeciGEL method originally used by some upgraded Amiga systems.

The particular board I am using has 256KB of RAM available, mapped starting at 0x100000 and I ended up with the following memory map by dividing it into 16x16KB parts:

|--------|--------|----------|
| Start  | End    | Use      |
|--------|--------|----------|
| 100000 | 103FFF | VBR      |
| 104000 | 107FFF | TPA      |
| 108000 | 10BFFF | TPA      |
| 10C000 | 10FFFF | TPA      |
| 110000 | 113FFF | TPA      |
| 114000 | 117FFF | RAM-Disk |
| 118000 | 11BFFF | RAM-Disk |
| 11C000 | 11FFFF | RAM-Disk |
| 120000 | 123FFF | RAM-Disk |
| 124000 | 127FFF | RAM-Disk |
| 128000 | 12BFFF | RAM-Disk |
| 12C000 | 12FFFF | RAM-Disk |
| 130000 | 133FFF | RAM-Disk |
| 134000 | 137FFF | RAM-Disk |
| 138000 | 13BFFF | CP/M     |
| 13C000 | 13FFFF | CP/M     |
|--------|--------|----------|
          

This board already has it's own BIOS that sets up the hardware (e.g. the UART in the Queued Serial Module) and ends up pointing the CPU Vector Base Register (VBR) to 0x100000. TPA is the "Transient Program Area" used by programs that run within CP/M.

The RAM disk image can be prepared on another Linux computer using cpmtools and the following definition:

diskdef ram-68332
  seclen 128
  tracks 72
  sectrk 16
  blocksize 1024
  maxdir 64
  skew 0
  boottrk 0
  os 2.2
end
          


The commands involved in preparing the disk and then a S-record representation would typically be:

mkfs.cpm -f ram-68332 ramdisk.bin
cpmcp -f ram-68332 ramdisk.bin <file> 0:<file>
srec_cat ramdisk.bin -binary -offset 0x114000 -o ramdisk.s68 -motorola -disable=data-count -execution_start_address 0
          


Assembling the BIOS and linking CP/M-68K for a new system requires another already running CP/M-68K system with the development tools. The Digital Research documentation from the 80's suggests using a Motorola EXORmacs development system, but luckily we have emulators available for this purpose now.

From within the emulator the following steps are used to assemble, link and relocate the CP/M-68K system binary. The final command dumps the binary in S-record format to the console:

AS68 BIOS332.S
LO68 -R -UCPM -O CPM.REL CPMLIB BIOS332.O
RELOC -B138000 CPM.REL CPM.SYS
SENDC68 CPM.SYS
          

The CPMLIB library I got from the CP/M-68K version 1.3 disk set here.

Here is the BIOS assembly source code (BIOS332.S) in 68000 assembly language, it is based on the "ERG" BIOS from the Digital Research manuals:

*****************************************************************
*                                                               *
*               CP/M-68K BIOS                                   *
*       Basic Input/Output Subsystem                            *
*       For Motorola 68332 Embedded System                      *
*                                                               *
*****************************************************************

        .globl  _init           * BIOS initialization entry point
        .globl  _ccp            * CCP entry point

_init:  move.l  #traphndl,$10008c * Set up trap #3 handler (on VBR offset)
        move.l  #privhndl,$100020 * Catch privilege exception (on VBR offset)
        move.l  #welcome,a0     * Display welcome message
weloop: move.b  (a0)+,d1
        cmpi.b  #$24,d1         * Compare against '$'
        beq     wedone
        jsr     conout
        bra     weloop
wedone: clr.l   d0              * Log on disk A:, user 0
        rts

traphndl:
        cmpi    #nfuncs,d0
        bcc     trapng
        lsl     #2,d0           * Multiply bios function by 4
        movea.l 6(pc,d0),a0     * Get handler address
        jsr     (a0)            * Call handler
trapng:
        rte

biosbase:
        .dc.l  _init    *  0 - Initialization
        .dc.l  wboot    *  1 - Warm boot
        .dc.l  constat  *  2 - Console status
        .dc.l  conin    *  3 - Read console character
        .dc.l  conout   *  4 - Write console character
        .dc.l  lstout   *  5 - List character output
        .dc.l  pun      *  6 - Auxiliary output
        .dc.l  rdr      *  7 - Auxiliary input
        .dc.l  home     *  8 - Home
        .dc.l  seldsk   *  9 - Select disk drive
        .dc.l  settrk   * 10 - Select track number
        .dc.l  setsec   * 11 - Select sector number
        .dc.l  setdma   * 12 - Set DMA address
        .dc.l  read     * 13 - Read sector
        .dc.l  write    * 14 - Write sector
        .dc.l  listst   * 15 - Return list status
        .dc.l  sectran  * 16 - Sector translate
        .dc.l  home     * 17 - N/A
        .dc.l  getseg   * 18 - Get address of memory region table
        .dc.l  getiob   * 19 - Get I/O byte
        .dc.l  setiob   * 20 - Set I/O byte
        .dc.l  flush    * 21 - Flush buffers
        .dc.l  setexc   * 22 - Set exception handle address

        nfuncs=(*-biosbase)/4

wboot:  jmp     _ccp

constat: move.b $fffc0d,d0      * Get status from SCSR register
        andi.b  #$40,d0         * Check for RDRF=1 data available?
        beq     noton           * Branch if not
        moveq.l #$1,d0          * Set result to true
        rts
noton:  clr.l   d0              * Set result to false
        rts

conin:  bsr     constat         * See if key pressed
        tst     d0
        beq     conin           * Wait until key pressed
        move.b  $fffc0f,d0      * Get key from SCDR register
        rts

conout: move.b  $fffc0c,d0      * Get status from SCSR register
        andi.b  #$01,d0         * Check for TDRE=1 transmit OK?
        beq     conout          * Wait until our port has aged...
        move.b  d1,$fffc0f      * And output it to SCDR register
        rts

lstout: rts

pun:    rts

rdr:    rts

listst: move.b  #$ff,d0 * Device not ready
        rts

home:   rts

seldsk:
        moveq   #0,d0
        cmpi.b  #1,d1           * Only disk A: RAM disk supported
        bpl     selrtn          * Return 0 in d0 for other disks
        move.l  #$114000,selmem * Prepare base memory address for RAM disk
        move.l  #dph,d0         * Point d0 at dph
selrtn: rts

settrk: move.b  d1,track
        rts

setsec: move.b  d1,sector
        rts

setdma: move.l  d1,dma
        rts

sectran: move.w d1,d0   * No sector translation, just 1-to-1 mapping
        rts

read:
        clr.l   d0
        move.b  track,d0
        mulu    #16,d0          * Multiply by SPT
        add.b   sector,d0
        mulu    #128,d0         * Multiply by sector size
        add.l   selmem,d0       * Add offset for RAM disk address in memory
        move.l  dma,a0
        move.l  d0,a1
        move.l  #127,d0         * Read 128 bytes
rloop:  move.b  (a1)+,(a0)+     * Transfer byte
        dbra    d0,rloop
        clr.l   d0
        rts

write:
        clr.l   d0
        move.b  track,d0
        mulu    #16,d0          * Multiply by SPT
        add.b   sector,d0
        mulu    #128,d0         * Multiply by sector size
        add.l   selmem,d0       * Add offset for RAM disk address in memory
        move.l  dma,a0
        move.l  d0,a1
        move.l  #127,d0         * Write 128 bytes
wloop:  move.b  (a0)+,(a1)+     * Transfer byte
        dbra    d0,wloop
        clr.l   d0
        rts

flush:  clr.l   d0      * Return successful
        rts

getseg: move.l  #memrgn,d0
        rts

getiob: rts

setiob: rts

setexc: andi.l  #$ff,d1         * Do only for exceptions 0 - 255
        cmpi    #8,d1
        beq     noset           * Skip touch privilege exception
        cmpi    #9,d1           * and Trace exception
        beq     noset
        lsl     #2,d1           * Multiply exception number by 4
        movea.l d1,a0
        add.l   #$100000,a0     * Add VBR to address
        move.l  (a0),d0         * Return old vector value
        move.l  d2,(a0)         * Insert new vector
noset:  rts

* DeciGEL patch originally made for Amiga systems using Motorola 68010
privhndl:
        movem.l D0/A0,-(SP)     * Save registers
        move.l  8+2(SP),A0      * Pointer to opcode
        move.w  (A0),D0         * Pickup opcode
        andi.w  #$FFC0,D0       * Mask out EA field
        cmpi.w  #$40C0,D0       * Is it a MOVE SR,ea?
        bne.s   privsk
        bset    #1,(A0)         * Convert it to MOVE CCR,ea
privsk: movem.l (SP)+,D0/A0     * Restore regs
        rte                     * Rerun new opcode



        .data

* Welcome text

welcome: .dc.b  13,10,'CP/M for Motorola 68332',13,10,'$'

* Disk variables

selmem: .dc.l   0       * Disk (RAM disk base address) requested by seldsk
track:  .dc.b   0       * Track requested by settrk
sector: .dc.b   0       * Sector requested by setsec
dma:    .dc.l   0       * DMA address requested by setdma

* Memory region definition

memrgn: .dc.w   1       * 1 memory region
        .dc.l   $104000 * Start
        .dc.l   $ffff   * Length/size

* Disk parameter header

dph:    .dc.l   0       * No translation table used
        .dc.w   0       * Dummy
        .dc.w   0
        .dc.w   0
        .dc.l   dirbuf  * Pointer to directory buffer
        .dc.l   dpb     * Pointer to disk parameter block
        .dc.l   0       * Check vector not used for unremovable RAM disk
        .dc.l   alv     * Pointer to allocation vector

* Disk parameter block

dpb:    .dc.w   16      * Sectors per track
        .dc.b   3       * Block shift
        .dc.b   7       * Block mask
        .dc.b   0       * Extent mask
        .dc.b   0       * Dummy fill
        .dc.w   143     * Disk size
        .dc.w   63      * 64 directory entries
        .dc.w   0       * Reserved
        .dc.w   0       * Check vector not used for unremovable RAM disk
        .dc.w   0       * Track offset zero since no boot sector on RAM disk



        .bss

dirbuf: .ds.b   128     * Directory buffer
alv:    .ds.b   18      * Allocation vector = (disk size / 8) + 1

        .end
          


Topic: Scripts and Code, by Kjetil @ 20/01-2023, Article Link

Sharp IQ-7100M Linux Link

Here is a way to download data from a Sharp IQ-7100M organizer into a modern Linux system. To be able to make the connection you will need to use a USB<->UART converter that operates on a lower TTL voltage level and most importantly INVERTS the TXD and RXD logic levels. I used the SparkFun DEV-09873 which is actually 3.3V, but the 5V version probably works as well since the Sharp organizer uses 5V TTL levels. The FTDI FT232RL chip on this supports inverting the TXD and RXD logic levels by using the "FT_PROG" program to reprogram the settings.

The connection table is as follows:

|------|--------------|
| FTDI | Sharp 15-Pin |
| -----|---|----------|
| GND  | 7 | GND      |
| RXD  | 2 | TXD      |
| TXD  | 3 | RXD      |
|------|---|----------|
          


With the SparkFun adapter it is possible to just use three wires with male Dupont connectors:

Sharp IQ-7100M


Here is a small C program that will download the "MEMO" database:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

#define TTY_DEVICE "/dev/ttyUSB0"

static const char command[] = "\x04R030000\r\nMEMO    1  \r\n";

int main(void)
{
  int fd;
  struct termios tios;
  unsigned char byte;
  const char *p;

  fd = open(TTY_DEVICE, O_RDWR | O_NOCTTY);
  if (fd == -1) {
    fprintf(stderr, "open() failed with errno: %d\n", errno);
    return EXIT_FAILURE;
  }

  if (tcgetattr(fd, &tios) == -1) {
    fprintf(stderr, "tcgetattr() failed with errno: %d\n", errno);
    close(fd);
    return EXIT_FAILURE;
  }

  cfmakeraw(&tios);
  cfsetispeed(&tios, B9600);
  cfsetospeed(&tios, B9600);

  if (tcsetattr(fd, TCSANOW, &tios) == -1) {
    fprintf(stderr, "tcsetattr() failed with errno: %d\n", errno);
    close(fd);
    return EXIT_FAILURE;
  }

  /* Send the command, using a small delay: */
  p = &command[0];
  while (*p != '\0') {
    if (write(fd, p, 1) == -1) {
      fprintf(stderr, "write() failed with errno: %d\n", errno);
      close(fd);
      return EXIT_FAILURE;
    }
    usleep(10000);
    p++;
  }

  /* Read the response until a EOF character appears: */
  do {
    if (read(fd, &byte, 1) != 1) {
      fprintf(stderr, "read() failed with errno: %d\n", errno);
      close(fd);
      return EXIT_FAILURE;
    }
    putc(byte, stdout);
  } while (byte != 0x1A);

  close(fd);
  return EXIT_SUCCESS;
}
          


This program just dumps the raw data, which is already in ASCII. Since there is no flow control, a sleep of 10ms was added between each byte sent to prevent the Sharp organizer from overflowing and giving up. Note that you will need to be in the "PC-Link" menu for any transfer to work, which is reached by pressing "Shift" + "Option" then "4".

Topic: Scripts and Code, by Kjetil @ 23/09-2022, Article Link

Monospaced 11x10 Font

For my VT100 terminal emulator on Raspberry Pi Pico project I needed to make my own font to fit the specific requirements. For convenience, that source code only contains the font data as a static "char.rom" binary file. For completeness sake I present the font data source here.

The source data is an image, so any modifications can be done in an image editor:

11x10 Font Data


As can be seen here the image is divided into a grid of 16x16 to provide 256 characters in total.

This image needs to be converted to PGM format and then be fed into a special utility that converts it to the actual ROM binary file.

The source code for the PGM to ROM utility is provided here, which operates on stdin/stdout for the conversion:

#include <stdio.h>
#include <stdbool.h>

#define HEIGHT 10
#define WIDTH 11
#define CHAR_PER_LINE 16
#define BYTES_PER_LINE 176
#define COLLECT (HEIGHT * BYTES_PER_LINE)

static unsigned char buffer[COLLECT];

static int power_of_two(int n)
{
  if (n == 0) {
    return 1;
  } else {
    return 2 * power_of_two(n - 1);
  }
}

int main(void)
{
  int skip = 4;
  int c, n, i, j, k;
  int b;

  n = 0;
  while ((c = fgetc(stdin)) != EOF) {
    if (skip > 0) {
      if (c == '\n') {
        skip--;
      }
      continue;
    }

    buffer[n] = c;
    n++;
    if (n >= COLLECT) {
      n = 0;

      /* Store as 16 bits by 10 lines, even if 11 bits wide. */

      for (i = 0; i < CHAR_PER_LINE; i++) {
        for (j = 0; j < HEIGHT; j++) {
          b = 0;
          for (k = 0; k < 8; k++) {
            if (buffer[k + (i * WIDTH) + (j * BYTES_PER_LINE)] == 0) {
              b += power_of_two(7 - k);
            }
          }
          fputc(b, stdout);

          b = 0;
          for (k = 0; k < 3; k++) {
            if (buffer[k + 8 + (i * WIDTH) + (j * BYTES_PER_LINE)] == 0) {
              b += power_of_two(2 - k);
            }
          }
          fputc(b, stdout);
        }
      }
    }
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 19/08-2022, Article Link

DTMF Sound Generator

This is a simple program to generate DTMF sounds using SDL as used by telephones. Simply pass the "phone number" as the command line argument.

The code:

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_audio.h>
#include <math.h> /* sin() */

#define AUDIO_SAMPLE_RATE 44100

typedef struct audio_data_s {
  uint32_t sample_no;
  double freq_1;
  double freq_2;
  bool stop;
  bool stopped;
} audio_data_t;

static void dtmf(char c, double *freq_h, double *freq_v)
{
  switch (c) {
  default:
  case '1':
  case '4':
  case '7':
  case '*':
    *freq_h = 1209;
    break;
  case '2':
  case '5':
  case '8':
  case '0':
    *freq_h = 1336;
    break;
  case '3':
  case '6':
  case '9':
  case '#':
    *freq_h = 1477;
    break;
  case 'A':
  case 'B':
  case 'C':
  case 'D':
    *freq_h = 1633;
    break;
  }

  switch (c) {
  default:
  case '1':
  case '2':
  case '3':
  case 'A':
    *freq_v = 697;
    break;
  case '4':
  case '5':
  case '6':
  case 'B':
    *freq_v = 770;
    break;
  case '7':
  case '8':
  case '9':
  case 'C':
    *freq_v = 852;
    break;
  case '*':
  case '0':
  case '#':
  case 'D':
    *freq_v = 941;
    break;
  }
}

static void audio_callback(void *userdata, Uint8 *stream, int len)
{
  int i;
  audio_data_t *audio_data;
  double sample;
  double time;
  
  audio_data = (audio_data_t *)userdata;

  if (audio_data->stop == false) {
    audio_data->stopped = false;
  }

  for (i = 0; i < len; i++) {
    time = audio_data->sample_no / (double)AUDIO_SAMPLE_RATE;
    sample = sin(2.0 * M_PI * audio_data->freq_1 * time);
    sample += sin(2.0 * M_PI * audio_data->freq_2 * time);
    sample /= 2;
    if (audio_data->stopped) {
      stream[i] = 128;
    } else {
      stream[i] = (Uint8)(127 + (sample * 127));
      if (audio_data->stop && stream[i] >= 126 && stream[i] <= 130) {
        audio_data->stopped = true;
      }
    }
    audio_data->sample_no++;
  }
}

int main(int argc, char *argv[])
{
  SDL_AudioSpec desired, obtained;
  audio_data_t audio_data;
  char *number;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s <number>\n", argv[0]);
    return 0;
  }

  if (SDL_Init(SDL_INIT_AUDIO) != 0) {
    fprintf(stderr, "SDL_Init() failed: %s\n", SDL_GetError());
    return 1;
  }
  atexit(SDL_Quit);

  desired.freq     = AUDIO_SAMPLE_RATE;
  desired.format   = AUDIO_U8;
  desired.channels = 1;
  desired.samples  = 1024;
  desired.userdata = &audio_data;
  desired.callback = audio_callback;

  if (SDL_OpenAudio(&desired, &obtained) != 0) {
    fprintf(stderr, "SDL_OpenAudio() failed: %s\n", SDL_GetError());
    return 1;
  }

  if (obtained.format != AUDIO_U8) {
    fprintf(stderr, "Did not get unsigned 8-bit audio format!\n");
    SDL_CloseAudio();
    return 1;
  }

  for (number = &argv[1][0]; *number != '\0'; number++) {
    audio_data.sample_no = 0;
    audio_data.stop = false;
    dtmf(*number, &audio_data.freq_1, &audio_data.freq_2);
    SDL_PauseAudio(0);
    SDL_Delay(200);
    audio_data.stop = true;
    SDL_Delay(20);
    SDL_PauseAudio(1);
    SDL_Delay(30);
  }

  SDL_CloseAudio();

  return 0;
}

          


Topic: Scripts and Code, by Kjetil @ 11/03-2022, Article Link

DOS Keyboard Fix TSR

I have an old AST Premium Exec 386SX/20 laptop running DOS. After the mechanical harddisk failed I replaced it with a flashdisk, but after re-assembly some of the keys on the keyboard would no longer work. I traced this to the '.', '', '', '+' and '\' keys, all of which happens to be on one row/column on the keyboard matrix. After further troubleshooting I could find no fault with the keyboard or associated cables, so unfortunately the problem seems to be within the keyboard controller which is embedded into a custom chipset.

Custom AST Actel Chipset


The lack of '.' and '\' makes it hard to navigate around in DOS, so I started looking into a solution. Since it is nearly impossible to find spare parts like that custom chipset I ended up with a software solution, specifically in the form of a TSR.

This TSR hooks into and intercepts the int 16h calls that are used for keyboard services. By pressing Alt+F1 a scancode representing '.' is returned instead. Also Alt+F2 returns '\' and Alt+F3 returns ':'. This will only work for DOS programs like COMMAND.COM or EDIT that actually use the BIOS services. For most games it probably won't work, but luckily the affected keys are seldom used in games.

The code:

org 0x100
bits 16
cpu 8086

section .text
start:
  jmp main

int16_interrupt:
  sti ; Allow other interrupts!
  mov word [int16_incoming_ax], ax ; Store incoming parameter for later use.

  ; Call original interrupt handler:
  pushf
  cli
original_int16:
  call original_int16:original_int16 ; Will be overwritten runtime!
  sti

  ; Check if result should be intercepted:
  pushf
  push bx
  push ax
  mov word bx, [int16_incoming_ax]
  cmp bh, 0x00 ; Check if AH was "Wait for Keypress".
  je int16_check_f1
  cmp bh, 0x10 ; Check if AH was "Extended Wait for Keypress".
  je int16_check_f1
  jmp int16_end_pop_ax

int16_check_f1:
  cmp ax, 0x6800 ; Alt + F1
  jne int16_check_f2
  pop ax
  mov ax, 0x342E ; '.'
  jmp int16_end

int16_check_f2:
  cmp ax, 0x6900 ; Alt + F2
  jne int16_check_f3
  pop ax
  mov ax, 0x2B5C ; '\'
  jmp int16_end

int16_check_f3:
  cmp ax, 0x6A00 ; Alt + F3
  jne int16_end_pop_ax
  pop ax
  mov ax, 0x273A ; ':'
  jmp int16_end

int16_end_pop_ax:
  pop ax
int16_end:
  pop bx
  popf
  retf 2

int16_incoming_ax:
  dw 0

tsr_end: ; TSR end marker.

main:
  ; NOTE: No protection to prevent TSR from being loaded twice or more!

  ; Call DOS to get original interrupt handler:
  mov al, 0x16
  mov ah, 0x35
  int 0x21
  mov word [original_int16 + 3], es
  mov word [original_int16 + 1], bx

  ; Call DOS to set new interrupt handler:
  mov al, 0x16
  mov ah, 0x25
  ; DS is already same as CS, no need to change.
  mov dx, int16_interrupt
  int 0x21

  ; Terminate and Stay Resident:
  mov dx, tsr_end
  shr dx, 1
  shr dx, 1
  shr dx, 1
  shr dx, 1
  add dx, 0x11 ; Add 0x1 for remainder and 0x10 for PSP.
  mov ax, 0x3100
  int 0x21
          


Assemble it with NASM: nasm fixkeys.asm -fbin -o fixkeys.com

Topic: Scripts and Code, by Kjetil @ 25/02-2022, Article Link

Airline Management Simulator

I made a simple airline management simulator game as part of studying how the GNU Readline library works with regards to command completion. This means the game is based around a CLI which offers tab completion.

The goal of the game is to reach a certain amount of cash after buying different airplanes and setting up routes between cities at different ticket prices. Most of the data, like the cities, are randomly generated on startup. The challenge lies in the hidden information that can only be found through trial and error, like finding which cities have citizens that are willing to pay for profitable ticket prices. I realized that it is very hard to balance simulation games, so this game as well might be too hard or too easy, I am not sure.

What it looks like:

Screenshot airline management simulator.


Anyway, here is the source code, try to compile it with: gcc -lreadline -lncurses -lm -Wall

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <inttypes.h>

#include <readline/readline.h>
#include <readline/history.h>



#define CITY_MAX 24
#define AIRPLANE_MAX 96

#define CITY_X_MAX 32768 /* Kilometers */
#define CITY_Y_MAX 32768 /* Kilometers */
#define CITY_DISTANCE_MINIMUM 500

#define COMMAND_NAME_MAX 10
#define COMMAND_ARG_MAX 4
#define COMMAND_ARG_NAME_MAX 16

#define GAME_CASH_START 200000
#define GAME_CASH_GOAL 1000000000
#define GAME_RED_DAYS_MAX 10



typedef enum {
  COMMAND_ARG_TYPE_NONE = -1,
  COMMAND_ARG_TYPE_PRICE,
  COMMAND_ARG_TYPE_CITY_NAME,
  COMMAND_ARG_TYPE_CITY_SRC,
  COMMAND_ARG_TYPE_CITY_DST,
  COMMAND_ARG_TYPE_AIRPLANE_ID,
  COMMAND_ARG_TYPE_AIRPLANE_TYPE,
  COMMAND_ARG_TYPE_MAX,
} command_arg_type_t;

typedef char *(*command_arg_completion_function_t)(const char *, int);

typedef struct command_arg_data_s {
  char name[6];
  bool optional;
  command_arg_completion_function_t function;
} command_arg_data_t;

typedef void (*command_function_t)(int, const char *[]);

typedef struct command_s {
  char name[COMMAND_NAME_MAX];
  command_function_t function;
  command_arg_type_t arg[COMMAND_ARG_MAX];
} command_t;

typedef struct city_s {
  char name[4]; /* Airport Code! */
  uint16_t x;
  uint16_t y;
  uint32_t demand[CITY_MAX];
  uint8_t size;
  uint8_t stinginess;
} city_t;

typedef enum {
  AIRPLANE_TYPE_INVALID = -1,
  AIRPLANE_TYPE_SMALL,
  AIRPLANE_TYPE_MEDIUM,
  AIRPLANE_TYPE_LARGE,
  AIRPLANE_TYPE_MAX,
} airplane_type_t;

typedef struct airplane_s {
  bool exists;
  bool parked;
  int src;
  int dst;
  uint32_t ticket_price;
  airplane_type_t type;
} airplane_t;

typedef struct airplane_data_s {
  char name[8];
  uint32_t capacity;
  uint32_t price;
  double fuel_consumption_factor;
  uint32_t upkeep_cost;
} airplane_data_t;

typedef struct game_state_s {
  bool loop_running;
  uint32_t day;
  int64_t cash;
  uint32_t current_fuel_price;
  uint32_t nominal_fuel_price;
  uint8_t days_in_the_red;
} game_state_t;



static void game_cycle(void);

static void function_airplane(int argc, const char *argv[]);
static void function_buy(int argc, const char *argv[]);
static void function_city(int argc, const char *argv[]);
static void function_help(int argc, const char *argv[]);
static void function_park(int argc, const char *argv[]);
static void function_quit(int argc, const char *argv[]);
static void function_route(int argc, const char *argv[]);
static void function_sell(int argc, const char *argv[]);
static void function_status(int argc, const char *argv[]);
static void function_wait(int argc, const char *argv[]);
#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[]);
#endif /* DEBUG_COMMAND */

static char *command_arg_completion_city(const char *text, int state);
static char *command_arg_completion_airplane_id(const char *text, int state);
static char *command_arg_completion_airplane_type(const char *text, int state);



static command_t commands[] = {
  {"airplane", function_airplane, {-1,-1,-1,-1}},
  {"buy",      function_buy,      {COMMAND_ARG_TYPE_AIRPLANE_TYPE,-1,-1,-1}},
  {"city",     function_city,     {COMMAND_ARG_TYPE_CITY_NAME,-1,-1,-1}},
  {"help",     function_help,     {-1,-1,-1,-1}},
  {"park",     function_park,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"quit",     function_quit,     {-1,-1,-1,-1}},
  {"route",    function_route,    {COMMAND_ARG_TYPE_AIRPLANE_ID,
                                   COMMAND_ARG_TYPE_CITY_SRC,
                                   COMMAND_ARG_TYPE_CITY_DST,
                                   COMMAND_ARG_TYPE_PRICE}},
  {"sell",     function_sell,     {COMMAND_ARG_TYPE_AIRPLANE_ID,-1,-1,-1}},
  {"status",   function_status,   {-1,-1,-1,-1}},
  {"wait",     function_wait,     {-1,-1,-1,-1}},
#ifdef DEBUG_COMMAND
  {"debug",    function_debug,    {-1,-1,-1,-1}},
#endif /* DEBUG_COMMAND */
};

#define COMMANDS_MAX (sizeof(commands) / sizeof(commands[0]))

static command_arg_data_t command_arg_data[COMMAND_ARG_TYPE_MAX] = {
  {"price", false, NULL},
  {"city",  true,  command_arg_completion_city},
  {"src",   false, command_arg_completion_city},
  {"dst",   false, command_arg_completion_city},
  {"id",    false, command_arg_completion_airplane_id},
  {"type",  false, command_arg_completion_airplane_type},
};

static airplane_data_t airplane_data[AIRPLANE_TYPE_MAX] = {
  {"small",   50,  50000, 0.5,  500},
  {"medium", 150, 150000, 1.0, 1000},
  {"large",  400, 400000, 2.0, 5000},
};



static game_state_t game;
static city_t city[CITY_MAX];
static airplane_t airplane[AIRPLANE_MAX];



static int city_distance(int city1_index, int city2_index)
{
  /* Pythagorean theorem */
  double a = abs(city[city1_index].x - city[city2_index].x);
  double b = abs(city[city1_index].y - city[city2_index].y);
  double c = sqrt((a * a) + (b * b));
  return (int)c;
}



static void city_generate(void)
{
  for (int i = 0; i < CITY_MAX; i++) {

    /* Generate uniqe name. */
    bool name_unique = false;
    while (! name_unique) {
      name_unique = true;
      city[i].name[0] = 0x41 + random() % 26;
      city[i].name[1] = 0x41 + random() % 26;
      city[i].name[2] = 0x41 + random() % 26;
      city[i].name[3] = '\0';
      for (int j = 0; j < i; j++) {
        if (0 == strncmp(city[i].name, city[j].name, strlen(city[i].name))) {
          name_unique = false;
        }
      }
    }

    /* Generate location, but at a minimum distance from others. */
    bool distance_ok = false;
    while (! distance_ok) {
      distance_ok = true;
      city[i].x = random() % CITY_X_MAX;
      city[i].y = random() % CITY_Y_MAX;
      for (int j = 0; j < i; j++) {
        if (city_distance(i, j) < CITY_DISTANCE_MINIMUM) {
          distance_ok = false;
        }
      }
    }

    city[i].size       = 1 + (random() % 255);
    city[i].stinginess = 1 + (random() % 255);
  }

  /* Calculate base demand when all cities are setup. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      city[i].demand[j] = city[i].size + city[j].size;
      city[i].demand[j] += random() % 64;
    }
  }
}



static int city_index_by_name(const char *city_name)
{
  for (int i = 0; i < CITY_MAX; i++) {
    if (0 == strncmp(city[i].name, city_name, strlen(city[i].name))) {
      return i;
    }
  }
  return -1;
}



static char *city_size_description(int city_index)
{
  uint8_t size = city[city_index].size;

  if (size > 224) {
    return "huge";
  } else if (size > 192) {
    return "large";
  } else if (size > 128) {
    return "medium";
  } else if (size > 64) {
    return "small";
  } else {
    return "tiny";
  }
}



static airplane_type_t airplane_type_by_name(const char *type_name)
{
  for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
    if (0 == strncmp(airplane_data[i].name, type_name,
      strlen(airplane_data[i].name))) {
      return i;
    }
  }
  return -1;
}



static void function_quit(int argc, const char *argv[])
{
  game.loop_running = false;
}



static void function_help(int argc, const char *argv[])
{
  for (int i = 0; i < COMMANDS_MAX; i++) {
    printf("%10s ", commands[i].name);
    for (int j = 0; j < COMMAND_ARG_MAX; j++) {
      command_arg_type_t type = commands[i].arg[j];
      if (type == COMMAND_ARG_TYPE_NONE) {
        break;
      }
      if (command_arg_data[type].optional) {
        printf("[");
      } else {
        printf("<");
      }
      printf("%s", command_arg_data[type].name);
      if (command_arg_data[type].optional) {
        printf("] ");
      } else {
        printf("> ");
      }
    }
    printf("\n");
  }
}



static void function_city(int argc, const char *argv[])
{
  if (argc == 1) {
    for (int i = 0; i < CITY_MAX; i++) {
      /* Convert X,Y to Latitude/Longitude. */
      char latitude_direction, longitude_direction;
      double latitude_degrees, longitude_degrees;
      latitude_degrees =  ((double)city[i].x / (double)CITY_X_MAX) * 180;
      longitude_degrees = ((double)city[i].y / (double)CITY_Y_MAX) * 180;
      if (latitude_degrees > 90.0) {
        latitude_direction = 'E';
        latitude_degrees = latitude_degrees - 90.0;
      } else {
        latitude_direction = 'W';
        latitude_degrees = 90.0 - latitude_degrees;
      }
      if (longitude_degrees > 90.0) {
        longitude_direction = 'N';
        longitude_degrees = longitude_degrees - 90.0;
      } else {
        longitude_direction = 'S';
        longitude_degrees = 90.0 - longitude_degrees;
      }
      printf("%s @ %4.1f %c, %4.1f %c (%s city)\n", city[i].name,
        longitude_degrees, longitude_direction,
        latitude_degrees, latitude_direction,
        city_size_description(i));
    }

  } else {

    int index = city_index_by_name(argv[1]);
    if (index < 0) {
      printf("City does not exist!\n");
      return;
    }

    for (int i = 0; i < CITY_MAX; i++) {
      if (index == i) {
        continue;
      }
      printf("%s -> %s   Demand: %d\n",
        city[index].name,
        city[i].name,
        city[index].demand[i]);
    }
  }
}



static void function_airplane(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      printf("#%d   ", i + 1);
      printf("Type: %-6s   ", airplane_data[airplane[i].type].name);
      if (airplane[i].parked) {
        printf("Parked\n");
      } else {
        printf("At: %s   Going To: %s   Charging: $%d\n",
          city[airplane[i].src].name,
          city[airplane[i].dst].name,
          airplane[i].ticket_price);
      }
      airplanes++;
    }
  }

  if (0 == airplanes) {
    printf("No airplanes owned, buy one...\n");
  }
}



static void function_buy(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane type!\n");
    printf("Valid types are: ");
    for (int i = 0; i < AIRPLANE_TYPE_MAX; i++) {
      printf("%s ", airplane_data[i].name);
    }
    printf("\n");
    return;
  }

  airplane_type_t type = airplane_type_by_name(argv[1]);
  if (AIRPLANE_TYPE_INVALID == type) {
    printf("Invalid airplane type specified!\n");
    return;
  }

  if (game.cash < airplane_data[type].price) {
    printf("Not enough cash!\n");
    return;
  }

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (! airplane[i].exists) {
      airplane[i].type = type;
      airplane[i].exists = true;
      airplane[i].parked = true;
      airplane[i].ticket_price = 0;
      game.cash -= airplane_data[type].price;

      printf("Airplane #%d with capacity %d bought for $%d.\n", i + 1,
        airplane_data[type].capacity, airplane_data[type].price);
      game_cycle();
      return;
    }
  }

  printf("Cannot buy more airplanes!\n");
}



static void function_route(int argc, const char *argv[])
{
  if (argc != 5) {
    printf("Missing arguments!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  int src_index = city_index_by_name(argv[2]);
  if (src_index < 0) {
    printf("Source city does not exist!\n");
    return;
  }

  int dst_index = city_index_by_name(argv[3]);
  if (dst_index < 0) {
    printf("Destination city does not exist!\n");
    return;
  }

  int price = atoi(argv[4]);
  if (price <= 0) {
    printf("Price is invalid!\n");
    return;
  }

  airplane[airplane_index].parked = false;
  airplane[airplane_index].src = src_index;
  airplane[airplane_index].dst = dst_index;
  airplane[airplane_index].ticket_price = price;

  printf("Airplane #%d now flying from %s to %s for $%d.\n",
    airplane_index + 1, city[src_index].name, city[dst_index].name, price);
  game_cycle();
}



static void function_park(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (true == airplane[airplane_index].parked) {
    printf("Airplane #%d already parked!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].parked = true;

  printf("Airplane #%d parked.\n", airplane_index + 1);
  game_cycle();
}



static void function_status(int argc, const char *argv[])
{
  int airplanes = 0;

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      airplanes++;
    }
  }

  printf("Day        : %d\n", game.day);
  printf("Cash       : $%"PRIi64"\n", game.cash);
  printf("Airplanes  : %d\n", airplanes);
  printf("Fuel price : $%d per kilometer\n", game.current_fuel_price);

  for (int i = 0; i < 3; i++) {
    printf("\n");
    printf("Data for '%s' airplane:\n", airplane_data[i].name);
    printf("  Capacity         : %d passengers\n", airplane_data[i].capacity);
    printf("  Purchase price   : $%d\n", airplane_data[i].price);
    printf("  Upkeep cost      : $%d\n", airplane_data[i].upkeep_cost);
    printf("  Fuel consumption : %d%%\n", 
      (int)(airplane_data[i].fuel_consumption_factor * 100));
  }
}



static void function_sell(int argc, const char *argv[])
{
  if (argc != 2) {
    printf("Please specify airplane ID!\n");
    return;
  }

  int airplane_index = atoi(argv[1]);
  airplane_index--; /* ID to index */
  if (airplane_index < 0 || airplane_index >= AIRPLANE_MAX) {
    printf("Invalid airplane ID!\n");
    return;
  }

  if (false == airplane[airplane_index].exists) {
    printf("Airplane #%d does not exist!\n", airplane_index + 1);
    return;
  }

  if (false == airplane[airplane_index].parked) {
    printf("Airplane #%d is in operation (not parked)!\n", airplane_index + 1);
    return;
  }

  airplane[airplane_index].exists = false;
  game.cash += airplane_data[airplane[airplane_index].type].price;

  printf("Airplane #%d sold for $%d.\n", airplane_index + 1,
    airplane_data[airplane[airplane_index].type].price);
  game_cycle();
}



static void function_wait(int argc, const char *argv[])
{
  game_cycle();
}



#ifdef DEBUG_COMMAND
static void function_debug(int argc, const char *argv[])
{
  printf("Raw City Data:\n");
  for (int i = 0; i < CITY_MAX; i++) {
    printf("%s @ %05hu,%05hu, Size: %d, Stinginess: %d\n",
      city[i].name, city[i].x, city[i].y, city[i].size, city[i].stinginess);
  }

  printf("\n");
  printf("Distances:\n");
  printf("     |");
  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
  }
  printf("\n");

  for (int i = 0; i < CITY_MAX; i++) {
    printf(" %s |", city[i].name);
    for (int j = 0; j < CITY_MAX; j++) {
      printf("%5d|", city_distance(i, j));
    }
    printf("\n");
  }

  printf("\n");
  printf("Nominal Fuel Price: %d\n", game.nominal_fuel_price);
}
#endif /* DEBUG_COMMAND */



static void game_fluctuate(uint32_t *value,
  int32_t variance_start, int32_t variance_end,
  uint32_t absolute_minimum, uint32_t absolute_maximum)
{
  /* Conversion to signed integers to handle negative values. */
  int32_t variance = (random() % ((variance_end - variance_start) + 1));
  variance += variance_start;
  int32_t new_value = *value;
  new_value += variance;
  if (new_value < (int32_t)absolute_minimum) {
    new_value = absolute_minimum;
  } else if (new_value > (int32_t)absolute_maximum) {
    new_value = absolute_maximum;
  }
  *value = new_value;
}



static void game_cycle(void)
{
  /* Fly airplanes according to routes. */
  for (int i = 0; i < AIRPLANE_MAX; i++) {
    if (airplane[i].exists) {
      if (! airplane[i].parked) {

        /* Calculate passengers willing to fly! */
        int distance = city_distance(airplane[i].src, airplane[i].dst);
        int demand = city[airplane[i].src].demand[airplane[i].dst];

        /* Nominal ticket price is based on having a full flight with a
           medium size airplane with the nominal fuel price
           giving exactly zero profits. */
        double nominal_price = ((distance * game.nominal_fuel_price) /
          airplane_data[AIRPLANE_TYPE_MEDIUM].capacity);

        /* The stinginess of the city gives an additional factor. */
        double stinginess = city[airplane[i].src].stinginess / 32.0;

        /* Passengers are divided into five groups of different size. */
        int group[5];
        group[0] = demand / 20; /*  5% */
        group[1] = demand / 5;  /* 20% */
        group[2] = demand / 2;  /* 50% */
        group[3] = demand / 5;  /* 20% */
        group[4] = demand / 20; /*  5% */

        /* The five groups accepts different prices. */
        double accepts_price[5];
        accepts_price[0] = nominal_price * 0.70 * stinginess;
        accepts_price[1] = nominal_price * 0.95 * stinginess;
        accepts_price[2] = nominal_price * 1.05 * stinginess;
        accepts_price[3] = nominal_price * 1.15 * stinginess;
        accepts_price[4] = nominal_price * 1.30 * stinginess;

#ifdef DEBUG_DEMAND
        printf("%s -> %s, Nominal: $%.2f, Stinginess: %.2f\n",
          city[airplane[i].src].name, city[airplane[i].dst].name,
          nominal_price, stinginess);
        for (int j = 0; j < 5; j++) {
          printf("  Group #%d of %d people accepts anything below $%.0f\n",
            j, group[j], accepts_price[j]);
        }
#endif /* DEBUG_DEMAND */

        int passengers = 0;
        for (int j = 0; j < 5; j++) {
          if (airplane[i].ticket_price < accepts_price[j]) {
            passengers += group[j];
          }
        }

        if (passengers > airplane_data[airplane[i].type].capacity) {
          passengers = airplane_data[airplane[i].type].capacity;
        }

        printf("Airplane #%d flew %d passenger%s from %s to %s\n", i + 1,
          passengers, (passengers == 1) ? "" : "s",
          city[airplane[i].src].name, city[airplane[i].dst].name);

        /* Swap source and destination. */
        int swap = airplane[i].src;
        airplane[i].src = airplane[i].dst;
        airplane[i].dst = swap;

        /* Generate cash from ticket prices. */
        game.cash += passengers * airplane[i].ticket_price;

        /* Subtract fuel price. */
        game.cash -= distance * ((double)game.current_fuel_price *
          airplane_data[airplane[i].type].fuel_consumption_factor);
      }
      /* Subtract upkeep price for airplane, even when parked. */
      game.cash -= airplane_data[airplane[i].type].upkeep_cost;
    }
  }

  /* Fluctuate traveling demand. */
  for (int i = 0; i < CITY_MAX; i++) {
    for (int j = 0; j < CITY_MAX; j++) {
      if (i == j) {
        continue;
      }

      uint32_t variance = city[i].size + city[j].size;
      variance += random() % 64;
      variance /= 10;
      game_fluctuate(&city[i].demand[j], -variance, variance, 0, 1000);
    }
  }

  /* Price/cost fluctuations. */
  game_fluctuate(&game.current_fuel_price, -1, 1, 3, 10);
  game_fluctuate(&airplane_data[0].upkeep_cost, -20, 20, 200, 1000);
  game_fluctuate(&airplane_data[1].upkeep_cost, -50, 50, 500, 2000);
  game_fluctuate(&airplane_data[2].upkeep_cost, -250, 250, 2000, 10000);
  game_fluctuate(&airplane_data[0].price, -500, 500, 20000, 100000);
  game_fluctuate(&airplane_data[1].price, -1000, 1000, 50000, 300000);
  game_fluctuate(&airplane_data[2].price, -5000, 5000, 100000, 800000);

  /* Check and possibly warn about balance. */
  if (game.cash < 0) {
    if (game.days_in_the_red >= GAME_RED_DAYS_MAX) {
      printf("Game over! You went bankrupt...\n");
      game.loop_running = false;
      return;
    }
    printf("WARNING! Cash in the red, you have %d day%s left resolve this.\n",
      GAME_RED_DAYS_MAX - game.days_in_the_red,
      (GAME_RED_DAYS_MAX - game.days_in_the_red == 1) ? "" : "s");
    game.days_in_the_red++;
  } else {
    game.days_in_the_red = 0;
  }

  if (game.cash >= GAME_CASH_GOAL) {
    printf("Congratulations! You reached the goal of %d$ in %d days.\n",
        GAME_CASH_GOAL, game.day);
    game.loop_running = false;
    return;
  }

  game.day++;
}



static void game_init(void)
{
  srandom(time(NULL));
  city_generate();

  for (int i = 0; i < AIRPLANE_MAX; i++) {
    airplane[i].exists = false;
  }

  game.day = 1;
  game.days_in_the_red = 0;
  game.cash = GAME_CASH_START;
  game.current_fuel_price = 4 + (random() % 4);
  game.nominal_fuel_price = game.current_fuel_price;
}



static char *command_arg_completion_city(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < CITY_MAX) {
    if (0 == strncmp(city[index].name, text, strlen(text))) {
      return strdup(city[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_id(const char *text, int state)
{
  static int index;
  static char airplane_id_string[AIRPLANE_MAX][4];
  static int airplanes;

  if (state == 0) {
    index = 0;
    airplanes = 0;
    /* Generate the list of possible numbers as strings. */
    for (int i = 0; i < AIRPLANE_MAX; i++) {
      if (airplane[i].exists) {
        snprintf(airplane_id_string[i], sizeof(airplane_id_string[i]),
          "%d", i + 1);
        airplanes++;
      }
    }
  }

  if (0 == airplanes) {
    return NULL;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_id_string[index], text, strlen(text))) {
      return strdup(airplane_id_string[index++]);
    }
    index++;
  }

  return NULL;
}



static char *command_arg_completion_airplane_type(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < AIRPLANE_TYPE_MAX) {
    if (0 == strncmp(airplane_data[index].name, text, strlen(text))) {
      return strdup(airplane_data[index++].name);
    }
    index++;
  }

  return NULL;
}



static char *command_completion(const char *text, int state)
{
  static int index;

  if (state == 0) {
    index = 0;
  }

  while (index < COMMANDS_MAX) {
    if (0 == strncmp(commands[index].name, text, strlen(text))) {
      return strdup(commands[index++].name);
    }
    index++;
  }

  return NULL;
}



static char **command_completion_entry(const char *text, int start, int end)
{
  rl_attempted_completion_over = 1;
  if (start == 0) {
    /* Command part. */
    return rl_completion_matches(text, command_completion);

  } else {
    /* Arguments part. */
    for (int i = 0; i < COMMANDS_MAX; i++) {
      if (0 == strncmp(commands[i].name, rl_line_buffer,
                strlen(commands[i].name))) {
        /* Found the command entered... */
        int arg_no = -1;
        bool saw_space = false;
        for (char *p = rl_line_buffer; *p != '\0'; p++) {
          if (*p == ' ') {
            if (! saw_space) {
              arg_no++;
              saw_space = true;
            }
          } else {
            saw_space = false;
          }
        }
        /* ...Found the argument number... */
        if (arg_no < COMMAND_ARG_MAX) {
          command_arg_type_t type = commands[i].arg[arg_no];
          command_arg_completion_function_t function = 
            command_arg_data[type].function;
          if (function != NULL) {
            /* ...Call the specific function. */
            return rl_completion_matches(text, function);
          }
        }
        break;
      }
    }

    return NULL;
  }
}



static bool command_function_call(char *command)
{
  const char *argv[COMMAND_ARG_MAX + 1]; /* Includes the command name. */

  /* Split command into argc and argv. */
  int argc = 0;
  char *token = strtok(command, " ");
  do {
    if (token != NULL) {
      argv[argc] = token;
      argc++;
      if (argc == COMMAND_ARG_MAX + 1) {
        break;
      }
    }
    token = strtok(NULL, " ");
  } while (token != NULL);

  /* Attempt to call matching command. */
  if (argc == 0) {
    return false;
  }
  for (int i = 0; i < COMMANDS_MAX; i++) {
    if (0 == strncmp(commands[i].name, argv[0],
              strlen(commands[i].name))) {
      (commands[i].function(argc, argv));
      return true;
    }
  }

  printf("Unknown command: %s\n", command);
  return false;
}



int main(int argc, const char *argv[])
{
  char *command;
  char prompt[32];

  printf("*** AIRLINE MANAGEMENT SIMULATOR ***\n");
  printf("Reach the goal of %d$\n", GAME_CASH_GOAL);
  printf("Type 'help' or use tab completion.\n");

  game_init();

  rl_attempted_completion_function = command_completion_entry;

  game.loop_running = true;
  while (game.loop_running) {
    snprintf(prompt, sizeof(prompt), "[%u] $%"PRIi64" > ", game.day, game.cash);
    command = readline(prompt);
    if (command == NULL) { /* EOF */
      return 0;
    }
    if (0 == strlen(command)) {
      continue;
    }

    add_history(command);
    command_function_call(command);
    free(command);
  }

  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 05/02-2022, Article Link

XT-IDE Loaded from Fake VBR

Here is a trick I have used to get my Commodore PC 50-II to boot DOS off a unsupported Compact Flash (CF) card. The BIOS on this machine is dated between 1987-1989 and only has a set of hard-coded hard disk types with specific C/H/S values. A common way to get around such limitations is to use the excellent XT-IDE BIOS, which is usually put in a separate option ROM. Instead of using a EPROM I wanted to find a way to load it using software instead.

My software trick uses a fake volume boot record (VBR) to load XT-IDE into RAM. This approach is similar to optromloader which loads ROMs from floppies.

Since the BIOS on this machine has support for hard disks, it will always be able to load the master boot record (MBR) located at the first sector 0/0/1, regardless of C/H/S settings. The VBR (for DOS) is usually stored at the first sector of the second head 0/1/1. Most of the hard disk types available in this BIOS has the "Sector per Track" set to 17, and this is what I will use. The CF card I use has real C/H/S values 1003/16/32. This means that when the MBR initially loads the VBR through the original BIOS int 13h routines it will actually get sector 18 from the CF card, and this is where to store the fake VBR that loads XT-IDE. When the boot sequence is re-initiated after this the MBR will get reloaded, but this time it will use the int 13h routines provided by XT-IDE and get the real DOS VBR from sector 33.

Here is a diagram that shows the layout of the start of the CF card:

 Content   Sector (Byte Offset)
+--------+ 1 (0)
|  MBR   |
+--------+ 2 (512)
|        |
| XT-IDE |
|  ROM   |
|        |
+--------+ 18 (8704)
|  Fake  |
|  VBR   |
+--------+ 19 (9216)
|        |
| Unused |
| Space  |
|        |
+--------+ 33 (16384)
|  Real  |
|  VBR   |
+--------+ 34 (16896)
|        |
|        |
          


Here is the assembly source code for the fake VBR that loads the XT-IDE ROM:

org 0x7C00
bits 16
cpu 8086

section .text
start:
  ; Setup stack area for boot loader code.
  cli
  xor ax, ax
  mov ss, ax
  mov sp, 0x7C00
  push ax
  pop ds
  sti

  ; Initial values for BIOS disk operations.
  mov bx, 0x9E00 ; Destination segment.
  mov es, bx
  xor bx, bx     ; Destination offset.
  mov cx, 0x0002 ; Sector 2, Cylinder 0.
  mov dx, 0x0080 ; First harddisk, Head 0.

read_again:
  ; Call BIOS to reset disk system.
  xor ax, ax
  int 0x13

  ; Push registers to stack before doing BIOS video service.
  push bx
  push cx
  push dx

  ; Display last BIOS disk operation status.
  mov dl, cl ; Column = Sector number.
  sub dl, 2
  mov dh, 1 ; Row 1
  mov bx, 0x000F
  mov ah, 0x02 ; Set cursor position.
  int 0x10
  mov cx, 1
  mov ah, 0x09
  mov al, [int13_status]
  add al, 0x30 ; Convert to ASCII numerical or higher.
  and al, 0x7F ; Make sure it is a valid ASCII value.
  int 0x10

  ; Pop registers from stack after doing BIOS video service.
  pop dx
  pop cx
  pop bx

  ; Call BIOS to read 1 sector.
  mov ax, 0x0201
  int 0x13
  mov [int13_status], ah
  jb read_again

  ; Increment destination memory and sector number.
  add bh, 2
  inc cl
  cmp cl, 18 ; Stop at 16 sectors (8KB).
  jne read_again

  ; Call BIOS to get memory size.
  int 0x12
  sub ax, 8       ; Subtract 8K blocks...
  mov [0x413], ax ; ...and store new memory size in BDA at 0x40:0x0013

  call 0x9E00:0003 ; Call the option ROM.
  int 0x19 ; Call BIOS to initiate boot process (again).

int13_status:
  db 0
          


To put this at the correct location on the CF card a utility coded in C is provided:

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  FILE *fh_vbr    = NULL;
  FILE *fh_optrom = NULL;
  FILE *fh_disk   = NULL;
  int c, n, vbr_sector;

  if (argc != 5) {
    printf("Usage: %s <vbr> <optrom> <disk> <vbr-sector>\n", argv[0]);
    return 1;
  }

  fh_vbr = fopen(argv[1], "rb");
  if (NULL == fh_vbr) {
    printf("Error: Unable to open '%s'\n", argv[1]);
    goto end;
  }

  fh_optrom = fopen(argv[2], "rb");
  if (NULL == fh_optrom) {
    printf("Error: Unable to open '%s'\n", argv[2]);
    goto end;
  }

  fh_disk = fopen(argv[3], "r+b");
  if (NULL == fh_disk) {
    printf("Error: Unable to open '%s'\n", argv[3]);
    goto end;
  }

  vbr_sector = atoi(argv[4]);
  if (0 == vbr_sector) {
    printf("Error: Invalid VBR sector: %s\n", argv[4]);
    goto end;
  }

  /* Write option ROM, after MBR on sector 2 (1-indexed) onwards. */
  fseek(fh_disk, 512, SEEK_SET);
  while ((c = fgetc(fh_optrom)) != EOF) {
    fputc(c, fh_disk);
  }

  /* Write fake VBR on selected sector (1-indexed). */
  n = 0;
  fseek(fh_disk, 512 * (vbr_sector - 1), SEEK_SET);
  while ((c = fgetc(fh_vbr)) != EOF) {
    fputc(c, fh_disk);
    n++;
  }

  /* Pad rest of the fake VBR area with zeroes... */
  for (; n < 0x1FE; n++) {
    fputc('\x00', fh_disk);
  }
  /* ...and finish with the correct signature. */
  fputc('\x55', fh_disk);
  fputc('\xAA', fh_disk);

end:
  if (NULL != fh_vbr)    fclose(fh_vbr);
  if (NULL != fh_optrom) fclose(fh_optrom);
  if (NULL != fh_disk)   fclose(fh_disk);
  return 0;
}
          


Both can be assembled/compiled with this Makefile:

all: vbr.bin modify-disk

vbr.bin: vbr.asm
	nasm vbr.asm -fbin -o vbr.bin

modify-disk: modify-disk.c
	gcc -o modify-disk -Wall -Wextra modify-disk.c

.PHONY: clean
clean:
	rm -f vbr.bin modify-disk
          


These tools are meant to be used with a XT-IDE ROM of 8KB in size. I should also mention that I did not get this machine to boot with the 386 version, this always resulted in "Boot sector not found" errors. Instead I have used the "ide_at.bin" version.

Once assembled and compiled the tools are typically used as such, where /dev/sdX is the CF card:

./modify-disk vbr.bin ide_at.bin /dev/sdX 18
          


A "screenshot" of what it should look like when booting:

XT-IDE loaded on Commodore PC 50-II

Take note of the 0s near the top, which represents status codes, in this case OK, from sector reads.

Topic: Scripts and Code, by Kjetil @ 08/01-2022, Article Link

Arduino Z80 CPU Tester

Here is a way to test Z80 CPUs that I created. I got the idea after seeing something about an Arduino being used as some sort of logic analyzer. For this I have used the Arduino Mega 2560 since this has a lot of digital I/O pins available.

It is a simple Arduino sketch, that uses the built-in USB CDC serial interface to communicate. It will respond to certain commands and on every cycle dump the status of the pins, which will look something like this:

  |------\_/------|
0 | A11       A10 | 0
0 | A12        A9 | 0
0 | A13        A8 | 0
0 | A14        A7 | 0
0 | A15        A6 | 0
1 | CLK        A5 | 0
0 | D4         A4 | 0
1 | D3         A3 | 0
0 | D5         A2 | 1
0 | D6         A1 | 0
- | +5V        A0 | 1
1 | D2        GND | -
0 | D7      ~RFSH | -
1 | D0        ~M1 | -
1 | D1     ~RESET | 1
- | ~INT   ~BUSRQ | -
- | ~NMI    ~WAIT | -
- | ~HALT  ~BUSAK | -
- | ~MREQ     ~WR | 1
- | ~IORQ     ~RD | 1
  |---------------|
Address: 0x5 Data: 0xF
          


The commands available in this version is 'c' to toogle the CLK clock pin, 'r' to toggle the RESET pin and 0 through 7 to toggle the data bit pins. It is possible to easily expand on this, but these are the minimum pins required to make the sketch program useful. A simple test that can be done with the Z80 is to put 0xC3 on the data bit pins, and then just toggle the clock over and over. This will cause it to read the opcodes 0xC3 0xC3 0xC3, meaning an absolute jump to address 0xC3C3 which will get reflected back on the address pins.

There is no circuit diagram, instead use the #define in the Arduino sketch to determine the connections. In addition to the signals defined there, +5V and GND is also needed of course, gotten from the Arduino as well.

Here is the sketch:

#define PIN_A0     11
#define PIN_A1     12
#define PIN_A2     10
#define PIN_A3     61
#define PIN_A4     60
#define PIN_A5     59
#define PIN_A6     58
#define PIN_A7     57
#define PIN_A8     56
#define PIN_A9     55
#define PIN_A10    54
#define PIN_A11    62
#define PIN_A12    63
#define PIN_A13    64
#define PIN_A14    65
#define PIN_A15    66
#define PIN_D0     17
#define PIN_D1     16
#define PIN_D2     19
#define PIN_D3     69
#define PIN_D4     68
#define PIN_D5     21
#define PIN_D6     20
#define PIN_D7     18
#define PIN_CLK    67
#define PIN_RESET  8 
#define PIN_WR     4 
#define PIN_RD     3 

void setup() {
  Serial.begin(115200);
  
  pinMode(LED_BUILTIN, OUTPUT);

  pinMode(PIN_A0, INPUT);
  pinMode(PIN_A1, INPUT);
  pinMode(PIN_A2, INPUT);
  pinMode(PIN_A3, INPUT);
  pinMode(PIN_A4, INPUT);
  pinMode(PIN_A5, INPUT);
  pinMode(PIN_A6, INPUT);
  pinMode(PIN_A7, INPUT);
  pinMode(PIN_A8, INPUT);
  pinMode(PIN_A9, INPUT);
  pinMode(PIN_A10, INPUT);
  pinMode(PIN_A11, INPUT);
  pinMode(PIN_A12, INPUT);
  pinMode(PIN_A13, INPUT);
  pinMode(PIN_A14, INPUT);
  pinMode(PIN_A15, INPUT);
  pinMode(PIN_D0, OUTPUT);
  pinMode(PIN_D1, OUTPUT);
  pinMode(PIN_D2, OUTPUT);
  pinMode(PIN_D3, OUTPUT);
  pinMode(PIN_D4, OUTPUT);
  pinMode(PIN_D5, OUTPUT);
  pinMode(PIN_D6, OUTPUT);
  pinMode(PIN_D7, OUTPUT);
  pinMode(PIN_CLK, OUTPUT);
  pinMode(PIN_RESET, OUTPUT);
  pinMode(PIN_WR, INPUT);
  pinMode(PIN_RD, INPUT);
}

void dump() {
  uint16_t bus;

  Serial.println("  |------\\_/------|  ");

  Serial.print(digitalRead(PIN_A11), DEC);
  Serial.print(" | A11       A10 | ");
  Serial.println(digitalRead(PIN_A10), DEC);

  Serial.print(digitalRead(PIN_A12), DEC);
  Serial.print(" | A12        A9 | ");
  Serial.println(digitalRead(PIN_A9), DEC);

  Serial.print(digitalRead(PIN_A13), DEC);
  Serial.print(" | A13        A8 | ");
  Serial.println(digitalRead(PIN_A8), DEC);

  Serial.print(digitalRead(PIN_A14), DEC);
  Serial.print(" | A14        A7 | ");
  Serial.println(digitalRead(PIN_A7), DEC);

  Serial.print(digitalRead(PIN_A15), DEC);
  Serial.print(" | A15        A6 | ");
  Serial.println(digitalRead(PIN_A6), DEC);

  Serial.print(digitalRead(PIN_CLK), DEC);
  Serial.print(" | CLK        A5 | ");
  Serial.println(digitalRead(PIN_A5), DEC);

  Serial.print(digitalRead(PIN_D4), DEC);
  Serial.print(" | D4         A4 | ");
  Serial.println(digitalRead(PIN_A4), DEC);

  Serial.print(digitalRead(PIN_D3), DEC);
  Serial.print(" | D3         A3 | ");
  Serial.println(digitalRead(PIN_A3), DEC);

  Serial.print(digitalRead(PIN_D5), DEC);
  Serial.print(" | D5         A2 | ");
  Serial.println(digitalRead(PIN_A2), DEC);

  Serial.print(digitalRead(PIN_D6), DEC);
  Serial.print(" | D6         A1 | ");
  Serial.println(digitalRead(PIN_A1), DEC);

  Serial.print("- | +5V        A0 | ");
  Serial.println(digitalRead(PIN_A0), DEC);

  Serial.print(digitalRead(PIN_D2), DEC);
  Serial.println(" | D2        GND | -");

  Serial.print(digitalRead(PIN_D7), DEC);
  Serial.println(" | D7      ~RFSH | -");

  Serial.print(digitalRead(PIN_D0), DEC);
  Serial.println(" | D0        ~M1 | -");

  Serial.print(digitalRead(PIN_D1), DEC);
  Serial.print(" | D1     ~RESET | ");
  Serial.println(digitalRead(PIN_RESET), DEC);

  Serial.println("- | ~INT   ~BUSRQ | -");
  Serial.println("- | ~NMI    ~WAIT | -");
  Serial.println("- | ~HALT  ~BUSAK | -");

  Serial.print("- | ~MREQ     ~WR | ");
  Serial.println(digitalRead(PIN_WR), DEC);

  Serial.print("- | ~IORQ     ~RD | ");
  Serial.println(digitalRead(PIN_RD), DEC);

  Serial.println("  |---------------|  ");

  bus = digitalRead(PIN_A15);
  bus <<= 1;
  bus += digitalRead(PIN_A14);
  bus <<= 1;
  bus += digitalRead(PIN_A13);
  bus <<= 1;
  bus += digitalRead(PIN_A12);
  bus <<= 1;
  bus += digitalRead(PIN_A11);
  bus <<= 1;
  bus += digitalRead(PIN_A10);
  bus <<= 1;
  bus += digitalRead(PIN_A9);
  bus <<= 1;
  bus += digitalRead(PIN_A8);
  bus <<= 1;
  bus += digitalRead(PIN_A7);
  bus <<= 1;
  bus += digitalRead(PIN_A6);
  bus <<= 1;
  bus += digitalRead(PIN_A5);
  bus <<= 1;
  bus += digitalRead(PIN_A4);
  bus <<= 1;
  bus += digitalRead(PIN_A3);
  bus <<= 1;
  bus += digitalRead(PIN_A2);
  bus <<= 1;
  bus += digitalRead(PIN_A1);
  bus <<= 1;
  bus += digitalRead(PIN_A0);
  Serial.print("Address: 0x");
  Serial.print(bus, HEX);
  Serial.print(" ");

  bus = digitalRead(PIN_D7);
  bus <<= 1;
  bus += digitalRead(PIN_D6);
  bus <<= 1;
  bus += digitalRead(PIN_D5);
  bus <<= 1;
  bus += digitalRead(PIN_D4);
  bus <<= 1;
  bus += digitalRead(PIN_D3);
  bus <<= 1;
  bus += digitalRead(PIN_D2);
  bus <<= 1;
  bus += digitalRead(PIN_D1);
  bus <<= 1;
  bus += digitalRead(PIN_D0);
  Serial.print("Data: 0x");
  Serial.println(bus, HEX);
}

void loop() {
  if (Serial.available() > 0) {
    switch (Serial.read()) {
    case 'r':
      digitalWrite(PIN_RESET, digitalRead(PIN_RESET) ? 0 : 1);
      break;

    case 'c':
      digitalWrite(PIN_CLK, digitalRead(PIN_CLK) ? 0 : 1);
      break;

    case '0':
      digitalWrite(PIN_D0, digitalRead(PIN_D0) ? 0 : 1);
      break;

    case '1':
      digitalWrite(PIN_D1, digitalRead(PIN_D1) ? 0 : 1);
      break;

    case '2':
      digitalWrite(PIN_D2, digitalRead(PIN_D2) ? 0 : 1);
      break;

    case '3':
      digitalWrite(PIN_D3, digitalRead(PIN_D3) ? 0 : 1);
      break;

    case '4':
      digitalWrite(PIN_D4, digitalRead(PIN_D4) ? 0 : 1);
      break;

    case '5':
      digitalWrite(PIN_D5, digitalRead(PIN_D5) ? 0 : 1);
      break;

    case '6':
      digitalWrite(PIN_D6, digitalRead(PIN_D6) ? 0 : 1);
      break;

    case '7':
      digitalWrite(PIN_D7, digitalRead(PIN_D7) ? 0 : 1);
      break;
      
    default:
      break;
    }
    dump();
  }

  digitalWrite(LED_BUILTIN, digitalRead(LED_BUILTIN) ? 0 : 1);
  delay(100);
}
          

The LED blinking is used as a indicator to see that the sketch program is actually running.

Here is a photo of the setup:

Arduino Z80 Tester


Topic: Scripts and Code, by Kjetil @ 27/11-2021, Article Link

FAT16 File Extraction

This is a program I made as part of another bigger project, where I needed to read files from a FAT16 file system. It implements the bare-minimum to be able to extract a file from the root directory, and is only setup to handle the specifications of FAT16. (FAT12 or FAT32 will not work.) It can be used either on a disk image, or actually on a disk block device directly which does not need to be mounted. Calling the program with only the disk image argument will print the root directory, then add the target filename to dump that file.

Here is the code:

#include <stdio.h>
#include <stdint.h>
#include <string.h>

static void read_sector(FILE *fh, int no, uint8_t sector[])
{
  fseek(fh, no * 512, SEEK_SET);
  fread(sector, sizeof(uint8_t), 512, fh);
}

int main(int argc, char *argv[])
{
  FILE *disk;
  uint8_t sector[512];

  uint32_t vbr_offset;
  uint32_t fat_offset;
  uint32_t root_dir_offset;
  uint32_t data_offset;

  uint8_t partition_type;
  uint8_t sectors_per_cluster;
  uint16_t reserved_sectors;
  uint8_t no_of_fats;
  uint16_t root_entries;
  uint16_t sectors_per_fat;

  char file_name[13]; /* 8.3 format at NULL byte. */
  uint32_t file_size;
  uint16_t file_cluster;

  uint16_t cluster = 0; /* Target cluster for dump. */
  uint32_t cluster_file_size;
  uint16_t cluster_end_indicator;
  uint16_t cluster_fat;
  uint16_t cluster_offset;

  if (argc <= 1) {
    printf("Usage: %s <disk image> [dump file]\n", argv[0]);
    return 1;
  }

  disk = fopen(argv[1], "rb");
  if (disk == NULL) {
    printf("Error: Unable to open: %s\n", argv[1]);
    return 1;
  }

  read_sector(disk, 0, sector);

  partition_type = sector[450];
  vbr_offset = sector[454];

  printf("First partiton type: 0x%02x\n", partition_type);
  printf("First VBR @ 0x%x\n", vbr_offset * 512);

  if (partition_type != 0x04 && partition_type != 0x06) {
    printf("Error: First partition on disk is not FAT16!");
    fclose(disk);
    return 1;
  }

  read_sector(disk, vbr_offset, sector);

  sectors_per_cluster = sector[13];
  reserved_sectors    = sector[14] + (sector[15] << 8);
  no_of_fats          = sector[16];
  root_entries        = sector[17] + (sector[18] << 8);
  sectors_per_fat     = sector[22] + (sector[23] << 8);

  printf("Sectors per cluster: %d\n", sectors_per_cluster);
  printf("Reserved sectors: %d\n", reserved_sectors);
  printf("No of FATs: %d\n", no_of_fats);
  printf("Root entries: %d\n", root_entries);
  printf("Sectors per FAT: %d\n", sectors_per_fat);

  fat_offset = vbr_offset + reserved_sectors;
  root_dir_offset = fat_offset + (sectors_per_fat * no_of_fats);
  data_offset = root_dir_offset + ((root_entries * 32) / 512);

  printf("FAT Region @ 0x%x\n", fat_offset * 512);
  printf("Root Directory Region @ 0x%x\n", root_dir_offset * 512);
  printf("Data Region @ 0x%x\n", data_offset * 512);

  printf("\nRoot Directory:\n");

  for (uint32_t offset = root_dir_offset; offset < data_offset; offset++) {
    read_sector(disk, offset, sector);

    for (int i = 0; i < 512; i += 32) {
      if (sector[i] == '\0') {
        break; /* End of files. */
      }

      if (sector[i+11] & 0x10 || sector[i+11] & 0x08) {
        continue; /* Subdirectory or volume label. */
      }

      int n = 0;
      for (int j = 0; j < 8; j++) {
        if (sector[i+j] == ' ') {
          break;
        }
        file_name[n] = sector[i+j];
        n++;
      }

      file_name[n] = '.';
      n++;

      for (int j = 0; j < 3; j++) {
        if (sector[i+8+j] == ' ') {
          break;
        }
        file_name[n] = sector[i+8+j];
        n++;
      }

      file_name[n] = '\0';

      file_cluster = sector[i+26] + (sector[i+27] << 8);
      file_size = sector[i+28] +
                 (sector[i+29] << 8) +
                 (sector[i+30] << 16) +
                 (sector[i+31] << 24);

      if (argc >= 3) {
        if (strcmp(argv[2], file_name) == 0) {
          cluster = file_cluster;
          cluster_file_size = file_size;
        }
      }
      printf("%12s (%d bytes) (cluster 0x%04x)\n",
        file_name,
        file_size,
        file_cluster);
    }
  }
  printf("\n");

  if (cluster > 0) { /* Dump a file? */
    FILE *out;

    out = fopen(argv[2], "wbx");
    if (out == NULL) {
      printf("Failed to open file for writing: %s\n", argv[2]);
      fclose(disk);
      return 1;
    }

    read_sector(disk, fat_offset, sector);

    cluster_end_indicator = sector[2] + (sector[3] << 8);
    
    printf("Cluster end indicator: 0x%04x\n", cluster_end_indicator);

    unsigned int bytes_written = 0;
    while ((cluster >> 3) != (cluster_end_indicator >> 3)) {
      printf("Read cluster: 0x%04x\n", cluster);

      for (int s = 0; s < sectors_per_cluster; s++) {
        read_sector(disk,
                    data_offset + 
                    ((cluster - 2) * sectors_per_cluster) +
                    s, sector);

        bytes_written += fwrite(sector, sizeof(uint8_t), 
          (bytes_written + 512 > cluster_file_size)
            ? cluster_file_size - bytes_written
            : 512, out);
      }

      /* Calculate next cluster. */
      cluster_fat = cluster / 256;
      cluster_offset = (cluster % 256) * 2;

      read_sector(disk, fat_offset + cluster_fat, sector);
      cluster = sector[cluster_offset] + (sector[cluster_offset + 1] << 8);
    }

    fclose(out);
  }

  fclose(disk);
  return 0;
}
          


Topic: Scripts and Code, by Kjetil @ 06/11-2021, Article Link

Space Ship Simulator in OpenGL

I finally finished a project I had suspended many years ago when I was looking into OpenGL. At that time my solution had problems that I now later have discovered was related to the dreaded Gimbal lock caused by the use of Euler angles. The new solution uses rotation matrices directly. It uses the GLUT library which is kind of deprecated, but still very widely available and convenient for simpler OpenGL programs and demos like this one.

This space ship simulator is quite simple, but allows movement in all directions. When the program is started, the "universe" is populated with some randomly generated (really small) planets which can be navigated around. Use W/S to pitch, A/S to yaw, Z/C to roll and R/F to increase/decrease thrusters.

Here is the code, compile it like so: gcc -o space space.c -lGL -lGLU -lglut -lm -Wall

#include <GL/glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define WORLD_SIZE 25 /* Distance to fake walls from origin. */

#define GRID_SPACING 5
#define GRID_START ((0 - WORLD_SIZE) + GRID_SPACING)
#define GRID_END (WORLD_SIZE - GRID_SPACING)
#define GRID_SIZE ((GRID_END - GRID_START) / GRID_SPACING)

#define PLANETS_MAX 10
#define PLANETS_CHANCE (GRID_SIZE * GRID_SIZE * GRID_SIZE) / PLANETS_MAX



typedef struct vector_s {
  float x;
  float y;
  float z;
} vector_t;

typedef struct planet_s {
  vector_t pos;
  float size;
  float color_r;
  float color_g;
  float color_b;
} planet_t;

typedef struct planet_distance_s {
  int planet_index;
  float distance;
} planet_distance_t;



static int camera_mode = 0;
static float ship_speed = 0.0;
static int old_mouse_x;
static int old_mouse_y;

static planet_t planets[PLANETS_MAX];
static int planets_count = 0;

static vector_t ship_forward_vector = {1,0,0};
static vector_t ship_up_vector      = {0,1,0};
static vector_t ship_right_vector   = {0,0,1};
static vector_t ship_position       = {0,0,0};



static vector_t vector_normalize(vector_t v)
{
  float magnitude;
  vector_t normalized;

  magnitude = sqrt((v.x * v.x) + (v.y * v.y) + (v.z * v.z));
  normalized.x = v.x / magnitude;
  normalized.y = v.y / magnitude;
  normalized.z = v.z / magnitude;

  return normalized;
}

static vector_t vector_cross_product(vector_t a, vector_t b)
{
  vector_t cross;

  cross.x = (a.y * b.z) - (a.z * b.y);
  cross.y = (a.z * b.x) - (a.x * b.z);
  cross.z = (a.x * b.y) - (a.y * b.x);

  return cross;
}

static vector_t vector_multiply(vector_t v, float f)
{
  vector_t product;

  product.x = v.x * f;
  product.y = v.y * f;
  product.z = v.z * f;

  return product;
}

static vector_t vector_sum(vector_t a, vector_t b)
{
  vector_t sum;

  sum.x = a.x + b.x;
  sum.y = a.y + b.y;
  sum.z = a.z + b.z;

  return sum;
}

static vector_t vector_difference(vector_t a, vector_t b)
{
  vector_t difference;

  difference.x = a.x - b.x;
  difference.y = a.y - b.y;
  difference.z = a.z - b.z;

  return difference;
}

static float vector_distance(vector_t a, vector_t b)
{
  return sqrt(((a.x - b.x) * (a.x - b.x)) +
              ((a.y - b.y) * (a.y - b.y)) +
              ((a.z - b.z) * (a.z - b.z)));
}



static void ship_pitch(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_yaw(float angle)
{
  ship_forward_vector = vector_normalize(
    vector_difference(
      vector_multiply(ship_forward_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_right_vector, sin(angle * (M_PI / 180)))));

  ship_right_vector = vector_cross_product(
    ship_forward_vector, ship_up_vector);
}

static void ship_roll(float angle)
{
  ship_right_vector = vector_normalize(
    vector_sum(
      vector_multiply(ship_right_vector, cos(angle * (M_PI / 180))),
      vector_multiply(ship_up_vector, sin(angle * (M_PI / 180)))));

  ship_up_vector = vector_cross_product(
    ship_forward_vector, ship_right_vector);

  ship_up_vector = vector_multiply(ship_up_vector, -1.0);
}

static void ship_advance(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_forward_vector, distance));
}

#ifdef DEBUG_MODE
static void ship_ascend(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_up_vector, distance));
}

static void ship_strafe(float distance)
{
  ship_position = vector_sum(ship_position, 
    vector_multiply(ship_right_vector, distance));
}
#endif /* DEBUG_MODE */



static void idle()
{
  if (ship_speed != 0) {
    ship_advance(ship_speed);
  }

  glutPostRedisplay();

#ifdef DEBUG_MODE
  fprintf(stderr, "Speed: %.2f, X: %.2f, Y: %.2f, Z: %.2f\n",
    ship_speed, ship_position.x, ship_position.y, ship_position.z);
#endif /* DEBUG_MODE */
}

static void draw_ship(void)
{
  float rotation[16];

  glTranslatef(ship_position.x, ship_position.y, ship_position.z);

  rotation[0]  = ship_right_vector.x;
  rotation[1]  = ship_right_vector.y;
  rotation[2]  = ship_right_vector.z;
  rotation[3]  = 0;
  rotation[4]  = ship_up_vector.x;
  rotation[5]  = ship_up_vector.y;
  rotation[6]  = ship_up_vector.z;
  rotation[7]  = 0;
  rotation[8]  = ship_forward_vector.x;
  rotation[9]  = ship_forward_vector.y;
  rotation[10] = ship_forward_vector.z;
  rotation[11] = 0;
  rotation[12] = 0;
  rotation[13] = 0;
  rotation[14] = 0;
  rotation[15] = 1;
  glMultMatrixf(rotation);

  glScalef(0.5, 0.5, 0.5);
  glColor3f(0.9, 0.9, 0.9);

  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, -1.0, 0.0);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(1.0, 1.0, 0.0);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
  glBegin(GL_LINE_LOOP);
    glVertex3f(-1.0, 1.0, 0.0);
    glVertex3f(-1.0, -1.0, 0.0);
    glVertex3f(0.0, 0.0, 4.0);
  glEnd();
}

#ifdef DEBUG_MODE
static void draw_axis_indicator(void)
{
  /* Draw it at origin. */
  GLfloat old_line_width;
  glGetFloatv(GL_LINE_WIDTH, &old_line_width);
  glLineWidth(3.0);

  glBegin(GL_LINES);
    /* Red X-Axis */
    glColor3f(1, 0, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(1, 0, 0); 
    /* Green Y-Axis */
    glColor3f(0, 1, 0);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 1, 0);
    /* Blue Z-Axis */
    glColor3f(0, 0, 1);
    glVertex3f(0, 0, 0);
    glVertex3f(0, 0, 1); 
  glEnd();

  glLineWidth(old_line_width);
}
#endif /* DEBUG_MODE */

static int planet_distance_compare(const void *p1, const void *p2)
{
  return ((planet_distance_t *)p1)->distance <
         ((planet_distance_t *)p2)->distance;
}

static void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glLoadIdentity();

  if (camera_mode == 0) {
    /* Inside ship. */
    vector_t view_point;
    view_point = vector_sum(ship_position, ship_forward_vector);

    gluLookAt(ship_position.x, ship_position.y, ship_position.z,
              view_point.x, view_point.y, view_point.z,
              ship_up_vector.x, ship_up_vector.y, ship_up_vector.z);
  } else {
    /* Outside ship, a static look towards origin. */
    glRotatef(45.0, 1.0, 0.0, 0.0);
    glRotatef(120.0, 0.0, 1.0, 0.0);
    glTranslatef(WORLD_SIZE / 2, 0 - (WORLD_SIZE / 2), WORLD_SIZE / 2);
  }

  /* Draw floor/roof/walls, which are actually big cubes! */
  glColor3f(0.0, 0.0, 0.0);

  glPushMatrix();
    glTranslatef(0.0, -WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, WORLD_SIZE, 0.0);
    glScalef(WORLD_SIZE * 2, 0.1, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(-WORLD_SIZE, 0.0, 0.0);
    glScalef(0.1, WORLD_SIZE * 2, WORLD_SIZE * 2);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.5, 0.0);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.5, 0.0, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  glPushMatrix();
    glTranslatef(0.0, 0.0, -WORLD_SIZE);
    glScalef(WORLD_SIZE * 2, WORLD_SIZE * 2, 0.1);
#ifdef DEBUG_MODE
    glColor3f(0.0, 0.5, 0.5);
#endif /* DEBUG_MODE */
    glutSolidCube(1.0);
  glPopMatrix();

  /* Calculate distance from ship to planets, because they need to be
     drawn/rendered from the furthest away first. */
  planet_distance_t distance[PLANETS_MAX];
  for (int i = 0; i < planets_count; i++) {
    distance[i].planet_index = i;
    distance[i].distance = vector_distance(ship_position, planets[i].pos);
  }
  qsort(distance, planets_count, sizeof(planet_distance_t),
    planet_distance_compare);

  /* Draw planets. */
  for (int i = 0; i < planets_count; i++) {
    int n = distance[i].planet_index;
    glPushMatrix();
      GLUquadric *quadric = gluNewQuadric();
      if (quadric != 0) {
        glTranslatef(planets[n].pos.x, planets[n].pos.y, planets[n].pos.z);
        glColor3f(planets[n].color_r, planets[n].color_g, planets[n].color_b);
        gluSphere(quadric, planets[n].size, 20, 20);
        gluDeleteQuadric(quadric);
      }
    glPopMatrix();
  }
    
  if (camera_mode != 0) {
    glPushMatrix();
      draw_ship();
    glPopMatrix();
  }

#ifdef DEBUG_MODE
  glPushMatrix();
    draw_axis_indicator();
  glPopMatrix();
#endif /* DEBUG_MODE */

  glFlush();
  glutSwapBuffers();
}

static void reshape(int w, int h)
{
  glViewport(0, 0, (GLsizei)w, (GLsizei)h);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 150.0);
  glMatrixMode(GL_MODELVIEW);
}

static void keyboard(unsigned char key, int x, int y)
{
  switch (key) {
  case 's':
    ship_pitch(-1.0);
    break;
  case 'w':
    ship_pitch(1.0);
    break;
  case 'd':
    ship_yaw(-1.0);
    break;
  case 'a':
    ship_yaw(1.0);
    break;
  case 'z':
    ship_roll(-1.0);
    break;
  case 'c':
    ship_roll(1.0);
    break;
  case 'r':
    ship_speed += 0.01;
    break;
  case 'f':
    ship_speed -= 0.01;
    break;
#ifdef DEBUG_MODE
  case 't':
    ship_advance(0.1);
    break;
  case 'g':
    ship_advance(-0.1);
    break;
  case 'y':
    ship_ascend(0.1);
    break;
  case 'h':
    ship_ascend(-0.1);
    break;
  case 'u':
    ship_strafe(0.1);
    break;
  case 'j':
    ship_strafe(-0.1);
    break;
  case 'x':
    camera_mode = (camera_mode) ? 0 : 1;
    break;
#endif /* DEBUG_MODE */
  case 'q':
    exit(0);
    break;
  }
}

static void motion(int x, int y)
{
  if (x > old_mouse_x) {
    ship_yaw(1.0);
  } else if (x < old_mouse_x) {
    ship_yaw(-1.0);
  }

  if (y > old_mouse_y) {
    ship_pitch(-1.0);
  } else if (y < old_mouse_y) {
    ship_pitch(1.0);
  }

  old_mouse_x = x;
  old_mouse_y = y;
}

void generate_planets(void)
{
  for (int x = GRID_START; x < GRID_END; x += GRID_SPACING) {
    for (int y = GRID_START; y < GRID_END; y += GRID_SPACING) {
      for (int z = GRID_START; z < GRID_END; z += GRID_SPACING) {
        if (x == 0 && y == 0 && z == 0) {
          /* Ignore origin. */
          continue;
        }

        if ((rand() % PLANETS_CHANCE) == 0) {
          planets[planets_count].pos.x = (x - 2) + (rand() % 4);
          planets[planets_count].pos.y = (y - 2) + (rand() % 4);
          planets[planets_count].pos.z = (z - 2) + (rand() % 4);
          planets[planets_count].size = 
            ((rand() % ((GRID_SPACING * 10) - 1)) / 20.0) + 0.1;
          planets[planets_count].color_r = (rand() % 11) / 10.0;
          planets[planets_count].color_g = (rand() % 11) / 10.0;
          planets[planets_count].color_b = (rand() % 11) / 10.0;

#ifdef DEBUG_MODE
          printf("X=%.1f, Y=%.1f, Z=%.1f, Size=%.1f, Col=%.1f;%.1f;%.1f\n", 
            planets[planets_count].pos.x,
            planets[planets_count].pos.y,
            planets[planets_count].pos.z,
            planets[planets_count].size,
            planets[planets_count].color_r,
            planets[planets_count].color_g,
            planets[planets_count].color_b);
#endif /* DEBUG_MODE */

          if ((planets_count + 1) >= PLANETS_MAX) {
            return;
          } else {
            planets_count++;
          }
        }
      }
    }
  }
}

int main(int argc, char *argv[])
{
  srand(time(NULL));
  generate_planets();
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
  glutInitWindowSize(800, 800);
  glutInitWindowPosition(100, 100);
  glutCreateWindow(argv[0]);
  glClearColor(0.0, 0.0, 0.0, 0.0);
  glShadeModel(GL_FLAT);
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutIdleFunc(idle);
  glutKeyboardFunc(keyboard);
  glutMotionFunc(motion);
  glutMainLoop();
  return 0;
}
          


Here is a screenshot of what it looks like, just some "planets" that looks like spheres:

Space Ship Simulator Screenshot


Topic: Scripts and Code, by Kjetil @ 05/03-2021, Article Link

Older articles