Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
IT'S ALIVE! reading seperate input from multiple mice plugged into one PC v0.30
#1
Latest version and multi-mouse Pong game at:
https://qb64phoenix.com/forum/showthread.php?tid=2836

Update: latest newer code can be found here

Yes, it IS possible to read seperate input from two or more USB mice plugged into your PC from your BASIC programs running on Windows!

So far, I have tested it with 5 mice plugged in simultaneously and no issues. I have some more mice laying around somewhere, will test with more when I locate them!

You need 2 programs for this, below.
Also place the files "makeint.h" and "winproc.h" in the same folder.
Compile the 2nd subprogram before running the main program.
(It won't run on its own but that's OK.)

Plug 2-8 USB mice into your PC. (You may need a USB hub for more ports!)

Run "ReadMiceMain" and if your PC prompts for permission say yes.
The main program will automatically open the subprogram which should have the focus.

NOTE: Make sure the subprogram has the focus - clicking the mouse button will shift focus to the main prog and the sub program won't be able to read mice - if that happens, click back on ReadMiceSub.

Try moving the different mice around.

When you quit be sure to close both programs.

It works a lot better with DSMan195276's network piece (no more messy temp files).
(Maybe Spriggsy's Pipecom will work better than using network?)

Still need to do and/or figure out 
  • Capturing mouse clicks and keyboard input (from ReadMiceSub, send to main)
  • Making ReadMiceSub window invisible (but keep focus on mouse clicks) 
    and hiding the mouse pointer.
    (Steffan-68 shared some code that may help with that, haven't gotten to looking at it yet.)
  • Line up the windows or else maximize ReadMiceSub so it captures mouse movement across entire screen
  • Capturing input in full screen mode???
  • Automatically kill ReadMouseSub when user closes main program.

Feel free to play around and make any improvements or share feedback!

Use it to make your own Pong Doubles, QuadraPong, OctoPong, 8-player Foosball Pong, Warlords, Spacewar!, Space Duel, multiplayer Tempest, cooperative puzzle solving or drawing game, weird multi-mouse musical instrument, or 8-player Duck Hunt games! Use the mice to make simple racing wheel controllers (see pics below, that thing works!) and create your own Super Sprint 8 racing game. The possibilities are endless, LoL.


"ReadMiceMain.bas" main program:
Code: (Select All)
' ################################################################################################################################################################
' Multimouse main program "ReadMiceMain.bas" v0.30
' ################################################################################################################################################################

' -------------------------------------------------------------------------------
' ABOUT
' -------------------------------------------------------------------------------
' Working proof of concept! (Windows only so far)
'
' 1. Plug 2-8 USB mice into your PC.
'
' 2. Place "makeint.h" and "winproc.h" in same folder as programs.
'
' 3. Compile the mouse reader program "ReadMiceSub.bas" first
'    (or run it once and exit).
'
' 4. Run the front end program "ReadMiceMain".
'    The main program will automatically start the subprogram "ReadMiceSub"
'    which should have the focus. If your PC prompts for permission say Yes.
'
' 5. Make sure the "ReadMiceSub" has the focus - currently, clicking the mouse
'    buttons will shift focus "ReadMiceMain" and "ReadMiceSub" won't be able
'    to read mice - if that happens, click back on "ReadMiceSub".
'
' 6. Try moving each mouse. Each one should move a different letter.
'
' 7. When quitting be sure to close both programs.

' To enable debugging output: change Const cDebugEnabled to TRUE
' It will output debugging info to the console.

' -------------------------------------------------------------------------------
' TO DO
' -------------------------------------------------------------------------------
' Some issues and things to fix:
' * Make the "ReadMiceSub" window invisible (but still have the focus to detect mice)
'   and get "ReadMiceSub" to receive clicking the mouse button (left, middle, right)
'   without losing focus (currently clicking the mouse changes focus to "ReadMiceMain"
'   and then "ReadMiceSub" can't read mice). Steffan-68 posted some code that might help.
' * Read keypresses and send those as well
'   ("ReadMiceSub" has focus so "ReadMiceMain" can't detect keypresses)
' * Hide the real mouse cursor and reactivate it when program closes
' * control the window of "ReadMiceSub" to match "ReadMiceMain" window size and position directly over it.
' * when "ReadMiceMain" quits, have it kill the "ReadMiceSub" subprogram.
' * detect moving the scroll wheel
' * get this working with _FullScreen _SquarePixels
' * figure out how to get the same functionality for Mac & Linux

' -------------------------------------------------------------------------------
' THANK YOU
' -------------------------------------------------------------------------------
' Much credit and thanks are due to:
' * jstookey who started the work that made this possible
' * SpriggsySpriggs who ported the hard stuff (APIs, events) to QB64 & QB64PE
' * SMcNeill who helped with so many things, most recently keeping the main window on top, getting file attributes
' * Steffan-68 for feedback, code for managing windows and ideas
' * DSMan195276 for the network code that lets the reader and main program talk smoothly
' * euklides, Ed Davis, mdijkens for file attributes code
' * grymmjack for words of encouragement
' * the QB64 and QB64PE communities for help with everything
' * (if I forgot anyone let me know!)

' -------------------------------------------------------------------------------
' CHANGES
' -------------------------------------------------------------------------------
' DATE        WHO        WHAT
' 2004-04-22  jstookey   added the ability to detect whether RawMouse is
'                        available or not so the application can either use a
'                        different multi-mouse system, or exit gracefully
'                        (thanks to Mark Healey).
' 2005-04-24  jstookey   Modified the code work with the latest version of
'                        MinGW. The new MinGW incorporates rawinput, so my
'                        winuser header and library is obsolete.
' 2006-03-05  jstookey   Initialized is_absolute and is_virtual_desktop to
'                        work better with newer versions of VStudio.
'                        Code at: https://jstookey.com/multiple-mice-raw-input/
' 2022-09-07  madscijr   Turned into a command line EXE that is called from
'                        QB64 with SpriggsySpriggs' pipecom from
'                        https://github.com/SpriggsySpriggs/Spriggsys-API-Collection/blob/master/Cross-Platform%20(Windows%2C%20Macintosh%2C%20Linux)/pipecomqb64.bas
'                        This version doesn't work.
' 2022-09-08  Spriggsy   Converted C to pure QB64 code.
' 2022-09-09  madscijr   Added demo code to move multiple objects on screen
'                        with separate mice independently.
' 2022-09-09  Spriggsy   Added a screen refresh.
' 2022-09-10  madscijr   Added detecting mouse buttons.
' 2024-05-19  madscijr   Try having the program write mice coordinates / button
'                        states to a file that main program "always on top"
'                        without focus can read.
' 2024-05-23  madscijr   Updated program to get mice coordinates via network calls
'                        Thanks to DSMan195276 for the networking code.

' -------------------------------------------------------------------------------
' RESOURCES:
'
' Multiple Mice in Windows using Raw Input by jstookey
' * https://jstookey.com/multiple-mice-raw-input/
'
' One of the original QB64 threads
' * https://qb64phoenix.com/forum/showthread.php?tid=864
'
' Using Raw Input from C# to handle multiple keyboards by Emma Burrows, Steve Messer
' * https://www.codeproject.com/Articles/17123/Using-Raw-Input-from-C-to-handle-multiple-keyboard
'
' Microsoft's Raw Input API docs at pages around:
' * https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputheader
' * https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputdevicelist
'
' Looking at RAWINPUT for more detail (HACKADAY.IO)
' * https://hackaday.io/project/5364-cheap-windows-jogkeyboard-controller-for-cncs/log/16843-looking-at-rawinput-for-more-detail
'
' TimothyTimbers Raw Input Class for indie Game Developers:
' * https://www.unknowncheats.me/forum/c-and-c-/64318-raw-input-class-indie-game-developers.html
'
' WINDOWS PROGRAMMING: ACCESSING RAW INPUT by DAVID OREJUELA
' * https://davidorejuela.blogspot.com/2013/12/windows-programming-accessing-raw-input.html
'
' Explorations in Game Development: Why Raw Input?
' * https://explore-gamedev.blogspot.com/2013/09/why-raw-input.html

' Forum threads:
' * https://stackoverflow.com/questions/44935905/why-i-can-not-retrieve-rawinput-data-from-mouse
' * https://stackoverflow.com/questions/6423729/get-current-cursor-position
' * https://www.gamedev.net/forums/topic/700010-winapi-raw-input-confusion/
' * https://cplusplus.com/forum/windows/95700/
' * https://www.codeproject.com/Questions/118950/Raw-input-in-QT-library
' * https://docs.unity3d.com/ScriptReference/Windows.Input.ForwardRawInput.html
' * https://forum.freecad.org/viewtopic.php?style=2&t=28306&start=40
' * https://java-native-access.github.io/jna/5.4.0/javadoc/com/sun/jna/platform/win32/WinUser.RAWINPUTDEVICELIST.html
' * https://community.appeon.com/index.php/qna/q-a/how-can-i-catch-tab-key
' * https://forum.3dconnexion.com/viewtopic.php?t=2698
' * https://en.sfml-dev.org/forums/index.php?topic=15879.0
' * https://www.autoitscript.com/forum/topic/95105-hid-human-interface-device-communications-in-xp/page/2/
' * https://forum.unity.com/threads/new-input-system-get-raw-mouse-delta-from-wm_input.1177393/
' * https://forums.codeguru.com/showthread.php?541051-WM_Input-runtime-crash
' * https://discourse.libsdl.org/t/sdl-fixed-allocation-and-alignment-of-raw-input-buffers/48112
' * https://microsoft.public.dotnet.framework.interop.narkive.com/gWykTvfJ/reading-raw-data-from-a-hid-device
' * https://forum.qt.io/topic/66064/how-to-get-mouse-actual-position
' * https://forum.powerbasic.com/forum/user-to-user-discussions/powerbasic-for-windows/55985-raw-keyboard-hid-input-discussion
' * https://handmade.network/forums/t/7638-how_to_read_joystick_data_from_raw_input
' * https://microsoft.public.development.device.drivers.narkive.com/UWm0qmMb/getrawinputdata-usb-keyboard-with-power-keys
' * https://www.quakeworld.nu/forum/topic/3634/46374/getting-sensitivity-to-match-quake-
' * https://discourse.libsdl.org/t/sdl-fixed-windows-rawinput-crash/50150
' * https://squid.nt.tuwien.ac.at/gitlab/platzgummer/qgroundcontrol/commit/779f13f4e38c8d06fc2a74e594017d984e42c58e.diff
' * https://forums.codeguru.com/showthread.php?511656-How-to-get-interpreted-mouse-coords-from-raw-mouse-input
' * https://blog.naver.com/promaker72/50102194598
' * https://ubuntuforums.org/showthread.php?t=1543385

' Other pages with Raw Input info:
' * https://dev.to/igorsegallafa/avoiding-the-use-of-auto-clicker-keyboard-tools-5469
' * https://dev.opencascade.org/doc/refman/html/class_w_n_t___h_i_d_space_mouse.html
' * https://chromium.googlesource.com/experimental/chromium/src/+/refs/wip/bajones/webvr/device/gamepad/raw_input_data_fetcher_win.cc
' * https://bobobobo.wordpress.com/2010/04/21/rawinput/
' * https://sidestore.io/SideSource/interfaces/RawInput.html

Option Explicit
_Title "ReadMiceMain"
$NoPrefix
'$Console:Only
'Console Off

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN CONSTANTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Const cProgName = "ReadMiceMain"
Const FALSE = 0
Const TRUE = Not FALSE
Const cDebugEnabled = FALSE

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' TEXT MODE COLORS:
Const cBlack = 0: Const cBlue = 1: Const cGreen = 2: Const cLtBlue = 3
Const cRed = 4: Const cPurple = 5: Const cOrange = 6: Const cWhite = 7
Const cGray = 8: Const cPeriwinkle = 9: Const cLtGreen = 10: Const cCyan = 11
Const cLtRed = 12: Const cPink = 13: Const cYellow = 14: Const cLtGray = 15

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR CONTROLLING WINDOW ON TOP, ETC.
Const SWP_NOSIZE = &H0001 'ignores cx and cy size parameters
Const SWP_NOMOVE = &H0002 'ignores x and y position parameters
Const SWP_NOZORDER = &H0004 'keeps z order and ignores hWndInsertAfter parameter
Const SWP_NOREDRAW = &H0008 'does not redraw window changes
Const SWP_NOACTIVATE = &H0010 'does not activate window
Const SWP_FRAMECHANGED = &H0020
Const SWP_SHOWWINDOW = &H0040
Const SWP_HIDEWINDOW = &H0080
Const SWP_NOCOPYBITS = &H0100
Const SWP_NOOWNERZORDER = &H0200
Const SWP_NOSENDCHANGING = &H0400
Const SWP_DRAWFRAME = SWP_FRAMECHANGED
Const SWP_NOREPOSITION = SWP_NOOWNERZORDER
Const SWP_DEFERERASE = &H2000
Const SWP_ASYNCWINDOWPOS = &H4000
Const HWND_TOP = 0 'window at top of z order no focus
Const HWND_BOTTOM = 1 'window at bottom of z order no focus
Const HWND_TOPMOST = -1 'window above all others no focus unless active
Const HWND_NOTOPMOST = -2 'window below active no focus

'' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'' CONSTANT FOR 2ND DIMENSION OF arrFile ARRAY
'Const cFileName = 0
'Const cFileData = 1

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' CONSTANT FOR WHAT DATA IS EXPECTED FROM THIS LINE IN FILE
Const cForPlayer = 1
Const cUpdateCount = 2
Const cMouseDX = 3
Const cMouseDY = 4
Const cMouseWheel = 5
Const cMouseLeftDown = 6
Const cMouseMiddleDown = 7
Const cMouseRightDown = 8
Const cMouseSpeedX = 1 ' smaller = faster
Const cMouseSpeedY = 3 ' smaller = faster

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CONSTANTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR CONTROLLING WINDOW ON TOP, ETC.
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
Declare Dynamic Library "user32"
    Function FindWindowA%& (ByVal lpClassName%&, Byval lpWindowName%&)
    Function SetWindowPos& (ByVal hWnd%&, Byval hWndInsertAfter%&, Byval X&, Byval Y&, Byval cx&, Byval cy&, Byval uFlags~&)
    Function GetForegroundWindow%&
End Declare

Declare Dynamic Library "kernel32"
    Function GetLastError~& ()
End Declare

' UDT TO HOLD THE INFO FOR EACH MOUSE
Type InfoType
    ID As String ' player identifier or mouse device ID
    char As String ' cursor character
    color As Integer ' character color
    row As Integer ' line to display values at

    UpdateCount As Integer ' if this value changes we know a value changed
    OldUpdateCount As Integer ' if this value changes we know a value changed

    countX As Integer ' increments by 1 every time mouse moves, when x movement count exceeds threshold cMouseSpeedX, send dx
    countY As Integer ' increments by 1 every time mouse moves, when y movement count exceeds threshold cMouseSpeedY, send dy
    dx As Integer ' mouse x position
    dy As Integer ' mouse y position
    x As Integer ' screen x position
    y As Integer ' screen y position
    oldX As Integer
    oldY As Integer
    wheel As Integer ' mouse wheel value
    LeftDown As Integer ' tracks left mouse button state, TRUE=down
    MiddleDown As Integer ' tracks middle mouse button state, TRUE=down
    RightDown As Integer ' tracks right mouse button state, TRUE=down
    LeftCount As Integer ' counts left clicks
    MiddleCount As Integer ' counts middle clicks
    RightCount As Integer ' counts right clicks
End Type ' InfoType
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BASIC PROGRAM METADATA
Dim Shared m_ProgramPath$: m_ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
Dim Shared m_ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)

' GLOBAL VARIABLES TO TRACK ERROR STATE
Dim Shared m_sError As String: m_sError = ""
Dim Shared m_sIncludeError As String: m_sIncludeError = ""

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ****************************************************************************************************************************************************************
' BEGIN DEBUG CONSOLE
' ****************************************************************************************************************************************************************
' ENABLE / DISABLE DEBUG CONSOLE WINDOW
If cDebugEnabled = TRUE Then
    $Console
    _Delay 4
    _Console On
    _Echo "Started " + m_ProgramName$
    _Echo "Debugging on..."
End If
' ****************************************************************************************************************************************************************
' END DEBUG CONSOLE
' ****************************************************************************************************************************************************************

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' EXECUTION STARTS HERE!
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' =============================================================================
' START THE MAIN ROUTINE
main

' =============================================================================
' FINISH
Print m_ProgramName$ + " finished."

' ****************************************************************************************************************************************************************
' DEACTIVATE DEBUGGING WINDOW
If cDebugEnabled = TRUE Then
    _Console Off
End If
' ****************************************************************************************************************************************************************

End
'System ' return control to the operating system

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GLOBAL ERROR HANDLER
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ErrorHandler:
m_sError = "Error #" + _Trim$(Str$(Err)) + " at line " + _Trim$(Str$(_ErrorLine)) + "."
m_sIncludeError = "File " + Chr$(34) + _InclErrorFile$ + Chr$(34) + " at line " + _Trim$(Str$(_InclErrorLine)) + "."
Resume Next
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GLOBAL ERROR HANDLER
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN MAIN ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////

