diff --git a/.gitignore b/.gitignore
index 5c8daed..1ac18e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -47,3 +47,17 @@ build/
*~
#*
.#*
+
+# Automatically generated source files
+acorn.asm
+asmb.asm
+data.asm
+eval.asm
+exec.asm
+main.asm
+math.asm
+
+# ZDS build files
+Debug/
+Release/
+*.wsp
\ No newline at end of file
diff --git a/README.md b/README.md
index 384e103..e011819 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,7 @@
-# BBCZ80
+# BBCZ80 for the Agon Light
+This is a fork of BBCZ80 by R.T.Russell with a rewritten MOS layer for the Agon Light.
+
+## BBCZ80
BBC BASIC (Z80) v5 is an implementation of the BBC BASIC programming language for the Z80 CPU.
It is largely compatible with Acorn's ARM BASIC V but with a few language extensions based on
features of 'BBC BASIC for Windows' and 'BBC BASIC for SDL 2.0'. These extensions include the
@@ -16,3 +19,23 @@ to build the Acorn Z80 Second Processor edition.
Note that the name 'BBC BASIC' is used by permission of the British Broadcasting Corporation
and is not transferrable to a derived or forked work.
+
+## Agon Light
+The Agon Light version uses the following files from the original fork:
+
+- ACORN.Z80
+- ASMB.Z80
+- DATA.Z80
+- EVAL.Z80
+- EXEC.Z80
+- MAIN.Z80
+- MATH.Z80
+
+All Agon specific source code and ZDSII project files are in the folder src/zds.
+
+### Building
+
+The files from the original fork are converted to work with the ZDS assembler by the Python script tools/transform_source.py
+and written out to the folder src/zds.
+
+To build, load the project "BBC BASIC" into the Zilog ZDSII IDE and press F7.
\ No newline at end of file
diff --git a/src/zds/BBC Basic.zdsproj b/src/zds/BBC Basic.zdsproj
new file mode 100644
index 0000000..a2d8bde
--- /dev/null
+++ b/src/zds/BBC Basic.zdsproj
@@ -0,0 +1,220 @@
+
+eZ80F92
+
+
+
+.\agon_graphics.asm
+.\agon_gpio.asm
+.\agon_init.asm
+.\agon_interrupt.asm
+.\agon_misc.asm
+.\agon_os.asm
+.\agon_sound.asm
+.\acorn.asm
+.\asmb.asm
+.\data.asm
+.\eval.asm
+.\exec.asm
+.\main.asm
+.\math.asm
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/zds/agon_gpio.asm b/src/zds/agon_gpio.asm
new file mode 100644
index 0000000..be171e8
--- /dev/null
+++ b/src/zds/agon_gpio.asm
@@ -0,0 +1,110 @@
+;
+; Title: BBC Basic for AGON - GPIO functions
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 04/12/2024
+;
+; Modinfo:
+
+ INCLUDE "macros.inc"
+ INCLUDE "equs.inc"
+
+ .ASSUME ADL = 0
+
+ SEGMENT CODE
+
+ XDEF GPIOB_SETMODE
+
+ XREF SWITCH_A
+
+; A: Mode
+; B: Pins
+;
+GPIOB_SETMODE: CALL SWITCH_A
+ DW GPIOB_M0 ; Output
+ DW GPIOB_M1 ; Input
+ DW GPIOB_M2 ; Open Drain IO
+ DW GPIOB_M3 ; Open Source IO
+ DW GPIOB_M4 ; Interrupt, Dual Edge
+ DW GPIOB_M5 ; Alt Function
+ DW GPIOB_M6 ; Interrupt, Active Low
+ DW GPIOB_M7 ; Interrupt, Active High
+ DW GPIOB_M8 ; Interrupt, Falling Edge
+ DW GPIOB_M9 ; Interrupt, Rising Edge
+
+; Output
+;
+GPIOB_M0: RES_GPIO PB_DDR, B
+ RES_GPIO PB_ALT1, B
+ RES_GPIO PB_ALT2, B
+ RET
+
+; Input
+;
+GPIOB_M1: SET_GPIO PB_DDR, B
+ RES_GPIO PB_ALT1, B
+ RES_GPIO PB_ALT2, B
+ RET
+
+; Open Drain IO
+;
+GPIOB_M2: RES_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ RES_GPIO PB_ALT2, B
+ RET
+
+; Open Source IO
+;
+GPIOB_M3: SET_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ RES_GPIO PB_ALT2, B
+ RET
+
+; Interrupt, Dual Edge
+;
+GPIOB_M4: SET_GPIO PB_DR, B
+ RES_GPIO PB_DDR, B
+ RES_GPIO PB_ALT1, B
+ RES_GPIO PB_ALT2, B
+ RET
+
+; Alt Function
+;
+GPIOB_M5: SET_GPIO PB_DDR, B
+ RES_GPIO PB_ALT1, B
+ SET_GPIO PB_ALT2, B
+ RET
+
+; Interrupt, Active Low
+;
+GPIOB_M6: RES_GPIO PB_DR, B
+ RES_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ SET_GPIO PB_ALT2, B
+ RET
+
+
+; Interrupt, Active High
+;
+GPIOB_M7: SET_GPIO PB_DR, B
+ RES_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ SET_GPIO PB_ALT2, B
+ RET
+
+
+; Interrupt, Falling Edge
+;
+GPIOB_M8: RES_GPIO PB_DR, B
+ SET_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ SET_GPIO PB_ALT2, B
+ RET
+
+; Interrupt, Rising Edge
+;
+GPIOB_M9: SET_GPIO PB_DR, B
+ SET_GPIO PB_DDR, B
+ SET_GPIO PB_ALT1, B
+ SET_GPIO PB_ALT2, B
+ RET
\ No newline at end of file
diff --git a/src/zds/agon_graphics.asm b/src/zds/agon_graphics.asm
new file mode 100644
index 0000000..6569d4c
--- /dev/null
+++ b/src/zds/agon_graphics.asm
@@ -0,0 +1,156 @@
+;
+; Title: BBC Basic for AGON - Graphics stuff
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 17/12/2024
+;
+; Modinfo:
+; 11/12/2024: Modified POINT_ to work with OSWORD
+; 17/12/2024: Modified GETSCHR
+
+ .ASSUME ADL = 0
+
+ INCLUDE "equs.inc"
+ INCLUDE "macros.inc"
+ INCLUDE "mos_api.inc" ; In MOS/src
+
+ SEGMENT CODE
+
+ XDEF MODE_
+ XDEF COLOUR_
+ XDEF POINT_
+ XDEF GETSCHR
+
+ XREF ACCS
+ XREF OSWRCH
+ XREF ASC_TO_NUMBER
+ XREF EXTERR
+ XREF EXPRI
+ XREF COMMA
+ XREF XEQ
+ XREF NXT
+ XREF BRAKET
+ XREF CRTONULL
+ XREF NULLTOCR
+ XREF CRLF
+ XREF EXPR_W2
+
+; MODE n: Set video mode
+;
+MODE_: PUSH IX ; Get the system vars in IX
+ MOSCALL mos_sysvars ; Reset the semaphore
+ RES.LIL 4, (IX+sysvar_vpd_pflags)
+ CALL EXPRI
+ EXX
+ VDU 16H ; Mode change
+ VDU L
+ MOSCALL mos_sysvars
+$$: BIT.LIL 4, (IX+sysvar_vpd_pflags)
+ JR Z, $B ; Wait for the result
+ POP IX
+ JP XEQ
+
+;
+; Fetch a character from the screen
+; - DE: X coordinate
+; - HL: Y coordinate
+; Returns
+; - A: The character or FFh if no match
+; - F: C if match, otherwise NC
+;
+GETSCHR: PUSH IX ; Get the system vars in IX
+ MOSCALL mos_sysvars ; Reset the semaphore
+ RES.LIL 1, (IX+sysvar_vpd_pflags)
+ VDU 23
+ VDU 0
+ VDU vdp_scrchar
+ VDU E
+ VDU D
+ VDU L
+ VDU H
+$$: BIT.LIL 1, (IX+sysvar_vpd_pflags)
+ JR Z, $B ; Wait for the result
+ LD.LIL A, (IX+sysvar_scrchar) ; Fetch the result in A
+ OR A ; Check for 00h
+ SCF ; C = character map
+ JR NZ, $F ; We have a character, so skip next bit
+ XOR A ; Clear carry
+$$: POP IX
+ RET
+
+; POINT(x,y): Get the pixel colour of a point on screen
+; Parameters:
+; - DE: X-coordinate
+; - HL: Y-coordinate
+; Returns:
+; - A: Pixel colour
+;
+POINT_: PUSH IX ; Get the system vars in IX
+ MOSCALL mos_sysvars ; Reset the semaphore
+ RES.LIL 2, (IX+sysvar_vpd_pflags)
+ VDU 23
+ VDU 0
+ VDU vdp_scrpixel
+ VDU E
+ VDU D
+ VDU L
+ VDU H
+$$: BIT.LIL 2, (IX+sysvar_vpd_pflags)
+ JR Z, $B ; Wait for the result
+;
+; Return the data as a 1 byte index
+;
+ LD.LIL A, (IX+(sysvar_scrpixelIndex))
+ POP IX
+ RET
+
+; COLOUR colour
+; COLOUR L,P
+; COLOUR L,R,G,B
+;
+COLOUR_: CALL EXPRI ; The colour / mode
+ EXX
+ LD A, L
+ LD (VDU_BUFFER+0), A ; Store first parameter
+ CALL NXT ; Are there any more parameters?
+ CP ','
+ JR Z, COLOUR_1 ; Yes, so we're doing a palette change next
+;
+ VDU 11h ; Just set the colour
+ VDU (VDU_BUFFER+0)
+ JP XEQ
+;
+COLOUR_1: CALL COMMA
+ CALL EXPRI ; Parse R (OR P)
+ EXX
+ LD A, L
+ LD (VDU_BUFFER+1), A
+ CALL NXT ; Are there any more parameters?
+ CP ','
+ JR Z, COLOUR_2 ; Yes, so we're doing COLOUR L,R,G,B
+;
+ VDU 13h ; VDU:COLOUR
+ VDU (VDU_BUFFER+0) ; Logical Colour
+ VDU (VDU_BUFFER+1) ; Palette Colour
+ VDU 0 ; RGB set to 0
+ VDU 0
+ VDU 0
+ JP XEQ
+;
+COLOUR_2: CALL COMMA
+ CALL EXPRI ; Parse G
+ EXX
+ LD A, L
+ LD (VDU_BUFFER+2), A
+ CALL COMMA
+ CALL EXPRI ; Parse B
+ EXX
+ LD A, L
+ LD (VDU_BUFFER+3), A
+ VDU 13h ; VDU:COLOUR
+ VDU (VDU_BUFFER+0) ; Logical Colour
+ VDU FFh ; Physical Colour (-1 for RGB mode)
+ VDU (VDU_BUFFER+1) ; R
+ VDU (VDU_BUFFER+2) ; G
+ VDU (VDU_BUFFER+3) ; B
+ JP XEQ
\ No newline at end of file
diff --git a/src/zds/agon_init.asm b/src/zds/agon_init.asm
new file mode 100644
index 0000000..88237e2
--- /dev/null
+++ b/src/zds/agon_init.asm
@@ -0,0 +1,244 @@
+;
+; Title: BBC Basic for AGON - Initialisation Code
+; Initialisation Code
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 14/12/2024
+;
+; Modinfo:
+; 14/12/2024: Fix for *BYE command
+
+ SEGMENT __VECTORS
+
+ XREF START
+ XREF ACCS
+ XREF TELL
+
+ .ASSUME ADL = 0
+
+ INCLUDE "equs.inc"
+
+argv_ptrs_max: EQU 16 ; Maximum number of arguments allowed in argv
+
+;
+; Start in mixed mode. Assumes MBASE is set to correct segment
+;
+ JP _start ; Jump to start
+ DS 5
+
+RST_08: RST.LIS 08h ; API call
+ RET
+ DS 5
+
+RST_10: RST.LIS 10h ; Output
+ RET
+ DS 5
+
+RST_18: RST.LIS 18h ; Block Output
+ RET
+ DS 5
+
+RST_20: DS 8
+RST_28: DS 8
+RST_30: DS 8
+
+;
+; The NMI interrupt vector (not currently used by AGON)
+;
+RST_38: EI
+ RETI
+;
+; The header stuff is from byte 64 onwards
+;
+ ALIGN 64
+
+ DB "MOS" ; Flag for MOS - to confirm this is a valid MOS command
+ DB 00h ; MOS header version 0
+ DB 00h ; Flag for run mode (0: Z80, 1: ADL)
+
+_exec_name: DB "BBCBASIC.BIN", 0 ; The executable name, only used in argv
+
+;
+; And the code follows on immediately after the header
+;
+_start: PUSH.LIL IY ; Preserve IY
+
+ LD IY, 0 ; Preserve SPS
+ ADD IY, SP
+ PUSH.LIL IY
+
+ EX (SP), HL ; Get the SPS part of the return address
+ PUSH.LIL HL
+ EX (SP), HL ; And restore it for BASIC
+
+ PUSH.LIL AF ; Preserve the rest of the registers
+ PUSH.LIL BC
+ PUSH.LIL DE
+ PUSH.LIL IX
+
+ LD A, MB ; Segment base
+ LD IX, argv_ptrs ; The argv array pointer address
+ CALL _set_aix24 ; Convert to a 24-bit address
+ PUSH.LIL IX
+ CALL _parse_params ; Parse the parameters
+ POP.LIL IX ; IX: argv
+ LD B, 0 ; C: argc
+ CALL _main ; Start user code
+
+ POP.LIL IX ; Restore the registers
+ POP.LIL DE
+ POP.LIL BC
+ POP.LIL AF
+
+ EX DE, HL ; DE: Return code from BASIC
+ POP.LIL HL ; The SPS part of the return address
+ POP.LIL IY ; Get the preserved SPS
+ LD SP, IY ; Restore SPS
+ EX (SP), HL ; Store the SPS part of the return address on the stack
+ EX DE, HL ; HL: Return code from BASIC
+
+ POP.LIL IY ; Restore IY
+ RET.L ; Return to MOS
+
+; The main routine
+; IXU: argv - pointer to array of parameters
+; C: argc - number of parameters
+; Returns:
+; HL: Error code, or 0 if OK
+;
+_main: LD HL, ACCS ; Clear the ACCS
+ LD (HL), 0
+ LD A, C
+ CP 2
+ JR Z, _autoload ; 2 parameters = autoload
+ JR C, _startbasic ; 1 parameter = normal start
+; CALL STAR_VERSION ; Output the AGON version
+ CALL TELL
+ DB "Usage:\n\r"
+ DB "RUN . \n\r", 0
+ LD HL, 0 ; The error code
+ RET
+;
+_autoload: LD.LIL HL, (IX+3) ; HLU: Address of filename
+ LD DE, ACCS ; DE: Destination address
+$$: LD.LIL A, (HL) ; Fetch the filename byte
+ LD (DE), A ;
+ INC.LIL HL ; Increase the source pointer
+ INC E ; We only need to increase E as ACCS is on a page boundary
+ JR NZ, $B ; Loop until we hit a 0 byte
+ DEC E
+ LD A, CR
+ LD (DE), A ; Replace the 0 byte with a CR for BBC BASIC
+;
+_startbasic: POP HL ; Pop the return address to init off SPS
+ PUSH.LIL HL ; Stack it on SPL (*BYE will use this as the return address)
+ JP START ; And start BASIC
+
+; Parse the parameter string into a C array
+; Parameters
+; - A: Segment base
+; - HLU: Address of parameter string
+; - IXU: Address for array pointer storage
+; Returns:
+; - C: Number of parameters parsed
+;
+_parse_params: LD BC, _exec_name ; Get the address of the app name in this segment
+ CALL _set_abc24 ; Convert it to a 24-bit address based upon segment base
+ LD.LIL (IX+0), BC ; ARGV[0] = the executable name
+ INC.LIL IX
+ INC.LIL IX
+ INC.LIL IX
+ CALL _skip_spaces ; Skip HL past any leading spaces
+;
+ LD BC, 1 ; C: ARGC = 1 - also clears out top 16 bits of BCU
+ LD B, argv_ptrs_max - 1 ; B: Maximum number of argv_ptrs
+;
+_parse_params_1: PUSH BC ; Stack ARGC
+ PUSH.LIL HL ; Stack start address of token
+ CALL _get_token ; Get the next token
+ LD A, C ; A: Length of the token in characters
+ POP.LIL DE ; Start address of token (was in HL)
+ POP BC ; ARGC
+ OR A ; Check for A=0 (no token found) OR at end of string
+ RET Z
+;
+ LD.LIL (IX+0), DE ; Store the pointer to the token
+ PUSH.LIL HL ; DE=HL
+ POP.LIL DE
+ CALL _skip_spaces ; And skip HL past any spaces onto the next character
+ XOR A
+ LD.LIL (DE), A ; Zero-terminate the token
+ INC.LIL IX
+ INC.LIL IX
+ INC.LIL IX ; Advance to next pointer position
+ INC C ; Increment ARGC
+ LD A, C ; Check for C >= A
+ CP B
+ JR C, _parse_params_1 ; And loop
+ RET
+
+; Get the next token
+; Parameters:
+; - HL: Address of parameter string
+; Returns:
+; - HL: Address of first character after token
+; - C: Length of token (in characters)
+;
+_get_token: LD C, 0 ; Initialise length
+$$: LD.LIL A, (HL) ; Get the character from the parameter string
+ OR A ; Exit if 0 (end of parameter string in MOS)
+ RET Z
+ CP 13 ; Exit if CR (end of parameter string in BBC BASIC)
+ RET Z
+ CP ' ' ; Exit if space (end of token)
+ RET Z
+ INC.LIL HL ; Advance to next character
+ INC C ; Increment length
+ JR $B
+
+; Skip spaces in the parameter string
+; Parameters:
+; - HL: Address of parameter string
+; Returns:
+; - HL: Address of next none-space character
+; F: Z if at end of string, otherwise NZ if there are more tokens to be parsed
+;
+_skip_spaces: LD.LIL A, (HL) ; Get the character from the parameter string
+ CP ' ' ; Exit if not space
+ RET NZ
+ INC.LIL HL ; Advance to next character
+ JR _skip_spaces ; Increment length
+
+; Set the MSB of BC (U) to A
+; Parameters:
+; - BC: 16-bit address
+; - A: Value to stick in U of BC
+; Returns:
+; - BCU
+;
+_set_abc24: PUSH.LIL HL ; Preserve HL
+ PUSH.LIL BC ; Stick BC onto SPL
+ LD.LIL HL, 2 ; HL: SP+2
+ ADD.LIL HL, SP
+ LD.LIL (HL), A ; Store A in it
+ POP.LIL BC ; Fetch ammended BC
+ POP.LIL HL ; Restore HL
+ RET
+
+; Set the MSB of BC (U) to A
+; Parameters:
+; - IX: 16-bit address
+; - A: Value to stick in U of BC
+; Returns:
+; - IXU
+;
+_set_aix24: PUSH.LIL IX ; Stick IX onto SPL
+ LD.LIL IX, 2 ; IX: SP+2
+ ADD.LIL IX, SP
+ LD.LIL (IX), A ; Store A in it
+ POP.LIL IX ; Fetch ammended IX
+ RET
+
+; Storage for the argv array pointers
+;
+argv_ptrs: BLKP argv_ptrs_max, 0 ; Storage for the argv array pointers
diff --git a/src/zds/agon_interrupt.asm b/src/zds/agon_interrupt.asm
new file mode 100644
index 0000000..fdbd37d
--- /dev/null
+++ b/src/zds/agon_interrupt.asm
@@ -0,0 +1,124 @@
+;
+; Title: BBC Basic for AGON - Interrupts
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 04/12/2024
+;
+; Modinfo:
+
+ .ASSUME ADL = 0
+
+ INCLUDE "macros.inc"
+ INCLUDE "equs.inc"
+ INCLUDE "mos_api.inc" ; In MOS/src
+
+ SEGMENT CODE
+
+ XDEF VBLANK_INIT
+ XDEF VBLANK_STOP
+ XDEF VBLANK_HANDLER
+
+ XREF ESCSET
+ XREF KEYDOWN ; In ram.asm
+ XREF KEYASCII ; In ram.asm
+ XREF KEYCOUNT ; In ram.asm
+
+; Hook into the MOS VBLANK interrupt
+;
+VBLANK_INIT: DI
+
+ LD A, MB ; Get a 24-bit pointer to
+ LD HL, VBLANK_HANDLER ; this interrupt handler routine who's
+ CALL SET_AHL16 ; address is a 16-bit pointer in BBC BASIC's segment
+
+ LD E, 32h ; Set up the VBlank Interrupt Vector
+ MOSCALL mos_setintvector
+
+ PUSH.LIL HL ; HLU: Pointer to the MOS interrupt vector
+ POP.LIL DE ; DEU: Pointer to the MOS interrupt vector
+
+ LD HL, VBLANK_HANDLER_JP + 1 ; Pointer to the JP address in this segment
+ LD A, MB ; Get the segment BBC BASIC is running in
+ LD (VBLANK_HANDLER_MB + 1), A ; Store in the interrupt handler
+ CALL SET_AHL16 ; Convert pointer to an absolute 24-bit address
+ LD.LIL (HL), DE ; Self-modify the code
+ EI
+ RET
+
+; Unhook the custom VBLANK interrupt
+;
+VBLANK_STOP: DI
+ LD HL, VBLANK_HANDLER_JP + 1 ; Pointer to the JP address in this segment
+ LD A, (VBLANK_HANDLER_MB + 1) ; The stored MB of the segment BBC BASIC is running in
+ PUSH AF ; Stack the MB for later
+ CALL SET_AHL16 ; Convert pointer to an absolute 24-bit address
+ LD.LIL DE, (HL) ; DEU: Address of MOS interrupt vector
+ PUSH.LIL DE ; Transfer to HL
+ POP.LIL HL
+ LD E, 32h
+ MOSCALL mos_setintvector ; Restore the MOS interrupt vector
+ POP AF ; Restore MB to this segment
+ LD MB, A
+ EI
+ RET
+
+; Set the MSB of HL (U) to A
+;
+SET_AHL16: PUSH.LIL HL
+ LD.LIL HL, 2
+ ADD.LIL HL, SP
+ LD.LIL (HL), A
+ POP.LIL HL
+ RET
+
+; A safe LIS call to ESCSET
+;
+DO_KEYBOARD: MOSCALL mos_sysvars ; Get the system variables
+ LD HL, KEYCOUNT ; Check whether the keycount has changed
+ LD.LIL A, (IX + sysvar_vkeycount) ; by comparing the MOS copy
+ CP (HL) ; with our local copy
+ JR NZ, DO_KEYBOARD_1 ; Yes it has, so jump to the next bit
+;
+DO_KEYBOARD_0: XOR A ; Clear the keyboard values
+ LD (KEYASCII), A
+ LD (KEYDOWN), A
+ RET.LIL ; And return
+;
+DO_KEYBOARD_1: LD (HL), A ; Store the updated local copy of keycount
+ LD.LIL A, (IX + sysvar_vkeydown) ; Fetch key down value (1 = key down, 0 = key up)
+ OR A
+ JR Z, DO_KEYBOARD_0 ; If it is key up, then clear the keyboard values
+;
+ LD (KEYDOWN), A ; Store the keydown value
+ LD.LIL A, (IX + sysvar_keyascii) ; Fetch key ASCII value
+ LD (KEYASCII), A ; Store locally
+ CP 1Bh ; Is it escape?
+ CALL Z, ESCSET ; Yes, so set the escape flags
+ RET.LIS ; Return to the interrupt handler
+
+;
+; Interrupts in mixed mode always run in ADL mode
+;
+ .ASSUME ADL = 1
+
+VBLANK_HANDLER: DI
+ PUSH AF
+ PUSH HL
+ PUSH IX
+ LD A, MB
+ PUSH AF
+VBLANK_HANDLER_MB: LD A, 0 ; This is self-modified by VBLANK_INIT
+ LD MB, A
+ CALL.LIS DO_KEYBOARD
+ POP AF
+ LD MB, A
+ POP IX
+ POP HL
+ POP AF
+;
+; Finally jump to the MOS interrupt
+;
+VBLANK_HANDLER_JP: JP 0 ; This is self-modified by VBLANK_INIT
+
+ .ASSUME ADL = 0
+
\ No newline at end of file
diff --git a/src/zds/agon_misc.asm b/src/zds/agon_misc.asm
new file mode 100644
index 0000000..ae48635
--- /dev/null
+++ b/src/zds/agon_misc.asm
@@ -0,0 +1,229 @@
+;
+; Title: BBC Basic for AGON - Miscellaneous helper functions
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 04/12/2024
+;
+; Modinfo:
+
+ INCLUDE "equs.inc"
+ INCLUDE "macros.inc"
+
+ .ASSUME ADL = 0
+
+ SEGMENT CODE
+
+ XDEF ASC_TO_NUMBER
+ XDEF SWITCH_A
+ XDEF NULLTOCR
+ XDEF CRTONULL
+ XDEF CSTR_FNAME
+ XDEF CSTR_LINE
+ XDEF CSTR_FINDCH
+ XDEF CSTR_ENDSWITH
+ XDEF CSTR_CAT
+
+ XREF OSWRCH
+ XREF KEYWDS
+ XREF KEYWDL
+
+; Read a number and convert to binary
+; If prefixed with &, will read as hex, otherwise decimal
+; Inputs: HL: Pointer in string buffer
+; Outputs: HL: Updated text pointer
+; DE: Value
+; A: Terminator (spaces skipped)
+; Destroys: A,D,E,H,L,F
+;
+ASC_TO_NUMBER: PUSH BC ; Preserve BC
+ LD DE, 0 ; Initialise DE
+ CALL SKIPSP ; Skip whitespace
+ LD A, (HL) ; Read first character
+ CP '&' ; Is it prefixed with '&' (HEX number)?
+ JR NZ, ASC_TO_NUMBER3 ; Jump to decimal parser if not
+ INC HL ; Otherwise fall through to ASC_TO_HEX
+;
+ASC_TO_NUMBER1: LD A, (HL) ; Fetch the character
+ CALL UPPRC ; Convert to uppercase
+ SUB '0' ; Normalise to 0
+ JR C, ASC_TO_NUMBER4 ; Return if < ASCII '0'
+ CP 10 ; Check if >= 10
+ JR C,ASC_TO_NUMBER2 ; No, so skip next bit
+ SUB 7 ; Adjust ASCII A-F to nibble
+ CP 16 ; Check for > F
+ JR NC, ASC_TO_NUMBER4 ; Return if out of range
+ASC_TO_NUMBER2: EX DE, HL ; Shift DE left 4 times
+ ADD HL, HL
+ ADD HL, HL
+ ADD HL, HL
+ ADD HL, HL
+ EX DE, HL
+ OR E ; OR the new digit in to the least significant nibble
+ LD E, A
+ INC HL ; Onto the next character
+ JR ASC_TO_NUMBER1 ; And loop
+;
+ASC_TO_NUMBER3: LD A, (HL)
+ SUB '0' ; Normalise to 0
+ JR C, ASC_TO_NUMBER4 ; Return if < ASCII '0'
+ CP 10 ; Check if >= 10
+ JR NC, ASC_TO_NUMBER4 ; Return if >= 10
+ EX DE, HL ; Stick DE in HL
+ LD B, H ; And copy HL into BC
+ LD C, L
+ ADD HL, HL ; x 2
+ ADD HL, HL ; x 4
+ ADD HL, BC ; x 5
+ ADD HL, HL ; x 10
+ EX DE, HL
+ ADD8U_DE ; Add A to DE (macro)
+ INC HL
+ JR ASC_TO_NUMBER3
+ASC_TO_NUMBER4: POP BC ; Fall through to SKIPSP here
+
+; Skip a space
+; HL: Pointer in string buffer
+;
+SKIPSP: LD A, (HL)
+ CP ' '
+ RET NZ
+ INC HL
+ JR SKIPSP
+
+; Skip a string
+; HL: Pointer in string buffer
+;
+SKIPNOTSP: LD A, (HL)
+ CP ' '
+ RET Z
+ INC HL
+ JR SKIPNOTSP
+
+; Convert a character to upper case
+; A: Character to convert
+;
+UPPRC: AND 7FH
+ CP '`'
+ RET C
+ AND 5FH ; Convert to upper case
+ RET
+
+; Switch on A - lookup table immediately after call
+; A: Index into lookup table
+;
+SWITCH_A: EX (SP), HL ; Swap HL with the contents of the top of the stack
+ ADD A, A ; Multiply A by two
+ ADD8U_HL ; Add to HL (macro)
+ LD A, (HL) ; follow the call. Fetch an address from the
+ INC HL ; table.
+ LD H, (HL)
+ LD L, A
+ EX (SP), HL ; Swap this new address back, restores HL
+ RET ; Return program control to this new address
+
+; Convert the buffer to a null terminated string and back
+; HL: Buffer address
+;
+NULLTOCR: PUSH BC
+ LD B, 0
+ LD C, CR
+ JR CRTONULL0
+;
+CRTONULL: PUSH BC
+ LD B, CR
+ LD C, 0
+;
+CRTONULL0: PUSH HL
+CRTONULL1: LD A, (HL)
+ CP B
+ JR Z, CRTONULL2
+ INC HL
+ JR CRTONULL1
+CRTONULL2: LD (HL), C
+ POP HL
+ POP BC
+ RET
+
+; Copy a filename to DE and zero terminate it
+; HL: Source
+; DE: Destination (ACCS)
+;
+CSTR_FNAME: LD A, (HL) ; Get source
+ CP 32 ; Is it space
+ JR Z, $F
+ CP CR ; Or is it CR
+ JR Z, $F
+ LD (DE), A ; No, so store
+ INC HL ; Increment
+ INC DE
+ JR CSTR_FNAME ; And loop
+$$: XOR A ; Zero terminate the target string
+ LD (DE), A
+ INC DE ; And point to next free address
+ RET
+
+; Copy a CR terminated line to DE and zero terminate it
+; HL: Source
+; DE: Destination (ACCS)
+;
+CSTR_LINE: LD A, (HL) ; Get source
+ CP CR ; Is it CR
+ JR Z, $F
+ LD (DE), A ; No, so store
+ INC HL ; Increment
+ INC DE
+ JR CSTR_LINE ; And loop
+$$: XOR A ; Zero terminate the target string
+ LD (DE), A
+ INC DE ; And point to next free address
+ RET
+
+; Find the first occurrence of a character (case sensitive)
+; HL: Source
+; C: Character to find
+; Returns:
+; HL: Pointer to character, or end of string marker
+;
+CSTR_FINDCH: LD A, (HL) ; Get source
+ CP C ; Is it our character?
+ RET Z ; Yes, so exit
+ OR A ; Is it the end of string?
+ RET Z ; Yes, so exit
+ INC HL
+ JR CSTR_FINDCH
+
+; Check whether a string ends with another string (case insensitive)
+; HL: Source
+; DE: The substring we want to test with
+; Returns:
+; F: Z if HL ends with DE, otherwise NZ
+;
+CSTR_ENDSWITH: LD A, (HL) ; Get the source string byte
+ CALL UPPRC ; Convert to upper case
+ LD C, A
+ LD A, (DE) ; Get the substring byte
+ CP C
+ RET NZ ; Return NZ if at any point the strings don't match
+ OR C ; Check whether both bytes are zero
+ RET Z ; If so, return, as we have reached the end of both strings
+ INC HL
+ INC DE
+ JR CSTR_ENDSWITH ; And loop
+
+; Concatenate a string onto the end of another string
+; HL: Source
+; DE: Second string
+;
+CSTR_CAT: LD A, (HL) ; Loop until we find the end of the first string
+ OR A
+ JR Z, CSTR_CAT_1
+ INC HL
+ JR CSTR_CAT
+;
+CSTR_CAT_1: LD A, (DE) ; Copy the second string onto the end of the first string
+ LD (HL), A
+ OR A ; Check for end of string
+ RET Z ; And return
+ INC HL
+ INC DE
+ JR CSTR_CAT_1 ; Loop until finished
\ No newline at end of file
diff --git a/src/zds/agon_os.asm b/src/zds/agon_os.asm
new file mode 100644
index 0000000..11cc877
--- /dev/null
+++ b/src/zds/agon_os.asm
@@ -0,0 +1,1084 @@
+;
+; Title: BBC Basic for AGON - MOS stuff
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 17/12/2024
+;
+; Modinfo:
+; 08/12/2024: Added OSCLI and file I/O
+; 11/12/2024: Added ESC key handling
+; Added OSWORD
+; 12/12/2024: Added OSRDCH, OSBYTE_81 and fixed *EDIT
+; 17/12/2024: Added OSWORD_01, OSWORD_02, OSWORD_0E, GET$(x,y), fixed INKEY, POS, VPOS and autoload
+
+ .ASSUME ADL = 0
+
+ INCLUDE "equs.inc"
+ INCLUDE "macros.inc"
+ INCLUDE "mos_api.inc" ; In MOS/src
+
+ SEGMENT CODE
+
+ XDEF OSWORD
+ XDEF OSBYTE
+ XDEF OSINIT
+ XDEF OSOPEN
+ XDEF OSSHUT
+ XDEF OSLOAD
+ XDEF OSSAVE
+ XDEF OSLINE
+ XDEF OSSTAT
+ XDEF OSWRCH
+ XDEF OSRDCH
+ XDEF OSBGET
+ XDEF OSBPUT
+ XDEF OSCLI
+ XDEF PROMPT
+ XDEF GETPTR
+ XDEF PUTPTR
+ XDEF GETEXT
+ XDEF TRAP
+ XDEF LTRAP
+ XDEF BYE
+ XDEF RESET
+ XDEF ESCSET
+
+ XREF EXTERR
+ XREF VBLANK_INIT
+ XREF VBLANK_STOP
+ XREF USER
+ XREF COUNT
+ XREF COUNT0
+ XREF COUNT1
+ XREF GETCSR
+ XREF GETSCHR_1
+ XREF NULLTOCR
+ XREF CRLF
+ XREF FLAGS
+ XREF OSWRCHPT
+ XREF OSWRCHCH
+ XREF OSWRCHFH
+ XREF KEYASCII
+ XREF KEYDOWN
+ XREF LISTON
+ XREF PAGE_
+ XREF CSTR_FNAME
+ XREF CSTR_FINDCH
+ XREF CSTR_CAT
+ XREF CSTR_ENDSWITH
+ XREF CSTR_LINE
+ XREF NEWIT
+ XREF BAD
+ XREF CLEAN
+ XREF LINNUM
+ XREF BUFFER
+ XREF NXT
+ XREF ERROR_
+ XREF XEQ
+ XREF LEXAN2
+ XREF GETTOP
+ XREF FINDL
+ XREF DEL
+ XREF LISTIT
+ XREF ESCAPE
+ XREF ASC_TO_NUMBER
+ XREF CLOOP
+ XREF SCRAP
+ XREF POINT_
+ XREF SOUND_
+ XREF EXPRI
+ XREF COMMA
+ XREF BRAKET
+ XREF GETSCHR
+ XREF ZERO
+ XREF TRUE
+
+;OSINIT - Initialise RAM mapping etc.
+;If BASIC is entered by BBCBASIC FILENAME then file
+;FILENAME.BBC is automatically CHAINed.
+; Outputs: DE = initial value of HIMEM (top of RAM)
+; HL = initial value of PAGE (user program)
+; Z-flag reset indicates AUTO-RUN.
+; Destroys: A,D,E,H,L,F
+;
+OSINIT: CALL VBLANK_INIT
+ XOR A
+ LD (FLAGS), A ; Clear flags and set F = Z
+ LD HL, USER
+ LD DE, RAM_Top
+ LD E, A ; Page boundary
+ LD A, (ACCS) ; Return NZ if there is a file to chain
+ OR A
+ RET
+
+; PROMPT: output the input prompt
+;
+PROMPT: LD A,'>' ; Falls through to OSWRCH
+
+; OSWRCH: Write a character out to the ESP32 VDU handler via the MOS
+; Parameters:
+; - A: Character to write
+;
+OSWRCH: PUSH HL
+ LD HL, LISTON ; Fetch the LISTON variable
+ BIT 3, (HL) ; Check whether we are in *EDIT mode
+ JR NZ, OSWRCH_BUFFER ; Yes, so just output to buffer
+;
+ LD HL, (OSWRCHCH) ; L: Channel #
+ DEC L ; If it is 1
+ JR Z, OSWRCH_FILE ; Then we are outputting to a file
+;
+ POP HL ; Otherwise
+ RST.LIS 10h ; Output the character to MOS
+ RET
+;
+OSWRCH_BUFFER: LD HL, (OSWRCHPT) ; Fetch the pointer buffer
+ CP 0AH ; Just ignore this
+ JR Z, OSWRCH_BUFFER2
+ CP 0DH ; Is it the end of line?
+ JR NZ, OSWRCH_BUFFER1 ; No, so carry on
+ XOR A ; Turn it into a NUL character
+OSWRCH_BUFFER1: LD (HL), A ; Echo the character into the buffer
+ INC HL ; Increment pointer
+ LD (OSWRCHPT), HL ; Write pointer back
+OSWRCH_BUFFER2: POP HL
+ RET
+;
+OSWRCH_FILE: PUSH DE
+ LD E, H ; Filehandle to E
+ CALL OSBPUT ; Write the byte out
+ POP DE
+ POP HL
+ RET
+
+; OSRDCH
+;
+OSRDCH: CALL NXT ; Check if we are doing GET$(x,y)
+ CP '('
+ JR Z, $F ; Yes, so skip to that functionality
+ MOSCALL mos_getkey ; Otherwise, read keyboard
+ CP 1Bh
+ JR Z, LTRAP1
+ RET
+;
+$$: INC IY ; Skip '('
+ CALL EXPRI ; Get the first parameter
+ EXX
+ PUSH HL
+ CALL COMMA ; Get the second parameter
+ CALL EXPRI
+ EXX
+ POP DE ; DE: X coordinate
+ CALL BRAKET ; Check for trailing bracket
+ JP GETSCHR ; Read the character
+
+; OSLINE: Invoke the line editor
+;
+OSLINE: LD E, 1 ; Default is to clear the buffer
+
+; Entry point to line editor that does not clear the buffer
+; Parameters:
+; - HL: addresses destination buffer (on page boundary)
+; Returns:
+; - A: 0
+; NB: Buffer filled, terminated by CR
+;
+OSLINE1: PUSH IY
+ PUSH HL ; Buffer address
+ LD BC, 256 ; Buffer length
+ MOSCALL mos_editline ; Call the MOS line editor
+ POP HL ; Pop the address
+ POP IY
+ PUSH AF ; Stack the return value (key pressed)
+ CALL NULLTOCR ; Turn the 0 character to a CR
+ CALL CRLF ; Display CRLF
+ POP AF
+ CP 1Bh ; Check if ESC terminated the input
+ JP Z, LTRAP1 ; Yes, so do the ESC thing
+ LD A, (FLAGS) ; Otherwise
+ RES 7, A ; Clear the escape flag
+ LD (FLAGS), A
+ CALL WAIT_VBLANK ; Wait a frame
+ XOR A ; Return A = 0
+ LD (KEYDOWN), A
+ LD (KEYASCII), A
+ RET
+
+;
+; ESCSET
+; Set the escape flag (bit 7 of FLAGS = 1) if escape is enabled (bit 6 of FLAGS = 0)
+;
+ESCSET: PUSH HL
+ LD HL,FLAGS ; Pointer to FLAGS
+ BIT 6,(HL) ; If bit 6 is set, then
+ JR NZ,ESCDIS ; escape is disabled, so skip
+ SET 7,(HL) ; Set bit 7, the escape flag
+ESCDIS: POP HL
+ RET
+
+;
+; ESCTEST
+; Test for ESC key
+;
+ESCTEST: CALL READKEY ; Read the keyboard
+ RET NZ ; Skip if no key is pressed
+ CP 1BH ; If ESC pressed then
+ JR Z,ESCSET ; jump to the escape set routine
+ RET
+
+; Read the keyboard
+; Returns:
+; - A: ASCII of the pressed key
+; - F: Z if the key is pressed, otherwise NZ
+;
+READKEY: LD A, (KEYDOWN) ; Get key down
+ DEC A ; Set Z flag if keydown is 1
+ LD A, (KEYASCII) ; Get key ASCII value
+ RET
+;
+; TRAP
+; This is called whenever BASIC needs to check for ESC
+;
+TRAP: CALL ESCTEST ; Read keyboard, test for ESC, set FLAGS
+;
+LTRAP: LD A,(FLAGS) ; Get FLAGS
+ OR A ; This checks for bit 7; if it is not set then the result will
+ RET P ; be positive (bit 7 is the sign bit in Z80), so return
+LTRAP1: LD HL,FLAGS ; Escape is pressed at this point, so
+ RES 7,(HL) ; Clear the escape pressed flag and
+ JP ESCAPE ; Jump to the ESCAPE error routine in exec.asm
+
+; RESET
+;
+RESET: RET ; Yes this is fine
+
+; OSOPEN
+; HL: Pointer to path
+; F: C Z
+; x x OPENIN
+; OPENOUT
+; x OPENUP
+; Returns:
+; A: Filehandle, 0 if cannot open
+;
+OSOPEN: LD C, fa_read
+ JR Z, $F
+ LD C, fa_write | fa_open_append
+ JR C, $F
+ LD C, fa_write | fa_create_always
+$$: MOSCALL mos_fopen
+ RET
+
+;OSSHUT - Close disk file(s).
+; E = file channel
+; If E=0 all files are closed (except SPOOL)
+; Destroys: A,B,C,D,E,H,L,F
+;
+OSSHUT: PUSH BC
+ LD C, E
+ MOSCALL mos_fclose
+ POP BC
+ RET
+
+; OSBGET - Read a byte from a random disk file.
+; E = file channel
+; Returns
+; A = byte read
+; Carry set if LAST BYTE of file
+; Destroys: A,B,C,F
+;
+OSBGET: PUSH BC
+ LD C, E
+ MOSCALL mos_fgetc
+ POP BC
+ RET
+
+; OSBPUT - Write a byte to a random disk file.
+; E = file channel
+; A = byte to write
+; Destroys: A,B,C,F
+;
+OSBPUT: PUSH BC
+ LD C, E
+ LD B, A
+ MOSCALL mos_fputc
+ POP BC
+ RET
+
+; OSSTAT - Read file status
+; E = file channel
+; Returns
+; F: Z flag set - EOF
+; A: If Z then A = 0
+; Destroys: A,D,E,H,L,F
+;
+OSSTAT: PUSH BC
+ LD C, E
+ MOSCALL mos_feof
+ POP BC
+ CP 1
+ RET
+
+; GETPTR - Return file pointer.
+; E = file channel
+; Returns:
+; DEHL = pointer (0-&7FFFFF)
+; Destroys: A,B,C,D,E,H,L,F
+;
+GETPTR: PUSH IY
+ LD C, E
+ MOSCALL mos_getfil ; HLU: Pointer to FIL structure
+ PUSH.LIL HL
+ POP.LIL IY ; IYU: Pointer to FIL structure
+ LD.LIL L, (IY + FIL.fptr + 0)
+ LD.LIL H, (IY + FIL.fptr + 1)
+ LD.LIL E, (IY + FIL.fptr + 2)
+ LD.LIL D, (IY + FIL.fptr + 3)
+ POP IY
+ RET
+
+; PUTPTR - Update file pointer.
+; A = file channel
+; DEHL = new pointer (0-&7FFFFF)
+; Destroys: A,B,C,D,E,H,L,F
+;
+PUTPTR: PUSH IY
+ LD C, A ; C: Filehandle
+ PUSH.LIL HL
+ LD.LIL HL, 2
+ ADD.LIL HL, SP
+ LD.LIL (HL), E ; 3rd byte of DWORD set to E
+ POP.LIL HL
+ LD E, D ; 4th byte passed as E
+ MOSCALL mos_flseek
+ POP IY
+ RET
+
+; GETEXT - Find file size.
+; E = file channel
+; Returns:
+; DEHL = file size (0-&800000)
+; Destroys: A,B,C,D,E,H,L,F
+;
+GETEXT: PUSH IY
+ LD C, E
+ MOSCALL mos_getfil ; HLU: Pointer to FIL structure
+ PUSH.LIL HL
+ POP.LIL IY ; IYU: Pointer to FIL structure
+ LD.LIL L, (IY + FIL.obj.objsize + 0)
+ LD.LIL H, (IY + FIL.obj.objsize + 1)
+ LD.LIL E, (IY + FIL.obj.objsize + 2)
+ LD.LIL D, (IY + FIL.obj.objsize + 3)
+ POP IY
+ RET
+
+;OSLOAD - Load an area of memory from a file.
+; Inputs: HL addresses filename (CR terminated)
+; DE = address at which to load
+; BC = maximum allowed size (bytes)
+; Outputs: Carry reset indicates no room for file.
+; Destroys: A,B,C,D,E,H,L,F
+;
+OSLOAD: PUSH BC ; Stack the size
+ PUSH DE ; Stack the load address
+ LD DE, ACCS ; Buffer address for filename
+ CALL CSTR_FNAME ; Fetch filename from MOS into buffer
+ LD HL, ACCS ; HL: Filename
+ CALL EXT_DEFAULT ; Tack on the extension .BBC if not specified
+ CALL EXT_HANDLER ; Get the default handler
+ POP DE ; Restore the load address
+ POP BC ; Restore the size
+ OR A
+ JP Z, OSLOAD_BBC
+;
+; Load the file in as a text file
+;
+OSLOAD_TXT: XOR A ; Set file attributes to read
+ CALL OSOPEN ; Open the file
+ LD E, A ; The filehandle
+ OR A
+ LD A, 4 ; File not found error
+ JP Z, OSERROR ; Jump to error handler
+ CALL NEWIT ; Call NEW to clear the program space
+;
+OSLOAD_TXT1: LD HL, ACCS ; Where the input is going to be stored
+;
+; First skip any whitespace (indents) at the beginning of the input
+;
+$$: CALL OSBGET ; Read the byte into A
+ JR C, OSLOAD_TXT3 ; Is it EOF?
+ CP LF ; Is it LF?
+ JR Z, OSLOAD_TXT3 ; Yes, so skip to the next line
+ CP 21h ; Is it less than or equal to ASCII space?
+ JR C, $B ; Yes, so keep looping
+ LD (HL), A ; Store the first character
+ INC L
+;
+; Now read the rest of the line in
+;
+OSLOAD_TXT2: CALL OSBGET ; Read the byte into A
+ JR C, OSLOAD_TXT4 ; Is it EOF?
+ CP 20h ; Skip if not an ASCII character
+ JR C, $F
+ LD (HL), A ; Store in the input buffer
+ INC L ; Increment the buffer pointer
+ JP Z, BAD ; If the buffer is full (wrapped to 0) then jump to Bad Program error
+$$: CP LF ; Check for LF
+ JR NZ, OSLOAD_TXT2 ; If not, then loop to read the rest of the characters in
+;
+; Finally, handle EOL/EOF
+;
+OSLOAD_TXT3: LD (HL), CR ; Store a CR for BBC BASIC
+ LD A, L ; Check for minimum line length
+ CP 2 ; If it is 2 characters or less (including CR)
+ JR C, $F ; Then don't bother entering it
+ PUSH DE ; Preserve the filehandle
+ CALL OSEDIT ; Enter the line in memory
+ CALL C,CLEAN ; If a new line has been entered, then call CLEAN to set TOP and write &FFFF end of program marker
+ POP DE
+$$: CALL OSSTAT ; End of file?
+ JR NZ, OSLOAD_TXT1 ; No, so loop
+ CALL OSSHUT ; Close the file
+ SCF ; Flag to BASIC that we're good
+ RET
+;
+; Special case for BASIC programs with no blank line at the end
+;
+OSLOAD_TXT4: CP 20h ; Skip if not an ASCII character
+ JR C, $F
+ LD (HL), A ; Store the character
+ INC L
+ JP Z, BAD
+$$: JR OSLOAD_TXT3
+;
+; This bit enters the line into memory
+; Also called from OSLOAD_TXT
+; Returns:
+; F: C if a new line has been entered (CLEAN will need to be called)
+;
+OSEDIT: XOR A ; Entry point after *EDIT
+ LD (COUNT),A
+ LD IY,ACCS
+ CALL LINNUM ; HL: The line number from the input buffer
+ CALL NXT ; Skip spaces
+ LD A,H ; HL: The line number will be 0 for immediate mode or when auto line numbering is used
+ OR L
+ JR Z,LNZERO ; Skip if there is no line number in the input buffer
+;
+; This bit does the lexical analysis and tokenisation
+;
+LNZERO: LD DE,BUFFER
+ LD C,1 ; LEFT MODE
+ PUSH HL
+ CALL LEXAN2 ; LEXICAL ANALYSIS
+ POP HL
+ LD (DE),A ; TERMINATOR
+ XOR A
+ LD B,A
+ LD C,E ; BC=LINE LENGTH
+ INC DE
+ LD (DE),A ; ZERO NEXT
+ LD A,H
+ OR L
+ LD IY,BUFFER ; FOR XEQ
+ JP Z,XEQ ; DIRECT MODE
+ PUSH BC
+ CALL FINDL
+ CALL Z,DEL
+ POP BC
+ LD A,C
+ OR A
+ RET Z
+ ADD A,4
+ LD C,A ; LENGTH INCLUSIVE
+ PUSH DE ; LINE NUMBER
+ PUSH BC ; SAVE LINE LENGTH
+ EX DE,HL
+ PUSH BC
+ CALL GETTOP
+ POP BC
+ PUSH HL
+ ADD HL,BC
+ PUSH HL
+ INC H
+ XOR A
+ SBC HL,SP
+ POP HL
+ JP NC,ERROR_ ; "No room"
+ EX (SP),HL
+ PUSH HL
+ INC HL
+ OR A
+ SBC HL,DE
+ LD B,H ; BC=AMOUNT TO MOVE
+ LD C,L
+ POP HL
+ POP DE
+ JR Z,ATEND
+ LDDR ; MAKE SPACE
+ATEND: POP BC ; LINE LENGTH
+ POP DE ; LINE NUMBER
+ INC HL
+ LD (HL),C ; STORE LENGTH
+ INC HL
+ LD (HL),E ; STORE LINE NUMBER
+ INC HL
+ LD (HL),D
+ INC HL
+ LD DE,BUFFER
+ EX DE,HL
+ DEC C
+ DEC C
+ DEC C
+ LDIR ; ADD LINE
+ SCF
+ RET
+;
+; Load the file in as a tokenised binary blob
+;
+OSLOAD_BBC: MOSCALL mos_load ; Call LOAD in MOS
+ RET NC ; If load returns with carry reset - NO ROOM
+ OR A ; If there is no error (A=0)
+ SCF ; Need to set carry indicating there was room
+ RET Z ; Return
+;
+OSERROR: PUSH AF ; Handle the MOS error
+ LD HL, ACCS ; Address of the buffer
+ LD BC, 256 ; Length of the buffer
+ LD E, A ; The error code
+ MOSCALL mos_getError ; Copy the error message into the buffer
+ POP AF
+ PUSH HL ; Stack the address of the error (now in ACCS)
+ ADD A, 127 ; Add 127 to the error code (MOS errors start at 128, and are trappable)
+ JP EXTERR ; Trigger an external error
+
+;OSSAVE - Save an area of memory to a file.
+; Inputs: HL addresses filename (term CR)
+; DE = start address of data to save
+; BC = length of data to save (bytes)
+; Destroys: A,B,C,D,E,H,L,F
+;
+OSSAVE: PUSH BC ; Stack the size
+ PUSH DE ; Stack the save address
+ LD DE, ACCS ; Buffer address for filename
+ CALL CSTR_FNAME ; Fetch filename from MOS into buffer
+ LD HL, ACCS ; HL: Filename
+ CALL EXT_DEFAULT ; Tack on the extension .BBC if not specified
+ CALL EXT_HANDLER ; Get the default handler
+ POP DE ; Restore the save address
+ POP BC ; Restore the size
+ OR A ; Is the extension .BBC
+ JR Z, OSSAVE_BBC ; Yes, so use that
+;
+; Save the file out as a text file
+;
+OSSAVE_TXT: LD A, (OSWRCHCH) ; Stack the current channel
+ PUSH AF
+ XOR A
+ INC A ; Make sure C is clear, A is 1, for OPENOUT
+ LD (OSWRCHCH), A
+ CALL OSOPEN ; Open the file
+ LD (OSWRCHFH), A ; Store the file handle for OSWRCH
+ LD IX, LISTON ; Required for LISTIT
+ LD HL, (PAGE_) ; Get start of program area
+ EXX
+ LD BC, 0 ; Set the initial indent counters
+ EXX
+OSSAVE_TXT1: LD A, (HL) ; Check for end of program marker
+ OR A
+ JR Z, OSSAVE_TXT2
+ INC HL ; Skip the length byte
+ LD E, (HL) ; Get the line number
+ INC HL
+ LD D, (HL)
+ INC HL
+ CALL LISTIT ; List the line
+ JR OSSAVE_TXT1
+OSSAVE_TXT2: LD A, (OSWRCHFH) ; Get the file handle
+ LD E, A
+ CALL OSSHUT ; Close it
+ POP AF ; Restore the channel
+ LD (OSWRCHCH), A
+ RET
+;
+; Save the file out as a tokenised binary blob
+;
+OSSAVE_BBC: MOSCALL mos_save ; Call SAVE in MOS
+ OR A ; If there is no error (A=0)
+ RET Z ; Just return
+ JR OSERROR ; Trip an error
+
+; Check if an extension is specified in the filename
+; Add a default if not specified
+; HL: Filename (CSTR format)
+;
+EXT_DEFAULT: PUSH HL ; Stack the filename pointer
+ LD C, '.' ; Search for dot (marks start of extension)
+ CALL CSTR_FINDCH
+ OR A ; Check for end of string marker
+ JR NZ, $F ; No, so skip as we have an extension at this point
+ LD DE, EXT_LOOKUP ; Get the first (default extension)
+ CALL CSTR_CAT ; Concat it to string pointed to by HL
+$$: POP HL ; Restore the filename pointer
+ RET
+
+; Check if an extension is valid and, if so, provide a pointer to a handler
+; HL: Filename (CSTR format)
+; Returns:
+; A: Filename extension type (0=BBC tokenised, 1=ASCII untokenised)
+;
+EXT_HANDLER: PUSH HL ; Stack the filename pointer
+ LD C, '.' ; Find the '.'
+ CALL CSTR_FINDCH
+ LD DE, EXT_LOOKUP ; The lookup table
+;
+EXT_HANDLER_1: PUSH HL ; Stack the pointer to the extension
+ CALL CSTR_ENDSWITH ; Check whether the string ends with the entry in the lookup
+ POP HL ; Restore the pointer to the extension
+ JR Z, EXT_HANDLER_2 ; We have a match!
+;
+$$: LD A, (DE) ; Skip to the end of the entry in the lookup
+ INC DE
+ OR A
+ JR NZ, $B
+ INC DE ; Skip the file extension # byte
+;
+ LD A, (DE) ; Are we at the end of the table?
+ OR A
+ JR NZ, EXT_HANDLER_1 ; No, so loop
+;
+ LD A,204 ; Throw a "Bad name" error
+ CALL EXTERR
+ DB "Bad name", 0
+;
+EXT_HANDLER_2: INC DE ; Skip to the file extension # byte
+ LD A, (DE)
+ POP HL ; Restore the filename pointer
+ RET
+
+; Extension lookup table
+; CSTR, TYPE
+; - 0: BBC (tokenised BBC BASIC for Z80 format)
+; - 1: Human readable plain text
+;
+EXT_LOOKUP: DB '.BBC', 0, 0 ; First entry is the default extension
+ DB '.TXT', 0, 1
+ DB '.ASC', 0, 1
+ DB '.BAS', 0, 1
+ DB 0 ; End of table
+; OSWORD
+;
+OSWORD: CP 01H ; GETIME
+ JR Z, OSWORD_01
+ CP 02H ; PUTIME
+ JR Z, OSWORD_02
+ CP 0EH ; GETIMS
+ JR Z, OSWORD_0E
+ CP 0FH ; PUTIMS
+ JR Z, $F
+ CP 07H ; SOUND
+ JR Z, OSWORD_07
+ CP 08H ; ENVELOPE
+ JR Z, $F
+ CP 09H ; POINT
+ JR Z, OSWORD_09
+ JP HUH ; Anything else trips an error
+$$: RET ; Dummy return for unimplemented functions
+
+; GETIME: return current time in centiseconds
+;
+OSWORD_01: PUSH IX
+ MOSCALL mos_sysvars
+ LD B, 4
+$$: LD.LIL A, (IX + sysvar_time)
+ LD (HL), A
+ INC HL
+ INC.LIL IX
+ DJNZ $B
+ POP IX
+ RET
+
+; PUTIME: set time in centiseconds
+;
+OSWORD_02: PUSH IX
+ MOSCALL mos_sysvars
+ LD B, 4
+$$: LD A, (HL)
+ LD.LIL (IX + sysvar_time), A
+ INC HL
+ INC.LIL IX
+ DJNZ $B
+ POP IX
+ RET
+
+; SOUND channel,volume,pitch,duration
+; Parameters:
+; - HL: Pointer to data
+; - 0,1: Channel
+; - 2,3: Volume 0 (off) to 15 (full volume)
+; - 4,5: Pitch 0 - 255
+; - 6,7: Duration -1 to 254 (duration in 20ths of a second, -1 = play forever)
+;
+OSWORD_07: EQU SOUND_
+
+; OSWORD 0x09: POINT
+; Parameters:
+; - HL: Address of data
+; - 0,1: X coordinate
+; - 2,3: Y coordinate
+;
+OSWORD_09: LD DE,(SCRAP+0)
+ LD HL,(SCRAP+2)
+ CALL POINT_
+ LD (SCRAP+4),A
+ RET
+
+; GETIMS - Get time from RTC
+;
+OSWORD_0E: PUSH IY
+ MOSCALL mos_getrtc
+ POP IY
+ RET
+
+;
+; OSBYTE
+; Parameters:
+; - A: FX #
+; - L: First parameter
+; - H: Second parameter
+;
+OSBYTE: CP 0BH ; Keyboard auto-repeat delay
+ JR Z, OSBYTE_0B
+ CP 0CH ; Keyboard auto-repeat rate
+ JR Z, OSBYTE_0C
+ CP 13H ; Wait for vblank
+ JR Z, OSBYTE_13
+ CP 76H ; Set keyboard LED
+ JR Z, OSBYTE_76
+ CP 81H ; Read the keyboard
+ JP Z, OSBYTE_81
+ CP 86H ; Get cursor coordinates
+ JP Z, OSBYTE_86
+ CP 87H ; Fetch current mode and character under cursor
+ JP Z, OSBYTE_87
+ CP A0H ; Fetch system variable
+ JP Z, OSBYTE_A0
+;
+; Anything else trips an error
+;
+HUH: LD A,254 ; Bad command error
+ CALL EXTERR
+ DB "Bad command"
+ DEFB 0
+
+; OSBYTE 0x0B (FX 11,n): Keyboard auto-repeat delay
+; Parameters:
+; - HL: Repeat delay
+;
+OSBYTE_0B: VDU 23
+ VDU 0
+ VDU vdp_keystate
+ VDU L
+ VDU H
+ VDU 0
+ VDU 0
+ VDU 255
+ RET
+
+; OSBYTE 0x0C (FX 12,n): Keyboard auto-repeat rate
+; Parameters:
+; - HL: Repeat rate
+;
+OSBYTE_0C: VDU 23
+ VDU 0
+ VDU vdp_keystate
+ VDU 0
+ VDU 0
+ VDU L
+ VDU H
+ VDU 255
+ RET
+
+; OSBYTE 0x13 (FX 19): Wait for vertical blank interrupt
+;
+OSBYTE_13: CALL WAIT_VBLANK
+ LD L, 0 ; Returns 0
+ JP COUNT0
+;
+; OSBYTE 0x76 (FX 118,n): Set Keyboard LED
+; Parameters:
+; - L: LED (Bit 0: Scroll Lock, Bit 1: Caps Lock, Bit 2: Num Lock)
+;
+OSBYTE_76: VDU 23
+ VDU 0
+ VDU vdp_keystate
+ VDU 0
+ VDU 0
+ VDU 0
+ VDU 0
+ VDU L
+ RET
+
+; OSBYTE 0x81: Read the keyboard
+; Parameters:
+; - HL = Time to wait (centiseconds)
+; Returns:
+; - F: Carry reset indicates time-out
+; - H: NZ if timed out
+; - L: The character typed
+; Destroys: A,D,E,H,L,F
+;
+OSBYTE_81: EXX
+ BIT 7, H ; Check for minus numbers
+ EXX
+ JR NZ, OSBYTE_81_1 ; Yes, so do INKEY(-n)
+ CALL READKEY ; Read the keyboard
+ JR Z, $F ; Skip if we have a key
+ CALL WAIT_VBLANK ; Wait a frame
+ LD A, H ; Check loop counter
+ OR L
+ DEC HL ; Decrement
+ JR NZ, OSBYTE_81 ; And loop
+ RET ; H: Will be set to 255 to flag timeout
+;
+$$: LD HL, KEYDOWN ; We have a key, so
+ LD (HL), 0 ; clear the keydown flag
+ CP 1BH ; If we are pressing ESC,
+ JP Z, ESCSET ; Then handle ESC
+ LD H, 0 ; H: Not timed out
+ LD L, A ; L: The character
+ RET
+;
+;
+; Check immediately whether a given key is being pressed
+; Result is integer numeric
+;
+OSBYTE_81_1: MOSCALL mos_getkbmap ; Get the base address of the keyboard
+ INC HL ; Index from 0
+ LD A, L ; Negate the LSB of the answer
+ NEG
+ LD C, A ; E: The positive keycode value
+ LD A, 1 ; Throw an "Out of range" error
+ JP M, ERROR_ ; if the argument < - 128
+;
+ LD HL, BITLOOKUP ; HL: The bit lookup table
+ LD DE, 0
+ LD A, C
+ AND 00000111b ; Just need the first three bits
+ LD E, A ; DE: The bit number
+ ADD HL, DE
+ LD B, (HL) ; B: The mask
+;
+ LD A, C ; Fetch the keycode again
+ AND 01111000b ; And divide by 8
+ RRCA
+ RRCA
+ RRCA
+ LD E, A ; DE: The offset (the MSW has already been cleared previously)
+ ADD.LIL IX, DE ; IX: The address
+ LD.LIL A, (IX+0) ; A: The keypress
+ AND B ; Check whether the bit is set
+ JP Z, ZERO ; No, so return 0
+ JP TRUE ; Otherwise return -1
+;
+; A bit lookup table
+;
+BITLOOKUP: DB 01h, 02h, 04h, 08h
+ DB 10h, 20h, 40h, 80h
+
+; OSBYTE 0x86: Fetch cursor coordinates
+; Returns:
+; - L: X Coordinate (POS)
+; - H: Y Coordinate (VPOS)
+;
+OSBYTE_86: PUSH IX ; Get the system vars in IX
+ MOSCALL mos_sysvars ; Reset the semaphore
+ RES.LIL 0, (IX+sysvar_vpd_pflags)
+ VDU 23
+ VDU 0
+ VDU vdp_cursor
+$$: BIT.LIL 0, (IX+sysvar_vpd_pflags)
+ JR Z, $B ; Wait for the result
+ LD.LIL L, (IX + sysvar_cursorX)
+ LD.LIL H, (IX + sysvar_cursorY)
+ POP IX
+ RET
+
+; OSBYTE 0x87: Fetch current mode and character under cursor
+;
+OSBYTE_87: PUSH IX
+ CALL GETCSR ; Get the current screen position
+ CALL GETSCHR ; Read character from screen
+ LD L, A
+ MOSCALL mos_sysvars
+ LD.LIL H, (IX+sysvar_scrMode) ; H: Screen mode
+ POP IX
+ JP COUNT1
+
+; OSBYTE 0xA0: Fetch system variable
+; Parameters:
+; - L: The system variable to fetch
+;
+OSBYTE_A0: PUSH IX
+ MOSCALL mos_sysvars ; Fetch pointer to system variables
+ LD.LIL BC, 0
+ LD C, L ; BCU = L
+ ADD.LIL IX, BC ; Add to IX
+ LD.LIL L, (IX + 0) ; Fetch the return value
+ POP IX
+ JP COUNT0
+
+; OSCLI
+;
+;
+;OSCLI - Process a MOS command
+;
+OSCLI: CALL SKIPSP
+ CP CR
+ RET Z
+ CP '|'
+ RET Z
+ EX DE,HL
+ LD HL,COMDS
+OSCLI0: LD A,(DE)
+ CALL UPPRC
+ CP (HL)
+ JR Z,OSCLI2
+ JR C,OSCLI6
+OSCLI1: BIT 7,(HL)
+ INC HL
+ JR Z,OSCLI1
+ INC HL
+ INC HL
+ JR OSCLI0
+;
+OSCLI2: PUSH DE
+OSCLI3: INC DE
+ INC HL
+ LD A,(DE)
+ CALL UPPRC
+ CP '.' ; ABBREVIATED?
+ JR Z,OSCLI4
+ XOR (HL)
+ JR Z,OSCLI3
+ CP 80H
+ JR Z,OSCLI4
+ POP DE
+ JR OSCLI1
+;
+OSCLI4: POP AF
+ INC DE
+OSCLI5: BIT 7,(HL)
+ INC HL
+ JR Z,OSCLI5
+ LD A,(HL)
+ INC HL
+ LD H,(HL)
+ LD L,A
+ PUSH HL
+ EX DE,HL
+ JP SKIPSP
+;
+OSCLI6: EX DE, HL ; HL: Buffer for command
+ LD DE, ACCS ; Buffer for command string is ACCS (the string accumulator)
+ PUSH DE ; Store buffer address
+ CALL CSTR_LINE ; Fetch the line
+ POP HL ; HL: Pointer to command string in ACCS
+ PUSH IY
+ MOSCALL mos_oscli ; Returns OSCLI error in A
+ POP IY
+ OR A ; 0 means MOS returned OK
+ RET Z ; So don't do anything
+ JP OSERROR ; Otherwise it's a MOS error
+
+SKIPSP: LD A,(HL)
+ CP ' '
+ RET NZ
+ INC HL
+ JR SKIPSP
+
+UPPRC: AND 7FH
+ CP '`'
+ RET C
+ AND 5FH ; CONVERT TO UPPER CASE
+ RET
+
+; Each command has bit 7 of the last character set, and is followed by the address of the handler
+; These must be in alphabetical order
+;
+COMDS: DB 'BY','E'+80h ; BYE
+ DW BYE
+ DB 'EDI','T'+80h ; EDIT
+ DW STAR_EDIT
+ DB 'F','X'+80h ; FX
+ DW STAR_FX
+; DB 'VERSIO','N'+80h ; VERSION
+; DW STAR_VERSION
+ DB FFh
+
+; *BYE
+;
+BYE: CALL VBLANK_STOP ; Restore MOS interrupts
+ POP.LIL IX ; The return address to init
+ LD HL, 0 ; The return code
+ JP (IX)
+
+; *EDIT linenum
+;
+STAR_EDIT: CALL ASC_TO_NUMBER ; DE: Line number to edit
+ EX DE, HL ; HL: Line number
+ CALL FINDL ; HL: Address in RAM of tokenised line
+ LD A, 41 ; F:NZ If the line is not found
+ JP NZ, ERROR_ ; Do error 41: No such line in that case
+;
+; Use LISTIT to output the line to the ACCS buffer
+;
+ INC HL ; Skip the length byte
+ LD E, (HL) ; Fetch the line number
+ INC HL
+ LD D, (HL)
+ INC HL
+ LD IX, ACCS ; Pointer to where the copy is to be stored
+ LD (OSWRCHPT), IX
+ LD IX, LISTON ; Pointer to LISTON variable in RAM
+ LD A, (IX) ; Store that variable
+ PUSH AF
+ LD (IX), 09h ; Set to echo to buffer
+ CALL LISTIT
+ POP AF
+ LD (IX), A ; Restore the original LISTON variable
+ LD HL, ACCS ; HL: ACCS
+ LD E, L ; E: 0 - Don't clear the buffer; ACCS is on a page boundary so L is 0
+ CALL OSLINE1 ; Invoke the editor
+ CALL OSEDIT
+ CALL C,CLEAN ; Set TOP, write out &FFFF end of program marker
+ JP CLOOP ; Jump back to immediate mode
+
+; OSCLI FX n
+;
+STAR_FX: CALL ASC_TO_NUMBER
+ LD C, E ; C: Save FX #
+ CALL ASC_TO_NUMBER
+ LD A, D ; Is first parameter > 255?
+ OR A
+ JR Z, STAR_FX1 ; Yes, so skip next bit
+ EX DE, HL ; Parameter is 16-bit
+ JR STAR_FX2
+;
+STAR_FX1: LD B, E ; B: Save First parameter
+ CALL ASC_TO_NUMBER ; Fetch second parameter
+ LD L, B ; L: First parameter
+ LD H, E ; H: Second parameter
+;
+STAR_FX2: LD A, C ; A: FX #
+ JP OSBYTE
+
+; Helper Functions
+;
+WAIT_VBLANK: PUSH IX ; Wait for VBLANK interrupt
+ MOSCALL mos_sysvars ; Fetch pointer to system variables
+ LD.LIL A, (IX + sysvar_time + 0)
+$$: CP.LIL A, (IX + sysvar_time + 0)
+ JR Z, $B
+ POP IX
+ RET
\ No newline at end of file
diff --git a/src/zds/agon_sound.asm b/src/zds/agon_sound.asm
new file mode 100644
index 0000000..3e26ef0
--- /dev/null
+++ b/src/zds/agon_sound.asm
@@ -0,0 +1,155 @@
+;
+; Title: BBC Basic for AGON - Audio stuff
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 11/12/2024
+;
+; Modinfo:
+; 11/12/2024: Modified SOUND_ to work with OSWORD
+
+ .ASSUME ADL = 0
+
+ INCLUDE "equs.inc"
+ INCLUDE "macros.inc"
+ INCLUDE "mos_api.inc" ; In MOS/src
+
+ SEGMENT CODE
+
+ XDEF SOUND_
+
+ XREF OSWRCH
+ XREF VDU_BUFFER
+ XREF LTRAP
+
+; SOUND channel,volume,pitch,duration
+; Parameters:
+; - HL: Pointer to data
+; - 0,1: Channel
+; - 2,3: Volume 0 (off) to 15 (full volume)
+; - 4,5: Pitch 0 - 255
+; - 6,7: Duration -1 to 254 (duration in 20ths of a second, -1 = play forever)
+;
+SOUND_: LD A, (HL) ; Channel
+ LD (VDU_BUFFER+0), A
+ XOR A ; Waveform
+ LD (VDU_BUFFER+1), A
+ INC HL
+ INC HL
+;
+; Calculate the volume
+;
+ LD C, (HL) ; Volume
+ LD B, 6 ; C already contains the volume
+ MLT BC ; Multiply by 6 (0-15 scales to 0-90)
+ LD A, C
+ LD (VDU_BUFFER+2), A
+ INC HL
+ INC HL
+;
+; And the frequency
+;
+ PUSH HL
+ LD L, (HL)
+ LD H, 0
+ LD DE, SOUND_FREQ_LOOKUP
+ ADD HL, HL
+ ADD HL, DE
+ LD A, (HL)
+ LD (VDU_BUFFER+3), A
+ INC HL
+ LD A, (HL)
+ LD (VDU_BUFFER+4), A
+ POP HL
+ INC HL
+ INC HL
+;
+; And now the duration - multiply it by 50 to convert from 1/20ths of seconds to milliseconds
+;
+ LD C, (HL)
+ LD B, 50 ; C contains the duration, so MLT by 50
+ MLT BC
+ LD (VDU_BUFFER+5), BC
+;
+ PUSH IX ; Get the system vars in IX
+ MOSCALL mos_sysvars ; Reset the semaphore
+SOUND0: RES.LIL 3, (IX+sysvar_vpd_pflags)
+;
+ VDU 23 ; Send the sound command
+ VDU 0
+ VDU vdp_audio
+ VDU (VDU_BUFFER+0) ; 0: Channel
+ VDU (VDU_BUFFER+1) ; 1: Waveform (0)
+ VDU (VDU_BUFFER+2) ; 2: Volume (0-100)
+ VDU (VDU_BUFFER+3) ; 3: Frequency L
+ VDU (VDU_BUFFER+4) ; 4: Frequency H
+ VDU (VDU_BUFFER+5) ; 5: Duration L
+ VDU (VDU_BUFFER+6) ; 6: Duration H
+;
+; Wait for acknowledgement
+;
+$$: BIT.LIL 3, (IX+sysvar_vpd_pflags)
+ JR Z, $B ; Wait for the result
+ CALL LTRAP ; Check for ESC
+ LD.LIL A, (IX+sysvar_audioSuccess)
+ AND A ; Check if VDP has queued the note
+ JR Z, SOUND0 ; No, so loop back and send again
+;
+ POP IX
+ RET
+
+; Frequency Lookup Table
+; Set up to replicate the BBC Micro audio frequencies
+;
+; Split over 5 complete octaves, with 53 being middle C
+; * C4: 262hz
+; + A4: 440hz
+;
+; 2 3 4 5 6 7 8
+;
+; B 1 49 97 145 193 241
+; A# 0 45 93 141 189 237
+; A 41 89+ 137 185 233
+; G# 37 85 133 181 229
+; G 33 81 129 177 225
+; F# 29 77 125 173 221
+; F 25 73 121 169 217
+; E 21 69 117 165 213
+; D# 17 65 113 161 209
+; D 13 61 109 157 205 253
+; C# 9 57 105 153 201 249
+; C 5 53* 101 149 197 245
+;
+SOUND_FREQ_LOOKUP: DW 117, 118, 120, 122, 123, 131, 133, 135
+ DW 137, 139, 141, 143, 145, 147, 149, 151
+ DW 153, 156, 158, 160, 162, 165, 167, 170
+ DW 172, 175, 177, 180, 182, 185, 188, 190
+ DW 193, 196, 199, 202, 205, 208, 211, 214
+ DW 217, 220, 223, 226, 230, 233, 236, 240
+ DW 243, 247, 251, 254, 258, 262, 265, 269
+ DW 273, 277, 281, 285, 289, 294, 298, 302
+ DW 307, 311, 316, 320, 325, 330, 334, 339
+ DW 344, 349, 354, 359, 365, 370, 375, 381
+ DW 386, 392, 398, 403, 409, 415, 421, 427
+ DW 434, 440, 446, 453, 459, 466, 473, 480
+ DW 487, 494, 501, 508, 516, 523, 531, 539
+ DW 546, 554, 562, 571, 579, 587, 596, 605
+ DW 613, 622, 631, 641, 650, 659, 669, 679
+ DW 689, 699, 709, 719, 729, 740, 751, 762
+ DW 773, 784, 795, 807, 819, 831, 843, 855
+ DW 867, 880, 893, 906, 919, 932, 946, 960
+ DW 974, 988, 1002, 1017, 1032, 1047, 1062, 1078
+ DW 1093, 1109, 1125, 1142, 1158, 1175, 1192, 1210
+ DW 1227, 1245, 1263, 1282, 1300, 1319, 1338, 1358
+ DW 1378, 1398, 1418, 1439, 1459, 1481, 1502, 1524
+ DW 1546, 1569, 1592, 1615, 1638, 1662, 1686, 1711
+ DW 1736, 1761, 1786, 1812, 1839, 1866, 1893, 1920
+ DW 1948, 1976, 2005, 2034, 2064, 2093, 2123, 2154
+ DW 2186, 2217, 2250, 2282, 2316, 2349, 2383, 2418
+ DW 2453, 2489, 2525, 2562, 2599, 2637, 2675, 2714
+ DW 2754, 2794, 2834, 2876, 2918, 2960, 3003, 3047
+ DW 3091, 3136, 3182, 3228, 3275, 3322, 3371, 3420
+ DW 3470, 3520, 3571, 3623, 3676, 3729, 3784, 3839
+ DW 3894, 3951, 4009, 4067, 4126, 4186, 4247, 4309
+ DW 4371, 4435, 4499, 4565, 4631, 4699, 4767, 4836
+
+
diff --git a/src/zds/eZ80F92_AGON_Flash.ztgt b/src/zds/eZ80F92_AGON_Flash.ztgt
new file mode 100644
index 0000000..47720be
--- /dev/null
+++ b/src/zds/eZ80F92_AGON_Flash.ztgt
@@ -0,0 +1,62 @@
+
+
+
+ Oscillator
+ 18432000
+
+
+ 000000
+ C0000
+ FFFF
+ true
+
+
+
+ 0
+ false
+ 40000
+ BFFFF
+
+ 1
+ false
+ true
+
+
+
+
+ 1
+ 8
+ 2
+ 9
+
+
+ 02
+ 28
+ C0
+ C7
+
+
+ 81
+ 28
+ 80
+ BF
+
+
+ 02
+ 00
+ 00
+ 00
+
+
+
+ 0
+ ff
+ true
+ true
+
+ 1
+
+ eZ80F92
+ 1.0.1
+ 1.00
+
diff --git a/src/zds/equs.inc b/src/zds/equs.inc
new file mode 100644
index 0000000..27d3ada
--- /dev/null
+++ b/src/zds/equs.inc
@@ -0,0 +1,56 @@
+;
+; Title: BBC Basic for AGON - Equs
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 05/12/2024
+;
+; Modinfo:
+; 05/12/2024: Removed Stack_Top
+
+ XREF STAVAR
+ XREF ACCS
+
+RAM_Top: EQU 0FF00h
+
+; For GPIO
+; PA not available on eZ80L92
+;
+PA_DR: EQU 96h
+PA_DDR: EQU 97h
+PA_ALT1: EQU 98h
+PA_ALT2: EQU 99h
+PB_DR: EQU 9Ah
+PB_DDR: EQU 9Bh
+PB_ALT1: EQU 9Ch
+PB_ALT2: EQU 9Dh
+PC_DR: EQU 9Eh
+PC_DDR: EQU 9Fh
+PC_ALT1: EQU A0h
+PC_ALT2: EQU A1h
+PD_DR: EQU A2h
+PD_DDR: EQU A3h
+PD_ALT1: EQU A4h
+PD_ALT2: EQU A5h
+
+GPIOMODE_OUT: EQU 0 ; Output
+GPIOMODE_IN: EQU 1 ; Input
+GPIOMODE_DIO: EQU 2 ; Open Drain IO
+GPIOMODE_SIO: EQU 3 ; Open Source IO
+GPIOMODE_INTD: EQU 4 ; Interrupt, Dual Edge
+GPIOMODE_ALTF: EQU 5; ; Alt Function
+GPIOMODE_INTAL: EQU 6 ; Interrupt, Active Low
+GPIOMODE_INTAH: EQU 7 ; Interrupt, Active High
+GPIOMODE_INTFE: EQU 8 ; Interrupt, Falling Edge
+GPIOMODE_INTRE: EQU 9 ; Interrupt, Rising Edge
+
+; Originally in ram.asm
+;
+OC: EQU STAVAR+15*4 ; CODE ORIGIN (O%)
+PC: EQU STAVAR+16*4 ; PROGRAM COUNTER (P%)
+VDU_BUFFER: EQU ACCS ; Storage for VDU commands
+
+; Originally in main.asm
+;
+CR: EQU 0DH
+LF: EQU 0AH
+ESC: EQU 1BH
diff --git a/src/zds/macros.inc b/src/zds/macros.inc
new file mode 100644
index 0000000..5a9b0fe
--- /dev/null
+++ b/src/zds/macros.inc
@@ -0,0 +1,52 @@
+;
+; Title: BBC Basic Interpreter - Z80 version
+; Useful macros
+; Author: Dean Belfield
+; Created: 04/12/2024
+; Last Updated: 04/12/2024
+;
+; Modinfo:
+
+EXREG: MACRO rp1, rp2
+ PUSH rp1
+ POP rp2
+ ENDMACRO
+
+ADD8U_DE: MACRO reg
+ ADD A, E
+ LD E, A
+ ADC A, D
+ SUB E
+ LD D, A
+ ENDMACRO
+
+ADD8U_HL: MACRO reg
+ ADD A, L
+ LD L, A
+ ADC A, H
+ SUB L
+ LD H, A
+ ENDMACRO
+
+VDU: MACRO VAL
+ LD A, VAL
+ CALL OSWRCH
+ ENDMACRO
+
+SET_GPIO: MACRO REG, VAL
+ IN0 A,(REG)
+ OR VAL
+ OUT0 (REG),A
+ ENDMACRO
+
+RES_GPIO: MACRO REG, VAL
+ PUSH BC
+ LD A, VAL
+ CPL
+ LD C, A
+ IN0 A,(REG)
+ AND C
+ OUT0 (REG),A
+ POP BC
+ ENDMACRO
+
\ No newline at end of file
diff --git a/tools/transform_source.py b/tools/transform_source.py
new file mode 100644
index 0000000..24e4e11
--- /dev/null
+++ b/tools/transform_source.py
@@ -0,0 +1,500 @@
+# Title: Z80 Source Transformer
+# Author: Dean Belfield
+# Created: 15/08/2024
+# Last Updated: 17/12/2024
+# Description: Convert Z80 assembler to work on various assemblers
+#
+# Modinfo:
+# 01/12/2024: Improved the label parsing state machine
+# 04/12/2024: Exported files for ZDS now assemble
+# 06/12/2024: Added directives in hints and tweaked hints data
+# 08/12/2024: Tweaked for OSCLI and file I/O
+# 17/12/2024: Tweaked for v1.00-RC2
+
+import sys
+import os
+import datetime
+import re
+
+# Global stuff
+#
+now = datetime.datetime.now()
+registers = ["A", "B", "C", "D", "E", "H", "L", "IXL", "IXH", "IYL", "IYH", "AF", "BC", "DE", "HL", "IX", "IY", "SP", "PC"]
+reservedWords = {
+ "zds": ["AND", "OR", "MOD", "DIV", "IF", "ADDR", "COND", "CPL", "ERROR", "EVAL", "INT", "PAGE", "STRING", "TEXT", "VAR"],
+ "sjasmplus": []
+}
+
+# Match a single regex
+#
+def matchOne(regex, statement):
+ l = re.search(regex, statement)
+ if(l and len(l.groups()) > 0):
+ return l.group(1)
+ return None
+
+# Get a label from the statement
+#
+def getLabel(statement):
+ if(statement == None):
+ return None
+ m = matchOne(r"^(?:CALL|JR|JP)+\s+(?:C|NC|Z|NZ|M|P|PE|PO)+\s*,+\s*([a-zA-Z]+\w*)+", statement)
+ if(m):
+ return m
+ m = matchOne(r"^(?:DEFW|GLOBAL|EXTRN|CALL|CP|DJNZ|JR|JP|RST)+\s+([a-zA-Z]+\w*)+", statement)
+ if(m):
+ return m
+ m = matchOne(r"^(?:LD|IN)+\s+(?:A|B|C|D|E|H|L|BC|DE|HL|IX|IY|SP)\s*,+\s*\(*([a-zA-Z]+\w*)+\)*", statement)
+ if(m):
+ return m
+ return matchOne(r"^(?:LD|OUT)+\s+\(+([a-zA-Z]+\w*)+\)+\s*,+\s*(?:A|B|C|D|E|H|L|BC|DE|HL|IX|IY|SP)+", statement)
+
+# Replace a string only if it is at the start of the string
+#
+def replaceInstruction(string, find, replace):
+ if(string.startswith(find)):
+ return string.replace(find, replace, 1)
+ return string
+
+# Replace a string on a word boundary provided it is not at the start
+#
+def replaceOperator(string, find, replace):
+ if(not string.startswith(find) and not f"'{find}'" in string):
+ return re.sub(r"\b" + find + r"\b", replace, string)
+ return string
+
+# Represents a single line of source
+# Members:
+# - line: The original line
+# - label: The label (if or )
+#
+class Line:
+ def __init__(self, line):
+ self.statement = None
+ self.label = None
+ self.comment = None
+ self.statementLabel = None
+
+ # Split the line into its component parts
+ #
+ state = 0
+ for c in line.rstrip():
+
+ if(state == 0): # Check if beginning of line is a label or not
+ if(c == ";"): # It is a comment?
+ self.comment = c
+ state = 4 # Go to the comment read state
+ elif(c.isspace()): # Is it a space?
+ state = 2 # Go to the read statement state
+ else:
+ self.label = c
+ state = 1 # It's a label, so go to the read label state
+
+ elif(state == 1): # Read a label in
+ if(c == ":" or c.isspace()): # Is it the end of the label?
+ state = 2 # Yes, so go to the read statement state
+ else:
+ self.label += c
+
+ elif(state == 2): # Read the statement in - first skip whitespace
+ if(not c.isspace()):
+ self.statement = c
+ state = 3
+
+ elif(state == 3): # Read the rest of the statement in
+ self.statement += c
+
+ elif(state == 4): # Read the comment in
+ self.comment += c
+
+ # Now get the statement label
+ #
+ label = getLabel(self.statement)
+ if(label != None):
+ if(self.statement.startswith("GLOBAL") or self.statement.startswith("EXTRN") or not label.upper() in registers):
+ self.statementLabel = label
+
+ # Do some line level refactoring
+ #
+ def refactor(self, target, indent, xdef):
+ if(self.label and self.label in reservedWords[target]):
+ self.label+="_"
+
+ if(self.statementLabel and self.statementLabel in reservedWords[target]):
+ self.statement = replaceOperator(self.statement, self.statementLabel, self.statementLabel + "_")
+
+ if(target == "zds"):
+ if(self.statement):
+ self.statement = replaceInstruction(self.statement, "EXTRN", "XREF")
+ self.statement = replaceInstruction(self.statement, "GLOBAL", "XDEF")
+ self.statement = replaceInstruction(self.statement, "DEFS", "DS")
+ self.statement = replaceInstruction(self.statement, "DEFW", "DW")
+ self.statement = replaceInstruction(self.statement, "DEFB", "DB")
+ self.statement = replaceInstruction(self.statement, "DEFM", "DB")
+ self.statement = replaceOperator(self.statement, "AND", "&")
+ self.statement = replaceOperator(self.statement, "OR", "|")
+ #
+ # TODO: This is a bit of a bodge, needs improving
+ # Replace escaped apostrophes ('') in the middle of strings
+ #
+ aposCount = self.statement.count("'")
+ if(aposCount > 2 and aposCount%2 == 0):
+ self.statement = self.statement.replace("''", "'", 1)
+
+ elif(target == "sjasmplus"):
+ if(self.statement):
+ if(self.statement.startswith("EXTRN") or self.statement.startswith("GLOBAL")):
+ self.comment = f";\t{self.statement}"
+ self.statement = None
+ else:
+ if(self.label and self.label in xdef):
+ self.label = f"@{self.label}"
+
+ if(self.label == None and self.statement == None):
+ return f"{self.comment or ''}"
+ else:
+ if(self.label):
+ return f"{(self.label + ':').ljust(indent)}{self.statement or ''}\t{self.comment or ''}"
+ else:
+ return f"{''.ljust(indent)}{self.statement}\t{self.comment or ''}"
+
+# The source file class
+# Parameters:
+# - filename: The filename of the source file
+#
+class Source:
+ def __init__(self, filename):
+ self.filename = filename
+ self.module = os.path.splitext(os.path.basename(filename))[0]
+ self.lines = []
+ self.xdef = set()
+ self.xref = set()
+ self.target = None
+ self.indent = None
+ self.hints = {}
+
+ # Set the target
+ # - target: zds or sjasmplus
+ #
+ def setTarget(self, target):
+ self.target = target
+
+ # Set the indent
+ # - indent: number of spaces to pad labels out to
+ #
+ def setIndent(self, indent):
+ self.indent = indent
+
+ # Set source hints
+ # - hints: Dictionary of manual fixes
+ #
+ def setHints(self, hints):
+ self.hints = hints
+
+ # Add a comment
+ # - comment: the comment to add (must be prefixed with ';')
+ #
+ def insertLine(self, comment):
+ self.lines.append(Line(comment))
+
+ # Open the file for reading
+ #
+ def open(self):
+ full_path = os.path.expanduser(self.filename)
+ self.file = open(full_path, "r")
+
+ # Read and process the file
+ # - ignoreFirstLine: set to true to ignore the first line
+ #
+ def read(self, ignoreFirstLine):
+ insert = not ignoreFirstLine
+ while(True):
+ line = self.file.readline()
+ if(not line):
+ break
+ if(insert):
+ self.lines.append(Line(line))
+ insert = True
+
+ # Close the file for reading
+ #
+ def close(self):
+ self.file.close()
+
+ # Do any source level refactoring
+ #
+ def refactor(self):
+ output = []
+
+ # Add the generic autogeneration comment
+ #
+ output.append(Line(f";"))
+ output.append(Line(f";Automatically created from original source on {now.strftime('%Y-%m-%d %H:%M:%S')}"))
+ output.append(Line(f";"))
+
+ if(self.target == "sjasmplus"):
+ output.append(Line(f"\tMODULE {self.module}"))
+ elif(self.target == "zds"):
+ output.append(Line(f"\t.ASSUME ADL = 0"))
+
+ # Source specific directives
+ #
+ if("directives" in self.hints[self.target]):
+ for item in self.hints[self.target]["directives"]:
+ output.append(Line(item))
+
+ # Build up the xref and xdef lists
+ #
+ for line in self.lines:
+ if(line.statement):
+ #
+ # xref labels are referenced in this module and are external
+ #
+ if(line.statement.startswith("EXTRN")): self.xref.add(line.statementLabel)
+ #
+ # xdef labels are exported from this module and referenced elsewhere
+ #
+ if(line.statement.startswith("GLOBAL")): self.xdef.add(line.statementLabel)
+ #
+ # Source specific hints
+ #
+ if(self.target in self.hints):
+ if("hints" in self.hints[self.target]):
+ for item in self.hints[self.target]["hints"]:
+ if(item["hint"] in line.statement):
+ if("prepend" in item):
+ for p in item["prepend"]:
+ output.append(Line(p))
+ if("update" in item):
+ line = Line(item["update"])
+
+ output.append(line)
+
+ # Add the module directives for sjasmplus
+ #
+ if(self.target == "sjasmplus"):
+ output.append(Line(f"\tENDMODULE"))
+
+ self.lines = output[:]
+
+ # Export the source
+ #
+ def export(self):
+ filename = os.path.basename(self.filename)
+ if(self.target == "zds"):
+ filename = filename.replace(".Z80", ".ASM").lower()
+ dirname = os.path.join(os.path.dirname(self.filename), self.target)
+ if(not os.path.exists(dirname)):
+ os.makedirs(dirname)
+ file = open(os.path.join(dirname, filename), "w")
+ for line in self.lines:
+ output = line.refactor(self.target, self.indent, self.xdef)
+ if(output):
+ file.write(f"{output}\n")
+ file.close()
+
+# The project class
+#
+class Project:
+ def __init__(self):
+ self.filenames = []
+ self.ignoreFirstLine = False
+ self.source = []
+ self.indent = 8
+ self.hints = {}
+
+ # Set the array of filenames to import
+ # - filenames: array of paths to filenames
+ #
+ def setFilenames(self, filenames):
+ self.filenames = filenames
+
+ # Ignore the first line of the source code
+ # - ignoreFirstLine: set to true to ignore
+ #
+ def setIgnoreFirstLine(self, ignoreFirstLine):
+ self.ignoreFirstLine = ignoreFirstLine
+
+ # Set the target
+ # - target: zds or sjasmplus
+ #
+ def setTarget(self, target):
+ if(target not in ["sjasmplus", "zds"]):
+ raise Exception(f"Invalid target {target}")
+ self.target = target
+ print(f"Set target to {target}")
+
+ # Set the indent
+ # - indent: number of spaces to pad labels out to
+ #
+ def setIndent(self, indent):
+ self.indent = indent
+
+ # Set source hints
+ # - hints: Dictionary of manual fixes
+ #
+ def setHints(self, hints):
+ self.hints = hints
+
+ # Parse the project
+ #
+ def parse(self):
+ for filename in self.filenames:
+ print(f"Loading {filename}")
+ s = Source(filename)
+ s.setTarget(self.target)
+ s.setIndent(self.indent)
+ if(filename in self.hints):
+ s.setHints(self.hints[filename])
+ s.open()
+ s.read(self.ignoreFirstLine)
+ s.close()
+ s.refactor()
+ self.source.append(s)
+
+ # Export the project
+ #
+ def export(self):
+ for s in self.source:
+ # Get list of labels in all the other sources that reference this - so this can export those labels
+ #
+ s.export()
+
+# Start here
+#
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+project = Project()
+project.setIgnoreFirstLine(True)
+project.setTarget("zds")
+project.setIndent(16)
+project.setFilenames([
+ "../src/ACORN.Z80",
+ "../src/ASMB.Z80",
+ "../src/DATA.Z80",
+ "../src/EVAL.Z80",
+ "../src/EXEC.Z80",
+ "../src/MAIN.Z80",
+ "../src/MATH.Z80",
+])
+project.setHints({
+ "../src/ACORN.Z80": {
+ "zds": {
+ "directives": [ "\tSEGMENT CODE" ],
+ "hints": [
+ {
+ "hint": "EQU\t0FFEEH",
+ "update": "\tXREF\tOSWRCH"
+ },
+ {
+ "hint": "EQU\t0FFF1H",
+ "update": "\tXREF\tOSWORD"
+ },
+ {
+ "hint": "EQU\t0FFF4H",
+ "update": "\tXREF\tOSBYTE"
+ }
+ ]
+ }
+ },
+ "../src/ASMB.Z80": {
+ "zds": {
+ "directives": [ "\tSEGMENT CODE" ],
+ }
+ },
+ "../src/DATA.Z80": {
+ "zds": {
+ "directives": [
+ "\tDEFINE LORAM, SPACE = ROM",
+ "\tSEGMENT LORAM",
+ ";",
+ "\tXDEF\tFLAGS",
+ "\tXDEF\tOSWRCHPT",
+ "\tXDEF\tOSWRCHCH",
+ "\tXDEF\tOSWRCHFH",
+ "\tXDEF\tKEYDOWN",
+ "\tXDEF\tKEYASCII",
+ "\tXDEF\tKEYCOUNT",
+ "\tXDEF\tSCRAP",
+ "\tXDEF\tBUFFER",
+ "\tXDEF\tLISTON",
+ "\tXDEF\tPAGE_",
+ ";",
+ "FLAGS:\tDS\t1",
+ "OSWRCHPT:\tDS\t2",
+ "OSWRCHCH:\tDS\t1",
+ "OSWRCHFH:\tDS\t1",
+ "KEYDOWN:\tDS\t1",
+ "KEYASCII:\tDS\t1",
+ "KEYCOUNT:\tDS\t1",
+ "SCRAP:\tDS\t31",
+ ";",
+ "\tALIGN 256"
+ ],
+ }
+ },
+ "../src/EVAL.Z80": {
+ "zds": {
+ "directives": [
+ "\tSEGMENT CODE",
+ ";",
+ "\tXDEF\tCOUNT0",
+ "\tXDEF\tCOUNT1",
+ "\tXDEF\tZERO",
+ "\tXDEF\tTRUE"
+ ],
+ "hints": [
+ {
+ "hint": "FUNTOK+($-FUNTBL)/2",
+ "prepend": [
+ "FUNTBL_END:\tEQU\t$"
+ ],
+ "update": "TCMD:\tEQU\tFUNTOK+(FUNTBL_END-FUNTBL)/2"
+ }
+ ]
+ }
+ },
+ "../src/EXEC.Z80": {
+ "zds": {
+ "directives": [ "\tSEGMENT CODE" ],
+ "hints": [
+ {
+ "hint": "TCMD-128+($-CMDTAB)/2",
+ "prepend": [
+ "CMDTAB_END:\tEQU\t$"
+ ],
+ "update": "TLAST:\tEQU\tTCMD-128+(CMDTAB_END-CMDTAB)/2"
+ }
+ ]
+ }
+ },
+ "../src/MAIN.Z80": {
+ "zds": {
+ "directives": [
+ "\tSEGMENT CODE",
+ ";",
+ "\tXDEF\tNEWIT",
+ "\tXDEF\tBAD",
+ "\tXDEF\tCLEAN",
+ "\tXDEF\tLINNUM",
+ "\tXDEF\tERROR_",
+ "\tXDEF\tGETTOP",
+ "\tXDEF\tDEL",
+ "\tXDEF\tLISTIT"
+ ],
+ "hints": [
+ {
+ "hint": "\'Can\'\'t match \'",
+ "update": "\tDB\t\"Can\'t match \""
+ }
+ ]
+ }
+ },
+ "../src/MATH.Z80": {
+ "zds": {
+ "directives": [ "\tSEGMENT CODE" ],
+ }
+ }
+})
+project.parse()
+project.export()
\ No newline at end of file