diff --git a/src/Script.s b/src/Script.s new file mode 100644 index 0000000..e54820d --- /dev/null +++ b/src/Script.s @@ -0,0 +1,148 @@ +; A simple scripting engine +; +; The scripting engine is driven by the GTE Timers, which are +; driven by the VBL Tick count. +; +; The scripting engine is composed of a sequence of simple commands, defined as follows +; +; COMMAND ARG1 ARG2 ARG3 +; +; The COMMAND word has its bits defined as: +; +; bit 15 = 1 if the end of a sequece +; bit 14 = 0 proceed to next action, 1 jump +; bit 13 = 0 (Reserved) +; bit 12 = 0 (Reserved) +; bit 11 - 8 = signed jump displacement +; bit 8 - 0 = command number +; +; The defined commands are +; +; COMMANDS ARG1 ARG2 ARG3 +; ----------------------------------------------------- +; $0002 SET_PALETTE_ENTRY ADDR COLOR ---- : Set the palette entry at ARG1 to the color in ARG2 +; $0004 SWAP_PALETTE_ENTRY ADDR1 ADDR2 ---- : Swap the palette entries in ADDR1 <-> ADDR2 +; $0010 CALLBACK LONG_ADDR PARAM : Call a user-defined function (JSL) with a parameter value in accumulator + +; Start a new script +; +; A = low word of script command array +; X = high word of script command array +; Y = number of ticks between each command step +StartScript ENT + phb + phk + plb + + phx ; Save the script array address + pha + + lda #_DoScriptStep ; Try to create a timer for this script + ldx #^_DoScriptStep + clc + jsl AddTimer + bcs :err ; No timer slots available :( + + tax ; Initialize the UeerData with the command array and PC + pla + sta Timers+8,x + pla + sta Timers+10,x + stz Timers+12,x ; Index of the commands + + plb + rtl + +:err + pla ; Pop the values and return with the carry flag set + pla + plb + rtl + +_DoScriptStep + lda Timers+8,x + lda Timers+10,x + ldy Timers+12,y + lda [CmdList],y + pha ; save the full command word + + and #$001E + tax + jsr (:commands,x) + + pla ; restore the command word + bit #$4000 ; If the branch bit is set, change the command pointer + beq :no_jump + +:no_jump + bit #$8000 ; If the terminate bit is set, remove this handler + beq :no_term + txa + jsl RemoveTimer +:no_term + rtl + +:commands dw _Null,_SetPalEntry,_SwapPalEntry,_Null,_Null,_Null,_Null,_Null + dw _UserCallback,_Null,_Null,_Null,_Null,_Null,_Null,_Null + +ARG1 equ 2 +ARG2 equ 4 +ARG3 equ 6 + +; Implementation of the built-in commands +_Null rts + +_SetPalEntry + txy + ldx: ARG1,y + lda: ARG2,y + stal SHR_PALETTES,x + rts + +_SwapPalEntry txy + + ldx ARG1,y ; Load palette values + ldal SHR_PALETTES,x + pha + ldx ARG2,y + ldal SHR_PALETTES,x + + ldx ARG1,y ; and swap + stal SHR_PALETTES,x + + ldx ARG2,y + pla + stal SHR_PALETTES,x + rts + +_UserCallback lda ARG1,x + sta :dispatch+1 + lda ARG1+1,x + sta :dispatch+2 + lda ARG3,x +:dispatch jsl $000000 + rts + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Timer.s b/src/Timer.s new file mode 100644 index 0000000..544e03c --- /dev/null +++ b/src/Timer.s @@ -0,0 +1,237 @@ +; Timer implementation +; +; The engire provides four timer slot that can be used by one-shot or +; recurring timers. Each timer is given an initial tick count, a +; reset tick count (0 = one-shot), and an action to perform. +; +; The timers handle overflow, so if a recurring timer has a tick count of 3 +; and 7 VBL ticks have passed, then the timer will be fired twice and +; a tick count of 2 will be set. +; +; As such, the timers are appropriate to drive physical and other game +; behaviors at a frame-independent rate. +; +; A collection of 4 timers that are triggered when their countdown +; goes below zero. Each timer takes up 16 bytes +; +; A timer can fire multiple times during a singular evaluation. For example, if the +; timer delay is set to 1 and 3 VBL ticks happen, then the timer delta is -2, will fire, +; have the delay added and get -1, fire again, increment to zero, first again and then +; finally reset to 1. +; +; +0 counter decremented by the number of ticks since last run +; +2 reset copied into counter when triggered. 0 turns off the timer. +; +4 addr long address of timer routine +; +8 user 8 bytes of user data space for timer state +MAX_TIMERS equ 4 +TIMER_REC_SIZE equ 16 + +lastTick ds 2 +Timers ds TIMER_REC_SIZE*MAX_TIMERS + +_GetVBLTicks + PushLong #0 + _GetTick + pla + plx + rts + +; Initialize the timers +InitTimers + jsr _GetVBLTicks + sta lastTick + + lda #0 + ldx #{TIMER_REC_SIZE*MAX_TIMERS}-2 +:loop sta Timers,x + dex + dex + bne :loop + rts + +; Add a new timer +; +; Input +; A = low word of timer callback function +; X = high word of timer callback function +; Y = number of ticks for the timer to delay +; C = set to 1 if the timer is a one-shot timer +; +; Return +; C = 0 if success, 1 if no timer slots are available +; A = timer slot ID if C = 0 +AddTimer ENT + phb + php ; Save the input parameters + phx + pha + phy + + phk + plb + + ldx #0 +:loop lda Timers,x ; If the counter is zero, timer is free + beq :freeslot + + txa ; Advance to the next timer record + clc + adc #TIMER_REC_SIZE + tax + + cpx #{TIMER_REC_SIZE*MAX_TIMERS} + bcc :loop + bra :notimers + +:freeslot pla + sta Timer+0,x ; set the counter and + stz Timer+2,x ; default to a zero reset value + pla + sta Timer+4,x ; set the callback address + pla + sta Timer+6,x + + stz Timer+8,x ; Clear the user data space + stz Timer+10,x ; Clear the user data space + stz Timer+12,x ; Clear the user data space + stz Timer+14,x ; Clear the user data space + + plp + bcc :oneshot + lda Timer+0,x ; if not a one-shot, put the counter + sta Timer+2,x ; value into the reset field + +:oneshot plb + txa ; return the slot ID and a success status + clc + rtl + +:notimers ply + pla + plx + plp + plb + + sec ; Return an error status + lda #0 + rtl + +; Small function to remove a timer +; +; A = Timer ID +RemoveTimer ENT + phb + phk + plb + cmp #{TIMER_REC_SIZE*{MAX_TIMERS-1}}+1 + bcs :exit + + tax + stz Timer,x + stz Timer+2,x + stz Timer+4,x + stz Timer+6,x + +:exit + plb + rtl +; Execute the timer functions +DoTimers ENT + phb + phk + plb + + jsr _GetVBLTicks + cmp lastTick ; Throttle to 60 fps + beq :exit + tax ; Calculate the increment + sec + sbc lastTick + stx lastTick + jsr _DoTimers + +:exit plb + rtl + +; Countdown the timers +; +; A = number of elapsed ticks +_DoTimers + pha + ldx #0 +:loop + lda Timers,x ; A zero means do not fire + beq :skip + + sec + sbc 1,s ; subtract the number of ticks + sta Timers,x + +:retry beq :fire ; getting <= zero triggers + bpl :skip + +:fire pha ; Save the counter + phx ; Save our index + + lda Timers+4,x ; execute the timer callback + sta :dispatch+1 + lda Timers+5,x + sta :dispatch+2 +:dispatch jsl $000000 + + plx + pla + + clc + adc Timers+2,x ; Add the increment + sta Timers,x ; Store in the count + bra :retry ; See if we have >0 ticks to wait until the next trigger + +:skip txa + clc + adc #8 + tax + cpx #8*MAX_TIMERS + bcc :loop + + pla + rts + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +