The programming of the cooley switch demonstrates an interesting feature of the FSPXW patch: you can program up to four different strings on each button. The first time you press a button, FSPXW emits the first string, the second time you press a button it emits the second string, then the third, and finally the fourth. If the string is empty, the FSPXW string skips it. The FSPXW patch uses the cooley switch to select the cockpit views. Pressing the cooley switch forward displays the forward view. Pulling the cooley switch backwards presents the rear view. However, the XWing game provides three left and right views. Pushing the cooley switch to the left or right once displays the 45 degree view. Pressing it a second time presents the 90 degree view. Pressing it to the left or right a third time provides the 135 degree view. The following diagram shows the default programming on the cooley switch:
One word of caution concerning this patch: it only works with the basic XWing game. It does not support the add-on modules (Imperial Pursuit, B-Wing, Tie Fighter, etc.). Furthermore, this patch assumes that the basic XWing code has not changed over the years. It could be that a recent release of the XWing game uses new joystick routines and the code associated with this application will not be able to locate or patch those new routines. This patch will detect such a situation and will not patch XWing if this is the case. You must have sufficient free RAM for this patch, XWing, and anything else you have loaded into memory at the same time (the exact amount of RAM XWing needs depends upon the features you've installed, a fully installed system requires slightly more than 610K free).
Without further ado, here's the FSPXW code:
.286 page 58, 132 name FSPXW title FSPXW (Flightstick Pro driver for XWING). subttl Copyright (C) 1994 Randall Hyde. ; FSPXW.EXE ; ; Usage: ; FSPXW ; ; This program executes the XWING.EXE program and patches it to use the ; Flightstick Pro. byp textequ <byte ptr> wp textequ <word ptr> cseg segment para public 'CODE' cseg ends sseg segment para stack 'STACK' sseg ends zzzzzzseg segment para public 'zzzzzzseg' zzzzzzseg ends include stdlib.a includelib stdlib.lib matchfuncs ifndef debug Installation segment para public 'Install' Installation ends endif CSEG segment para public 'CODE' assume cs:cseg, ds:nothing ; Timer interrupt vector Int1CVect dword ? ; PSP- Program Segment Prefix. Needed to free up memory before running ; the real application program. PSP word 0 ; Program Loading data structures (for DOS). ExecStruct word 0 ;Use parent's Environment blk. dword CmdLine ;For the cmd ln parms. dword DfltFCB dword DfltFCB LoadSSSP dword ? LoadCSIP dword ? PgmName dword Pgm ; Variables for the throttle pot. ; LastThrottle contains the character last sent (so we only send one copy). ; ThrtlCntDn counts the number of times the throttle routine gets called. LastThrottle byte 0 ThrtlCntDn byte 10 ; Button Mask- Used to mask out the programmed buttons when the game ; reads the real buttons. ButtonMask byte 0f0h ; The following variables allow the user to reprogram the buttons. KeyRdf struct Ptrs word ? ;The PTRx fields point at the ptr2 word ? ; four possible strings of 8 chars ptr3 word ? ; each. Each button press cycles ptr4 word ? ; through these strings. Index word ? ;Index to next string to output. Cnt word ? Pgmd word ? ;Flag = 0 if not redefined. KeyRdf ends ; Left codes are output if the cooley switch is pressed to the left. ; Note that the strings ares actually zero terminated strings of words. Left KeyRdf <Left1, Left2, Left3, Left4, 0, 6, 1> Left1 word '7', 0 Left2 word '4', 0 Left3 word '1', 0 Left4 word 0 ; Right codes are output if the cooley switch is pressed to the Right. Right KeyRdf <Right1, Right2, Right3, Right4, 0, 6, 1> Right1 word '9', 0 Right2 word '6', 0 Right3 word '3', 0 Right4 word 0 ; Up codes are output if the cooley switch is pressed Up. Up KeyRdf <Up1, Up2, Up3, Up4, 0, 2, 1> Up1 word '8', 0 Up2 word 0 Up3 word 0 Up4 word 0 ; DownKey codes are output if the cooley switch is pressed Down. Down KeyRdf <Down1, Down2, Down3, Down4, 0, 2, 1> Down1 word '2', 0 Down2 word 0 Down3 word 0 Down4 word 0 ; Sw0 codes are output if the user pulls the trigger.(This switch is not ; redefined.) Sw0 KeyRdf <Sw01, Sw02, Sw03, Sw04, 0, 0, 0> Sw01 word 0 Sw02 word 0 Sw03 word 0 Sw04 word 0 ; Sw1 codes are output if the user presses Sw1 (the left button ; if the user hasn't swapped the left and right buttons). Not Redefined. Sw1 KeyRdf <Sw11, Sw12, Sw13, Sw14, 0, 0, 0> Sw11 word 0 Sw12 word 0 Sw13 word 0 Sw14 word 0 ; Sw2 codes are output if the user presses Sw2 (the middle button). Sw2 KeyRdf <Sw21, Sw22, Sw23, Sw24, 0, 2, 1> Sw21 word 'w', 0 Sw22 word 0 Sw23 word 0 Sw24 word 0 ; Sw3 codes are output if the user presses Sw3 (the right button ; if the user hasn't swapped the left and right buttons). Sw3 KeyRdf <Sw31, Sw32, Sw33, Sw34, 0, 0, 0> Sw31 word 0 Sw32 word 0 Sw33 word 0 Sw34 word 0 ; Switch status buttons: CurSw byte 0 LastSw byte 0 ;**************************************************************************** ; FSPXW patch begins here. This is the memory resident part. Only put code ; which which has to be present at run-time or needs to be resident after ; freeing up memory. ;**************************************************************************** Main proc mov cs:PSP, ds mov ax, cseg ;Get ptr to vars segment mov ds, ax ; Get the current INT 1Ch interrupt vector: mov ax, 351ch int 21h mov wp Int1CVect, bx mov wp Int1CVect+2, es ; The following call to MEMINIT assumes no error occurs. If it does, ; we're hosed anyway. mov ax, zzzzzzseg mov es, ax mov cx, 1024/16 meminit2 ; Do some initialization before running the game. These are calls to the ; initialization code which gets dumped before actually running XWING. call far ptr ChkBIOS15 call far ptr Identify call far ptr Calibrate ; If any switches were programmed, remove those switches from the ; ButtonMask: mov al, 0f0h ;Assume all buttons are okay. cmp sw0.pgmd, 0 je Sw0NotPgmd and al, 0e0h ;Remove sw0 from contention. Sw0NotPgmd: cmp sw1.pgmd, 0 je Sw1NotPgmd and al, 0d0h ;Remove Sw1 from contention. Sw1NotPgmd: cmp sw2.pgmd, 0 je Sw2NotPgmd and al, 0b0h ;Remove Sw2 from contention. Sw2NotPgmd: cmp sw3.pgmd, 0 je Sw3NotPgmd and al, 070h ;Remove Sw3 from contention. Sw3NotPgmd: mov ButtonMask, al ;Save result as button mask ; Now, free up memory from ZZZZZZSEG on to make room for XWING. ; Note: Absolutely no calls to UCR Standard Library routines from ; this point forward! (ExitPgm is okay, it's just a macro which calls DOS.) ; Note that after the execution of this code, none of the code & data ; from zzzzzzseg on is valid. mov bx, zzzzzzseg sub bx, PSP inc bx mov es, PSP mov ah, 4ah int 21h jnc GoodRealloc print byte "Memory allocation error." byte cr,lf,0 jmp Quit GoodRealloc: ; Now load the XWING program into memory: mov bx, seg ExecStruct mov es, bx mov bx, offset ExecStruc ;Ptr to program record. lds dx, PgmName mov ax, 4b01h ;Load, do not exec, pgm int 21h jc Quit ;If error loading file. ; Search for the joystick code in memory: mov si, zzzzzzseg mov ds, si xor si, si mov di, cs mov es, di mov di, offset JoyStickCode mov cx, JoyLength call FindCode jc Quit ;If didn't find joystick code. ; Patch the XWING joystick code here mov byp ds:[si], 09ah ;Far call mov wp ds:[si+1], offset ReadGame mov wp ds:[si+3], cs ; Find the Button code here. mov si, zzzzzzseg mov ds, si xor si, si mov di, cs mov es, di mov di, offset ReadSwCode mov cx, ButtonLength call FindCode jc Quit ; Patch the button code here. mov byp ds:[si], 9ah mov wp ds:[si+1], offset ReadButtons mov wp ds:[si+3], cs mov byp ds:[si+5], 90h ;NOP. ; Patch in our timer interrupt handler: mov ax, 251ch mov dx, seg MyInt1C mov ds, dx mov dx, offset MyInt1C int 21h ; Okay, start the XWING.EXE program running mov ah, 62h ;Get PSP int 21h mov ds, bx mov es, bx mov wp ds:[10], offset Quit mov wp ds:[12], cs mov ss, wp cseg:LoadSSSP+2 mov sp, wp cseg:LoadSSSP jmp dword ptr cseg:LoadCSIP Quit: lds dx, cs:Int1CVect ;Restore timer vector. mov ax, 251ch int 21h ExitPgm Main endp ;**************************************************************************** ; ; ReadGame- This routine gets called whenever XWing reads the joystick. ; On every 10th call it will read the throttle pot and send ; appropriate characters to the type ahead buffer, if ; necessary. assume ds:nothing ReadGame proc far dec cs:ThrtlCntDn ;Only do this each 10th time jne SkipThrottle ; XWING calls the joystick mov cs:ThrtlCntDn, 10 ; routine. push ax push bx ;No need to save bp, dx, or cx as push di ; XWING preserves these. mov ah, 84h mov dx, 103h ;Read the throttle pot int 15h ; Convert the value returned by the pot routine into the four characters ; 0..63:"\", 64..127:"[", 128..191:"]", 192..255:<bs>, to denote zero, 1/3, ; 2/3, and full power, respectively. mov dl, al mov ax, "\" ;Zero power cmp dl, 192 jae SetPower mov ax, "[" ;1/3 power. cmp dl, 128 jae SetPower mov ax, "]" ;2/3 power. cmp dl, 64 jae SetPower mov ax, 8 ;BS, full power. SetPower: cmp al, cs:LastThrottle je SkipPIB mov cs:LastThrottle, al call PutInBuffer SkipPIB: pop di pop bx pop ax SkipThrottle: neg bx ;XWING returns data in these registers. neg di ;We patched the NEG and STI instrs sti ; so do that here. ret ReadGame endp assume ds:nothing ReadButtons proc far mov ah, 84h mov dx, 0 int 15h not al and al, ButtonMask ;Turn off pgmd buttons. ret ReadButtons endp ; MyInt1C- Called every 1/18th second. Reads switches and decides if it ; should shove some characters into the type ahead buffer. assume ds:cseg MyInt1c proc far push ds push ax push bx push dx mov ax, cseg mov ds, ax mov al, CurSw mov LastSw, al mov dx, 900h ;Read the 8 switches. mov ah, 84h int 15h mov CurSw, al xor al, LastSw ;See if any changes jz NoChanges and al, CurSw ;See if sw just went down. jz NoChanges ; If a switch has just gone down, output an appropriate set of scan codes ; for it, if that key is active. Note that pressing *any* key will reset ; all the other key indexes. test al, 1 ;See if Sw0 (trigger) was pulled. jz NoSw0 cmp Sw0.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset the key indexes for all keys mov Right.Index, ax ; except SW0. mov Up.Index, ax mov Down.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Sw0.Index mov ax, Sw0.Index mov bx, Sw0.Ptrs[bx] add ax, 2 cmp ax, Sw0.Cnt jb SetSw0 mov ax, 0 SetSw0: mov Sw0.Index, ax call PutStrInBuf jmp NoChanges NoSw0: test al, 2 ;See if Sw1 (left sw) was pressed. jz NoSw1 cmp Sw1.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset the key indexes for all keys mov Right.Index, ax ; except Sw1. mov Up.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Sw1.Index mov ax, Sw1.Index mov bx, Sw1.Ptrs[bx] add ax, 2 cmp ax, Sw1.Cnt jb SetSw1 mov ax, 0 SetSw1: mov Sw1.Index, ax call PutStrInBuf jmp NoChanges NoSw1: test al, 4 ;See if Sw2 (middle sw) was pressed. jz NoSw2 cmp Sw2.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset the key indexes for all keys mov Right.Index, ax ; except Sw2. mov Up.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw3.Index, ax mov bx, Sw2.Index mov ax, Sw2.Index mov bx, Sw2.Ptrs[bx] add ax, 2 cmp ax, Sw2.Cnt jb SetSw2 mov ax, 0 SetSw2: mov Sw2.Index, ax call PutStrInBuf jmp NoChanges NoSw2: test al, 8 ;See if Sw3 (right sw) was pressed. jz NoSw3 cmp Sw3.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset the key indexes for all keys mov Right.Index, ax ; except Sw3. mov Up.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov bx, Sw3.Index mov ax, Sw3.Index mov bx, Sw3.Ptrs[bx] add ax, 2 cmp ax, Sw3.Cnt jb SetSw3 mov ax, 0 SetSw3: mov Sw3.Index, ax call PutStrInBuf jmp NoChanges NoSw3: test al, 10h ;See if Cooly was pressed upwards. jz NoUp cmp Up.Pgmd, 0 je NoChanges mov ax, 0 mov Right.Index, ax ;Reset all but Up. mov Left.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Up.Index mov ax, Up.Index mov bx, Up.Ptrs[bx] add ax, 2 cmp ax, Up.Cnt jb SetUp mov ax, 0 SetUp: mov Up.Index, ax call PutStrInBuf jmp NoChanges NoUp: test al, 20h ;See if Cooley was pressed left. jz NoLeft cmp Left.Pgmd, 0 je NoChanges mov ax, 0 mov Right.Index, ax ;Reset all but Left. mov Up.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Left.Index mov ax, Left.Index mov bx, Left.Ptrs[bx] add ax, 2 cmp ax, Left.Cnt jb SetLeft mov ax, 0 SetLeft: mov Left.Index, ax call PutStrInBuf jmp NoChanges NoLeft: test al, 40h ;See if Cooley was pressed Right jz NoRight cmp Right.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset all but Right. mov Up.Index, ax mov Down.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Right.Index mov ax, Right.Index mov bx, Right.Ptrs[bx] add ax, 2 cmp ax, Right.Cnt jb SetRight mov ax, 0 SetRight: mov Right.Index, ax call PutStrInBuf jmp NoChanges NoRight: test al, 80h ;See if Cooly was pressed Downward. jz NoChanges cmp Down.Pgmd, 0 je NoChanges mov ax, 0 mov Left.Index, ax ;Reset all but Down. mov Up.Index, ax mov Right.Index, ax mov Sw0.Index, ax mov Sw1.Index, ax mov Sw2.Index, ax mov Sw3.Index, ax mov bx, Down.Index mov ax, Down.Index mov bx, Down.Ptrs[bx] add ax, 2 cmp ax, Down.Cnt jb SetDown mov ax, 0 SetDown: mov Down.Index, ax call PutStrInBuf NoChanges: pop dx pop bx pop ax pop ds jmp cs:Int1CVect MyInt1c endp assume ds:nothing ; PutStrInBuf- BX points at a zero terminated string of words. ; Output each word by calling PutInBuffer. PutStrInBuf proc near push ax push bx PutLoop: mov ax, [bx] test ax, ax jz PutDone call PutInBuffer add bx, 2 jmp PutLoop PutDone: pop bx pop ax ret PutStrInBuf endp ; PutInBuffer- Outputs character and scan code in AX to the type ahead ; buffer. assume ds:nothing KbdHead equ word ptr ds:[1ah] KbdTail equ word ptr ds:[1ch] KbdBuffer equ word ptr ds:[1eh] EndKbd equ 3eh Buffer equ 1eh PutInBuffer proc near push ds push bx mov bx, 40h mov ds, bx pushf cli ;This is a critical region! mov bx, KbdTail ;Get ptr to end of type inc bx ; ahead buffer and make room inc bx ; for this character. cmp bx, buffer+32 ;At physical end of buffer? jb NoWrap mov bx, buffer ;Wrap back to 1eH if at end. ; NoWrap: cmp bx, KbdHead ;Buffer overrun? je PIBDone xchg KbdTail, bx ;Set new, get old, ptrs. mov ds:[bx], ax ;Output AX to old location. PIBDone: popf ;Restore interrupts pop bx pop ds ret PutInBuffer endp ;**************************************************************************** ; ; FindCode: On entry, ES:DI points at some code in *this* program which ; appears in the ATP game. DS:SI points at a block of memory ; in the XWing game. FindCode searches through memory to find the ; suspect piece of code and returns DS:SI pointing at the start of ; that code. This code assumes that it *will* find the code! ; It returns the carry clear if it finds it, set if it doesn't. FindCode proc near push ax push bx push dx DoCmp: mov dx, 1000h CmpLoop: push di ;Save ptr to compare code. push si ;Save ptr to start of string. push cx ;Save count. repe cmpsb pop cx pop si pop di je FoundCode inc si dec dx jne CmpLoop sub si, 1000h mov ax, ds inc ah mov ds, ax cmp ax, 9000h jb DoCmp pop dx pop bx pop ax stc ret FoundCode: pop dx pop bx pop ax clc ret FindCode endp ;**************************************************************************** ; ; Joystick and button routines which appear in XWing game. This code is ; really data as the INT 21h patch code searches through memory for this code ; after loading a file from disk. JoyStickCode proc near sti neg bx neg di pop bp pop dx pop cx ret mov bp, bx in al, dx mov bl, al not al and al, ah jnz $+11h in al, dx JoyStickCode endp EndJSC: JoyLength = EndJSC-JoyStickCode ReadSwCode proc mov dx, 201h in al, dx xor al, 0ffh and ax, 0f0h ReadSwCode endp EndRSC: ButtonLength = EndRSC-ReadSwCode cseg ends Installation segment ; Move these things here so they do not consume too much space in the ; resident part of the patch. DfltFCB byte 3," ",0,0,0,0,0 CmdLine byte 2, " ", 0dh, 126 dup (" ") ;Cmd line for program Pgm byte "XWING.EXE",0 byte 128 dup (?) ;For user's name ; ChkBIOS15- Checks to see if the INT 15 driver for FSPro is present in memory. ChkBIOS15 proc far mov ah, 84h mov dx, 8100h int 15h mov di, bx strcmpl byte "CH Products:Flightstick Pro",0 jne NoDriverLoaded ret NoDriverLoaded: print byte "CH Products SGDI driver for Flightstick Pro is not " byte "loaded into memory.",cr,lf byte "Please run FSPSGDI before running this program." byte cr,lf,0 exitpgm ChkBIOS15 endp ;**************************************************************************** ; ; Identify- Prints a sign-on message. assume ds:nothing Identify proc far ; Print a welcome string. Note that the string "VersionStr" will be ; modified by the "version.exe" program each time you assemble this code. print byte cr,lf,lf byte "X W I N G P A T C H",cr,lf byte "CH Products Flightstick Pro",cr,lf byte "Copyright 1994, Randall Hyde",cr,lf byte lf byte 0 ret Identify endp ;**************************************************************************** ; ; Calibrate the throttle down here: assume ds:nothing Calibrate proc far print byte cr,lf,lf byte "Calibration:",cr,lf,lf byte "Move the throttle to one extreme and press any " byte "button:",0 call Wait4Button mov ah, 84h mov dx, 1h int 15h push dx ;Save pot 3 reading. print byte cr,lf byte "Move the throttle to the other extreme and press " byte "any button:",0 call Wait4Button mov ah, 84h mov dx, 1 int 15h pop bx mov ax, dx cmp ax, bx jb RangeOkay xchg ax, bx RangeOkay: mov cx, bx ;Compute a centered value. sub cx, ax shr cx, 1 add cx, ax mov ah, 84h mov dx, 303h ;Calibrate pot three. int 15h ret Calibrate endp Wait4Button proc near mov ah, 84h ;First, wait for all buttons mov dx, 0 ; to be released. int 15h and al, 0F0h cmp al, 0F0h jne Wait4Button mov cx, 0 Delay: loop Delay Wait4Press: mov ah, 1 ;Eat any characters from the int 16h ; keyboard which come along, and je NoKbd ; handle ctrl-C as appropriate. getc NoKbd: mov ah, 84h ;Now wait for any button to be mov dx, 0 ; pressed. int 15h and al, 0F0h cmp al, 0F0h je Wait4Press ret Wait4Button endp Installation ends sseg segment para stack 'STACK' word 256 dup (0) endstk word ? sseg ends zzzzzzseg segment para public 'zzzzzzseg' Heap byte 1024 dup (0) zzzzzzseg ends end Main