Jump to content
IGNORED

Player fine positioning issues & questions


Recommended Posts

 

Hi,

 

I'm looking into setting position for player objects and I used the sethorizpos subroutine but it's not behaving as I expected close to both boundaries. 

See code below. With the joystick you can move left or right and for some reason it's jumping when < 14 and > 135.

 

Related questions that might explain some of this:

  • What happens when you strobe RESP while in HBLANK? TIA notes you can only set it in the visible area but then how would this work for small X numbers? If only in visible area, how could you position P0/P1 at position 1 since the STA RESP takes already 9 + 5 color clocks? The offset is max -7 so then minimum position is really 7 (which is still ok with HMOVE)?
  • Can you position P0/P1 at position 152? Seems you would run out of color clocks for the scanline and you get an extra scanline. Or is there a trick?
  • What is the best place to reset HMCLR? I think it needs to be reset before the next scanline but also cannot happen to soon after HMOVE. I added lots of NOP but is there a better way?

 

Thanks!

Lucien

 

    processor 6502
    include "vcs.h"
    include "macro.h"

    seg.u Variables
    org  $80

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Variables and Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Player0_X       byte
Player0_X_Fine  byte
OVERSCAN_LINES  equ 30
X_Fine_Res      equ 10


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    seg Code
    org $f000

Start
    CLEAN_START
    lda #20
    sta Player0_X
    lda #1
    sta Player0_X_Fine

   ; -----------------------------------------------
NextFrame
	VERTICAL_SYNC  ;  3 lines

; -----------------------------------------------
; 0. Vertical Blank Area. Add game logic here
; -----------------------------------------------
    ldx #36
    lda #$0      ; mask last line when we turn on again
WaitVBlank
    sta WSYNC
    sta COLUBK
    dex
    bne WaitVBlank
    lda #0
    sta VBLANK    ;
; we are now at scanline = 36, pixel pos = -32 (next kernel will close this scanline)

Kernel1_SetX
; the next two scanlines position the player horizontally
	lda Player0_X 	; get X coordinate
        ldx #0		; player 0
        jsr SetHorizPos	; set coarse offset
        sta WSYNC	; sync w/ scanline
        sta HMOVE	; apply fine offsets

        lda #$0E
        sta COLUBK
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        sta HMCLR       ; reset fine position
; we are now at scanline = 38

Kernel2_DrawP0
        ldy #8
P0Loop
        lda P0_Bitmap-1,y	; lookup color
        sta WSYNC	; sync w/ scanline
        sta HMOVE	; apply fine offsets
        sta GRP0	; store bitmap
        lda P0_Color-1,y ; lookup color
        sta COLUP0	; store color
        dey		; decrement X
        bne P0Loop
; we are now at scanline = 46

Kernel3_Setup
    ldx #228-46
Kernel3_Loop
    sta WSYNC
    sta HMOVE
    dex
    bne Kernel3_Loop

JoystickMovePlayer
    sec
    ldx Player0_X_Fine
    bit SWCHA
    bvs .CheckRight
    dex
    bmi .DecPlayer0_X
    jmp .CheckRight
.DecPlayer0_X
    dec Player0_X
    ldx #X_Fine_Res-1

.CheckRight
    stx Player0_X_Fine

    bit SWCHA
    bmi .SkipMoveRight
    clc
    inx
    cpx #X_Fine_Res
    beq .IncrPlayer0_X
    jmp .SkipMoveRight
.IncrPlayer0_X
    inc Player0_X
    ldx #0
.SkipMoveRight
    stx Player0_X_Fine    

    ;inc Player0_X
    ;lda Player0_X
    ;cmp #120
    ;beq resetPlayer0_X
    jmp OverScanArea
resetPlayer0_X
    lda #0
    sta Player0_X

OverScanArea
; -----------------------------------------------
; Overscan area. Add game logic here
; -----------------------------------------------
    sta WSYNC     ; close previous kernel
    
    lda #2        ; turn on VBLANK
    sta VBLANK

    ldx #OVERSCAN_LINES
NextOverscanLine
    sta WSYNC
    dex
    bne NextOverscanLine
    jmp NextFrame

; -----------------------------------------------
; SUBROUTINE SECTION
; -----------------------------------------------
; SetHorizPos - Sets the horizontal position of an object.
; The X register contains the index of the desired object:
;  X=0: player 0
;  X=1: player 1
;  X=2: missile 0
;  X=3: missile 1
;  X=4: ball
; This routine does a WSYNC and HMOVE before executing,
; so whatever you do here will not take effect until you
; call the routine again or do your own WSYNC and HMOVE.
SetHorizPos
    sta WSYNC
    sta HMOVE
    sec
SetHorizPosLoop
    sbc #15
    bcs SetHorizPosLoop
    eor #7         ; calculate fine offset
    asl
    asl
    asl
    asl
    sta RESP0,x    ; fix coarse position
    sta HMP0,x     ; set fine offset
    rts

P0_Bitmap
        .byte #0
        .byte #%11111111
        .byte #%10000001
        .byte #%10000001
        .byte #%11111111
        .byte #%10000001
        .byte #%10000001
        .byte #%11111111

P0_Color
        .byte #0
        .byte #$AE
        .byte #$AC
        .byte #$A8
        .byte #$AC
        .byte #$8E
        .byte #$8E
        .byte #$98

        
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Epilogue

	org $fffc
        .word Start	; reset vector
        .word Start	; BRK vector

 

Link to comment
Share on other sites

Your spreadsheet is linear and for some reason I'm not seeing that. Seems a bug somewhere in the code. Need to look at some other sample code but so far haven't seen any code where x corresponds to full 0..159 range.

Edited by lucienEn
Link to comment
Share on other sites

Hi there,

 

10 hours ago, lucienEn said:

Your spreadsheet is linear and for some reason I'm not seeing that. Seems a bug somewhere in the code. Need to look at some other sample code but so far haven't seen any code where x corresponds to full 0..159 range.

Here is a link to my github with numerous ROM reverse engineered that may help. There is also code posted here on AA from fellow homebrew authors that may help as well.

 

I haven't run your example so hopefully I'm following you. A value less than 14 should position your sprite in the color clock range of 63 - 76. A value of 5 should place you at color clock 68. The problem I see with anything greater than 134 is the RTS. Anything greater than 134 will hit the RESPx at cycle 68. You would only have 8 cycles left before the next scanline. The HMPx and RTS push this into the next scanline, returning at cycle 2 which then a WSYNC is called to wait for the next scanline. If you are going to use this in a subroutine; the lowest you can use is 134 which will place your sprite to color clock 197. If you want full range then I would suggest not using the subroutine or doing a table lookup for the fine motion adjustment. Using a table lookup for the fine motion would save ~6 cycles.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

2 hours ago, DEBRO said:

Hi there,

 

Here is a link to my github with numerous ROM reverse engineered that may help. There is also code posted here on AA from fellow homebrew authors that may help as well.

 

I haven't run your example so hopefully I'm following you. A value less than 14 should position your sprite in the color clock range of 63 - 76. A value of 5 should place you at color clock 68. The problem I see with anything greater than 134 is the RTS. Anything greater than 134 will hit the RESPx at cycle 68. You would only have 8 cycles left before the next scanline. The HMPx and RTS push this into the next scanline, returning at cycle 2 which then a WSYNC is called to wait for the next scanline. If you are going to use this in a subroutine; the lowest you can use is 134 which will place your sprite to color clock 197. If you want full range then I would suggest not using the subroutine or doing a table lookup for the fine motion adjustment. Using a table lookup for the fine motion would save ~6 cycles.

Thanks. Yes > 134 is clear why that causes extra scanline. Regarding smaller values it seems it's because the SetHorizPos is not strobing RESP in the visible area (and in my case I have HMOVE in SetHorizPos). See below. in that case you get wrong results and not 100% why it then adds extra 5. But that means I'd need another subroutine indeed if I wanted full range.

 

