• Please review our updated Terms and Rules here

MagiDuck, a DOS / CGA text mode game project

Geez, you're absolutely right. I apologize for being lazy, and for having grown up with clone cards! I checked my own code (that works perfectly on a real CGA) and found this:

Code:
Procedure m40x50_set;
{
Use "2 pixel high" text characters. On CGA-type cards that use more than 8
pixels for text characters, like many non-IBM CGA cards and MCGA, this will
work anyway.  Blink is suppressed to get 16 background colors.
}
Begin
  m40x25_set;
  m6845_SetRegData(m6845_vertical_total, 62);
  m6845_SetRegData(m6845_vertical_total_adjust, 12);
  m6845_SetRegData(m6845_vertical_displayed, 50);
  m6845_SetRegData(m6845_vertical_sync_position, 56);
  m6845_SetRegData(m6845_maximum_scanline, 3); {value here is one LESS than number of character cell lines to display}
  hide_text_cursor; { Don't want cursor in "graphics" mode! }
  m6845_SetMode(c_videosignal_enable);
End;
 
Code:
  m6845_SetRegData(m6845_vertical_total, 62);
  m6845_SetRegData(m6845_vertical_total_adjust, 12);
  m6845_SetRegData(m6845_maximum_scanline, 3);

That'll probably work on most monitors but it's doesn't give the standard timing. It'll give (62+1)*(3+1)+12 = 264 scanlines instead of the usual 262, so a frame rate of 59.49Hz instead of 59.92Hz. You could do:

Code:
  m6845_SetRegData(m6845_vertical_total, 63);
  m6845_SetRegData(m6845_vertical_total_adjust, 6);
  m6845_SetRegData(m6845_maximum_scanline, 3);

which is what I think mangis is doing. Or you could reduce Vertical Total Adjust to the minimum if you prefer:

Code:
  m6845_SetRegData(m6845_vertical_total, 64);
  m6845_SetRegData(m6845_vertical_total_adjust, 2);
  m6845_SetRegData(m6845_maximum_scanline, 3);
 
That'll probably work on most monitors but it's doesn't give the standard timing. It'll give (62+1)*(3+1)+12 = 264 scanlines instead of the usual 262, so a frame rate of 59.49Hz instead of 59.92Hz. You could do:

Thanks, I've updated my 40x50 and 80x50 code. (They were originally derived not through calculation but rather by experimentation :-O
 
Well, now you can -- your latest build works 100% perfectly:

Wow, that is just awesome! Thank you so much! I was cheering out loud when I saw this at work, it was such a relief to see this wasn't a completely wasted effort. :)

Sorry for not replying to this sooner, I'm trying to avoid spamming this thread unless I've made some progress.

...and now you can consult the actual code yourself: https://github.com/keendreams/keen

Cool! Looks interesting and well commented... Actually learned something right away from this. Apparently the handle game actors as a linked list (which I didn't even know existed about before reading this).

Might be useful for MagiDuck as well. Currently it has an unordered array of objects and an ordered index-pointer array, which refers to the object array. A linked list would probably consume more memory though.

Here's how things are looking at the moment:

I've tackling with basic gameplay and enemy behavior. Here are some conclusions I've come to on different approaches:

Rainbow Islands / Bubble Bobble type gameplay:
- Relies on large screen size, which allows player to make many seconds worth of decisions based on level layout and enemy placement.
- Many enemies and pickups on screen.
- Player decisions mostly about finding an optimal route for collecting pickups and avoiding enemies in very chaotic situations.

* MagiDuck screen size doesn't allow player to be informed enough for this type of gameplay.
* MD sprite routines would completely choke on this type of design.
* MD physics would fit well though.

Megaman type gameplay:
- Very linear levels with precision timed enemy spawns and movement.
- Many isolated rooms that are crafted for a specific enemy type and player strategy.
- Projectile dodging is a big part of the challenge.
- Variety relies on numerous enemy types and player powerups.

* MD would benefit from tightly scripted enemy spawns and movement patterns, since they would be easier to optimize for minimum object count per situation.
* MD's vertical levels and small screen size probably wouldn't allow for very interesting situations.
* MD won't have many enemy types or a complicated powerup-system. I don't have the time for it ;)
* MD physics would fit this fairly well.
* Dodging projectiles works for MD too.

Mario 1 type gameplay:
- Challenge is mostly based on intricate physics.
- Game objects are very dynamic and provide for interesting strategy and unpredictable situations. (Turtle shells can benefit you or render some areas hazardous etc...)
- Few objects on screen at once and very easy to optimize for 1-direction scrolling playfield.

* MD would have to change to horizontally scrolling levels.
* MD can't handle intricate physics, because of the low resolution. Horizontal movement happens in two-pixel increments, making it even worse.
* Dynamic and bug-free objects sound like a huge amount of work.
* Creating game mechanics that wouldn't be a complete Mario ripoff might be beyond my capacity... ;)

Duke Nukem 1 type gameplay:
- Very coarse physics and small screen size.
- Levels based on exploration, rather than well programmed enemy waves or any specific mechanic.
- No real threat of dying, unless player makes numerous very hasty moves. Very generous health pickups.
- Challenge seems to be about optimizing score in situations with destructible pickups and enemies on screen.
- Satisfying effects and destructible environments / pickups make for some fun gameplay, even if it's extremely flat.
- Interesting level layouts and lots of variety in graphics to keep things fresh.

* Kind of what MD has been so far, except with minimal exploration.
* Making Duke1 with less graphics and effects doesn't sound like fun.
* I really don't want MD to be a straight up shoot'em up...

Where MagiDuck is going at the moment:
- Tower climbing platformer with some shooting.
- Lower edge of the screen kills the player. Choosing routes becomes important since there's no way back down.
- Most enemies will shoot. Projectile dodging while shooting back is important for the player.
- Killing enemies in good spots in order to collect their pickups.
- Finding keys that open chests for sweet loot.
- Time is a big part of the final score, so speedrunning will be rewarded.


I don't really know what to say about the coding part though... There are multiple things that could be optimized in the BASIC code still. I already changed most of my collisions from rectangle-rectangle-tests to point-rectangle-tests. I'm thinking about seperating collisions from sprite data, so each object would just have a predefined collision rectangle. The engine would be slightly less flexible after that but it probably doesn't need to be.
 
Apparently the handle game actors as a linked list (which I didn't even know existed about before reading this).

Linked lists gain flexibility but lose speed. Use accordingly.

- Lower edge of the screen kills the player. Choosing routes becomes important since there's no way back down.

I won't make any claims as a game designer, but as a game player I really hate "lower edge kills player" games and avoid them. I like exploring :) Just my $0.02.
 
I won't make any claims as a game designer, but as a game player I really hate "lower edge kills player" games and avoid them. I like exploring :) Just my $0.02.

Appreciated. :)
Yeah, I actually have similiar tastes, but have been interested in arcade style game mechanics lately. Also, since this game is a curiosity really, I figured it might be better to have really clear goals and tight mechanics, since players might not hold interest in the game for a long period of time.

Most of all the thinking behind "lower edge kills player", was to make the players commit to their decisions and create well planned situations in the levels. It seemed very difficult to create interesting situations with such a low object count, since it's very easy to run away and exploit the idiotic AI.

Anyways, the current version does not have the killing lower edge anymore. I added some invincible enemy types, that should force the player to think about running back too fast. The game is surprisingly difficult if you want to collect all the bonus items, so I guess this might work? I had to revise my spawner system to allow non-linear progession, but it was surprisingly easy. The levels are still divided into entity "waves", which have a maximum of 16 entity spawners in them. One or more of those entities can be proximity-triggered portals, that activate another "wave" and destroy any previous entities that are far enough from the player. The entity spawners remember if an entity has been destroyed permanently (by player for example) and will not respawn them when a portal tries that.

The levels also have some new special blocks, that allow for some puzzle elements and other variety.

Here's a video showing some of the latest mechanics:


There are also ice blocks, but they're missing from the video. By the way, I don't regard myself as a game designer at all either. Most of my previous analysis was based on watching let's play-videos, so it was admittedly quite shallow.

So coding-wise all this has mostly been scripting type stuff. Just one interesting thing maybe, I learned a nice QB linker trick, that allows for a smaller exe-size by using stub .OBJ files like this:

Code:
link.exe /EX /NOE /NOD:BRT71FR.LIB game.obj+noedit.obj+nocom.obj+nolpt.obj+smallerr.o  bj,game.exe,nul.map,c:\qb71\lib\BCL71EFR.LIB+c:\qb  71\lib\qbx.lib

Yay! Shaved off 5kb from my exe-file. I'm thinking those 5K will be filled right back up very quickly. :)

And if anyone's interested in the custom tools that MagiDuck has, there's a video on them here: http://youtu.be/gqAwmbhQjrk
 
Progress, whee.

Most of my time has gone to making EGA and VGA compatibility work and splitting sprite-memory into two banks. The additional sprite bank is slightly smaller and is meant for quickly loading things like alternate enemy-graphics. Animation and sprite-atlas data are now stored in binary format to make loading faster. Development iteration wise it's slightly clunkier, but that won't be a problem at this point.

Making VGA/EGA support was surprisingly difficult. Quite a step up in documentation complexity, from IBM's CGA to EGA and VGA references... It's certainly nice to have interrupts to do most of the work.

The only register OUT i'm doing for VGA is 400-to-200 scanline conversion and character height. I'll probably change that to an interrupt call though.

For EGA register OUTs, I just copied values from the mode-specific example charts for Mode 0 without an EGA monitor. I can't believe they didn't have these charts in the VGA documentation. Then again, maybe mode-setting isn't something anyone would like to do via registers.

Dosbox doesnt't seem to allow blink-disable in EGA-mode. Here's hoping there aren't any other inconsistensies.

Here's the current video-detection code:

Code:
aInitVideo PROC

;============================================================================
;
;   Checks for video adapters VGA, EGA, CGA or MONO/OTHER
;   
;   If MONO/OTHER is found, 0 is returned at ES:DI (and the game won't run).
;
;   VGA/EGA/CGA will initialize 40x50 text mode with 8x8 characters.
;
;============================================================================

; Parameter stack offsets
; Order is inverted from qbasic CALL ABSOLUTE parameter order

;00 bp
;02 Qbasic return segment
;04 Qbasic return offset

;06 Hudbuffer offset    /   Use Hudbuffer to accommodate
;08 Hudbuffer segment   /   VGA state check and adapter test result.

;============================================================================

    push bp
    mov bp,sp

;---------------------------------------------------------------------------
test_VGA:
    mov es, [bp + 08]               ;ES = Write seg (VGA video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (VGA video adapter data)

    xor ax, ax                      ;Interrupt call 10h AH=1Bh AL=0 BX=0
    mov ah, 01bh                    ;Get VGA functionality and state.
    xor bx, bx                      ;dumps 64 bytes at ES:[DI]
    
    int 10h
        
    cmp al, 01bh                    ;If AL=1Bh this is a VGA
    je detected_VGA
    
test_EGA:
    xor ax, ax
    xor bx, bx                      ;Interrupt call 10h:
    mov ah, 12h                     ;Get EGA information.
    mov bl, 10h                     ;Returns BL > 4 if not EGA.
                                    
    int 10h                         
                                    
    cmp bl, 4                       ;BL =< 4 means we're EGA
    ja test_CGA                     ;compatible.
    jmp detected_EGA

test_CGA:
    
    int 11h                         ;Interrupt call: Equipment check.
    
    and ax, 30h                     ;Check bits 4 & 5
    
    cmp ax, 30h                     ;If bits are on, this is a mono adapter.
    jz detected_MONO
    jmp detected_CGA

;---------------------------------------------------------------------------
    
detected_MONO:
    mov dx, 0                       ;Video adapter data for game
    jmp get_current_mode
    
detected_VGA:
    mov ax, 1003h                   ;Disable blink for VGA one extra time to be sure.
    xor bx, bx                      
    int 10h
    
    mov dx, 3                       ;Video adapter data for game
    jmp get_current_mode

detected_EGA:
    mov dx, 2                       ;Video adapter data for game
    jmp get_current_mode

detected_CGA:
    mov dx, 1                       ;Video adapter data for game
    jmp get_current_mode

get_current_mode:
    mov ax, 0F00h                   ;Get current video mode
    int 10h                         ;Stored to BL
    
    mov es, [bp + 08]               ;ES = Write seg (video adapter data)
    mov di, [bp + 06]               ;DI = Write ofs (video adapter data)
    add di, 2
    stosw                           ;AL = current video mode
    
    cmp dx, 1                       ;If adapter is EGA/VGA
    ja set_EGAVGA                   ;set mode with interrupts
    jmp exit
    
set_EGAVGA:
    xor ax, ax
    mov al, 01h                     ;Set video mode 01h
    int 10h                         ;40x25 colour text mode
    
    mov ax, 1112h                   ;Load and activate 8x8 character
    xor bx, bx                      ;set 0
    int 10h
    
    mov ax, 1003h                   ;Select FG Blink / 16 bg colors (BL = 0)
    xor bx, bx                      ;Clear whole BX to avoid problems on some adapters.
    int 10h
    
;------------------------------------------------------------------------------
exit:
    mov es, [bp + 08]               ;ES = Write seg (hudbuffer)
    mov di, [bp + 06]               ;DI = Write ofs (hudbuffer)
    
    mov ax, dx                      ;Store detected video adapter
    stosw                           ;data at ES:[DI]
    
    pop bp
    retf 4

Getting page-flipping and vSync to work with EGA and VGA was pretty damn puzzling. The game flickered like crazy. I couldn't find anything useful in IBM's documentations or even any VGA tutorials. Probably because most tutorials didn't handle page-flipping by changing the screen memory address, but only timed their drawing to match with vSync.

Finally Abrash's Black Book had the answer, VGA (and apparently EGA too?) need to have their start memory address changed before vertical retrace, for it to take effect after the beam is back up. On CGA however, the address CAN'T be changed before vertical retrace, or it'll mess up the screen during drawing.

Things started to work, after I made a seperate vSync wait for EGA and VGA. It waits for 0 from both Display-enable and Vertical-retrace bits from 0x3DA, to be sure vertical retrace isn't happening before changing the starting address. After the change, it waits for Vertical-retrace to enable before it starts to clear the previous page address.

Here's the code:

Code:
;================================================================================   
aPageFlip PROC
                                                           
    ; Page flip and vertical sync.
    ;
    ; Waits for vertical retrace and changes video start offset.

    ; Parameter stack offsets
    ; Order is inverted from qbasic CALL parameter order

    ;00 bp
    ;02 Qbasic return segment
    ;04 Qbasic return offset

    ;06 set page offset
    ;08 previous page offset
    ;10 hud string offset
    ;12 hud string segment
    ;14 video wrap offset

    ;============================================================================

        push bp
        mov bp,sp

    ;---------------------------------------------------------------------------
    ; Copy HUD to set page
    ;---------------------------------------------------------------------------
        mov dx, [bp + 14]               ;DX = Video wrap offset

        mov ds, [bp + 12]
        mov si, [bp + 10]

        mov es, screenSeg               ;ES:DI = Page offset * 2
        mov di, [bp + 06]
        shl di, 1
    mov cx, 16                          ;Hud string = 160 bytes
    copyHud:        
        movsw                           ;Copy character and attribute
        and di, dx                      ;Wrap DI with video wrap offset     
        movsw                           
        and di, dx                      
        movsw                           
        and di, dx                      
        movsw                           
        and di, dx                      
        movsw                           
        and di, dx                      
    loop copyHud
    
        lodsb                           ;CL = Video adapter (stored at the
        cmp al, 1                       ;                    end of hudBUffer)
        jne egaVgaPageflip
    
    
    ;---------------------------------------------------------------------------
    ; CGA Wait for vertical overscan and retrace.
    ;---------------------------------------------------------------------------
        mov dx, 03DAh
        mov ah, 8

    cgawait1:                           ;If currently in retrace, wait
        in  al, dx                      ;until that's finished.
        and al, ah
    jnz cgawait1                                
        
    cgawait2:                           ;Wait until retrace starts.
        in  al, dx
        and al, ah
    jz cgawait2
    
    ;---------------------------------------------------------------------------
    ; CGA Change page offset
    ;---------------------------------------------------------------------------
    
        mov dx, 03d4h 
        mov bx, 03d5h

        mov al, 0dh
        out dx, al
        xchg dx, bx
        mov al, [bp + 06]
        out dx, al

        xchg dx, bx

        mov al, 0ch
        out dx, al
        xchg dx, bx
        mov al, [bp + 07]
        out dx, al
    
    jmp clearHudPrevPage                        
    
    ;---------------------------------------------------------------------------
    ; EGA/VGA Wait for vertical overscan and retrace.
    ;---------------------------------------------------------------------------
    egaVgaPageflip: 
        mov dx, 03DAh                   ;Check for both Display enable- and
        mov ah, 9                       ;Vertical retrace bits (0 and 3).

    egavgawait1:                        ;If currently in retrace, wait
        in  al, dx                      ;until that's finished.
        and al, ah
    jnz egavgawait1                             

    ;---------------------------------------------------------------------------
    ; EGA/VGA Change page offset
    ; This needs to be done before vertical retrace begins with these adapters.
    ;---------------------------------------------------------------------------
                
        cli                             ;Close interrupts
                
        mov dx, 03d4h 
        mov bx, 03d5h

        mov al, 0dh
        out dx, al
        xchg dx, bx
        mov al, [bp + 06]
        out dx, al

        xchg dx, bx

        mov al, 0ch
        out dx, al
        xchg dx, bx
        mov al, [bp + 07]
        out dx, al
                
        sti                             ;Restore interrupts
                
    ;---------------------------------------------------------------------------
    
        mov dx, 03DAh
        mov ah, 8

    egavgawait2:                        ;Wait until retrace starts.
        in  al, dx
        and al, ah
    jz egavgawait2
    
    
    ;---------------------------------------------------------------------------
    ; Clear HUD from previous page
    ;---------------------------------------------------------------------------
    clearHudPrevPage:
        mov dx, [bp + 14]               ;DX = Video wrap offset
        mov di, [bp + 08]               ;DI = previous page offset * 2
        shl di, 1
        mov ax, 011DEh                  ;Clear attribute = 11, character = 222
    mov cx, 16
    clearHud:       
        stosw                           ;Copy character and attribute
        and di, dx                      ;Wrap DI with video wrap offset     
        stosw                           
        and di, dx                      
        stosw                           
        and di, dx                      
        stosw                           
        and di, dx                      
        stosw                           
        and di, dx                      
    loop clearHud

    ;============================================================================

    exit:
        pop bp
        retf 10
aPageFlip endp

One big issue ahead might be the timer-routine. It's using Quickbasic's TIMER command, which is modified to tick eight times faster than normal to allow precision beyond the usual 18.2 Hz. Unfortunately it also seems to mess up the computer clock... So I'd either need my own timer-routine or a way to calculate the correct clock-value on exit.

I have to admit, this project is starting to look more messy every month. It's a little disheartening to see how adding content and optimizing makes things less and less readable. While I'm now less sure if this'll be a fun game to play, I still want to finish it. But the emphasis has definitely changed from play-with-it to just-finish-it ;)

Coming up: new levels, hub-world, savegames, hi-scores, first actually playable release and probably many disasters.
 
If you're going to play with the timer, you REALLY should be putting your own ISR on it. My question would be are you using TIMER to simply loop to wait until it's time for the next frame, or are you using it to scale the game logic in realtime so it can run more frames on faster machines while still being the same "speed" of play?

If we assume the former, and that you're running the divisor at 8192 for that 8x speed...I JUST got done tacking down the corners on mine for a non-quotient of 65536, this would be way simpler. Something like:

Code:
segment CODE

timerTick dw 0
timerCount db 8
timerActive db 0

timerISR:
	inc   WORD [cs : timerTick]
	dec   BYTE [cs : timerCount]
	jz    .callOldTimerISR
	push  ax
	mov   al, 0x20
	out   0x20, al
	pop   ax
	iret
.callOldTimerISR:
	mov  [cs : timerCount], 8
	db   0xEA    ; jmp FAR
oldTimerISR:
	dd   0x00000000    ; address for jump FAR

as the ISR, this to set it up:
Code:
timerStart:
	cmp   [cs : timerActive], BYTE 0x00
	ja    .done
	inc   [cs : timerActive]
	mov   [cs : timerTick], WORD 0x00
	mov   [cs : timerCount], BYTE 0x08
	push  ax
	push  bx
	push  dx
	push  ds
	push  es
	mov   ax, 3508
	int   0x21
	mov   [cs : oldTimerISR], bx
	mov   [cs : oldTimerISR + 2], es
	cli
	mov   ax, cs
	mov   ds, ax
	mov   dx, timerISR
	mov   ax, 0x2508
	int   0x21
	mov   al, 0x34
	out   0x43, al
	mov   ax, 0x2000
	out   0x40, al
	mov   al, ah
	out   0x40, al
	sti
	pop   es
	pop   ds
	pop   dx
	pop   bx
	pop   ax
.done:
	retf

This to end it:
Code:
timerEnd:
	cmp   [cs : timerActive], BYTE 0x00
	je    .done
	push  ax
	push  dx
	push  ds
	cli
	mov   al, 0x34
	out   0x43, al
	xor   al, al
	out   0x40, al
	out   0x40, al
	mov   [cs : timerActive], al
	lds   dx, [cs : oldTimerISR]
	mov   ax, 0x2508
	int   0x21
	sti
	pop   ds
	pop   dx
	pop   ax
.done:
	retf

... and this as the user calls for the main loops:
Code:
timerWait:
	cmp   [cs : timerTick], WORD 0x0000
	je    timerWait
	dec   [cs : timerTick]
	retf

You'd also want a reset that you'd call before your main loop.

Code:
timerReset:
	mov   [cs : timerTick], WORD 0x0000
	retf

Keeping the values in the code segment means no dicking around with DS setting/swapping inside the ISR, for a smaller and simpler (and therin on 8088 faster) handler. Instead of just saying "we have gone past a timer tick" storing how many ticks have passed means that if one of your frames has a spike but the next few frames use less cpu time, it will "flatten" out the frame rate so that a single "took too long" frame is smoothed out to be less noticeable. If you were just flat polling for a single-state change not only would the 'long' frame take longer, the next one would take TWO ticks instead of one.

But, since we're storing the number of ticks, you'll want a reset to be called before your main loop otherwise your first xxxx frames (however many since the timer was started) will run flat out as if you had no wait.

A nice side effect of that is that you can also do longer waits VERY simply:

Code:
timerWaitTicks:
; INPUT
;   [bp + 6] = WORD number of ticks to wait
	push  ax
	mov   ax, [bp + 6]
.loop:
	cmp   [cs : timerTick], ax
	jb    .loop
	mov   [cs : timerTick], WORD 0x0000
	pop   ax
	retf

I was literally JUST adding this to my 160x100 engine for Paku Paku 2.0, so there's very little rewrite here other than the simpler ISR... since mine runs a divisor of 9956 (2x the frame rate) instead of 8192, I have to do a inc/dec Bresenham style to smooth out the rate the old system timer is called thus:

Code:
%define countInc WORD 0x4000
%define countDec WORD 0x09B9

timerISR:
	inc   WORD [cs : timerTick]
	sub   [cs : timerCount], countDec
	js    .oldCall
	push  ax
	mov   al, 0x20
	out   0x20, al
	pop   ax
	iret
.callOldTimerISR:
	add   [cs : timerCount], countInc
	db   0xEA
oldTimerISR:
	dd   0x00000000

The big worry isn't even the system time going akilter -- failing to maintain it can also mess up floppy disk access and other hardware drivers that expect that timer to be 'correct'.

Hope that helps. Love your project --- it's exactly the type of ice-skating uphill I love trying to do.
 
If you're going to play with the timer, you REALLY should be putting your own ISR on it. My question would be are you using TIMER to simply loop to wait until it's time for the next frame, or are you using it to scale the game logic in realtime so it can run more frames on faster machines while still being the same "speed" of play?

If we assume the former, and that you're running the divisor at 8192 for that 8x speed...

Many thanks for the code! I think you saved me some years there. My sprite masking logic was already stolen from you, but now I've got some actual copy-pasted lines too, dang. Credit will be given, of course :)

That's right, the game only waits for the next frame and doesn't scale any transformations by time. Testing collisions would've been quite a bit more expensive if time was accounted for.

I added your routines to my library and got things working after a few days of fiddling and reading. Nothing wrong with your code of course, but MASM didn't like some things and I lost plenty of hours because I'd copied 3508 as decimal instead of hex, hah. Ended up learning more this way, so no big deal :)

One disconcerting side effect appeared though, Quickbasic tends to crash after a few minutes if I run the game from the editor... I wonder what QB does with the timer, since the original timers memory location is the same, whether it's requested from a compiled runtime or from the interpreted version (0000:FEA5).

Compiled versions of the game seem stable (running and exiting the game five times), so I ended up ditching the Qbasic editor and started using Notepad++ and compiling from command line.

Sometimes QB gets far enough before crashing to show a "String space corrupt" -error, so I think more testing with the runtime should be in order though.
 
Last edited:
Code:
%define countInc WORD 0x4000
%define countDec WORD 0x09B9

timerISR:
    inc   WORD [cs : timerTick]
    sub   [cs : timerCount], countDec
    js    .oldCall
    push  ax
    mov   al, 0x20
    out   0x20, al
    pop   ax
    iret
.callOldTimerISR:
    add   [cs : timerCount], countInc
    db   0xEA
oldTimerISR:
    dd   0x00000000

I just noticed this -- why are you adding to cs:timerCount when calling the old handler? A single op should be all that is necessary, ie.:

Code:
        add     [cs:timerCount],PITDIVRATE ;add to our BIOS counter
        jc      @@handlebiostick        ;if didn't roll over, skip BIOS tick
        push    ax
        mov     al,20h                  ;acknowledge PIC so others may fire
        out     20h,al                  ;ok to do this here since we are
        pop     ax                      ;top priority in PIC chain
        iret
@@handlebiostick:
        db      0xEA
oldTimerISR:
        dd      0x00000000

...or equivalent. timerCount is your own var; no need to do special handling just because you're chaining to the BIOS int...
 
Heh, good catch. I was thinking bresenham in my head, reversing the logic (so PITDIVRATE would be... 0x26E4?) would give the same result.

So used to coding line-draw where the 'inc' is never a fixed value... I just automatically did it that way without thinking about it.

Thanks. Probably won't have a significant performance impact, but it sure can't hurt.
 
When you add to or subtract from a 16-bit register and the low byte of the immediate value is zero, like this;
Code:
sub cx, 4000
then you can save a byte by doing it like this;
Code:
sub ch, 40
Oops! I just noticed that the above numbers are in decimal format. My advice applies only when the numbers are in hexadecimal format. Sorry if this wasted your time. Also, just to clarify, this is only an optimization when the register is not AX.

Sometimes QB gets far enough before crashing to show a "String space corrupt" -error, so I think more testing with the runtime should be in order though.

It's been a while since I did anything in QB but IIRC, this can also happen when you use a lot of memory (string space) and/or have very big strings. In other words, it can just as well be a bug in QB.
 
It's been a while since I did anything in QB but IIRC, this can also happen when you use a lot of memory (string space) and/or have very big strings. In other words, it can just as well be a bug in QB.

Probably not a bug in QB, the EXE-version got some random corrupted pixels in it's sprite memory (lucky, easy to spot) as well.

Tried a simple thing today and it seemed to fix everything:

Moving all variables (timerTick, timerCount, etc) from .DATA to .CODE in the assembly listing seemed to fix the problem. The game runs solid both from Qbasic and as EXE, even when restarted muliple times.

I guess anything put in .DATA would get moved around to where ever LINK, LIB or BC would put data (from the whole program, not just the library) during library compiling. Maybe writing to a variable offset supposedly in CS could go in all kinds of places after that?

Since variables seem to work so well now, my other routines could really benefit from them too... I wouldn't need to send obvious information like sprite bank memory offsets every frame through stack.

Just to show how things are ordered in the assembly routines now:
Code:
.model medium,basic

.data

.code   
;==============================================================================
;
; Procedures
;
;============================================================================== 

aTimerStart PROC
aTimerStart ENDP

aTimerEnd PROC
aTimerEnd ENDP

aTimerWait PROC
aTimerWait ENDP

aTimerReset PROC
aTimerReset ENDP

;==============================================================================
;
; Timer ISR
;
;============================================================================== 

    timerISR:
        inc     timerTick
        dec     timerCount
        jz      callOldTimerISR
        push    ax
        mov     al, 20h
        out     20h, al
        pop     ax
        iret
            

    callOldTimerISR:
        mov  timerCount, 8
        db 234
        oldTimerISR dw 1234h, 5678h, 0000h, 0000h, 0000h
        ; Added some dummy data for safety(?)

;============================================================================== 
;
; Code segment variables
;
;============================================================================== 
    
    screenSeg       dw  0b800h

    timerTick       dw  0
    timerCount      db  8
    timerActive     db  0

;============================================================================== 
    

public aTimerStart

public aTimerEnd

public aTimerReset

public aTimerWait

end
 
I can't speak for QB, but I know "good old" GW-Basic played fast and loose with segments to the point integrating ASM was a chore at best. If it has any of the same mechanisms in place I'd not be at all surprised if half the time you went to call those routines you were getting a different value in DS than your .DATA declaration was allocating... hence why putting them in your code segment is running fine.

It's part of why BASIC -- ANY form of BASIC -- would have been the LAST language I'd have gone to for building something like this excepting perhaps Turbo Basic -- IMHO that makes what you've accomplished all the more impressive; but also makes me wonder how much faster/more effficient/easier to develop it would have been to use a Borland Pascal or C compiler.
 
Moving all variables (timerTick, timerCount, etc) from .DATA to .CODE in the assembly listing seemed to fix the problem. The game runs solid both from Qbasic and as EXE, even when restarted muliple times.

This is obvious though...
You are using an interrupt handler. Interrupts are basically far-calls, and you don't know what any segment other than cs is (cs being your int handler).
So you either need to store all your data for your int handler in the same segment as the code of the int handler... Or you need to store a variable containing the segment for your data in the int handler segment, and load ds (and/or es) yourself (don't forget to restore it before exiting the handler).

That isn't language-specific. It's just how the x86 works.
 
This is obvious though...
OH, yeah. I thought he was talking all his .data -- of course the ISR stuff HAS to be in CS as you can't rely on DS being static... and .DATA is DS based. That's why my code example had the segment override.
 
It's part of why BASIC -- ANY form of BASIC -- would have been the LAST language I'd have gone to for building something like this excepting perhaps Turbo Basic -- IMHO that makes what you've accomplished all the more impressive; but also makes me wonder how much faster/more effficient/easier to develop it would have been to use a Borland Pascal or C compiler.

Certainly can't disagree there. Almost every line of code is trying so hard not be BASIC at this point, it might as well not be.
With C it might be possible to shave off 30kb from the executable size. Saving many of the variables as bytes instead of words would be nice as well... Using that memory and better speed to improve the game is pretty tempting.

I tried playing around with Turbo C 2 last year, but this doesn't feel like the right time for a rewrite... I'm not really convinced the game is worth redoing 2500 lines of Assembly and 2000 lines of BASIC, while learning C nearly from scratch. At least yet. It might be easier when I've got the games features well in place to avoid useless work.

Moving from Quickbasic editor to Notepad++ already boosted up my productivity slightly, I can make the code much more readable and don't have to worry about comments using precious memory. Everything feels much less cramped now.

I also ditched my custom level editor and made a batch-conversion program to convert levels from Tiled. Creating and iterating on levels is much faster now.

This is obvious though...

Yup, it was pretty embrassing to realise how "un-assembly" that thinking was.
 
Last edited:
Back
Top