English Amiga Board

English Amiga Board (http://eab.abime.net/index.php)
-   Coders. Asm / Hardware (http://eab.abime.net/forumdisplay.php?f=112)
-   -   Game Programming Framework (http://eab.abime.net/showthread.php?t=99244)

mcgeezer 17 October 2019 23:07

Game Programming Framework
 
So after a fair bit of thought on what to do next, I've long set my heart on doing some Game programming tutorial streams for the Amiga.

With that said, looking back I made many many mistakes when coding Rygar... things that were just bad habits or because I was under a time constraint to finish the game for the competition.

So what I'd like to do is come up with a set of assembler functions that facilitate making game coding easier, while the implementation will be assembler I'll try and comform to high level languages like C (although I've never written any C before).

So here goes... I've documented (but not written) a couple of functions to set the tone, the idea being writing the actual function will be the assembler tutorial and the hope is that a picture of how to build a game will slowly come across to following...with better games being made for the Amiga.

Here are the off the cuff high level functions (just a brain dump)...I have more to add.

Code:


[+] rawHandle = amgLoadAndUnpackRncAsset(*name, bufsize, memtype)
[+] assetHandle = amgParseAsset(rawHandle)

[+] cameraHandle = amgCreateScreenBuffer(XSize, YSize, BitPlanes)
        Allocate screen memory, Creates Bitplane Handles
       
[+] copperHandle = amgCreateInitalCopperList(cameraHandle,paletteHandle)
        Build a default copper list from a camera handle.
       
[+] spriteSheetHandle = amgGenerateSpriteSheet(assetHandle, *Buffer, SpriteCutSize, MaxCuts, bool Mask)
        Build a list of pointers to the sprites and their masks       
       
[+] spriteHandle = amgCreateSprite(spriteSheetHandle)
[+] result = amgSetSpriteHandler(spriteHandle, *Address, State)
[+] result = amgSetPlayerSprite(spriteHandle *Address)
[+] result = amgDestroySprite(spriteHandle)       

[+] spriteAnimHandle = amgCreateSpriteAnimation(spriteHandle, spriteAnimationType)
[+] result = amgAppendSpriteAnimationFrame(spriteAnimHandle,*Handler, Frame, Speed)
[+] result = amgSetSpriteAnimation(spriteAnimHandle, bool [loop|term])

[+] SpriteNumber = amgCastSprite(spriteHandle,initXpos,initYpos,State)
[+] result = amgSetSpriteStatus(spriteHandle, bool State)
[+] result = amgSetSpriteAnimationSpeed(spriteAnimHandle)

[+] tileHandle = amgSetTileSheetAttributes = (assetHandle, sizeX, size Y)
[+] result = amgSetTilePlatform(tileId,tileHandle,spriteHandle,maskId)
[+] result = amgSetTileObstacle(tileId,tileHandle,spriteHandle,maskId)
[+] result = amgSetTileCeiling(tileId,tileHandle,spriteHandle,maskId)

[+] mapHandle  = amgCreateTileMap(rawHandle,xSize,ySize,size)

[+] canvasHandle = amgCreateCanvas(mapHandle, tileHandle)
[+] result = amgSetCanvasLimits(canvasHandle,x1,y1,x2,y2)
[+] result = amgSetCanvasPlayerLimits(canvasHandle,spriteHandle,x1,y1,x2,y2)

[+] amgBlitTileToCamera(canvasHandle,cameraHandle, sourceTile, destTile)
[+] copperList = amgDrawCurrentCameraPosition(canvasHandle,cameraHandle)

[+] copperListHandle = amgSetCanvasMode(canvasHandle,mode)
                        Mode can be:
                                0 = Static
                                1 = Horizontal Bi-Directional
                                2 = Horizontal Right
                                3 = Horizontal Left
                                4 = Vertical Bi-Directional
                                5 = Vertical Down
                                6 = Vertical Up
                                7 = 8 Way
                                10 = Dual Playfield Static
                                11 = Dual Playfield Horizontal Bi-Directional
                                12 = Dual Playfield Horizontal Right
                                13 = Dual Playfield Horizontal Left
                                14 = Dual Playfield Vertical Bi-Directional
                                15 = Dual Playfield Vertical Down
                                16 = Dual Playfield Vertical Up
                                17 = Dual Playfield 8 Way
                               

[+] amgSetCanvasCameraPosition(canvasHandle,cameraHandle,x1,y1,x2,y2)                              ; Can use this to scroll around


And here's the first two functions documented:

Code:


NAME
        amgLoadAndUnpackRncAsset - Load and unpack file


SYNOPSIS
        rawHandle = amgLoadAndUnpackRncAsset(*name, bufsize, memtype)
        d0                                  a0    d0      d1


FUNCTION
        The file 'name' should be a ProPacked file using method 1 or 2.
        Memory of the required type and bufsize in bytes is allocated using AmigaDos
        malloc(), If the supplied bufsize is 0 then the packed file header is read to find
        the number of first before loading and unpacking the file.
        The memtype argument can either by MEMF_CHIP to allocate chip ram, MEMF_FAST to
        allocate Fast Ram or MEMF_ANY to allocate any available ram type, however should
        fast ram be available as well as chip then fast will be used as a priority.


INPUTS
        name - (a0) pointer to null-terminated file name of asset to load.

        bufsize - (d0) buffer size in bytes to allocate

        memtype - (d1) MEMF_CHIP | MEMF_FAST | MEMF_ANY


RESULTS
        rawHandle - (d0) handle code for the loaded asset


ERRORS
        error - (d0) failure to load and unpack the asset will result in any of the following
                                  errors.

                        -1 - File not found
                        -2 - Unable to allocate ram type
                        -3 - Unpack failed
                        -4 - Max handles reached


Code:

NAME
        amgParseAsset - Parse and store the information found in a loaded asset


SYNOPSIS
        assetHandle = amgParseAsset(rawHandle)
        d0                          d0


FUNCTION
        The handle supplied should be the result from calling the 'amgLoadAndUnpackRncAsset'
        function.  The supported assets are Protracker Music Modules, 8SVX Sound samples and
        uncompressed Amiga ILBM image files.  Upon successful execution of the function
        a new handle is returned with the requested information parsed based on the found
        asset type.  See the AMG handles structure for more information.


INPUTS
        handle (d0) - A handle number that has previously been returned from
                                  'amgLoadAndUnpackRncAsset'.


RESULTS
        assetHandle (d0) - Asset handle number.

ERRORS
        error - (d0) failure to parse information found with the associated handle.

                        -1 - Supplied handle not found
                        -2 - No supported Interchangable File Format header could be found (FORM)
                        -3 - Failed to locate Bitmap Header (BMHD)
                        -4 - Failed to locate Body Header (BODY)
                        -5 - Failed to locate Colour Map header (CMAP)
                        -6 - Asset is compressed
                        -7 - Failed to locate Sample header (8SVX)
                        -8 - Failed to locate Module header (MOD)

SEE ALSO

And here's the handle structures...

Code:

Struct handles
        STATUS                  X      byte    (-1 - invlaid, 0 - valid)                                      * AmgLoadAndUnpackRncAsset()
        ADDRESS                X      long    Pointer in ram to asset                                        * amgLoadAndUnpackRncAsset()
        TYPE                    X      byte    (-1 - undefined,                                                * amgParseAsset()
                                                  0 - IFF Image,
                                                  1 - Music Module,
                                                  2 - Sound Sample )   
        IMGIFFSIZE              X      word    Size in bytes of entire                                        * amgParseAsset()
        IMGBODYSIZE            X      long    Size in bytes of body                                          * amgParseAsset()
        IMGCMAPSIZE            X      long    Size in bytes of colour map                                    * amgParseAsset()
        IMGBODYPTR              X      long    Pointer to body data origin                                    * amgParseAsset()
        IMGCMAPPTR              X      long    Pointer to cmap data origin                                    * amgParseAsset()
        IMGCMAPTYPE            X      byte    Type of colour map 0=12bit/1=24bit                            * amgParseAsset()
        IMGWIDTH                X      word    Size in pixels of image width                                  * amgParseAsset()
        IMGDEPTH                X      word    Size in pixels of image depth                                  * amgParseAsset()
        IMGPLANESIZE            X      byte    Number of bitplanes in the image                              * amgParseAsset()
        IMGBYTEWIDTH            X      byte    Number of bytes in width of the image                          * amgParseAsset()
        IMGMODULO              X      byte    Image modulo                                                  * amgParseAsset()
        8SXIFFSIZE              X      word    Size in bytes of entire asset                                  * amgParseAsset()
        8SXBODYSIZE            X      long    Size in bytes of body data                                    * amgParseAsset()
        8SXPERIOD              X      word    Sample period                                                  * amgSetAssetData()


result = amgSetAssetData(handle, offset, data)
d0                        d0    d1    d2

result = amgGetAssetData(handle, offset)
d0                        d0    d1

Index to handle data
*ptr = amgIndexToHandle(handle)
  a0


So the hope is that I can build on this. As I have a strong Unix/Shell/Python background I tend to operate everything on file handles and OOP so I'm hoping to take those principles into this.

This project might not go very far... but then again, it might really be useful.
It's certainly something I can take at a much slower pace than coding something like Rygar.

Ideas and comments as usual are really welcome.

Geezer

Spec-Chum 17 October 2019 23:42

Awesome!

You probably don't remember but I asked you about this on Facebook few weeks ago, so glad it looks like it's going ahead!

tolkien 18 October 2019 00:15

great! I tick this thread to favourites!

Spec-Chum 18 October 2019 00:23

Quote:

Originally Posted by mcgeezer (Post 1352129)
(although I've never written any C before).

I really doubt a man of your skill will have much issues mate, if I'm honest.

C can be made very asm like, indeed that was what it was designed for, as an almost 1 to 1 correlation with asm.

As an example I've decided to code Scoopex's tutorials into C, so I'm not just copying and pasting. Here's tut4. You can most probably guess the code this produces, as it's virtually identical to Photon's source.

Code:

#include <exec/types.h>

#define VPOSR 0xdff004
#define VHPOSR 0xdff006
#define INTENAR 0xdff01c
#define INTENA 0xdff09a
#define COLOR00 0xdff180
#define CIAPRAA 0xbfe001

int main()
{
        volatile UBYTE *vhposr = (UBYTE *)VHPOSR;
        volatile UBYTE *vposr_lo = (UBYTE *)VPOSR + 1;        // point to low byte
        UWORD *color00 = (UWORD *)COLOR00;
        UWORD *intena = (UWORD *)INTENA;

        // line starting position
        UBYTE yPos = 0xac;

        // line direction
        BYTE yDir = 1;

        // save interrupts
        UWORD oldInt = *(UWORD *)INTENAR;

        // disable all interrupts
        *intena = 0x7fff;

        // do main loop until mouse pressed
        while(*(volatile UBYTE *)CIAPRAA & 64)
        {
                // wait for start of frame
                if((*vposr_lo & 1) == 0 && *vhposr == 0x2c)
                {
                        *color00 = 0;        // bg black

                        // update line y position
                        yPos += yDir;

                        // bounce line
                        if(yPos > 0xf0 || yPos < 0x40)
                        {
                                yDir = -yDir;
                        }

                        // do literally nothing until we reach yPos
                        while(*vhposr != yPos)
                        {}
                        *color00 = 0xfff;        // bg white

                        // now do nothing until we're not on yPos
                        while(*vhposr == yPos)
                        {}
                        *color00 = 0x116;        // bg blue
                }
        }

        // restore interrupts
        *intena = oldInt | 0xc000;       
       
    return 0;
}

On the other side of the coin, you can use the headers to (to some) make your life a little easier, here's my asm takeover code I converted to C:

Code:

#include <proto/exec.h>
#include <proto/dos.h>
#include <proto/graphics.h>
#include <exec/execbase.h>
#include <hardware/cia.h>
#include <hardware/custom.h>
#include <graphics/gfxbase.h>

extern struct GfxBase *GfxBase;
extern struct Custom custom;
extern struct CIA ciaa;

struct View *OldView;
static UWORD oldInt;
static UWORD oldDMA;

__chip UWORD copperlist[] =
{
        0x0106, 0x0000,    // AGA - BPLCON3
        0x01fc, 0x0000,    // AGA - Slow FMODE

            0x0180, 0x0f00,                // Colour00 = red
        0x8001, 0xff00,    // wait until VPos = 0x80
        0x0180, 0x0fff,    // Colour00 = white
        0xd001, 0xff00,                // wait until VPos 0xd0
        0x0180, 0x000f,        // Colour00 = blue
               
                0xffff, 0xfffe                // end list
};

int main()
{
        // Save original view
        OldView = GfxBase->ActiView;

        // Set clean view
        LoadView(NULL);
        WaitTOF();
        WaitTOF();

        // Save interrupts and DMA
        oldInt = custom.intenar;
        oldDMA = custom.dmaconr;

        // disable all interrupts
        custom.intena = 0x7fff;

        // disable all DMA
        custom.dmacon = 0x7fff;

        // set cl1 to our copper
        custom.cop1lc = (ULONG)copperlist;
       
        // enable selected DMA
        custom.dmacon = 0x8280;        // No Blitter or sprites, only copper

        // loop until mouse clicked
        while(ciaa.ciapra & CIAF_GAMEPORT0)
        {
                // do nothing
        }

        // restore interrupts and DMA
        custom.dmacon = oldDMA | 0x8000;
        custom.intena = oldInt | 0x8000;

        // restore orginal copper
        custom.cop1lc = (ULONG)GfxBase->copinit;

        // restore old view
        LoadView(OldView);
        WaitTOF();
        WaitTOF();

        return 0;
}


mcgeezer 21 October 2019 00:57

So I've implemeted a couple of these functions now and added another few.

The function added is amgUnloadAsset() - it's pretty simple.

Code:

NAME
        amgUnloadAsset - Release handle and ram held by an asset


SYNOPSIS
        amgUnloadAsset(assetHandle)
                        d0


FUNCTION
        The specified handle is marked with a status of unused within its structure.
        The address and length pointers of the handle are left in tact, these are
        subsequently used to deallocate the ram of the asset using LVOFreeMem().


INPUTS
        handle - (d0) A previously allocated handle using amgLoadAndUnpackRncAsset().



RESULTS
        NULL


ERRORS


As I've implemented the functions I can post them here.

We have six files in total

main.asm - this has the main calling program.
loader.asm/loader.dat - this is what does the load header, allocate ram, read packed file, unpack file.
handle.asm/handle.dat - this contains the code for managing handles
unpack.asm - RNC unpacker


The main program looks like this, it simply reads the header of an RNC packed file, reads the unpacked size from the header, allocates that amount of ram, loads and unpacks the whole file to that allocated ram, allocates a handle for that asset that has been loaded....then... deallocates the handle and frees ram. This is pretty much a key aspect into managing assets in an Amiga game.

Code:

ExecBase:                      equ    4

        cseg

        basereg data,a4
        near    a4

CODE_BASE:

        bra        MAIN

        CNOP        0,4
               
        include i/custom.i              ; Custom Register definitions
        include i/cia.i                ; CIA Hardware definitions
        include 'exec/types.i'
        include 'exec/exec.i'
        include 'exec/exec_lib.i'
        include 'libraries/dos.i'
        include 'libraries/dos_lib.i'
        include 'hardware/blit.i'
        include 'hardware/dmabits.i'       
        include s/unpack.asm
        include s/loader.asm
        include s/handle.asm
       
       
MAIN:
; Reserve a4 and a5 for global pointers.
        lea        data,a4                                       
        lea        CHIPBASE,a5                               

; Open the DOS library
        move.l  ExecBase,a6                                ; handle[d0] = OpenLibrary=(version[d0],libname[a1])
        lea    DOSNAME(a4),a1
        moveq  #0,d0
        jsr    _LVOOpenLibrary(a6)
        move.l        d0,DOSBASE(a4)                                ; save DOSBASE for file reading

        lea        asset_lower(a4),a0                        ; rawHandle = amgLoadAndUnpackRncAsset(*name[a0], memtype[d1])
        moveq        #0,d0
        moveq        #MEMF_CHIP,d1
        bsr        amgLoadAndUnpackRncAsset
; d0 has allocated handle number

; now lets unload the asset from memory.
        bsr        amgUnloadAsset                                ; amgUnloadUnpackedAsset(handle[d0])
       
        move.l  ExecBase,a6                                ; Close the DOS library.
        move.l  DOSBASE(a4),a1
        jsr    _LVOCloseLibrary(a6)

; End program
        moveq  #0,d0
        rts
       
; Data section here.

        dseg
       

dats:
        include s/loader.dat
        include s/handle.dat

DOSBASE:        dc.l        0
DOSNAME:        dc.b        'dos.library',0
                even

asset_lower:        dc.b        "dh1:Rygar/ry_lower.rnc",0                ; Packed RNC file to load.
                even

data:


loader.asm looks like this...

Code:


mode_old  =1005
mode_new  =1006
       
; rawHandle = amgLoadAndUnpackRncAsset(*name, memtype)
;    d0                                  a0    d1

amgLoadAndUnpackRncAsset:
        movem.l        d1-d7/a0-a1,-(a7)                ; Save Registers
        move.l        d1,d5                                ; Save Requested Memory Type
        move.l        a0,d6                                ; Save filename pointer (we'll be needing it twice)
       
; Open the file for reading
        move.l        a0,d1                                ; handle[d0] = LVOOpenFile(filename[d1],mode[d2])
        move.l  DOSBASE(a4),a6     
        move.l  #mode_old,d2
        jsr    _LVOOpen(a6)                       
        tst.l        d0                               
        bmi        .open_error
       
; Read the RNC header file
        move.l        d0,d4                                ; Save handle for LVOClose
       
        lea        amgRncHeaderBuffer(a4),a0        ; Buffer to read RNC header bytes
        move.l        a0,d2                                ; bytes[d0] = LVORead(handle[d1],buffer[d2],size[d3])               
        moveq        #20,d3                               
        jsr    _LVORead(a6)                       
        tst.l        d0                                ; Was there an error?
        bmi        .header_error
        cmp.l        d0,d3                                ; Did we read the correct number of bytes?
        bne        .read_error
; Close the file
        move.l        d4,d1                                ; result = LVOClose(handle[d1])
        move.l  DOSBASE(a4),a6             
        jsr    _LVOClose(a6)       
       
; Check the RNC header
        lea        amgRncHeaderBuffer(a4),a0        ; Get pointer to the read header
        move.l        (a0),d0
        lsr.l        #8,d0
        cmp.l        #"RNC",d0                        ; Is it RNC?
        bne        .rnc_error

; Allocate ram based on RNC header       
        move.l        4(a0),d0                        ; address[d0] = LVOAllocMem(buffsize[d0],MEM_TYPE[d1])
        move.l        d5,d1                               
        move.l  ExecBase,a6
        jsr    _LVOAllocMem(a6)
        tst.l        d0
        bmi.s        .alloc_error
       
        move.l        d0,d4                                ; Save the allocated buffer origin
       
; Open the file again and read it.
        move.l        d6,d1                                ; handle[d0] = LVOOpenFile(filename[d1],mode[d2])
        move.l  DOSBASE(a4),a6     
        move.l  #mode_old,d2
        jsr    _LVOOpen(a6)                       
        tst.l        d0
        bmi.s        .open_error
        move.l        d0,d5                                ; Save handle for read and close.
       
       
; Read the entire file
        move.l        d4,d2                                ; bytes[d0] = LVORead(handle[d1],buffer[d2],size[d3])
        move.l        #$ffffff,d3               
        jsr    _LVORead(a6)               
        tst.l        d0
        bmi.s        .read_error
       
; Close the file
        move.l        d5,d1                                ; result = LVOClose(handle[d1])
        move.l  DOSBASE(a4),a6     
        jsr    _LVOClose(a6)       

; Allocate a new handle here for the asset that was just read.
        bsr        amgAllocateHandle                ; assetHandle[d0], handleOrigin[a0] = amgAllocateHandle()
        tst.l        d0
        bmi.s        .handle_error
        move.l        d0,d1                                ; Save handle
       
        move.l        d2,a1
        move.l        d4,HANDLE_STRUCT_ADDRESS(a0)        ; Save the address of the asset in ram in the handle
        move.l        4(a1),HANDLE_STRUCT_LENGTH(a0)        ; Save the length of the asset in ram in the handle
       
        move.l        a1,a0                                ; UnpackSize[d0] = Unpack[source[a0],dest[a0]
        bsr        Unpack
       
; Verify the unpack size against what was in the header
        lea        amgHandlesStruct(a4),a0
        lsl.w        #4,d1
        cmp.l        HANDLE_STRUCT_LENGTH(a0,d1),d0
        bne.s        .unpack_error
       
        move.l        d1,d0
       


        bra        .exit                                ; All done.
                       
.open_error:
        moveq        #ERROR_HANDLE_FILE_OPEN,d0
        bra.s        .exit
.header_error:
        moveq        #ERROR_HANDLE_HEADER_NOT_FOUND,d0
        bra.s        .exit
.alloc_error:
        moveq        #ERROR_HANDLE_ALLOCATE_FAIL,d0
        bra.s        .exit
.read_error:
        moveq        #ERROR_HANDLE_FILE_READ,d0
        bra.s        .exit
.rnc_error:
        moveq        #ERROR_HANDLE_RNC,d0
        bra.s        .exit
.handle_error:
        moveq        #ERROR_HANDLE_GENERIC,d0
        bra.s        .exit
.unpack_error:
        moveq        #ERROR_HANDLE_UNPACK,d0
        bra.s        .exit
        nop
.exit:       
        movem.l        (a7)+,d1-d7/a0-a1
        rts
       



; d0=handle number
amgUnloadAsset:
; Now lets release the handle and the memory we just allocated.
        lsr.w        #4,d0
        bsr        amgReleaseHandle                ; handleOrigin[a0] = amgReleaseHandle(assetHandle[d0])

        move.l        HANDLE_STRUCT_ADDRESS(a0),a1
        move.l        HANDLE_STRUCT_LENGTH(a0),d1
        move.l  ExecBase,a6
        jsr    _LVOFreeMem(a6)
        rts

loader.dat looks like this

Code:


                        CNOP        0,4

amgRncHeaderBuffer:       
                        ds.w        20

                        CNOP        0,4


handle.asm looks like this...

Code:

maxHandles:                        equ        20               

; Error return codes for handles.
ERROR_HANDLE_FILE_OPEN:                equ        -1
ERROR_HANDLE_HEADER_NOT_FOUND:        equ        -2
ERROR_HANDLE_ALLOCATE_FAIL:        equ        -3
ERROR_HANDLE_FILE_READ:                equ        -4
ERROR_HANDLE_RNC:                equ        -5
ERROR_HANDLE_GENERIC:                equ        -6
ERROR_HANDLE_UNPACK:                equ        -7


; Handle structure indexes
HANDLE_STRUCT_STATUS:                equ        0
HANDLE_STRUCT_TYPE:                equ        1
HANDLE_STRUCT_ADDRESS:                equ        2
HANDLE_STRUCT_LENGTH:                equ        6

; OUTPUTS:
; Returns new handle number in d0
; Returns pointer to handle structure in a0
amgAllocateHandle:
        move.l        d7,-(a7)
        lea        amgHandleAllocs(a4),a0
        moveq        #0,d0
        moveq        #maxHandles-1,d7
.loop:        tst.w        (a0)+
        bmi.s        .found
        addq.w        #1,d0
        dbf        d7,.loop
        moveq        #-1,d0
        bra.s        .exit
       
.found:
        subq.w        #2,a0
        move.w        d0,(a0)
.exit:
        move.l        d0,d7
        lsl.w        #4,d7
        lea        amgHandlesStruct(a4),a0
        add.l        d7,a0
        move.b        d0,(a0)

        move.l        (a7)+,d7
        rts
       

; Frees up a handle previously allocated with amgAllocateHandle()
; INPUT [d0] = handle to release       
amgReleaseHandle:
        lea        amgHandleAllocs(a4),a0
        move.w        #-1,(a0,d0*2)

        lea        amgHandlesStruct(a4),a0
        lsl.w        #4,d0
        add.l        d0,a0
        move.w        #-1,(a0)
        rts

and handle.dat like this...

Code:


amgHandleCurrent:        dc.w        0                        ; Current handle pointer

amgHandleAllocs:       
                        rept        maxHandles
                        dc.w        -1
                        endr
                       
amgHandlesStruct:
                        rept        maxHandles
                        dc.b        -1                        ; Status -1/0
                        dc.b        -1                        ; Type (-1 = unallocated)
                        dc.l        0                        ; Address
                        dc.l        0                        ; Length
                        dc.b        "HANDLE"                ; Padding.
                        endr


Also included in the archive I have uploaded is the RNC unpack code.

And a link to the source code is here.

For those who want to follow along I will do a video showing setting up the tool chain and debugging the source here, feel free though to give it a try yourself if you want to race ahead.

http://109.228.4.199/downloads/Tutorial%201.zip


All times are GMT +2. The time now is 10:00.

Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2020, vBulletin Solutions Inc.

Page generated in 0.04869 seconds with 11 queries