It seems lowest contiguous x value would be to strobe at pixelpos=0 (68 colorclk) with HM=-7 whch results then in x = 0 + 3x3 (STA RESP) +5 -7 = 7.

 

X     RESP Start  End PixelPos  HMP   Position Shown       

03    -11          1 (+5)        -3    8? Instead of 3

..

05    -11          1 (+5)        -1    10

..

13    -11 (+5)     1 (+5)        +7    18 ?? Shouldn't this be 13?

14    -11          1 (+5)        +8    19 ?? Shouldn't this be 14?

15    4           16 (+5)        -6    15

Edited by lucienEn
Link to comment
Share on other sites

1 hour ago, lucienEn said:

Regarding smaller values it seems it's because the SetHorizPos is not strobing RESP in the visible area (and in my case I have HMOVE in SetHorizPos).

Oh my...I missed that! Yes, the HMOVE appears to be affecting this. You can do a SLEEP 3 to avoid the HMOVE or if you need it; adjust your horizontal minimum so the RESPx hits within the visible screen.

Link to comment
Share on other sites

Just now, DEBRO said:

Oh my...I missed that! Yes, the HMOVE appears to be affecting this. You can do a SLEEP 3 to avoid the HMOVE or if you need it; adjust your horizontal minimum so the RESPx hits within the visible screen.

Thanks! Yes that causes the visible area to be smaller and not yet understanding the effect of strobing in the HBLANK area. I got this working now but not the prettiest code perhaps with the x adj. and it takes now 2 scanlines for positioning:

 

    processor 6502
    include "vcs.h"
    include "macro.h"

    seg.u Variables
    org  $80

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Variables and Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Player0_X       byte
Player0_X_Fine  byte
OVERSCAN_LINES  equ 30
X_Fine_Res      equ 5
adjSetHorizPos  equ 6
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    seg Code
    org $f000

Start
    CLEAN_START
    lda #120
    sta Player0_X
    lda #1
    sta Player0_X_Fine

   ; -----------------------------------------------
NextFrame
	VERTICAL_SYNC  ;  3 lines

; -----------------------------------------------
; 0. Vertical Blank Area. Add game logic here
; -----------------------------------------------
    ldx #36
    lda #$0      ; mask last line when we turn on again
WaitVBlank
    sta WSYNC
    sta COLUBK
    dex
    bne WaitVBlank
    lda #0
    sta VBLANK    ;
; we are now at scanline = 36, pixel pos = -32

    sta WSYNC
    sta HMOVE
    lda #$0E
    sta COLUBK
; we are now at scanline = 37

Kernel1_SetX
; the next two scanlines position the player horizontally
	lda Player0_X 	; get X coordinate
    ldx #0		    ; player 0
    jsr SetHorizPos	; set coarse offset

    ; wait at least 24-9 (HMOVE) cycles to finish fine positioning
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    sta HMCLR       ; reset fine position for next scanline
; we are now at scanline = 39

Kernel2_DrawP0
        ldy #8
P0Loop
        lda P0_Bitmap-1,y	; lookup color
        sta WSYNC	; sync w/ scanline
        sta HMOVE	; apply fine offsets
        sta GRP0	; store bitmap
        lda P0_Color-1,y ; lookup color
        sta COLUP0	; store color
        dey		; decrement X
        bne P0Loop
; we are now at scanline = 47

Kernel3_Setup
    ldx #228-36-2-1-8
Kernel3_Loop
    sta WSYNC
    sta HMOVE
    dex
    bne Kernel3_Loop

JoystickMovePlayer
    sec
    ldx Player0_X_Fine
    bit SWCHA
    bvs .CheckRight
    dex
    bmi .DecPlayer0_X
    jmp .CheckRight
.DecPlayer0_X
    dec Player0_X
    ldx #X_Fine_Res-1