Sub main
    ' CONTROLS MIN/MAX SCREEN POSITIONS
    Const cMinX = 1 ' 2
    Const cMaxX = 80 ' 79
    Const cMinY = 14 ' 1
    Const cMaxY = 30

    ' BASELINE STARTING POSITION
    Const cStartX = cMinX + 5
    Const cStartY = cMaxY - 5

    ' MOUSE TEST VARIABLES
    Dim arrMouse(0 To 8) As InfoType ' STORES INFO FOR EACH MOUSE
    Dim iMouseCount As Integer ' # OF MICE ATTACHED

    ' WINDOW VARIABLES
    Dim hWndThis As _Offset ' hWndThis%&
    Dim hWndTop As _Offset ' x%&

    ' OTHER VARS
    Dim iLoop1 As Integer
    Dim iLoop2 As Integer
    Dim sNextError As String
    Dim iIndex As Integer
    Dim sLine As String
    Dim iLineNum As Integer
    Dim iCount As Integer
    Dim iCol As Integer
    Dim iRow As Integer
    Dim arrColor(0 To 31) As Integer
    Dim in$
    Dim iNextX As Integer
    Dim iNextY As Integer

    ' VARIABLES FOR READING MICE FROM CLIENT
    Dim uintPort As _Unsigned Integer ' port
    Dim lngHost ' h&
    Dim lngConn ' c&
    Dim sInput As String ' s$

    ' FOR LOOPING THROUGH DATA
    ReDim arrMice(-1 To -1) As String
    ReDim arrValues(-1 To -1) As String
    Dim sNextChunk As String
    Dim sNextValue As String
    Dim iValuePosition As Integer

    ' =============================================================================
    ' SET ERROR TRAPPING
    On Error GoTo ErrorHandler

    ' =============================================================================
    ' OPEN A PORT TO TALK TO THE MOUSE READER
    Randomize Timer
    uintPort = Rnd * 10000 + 40000 ' between 40000 and 50000

    lngHost = _OpenHost("tcp/ip:" + _Trim$(Str$(uintPort)))
    Print lngHost

    ' =============================================================================
    ' START THE MOUSE READER PROGRAM
    Shell _DontWait "readmicesub.exe " + _Trim$(Str$(uintPort))

    ' =============================================================================
    ' GET CONNECTION WITH MOUSE READER
    lngConn = 0
    While lngConn = 0
        lngConn = _OpenConnection(lngHost)
        _Limit 60
    Wend

    ' =============================================================================
    ' INITIALIZE VARIABLES

    '' INITIALIZE MOUSE INPUT FILENAMES
    'For iLoop1 = LBound(arrFile) To UBound(arrFile)
    '    arrFile(iLoop1, cFileName) = m_ProgramPath$ + "mouse" + _Trim$(Str$(iLoop1)) + ".txt"
    '    arrFile(iLoop1, cFileData) = ""
    'Next iLoop1

    ' INITALIZE COLORS
    iCount = 0
    For iLoop1 = LBound(arrColor) To UBound(arrColor)
        iCount = iCount + 1: If iCount > 15 Then iCount = 1
        arrColor(iLoop1) = iCount
    Next iLoop1

    ' INITIALIZE USER DATA
    iNextX = cStartX
    iNextY = cStartY

    iCount = 0
    For iIndex = LBound(arrMouse) To UBound(arrMouse)
        iCount = iCount + 1
        arrMouse(iIndex).ID = "Mouse" + _Trim$(Str$(iCount))
        arrMouse(iIndex).char = Chr$(64 + iCount)
        arrMouse(iIndex).color = arrColor(iCount)
        arrMouse(iIndex).row = iCount + 4

        arrMouse(iIndex).UpdateCount = 0
        arrMouse(iIndex).OldUpdateCount = 0

        arrMouse(iIndex).countX = 0
        arrMouse(iIndex).countY = 0
        arrMouse(iIndex).dx = 0
        arrMouse(iIndex).dy = 0

        arrMouse(iIndex).x = iNextX
        arrMouse(iIndex).y = iNextY

        ' POSITION NEXT PLAYER
        iNextX = iNextX + 1
        iNextY = iNextY - 1

        arrMouse(iIndex).oldX = 1
        arrMouse(iIndex).oldY = 1
        arrMouse(iIndex).wheel = 0
        arrMouse(iIndex).LeftDown = FALSE
        arrMouse(iIndex).MiddleDown = FALSE
        arrMouse(iIndex).RightDown = FALSE

        arrMouse(iIndex).LeftCount = 0
        arrMouse(iIndex).MiddleCount = 0
        arrMouse(iIndex).RightCount = 0
    Next iIndex

    ' =============================================================================
    ' MOVE WINDOW TO TOP

    ' GET WINDOW HANDLES
    hWndThis = _WindowHandle ' FindWindowA(0, _OFFSET(t))
    hWndTop = GetForegroundWindow%& ' find currently focused process handle

    ' GET FOCUS
    If hWndThis <> hWndTop Then
        _ScreenClick 240, 240 ' add 40 to x and y to focus on positioned window
    End If

    ' MOVE TO TOP
    If SetWindowPos(hWndThis, HWND_TOPMOST, 200, 200, 0, 0, SWP_NOSIZE Or SWP_NOACTIVATE) = 0 Then
        'sNextError = "SetWindowPos failed. 0x" + LCase$(Hex$(GetLastError))
        m_sError = "SetWindowPos failed. 0x" + LCase$(Hex$(GetLastError))
    End If

    ' =============================================================================
    ' INIT SCREEN
    Screen 12 ' SCREEN 12 can use 16 color attributes with a black background. 256K possible RGB color hues. Background colors can be used with QB64.
    'Screen _NewImage(1024, 768, 32)

    ' window needs to be lined up directly under the main program, so the mouse coordinates align with the display
    _ScreenMove 0, 0
    '_SCREENMOVE _MIDDLE

    Cls , cBlack

    ' =============================================================================
    ' MAIN LOOP
    Do
        ' PRINT MESSAGE
        iRow = 1: iCol = 1
        Color cLtRed, cBlack
        PrintString1 iRow, iCol, "*** MAKE SURE PROGRAM READMICE HAS THE FOCUS ***"
        iRow = 2: iCol = 1
        Color cCyan, cBlack
        PrintString1 iRow, iCol, "Plug in 2 or more USB mice and move them around over the window."

        ' PRINT HEADER ROW
        iRow = 4: iCol = 1
        Color cBlack, cWhite

        PrintString1 iRow, iCol, "CHAR    ": iCol = iCol + 9
        PrintString1 iRow, iCol, "Mouse X ": iCol = iCol + 9
        PrintString1 iRow, iCol, "X       ": iCol = iCol + 9
        PrintString1 iRow, iCol, "Mouse Y ": iCol = iCol + 9
        PrintString1 iRow, iCol, "Y       ": iCol = iCol + 9
        PrintString1 iRow, iCol, "WHEEL   ": iCol = iCol + 9
        PrintString1 iRow, iCol, "LEFT    ": iCol = iCol + 9
        PrintString1 iRow, iCol, "MIDDLE  ": iCol = iCol + 9
        PrintString1 iRow, iCol, "RIGHT   ": iCol = iCol + 9

        ' -----------------------------------------------------------------------------
        ' READ MICE COORDINATES FROM CONNECTION...
        Get #lngConn, , sInput

        ' DID IT WORK?
        If Len(m_sError) = 0 Then
            DebugPrint "    GOT DATA FROM CONNECTION"

            ' HAVE DATA?
            If Len(sInput) > 0 Then
                DebugPrint "    DATA NOT EMPTY"

                ' BREAK UP INPUT INTO CHUNKS (1 CHUNK PER MOUSE)
                split sInput, Chr$(13), arrMice() ' SPLIT OUTPUT INTO PAGES

                ' PROCESS CHUNKS
                For iLoop1 = LBound(arrMice) To UBound(arrMice)
                    DebugPrint "        PROCESSING CHUNK arrMice(" + _Trim$(Str$(iLoop1)) + ")"

                    sNextChunk = _Trim$(arrMice(iLoop1))

                    ' DOES IT CONTAIN DATA?
                    If Len(sNextChunk) > 0 Then

                        ' BREAK UP CHUNK INTO VALUES
                        ' COORDINATES COME IN THE TAB-DELIMITED FORMAT:
                        ' {mouse #}\t{count}\t{dx}\t{dy}\t{wheel}\t{leftDown}\t{middleDown}\t{rightDown}\n
                        split sNextChunk, Chr$(9), arrValues() ' SPLIT OUTPUT INTO PAGES

                        ' PROCESS VALUES
                        iIndex = -1
                        iValuePosition = 0

                        For iLoop2 = LBound(arrValues) To UBound(arrValues)
                            ' TRACK WHAT VALUE # WE'RE ON
                            iValuePosition = iValuePosition + 1

                            DebugPrint "            iValuePosition = " + _Trim$(Str$(iValuePosition))
                            DebugPrint "            arrValues(" + _Trim$(Str$(iLoop2)) + ")"

                            ' GET VALUE
                            sNextValue = _Trim$(arrValues(iLoop2))
                            DebugPrint "            sNextValue=" + Chr$(34) + sNextValue + Chr$(34)

                            ' IS IT A VALID INTEGER?
                            If IsNumber%(sNextValue) Then
                                ' DETERMINE WHICH VALUE IT IS FROM ORDINAL POSITION IN THE INPUT
                                ' AND WRITE TO APPROPRIATE VARIABLE
                                Select Case iValuePosition
                                    Case cForPlayer
                                        ' THIS VALUE TELLS US WHO IT'S FOR
                                        iIndex = Val(sNextValue)
                                        DebugPrint "                PLAYER #" + _Trim$(Str$(iIndex))

                                    Case cUpdateCount
                                        ' THIS VALUE TELLS US IF THERE IS NEW INPUT
                                        arrMouse(iIndex).UpdateCount = Val(sNextValue)
                                        DebugPrint "                UpdateCount = " + _Trim$(Str$(arrMouse(iIndex).UpdateCount))

                                    Case cMouseDX:
                                        ' READ RAW VALUE
                                        arrMouse(iIndex).dx = Val(sNextValue)
                                        DebugPrint "                DX = " + _Trim$(Str$(arrMouse(iIndex).dx))

                                        ' CONTROL SPEED (MOVE AFTER NUMBER MOVEMENTS EXCEEDS THRESHOLD)
                                        arrMouse(iIndex).countX = arrMouse(iIndex).countX + 1
                                        DebugPrint "                    COUNT = " + _Trim$(Str$(arrMouse(iIndex).countX)) + "/" + _Trim$(Str$(cMouseSpeedX))

                                        If arrMouse(iIndex).countX > cMouseSpeedX Then
                                            DebugPrint "                    MOVE NOW"

                                            ' ADJUST COORDINATES BASED ON MOVEMENT
                                            arrMouse(iIndex).x = arrMouse(iIndex).x + arrMouse(iIndex).dx

                                            ' CHECK BOUNDARIES
                                            If arrMouse(iIndex).x < cMinX Then
                                                arrMouse(iIndex).x = cMinX
                                            ElseIf arrMouse(iIndex).x > cMaxX Then
                                                arrMouse(iIndex).x = cMaxX
                                            End If

                                            ' RESET SPEED COUNTER
                                            arrMouse(iIndex).countX = 0
                                        Else
                                            DebugPrint "                    DON'T MOVE YET"
                                        End If

                                    Case cMouseDY:
                                        ' READ RAW VALUE
                                        arrMouse(iIndex).dy = Val(sNextValue)
                                        DebugPrint "                DY = " + _Trim$(Str$(arrMouse(iIndex).dy))

                                        ' CONTROL SPEED (MOVE AFTER NUMBER MOVEMENTS EXCEEDS THRESHOLD)
                                        arrMouse(iIndex).countY = arrMouse(iIndex).countY + 1
                                        DebugPrint "                    COUNT = " + _Trim$(Str$(arrMouse(iIndex).countY)) + "/" + _Trim$(Str$(cMouseSpeedY))

                                        If arrMouse(iIndex).countY > cMouseSpeedY Then
                                            DebugPrint "                    MOVE NOW"

                                            ' ADJUST COORDINATES BASED ON MOVEMENT
                                            arrMouse(iIndex).y = arrMouse(iIndex).y + arrMouse(iIndex).dy

                                            ' CHECK BOUNDARIES
                                            If arrMouse(iIndex).y < cMinY Then
                                                arrMouse(iIndex).y = cMinY
                                            ElseIf arrMouse(iIndex).y > cMaxY Then
                                                arrMouse(iIndex).y = cMaxY
                                            End If

                                            ' RESET SPEED COUNTER
                                            arrMouse(iIndex).countY = 0
                                        Else
                                            DebugPrint "                    DON'T MOVE YET"
                                        End If

                                    Case cMouseWheel:
                                        '' READ RAW VALUE
                                        'arrMouse(iIndex).wheel = Val(sNextValue)

                                    Case cMouseLeftDown:
                                        '' READ RAW VALUE
                                        'arrMouse(iIndex).LeftDown = Val(sNextValue)

                                    Case cMouseMiddleDown:
                                        '' READ RAW VALUE
                                        'arrMouse(iIndex).MiddleDown = Val(sNextValue)

                                    Case cMouseRightDown:
                                        '' READ RAW VALUE
                                        'arrMouse(iIndex).RightDown = Val(sNextValue)

                                    Case Else:
                                        ' Unknown
                                End Select

                                ' EXIT IF VALUES HAVEN'T CHANGED
                                If iLoop2 > cUpdateCount Then
                                    If arrMouse(iIndex).UpdateCount = arrMouse(iIndex).OldUpdateCount Then
                                        ' STOP THE MOUSE DX / DY
                                        arrMouse(iIndex).dy = 0
                                        arrMouse(iIndex).dx = 0

                                        DebugPrint "                UpdateCount HASN'T CHANGED FROM OLD:"
                                        DebugPrint "                OldUpdateCount = " + _Trim$(Str$(arrMouse(iIndex).OldUpdateCount))
                                        DebugPrint "                EXITING CHUNK..."

                                        ' WE CAN STOP PROCESSING THIS CHUNK
                                        Exit For
                                    End If
                                End If

                            Else
                                DebugPrint "                (VALUE ISN'T A NUMBER)"
                                'DebugPrint "                ** sNextValue NOT A NUMBER: " + chr$(34) + sNextValue + chr$(34)
                                ' (VALUE ISN'T A NUMBER)
                                ' (DO NOTHING)
                            End If

                        Next iLoop2

                    End If

                Next iLoop1

                ' UPDATE OLD VALUE FOR NEXT CHANGE TEST
                If arrMouse(iIndex).UpdateCount <> arrMouse(iIndex).OldUpdateCount Then
                    arrMouse(iIndex).OldUpdateCount = arrMouse(iIndex).UpdateCount
                End If

            Else
                ' RETRIEVED DATA IS EMPTY
                DebugPrint "    DATA IS EMPTY"

                ' (JUST IGNORE FOR NOW)

                ' TODO:
                ' WE SHOULD QUIT IF NO DATA IS RETRIEVED IN A CERTAIN THRESHOLD OF TIME...

            End If
        Else
            ' ERROR RETRIEVING DATA...
            DebugPrint "    ERROR RETRIEVING DATA FROM CONNECTION: " + m_sError

            ' (OUTPUT ERROR TO LOG HERE)
            'color cLtRed, cBlack
            'PrintString1 1, 1, "Error opening file " + chr$(34) + arrFile(iIndex, cFileName) + chr$(34)
            'PrintString1 1, 1, m_sError
            ErrorClear

            ' TODO:
            ' WE SHOULD QUIT HERE, AND RESTART THE CLIENT...
        End If

        ' -----------------------------------------------------------------------------
        ' UPDATE SCREEN

        For iIndex = LBound(arrMouse) To UBound(arrMouse)
            Color arrMouse(iIndex).color, cBlack

            ' CHAR
            iRow = arrMouse(iIndex).row: iCol = 1
            PrintString1 iRow, iCol, arrMouse(iIndex).char + "         ": iCol = iCol + 9

            ' DX
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).dx)) + "         ": iCol = iCol + 9

            ' X
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).x)) + "         ": iCol = iCol + 9

            ' DY
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).dy)) + "         ": iCol = iCol + 9

            ' Y
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).y)) + "         ": iCol = iCol + 9

            ' WHEEL
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).wheel)) + "         ": iCol = iCol + 9


            ' LEFT
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).LeftDown)) + "         ": iCol = iCol + 9

            ' MIDDLE
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).MiddleDown)) + "         ": iCol = iCol + 9

            ' RIGHT
            PrintString1 iRow, iCol, _Trim$(Str$(arrMouse(iIndex).RightDown)) + "         ": iCol = iCol + 9

            ' -----------------------------------------------------------------------------
            ' REDRAW AND SAVE OLD COORDINATES
            PrintString1 arrMouse(iIndex).oldY, arrMouse(iIndex).oldX, " "
            PrintString1 arrMouse(iIndex).y, arrMouse(iIndex).x, arrMouse(iIndex).char
            If arrMouse(iIndex).oldX <> arrMouse(iIndex).x Or arrMouse(iIndex).oldY <> arrMouse(iIndex).y Then
                arrMouse(iIndex).oldY = arrMouse(iIndex).y
                arrMouse(iIndex).oldX = arrMouse(iIndex).x
            End If

        Next iIndex

        ' -----------------------------------------------------------------------------
        ' GET KEYBOARD INPUT
        While _DeviceInput(1): Wend ' clear and update the keyboard buffer
        If _KeyDown(27) Then
            DebugPrint "DETECTED ESC = _KeyDown(27) = EXITING"

            Exit Do ' leave loop when ESC key pressed
        End If

        _Limit 60 ' run 60 fps
    Loop While _Connected(lngConn)

End Sub ' main

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END MAIN ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN ERROR HANDLING HELPER FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Sub ErrorClear
    m_sError = ""
    m_sIncludeError = ""
End Sub ' ErrorClear
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END ERROR HANDLING HELPER FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GRAPHIC PRINTING ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
' iRow and iCol are 0-based.
' See also: PrintString1

Sub PrintString0 (iRow As Integer, iCol As Integer, MyString As String)
    Dim iX As Integer
    Dim iY As Integer
    iX = _FontWidth * iCol
    iY = _FontHeight * iRow ' (iRow + 1)
    _PrintString (iX, iY), MyString
End Sub ' PrintString0

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
' iRow and iCol are 1-based.
' See also: PrintString0

Sub PrintString1g (iRow As Integer, iCol As Integer, MyString As String)
    Dim iX As Integer
    Dim iY As Integer
    iX = _FontWidth * (iCol - 1)
    iY = _FontHeight * (iRow - 1)
    _PrintString (iX, iY), MyString
End Sub ' PrintString1g

' /////////////////////////////////////////////////////////////////////////////
' Prints a string at the specified row+column.
' iRow and iCol are 1-based.
' See also: PrintString0

Sub PrintString1 (iRow As Integer, iCol As Integer, MyString As String)
    Locate iRow, iCol
    Print MyString;
End Sub ' PrintString1

' /////////////////////////////////////////////////////////////////////////////
' Eliminates the math.

' Text resolution:
'  648 x  480:  80 x 30
'  720 x  480:  90 x 30
'  800 x  600: 100 x 37
' 1024 x  768: 128 x 48
' 1280 x 1024: 160 x 64
' 1920 x 1080: 240 x 67
' 2048 x 1152: 256 x 72 (truncated after 70 rows, 255 columns)
' 3840 x 2160: 480 x135 (truncated after 133 rows, 479 columns)

Sub PrintStringCR1 (iCol As Integer, iRow As Integer, MyString As String)
    Dim iCols As Integer
    Dim iRows As Integer
    Dim iX As Integer
    Dim iY As Integer
    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight
    iX = _FontWidth * (iCol - 1)
    iY = _FontHeight * (iRow - 1)
    _PrintString (iX, iY), MyString
End Sub ' PrintStringCR1

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GRAPHIC PRINTING ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GENERAL PURPOSE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' FOR BITWISE OPERATIONS

Function HasBit% (iByte As Integer, iBit As Integer)
    ''TODO: precalculate
    'dim shared m_arrBitValue(1 To 8) As Integer
    'dim iLoop as Integer
    'For iLoop = 0 To 7
    '   m_arrBitValue(iLoop + 1) = 2 ^ iLoop
    'Next iLoop
    'HasBit% = ((iByte And m_arrBitValue(iBit)) = m_arrBitValue(iBit))
    Dim iBitValue As Integer
    iBitValue = 2 ^ (iBit - 1)
    HasBit% = ((iByte And iBitValue) = iBitValue)
End Function ' HasBit%

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if value OriginalString$ is numeric.

' Re: Does a Is Number function exist in QB64?
' https://www.qb64.org/forum/index.php?topic=896.15

' Version 2 by madscijr
' Returns TRUE (-1) if string is an integer, FALSE (0) if not

