|Letter from the Editor
Hello fellow QB Programmers! Welcome to the first issue of the QB Chronicles! I've got some neat articles for your reading pleasure. I've written three articles: one a newbie article on VARSEG and VARPTR, two commands in QB that can be quite scary and useful at the same time. I've also started a series that focuses more on the programming aspect of QB RPGs. This month's article is more tuned in on programming the "frame" for your RPG, the engine, and its a pretty thick one too! I've also included an article on using XMS in QB which I originally submitted to the QB Times, but has a few corrections in it =). Also you can check out a site review I did on GB Games a fantastic site which you should check out if you already haven't.
If you'd like to write articles for publication, review games and/or sites, or be a "news reporter", then please e-mail me and I'll try to get back to you as soon as I can. Any help would be appreciated greatly!
Also if you spot any errors at all in this issue please e-mail me and tell me about it so I can fix it.
Hopefully you'll enjoy this issue and I look forward to hearing from you! See you in the next month or so!
Project and Site News and Information
This section is where the authors of the QB Chronicles will put information on projects and sites from the QB community. Don't forget to submit your information to us!
GB Games Review
|Graphics:||Not to many graphics on this sitei It's more of a text only layout. Though he does have a neat logo.... =)||6/10|
|Content:||Some great content. The theme of the site is game reviews and there are about 30. They are all really detailed and show screenshots of the games. Also has a good amount of links. I like how he also puts news from the QB community on the main page too. There's a message board too, so you can post stuff.||19/20|
|Layout:||Good layout. There are two frames on of course has the site navigation bar. Also it's easy to find what you're looking for too.||8/10|
|Downloads:||Not many downloads, but then that's not the point of the site =)||4/5|
Beginners guide to VARSEG and VARPTR
This article was written by Fling-master
This article will discuss the use of VARSEG and VARPTR in your QBasic programs. It is intended mainly for newbies to QB but also for people who aren't 100% confident with using these commands.
VARSEG and VARPTR, first of all deal with a variables position in your computer's memory. To get a firm grasp of these commands you should know how to use the values returned. So I will discuss segments and offsets! =)
Segments and offsets are a pair of numbers telling the computer where to read and write data to. A segment is a group of 16 bytes in memory. An offset on the other hand is a group of 1 byte. These two numbers are used together to get the address of something in memory. So if I want to get the data at address 156 in memory I would set the segment to 9 and the offset to 12. If I wanted to get the data at address 34 in memory I would set the segment to 2 and the offset to 1. You could just set the offset to 156 in the first example and 34 in the second but we just don't do that, especially if the desired address is a big number 64000! Also the segment is always first e.g. segment:offset.
So now you know what they are. Now what do they have in common with VARSEG and VARPTR you ask? Well VARSEG returns the passed variables segment address as an unsigned integer (a positive number), and VARPTR returns the passed variables offset address also as an unsigned integer. This is a handy thing especially if you're using stuff like BSAVE, BLOAD, POKE, PEEK, and CALL ABSOLUTE.
So let's do a simple example program here, to show you how you'd use it. We're gonna get the segment and offset address of an integer variable in QB:
'This program will print an integer's segment and offset address. DIM myvariable% 'We're gonna dim in just in case. =) segment% = VARSEG(myvariable%) 'Get the segment address offset% = VARPTR(myvariable%) 'Get the offset address 'Display the variable memory location PRINT "myvariable% is located at "; segment%; ":"; offset%
There you go! Try it to see if it works. This example though really isn't useful. BUT, when you get a variables address you can do other things like use BSAVE to save it to a file. Make sure that when you get a variables address you do whatever it is you are gonna do with it quickly as the address could change!
Let's make an example that actually does something that could turn into something useful. We're gonna make a program that adds two numbers together and saves the result, using BSAVE to a file. This will require the use of VARSEG.
DIM c% 'Just in case =) INPUT "Enter number 1: ", a% 'Get number 1 to add INPUT "Enter number 2: ", b% 'with number 2! c% = a% + b% 'Add them together 'Here we're setting the default data segment used by QB to the c% variable. This way BSAVE 'get the data from there and save it to the file. DEF SEG = VARSEG(c%) 'Here we're specifying a file and secondly the offset of c%, then we tell BSAVE the length 'to save which, because c% is an integer, happens to be 2. BSAVE "c:\number.dat", VARPTR(c%), 2 'Unless you know what you're doing, you should always restore the default segment back to the 'original basic segment. Which is what this does. DEF SEG PRINT "Result is"; c%; "!" 'Show the user the result of the addition
Go ahead and try it out! You will see that after the program has finished there will be a file called number.dat in your harddrives root directory. Although the file will be saved as binary. To read this file we use BLOAD. Here's an example which will read the number.dat file from before and display the contents:
DIM c% 'Just in case =) DEF SEG = VARSEG(c%) 'Set the default segment to c% 'Here we use BLOAD and specifiy the file which contains the number, and the offset of c% BLOAD "c:\number.dat", VARPTR(c%) DEF SEG 'Restore the default segment PRINT "The number you got when you added was"; c%; "!" 'Show the result
There! Simple right? Now you should have a good understanding of VARSEG and VARPTR. You can also use this with strings to but instead of using VARPTR use SADD. Now you can use POKE and PEEK, especially since you know what segments and offsets are. Before using POKE or PEEK you should first set the default segment using DEF SEG to the segment address you want to write/read data from, similar to the above examples.
'$DYNAMIC DEFINT A-Z DIM myvar 'Dimension just in case PRINT "myvar is set to 13." myvar = 13 'Set myvar=13 DEF SEG = VARSEG(myvar) 'Set default segment to myvar POKE VARPTR(myvar), 27 'Poke a new value, 27, into the offset of myvar DEF SEG 'Resetore the default segment PRINT "myvar now equals:"; myvar 'Print the new value of myvar
There ya go. That'll let you put numbers into variables. Now these examples are sorta useless, but they can be expanded and worked with to meet your needs. A good use for POKE and PEEK is with the famous memory segment A000. This of course is the address of the video segment. Since it's in hexadecimal you should precede it in QB with a &H. POKE will let you draw a pixel, and PEEK will let you read a pixel. One last thing, you can't POKE a value of more than 255, if you do you'll get the value of the number you poked minus 255 (This will never be larger than 255 either.)
If you don't understand this then please e-mail me. Have fun!
RPG Creation 101
This article was written by Fling-master
Unlike most RPG tutorials I'm gonna skip the long part of creating a story, and characters, etc. Now just because I'm skipping that doesn't mean it's not important! Before going in to create an RPG you should sit down and think about that, maybe jot down a few ideas you have. Next comes planning the engine for your RPG. Here are some things you should take into account:
These are just some ideas to think about. And hopefully this series will cover them all! (Though don't expect it to show you how to setup every thing mentioned above heheh.) This month I'll show you how to create a pixel-by-pixel scrolling engine, which'll use two map layers which can give you some neat effects, such as bridges, etc. We'll also touch on some map format things. Two things I'm going to leave for you to decide is the method you use to load tiles/tilesets, and the palette. These things are both quite programmer specific and is not really that much of a trouble to set up.
So lets get into making an engine which is basically the shell for the RPG. This engine which I'm going to help you build is by no means carved in stone. Feel free to change things around and modify it to your needs. You might even know of a few ways to speed it up/enchance it in some way! (If you do then e-mail me and I'll put it in a future article.) Also the graphics routines used are not actually real. They are just put in so you know what it should be. The graphics routines should be in assembly. Maybe sometime in the future I'll tell you how to make some custom routines.
So the basic subs we'll want for now is:
'$DYNAMIC DEFINT A-Z DECLARE SUB DrawMap () 'Draws our map DECLARE SUB LoadMap (FileName$) 'Loads the map and processes it DECLARE SUB MoveUp () 'These next subs will handle moving up, down, left, and right DECLARE SUB MoveDown () DECLARE SUB MoveLeft () DECLARE SUB MoveRight ()
Those subs are the ones we'll use for this part of the series. We'll add on to them eventually. So next we need to set up our data types and global variables:
TYPE EngineType 'This'll hold our players info and some other things x AS INTEGER 'Player X position y AS INTEGER 'Player Y ... MaxX AS INTEGER 'Maximum X position in map MaxY AS INTEGER 'Maximum Y ... Direction AS INTEGER 'Current direction Animate AS INTEGER 'Animation frame tracker Action AS INTEGER 'Our action tracker MaxNPCs AS INTEGER 'Maximum amount of NPCs END TYPE TYPE MapType 'This'll hold the map data. tile AS INTEGER 'Tile number in map tile2 AS INTEGER 'Tile number for layer 2 Collision AS INTEGER 'Collision data in map END TYPE CONST South = 1, North = 2, West = 3, East = 4 'Constant values for directions CONST True = -1, False = 0 'True & false (duh!)
DIM SHARED Engine AS EngineType 'Main engine variable DIM SHARED map(0 TO 63, 0 TO 63) AS MapType 'Map data. 'Here we're gonna dimension our tileset arrays and our offscreen video buffer. DIM SHARED tilearray(1, 1), PlayerGFX(1, 1), buffer(31999)
These are the data structures we'll be using for right now. Notice we're gonna use a 64x64 map. If we were using XMS to store the map data we could have a much larger map. Some things like Engine.MaxNPCs we'll leave for now. So now we need to load in the tiles, palettes, maps, and setup the screen mode to SCREEN 13:
'320x200x256 resolution SCREEN 13 DEF SEG = &HA000 'Video segment is default segment LoadPalette "yourpal.pal" 'Load our palette LoadTiles "tiles.dat", tilearray() 'Load the tiles LoadTiles "npcs.dat", PlayerGFX() 'Load the NPCs LoadMap "yourmap.map" 'Load the map
The routines to load the palette and tiles are not real, they are just shown so you know where to put the call to your routines. You will notice that when I load the tiles it is actually loading a tileset. So now we're ready to draw our first view and let the player move around the map. How are we gonna do that? Well we'll create a loop that'll contiually check if a key is being pressed and we'll update the screen no matter if a key is being pressed or not. This allows us to check the FPS and it'll be easier when we implement NPC's and other things like animations. So here's out main loop:
DrawMap 'Draw the map VideoCopy VARSEG(buffer(0)), VARPTR(buffer(0)) 'Copy it to video memory DO 'Start the loop k = INP(&H60) 'Read keyboard status. Faster than INKEY$ SELECT CASE k 'Now we're gonna find out what key was pressed CASE 72 'The up arrow key MoveUp 'Move player up Engine.Action = True CASE 80 'The down arrow key MoveDown 'Move player down Engine.Action = True CASE 75 'The left arrow key MoveLeft 'Move player left Engine.Action = True CASE 77 'The right arrow key MoveRight 'Move player right Engine.Action = True CASE 1 'The ESC key END 'Stop the program END SELECT IF Engine.Action = True THEN 'If the user moved we'll call the script interpreter Engine.Action = False 'The script interpreter will be put in later =) END IF DrawMap 'Update the screen RPGVideoCopy VARSEG(buffer(0)), VARPTR(buffer(0)) fps = fps + 1 'Update FPS counter IF starttime& + 1 < TIMER THEN 'If one second has elapsed then we'll show the FPS fps2 = fps fps = 0 'Set it back to 0 starttime& = TIMER END IF LOCATE 1, 1: PRINT fps2 'Display the frame rate LOOP
Notice that we have put in a FPS counter. This should be removed when we actually make a game. But we'll leave it in for now. I think that was pretty self-explanatory especially with the comments. The code itself is not to hard too understand I think.
Now then, we want to add the MoveXXXX routines to let the player stretch his legs a bit right? Now this is pretty simple in itself. I think we'll make a pixel-by-pixel scroller with Zelda-style free movement! This is actually pretty easy to do excluding the collision detection. Our collision detection is gonna be "map-grid-perfect" which will make it hard to walk through areas with small openings all around. This is easily fixed once we have a multiple keypress handler. So I'm gonna show you the MoveXXXX routines now. Read the comments if you don't understand it:
SUB MoveDown Engine.Direction = South 'We're moving south. 'What we want to do is get the collision status of two parts of the tile below us. Since our engine deals 'with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT(Engine.x \ 16), INT((Engine.y + 16) \ 16)).Collision tile2 = map(INT((Engine.x + 15) \ 16), INT((Engine.y + 16) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.y = Engine.y + 1 'Set to something different for faster movement END IF END SUB SUB MoveLeft Engine.Direction = West 'We're moving west 'What we want to do is get the collision status of two parts of the tile to the west of us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT((Engine.x - 1) \ 16), INT((Engine.y) \ 16)).Collision tile2 = map(INT((Engine.x - 1) \ 16), INT((Engine.y + 15) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.x = Engine.x - 1 'Set to something different for faster movement END IF END SUB SUB MoveRight Engine.Direction = East 'What we want to do is get the collision status of two parts of the tile to the east of us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT((Engine.x + 16) \ 16), INT((Engine.y) \ 16)).Collision tile2 = map(INT((Engine.x + 16) \ 16), INT((Engine.y + 15) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.x = Engine.x + 1 'Set to something different for faster movement END IF END SUB SUB MoveUp Engine.Direction = North 'What we want to do is get the collision status of two parts of the tile to above us. Since our engine 'deals with the actual x and y values not the maps x-grid and y-grid values we need to divide by the tilesize 'which happens to be 16 for this tutorial =) tile = map(INT(Engine.x \ 16), INT((Engine.y - 1) \ 16)).Collision tile2 = map(INT((Engine.x + 15) \ 16), INT((Engine.y - 1) \ 16)).Collision IF tile <> 2 AND tile2 <> 2 THEN 'If the tile is walkable then move the player Engine.y = Engine.y - 1 'Set to something different for faster movement END IF END SUB
Hopefully that isn't to hard to understand. =) Read the comments, they explain it all. Now for the drawing of the map screen. We're gonna have a camera in our RPG which can also be used for cutscenes. Here's the code:
SUB DrawMap CameraX = Engine.x - 160 'Get the upper left map position on the CameraY = Engine.y - 96 'the screen. xtile = INT(CameraX \ 16) 'The the actual map gird position. ytile = INT(CameraY \ 16) IF CameraX < 0 THEN CameraX = 0 'If the Camera pos is less then 0 make it 0 IF CameraY < 0 THEN CameraY = 0 'If the Camera position is greater then the screen boundaries make it equal 'the screen boundaries IF CameraX > Engine.MaxX - 320 THEN CameraX = Engine.MaxX - 320 IF CameraY > Engine.MaxY - 200 THEN CameraY = Engine.MaxY - 200 FOR x = 0 TO 21 '22 Columns FOR y = 0 TO 13 '14 Rows tile = map(x + xtile, y + ytile).tile 'Get layer 1 tile from map buffer tile2 = map(x + xtile, y + ytile).tile2 'layer 2... Xpos = CameraX MOD 16 'Get offset into the screen to Ypos = CameraY MOD 16 'draw at. 'Now draw layer 1 which is not drawn with transparency PutSolid VARSEG(buffer(0)), (x * 16) - Xpos, (y * 16) - Ypos, tilearray(0, tile) 'Now we're gonna draw the second layer IF tile2 <> BlankTileNum THEN 'We don't want to draw a blank tile 'If the tile isn't blank then draw the second layer tile with transparency Put VARSEG(buffer(0)), (x * 16) - Xpos, (y * 16) - Ypos, tilearray(0, tile2) END IF NEXT y NEXT x 'Here is where you draw the NPC's including the player. Xpos = Engine.x - CameraX 'Now get the player's coords Ypos = Engine.y - CameraY 'to draw at on screen PlayerPic = ((Engine.Direction * 2) - 1) 'Get frame to draw player at Put VARSEG(buffer(0)), Xpos, Ypos, PlayerGFX(0, PlayerPic) 'Draw it END SUB
Well that will draw two layers onto the off screen buffer. The routine which copies the buffer to video memory should be an asm routine. For right now to see it work you can do this: change all the VARSEG(buffer(0)) to &HA000, then it'll draw the sprites to video memory. But also you'll notice the put routine used is also from a library! Well, I didn't want to get too library specific so I made up some hypothetical graphics routines. Use DirectQB or Future.Library or something for now...
Lastly I'll show you a map loading routine I use. The map data is stored like this:
Map data (layer 1 tile number, layer 2 tile number, collision number)
It's pretty simple. Throughout this series we'll modify this to accomodate NPC's and other things. Here's my map loading routine:
SUB LoadMap (FileName$) IF INSTR(FileName$, ".") = 0 THEN 'Add a .MAP extension if there FileName$ = FileName$ + ".MAP" 'isn't one END IF OPEN UCASE$(FileName$) FOR INPUT AS #1 'Open it INPUT #1, ImageFile$ 'Read tileset file INPUT #1, PalName$ 'Palette... INPUT #1, StartX, StartY 'Starting coords INPUT #1, Engine.MaxX, Engine.MaxY 'Map size 'Set maximum row and column values to 1 less, since the map buffer starts at 0 not 1 Engine.MaxY = Engine.MaxY - 1 Engine.MaxX = Engine.MaxX - 1 'Load map data into 2 layers FOR i = 0 TO Engine.MaxY FOR J = 0 TO Engine.MaxX INPUT #1, map(J, i).tile, map(J, i).tile2, map(J, i).Collision NEXT J NEXT i 'Calculate the map size in pixels. Engine.MaxX = Engine.MaxX * 16 Engine.MaxY = Engine.MaxY * 16 CLOSE #1 END SUB
The map data could also have been stored in binary too, but regardless of format type, it works. Notice that you don't have to specify an extension on the filename if it ends in .map =) Well that's it for this part of the tutorial and I'd have to say it's pretty big! Next time I'll cover stuff like NPC's and animation. I also might include a demo program. If you don't understand something here then please e-mail me (address is at the top).
Using XMS in QB
This article was written by Fling-master
This tutorial will give you a start to programming routines for the Extended Memory Specification, or XMS. Please note that this is for advanced QB programmers only! I also expect you know some basic assembly. If not than I would suggest not reading this. Don't say I didn't warn you! =) Now that that's out of the way, I can begin.
Is your game, utility, or whatever running out of memory? Do you get all red in the face when QuickBASIC stops your program and displays the "Out of memory" error? If so then XMS is for you. XMS gives you huge amounts of memory which is limited by your computers RAM. This will usually be enough for most QB programs. "Great," you say "So how do I use it then?" Well first you need to find out if your computer has XMS. (I feel sorry for you if it doesn't.) This is determined via interrupt 2Fh. The following example routine will check if it exists, initialize it for use, AND return the version installed:
XMSInit PROC mov ax,4300h ;This function lets us detect XMS int 2Fh ;Use interrupt 2Fh. cmp al,80h ;If AL isn't 80h then XMS is not present! jne NoXMS mov ax,4310h ;Since XMS is present lets get the version number int 2Fh ;and the address of the control function. mov word ptr [XMSControl],bx ;XMSControl must be a DWORD. mov word ptr [XMSControl+2],es xor ax,ax ;Now run function 00h which returns the version. call [XMSControl] ;Call the XMS Control function. ret NoXMS: mov ax,-1 ;If there's no XMS driver we return a -1 to QB. ret ENDP XMSInit
If you found that confusing the read the comments carefully. There's a funny thing about the version number returned. If you try to print it, it won't be the right number. Use this code to fix that:
XMSversion$ = str$(version% / &H100)+"."+str$(version% AND &HFF)
That will return the version as a string. version% would be the number returned by the above function. If you don't have version 3.0 or later than these functions I'm showing you might not work.
Now wouldn't you like to know how much memory is available to you? Well there's a function for that. Function 08h. It returns the largest free block in kb into AX, and the total free memory in kb into DX. Here's an example routine:
XMSInfo PROC freemem:WORD, totalmem:WORD mov ah,08h ;Use function 08h call [XMSControl] ;Call the API mov bx,totalmem ;Put the value in DX in totalmem. mov [bx],dx mov bx,freemem ;Put the value of AX in freemem mov [bx],ax ret ENDP XMSInfo
That will return the available memory in freemem and the total memory in totalmem. One thing about the above routine, it won't show an amount greater than 64MB. There is a function which will but I'm not showing you it now. I want you first to understand how it all works. Next issue you'll see. =P
Ok now, to actually use XMS you have to allocate a block of it. That is, you have to tell the computer that you want x amount of XMS for use in your program. We can do that via function 09h. With this routine it is not possible to allocate over 64MB or memory. There is a routine for it but I'll show you it next issue. Before you call the routine you have to have the amount of kb you want to allocate in DX. After you call it AX will be 0001h if it was successful. If not 0000h is returned. DX will also return the 16-bit handle of the allocated block. "Handle? What the heck!?" The handle you get is your "password" to accessing the block of memory. So when you allocate memory make sure that you keep the handle! There is no way to get it back and worse, you can't deallocate the block without it! Then other programs won't be able to use that block until you restart your computer! So before I get ahead of myself, here's an example routine to allocate a block:
XMSAlloc PROC kb:WORD mov ah,09h ;Use function 09h mov dx,kb ;Move kb into DX. call [XMSControl] ;Call the API cmp ax,0001h ;Is AX = 0001h? If it is then it worked! jne AllocErr ;If it isn't then . . . too bad! =P mov ax,dx ;Move the handle into AX so its returned. ret AllocErr: ;Since we can't get a handle if it didn't work we don't ret ;want DX to get into AX. ENDP XMSAlloc
There ya go! That will allocate up to 64MB. So lets find out how to deallocate a block of memory.
Deallocating isn't that hard. It basically works the same way. We need to make use of function 0Ah for it. DX must hold the handle of the allocated block to be freed up. After called AX will have 0001h in it if successful. If not the its 0000h. Here's the code:
XMSDeAlloc PROC handle:WORD mov ah,0Ah ;Use function 0Ah mov bx,handle ;Put the handle into DX mov dx,[bx] call [XMSControl] ;Call the API ret ENDP XMSDeAlloc
Pretty simple ain't it? Now if used as a function it'll return 0001h if successful.
Now you've probably asked yourself: "How do I know what error occurs?" Well every XMS function will return an error code into BL if an error happened. There's quite a few error codes so I'll put 'em in a nice table for you all to see!
|80h||Function not implemented||92h||DX less than /HMAMIN= parameter||A4h||SOffset is invalid||ABh||Block is locked|
|81h||VDISK device detected||93h||HMA not allocated||A5h||DHandle is invalid||ACh||Block lock count overflow|
|82h||A20 error occured||94h||A20 line still enabled||A6h||DOffset is invalid||ADh||Lock failed|
|8Eh||General driver error occured||A0h||All XMS allocated||A7h||Len is invalid||B0h||Smaller UMB is available|
|8Fh||Unrecoverable driver error occured||A1h||All available XMS handles in use||A8h||Move has an invalid overlap||B1h||No UMBs are available|
|90h||HMA does not exist||A2h||Handle is invalid||A9h||A parity error occurs||B2h||UMB segment number is invalid|
|91h||HMA already in use||A3h||SHandle is invalid||AAh||Block is not locked|
Some of those errors won't apply here so you can ignore them. (Like the ones dealing with the A20 line.) What you could do is make the routine check if an error occured and then it could return that to QB. Although you don't have to.
Alright, now comes moving between XMS and QB. To do this you have to define a data structure at the beginning of your assembly source file like the one below:
MoveXMS struc ;Our main XMS data structure Len dd ? ;number of bytes to transfer SHandle dw ? ;handle of the source block SOffset dd ? ;offset into the source block DHandle dw ? ;handle of the destination block DOffset dd ? ;offset into the destination block MoveXMS ends XMSmove MoveXMS ? ;This makes it so we can use it =)
Using this structure we'll be able to specify what variable you want to move from QB to XMS and vice-versa. The best way to explain this I think is with an example. This sample routine will let you move data from QB to XMS:
XMSPut proc handle:WORD, segm:WORD, offs:WORD, bytes:DWORD, XMSoffs:DWORD mov eax,bytes ;Get the length of the data to move into Len mov XMSmove.Len,eax mov XMSmove.SHandle,0000h ;SHandle must be set to 0000h to move TO XMS. mov bx,offs ;Now we move the offset of the variable to SOffset. mov word ptr [XMSmove.SOffset],bx mov bx,segm ;Then the segment goes to SOffset+2 mov word ptr [XMSmove.SOffset+2],bx mov bx,handle ;Now we must move the destination handle. This is the mov XMSmove.DHandle,bx ;handle which we're moving the data to. mov eax,XMSoffs ;This is the location in the specified handle to move mov XMSmove.DOffset,eax ;the data to. mov si,Offset XMSmove ;Now we get the offset address of the data structure into SI mov ah,0bh ;And make sure that we call function 0Bh call [XMSControl] ;Now call the XMS driver to move the data. ret endp XMSPut
There. That will correctly move data from a variable, array, whatever in QB to the specified XMS handle. DON'T use this function unless you've allocated a block! If the move was successful AX will return 0001h or else it returns 0000h. I would make this routine a function so that you can tell from QB if it was successful or not. If that was too complicated for you than just go over it again slowly until you get it. Now, how do we move data from XMS to a variable in QB? Well we really only have to change a little bit of the above code:
XMSGet proc handle:WORD, segm:WORD, offs:WORD, bytes:DWORD, XMSoffs:DWORD mov eax,bytes ;Get the length to move into Len. mov XMSmove.Len,eax mov bx,handle ;Get the handle of the XMS block with the wanted data into mov XMSmove.SHandle,bx ;SHandle as XMS is now the source. mov eax,XMSoffs ;Also get the offset into the handle into SOffset. mov XMSmove.SOffset,eax mov XMSmove.DHandle,0000h ;DHandle must be 0000h so that we can pass the segment and ;offset of a QB variable to DOffset. mov bx,offs ;Get the offset of the QB variable. mov word ptr [XMSmove.DOffset],bx mov bx,segm ;And the segment. mov word ptr [XMSmove.DOffset+2],bx mov si, Offset XMSmove ;Now get the offset of the data structure into SI. mov ah,0bh ;Use function 0Bh call [XMSControl] ;Call the API! ret endp XMSGet
That will return the same as the other function into AX depending on what happened. As you probably saw we just used function 0Bh like before just we switched some things around. Not that hard, eh? Now all that's left for me to show you this issue is how to Reallocate a XMS block. This is handy if you need more memory for your game or something, I really don't know =). To reallocate a block you call subfunction 0Fh. Heres the commented code:
XMSReAlloc proc handle:WORD, kb:WORD mov ah,0Fh ;Use subfunction 0Fh mov bx,handle ;Get the handle to deallocate into DX. mov dx,[bx] mov bx,kb ;Get the new amount to resize the handle to into BX. call [XMSControl] ;Call the API. mov bx,handle ;Return the handle to QB. mov [bx],ax endp XMSReAlloc
As you can see it's pretty similar to the allocation routine. This like the allocation routine will only allocate up to 64MB. There is a routine that'll allow more (up to 4GB) but I want you to understand these first before you dive in. And thats all I have to say for this issue. Next issue I'll show you the other allocation routines and we'll combine this into a lib you can use. If you still don't understand any of this than don't hesitate to e-mail me!
Only me Fling-master, the editor, this month =)
Well that's it for this issue! I hope to hear from you on what you thought about this issue here. I'll put a few responses provided I get any, in issue #2. Next time you can expect part two of RPG Creation 101, part two of the XMS article, and maybe a tutorial on using the palette, among other articles. The next issue should be out in a month or so. See ya then!
|All site content is © Copyright 2001, HyperRealistic Games. This excludes content submitted by other people.|