.CheckRight
    stx Player0_X_Fine

    bit SWCHA
    bmi .SkipMoveRight
    clc
    inx
    cpx #X_Fine_Res
    beq .IncrPlayer0_X
    jmp .SkipMoveRight
.IncrPlayer0_X
    inc Player0_X
    ldx #0
.SkipMoveRight
    stx Player0_X_Fine
    jmp OverScanArea
resetPlayer0_X
    lda #0
    sta Player0_X

OverScanArea
; -----------------------------------------------
; Overscan area. Add game logic here
; -----------------------------------------------
    sta WSYNC     ; close previous kernel
    
    lda #2        ; turn on VBLANK
    sta VBLANK

    ldx #OVERSCAN_LINES
NextOverscanLine
    sta WSYNC
    dex
    bne NextOverscanLine
    jmp NextFrame

; -----------------------------------------------
; SUBROUTINE SECTION
; -----------------------------------------------
SetHorizPos
    sec
    sbc #adjSetHorizPos

    sta WSYNC
    sta HMOVE
    sec
SetHorizPosLoop
    sbc #15
    bcs SetHorizPosLoop
    eor #7         ; calculate fine offset
    asl
    asl
    asl
    asl
    sta HMP0     ; set fine offset
    sta RESP0    ; fix coarse position
    sta WSYNC
    sta HMOVE
    rts

P0_Bitmap
        .byte #0
        .byte #%11111111
        .byte #%10000001
        .byte #%10000001
        .byte #%11111111
        .byte #%10000001
        .byte #%10000001
        .byte #%11111111

P0_Color
        .byte #0
        .byte #$AE
        .byte #$AC
        .byte #$A8
        .byte #$AC
        .byte #$8E
        .byte #$8E
        .byte #$98
       
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
	org $fffc
        .word Start	; reset vector
        .word Start	; BRK vector

 

Link to comment
Share on other sites

Here's a modified SetHorizPos routine that works for any object for full 0..160 range but it will require 2 scanlines:

 

adjSetHorizPos  equ 6

; -----------------------------------------------
; SUBROUTINE SECTION
; -----------------------------------------------
; SetHorizPos - Sets the horizontal position of an object.
; The X register contains the index of the desired object:
;  X=0: player 0
;  X=1: player 1
;  X=2: missile 0
;  X=3: missile 1
;  X=4: ball
; This routine does a WSYNC and HMOVE before executing,
; so whatever you do here will not take effect until you
; call the routine again or do your own WSYNC and HMOVE.
SetHorizPos
    sec
    sbc #adjSetHorizPos

    sec
    sta WSYNC
    sta HMOVE
SetHorizPosLoop
    sbc #15
    bcs SetHorizPosLoop
    eor #7         ; calculate fine offset
    asl
    asl
    asl
    asl
    sta HMP0,X     ; set fine offset
    sta RESP0,X    ; fix coarse position
    sta WSYNC
    sta HMOVE
    ; wait at least 24-3 (HMOVE) cycles to finish fine positioning
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    sta HMCLR       ; reset fine position for next scanline
    rts

 

Link to comment
Share on other sites

; -----------------------------------------------
; SUBROUTINE SECTION
; -----------------------------------------------
SetHorizPos
    sec
    sbc #adjSetHorizPos

    sta WSYNC
    sta HMOVE           ; 3  3
    sec                 ; 2  5
SetHorizPosLoop
    sbc #15             ; 2  7
    bcs SetHorizPosLoop ; 2  9 (3 10 if taken)
    eor #7              ; 2 11 calculate fine offset
    asl                 ; 2 13
    asl                 ; 2 15
    asl                 ; 2 17
    asl                 ; 2 19
    sta HMP0            ; 3 21 set fine offset
    sta RESP0           ; 3 24 fix coarse position
    sta WSYNC
    sta HMOVE
    rts

 

 

From 8.0 Horizontal Motion in the Stella Programmer's Guide:

 