' Version 1 by MWheatley
' Reply #18 on: January 01, 2019, 11:24:30 AM
' returns 1 if string is an integer, 0 if not

Function IsNumber% (OriginalString$)
    Dim bResult%: bResult% = FALSE
    Dim iLoop%
    Dim TestString$
    'Dim bNegative%
    Dim iDecimalCount%
    Dim sNextChar$

    'THEY SHOULD TRIM OUTSIDE THE FUNCTION!
    'TestString$ = _TRIM$(OriginalString$)

    If Len(OriginalString$) > 0 Then
        TestString$ = ""
        If Left$(OriginalString$, 1) = "+" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = FALSE
        ElseIf Left$(OriginalString$, 1) = "-" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = TRUE
        Else
            TestString$ = OriginalString$
            'bNegative% = FALSE
        End If
        If Len(TestString$) > 0 Then
            bResult% = TRUE
            iDecimalCount% = 0
            For iLoop% = 1 To Len(TestString$)
                sNextChar$ = Mid$(TestString$, iLoop%, 1)
                If sNextChar$ = "." Then
                    iDecimalCount% = iDecimalCount% + 1
                    If iDecimalCount% > 1 Then
                        ' TOO MANY DECIMAL POINTS, INVALID!
                        bResult% = FALSE
                        Exit For
                    End If
                ElseIf Asc(sNextChar$) < 48 Or Asc(sNextChar$) > 57 Then
                    ' NOT A NUMERAL OR A DECIMAL, INVALID!
                    bResult% = FALSE
                    Exit For
                End If
            Next iLoop%
        End If
    End If
    IsNumber% = bResult%
End Function ' IsNumber%

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0

'Combine all elements of in$() into a single string with delimiter$ separating the elements.

Function join$ (in$(), delimiter$)
    Dim result$
    Dim iLoop%
    result$ = in$(LBound(in$))
    For iLoop% = LBound(in$) + 1 To UBound(in$)
        result$ = result$ + delimiter$ + in$(iLoop%)
    Next iLoop%
    join$ = result$
End Function ' join$

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0
'
' FROM luke, QB64 Developer
' Date: February 15, 2019, 04:11:07 AM
'
' Given a string of words separated by spaces (or any other character),
' splits it into an array of the words. I've no doubt many people have
' written a version of this over the years and no doubt there's a million
' ways to do it, but I thought I'd put mine here so we have at least one
' version. There's also a join function that does the opposite
' array -> single string.
'
' Code is hopefully reasonably self explanatory with comments and a little demo.
' Note, this is akin to Python/JavaScript split/join, PHP explode/implode.

'Split in$ into pieces, chopping at every occurrence of delimiter$. Multiple consecutive occurrences
'of delimiter$ are treated as a single instance. The chopped pieces are stored in result$().
'
'delimiter$ must be one character long.
'result$() must have been REDIMmed previously.

' Modified to handle multi-character delimiters

Sub split (in$, delimiter$, result$())
    Dim start As Integer
    Dim finish As Integer
    Dim iDelimLen As Integer
    ReDim result$(-1)

    iDelimLen = Len(delimiter$)

    start = 1
    Do
        'While Mid$(in$, start, 1) = delimiter$
        While Mid$(in$, start, iDelimLen) = delimiter$
            'start = start + 1
            start = start + iDelimLen
            If start > Len(in$) Then
                Exit Sub
            End If
        Wend
        finish = InStr(start, in$, delimiter$)
        If finish = 0 Then
            finish = Len(in$) + 1
        End If

        ReDim _Preserve result$(0 To UBound(result$) + 1)

        result$(UBound(result$)) = Mid$(in$, start, finish - start)
        start = finish + 1
    Loop While start <= Len(in$)
End Sub ' split

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GENERAL PURPOSE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ################################################################################################################################################################
' BEGIN DEBUGGING ROUTINES #DEBUG
' ################################################################################################################################################################

' /////////////////////////////////////////////////////////////////////////////
' Prints MyString to console with linebreaks.

' Thanks to:
' SpriggsySpriggs for how to use the QB64 debug console:
' https://www.qb64.org/forum/index.php?topic=3949.0

Sub DebugPrint (MyString As String)
    If cDebugEnabled = TRUE Then
        ReDim arrLines(-1) As String
        Dim iLoop As Integer
        split MyString, Chr$(13), arrLines()
        For iLoop = LBound(arrLines) To UBound(arrLines)
            _Echo arrLines(iLoop)
        Next iLoop
    End If
End Sub ' DebugPrint

' /////////////////////////////////////////////////////////////////////////////
' Simply prints s$ to console (no linebreaks).

Sub DebugPrint1 (s$)
    If cDebugEnabled = TRUE Then
        _Echo s$
    End If
End Sub ' DebugPrint1

' ################################################################################################################################################################
' END DEBUGGING ROUTINES @DEBUG
' ################################################################################################################################################################

' ################################################################################################################################################################
' #REFERENCE

' =============================================================================
' SOME USEFUL STUFF FOR REFERENCE:

' Type Name               Type suffix symbol   Minimum value                  Maximum value                Size in Bytes
' ---------------------   ------------------   ----------------------------   --------------------------   -------------
' _BIT                    `                    -1                             0                            1/8
' _BIT * n                `n                   -128                           127                          n/8
' _UNSIGNED _BIT          ~`                   0                              1                            1/8
' _BYTE                   %%                   -128                           127                          1
' _UNSIGNED _BYTE         ~%%                  0                              255                          1
' INTEGER                 %                    -32,768                        32,767                       2
' _UNSIGNED INTEGER       ~%                   0                              65,535                       2
' LONG                    &                    -2,147,483,648                 2,147,483,647                4
' _UNSIGNED LONG          ~&                   0                              4,294,967,295                4
' _INTEGER64              &&                   -9,223,372,036,854,775,808     9,223,372,036,854,775,807    8
' _UNSIGNED _INTEGER64    ~&&                  0                              18,446,744,073,709,551,615   8
' SINGLE                  ! or none            -2.802597E-45                  +3.402823E+38                4
' DOUBLE                  #                    -4.490656458412465E-324        +1.797693134862310E+308      8
' _FLOAT                  ##                   -1.18E-4932                    +1.18E+4932                  32(10 used)
' _OFFSET                 %&                   -9,223,372,036,854,775,808     9,223,372,036,854,775,807    Use LEN
' _UNSIGNED _OFFSET       ~%&                  0                              18,446,744,073,709,551,615   Use LEN
' _MEM                    none                 combined memory variable type  N/A                          Use LEN

' div: int1% = num1% \ den1%
' mod: rem1% = num1% MOD den1%

' @END

"ReadMiceSub.bas" subprogram:

Code: (Select All)
' ################################################################################################################################################################
' Multimouse sub-program "ReadMiceSub.bas" v0.30
' ################################################################################################################################################################

' -------------------------------------------------------------------------------
' ABOUT
' -------------------------------------------------------------------------------
' (Subprogram for READMICEMAIN.BAS, see that for more info.)

Option Explicit
_Title "ReadMiceSub"
$NoPrefix
'$Console:Only
'Console Off

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN CONSTANTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Const cProgName = "ReadMiceSub"
Const FALSE = 0
Const TRUE = Not FALSE

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' USED TO CONVERT MOUSE POSITION TO GET SCREEN POSITION
' ScreenPos = MousePos / ScaleValue
' This doesn't really work too accurately!
Const cScaleX = 3
Const cScaleY = 4

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR RAW INPUT API
Const CS_HREDRAW = &H0002
Const CS_VREDRAW = &H0001

Const IDI_APPLICATION = 32512
Const IDC_ARROW = 32512
Const COLOR_WINDOW = 5

Const WS_OVERLAPPED = &H00000000
Const WS_CAPTION = &H00C00000
Const WS_SYSMENU = &H00080000
Const WS_THICKFRAME = &H00040000
Const WS_MINIMIZEBOX = &H00020000
Const WS_MAXIMIZEBOX = &H00010000
Const WS_CHILD = &H40000000
Const WS_VISIBLE = &H10000000
Const WS_OVERLAPPEDWINDOW = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_THICKFRAME Or WS_MINIMIZEBOX Or WS_MAXIMIZEBOX
Const CW_USEDEFAULT = &H80000000

Const WM_DESTROY = &H0002
Const WM_INPUT = &H00FF

Const SW_SHOW = 5

Const RID_INPUT = &H10000003

Const RIM_TYPEMOUSE = 0 ' Raw input comes from the mouse.
Const RIM_TYPEKEYBOARD = 1 ' Raw input comes from the keyboard.
Const RIM_TYPEHID = 2 ' Raw input comes from some device that is not a keyboard or a mouse.

Const MOUSE_MOVE_RELATIVE = &H00
Const MOUSE_MOVE_ABSOLUTE = &H01
Const MOUSE_VIRTUAL_DESKTOP = &H02
Const MOUSE_ATTRIBUTES_CHANGED = &H04
Const MOUSE_MOVE_NOCOALESCE = &H08

Const WM_MOUSEMOVE = &H0200

Const WM_PAINT = &H000F

Const DT_CENTER = &H00000001

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' MIN/MAX VALUES FOR MOUSE TEST
Const cMinX = 1
Const cMaxX = 80
Const cMinY = 1
Const cMaxY = 30 ' 24
Const cMinWheel = 0
Const cMaxWheel = 255

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' CONSTANT FOR 2ND DIMENSION OF arrFile ARRAY
Const cFileName = 0
Const cFileData = 1

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CONSTANTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN UDTs
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Type RAWINPUTDEVICE
    As Unsigned Integer usUsagePage, usUsage
    As Unsigned Long dwFlags
    As Offset hwndTarget
End Type

Type RAWINPUTDEVICELIST
    As Offset hDevice
    As Unsigned Long dwType
    $If 64BIT Then
        As String * 4 alignment
    $End If
End Type

Type POINT
    As Long x, y
End Type

Type MSG
    As Offset hwnd
    As Unsigned Long message
    As Unsigned Offset wParam
    As Offset lParam
    As Long time
    As POINT pt
    As Long lPrivate
End Type

Type WNDCLASSEX
    As Unsigned Long cbSize, style
    As Offset lpfnWndProc
    As Long cbClsExtra, cbWndExtra
    As Offset hInstance, hIcon, hCursor, hbrBackground, lpszMenuName, lpszClassName, hIconSm
End Type

Type RECT
    As Long left, top, right, bottom
End Type

Type PAINTSTRUCT
    As Offset hdc
    As Long fErase
    $If 64BIT Then
        As String * 4 alignment
    $End If
    As RECT rcPaint
    As Long fRestore, fIncUpdate
    As String * 32 rgbReserved
End Type

Type RAWINPUTHEADER
    As Unsigned Long dwType, dwSize
    As Offset hDevice
    As Unsigned Offset wParam
End Type

Type RAWMOUSE
    As Unsigned Integer usFlags
    $If 64BIT Then
        As String * 2 alignment
    $End If
    'As Unsigned Long ulButtons  'commented out because I'm creating this value using MAKELONG
    As Unsigned Integer usButtonFlags, usButtonData
    As Unsigned Long ulRawButtons
    As Long lLastX, lLastY
    As Unsigned Long ulExtraInformation
End Type

Type RAWINPUT
    As RAWINPUTHEADER header
    As RAWMOUSE mouse
End Type

' UDT TO HOLD THE INFO FOR EACH MOUSE
Type MouseInfoType
    UpdateCount As Integer ' if this value changes we know a value changed
    ID As String ' mouse device ID
    c As String ' cursor character
    x As Integer ' screen x position
    y As Integer ' screen y position
    dx As Integer ' mouse x movement -1=left, 1=right, 0=none
    dy As Integer ' mouse y movement -1=up  , 1=down , 0=none
    wheel As Integer ' mouse wheel value
    LeftDown As Integer ' tracks left mouse button state, TRUE=down
    MiddleDown As Integer ' tracks middle mouse button state, TRUE=down
    RightDown As Integer ' tracks right mouse button state, TRUE=down
    LeftCount As Integer ' counts left clicks
    MiddleCount As Integer ' counts middle clicks
    RightCount As Integer ' counts right clicks
End Type ' MouseInfoType

' UDT TO HOLD THE INFO FOR EACH KEYBOARD
Type KeyboardInfoType
    UpdateCount As Integer ' if this value changes we know a value changed
    ID As String ' keyboard device ID
    'TBD
End Type ' KeyboardInfoType

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END UDTs
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR RAW INPUT API
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
Declare CustomType Library
    Function GetRawInputDeviceList~& (ByVal pRawInputDeviceList As Offset, Byval puiNumDevices As Offset, Byval cbSize As Unsigned Long)
    Sub GetRawInputDeviceList (ByVal pRawInputDeviceList As Offset, Byval puiNumDevices As Offset, Byval cbSize As Unsigned Long)
    Function RegisterRawInputDevices& (ByVal pRawInputDevices As Offset, Byval uiNumDevices As Unsigned Long, Byval cbSize As Unsigned Long)
    Function GetModuleHandle%& (ByVal lpModulename As Offset)
    Function LoadIcon%& (ByVal hInstance As Offset, Byval lpIconName As Offset)
    Function LoadCursor%& (ByVal hInstance As Offset, Byval lpCursorName As Offset)
    Function RegisterClassEx~% (ByVal wndclassex As Offset)
    Function CreateWindowEx%& (ByVal dwExStyle As Unsigned Long, Byval lpClassName As Offset, Byval lpWindowName As Offset, Byval dwStyle As Unsigned Long, Byval x As Long, Byval y As Long, Byval nWidth As Long, Byval nHeight As Long, Byval hWndParent As Offset, Byval hMenu As Offset, Byval hInstance As Offset, Byval lpParam As Offset)
    Sub ShowWindow (ByVal hWnd As Offset, Byval nCmdShow As Long)
    Sub UpdateWindow (ByVal hWnd As Offset)
    Function GetMessage& (ByVal lpMsg As Offset, Byval hWnd As Offset, Byval wMsgFilterMin As Unsigned Long, Byval wMsgFilterMax As Unsigned Long)
    Sub TranslateMessage (ByVal lpMsg As Offset)
    Sub DispatchMessage (ByVal lpMsg As Offset)
    Sub PostQuitMessage (ByVal nExitCode As Long)
    Function DefWindowProc%& (ByVal hWnd As Offset, Byval Msg As Unsigned Long, Byval wParam As Unsigned Offset, Byval lParam As Offset)
    Sub GetRawInputData (ByVal hRawInput As Offset, Byval uiCommand As Unsigned Long, Byval pData As Offset, Byval pcbSize As Offset, Byval cbSizeHeader As Unsigned Long)
    Function GetRawInputData~& (ByVal hRawInput As Offset, Byval uiCommand As Unsigned Long, Byval pData As Offset, Byval pcbSize As Offset, Byval cbSizeHeader As Unsigned Long)
    Sub InvalidateRect (ByVal hWnd As Offset, Byval lpRect As Offset, Byval bErase As Long)
    Sub SendMessage (ByVal hWnd As Offset, Byval Msg As Unsigned Long, Byval wParam As Unsigned Offset, Byval lParam As Offset)
    Function BeginPaint%& (ByVal hWnd As Offset, Byval lpPaint As Offset)
    Sub GetClientRect (ByVal hWnd As Offset, Byval lpRect As Offset)
    Sub DrawText (ByVal hdc As Offset, Byval lpchText As Offset, Byval cchText As Long, Byval lprc As Offset, Byval format As Unsigned Long)
    Sub OffsetRect (ByVal lprc As Offset, Byval dx As Long, Byval dy As Long)
    Sub EndPaint (ByVal hWnd As Offset, Byval lpPaint As Offset)
End Declare

' Header file "makeint.h" must be in same folder as this program.
Declare CustomType Library ".\makeint"
    Function MAKEINTRESOURCE%& Alias "MAKEINTRSC" (ByVal i As _Offset)
End Declare

Declare Library
    Function MAKELPARAM%& (ByVal l As Integer, Byval h As Integer)
    Function MAKELONG~& (ByVal l As Unsigned Integer, Byval h As Unsigned Integer)
End Declare

$If 64BIT Then
    Declare Library ".\internal\c\c_compiler\x86_64-w64-mingw32\include\windowsx"
    $Else
    Declare Library ".\internal\c\c_compiler\i686-w64-mingw32\include\windowsx"
    $End If
    Function GET_Y_LPARAM& (ByVal lp As Offset)
    Function GET_X_LPARAM& (ByVal lp As Offset)
End Declare

' Header file "winproc.h" must be in same folder as this program.
Declare Library ".\winproc"
    Function WindowProc%& ()
End Declare

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BASIC PROGRAM METADATA
Dim Shared m_ProgramPath$: m_ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
Dim Shared m_ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)

' GLOBAL VARIABLES TO TRACK ERROR STATE
Dim Shared m_sError As String: m_sError = ""
Dim Shared m_sIncludeError As String: m_sIncludeError = ""

' RAW INPUT VARIABLES
Dim Shared mousemessage As String
Dim Shared rawinputdevices As String

' MOUSE VARIABLES
Dim Shared arrMouse(0 To 8) As MouseInfoType ' STORES INFO FOR EACH MOUSE
'Dim Shared arrRawMouseID(8) As Long ' device IDs for mice connected to system (guessing this would be a string, dunno)
Dim Shared iMouseCount As Integer ' # OF MICE ATTACHED

' KEYBOARD VARIABLES
Dim Shared arrKeyboard(0 To 8) As KeyboardInfoType ' STORES INFO FOR EACH KEYBOARD
Dim Shared iKeyboardCount As Integer ' # OF KEYBOARDS ATTACHED
Dim Shared arrKeyState(0 To 8, 1 To 512) As Integer ' arrKeyState({device#}, {keyCode}) = TRUE if key {keyCode} on keyboard {device#} is currently held down.

Dim Shared arrScreen(1 To 80, 1 To 25) As String ' STORES TEXT FOR SCREEN

Dim Shared iMinX As Long
Dim Shared iMaxX As Long
Dim Shared iMinY As Long
Dim Shared iMaxY As Long

' RAW FILE NAMES
Dim Shared arrFile(0 To 31, 0 To 1) As String

' NETWORK VARIABLES
Dim Shared uintPort As _Unsigned Integer ' port
Dim Shared lngConn As Long ' c&
Dim Shared iData As Integer ' i
Dim Shared sOutput As String ' s$

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' EXECUTION STARTS HERE!
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' =============================================================================
' START THE MAIN ROUTINE
main

' =============================================================================
' FINISH
Print m_ProgramName$ + " finished."
End
'System ' return control to the operating system

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GLOBAL ERROR HANDLER
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ErrorHandler:
m_sError = "Error #" + _Trim$(Str$(Err)) + " at line " + _Trim$(Str$(_ErrorLine)) + "."
m_sIncludeError = "File " + Chr$(34) + _InclErrorFile$ + Chr$(34) + " at line " + _Trim$(Str$(_InclErrorLine)) + "."
Resume Next
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GLOBAL ERROR HANDLER
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN DATA STATEMENTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' MOUSE CURSORS (JUST SOME LETTERS)
CData:
Data A,b,C,D,E,f,G,H

' DEFAULT/INTIAL X COORDINATE OF EACH CURSOR ON SCREEN
XData:
Data 5,15,25,35,45,55,65,75

' DEFAULT/INTIAL Y COORDINATE OF EACH CURSOR ON SCREEN
YData:
Data 17,17,19,19,21,21,23,23

' DEFAULT/INITIAL VALUE OF EACH SCROLL WHEEL
WData:
Data 224,192,160,128,96,64,32,0
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END DATA STATEMENTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN MAIN ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////

Sub main
    Dim sPort As String
    Dim iLoop As Integer
    Dim in$
   
    ' MAKE SURE WE HAVE INPUT
    sPort = Command$(1)
    If Len(sPort) > 0 Then
        If IsNumber%(sPort) = TRUE Then
            ' OPEN CONNECTION
            uintPort = Val(sPort)
            lngConn = _OpenClient("tcp/ip:" + _Trim$(Str$(uintPort)) + ":localhost")
            Print lngConn
           
           
           
            ' ****************************************************************************************************************************************************************
            ' ****************************************************************************************************************************************************************
            ' RETHINK DATA STRUCTURE
            ' ****************************************************************************************************************************************************************
            ' ****************************************************************************************************************************************************************
            ' INITIALIZE
            For iLoop = LBound(arrFile) To UBound(arrFile)
                arrFile(iLoop, cFileName) = m_ProgramPath$ + "mouse" + _Trim$(Str$(iLoop)) + ".txt"
                arrFile(iLoop, cFileData) = ""
            Next iLoop

            ' INITIALIZE
            iMinX = 0
            iMaxX = 3583
            iMinY = 0
            iMaxY = 8202

            ' ****************************************************************************************************************************************************************
            ' ****************************************************************************************************************************************************************
            ' SET UP WINDOW TO BE SAME SIZE AS, AND OVERLAPPED WITH HOST WINDOW
            ' ****************************************************************************************************************************************************************
            ' ****************************************************************************************************************************************************************
            ' SET UP WINDOW
            'Screen _NewImage(1024, 768, 32)
            Screen 12 ' SCREEN 12 can use 16 color attributes with a black background. 256K possible RGB color hues. Background colors can be used with QB64.
           
            ' window needs to be lined up directly under the main program, so the mouse coordinates align with the display
            _ScreenMove 0, 0 ' <<< NOT WORKING, HOW DO WE DO THIS IN THE EVENT MODEL?

            ' GIVE CONTROL TO THE EVENT-ORIENTED CODE
            System Val(Str$(WinMain))
           
           
           
        Else
            Print "Invalid non-numeric input " + Chr$(34) + sPort + Chr$(34) + ". Exiting."
        End If
    Else
        Print "No input. Exiting."
    End If
   
End Sub ' main

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END MAIN ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN RAW INPUT FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Runs first

Function WinMain~%& ()
    Dim As Offset hwndMain, hInst
    Dim As MSG msg
    Dim As WNDCLASSEX wndclass
    Dim As String szMainWndClass
    Dim As String szWinTitle
    Dim As Unsigned Integer reg
   
    'DEBUG: TRY FULL SCREEN <- PROGRAM CRASHES!
    '_FullScreen _SquarePixels
   
    hInst = GetModuleHandle(0)
    szMainWndClass = "WinTestWin" + Chr$(0)
    'szWinTitle = "Hello" + Chr$(0)
    szWinTitle = cProgName + Chr$(0)
   
    wndclass.lpszClassName = Offset(szMainWndClass)
    wndclass.cbSize = Len(wndclass)
    wndclass.style = CS_HREDRAW Or CS_VREDRAW
    wndclass.lpfnWndProc = WindowProc
    wndclass.hInstance = hInst 'GetModuleHandle(0) will return the hInstance of this EXE
    wndclass.hIcon = LoadIcon(0, MAKEINTRESOURCE(IDI_APPLICATION))
    wndclass.hIconSm = LoadIcon(0, MAKEINTRESOURCE(IDI_APPLICATION))
    wndclass.hCursor = LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW))
    wndclass.hbrBackground = COLOR_WINDOW + 1
   
    reg = RegisterClassEx(Offset(wndclass)) 'I prefer to use the output of RegisterClassEx rather than the window name
   
    'DEBUG: SUBSTITUTE _WindowHandle
   
    'Function  CreateWindowEx%& (
    '   ByVal dwExStyle    As Unsigned Long = 0
    '   Byval lpClassName  As Offset        = MAKELPARAM(reg, 0)
    '   Byval lpWindowName As Offset        = Offset(szWinTitle)
    '   Byval dwStyle      As Unsigned Long = WS_OVERLAPPEDWINDOW
    '   Byval x            As Long          = CW_USEDEFAULT
    '   Byval y            As Long          = CW_USEDEFAULT
    '   Byval nWidth       As Long          = CW_USEDEFAULT
    '   Byval nHeight      As Long          = CW_USEDEFAULT
    '   Byval hWndParent   As Offset        = 0
    '   Byval hMenu        As Offset        = 0
    '   Byval hInstance    As Offset        = hInst
    '   Byval lpParam      As Offset        = 0
   
    '    hwndMain = CreateWindowEx( _
    '       0, _
    '       MAKELPARAM(reg, 0), _
    '       Offset(szWinTitle), _
    '       WS_OVERLAPPEDWINDOW, _
    '       CW_USEDEFAULT, _
    '       CW_USEDEFAULT, _
    '       CW_USEDEFAULT, _
    '       CW_USEDEFAULT, _
    '       0, _
    '       0, _
    '       hInst, _
    '       0)
   
    hwndMain = CreateWindowEx( _
        0, _
        MAKELPARAM(reg, 0), _
        Offset(szWinTitle), _
        WS_OVERLAPPEDWINDOW, _
        0, _
        0, _
        1024, _
        768, _
        0, _
        0, _
        hInst, _
        0)
   
    'hwndMain = _WindowHandle
   
    'DEBUG: SUBSTITUTE _WindowHandle
    ShowWindow hwndMain, SW_SHOW
    'ShowWindow _WindowHandle, SW_SHOW
   
    'DEBUG: SUBSTITUTE _WindowHandle
    UpdateWindow hwndMain
    'UpdateWindow _WindowHandle
   
    InitRawInput
    InitMouseTest 'TODO: SAVE_MOUSE_INFO
   
    While GetMessage(Offset(msg), 0, 0, 0)
        TranslateMessage Offset(msg)
        DispatchMessage Offset(msg)
    Wend
   
    WinMain = msg.wParam