image.thumb.png.7cf9160810a565f99c0fb068bc393f4f.png

 

 

This is probably your issue for positions < 14 as RESP0 would be strobed 21 cycles after your first HMOVE.

 

 

 

Link to comment
Share on other sites

You might like to check out my tutorial, which covers writing a complete 2K game from scratch.

 

Step 4 of the tutorial is when object positioning comes into play. It uses this subroutine:

 

;===============================================================================
; PosObject
;----------
; subroutine for setting the X position of any TIA object
; when called, set the following registers:
;   A - holds the X position of the object
;   X - holds which object to position
;       0 = player0
;       1 = player1
;       2 = missile0
;       3 = missile1
;       4 = ball
; the routine will set the coarse X position of the object, as well as the
; fine-tune register that will be used when HMOVE is used.
;===============================================================================
PosObject:
        sec
        sta WSYNC
DivideLoop
        sbc #15        ; 2  2 - each time thru this loop takes 5 cycles, which is 
        bcs DivideLoop ; 2  4 - the same amount of time it takes to draw 15 pixels
        eor #7         ; 2  6 - The EOR & ASL statements convert the remainder
        asl            ; 2  8 - of position/15 to the value needed to fine tune
        asl            ; 2 10 - the X position
        asl            ; 2 12
        asl            ; 2 14
        sta.wx HMP0,X  ; 5 19 - store fine tuning of X
        sta RESP0,X    ; 4 23 - set coarse X position of object
        rts            ; 6 29

 

 

 

Which is called by this loop in Step 4 to position the players:

 

;===============================================================================
; PositionObjects
; --------------
; Updates TIA for X position of all objects
; Updates Kernel variables for Y position of all objects
;===============================================================================
PositionObjects:
        ldx #1              ; position objects 0-1: player0 and player1
POloop        
        lda ObjectX,x       ; get the object's X position
        jsr PosObject       ; set coarse X position and fine-tune amount 
        dex                 ; DEcrement X
        bpl POloop          ; Branch PLus so we position all objects
        sta WSYNC           ; wait for end of scanline
        sta HMOVE           ; use fine-tune values to set final X positions

 

 

 

Later on in the tutorial that loop becomes this as missiles and ball are added into the mix:

 

;===============================================================================
; PositionObjects
; --------------
; Updates TIA for X position of all objects
; Updates Kernel variables for Y position of all objects
;===============================================================================
PositionObjects:
        ldx #4              ; position all objects
POloop        
        lda ObjectX,x       ; get the object's X position
        jsr PosObject       ; set coarse X position and fine-tune amount 
        dex                 ; DEcrement X
        bpl POloop          ; Branch PLus so we position all objects
        sta WSYNC           ; wait for end of scanline
        sta HMOVE           ; use fine-tune values to set final X positions

 

Link to comment
Share on other sites

1 hour ago, SpiceWare said:

You might like to check out my tutorial, which covers writing a complete 2K game from scratch.

 

Step 4 of the tutorial is when object positioning comes into play. It uses this subroutine:

 

;===============================================================================
; PosObject
;----------
; subroutine for setting the X position of any TIA object
; when called, set the following registers:
;   A - holds the X position of the object
;   X - holds which object to position
;       0 = player0
;       1 = player1
;       2 = missile0
;       3 = missile1
;       4 = ball
; the routine will set the coarse X position of the object, as well as the
; fine-tune register that will be used when HMOVE is used.
;===============================================================================
PosObject:
        sec
        sta WSYNC
DivideLoop
        sbc #15        ; 2  2 - each time thru this loop takes 5 cycles, which is 
        bcs DivideLoop ; 2  4 - the same amount of time it takes to draw 15 pixels
        eor #7         ; 2  6 - The EOR & ASL statements convert the remainder
        asl            ; 2  8 - of position/15 to the value needed to fine tune
        asl            ; 2 10 - the X position
        asl            ; 2 12
        asl            ; 2 14
        sta.wx HMP0,X  ; 5 19 - store fine tuning of X
        sta RESP0,X    ; 4 23 - set coarse X position of object
        rts            ; 6 29

 

 

 

Which is called by this loop in Step 4 to position the players:

 

;===============================================================================
; PositionObjects
; --------------
; Updates TIA for X position of all objects
; Updates Kernel variables for Y position of all objects
;===============================================================================
PositionObjects:
        ldx #1              ; position objects 0-1: player0 and player1
POloop        
        lda ObjectX,x       ; get the object's X position
        jsr PosObject       ; set coarse X position and fine-tune amount 
        dex                 ; DEcrement X
        bpl POloop          ; Branch PLus so we position all objects
        sta WSYNC           ; wait for end of scanline
        sta HMOVE           ; use fine-tune values to set final X positions

 

 

 

Later on in the tutorial that loop becomes this as missiles and ball are added into the mix:

 

;===============================================================================
; PositionObjects
; --------------
; Updates TIA for X position of all objects
; Updates Kernel variables for Y position of all objects
;===============================================================================
PositionObjects:
        ldx #4              ; position all objects
POloop        
        lda ObjectX,x       ; get the object's X position
        jsr PosObject       ; set coarse X position and fine-tune amount 
        dex                 ; DEcrement X
        bpl POloop          ; Branch PLus so we position all objects
        sta WSYNC           ; wait for end of scanline
        sta HMOVE           ; use fine-tune values to set final X positions

 

That's the code where I get the subroutine from, except I wanted to add HMOVE to blank out left side throughout the entire screen (to make it easier to remove comb effect).

With HMOVE added I needed to adjust the routine timing to make it work. The issue it failed < 14, was that it was strobing RESP in the invisible screen area and that causes incorrect positioning (not sure what then really happens). The extra CPU cycles needed for HMCLR is explained in TIA HW notes but that wasn't the issue here as I had enough NOP in there.

Link to comment
Share on other sites

1 hour ago, splendidnut said:

Why are you doing STA HMOVE both before and after positioning?  You only need to do it after.

Easy way to blank out entire left side of the screen without requiring to draw some ball object or other. Unless there's an even easier way.

Link to comment
Share on other sites

3 minutes ago, lucienEn said:

Easy way to blank out entire left side of the screen without requiring to draw some ball object or other. Unless there's an even easier way.

Yeah, I just saw that in your other response.  That makes sense.

Link to comment
Share on other sites

32 minutes ago, lucienEn said:

With HMOVE added I needed to adjust the routine timing to make it work. The issue it failed < 14, was that it was strobing RESP in the invisible screen area and that causes incorrect positioning (not sure what then really happens).

 

That'd be due to strobing RESP0 too soon after HMOVE was strobed.

 

You might be able add 160 to the position for anything < 14, that would wrap around to the position you want.

 

You'll probably need to use multiple reposition routines.  An example would be here, though do note this example is using early HMOVE (cycle 73) which eliminates the HMOVE bars. That might also be an option - use early HMOVE, then you don't have to worry about hiding HMOVE bars on the left.  

 

Of course early HMOVe wouldn't help help if you were planning to use those 8 pixels to implement smooth on/off screen movement of objects.

Link to comment
Share on other sites

4 minutes ago, SpiceWare said:

 

That'd be due to strobing RESP0 too soon after HMOVE was strobed.

 

You might be able add 160 to the position for anything < 14, that would wrap around to the position you want.

 

You'll probably need to use multiple reposition routines.  An example would be here, though do note this example is using early HMOVE (cycle 73) which eliminates the HMOVE bars. That might also be an option - use early HMOVE, then you don't have to worry about hiding HMOVE bars on the left.  

 

Of course early HMOVe wouldn't help help if you were planning to use those 8 pixels to implement smooth on/off screen movement of objects.

Thanks, yes that's what happened. But I fixed it, see my sample code above and the code you quoted. That works for 0..159 and for any object. Not prettiest code with the -6 adj. but otherwise does the job.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...