End Function ' WinMain

' /////////////////////////////////////////////////////////////////////////////
' Handles main window events

Function MainWndProc%& (hwnd As Offset, nMsg As Unsigned Long, wParam As Unsigned Offset, lParam As Offset)
    Static As Offset hwndButton
    Static As Long cx, cy
    Dim As Offset hdc
    Dim As PAINTSTRUCT ps
    Dim As RECT rc
    Dim As MEM lpb
    Dim As Unsigned Long dwSize
    Dim As RAWINPUT raw
    Dim As Long tmpx, tmpy
    Static As Long maxx
    Dim As RAWINPUTHEADER rih
   
    ' TEMP VARIABLES FOR DISPLAYING FORMATTED VALUES TO SCREEN
    Dim strNextID As String
    Dim iIndex As Integer
    Dim iRowOffset As Integer
    Dim iLen As Integer
    Dim sCount As String
    Dim sX As String
    Dim sY As String
    Dim sWheel As String
    Dim sLeftDown As String
    Dim sMiddleDown As String
    Dim sRightDown As String
    Dim sLeftCount As String
    Dim sMiddleCount As String
    Dim sRightCount As String
    Dim sNext As String
    Dim iNewX As Integer
    Dim iNewY As Integer
    Dim iDX As Integer
    Dim iDY As Integer
   
    ' MORE TEMP VARIABLES
    Dim iMouseNum As Integer
   
    ' HANDLE EVENTS
    Select Case nMsg
        Case WM_DESTROY
            PostQuitMessage 0
            MainWndProc = 0
            Exit Function
          
        Case WM_INPUT
            GetRawInputData lParam, RID_INPUT, 0, Offset(dwSize), Len(rih)
            lpb = MemNew(dwSize)
            If lpb.SIZE = 0 Then
                MainWndProc = 0
                Exit Function
            End If
            If GetRawInputData(lParam, RID_INPUT, lpb.OFFSET, Offset(dwSize), Len(rih)) <> dwSize Then
                'Print "GetRawInputData doesn't return correct size!"
                mousemessage = "GetRawInputData doesn't return correct size!"
            End If
            MemGet lpb, lpb.OFFSET, raw
           
            If raw.header.dwType = RIM_TYPEMOUSE Then
                tmpx = raw.mouse.lLastX
                tmpy = raw.mouse.lLastY
                maxx = tmpx
               
                ' GET MOUSE INFO
                ' NOTES:
                ' ulButtons and usButtonFlags both return the same thing (buttons)
                ' usButtonData changes value when scroll wheel moved (just stays at one value)
                'mousemessage = ""
                'mousemessage = mousemessage + "Mouse:hDevice" + Str$(raw.header.hDevice)
                'mousemessage = mousemessage + "usFlags=" + Hex$(raw.mouse.usFlags)
                'mousemessage = mousemessage + "ulButtons=" + Hex$(MAKELONG(raw.mouse.usButtonFlags, raw.mouse.usFlags))
                'mousemessage = mousemessage + "usButtonFlags=" + Hex$(raw.mouse.usButtonFlags)
                'mousemessage = mousemessage + "usButtonData=" + Hex$(raw.mouse.usButtonData)
                'mousemessage = mousemessage + "ulRawButtons=" + Hex$(raw.mouse.ulRawButtons)
                'mousemessage = mousemessage + "lLastX=" + Str$(raw.mouse.lLastX)
                'mousemessage = mousemessage + "lLastY=" + Str$(raw.mouse.lLastY)
                'mousemessage = mousemessage + "ulExtraInformation=" + Hex$(raw.mouse.ulExtraInformation) + Chr$(13)
               
                ' UPDATE RANGE OF MOUSE COORDINATES
                If GET_X_LPARAM(lParam) < iMinX Then iMinX = GET_X_LPARAM(lParam)
                If GET_X_LPARAM(lParam) > iMaxX Then iMaxX = GET_X_LPARAM(lParam)
                If GET_Y_LPARAM(lParam) < iMinY Then iMinY = GET_Y_LPARAM(lParam)
                If GET_Y_LPARAM(lParam) > iMaxY Then iMaxY = GET_Y_LPARAM(lParam)
               
                ' IDENTIFY WHICH MOUSE IT IS
                strNextID = _Trim$(Str$(raw.header.hDevice))
                iIndex = GetMouseIndex%(strNextID)
                If iIndex >= LBound(arrMouse) Then
                    If iIndex <= UBound(arrMouse) Then
                      
                        ' =============================================================================
                        ' READ MOUSE MOVEMENT
                      
                        ' DOESN'T WORK, MOVES ALL OVER THE PLACE:
                        '' METHOD #1: SCALE MOUSE POSITION TO 80X25 POSITION
                        'iNewX = ( (GET_X_LPARAM(lParam) + 1) * 80) \ (iMaxX+1)
                        'iNewY = ( (GET_Y_LPARAM(lParam) + 1) * 25) \ (iMaxY+1)
                        'arrMouse(iIndex).x = iNewX
                        'arrMouse(iIndex).y = iNewY
                      
                        ' WORKS BUT NOT THAT ACCURATE:
                        ' METHOD #2: INCREMENT/DECREMENT DELTA
                        If raw.mouse.lLastX < 0 Then
                            arrMouse(iIndex).dx = -1
                        ElseIf raw.mouse.lLastX > 0 Then
                            arrMouse(iIndex).dx = 1
                        Else
                            arrMouse(iIndex).dx = 0
                        End If
                       
                        If raw.mouse.lLastY < 0 Then
                            arrMouse(iIndex).dy = -1
                        ElseIf raw.mouse.lLastY > 0 Then
                            arrMouse(iIndex).dy = 1
                        Else
                            arrMouse(iIndex).dy = 0
                        End If
                      
                        ' =============================================================================
                        'TODO: SAVE SCROLL WHEEL + BUTTONS
                        'Hex$(raw.mouse.usButtonFlags)
                      
                        ' left button = 1 when down, 2 when released
                        If ((raw.mouse.usButtonFlags And 1) = 1) Then
                            arrMouse(iIndex).LeftDown = TRUE
                        ElseIf ((raw.mouse.usButtonFlags And 2) = 2) Then
                            arrMouse(iIndex).LeftDown = FALSE
                        End If
                      
                        ' middle button = 16 when down, 32 when released
                        If ((raw.mouse.usButtonFlags And 16) = 16) Then
                            arrMouse(iIndex).MiddleDown = TRUE
                        ElseIf ((raw.mouse.usButtonFlags And 32) = 32) Then
                            arrMouse(iIndex).MiddleDown = FALSE
                        End If
                      
                        ' right button = 4 when down, 8 when released
                        If ((raw.mouse.usButtonFlags And 4) = 4) Then
                            arrMouse(iIndex).RightDown = TRUE
                        ElseIf ((raw.mouse.usButtonFlags And 8) = 8) Then
                            arrMouse(iIndex).RightDown = FALSE
                        End If
                      
                        ' scroll wheel = ???
                        'arrMouse(iIndex).wheel = ???
                       
                        ' DID VALUE CHANGE?
                        If arrMouse(iIndex).UpdateCount = 32767 Then
                            arrMouse(iIndex).UpdateCount = 1
                        Else
                            arrMouse(iIndex).UpdateCount = arrMouse(iIndex).UpdateCount + 1
                        End If
                       
                        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                        ' COLLECT VALUES FOR THIS MOUSE TO SEND
                        ' IN THE FOLOWING TAB-DELIMITED FORMAT:
                        ' {mouse #}\t{count}\t{dx}\t{dy}\t{wheel}\t{leftDown}\t{middleDown}\t{rightDown}\n
                        sOutput = ""
                        sOutput = sOutput + _Trim$(Str$(iIndex)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).UpdateCount)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).dx)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).dy)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).wheel)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).LeftDown)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).MiddleDown)) + Chr$(9)
                        sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).RightDown)) + Chr$(13)
                       
                        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                        ' SEND VALUES FOR THIS MOUSE TO HOST
                        Put #lngConn, , sOutput
                       
                        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                        ' CLEAR MOVEMENT
                        arrMouse(iIndex).dx = 0
                        arrMouse(iIndex).dy = 0
                       
                    End If
                End If
               
                ' UPDATE mousemessage WITH PLAYING FIELD
                mousemessage = ScreenToString$
                ' ================================================================================================================================================================
                ' END WRITE OUTPUT FILE
                ' ================================================================================================================================================================
               
                InvalidateRect hwnd, 0, -1
                SendMessage hwnd, WM_PAINT, 0, 0
                MainWndProc = 0
               
            ElseIf raw.header.dwType = RIM_TYPEKEYBOARD Then
                ' TODO: READ KEYBOARD INPUT
               
                ' SEE:
                ' https://www.codeproject.com/Articles/17123/Using-Raw-Input-from-C-to-handle-multiple-keyboard
                ' https://forum.powerbasic.com/forum/user-to-user-discussions/powerbasic-for-windows/55985-raw-keyboard-hid-input-discussion
                ' https://hackaday.io/project/5364-cheap-windows-jogkeyboard-controller-for-cncs/log/16843-looking-at-rawinput-for-more-detail
               
                ' WinAPI Raw Input confusion - For Beginners - GameDev.net
                ' https://www.gamedev.net/forums/topic/700010-winapi-raw-input-confusion/
               
                'iKeyboardCount = iKeyboardCount + 1 ' # KEYBOARDS ATTACHED
                'strNextID = _Trim$(Str$(rawdevs(x).hDevice))
                'arrKeyboard(iKeyboardCount - 1).ID = strNextID
                ' TODO: READ KEYBOARD AND STORE KEYBOARD STATE
                'arrKeyState(0 To 8, 1 To 512) As Integer ' arrKeyState({device#}, {keyCode}) = TRUE if key {keyCode} on keyboard {device#} is currently held down.
               
            End If
           
            MemFree lpb
            MainWndProc = 0
            Exit Function
          
        Case WM_MOUSEMOVE
            'mousemessage = mousemessage + " X:" + Str$(GET_X_LPARAM(lParam))
            'mousemessage = mousemessage + " Y:" + Str$(GET_Y_LPARAM(lParam))
            'mousemessage = mousemessage + Chr$(0)
           
            ' SAVE RANGE OF MOUSE COORDINATES
            If GET_X_LPARAM(lParam) < iMinX Then
                iMinX = GET_X_LPARAM(lParam)
                arrMouse(iIndex).dx = -1
            ElseIf GET_X_LPARAM(lParam) > iMaxX Then
                iMaxX = GET_X_LPARAM(lParam)
                arrMouse(iIndex).dx = 1
            Else
                arrMouse(iIndex).dx = 0
            End If
           
            If GET_Y_LPARAM(lParam) < iMinY Then
                iMinY = GET_Y_LPARAM(lParam)
                arrMouse(iIndex).dy = -1
            ElseIf GET_Y_LPARAM(lParam) > iMaxY Then
                iMaxY = GET_Y_LPARAM(lParam)
                arrMouse(iIndex).dy = 1
            Else
                arrMouse(iIndex).dy = 0
            End If
           
            ' IDENTIFY WHICH MOUSE IT IS
            strNextID = _Trim$(Str$(raw.header.hDevice))
            iIndex = GetMouseIndex%(strNextID)
            If iIndex >= LBound(arrMouse) Then
                If iIndex <= UBound(arrMouse) Then
                   
                    ' =============================================================================
                    ' UPDATE ABSOLUTE POSITION
                   
                    ' DOESN'T WORK, MOVES ALL OVER THE PLACE:
                    '' METHOD #1: SCALE MOUSE POSITION TO 80X25 POSITION
                    ''iNewX = ( (GET_X_LPARAM(lParam) + 1) * 80) \ 1520
                    'iNewX = ( (GET_X_LPARAM(lParam) + 1) * 80) \ (iMaxX+1)
                    ''iNewY = ( (GET_Y_LPARAM(lParam) + 1) * 25) \ 782
                    'iNewY = ( (GET_Y_LPARAM(lParam) + 1) * 25) \ (iMaxY+1)
                    'arrMouse(iIndex).x = iNewX
                    'arrMouse(iIndex).y = iNewY
                   
                    ' WORKS BUT NOT THAT ACCURATE:
                    ' METHOD #2: INCREMENT/DECREMENT DELTA
                    ' (should we update here too?)
                   
                    'TODO: SAVE SCROLL WHEEL + BUTTONS
                    ' (should we update here too?)
                    'arrMouse(iIndex).wheel =
                    'arrMouse(iIndex).LeftDown =
                    'arrMouse(iIndex).MiddleDown =
                    'arrMouse(iIndex).RightDown =
                End If
            End If
           
            'DEBUG: SUBSTITUTE _WindowHandle
            InvalidateRect hwnd, 0, -1
            'InvalidateRect _WindowHandle, 0, -1
          
            'DEBUG: SUBSTITUTE _WindowHandle
            SendMessage hwnd, WM_PAINT, 0, 0
            'SendMessage _WindowHandle, WM_PAINT, 0, 0
           
            MainWndProc = 0
            Exit Function
           
        Case WM_PAINT
            'DEBUG: SUBSTITUTE _WindowHandle
            hdc = BeginPaint(hwnd, Offset(ps))
            'hdc = BeginPaint(_WindowHandle, Offset(ps))
           
            'DEBUG: SUBSTITUTE _WindowHandle
            GetClientRect hwnd, Offset(rc)
            'GetClientRect _WindowHandle, Offset(rc)
           
            DrawText hdc, Offset(mousemessage), Len(mousemessage), Offset(rc), DT_CENTER
            OffsetRect Offset(rc), 0, 200
           
            '' PRINT LIST OF RawInput DEVICES:
            'DrawText hdc, Offset(rawinputdevices), Len(rawinputdevices), Offset(rc), DT_CENTER
           
            'DEBUG: SUBSTITUTE _WindowHandle
            EndPaint hwnd, Offset(ps)
            'EndPaint _WindowHandle, Offset(ps)
           
            MainWndProc = 0
            Exit Function
           
        Case Else
            'DEBUG: SUBSTITUTE _WindowHandle
            MainWndProc = DefWindowProc(hwnd, nMsg, wParam, lParam)
            'MainWndProc = DefWindowProc(_WindowHandle, nMsg, wParam, lParam)
    End Select
   
    If _KeyDown(27) Then End
   
End Function ' MainWndProc

' /////////////////////////////////////////////////////////////////////////////
' Initializes raw input stuff

Sub InitRawInput ()
    Dim As RAWINPUTDEVICE Rid(0 To 49)
    Dim As Unsigned Long nDevices
    Dim As RAWINPUTDEVICELIST RawInputDeviceList
    Dim As MEM pRawInputDeviceList
    ReDim As RAWINPUTDEVICELIST rawdevs(-1)
    Dim As Unsigned Long x
    Dim strNextID As String
    'dim lngNextID as long
   
    If GetRawInputDeviceList(0, Offset(nDevices), Len(RawInputDeviceList)) <> 0 Then
        Exit Sub
    End If
   
    pRawInputDeviceList = MemNew(Len(RawInputDeviceList) * nDevices)
    GetRawInputDeviceList pRawInputDeviceList.OFFSET, Offset(nDevices), Len(RawInputDeviceList)
   
    ' This small block of commented code proves that we've got the device list
    ReDim As RAWINPUTDEVICELIST rawdevs(0 To nDevices - 1)
    MemGet pRawInputDeviceList, pRawInputDeviceList.OFFSET, rawdevs()
   
    ' GET MOUSE / KEYBOARD INFO
    iMouseCount = 0
    iKeyboardCount = 0
   
    rawinputdevices = "Number of raw input devices:" + Str$(nDevices) + Chr$(13)
   
    For x = 0 To UBound(rawdevs)
        rawinputdevices = rawinputdevices + Str$(rawdevs(x).hDevice) + ":" + Str$(rawdevs(x).dwType) + Chr$(13)
       
        ' RAWINPUTHEADER (winuser.h) - Win32 apps | Microsoft Learn
        ' https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputheader
        ' dwType
        ' Type: DWORD
        ' The type of raw input. It can be one of the following values:
        ' Constant           Value   Meaning
        ' RIM_TYPEMOUSE      0       Raw input comes from the mouse.
        ' RIM_TYPEKEYBOARD   1       Raw input comes from the keyboard.
        ' RIM_TYPEHID        2       Raw input comes from some device that is not a keyboard or a mouse.
       
        ' WHAT TYPE OF DEVICE IS IT?
        'If rawdevs(x).dwType = 0 Then
        If rawdevs(x).dwType = RIM_TYPEMOUSE Then
            iMouseCount = iMouseCount + 1
            strNextID = _Trim$(Str$(rawdevs(x).hDevice))
            'lngNextID = Val(strNextID)
            'arrMouse(iMouseCount-1).ID = lngNextID
            arrMouse(iMouseCount - 1).ID = strNextID
            arrMouse(iMouseCount - 1).UpdateCount = 0
            'TODO: SAVE_MOUSE_INFO
           
        ElseIf rawdevs(x).dwType = RIM_TYPEKEYBOARD Then
            iKeyboardCount = iKeyboardCount + 1 ' # KEYBOARDS ATTACHED
            strNextID = _Trim$(Str$(rawdevs(x).hDevice))
            arrKeyboard(iKeyboardCount - 1).ID = strNextID
            arrKeyboard(iKeyboardCount - 1).UpdateCount = 0
            ' TODO: READ KEYBOARD AND STORE KEYBOARD STATE
           
            'arrKeyState(0 To 8, 1 To 512) As Integer ' arrKeyState({device#}, {keyCode}) = TRUE if key {keyCode} on keyboard {device#} is currently held down.
        End If
       
    Next x
    rawinputdevices = rawinputdevices + Chr$(0)
   
    MemFree pRawInputDeviceList
   
    Rid(0).usUsagePage = &H01
    Rid(0).usUsage = &H02
    Rid(0).dwFlags = 0
   
    'DEBUG: SUBSTITUTE _WindowHandle
    Rid(0).hwndTarget = 0
    'Rid(0).hwndTarget = _WindowHandle
   
    If RegisterRawInputDevices(Offset(Rid()), 1, Len(Rid(0))) = 0 Then
        mousemessage = "RawInput init failed" + Chr$(0)
    End If
End Sub ' InitRawInput

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END RAW INPUT FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN MOUSE TEST FUNCTIONS #1
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Initialize mouse test stuff

'TODO: SAVE_MOUSE_INFO

Sub InitMouseTest
    Dim iIndex As Integer
    Dim iLoop As Integer
   
    ' FOR NOW ONLY SUPPORT UPTO 8 MICE
    If (iMouseCount > 8) Then iMouseCount = 8
   
    ' INITIALIZE CURSORS, MOUSE STATE, ETC.
    Restore CData
    iIndex = LBound(arrMouse) - 1
    For iLoop = 1 To iMouseCount
        iIndex = iIndex + 1
        Read arrMouse(iIndex).c
        ' INITIALIZED BELOW: arrMouse(iIndex).x = 0
        ' INITIALIZED BELOW: arrMouse(iIndex).y = 0
        ' INITIALIZED BELOW: arrMouse(iIndex).wheel = 127
        arrMouse(iIndex).LeftDown = FALSE
        arrMouse(iIndex).MiddleDown = FALSE
        arrMouse(iIndex).RightDown = FALSE
        arrMouse(iIndex).LeftCount = 0
        arrMouse(iIndex).MiddleCount = 0
        arrMouse(iIndex).RightCount = 0
        arrMouse(iIndex).UpdateCount = 1
    Next iLoop
   
    ' INITIALIZE X COORDINATES
    Restore XData
    iIndex = LBound(arrMouse) - 1
    For iLoop = 1 To iMouseCount
        iIndex = iIndex + 1
        Read arrMouse(iIndex).x
    Next iLoop
   
    ' INITIALIZE Y COORDINATES
    Restore YData
    iIndex = LBound(arrMouse) - 1
    For iLoop = 1 To iMouseCount
        iIndex = iIndex + 1
        Read arrMouse(iIndex).y
    Next iLoop
   
    ' INITIALIZE SCROLL WHEEL
    Restore WData
    iIndex = LBound(arrMouse) - 1
    For iLoop = 1 To iMouseCount
        iIndex = iIndex + 1
        Read arrMouse(iIndex).wheel
    Next iLoop
   
End Sub ' InitMouseTest

' /////////////////////////////////////////////////////////////////////////////
' Finds position in array arrMouse where .ID = MouseID

Function GetMouseIndex% (MouseID As String)
    Dim iLoop As Integer
    Dim iIndex%
    iIndex% = LBound(arrMouse) - 1
    For iLoop = LBound(arrMouse) To UBound(arrMouse)
        If arrMouse(iLoop).ID = MouseID Then
            iIndex% = iLoop
            Exit For
        Else
            ' not it
        End If
    Next iLoop
    GetMouseIndex% = iIndex%
End Function ' GetMouseIndex%

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END MOUSE TEST FUNCTIONS #1
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN TEST OUTPUT FUNCTIONS FOR API CONTROLLED UI
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Clears global array arrScreen

Sub ClearText
    Dim iColNum As Integer
    Dim iRowNum As Integer
    For iColNum = LBound(arrScreen, 1) To UBound(arrScreen, 1)
        For iRowNum = LBound(arrScreen, 2) To UBound(arrScreen, 2)
            arrScreen(iColNum, iRowNum) = " "
        Next iRowNum
    Next iColNum
End Sub ' ClearText

' /////////////////////////////////////////////////////////////////////////////
' Plots string MyString to position (iX, iY) in global array arrScreen.

Sub WriteText (iRow As Integer, iColumn As Integer, MyString As String)
    Dim iPos As Integer
    Dim iLoop As Integer
    If iColumn > 0 And iColumn < 81 Then
        If iRow > 0 And iRow < 26 Then
            For iLoop = 1 To Len(MyString)
                iPos = iColumn + (iLoop - 1)
                If iPos < 81 Then
                    arrScreen(iPos, iRow) = Mid$(MyString, iLoop, 1)
                Else
                    Exit For
                End If
            Next iLoop
        End If
    End If
End Sub ' WriteText

' /////////////////////////////////////////////////////////////////////////////
' Converts global array arrScreen to a string.

Function ScreenToString$
    Dim sResult As String
    Dim iColNum As Integer
    Dim iRowNum As Integer
    sResult = ""
    For iRowNum = LBound(arrScreen, 2) To UBound(arrScreen, 2)
        For iColNum = LBound(arrScreen, 1) To UBound(arrScreen, 1)
            sResult = sResult + arrScreen(iColNum, iRowNum)
        Next iColNum
        sResult = sResult + Chr$(13)
    Next iRowNum
    ScreenToString$ = sResult
End Function ' ScreenToString$

' /////////////////////////////////////////////////////////////////////////////
' based on code from:
' Qbasic Programs - Download free bas source code
' http://www.thedubber.altervista.org/qbsrc.htm

Sub DrawTextLine (y%, x%, y2%, x2%, c$)
    Dim i%
    Dim steep%
    Dim e%
    Dim sx%
    Dim dx%
    Dim sy%
    Dim dy%
   
    i% = 0: steep% = 0: e% = 0
    If (x2% - x%) > 0 Then sx% = 1: Else sx% = -1
    dx% = Abs(x2% - x%)
    If (y2% - y%) > 0 Then sy% = 1: Else sy% = -1
    dy% = Abs(y2% - y%)
    If (dy% > dx%) Then
        steep% = 1
        Swap x%, y%
        Swap dx%, dy%
        Swap sx%, sy%
    End If
    e% = 2 * dy% - dx%
    For i% = 0 To dx% - 1
        If steep% = 1 Then
            ''PSET (y%, x%), c%:
            'Locate y%, x% : Print c$;
            WriteText y%, x%, c$
        Else
            ''PSET (x%, y%), c%
            'Locate x%, y% : Print c$;
            WriteText x%, y%, c$
        End If

        While e% >= 0
            y% = y% + sy%: e% = e% - 2 * dx%
        Wend
        x% = x% + sx%: e% = e% + 2 * dy%
    Next
    ''PSET (x2%, y2%), c%
    'Locate x2%, y2% : Print c$;
    WriteText x2%, y2%, c$
End Sub ' DrawTextLine

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END TEST OUTPUT FUNCTIONS FOR API CONTROLLED UI
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN MOUSE FUNCTIONS TO COME
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Returns a count of # of RawInput mouse devices connected to the system

' *****************************************************************************
' TODO: GET COUNT FROM RawInput API
' For now, hardcoded to 1 until we figure out how to do this.
' *****************************************************************************
Function GetRawMouseCount% ()
    GetRawMouseCount% = 1
End Function ' GetRawMouseCount%

' /////////////////////////////////////////////////////////////////////////////
' Gets ID of each RawInput mouse device connected to the system (for now upto 8)

' Returns the IDs in an array of LONG <- may change depending on whether
' we save each the device handle for each mouse or the index

' If no mouse found, the ID will just be 0 <- or whatever value we decide as default/none

' *****************************************************************************
' TODO: GET THIS FROM RawInput API
' For now, hardcoded arrRawMouseID(1) to 1, and the rest 0, until we figure out how to do this.
' *****************************************************************************
'Sub GetRawMouseIDs (arrRawMouseID( 8) As Integer)
Sub GetRawMouseIDs ()
    Dim iLoop As Integer
   
    ' CLEAR OUT IDs
    For iLoop = 1 To 8
        ''arrRawMouseID(iLoop) = 0
        'arrMouse(iLoop).ID = 0
        arrMouse(iLoop).ID = ""
    Next iLoop
   
    ' GET IDs
    'TODO: get this from RawInput API
    ''arrRawMouseID(1) = 1 ' for now just fudge it!
    'arrMouse(0).ID = 1 ' for now just fudge it!
End Sub ' GetRawMouseIDs

' /////////////////////////////////////////////////////////////////////////////
' Read mouse using RawInput API

' Gets input from mouse, MouseID% = which mouse

' NOTE: click events (mouse up/mouse down) are handled by the calling sub,
'       this routine just sends back
'       TRUE if the given button is currently down or FALSE if it is up.

' Parameters (input only):
' MouseID% = which mouse to return input for
' wheelMin% = minimum value to allow wheelValue% to be decremented to
' wheelMax% = maximum value to allow wheelValue% to be incremened to

' Parameters (values returned):
' x% = mouse x position
' y% = mouse y position
' leftButton% = current state of left mouse button (up or down)
' middleButton% = current state of middle mouse button / scroll wheel button (up or down)
' rightButton% = current state of right mouse button (up or down)
' wheelValue% = value of mouse scroll wheel (passed in and incremented/decremented by 1 if wheel move detected)

Sub ReadRawMouse (MouseID%, x%, y%, leftButton%, middleButton%, rightButton%, wheelValue%, wheelMin%, wheelMax%)
    Dim scrollAmount%
    Dim dx%
    Dim dy%
   
    ' =============================================================================
    ' BEGIN READ MOUSE THE NEW RawInput WAY:
   
    ' read scroll wheel
    'TODO: get this from RawInput API
   
    ' determine mouse x position
    'TODO: get this from RawInput API
    dx% = 0 ' = getMouseDx(MouseID%)
    x% = x% + dx% ' adjust mouse value by dx
   
    ' determine mouse y position
    'TODO: get this from RawInput API
    dy% = 0 ' = getMouseDy(MouseID%)
    y% = y% + dy% ' adjust mouse value by dx
   
    ' read mouse buttons
    'TODO: get this from RawInput API
    leftButton% = FALSE
    middleButton% = FALSE
    rightButton% = FALSE
   
    ' END READ MOUSE THE NEW RawInput WAY:
    ' =============================================================================
   
    ' =============================================================================
    ' BEGIN READ MOUSE THE OLD QB64 WAY:
    '
    '' read scroll wheel
    'WHILE _MOUSEINPUT ' get latest mouse information
    '    scrollAmount% = _MOUSEWHEEL ' (Returns -1 when scrolling up and 1 when scrolling down with 0 indicating no movement since last read.)
    '    IF (scrollAmount% = -1) AND (wheelValue% > wheelMin%) THEN
    '        wheelValue% = wheelValue% + scrollAmount%
    '    ELSEIF (scrollAmount% = 1) AND (wheelValue% < wheelMax%) THEN
    '        wheelValue% = wheelValue% + scrollAmount%
    '    END IF
    'WEND
    '
    '' determine mouse x position
    'x% = _MOUSEX
    '
    '' determine mouse y position
    'y% = _dy
    '
    '' read mouse buttons
    'leftButton% = _MOUSEBUTTON(1)
    'middleButton% = _MOUSEBUTTON(3)
    'rightButton% = _MOUSEBUTTON(2)
    '
    ' END READ MOUSE THE OLD QB64 WAY:
    ' =============================================================================
   
End Sub ' ReadRawMouse

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END MOUSE FUNCTIONS TO COME
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN ERROR HANDLING HELPER FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Sub ErrorClear
    m_sError = ""
    m_sIncludeError = ""
End Sub ' ErrorClear
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END ERROR HANDLING HELPER FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GENERAL PURPOSE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' FOR BITWISE OPERATIONS

Function HasBit% (iByte As Integer, iBit As Integer)
    ''TODO: precalculate
    'dim shared m_arrBitValue(1 To 8) As Integer
    'dim iLoop as Integer
    'For iLoop = 0 To 7
    '   m_arrBitValue(iLoop + 1) = 2 ^ iLoop
    'Next iLoop
    'HasBit% = ((iByte And m_arrBitValue(iBit)) = m_arrBitValue(iBit))
    Dim iBitValue As Integer
    iBitValue = 2 ^ (iBit - 1)
    HasBit% = ((iByte And iBitValue) = iBitValue)
End Function ' HasBit%

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if value OriginalString$ is numeric.

' Re: Does a Is Number function exist in QB64?
' https://www.qb64.org/forum/index.php?topic=896.15

' Version 2 by madscijr
' Returns TRUE (-1) if string is an integer, FALSE (0) if not

' Version 1 by MWheatley
' Reply #18 on: January 01, 2019, 11:24:30 AM
' returns 1 if string is an integer, 0 if not

Function IsNumber% (OriginalString$)
    Dim bResult%: bResult% = FALSE
    Dim iLoop%
    Dim TestString$
    'Dim bNegative%
    Dim iDecimalCount%
    Dim sNextChar$

    'THEY SHOULD TRIM OUTSIDE THE FUNCTION!
    'TestString$ = _TRIM$(OriginalString$)

    If Len(OriginalString$) > 0 Then
        TestString$ = ""
        If Left$(OriginalString$, 1) = "+" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = FALSE
        ElseIf Left$(OriginalString$, 1) = "-" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = TRUE
        Else
            TestString$ = OriginalString$
            'bNegative% = FALSE
        End If
        If Len(TestString$) > 0 Then
            bResult% = TRUE
            iDecimalCount% = 0
            For iLoop% = 1 To Len(TestString$)
                sNextChar$ = Mid$(TestString$, iLoop%, 1)
                If sNextChar$ = "." Then
                    iDecimalCount% = iDecimalCount% + 1
                    If iDecimalCount% > 1 Then
                        ' TOO MANY DECIMAL POINTS, INVALID!
                        bResult% = FALSE
                        Exit For
                    End If
                ElseIf Asc(sNextChar$) < 48 Or Asc(sNextChar$) > 57 Then
                    ' NOT A NUMERAL OR A DECIMAL, INVALID!
                    bResult% = FALSE
                    Exit For
                End If
            Next iLoop%
        End If
    End If
    IsNumber% = bResult%
End Function ' IsNumber%

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0

'Combine all elements of in$() into a single string with delimiter$ separating the elements.

Function join$ (in$(), delimiter$)
    Dim result$
    Dim iLoop%
    result$ = in$(LBound(in$))
    For iLoop% = LBound(in$) + 1 To UBound(in$)
        result$ = result$ + delimiter$ + in$(iLoop%)
    Next iLoop%
    join$ = result$
End Function ' join$

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0
'
' FROM luke, QB64 Developer
' Date: February 15, 2019, 04:11:07 AM
'
' Given a string of words separated by spaces (or any other character),
' splits it into an array of the words. I've no doubt many people have
' written a version of this over the years and no doubt there's a million
' ways to do it, but I thought I'd put mine here so we have at least one
' version. There's also a join function that does the opposite
' array -> single string.
'
' Code is hopefully reasonably self explanatory with comments and a little demo.
' Note, this is akin to Python/JavaScript split/join, PHP explode/implode.

'Split in$ into pieces, chopping at every occurrence of delimiter$. Multiple consecutive occurrences
'of delimiter$ are treated as a single instance. The chopped pieces are stored in result$().
'
'delimiter$ must be one character long.
'result$() must have been REDIMmed previously.

' Modified to handle multi-character delimiters

Sub split (in$, delimiter$, result$())
    Dim start As Integer
    Dim finish As Integer
    Dim iDelimLen As Integer
    ReDim result$(-1)

    iDelimLen = Len(delimiter$)

    start = 1
    Do
        'While Mid$(in$, start, 1) = delimiter$
        While Mid$(in$, start, iDelimLen) = delimiter$
            'start = start + 1
            start = start + iDelimLen
            If start > Len(in$) Then
                Exit Sub
            End If
        Wend
        finish = InStr(start, in$, delimiter$)
        If finish = 0 Then
            finish = Len(in$) + 1
        End If

        ReDim _Preserve result$(0 To UBound(result$) + 1)

        result$(UBound(result$)) = Mid$(in$, start, finish - start)
        start = finish + 1
    Loop While start <= Len(in$)
End Sub ' split

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GENERAL PURPOSE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ################################################################################################################################################################
' #REFERENCE

' =============================================================================
' SOME USEFUL STUFF FOR REFERENCE:

' Type Name               Type suffix symbol   Minimum value                  Maximum value                Size in Bytes
' ---------------------   ------------------   ----------------------------   --------------------------   -------------
' _BIT                    `                    -1                             0                            1/8
' _BIT * n                `n                   -128                           127                          n/8
' _UNSIGNED _BIT          ~`                   0                              1                            1/8
' _BYTE                   %%                   -128                           127                          1
' _UNSIGNED _BYTE         ~%%                  0                              255                          1
' INTEGER                 %                    -32,768                        32,767                       2
' _UNSIGNED INTEGER       ~%                   0                              65,535                       2
' LONG                    &                    -2,147,483,648                 2,147,483,647                4
' _UNSIGNED LONG          ~&                   0                              4,294,967,295                4
' _INTEGER64              &&                   -9,223,372,036,854,775,808     9,223,372,036,854,775,807    8
' _UNSIGNED _INTEGER64    ~&&                  0                              18,446,744,073,709,551,615   8
' SINGLE                  ! or none            -2.802597E-45                  +3.402823E+38                4
' DOUBLE                  #                    -4.490656458412465E-324        +1.797693134862310E+308      8
' _FLOAT                  ##                   -1.18E-4932                    +1.18E+4932                  32(10 used)
' _OFFSET                 %&                   -9,223,372,036,854,775,808     9,223,372,036,854,775,807    Use LEN
' _UNSIGNED _OFFSET       ~%&                  0                              18,446,744,073,709,551,615   Use LEN
' _MEM                    none                 combined memory variable type  N/A                          Use LEN

' div: int1% = num1% \ den1%
' mod: rem1% = num1% MOD den1%

' @END


"makeint.h" headerfile:
Code: (Select All)
LPSTR MAKEINTRSC(ptrszint i){
    return MAKEINTRESOURCE(i);
}

"winproc.h" header file:
Code: (Select All)
ptrszint FUNC_MAINWNDPROC(ptrszint*_FUNC_MAINWNDPROC_OFFSET_HWND,uint32*_FUNC_MAINWNDPROC_ULONG_NMSG,uptrszint*_FUNC_MAINWNDPROC_UOFFSET_WPARAM,ptrszint*_FUNC_MAINWNDPROC_OFFSET_LPARAM);

LRESULT CALLBACK MainWndProc(HWND hwnd, UINT nMsg, WPARAM wParam, LPARAM lParam){
    return FUNC_MAINWNDPROC((ptrszint *) (&hwnd), &nMsg, &wParam, (ptrszint *)(&lParam));
}

void * WindowProc(){
    return (void *) MainWndProc;
}

Make your own homemade racing wheel for cheap:
Parts
  • USB optical mouse
  • wood dowel, PVC pipe and/or aluminum tube
  • a section of pool noodle
  • a small box
  • a wheel of some sort
  • duct tape
Works good! You can also use a mouse to make a spinner for games like Pong / Breakout / Tempest.

[Image: diy-optical-mouse-racing-wheel-1.png]

[Image: diy-optical-mouse-racing-wheel-2.png]


Attached Files
.h   makeint.h (Size: 61 bytes / Downloads: 41)
.h   winproc.h (Size: 420 bytes / Downloads: 48)
.bas   readmicemain.bas (Size: 51.06 KB / Downloads: 53)
.bas   readmicesub.bas (Size: 62.7 KB / Downloads: 57)
Reply
#2
BTW these two lines are key to setting up the Raw Input API window to line up with and match the size of the main program's window, and may even allow us to eliminate the 2nd program altogether, if we can get the hwnd window handle of our QB64PE main program and get the Raw Input to read its input from that:

Code: (Select All)
 
 
   hwndMain = CreateWindowEx( _
        0, _
        MAKELPARAM(reg, 0), _
        Offset(szWinTitle), _
        WS_OVERLAPPEDWINDOW, _
        0, _
        0, _
        1024, _
        768, _
        0, _
        0, _
        hInst, _
        0)
 
If SetWindowPos(hwndMain, HWND_TOPMOST, 200, 200, 0, 0, SWP_NOSIZE Or SWP_NOACTIVATE) = 0 Then
 
Reply




Users browsing this thread: 1 Guest(s)