Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Multi-Mouse Pong v0.33 (requires 2-4 USB mice plugged into your PC)
#1
For those who wonder, Whatever happened to Pong?, here is an answer.

It's a multiplayer paddle game programmed in BASIC that uses upto 4 USB mice for the controls.


.zip   multimouse-pong-0-33-x64.zip (Size: 2.22 MB / Downloads: 23)


[Image: pong-doubles-0-33.png]


[Image: pong-regular-0-33.png]


[Image: pong-regular-4player-0-33.png]



To play:
  • Download & unzip zip file (contains ready-to-run Windows x64 binaries and source code)
  • Make sure all files are in the same folder.
  • Plug in 2-4 USB mice into your PC.
  • Start "pongmain33".
  • There are 2 game variations:
    doubles means 2 players side by side, currently you need all 4 players (3 player version soon)
    volleyball means players are in front and back.
  • Instructions to play will be displayed on the screen. 
    Press 1 to cycle through the game variations.
    Press 2 to start.
    Press 3 to cancel a game in progress.
    Press 4 for debug messages
    Press 5 for test mode (ball bounces back whether or not it hits pddle)
    Press P to pause a game in progress.
    Press - (dash) to slow down ball.
    Press =/+ to speed up ball.
    Press Escape to exit program.

For anyone who wants to run from the source code:
  • Make sure "pongmain33.bas", "pongsub33.bas", "makeint.h" and "winproc.h" are in the same folder.
  • Plug in 2-4 USB mice before you start.
  • First compile "pongsub33.bas" (or run it, it will briefly run then quit).
  • Then run the game "pongmain33.bas". Give it a few seconds to get started.
    NOTE: You will see 2 programs running in your taskbar (the game + the subprogram that reads the mouse input).

Any feedback welcome.

Thanks again SpriggsySpriggs, Steffan-68, DSMan195276, SMcNeill and the QB64PE community for their invaluable contributions.

"PongMain33.bas":
Code: (Select All)
' ################################################################################################################################################################
' MULTI-MOUSE PONG main program "PongMain"
' ################################################################################################################################################################

' MULTI MOUSE PONG v0.33 by Softintheheadware

' -------------------------------------------------------------------------------
' ABOUT
' -------------------------------------------------------------------------------
' "Whatever happened to Pong?" - Frank Black
'
' Back in the '70s, many home Pong games and the Atari 2600 VCS
' came with 4 (four!) paddle controllers, so you could play with a buddy or 3.
' With paddle controllers, you could move your player as fast as your brain
' could move your hand, so these games were true tests of reflexes.
'
' Since then, personal computers and video games have evolved,
' and paddle controllers are not included with modern computers.
'
' Computers do however come with some sort of mouse, which is as responsive
' as the old paddle controllers.
' However computers typically work with only one mouse,
' so even though Pong games are common for people learning how to make games,
' they're not very fun, because if you want to play your friend,
' one of you has to use the keyboard, which kind of sucks!
'
' But what if you could just plug in an extra mouse for player 2?
' What if you could plug in 3 or 4 mice for 4-player Pong games?
'
' Well, go get yourself an extra USB mouse or 3, plug 'em in, and have fun!

' -------------------------------------------------------------------------------
' HOW TO USE
' -------------------------------------------------------------------------------
' 1. Plug 2-4 USB mice into your PC.
'
' 2. Place "makeint.h" and "winproc.h" in same folder as the program.
'
' 3. Compile the mouse reader program "PongSub" first
'    (or run it once, it will run briefly then exit).
'
' 4. Run the front end program "PongMain".
'    The main program will automatically start the subprogram "PongSub"
'    which should have the focus.
'
' 5. If your PC prompts for permission say Yes.
'    Be patient, the program takes a few seconds to start everything up
'    and get "situated"!
'
' 6. Click the mouse to make sure "PongSub" has the focus
'    (it's invisible but sitting on top of "PongMain").
'
' 7. Follow the instructions on screen. Basic instructions:
'    KEY       ACTION
'    1         Select game variation
'    2         Start game
'    3         Cancel game in progress
'    4         Toggle debug messages
'    5         Toggle test mode
'    P         Pause game in progress
'    - (dash)  Slow down ball
'    =/+       Speed up ball
'    Escape    Exit program
'
' 8. Press Esc to quit, or delete the file "Pong.DELETE-TO-CLOSE".
'    The main program creates this file at startup, and both the main and
'    sub programs periodically check that the file is still present.
'    If it is missing, both programs shut down.

' DEBUGGING CODE:
' There is currently a ton of commented out debugging code -
' sorry about the mess! [tosses coins to bartender]
' There were some terribly elusive problems to figure out.
' To enable debugging output: change Const cDebugEnabled to TRUE
' and search for "PrintDebug" and "DebugLog" and uncomment those lines.
' DebugPrint outputs to the console, DebugLog outputs to a file
' (named the same as the executable but with .txt appended to the filename).
' Enabling console in "PongSub" caused all sorts of problems,
' so to debug that, just use DebugLog.

' -------------------------------------------------------------------------------
' CHANGES
' -------------------------------------------------------------------------------
' DATE        WHO        WHAT
' 2021-02-13  madscijr   2-player pong controlled by keyboard & 1 mouse = NO FUN!
' 2022-09-07  madscijr   Started trying to read multiple mice.
' 2024-06-19  madscijr   Multiple mice working with help from QB64PE community.
' 2024-06-23  madscijr   MULTI-MOUSE PONG v0.27 first beta release.
' 2024-06-26  madscijr   v0.31 Added primitive splash screen with instructions,
'                        error message if subprogram not found,
'                        improved paddle bounce angle logic,
'                        speed up ball in X direction every few volleys,
'                        added press "F11" to show values on screen,
'                        added press "F12" to put in game for test mode,
'                        increased paddle width and max speeed,
'                        added press "-" (minus) to descrease ball speed
'                        and press =/+ to increase ball speed.
' 2024-06-27  madscijr   v0.32 Added Pong Doubles (default) game option.
' 2024-06-28  madscijr   v0.33 Moved a lot of duplicated code into routines,
'                        moved game variables into GameType UDT,
'                        added debugging variables (sMessage1..4) to help.

' -------------------------------------------------------------------------------
' TO DO
' -------------------------------------------------------------------------------
' * Hide the real mouse cursor and reactivate it when program closes.
' * Fix issue where when speed = max speed, ball.dx somehow changes to 1!
' * Let player choose options in attract mode
' * Cycle screen color in attract mode (Atari 2600 style screen saver)
' * Make it faster / more efficient (move logic into subroutines; LOTS of bloat!)
' * Improve ball collision logic
'   - Check for vertical collisions (for games like breakout, warlords)
'     * look ahead so if ball speed > paddle width, ball doesn't jump over paddle?
' * Get it working with _FullScreen _SquarePixels.
' * Get multi-mouse working for Mac & Linux.
' * Add game variation: doubles (teams side by side each 1/2 of court)
' * Add game variations: quadrapong, octopong, foosball, warlords, s'morelords!
' * Pongball Construction Set!
'   - let users customize any number of paddle games
'     eventually they should be able to create
'     Pong = every game variant in Atari 2600 Video Olympics,
'     Quadrapong = one player per side of the screen (top/bottom/right/left)
'     Pong Doubles = 2 players each side, each player gets 1/2 of the court
'     Breakout / Super Breakout / Tetris Breakout mashup
'     Warlords
'     Video Pinball / Pinball Construction Set
'   - Graphic options
'     * screen color
'     * object color
'     * object fill pattern
'     * customize court lines / net drawn on screen
'   - paddle size
'     * player can choose (novice, expert, etc.)
'     * grows/shrinks when?
'       - shrinks when score exceeds x% of winning score
'       - grows if opponent exceeds x% of winning score
'       - when hit trigger
'       - every x points
'       - when  opponent scores
'     * paddle type
'       - standard pong / breakout
'       - pinball flippers
'       - unusual shape (ie hole in middle)
'   - paddle movement
'     * x*axis only
'     * y*axis only
'     * diagonally
'     * freeform (x and y axis)
'     * momentum?
'     * slippery? (triggered for x seconds?)
'     * one mouse controls >1 paddles?
'   - paddle controls / mouse button functions
'     * speed up ball
'     * slow down ball
'     * push
'     * open/close like flippers
'     * catch ball / release
'     * release vs throw
'     * rotate paddle (up/down vs right/left)
'   - team play or every wo/man for themself?
'   - game rules
'     * boundaries
'       - score greater if ball thrown/hit from further away (like 3*pointer in basketball)
'       - player can't move outside defined area
'       - player penalized if they move outisde area
'       - player penalized if they stay in an area too long (basketball rules)
'     * score x to win or timed?
'     * allow tie or overtime?
'   - ball(s)
'     * one or multi?
'     * can be enclosed in walls (like super breakout)
'     * released by ball hitting trigger object
'   - viewing angle?
'     * top down (pong)
'     * front (breakout)
'     * side view with gravity (basketball / volleyball)
'     * top view with some gravity (pinball)
'   - movement rules
'     * gravity?
'     * friction?
'   - scoring goal
'     * types
'       - simple = edge of screen behind player
'       - hole in wall
'       - net (like in hockey/soccer)
'       - free*standing object
'       - basket (for side view)
'     * size
'       - can change?
'         * expands contracts continually (speed?)
'         * changes when (trigger)
'         * grows/shrinks every n seconds
'         * grows/shrinks when (score)
'     * if ball in goal, award points or game lost or player out?
'     * "anti*goal" = reduces score!
'   - walls (warlords / breakout)
'     * color of each layer
'     * destructible?
'       - # hits to destroy
'       - higher become lower?
'       - not destructible until hit from outside (contain other balls)
'   - how is ball served?
'     * automatically
'     * players take turns
'     * player who scored last
'     * pinball
'   - pinball type objects: see https://archive.org/details/pinball-construction-set-user-guide
'     * flippers
'     * ball launcher / plunger
'     * bumpers (various types)
'     * tunnel (entrance, exit, exit direction/velocity?)
'     * ball splitter = if a ball hits this it splits in two
'     * polygons = can have elasticity (bounce) or negative elasticity (slow down ball)
'     * slingshot
'     * kickers
'     * drop target
'     * ball hopper
'     * ball eater
'     * spinner
'     * magnet
'     * lanes
'     * gates
'     * rollover
'     * knife edge
'     * target = can take multiple hits to score
'     * trigger
'       - releases ball(s)
'       - opens/closes door
'       - moves object(s)
'       - changes paddle size
'       - activates object
'       - temporarily change gravity (for n seconds or until next trigger)
'       - can 1 trigger do >1 action, or do we just make multi triggers and place them near each other?
'       - if multiple, all triggers must be hit to cause action
'       - single use, reset immediately, times reset, or (re)activated by trigger?
'       - can take multiple hits to activeate?
'     * timed? appear after n seconds (or every n seconds)
'     * elastic = bounce object back
'     * speed up/slow down ball
'     * fling ball in random direction (spins around fast)
'     * reverser = auto*reverse dx and/or dy
'     * ball trap = stop (with gravity?)
'     * polygons / line objects?
'   - special objects
'     * automatic vs player control
'     * walls that extend
'     * puck launcher (hits ball then disappears, changes ball's trajectory)
'     * vacuum (suck ball in)
'   - (Find brainstorming with Pomly notes from 1996!)
' * Smarter collision detection
'   - Line intersection?
'   - Square vs Irregular polygon
'   - Pre-draw polygons
'   - Pixel detect under ball's pixels
' * Pre-calculate trajectories?
' * More multimouse games: 2-mouse archery?

' -------------------------------------------------------------------------------
' Thoughts
' -------------------------------------------------------------------------------
'   1 line for every angle 0-359°
'   lines stores in arrLineDX(z), arrLineDY(z)
'   1024x768 screen
'   4x4 ball size
'   ball at (x,y) with angle z
'   each line is 1-dimensional
'   linear # = arrAngle(x,y,z)
'   arrMove(z, linear #)
'   where linear # moves top left to bottom right
'   to calculate arrAngle and arrMove:
'   draw lines from/to (off screen? every location?)
'   for every angle, and for each pixel on the line and angle,
'   save the x,y,z to arrAngle
'   then use areAngle to populate  arrMove(z, linear #)
'   with 1024x768 screen and 4x4 ball size
'   that's 256x192 4x4 locations
'   256x192 = 49,152
'   256x192 x 360 = 17,694,720
'   with 1024x768 x 360 = 283,115,520

Option Explicit
_Title "PongMain"

'NOT SURE WHAT THIS LINE IS SUPPOSED TO DO? DISABLED FOR NOW:
'$ dynamic

$NoPrefix

'$Console:Only
'Console Off

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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' CONSTANTS FOR GAME MODE USED FOR VARIABLE ThisGame.iGameMode
Const cAttractMode = 0
Const cInPlay = 1
Const cPaused = 2
'const cGameOver = 3

' OTHER GAME CONSTANTS
Const cNoDir = 0
Const cLeftDir = 1
Const cRightDir = 2
Const cUpDir = 3
Const cDownDir = 4

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' TEXT MODE COLORS:
Const c_Black = 0: Const c_Blue = 1: Const c_Green = 2: Const c_LtBlue = 3
Const c_Red = 4: Const c_Purple = 5: Const c_Orange = 6: Const c_White = 7
Const c_Gray = 8: Const c_Periwinkle = 9: Const c_LtGreen = 10: Const c_Cyan = 11
Const c_LtRed = 12: Const c_Pink = 13: Const c_Yellow = 14: Const c_LtGray = 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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR CONTROLLING WINDOW FOCUS
Const SW_SHOW = 5

'' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'' 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 = 2
Const cMouseDY = 3
Const cMousePosX = 4
Const cMousePosY = 5
Const cMouseWheel = 6
Const cMouseLeftDown = 7
Const cMouseMiddleDown = 8
Const cMouseRightDown = 9

Const cMouseSpeedX = 0 ' smaller = faster
Const cMouseSpeedY = 2 ' smaller = faster

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' BEGIN Virtual-Key Codes
' https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' NOTE: raw.data.Keyboard.vKey may require set extended bit
Const VK_LBUTTON = &H01 ' dec = 1, Left mouse button
Const VK_RBUTTON = &H02 ' dec = 2, Right mouse button
Const VK_CANCEL = &H03 ' dec = 3, Control-break processing
Const VK_MBUTTON = &H04 ' dec = 4, Middle mouse button
Const VK_XBUTTON1 = &H05 ' dec = 5, X1 mouse button
Const VK_XBUTTON2 = &H06 ' dec = 6, X2 mouse button
'??? = &H07 ' dec = 7, Reserved
Const VK_BACK = &H08 ' dec = 8, BACKSPACE key
Const VK_TAB = &H09 ' dec = 9, TAB key
'??? = &H0A-0B ' dec = 10-11, Reserved
Const VK_CLEAR = &H0C ' dec = 12, CLEAR key
Const VK_RETURN = &H0D ' dec = 13, ENTER key
'??? = &H0E-0F ' dec = 14-15, Unassigned
Const VK_SHIFT = &H10 ' dec = 16, SHIFT key
Const VK_CONTROL = &H11 ' dec = 17, CTRL key
Const VK_MENU = &H12 ' dec = 18, ALT key
Const VK_PAUSE = &H13 ' dec = 19, PAUSE key
Const VK_CAPITAL = &H14 ' dec = 20, CAPS LOCK key
Const VK_KANA = &H15 ' dec = 21, IME Kana mode
Const VK_HANGUL = &H15 ' dec = 21, IME Hangul mode
Const VK_IME_ON = &H16 ' dec = 22, IME On
Const VK_JUNJA = &H17 ' dec = 23, IME Junja mode
Const VK_FINAL = &H18 ' dec = 24, IME final mode
Const VK_HANJA = &H19 ' dec = 25, IME Hanja mode
Const VK_KANJI = &H19 ' dec = 25, IME Kanji mode
Const VK_IME_OFF = &H1A ' dec = 26, IME Off
Const VK_ESCAPE = &H1B ' dec = 27, ESC key
Const VK_CONVERT = &H1C ' dec = 28, IME convert
Const VK_NONCONVERT = &H1D ' dec = 29, IME nonconvert
Const VK_ACCEPT = &H1E ' dec = 30, IME accept
Const VK_MODECHANGE = &H1F ' dec = 31, IME mode change request
Const VK_SPACE = &H20 ' dec = 32, SPACEBAR
Const VK_PRIOR = &H21 ' dec = 33, PAGE UP key
Const VK_NEXT = &H22 ' dec = 34, PAGE DOWN key
Const VK_END = &H23 ' dec = 35, END key
Const VK_HOME = &H24 ' dec = 36, HOME key
Const VK_LEFT = &H25 ' dec = 37, LEFT ARROW key
Const VK_UP = &H26 ' dec = 38, UP ARROW key
Const VK_RIGHT = &H27 ' dec = 39, RIGHT ARROW key
Const VK_DOWN = &H28 ' dec = 40, DOWN ARROW key
Const VK_SELECT = &H29 ' dec = 41, SELECT key
Const VK_PRINT = &H2A ' dec = 42, PRINT key
Const VK_EXECUTE = &H2B ' dec = 43, EXECUTE key
Const VK_SNAPSHOT = &H2C ' dec = 44, PRINT SCREEN key
Const VK_INSERT = &H2D ' dec = 45, INS key
Const VK_DELETE = &H2E ' dec = 46, DEL key
Const VK_HELP = &H2F ' dec = 47, HELP key

' MADE OUR OWN CONSTANTS FOR THESE:
Const VK_0 = &H30 ' dec = 48, 0 key
Const VK_1 = &H31 ' dec = 49, 1 key
Const VK_2 = &H32 ' dec = 50, 2 key
Const VK_3 = &H33 ' dec = 51, 3 key
Const VK_4 = &H34 ' dec = 52, 4 key
Const VK_5 = &H35 ' dec = 53, 5 key
Const VK_6 = &H36 ' dec = 54, 6 key
Const VK_7 = &H37 ' dec = 55, 7 key
Const VK_8 = &H38 ' dec = 56, 8 key
Const VK_9 = &H39 ' dec = 57, 9 key
'??? = &H3A-40 ' dec = 58-64, Undefined
Const VK_A = &H41 ' dec = 65, A key
Const VK_B = &H42 ' dec = 66, B key
Const VK_C = &H43 ' dec = 67, C key
Const VK_D = &H44 ' dec = 68, D key
Const VK_E = &H45 ' dec = 69, E key
Const VK_F = &H46 ' dec = 70, F key
Const VK_G = &H47 ' dec = 71, G key
Const VK_H = &H48 ' dec = 72, H key
Const VK_I = &H49 ' dec = 73, I key
Const VK_J = &H4A ' dec = 74, J key
Const VK_K = &H4B ' dec = 75, K key
Const VK_L = &H4C ' dec = 76, L key
Const VK_M = &H4D ' dec = 77, M key
Const VK_N = &H4E ' dec = 78, N key
Const VK_O = &H4F ' dec = 79, O key
Const VK_P = &H50 ' dec = 80, P key
Const VK_Q = &H51 ' dec = 81, Q key
Const VK_R = &H52 ' dec = 82, R key
Const VK_S = &H53 ' dec = 83, S key
Const VK_T = &H54 ' dec = 84, T key
Const VK_U = &H55 ' dec = 85, U key
Const VK_V = &H56 ' dec = 86, V key
Const VK_W = &H57 ' dec = 87, W key
Const VK_X = &H58 ' dec = 88, X key
Const VK_Y = &H59 ' dec = 89, Y key
Const VK_Z = &H5A ' dec = 90, Z key

' Microsoft's Virtual-Key Codes constants (continued):
Const VK_LWIN = &H5B ' dec = 91, Left Windows key
Const VK_RWIN = &H5C ' dec = 92, Right Windows key
Const VK_APPS = &H5D ' dec = 93, Applications key
'??? = &H5E ' dec = 94, Reserved
Const VK_SLEEP = &H5F ' dec = 95, Computer Sleep key
Const VK_NUMPAD0 = &H60 ' dec = 96, Numeric keypad 0 key
Const VK_NUMPAD1 = &H61 ' dec = 97, Numeric keypad 1 key
Const VK_NUMPAD2 = &H62 ' dec = 98, Numeric keypad 2 key
Const VK_NUMPAD3 = &H63 ' dec = 99, Numeric keypad 3 key
Const VK_NUMPAD4 = &H64 ' dec = 100, Numeric keypad 4 key
Const VK_NUMPAD5 = &H65 ' dec = 101, Numeric keypad 5 key
Const VK_NUMPAD6 = &H66 ' dec = 102, Numeric keypad 6 key
Const VK_NUMPAD7 = &H67 ' dec = 103, Numeric keypad 7 key
Const VK_NUMPAD8 = &H68 ' dec = 104, Numeric keypad 8 key
Const VK_NUMPAD9 = &H69 ' dec = 105, Numeric keypad 9 key
Const VK_MULTIPLY = &H6A ' dec = 106, Multiply key
Const VK_ADD = &H6B ' dec = 107, Add key
Const VK_SEPARATOR = &H6C ' dec = 108, Separator key
Const VK_SUBTRACT = &H6D ' dec = 109, Subtract key
Const VK_DECIMAL = &H6E ' dec = 110, Decimal key
Const VK_DIVIDE = &H6F ' dec = 111, Divide key
Const VK_F1 = &H70 ' dec = 112, F1 key
Const VK_F2 = &H71 ' dec = 113, F2 key
Const VK_F3 = &H72 ' dec = 114, F3 key
Const VK_F4 = &H73 ' dec = 115, F4 key
Const VK_F5 = &H74 ' dec = 116, F5 key
Const VK_F6 = &H75 ' dec = 117, F6 key
Const VK_F7 = &H76 ' dec = 118, F7 key
Const VK_F8 = &H77 ' dec = 119, F8 key
Const VK_F9 = &H78 ' dec = 120, F9 key
Const VK_F10 = &H79 ' dec = 121, F10 key
Const VK_F11 = &H7A ' dec = 122, F11 key
Const VK_F12 = &H7B ' dec = 123, F12 key
Const VK_F13 = &H7C ' dec = 124, F13 key
Const VK_F14 = &H7D ' dec = 125, F14 key
Const VK_F15 = &H7E ' dec = 126, F15 key
Const VK_F16 = &H7F ' dec = 127, F16 key
Const VK_F17 = &H80 ' dec = 128, F17 key
Const VK_F18 = &H81 ' dec = 129, F18 key
Const VK_F19 = &H82 ' dec = 130, F19 key
Const VK_F20 = &H83 ' dec = 131, F20 key
Const VK_F21 = &H84 ' dec = 132, F21 key
Const VK_F22 = &H85 ' dec = 133, F22 key
Const VK_F23 = &H86 ' dec = 134, F23 key
Const VK_F24 = &H87 ' dec = 135, F24 key
'??? = &H88-8F ' dec = 136-143, Reserved
Const VK_NUMLOCK = &H90 ' dec = 144, NUM LOCK key
Const VK_SCROLL = &H91 ' dec = 145, SCROLL LOCK key
'??? = &H92-96 ' dec = 146-150, OEM specific
'??? = &H97-9F ' dec = 151-159, Unassigned
Const VK_LSHIFT = &HA0 ' dec = 160, Left SHIFT key
Const VK_RSHIFT = &HA1 ' dec = 161, Right SHIFT key
Const VK_LCONTROL = &HA2 ' dec = 162, Left CONTROL key
Const VK_RCONTROL = &HA3 ' dec = 163, Right CONTROL key
Const VK_LMENU = &HA4 ' dec = 164, Left ALT key
Const VK_RMENU = &HA5 ' dec = 165, Right ALT key
Const VK_BROWSER_BACK = &HA6 ' dec = 166, Browser Back key
Const VK_BROWSER_FORWARD = &HA7 ' dec = 167, Browser Forward key
Const VK_BROWSER_REFRESH = &HA8 ' dec = 168, Browser Refresh key
Const VK_BROWSER_STOP = &HA9 ' dec = 169, Browser Stop key
Const VK_BROWSER_SEARCH = &HAA ' dec = 170, Browser Search key
Const VK_BROWSER_FAVORITES = &HAB ' dec = 171, Browser Favorites key
Const VK_BROWSER_HOME = &HAC ' dec = 172, Browser Start and Home key
Const VK_VOLUME_MUTE = &HAD ' dec = 173, Volume Mute key
Const VK_VOLUME_DOWN = &HAE ' dec = 174, Volume Down key
Const VK_VOLUME_UP = &HAF ' dec = 175, Volume Up key
Const VK_MEDIA_NEXT_TRACK = &HB0 ' dec = 176, Next Track key
Const VK_MEDIA_PREV_TRACK = &HB1 ' dec = 177, Previous Track key
Const VK_MEDIA_STOP = &HB2 ' dec = 178, Stop Media key
Const VK_MEDIA_PLAY_PAUSE = &HB3 ' dec = 179, Play/Pause Media key
Const VK_LAUNCH_MAIL = &HB4 ' dec = 180, Start Mail key
Const VK_LAUNCH_MEDIA_SELECT = &HB5 ' dec = 181, Select Media key
Const VK_LAUNCH_APP1 = &HB6 ' dec = 182, Start Application 1 key
Const VK_LAUNCH_APP2 = &HB7 ' dec = 183, Start Application 2 key
'??? = &HB8-B9 ' dec = 184-137, Reserved
Const VK_OEM_1 = &HBA ' dec = 186, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ;: key
Const VK_OEM_PLUS = &HBB ' dec = 187, For any country/region, the + key
Const VK_OEM_COMMA = &HBC ' dec = 188, For any country/region, the , key
Const VK_OEM_MINUS = &HBD ' dec = 189, For any country/region, the - key
Const VK_OEM_PERIOD = &HBE ' dec = 190, For any country/region, the . key
Const VK_OEM_2 = &HBF ' dec = 191, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the /? key
Const VK_OEM_3 = &HC0 ' dec = 192, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the `~ key
'??? = &HC1-DA ' dec = 193-218, Reserved
Const VK_OEM_4 = &HDB ' dec = 219, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the [{ key
Const VK_OEM_5 = &HDC ' dec = 220, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the \\| key
Const VK_OEM_6 = &HDD ' dec = 221, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ]} key
Const VK_OEM_7 = &HDE ' dec = 222, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '" key
Const VK_OEM_8 = &HDF ' dec = 223, Used for miscellaneous characters; it can vary by keyboard.
'??? = &HE0 ' dec = 224, Reserved
'??? = &HE1 ' dec = 225, OEM specific
Const VK_OEM_102 = &HE2 ' dec = 226, The <> keys on the US standard keyboard, or the \\| key on the non-US 102-key keyboard
'??? = &HE3-E4 ' dec = 227-228, OEM specific
Const VK_PROCESSKEY = &HE5 ' dec = 229, IME PROCESS key
'??? = &HE6 ' dec = 230, OEM specific
Const VK_PACKET = &HE7 ' dec = 231, Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
'??? = &HE8 ' dec = 232, Unassigned
'??? = &HE9-F5 ' dec = 233-245, OEM specific
Const VK_ATTN = &HF6 ' dec = 246, Attn key
Const VK_CRSEL = &HF7 ' dec = 247, CrSel key
Const VK_EXSEL = &HF8 ' dec = 248, ExSel key
Const VK_EREOF = &HF9 ' dec = 249, Erase EOF key
Const VK_PLAY = &HFA ' dec = 250, Play key
Const VK_ZOOM = &HFB ' dec = 251, Zoom key
Const VK_NONAME = &HFC ' dec = 252, Reserved
Const VK_PA1 = &HFD ' dec = 253, PA1 key
Const VK_OEM_CLEAR = &HFE ' dec = 254, Clear key
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' END Virtual-Key Codes
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN TYPE DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' UDT TO HOLD THE INFO FOR EACH PLAYER
Type PlayerType
    IsAvailable As Integer ' IF FALSE, THIS PLAYER IS NOT AVAILABLE FOR THE CURRENT GAME OPTION
    IsActive As Integer ' MOUSE DETECTED, CHANGE THIS TO TRUE AND SHOW PLAYER (IF AVAILABLE FOR CURRENT GAME)
    TeamNumber As Integer ' 1 = LEFT SIDE OF SCREEN, 2 = RIGHT; LATER EXPAND FOR OTHER GAME VARIANTS (WARLORDS, QUADRAPONG, OCTOPONG, ETC.)

    ID As String ' player identifier or mouse device ID
    'Name As String ' player name

    ' GAME VALUES
    Score As Integer

    ' WHERE TO DISPLAY PLAYER INFO
    scoreCol As Integer ' text column to display score at
    scoreRow As Integer ' text row    to display score at
    nameCol As Integer '  text column to display name  at
    nameRow As Integer '  text row    to display name  at

    ' POSITION + GRAPHIC VALUES
    color As _Unsigned Long
    x As Long ' player x position
    y As Long ' player y position
    width As Integer
    height As Integer

    BounceSegmentWidth As Double
    BounceSegmentHeight As Double
    'x1 As Integer
    'y1 As Integer
    'x2 As Integer
    'y2 As Integer

    ' BOUNDARIES
    minX As Long
    maxX As Long
    minY As Long
    maxY As Long

    ' MOUSE INPUT VARIABLES
    px As Long ' pointer x position (hires) for absolute position of mouse from raw input api
    py As Long ' pointer y position (hires) for absolute position of mouse from raw input api
    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
    LeftDownOld As Integer ' tracks left mouse button state, TRUE=down
    MiddleDownOld As Integer ' tracks middle mouse button state, TRUE=down
    RightDownOld As Integer ' tracks right mouse button state, TRUE=down
    'wheel As Integer ' mouse wheel value
    'wheelOld As Integer ' old mouse wheel value

    ' KEYBOARD INPUT VARIABLES
    UpKey As Integer
    DownKey As Integer

    ' NOT USED HERE
    'dx As Integer ' mouse x movement -1=left, 1=right, 0=none
    'dy As Integer ' mouse y movement -1=up  , 1=down , 0=none
    'oldX As Long ' tracks old x position to erase screen
    'oldY As Long ' tracks old y position to erase screen
    'pxOld As Long ' old pointer x position (hires) for absolute position of mouse from raw input api
    'pyOld As Long ' old pointer y position (hires) for absolute position of mouse from raw input api
    'pdx As Long ' x delta (hires) for absolute position of mouse from raw input api
    'pdy As Long ' y delta (hires) for absolute position of mouse from raw input api

    ' TEST VALUES
    'char As String ' cursor character
    'row As Integer ' line to display values at
    'Message As String
End Type ' PlayerType

' UDT TO HOLD THE INFO FOR EACH BALL
Type BallType
    size As Integer
    color As _Unsigned Long

    ' POSITION
    x As Integer
    y As Integer

    '' FOR LOOK AHEAD
    'nextX as integer
    'nextY as integer

    ' MOVEMENT
    dx As Integer
    dy As Integer

    ' BOUNDARIES
    minX As Long
    maxX As Long
    minY As Long
    maxY As Long

    'x1 as integer
    'y1 as integer
    'x2 as integer
    'y2 as integer
End Type ' BallType

' UDT TO HOLD OPTIONS FOR EACH GAME VARIANT
' FOR NOW IT JUST HOLDS THE NAME
' TODO: MAYBE DEVELOP A NOTATION TO CONTROL THINGS LIKE
'       HOW EACH PLAYER MOVES (HORIZONTALLY, VERTICALLY, BOTH, ETC.)
'       EACH PLAYER'S BOUNDARIES
'       ETC.
Type GameOptionType
    name As String ' the name of the game
End Type ' GameOptionType

' UDT TO HOLD VALUES FOR THE CURRENT GAME
' THAT ARE SET INSIDE InitGame DEPENDING ON THE GAME VARIANT CHOSEN
Type GameType
    imgBackground As Long ' NEEDS _FREEIMAGE AT END
    imgWalls As Long ' NEEDS _FREEIMAGE AT END
    imgText As Long ' NEEDS _FREEIMAGE AT END
    imgPlayers As Long ' NEEDS _FREEIMAGE AT END
    imgScore As Long ' NEEDS _FREEIMAGE AT END

    ulngScreenColor As _Unsigned Long ' screen_color~& ulngScreenColor
    ulngWallColor As _Unsigned Long ' wall_color~&
    iTeam1ScoreColumn As Integer
    iTeam2ScoreColumn As Integer
    iTeam1ScoreRow As Integer
    iTeam2ScoreRow As Integer

    iScoreToWin As Integer

    iScore1 As Integer
    iScore2 As Integer

    iGameMode As Integer ' TRACK GAME STATE, can be cAttractMode, cInPlay, cPaused
    'iWhoLastHitBall as integer

    iGameType As Integer ' the current game (variable index)
    sGameName As String ' the current game (name)

    ' SPEED SETTINGS
    MinSpeed As Integer
    MaxSpeed As Integer
    iIncreaseSpeedEvery As Integer ' # times ball crosses net before increasing speed (add +1 to speed)

    ' TRACK GAME SPEED, COUNT # VOLLEYS, ETC.
    iSpeed As Integer ' for now just the overall speed of the game
    iVolleyNum As Integer
    iVolleyDir As Integer
    iIncreaseSpeedAtVolley As Integer ' next iVolleyNum to increase speed at
    bSpeedChanged As Integer

    ' FOR DEBUGGING
    bTestMode As Integer ' IF TRUE, BALL BOUNCES BACK WHETHER IT HITS PADDLES OR NOT
    bShowValues As Integer ' IF TRUE, DISPLAYS sMessage1..4 at bottom of screen, which can be set to show debug info
    sMessage1 As String
    sMessage2 As String
    sMessage3 As String
    sMessage4 As String

End Type ' GameType

' UDT TO HOLD THE PARAMETERS FOR EACH PLAYER PER GAME OPTION
Type InitPlayerType
    IsAvailable As Integer ' IF FALSE, THIS PLAYER IS NOT AVAILABLE FOR THE CURRENT GAME OPTION
    TeamNumber As Integer ' TEAM PLAYER IS ON FOR THIS OPTION; 1 = LEFT SIDE OF SCREEN, 2 = RIGHT; LATER EXPAND FOR OTHER GAME VARIANTS (WARLORDS, QUADRAPONG, OCTOPONG, ETC.)
    ID As String ' default player identifier, can be used to describe positions, e.g., "center", "halfback", "fullback", etc.

    ' POSITION + GRAPHIC VALUES
    color As _Unsigned Long
    x As Long ' player x position
    y As Long ' player y position
    width As Integer
    height As Integer

    ' BOUNDARIES
    minX As Long
    maxX As Long
    minY As Long
    maxY As Long

    ' KEYBOARD INPUT VARIABLES
    UpKey As Integer
    DownKey As Integer

    '' WHERE TO DISPLAY PLAYER INFO
    'scoreCol As Integer ' text column to display score at
    'scoreRow As Integer ' text row    to display score at
    'nameCol As Integer '  text column to display name  at
    'nameRow As Integer '  text row    to display name  at
End Type ' InitPlayerType

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END TYPE DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ================================================================================================================================================================
' BEGIN Dynamic Library definitions
' ================================================================================================================================================================

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

    ' FOR CONTROLLING SUB WINDOW FOCUS:
    Sub ShowWindow (ByVal hWnd As _Offset, Byval nCmdShow As Long)

End Declare

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

' ================================================================================================================================================================
' END Dynamic Library definitions
' ================================================================================================================================================================

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' 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)
Dim Shared m_VersionNum$: m_VersionNum$ = GetVersionNum$(m_ProgramName$)

' GLOBAL VARIABLES TO TRACK ERROR STATE
Dim Shared m_sError As String: m_sError = ""
Dim Shared m_sIncludeError As String: m_sIncludeError = ""
Dim Shared m_sTriggerFile As String: m_sTriggerFile = m_ProgramPath$ + "Pong.DELETE-TO-CLOSE"
Dim Shared m_sDebugFile As String: m_sDebugFile = m_ProgramPath$ + m_ProgramName$ + ".txt"

' GLOBAL VARIABLES FOR SCREEN
Dim Shared m_iScreenWidth As Long: m_iScreenWidth = 1024 ' _DesktopWidth
Dim Shared m_iScreenHeight As Long: m_iScreenHeight = 768 ' _DesktopHeight
Dim Shared m_iTextRows As Long: m_iTextRows = (m_iScreenHeight / _FontHeight)
Dim Shared m_iTextCols As Long: m_iTextCols = (m_iScreenWidth / _FontWidth)

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' 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
'DebugLog m_ProgramName$ + " starting sub main"
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
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' TODO:
' * player colors
' * how many volleys to increase ball speed
' * extra balls?
' * configure controls
' * paddle size
' * paddle distance from edge
' * court color
' * court line style (dash/dot/solid/double line/etc., line thickness, etc.)
' * reset ball speed on serve? (until volley x?)

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN PONG ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' TODO:
' * reset ball speed when serving

Sub main
    ' PONG GAME VARIABLES
    Dim iPlayer As Integer
    Dim iBall As Integer

    Dim ThisGame As GameType ' HOLDS VALUES FOR THE CURRENT GAME

    ' FOR ANGLE BALL BOUNCES OFF PADDLE
    Dim arrZone(8, 7) As Integer ' 8 speeds, 7 paddle zones
    Dim iDeflectionZone As Integer

    ' FOR FIGURING OUT BOUNCE ANGLE #1
    Dim iCenterY As Integer
    Dim dblHalf As Double
    Dim iValueA As Integer
    Dim iValueB As Integer
    Dim dblValueC As Double

    ' OTHER
    Dim sMessage As String
    Dim bFinished As Integer
    Dim iNextValue As Integer
    Dim iNextX As Integer
    Dim iNextY As Integer

    '  GAME OPTIONS
    Dim arrGameOption(1 To 2) As GameOptionType

    ' GAME OBJECTS
    Dim arrPlayer(1 To 8) As PlayerType
    Dim arrBall(1 To 8) As BallType

    ' WINDOW VARIABLES
    Dim hwndSub As _Offset ' FOR CONTROLLING WINDOW FOCUS

    ' MOUSE READER PROG
    Dim sMouseReaderProg As String

    ' VARIABLES FOR READING MICE FROM CLIENT
    Dim uintPort As _Unsigned Integer ' port
    Dim lngHost ' h&
    Dim lngConn ' c&
    Dim fTime As _Float ' MyTime##
    Dim sInput As String ' s$
    Dim sMessageType As String
    Dim sValue As String

    ' OTHER VARS
    Dim iLoop1 As Integer
    Dim iLoop2 As Integer
    Dim sNextError As String
    Dim iBallSpeed As Integer
    Dim iIndex As Integer
    Dim iCount As Integer
    Dim iCol As Integer
    Dim iRow As Integer
    Dim in$
    Dim sDebug As String

    ' FOR LOOPING THROUGH INPUT DATA FROM SUBPROGRAM
    ReDim arrMice(-1 To -1) As String
    ReDim arrMessage(-1 To -1) As String
    ReDim arrValue(-1 To -1) As String
    Dim sNextChunk As String
    Dim sNextValue As String
    Dim iValuePosition As Integer
    Dim sText As String

    ' TEST VALUES
    Dim iLastKeyDown As Integer
    Dim iLastKeyUp As Integer
    Dim sLastKeyDown As String
    Dim sLastKeyUp As String
    Dim iTestCount As Integer

    ' INITIALIZE
    m_sError = ""

    ' SHOW SOME INSTRUCTIONS
    Cls
    Color c_Cyan
    Print "MULTI MOUSE PONG v0.33 by Softintheheadware"
    Print "-------------------------------------------"
    Print "Plug in 2 or more USB mice now, then press any key to start the game."
    Print "If your PC prompts for network permission, click Allow. This will allow the "
    Print "Raw Input API subprogram to send the mouse and keyboard input to the game."
    Print "This is just to allow the program to talk to the subprogram on the same PC,"
    Print "it does NOT attempt to communicate with another computer over Internet or LAN!"
    Print "(Maybe one day we'll figure out how to do this all in one program!)"
    Print "(Also maybe we'll figure out how to read >1 mouse for Mac and Linux, too!)"
    Print "When the game first starts, it might take a few seconds to get going."
    Print
    Print "Press any key to begin..."
    Sleep
    'print "-------------------------------------------------------------------------------"

    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' BEGIN INITIALIZE INPUT SUBPROGRAM
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

    ' ================================================================================================================================================================
    ' OPEN A PORT TO TALK TO THE MOUSE READER SUBPROGRAM
    If Len(m_sError) = 0 Then
        'DebugLog "OPEN A PORT TO TALK TO THE MOUSE READER SUBPROGRAM..."

        Randomize Timer
        uintPort = Rnd * 10000 + 40000 ' between 40000 and 50000
        lngHost = _OpenHost("tcp/ip:" + _Trim$(Str$(uintPort)))
    End If

    ' ================================================================================================================================================================
    ' START THE MOUSE READER SUBPROGRAM
    If Len(m_sError) = 0 Then
        'DebugLog "START THE MOUSE READER SUBPROGRAM..."

        sMouseReaderProg = "pongsub" + m_VersionNum$ + ".exe"
        If _FileExists(sMouseReaderProg) = FALSE Then
            m_sError = ""
            m_sError = m_sError + "Subprogram " + Chr$(34) + sMouseReaderProg + Chr$(34) + " not found." + Chr$(13)
            m_sError = m_sError + "Be sure to compile " + Chr$(34) + "pongsub" + m_VersionNum$ + ".bas" + Chr$(34) + " before running this." + Chr$(13)

            Cls
            Color c_Cyan
            Print "Subprogram " + Chr$(34) + sMouseReaderProg + Chr$(34) + " not found."
            Print "Please move the file to the same folder as " + m_ProgramName$
            Print "then restart the game."
            Print
            Print "Press any key to exit"
            Sleep
            'print "-------------------------------------------------------------------------------"

        End If
    End If

    If Len(m_sError) = 0 Then
        'sDebug = "Shell _DontWait `pongsub` + m_VersionNum$ + `.exe ` + _Trim$(Str$(uintPort))"
        'sDebug = Replace$(sDebug, "`", chr$(34))
        ''DebugLog sDebug
        'sDebug = "Shell _DontWait " + chr$(34) + "pongsub" + m_VersionNum$ + ".exe" + chr$(34) + " " + _Trim$(Str$(uintPort))
        ''DebugLog sDebug
        Shell _DontWait "pongsub" + m_VersionNum$ + ".exe " + _Trim$(Str$(uintPort))
    End If

    ' ================================================================================================================================================================
    ' OPEN TCP/IP CONNECTION AS HOST
    If Len(m_sError) = 0 Then

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

        'DebugLog "OPEN TCP/IP CONNECTION AS HOST..."

        lngConn = 0
        fTime = ExtendedTimer + 30 ' TRY FOR 30 SECONDS
        Do
            lngConn = _OpenConnection(lngHost)
            If lngConn <> 0 Then Exit Do
            _Limit 60
        Loop Until Timer > fTime
        If lngConn = 0 Then
            m_sError = "Failed to open tcp/ip host connection."
            'DebugLog m_sError
        End If
    End If

    ' ================================================================================================================================================================
    ' GET CONNECTION WITH MOUSE READER SUBPROGRAM
    If Len(m_sError) = 0 Then
        'DebugLog "GET CONNECTION WITH MOUSE READER SUBPROGRAM..."

        sInput = ""
        fTime = ExtendedTimer + 30 ' TRY FOR 30 SECONDS
        Do
            Get #lngConn, , sInput
            If Left$(sInput, 2) = "w:" Then Exit Do
        Loop Until Timer > fTime

        ' remove trailing cr
        If Right$(sInput, 1) = Chr$(13) Then
            sInput = Left$(sInput, Len(sInput) - 1)
        End If

        ' is it a window handle?
        If Left$(sInput, 2) = "w:" Then
            sValue = Right$(sInput, Len(sInput) - 2)

            If IsNumber%(sValue) Then
                hwndSub = Val(sValue)
            Else
                m_sError = "Mouse reader subprogram failed to return a valid window handle."
                m_sError = m_sError + Chr$(13)
                m_sError = m_sError + "sInput=" + Chr$(34) + sInput + Chr$(34) + Chr$(13)
                m_sError = m_sError + "sValue=" + Chr$(34) + sValue + Chr$(34) + Chr$(13)
            End If
        ElseIf Left$(sInput, 2) = "e:" Then
            If Len(sInput) > 2 Then
                m_sError = Right$(sInput, Len(sInput) - 2)
            Else
                m_sError = "Unspecified error."
            End If
        Else
            m_sError = "Mouse reader subprogram failed to return a window handle."
            m_sError = m_sError + Chr$(13)
            m_sError = m_sError + "sInput=" + Chr$(34) + sInput + Chr$(34) + Chr$(13)
        End If
    End If

    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' END INITIALIZE INPUT SUBPROGRAM
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX


    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' BEGIN PRECALCULATIONS
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    If Len(m_sError) = 0 Then
        ' THIS NEEDS WORK!
        ' dx SHOULD INCREASE BASED ON SPEED, NOT dy!
        ' WHAT WAS I THINKING??

        ' SET UP BOUNCE ZONES
        ' * Ball direction depends on which of 7 segments the ball hits (NOTE: the vertical center of the ball)
        ' * The faster the horizontal speed, the greater the deflection angles
        ' * Middle = always straight

        '               HORIZONTAL SPEED (pixels)
        ' PADDLE ZONE    1  2  3  4  5  6  7  8
        ' -----------   -- -- -- -- -- -- -- --
        '           1   -3 -3 -4 -4 -5 -6 -7 -8
        '           2   -2 -2 -3 -3 -4 -5 -6 -7
        '           3   -1 -1 -2 -2 -3 -4 -5 -6
        ' MIDDLE -> 4    0  0  0  0  0  0  0  0
        '           5   +1 +1 +2 +2 +3 +4 +5 +6
        '           6   +2 +2 +3 +3 +4 +5 +6 +7
        '           7   +3 +3 +4 +4 +5 +6 +7 +8

        ' If we set this up this way in data statements,
        ' it turns out we have to do lots of loops within loops
        ' which makes for ugly complicated code - DOH!
        ' There has to be a better way!
        ' To set up the assigning the values in the simplest way,
        ' we'll rearrange the numbers in data statements
        ' so they correspond to speed first (rows), then zone (columns):

        '              ZONE
        ' SPEED  1  2  3  4  5  6  7
        ' -----  -- -- -- -- -- -- --
        '     1  -3 -2 -1  0  1  2  3
        '     2  -3 -2 -1  0  1  2  3
        '     3  -4 -3 -2  0  2  3  4
        '     4  -4 -3 -2  0  2  3  4
        '     5  -5 -4 -3  0  3  4  5
        '     6  -6 -5 -4  0  4  5  6
        '     7  -7 -6 -5  0  5  6  7
        '     8  -8 -7 -6  0  6  7  8

        BounceData:
        Data -3,-2,-1,0,1,2,3: ' DY VALUES FOR SPEED 1 (ZONES 1-7)
        Data -3,-2,-1,0,1,2,3: ' DY VALUES FOR SPEED 2 (ZONES 1-7)
        Data -4,-3,-2,0,2,3,4: ' DY VALUES FOR SPEED 3 (ZONES 1-7)
        Data -4,-3,-2,0,2,3,4: ' DY VALUES FOR SPEED 4 (ZONES 1-7)
        Data -5,-4,-3,0,3,4,5: ' DY VALUES FOR SPEED 5 (ZONES 1-7)
        Data -6,-5,-4,0,4,5,6: ' DY VALUES FOR SPEED 6 (ZONES 1-7)
        Data -7,-6,-5,0,5,6,7: ' DY VALUES FOR SPEED 7 (ZONES 1-7)
        Data -8,-7,-6,0,6,7,8: ' DY VALUES FOR SPEED 8 (ZONES 1-7)

        ' FIRST GET THE VERTICAL BALL SPEEDS FOR THE 7 PADDLE ZONES, AT 8 DIFFERENT HORIZONTAL BALL SPEEDS
        Restore BounceData
        For iBallSpeed = 1 To 8
            For iDeflectionZone = 1 To 7
                Read iNextValue
                arrZone(iBallSpeed, iDeflectionZone) = iNextValue
            Next iDeflectionZone
        Next iBallSpeed

    End If
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' END PRECALCULATIONS
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' BEGIN INITIALIZE VARIABLES
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    If Len(m_sError) = 0 Then

        ' SET UP SCREEN
        m_iScreenWidth = 1024 ' _DesktopWidth
        m_iScreenHeight = 768 ' _DesktopHeight

        ' GAME OPTIONS
        arrGameOption(1).name = "Pong Doubles"
        arrGameOption(2).name = "Pong Volleyball"
        'arrGameOption(xx).name = "Quadrapong"
        'arrGameOption(xx).name = "Foos-pong"
        'arrGameOption(xx).name = "Octopong"
        'arrGameOption(xx).name = "Warlords"
        'arrGameOption(xx).name = "S'morelords"
        'arrGameOption(xx).name = "Breakout"
        'arrGameOption(xx).name = "Hockey"
        'arrGameOption(xx).name = "Basketball"
        'arrGameOption(xx).name = "Volleyball"
        'arrGameOption(xx).name = "Curling"
        'arrGameOption(xx).name = "Pinball"
        'arrGameOption(xx).name = "Kaboom"
        'arrGameOption(xx).name = "Maze Craze"
        'arrGameOption(xx).name = "Maze War"
        'arrGameOption(xx).name = "Spacewar!"
        'arrGameOption(xx).name = "Sprint" ' = Indy 500 / Sprint / Sprint 4 / Sprint 8
        'arrGameOption(xx).name = "Omega Race"
        'arrGameOption(xx).name = "Super Octopong"
        'arrGameOption(xx).name = "Super Deluxe Octopong"
        'arrGameOption(xx).name = "Super Deluxe Octohookapong"
        'arrGameOption(xx).name = "Surround" ' Tron
        'arrGameOption(xx).name = "Drone Hunt" ' like duck hunt

        ' INITIALIZE PLAYERS
        For iPlayer = 1 To 4
            arrPlayer(iPlayer).IsActive = FALSE ' WAIT UNTIL GIVEN MOUSE IS MOVED TO ACTIVATE EACH PLAYER (IsActive = TRUE)
            arrPlayer(iPlayer).px = 0
            arrPlayer(iPlayer).py = 0
            arrPlayer(iPlayer).LeftDown = FALSE
            arrPlayer(iPlayer).LeftDownOld = FALSE
            arrPlayer(iPlayer).MiddleDown = FALSE
            arrPlayer(iPlayer).MiddleDownOld = FALSE
            arrPlayer(iPlayer).RightDown = FALSE
            arrPlayer(iPlayer).RightDownOld = FALSE
        Next iPlayer

        ' CURRENT GAME OPTIONS
        ThisGame.iGameType = LBound(arrGameOption)
        ThisGame.sGameName = arrGameOption(ThisGame.iGameType).name
        InitGame ThisGame, arrPlayer(), arrBall()

        ' INITIALIZE GAME
        bFinished = FALSE ' NO ONE HAS WON YET

        ' TEST VARIABLES
        iTestCount = -63
        iLastKeyDown = 0
        iLastKeyUp = 0
        sLastKeyDown = "(None)"
        sLastKeyUp = "(None)"
    End If
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' END INITIALIZE VARIABLES
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    ' BEGIN SETUP SCREEN + LAYERS
    ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    If Len(m_sError) = 0 Then

        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        ' INIT SCREEN
        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        Screen _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
        'Screen _NewImage(_DesktopWidth, _DesktopHeight, 32)

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

        ' CLEAR THE SCREEN
        _Dest 0: Cls , cEmpty

        ' update screen with changes & wait for next update
        _Display

        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        ' ATTEMPT FULLSCREEN
        ' *** NOT WORKING ***
        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        '_FULLSCREEN _SQUAREPIXELS
        ''_FULLSCREEN _STRETCH, _SMOOTH

        ' check that a full screen mode initialized:
        'IF _FULLSCREEN = 0 THEN
        '   _FULLSCREEN _OFF
        '   SOUND 100, .75
        'END IF

        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        ' DRAW INITIAL SCREEN
        ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
        DrawInstructionsLayer ThisGame
        RenderScreen ThisGame

        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        ' END SETUP SCREEN + LAYERS
        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        ' BEGIN MAIN LOOP
        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

        Do
            ' ================================================================================================================================================================
            ' BEGIN PROCESS INPUT FROM SUBPROGRAM
            ' ================================================================================================================================================================

            ' KEEP PONGSUB WINDOW ON TOP
            If _WindowHasFocus = TRUE Then
                ShowWindow hwndSub, 1
                '_ScreenIcon

                ' CLICK ON SCREEN TO GIVE IT THE FOCUS
                _ScreenClick 0, 0
            End If

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

            ' DID IT WORK?
            If Len(m_sError) = 0 Then

                'DebugLog "sInput=" + chr$(34) + sInput + chr$(34)

                ' HAVE DATA?
                If Len(sInput) > 0 Then
                    ' * The subprogram "PongSub" sends its data back to "PongMain"
                    '   via TCPIP in the form of a tab-delimited string. Tab is chr$(9).
                    '   Sometimes it sends multiple messages, which are separated by chr$(13).
                    '   Here we split the input by chr$(13) and process each line seperately,
                    '   which should make concurrent input smoother:

                    split sInput, Chr$(13), arrMessage() ' SPLIT OUTPUT INTO PAGES

                    For iLoop2 = LBound(arrMessage) To UBound(arrMessage)
                        sInput = arrMessage(iLoop2)

                        ' WHAT KIND OF MESSAGE?
                        sMessageType = Left$(sInput, 2)

                        'DebugLog "sMessageType=" + chr$(34) + sMessageType + chr$(34)

                        If sMessageType = "d:" Then
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' BEGIN KEY DOWN
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            sValue = Right$(sInput, Len(sInput) - 2)
                            If IsNumber%(sValue) Then
                                iLastKeyDown = Val(sValue)
                                sLastKeyDown = VirtualKeyCodeToString$(iLastKeyDown)

                                'DebugLog "sLastKeyDown=" + chr$(34) + sLastKeyDown + chr$(34)

                                ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                ' BEGIN PROCESS KEYBOARD INPUT
                                ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                ' TODO: cleanup logic, move into Case or ElseIf structure

                                '' PLAYER 1 KEYBOARD INPUT
                                'if iLastKeyDown = arrPlayer(1).UpKey then
                                '   arrPlayer(1).y = arrPlayer(1).y - arrBall(1).size
                                '   IF arrPlayer(1).y < arrPlayer(1).minY THEN
                                '       arrPlayer(1).y = arrPlayer(1).minY
                                '   END IF
                                'ElseIf iLastKeyDown = arrPlayer(1).DownKey then
                                '   arrPlayer(1).y = arrPlayer(1).y + arrBall(1).size
                                '   IF arrPlayer(1).y > arrPlayer(1).maxY THEN
                                '       arrPlayer(1).y = arrPlayer(1).maxY
                                '   END IF
                                'END IF
                                '
                                '' PLAYER 2 KEYBOARD INPUT
                                'if iLastKeyDown = arrPlayer(2).UpKey then
                                '   arrPlayer(2).y = arrPlayer(2).y - arrBall(1).size
                                '   IF arrPlayer(2).y < arrPlayer(2).minY THEN
                                '       arrPlayer(2).y = arrPlayer(2).minY
                                '   END IF
                                'ElseIf iLastKeyDown = arrPlayer(2).DownKey then
                                '   arrPlayer(2).y = arrPlayer(2).y + arrBall(1).size
                                '   IF arrPlayer(2).y > arrPlayer(2).maxY THEN
                                '       arrPlayer(2).y = arrPlayer(2).maxY
                                '   END IF
                                'END IF

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF "1" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_1 Then
                                    ' SELECT GAME VARIATION
                                    ThisGame.iGameType = ThisGame.iGameType + 1
                                    If ThisGame.iGameType > UBound(arrGameOption) Then
                                        ThisGame.iGameType = LBound(arrGameOption)
                                    End If
                                    ThisGame.sGameName = arrGameOption(ThisGame.iGameType).name

                                    ThisGame.sMessage1 = "ThisGame.iGameType=" + _Trim$(Str$(ThisGame.iGameType))

                                    InitGame ThisGame, arrPlayer(), arrBall()

                                    DrawInstructionsLayer ThisGame
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF "2" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                'If iLastKeyDown = VK_RETURN Then
                                If iLastKeyDown = VK_2 Then
                                    If ThisGame.iGameMode = cAttractMode Then ' or ThisGame.iGameMode = cGameOver then
                                        ' ARE 2 OR MORE PLAYERS READY?
                                        iCount = 0
                                        For iPlayer = 1 To 4
                                            If arrPlayer(iPlayer).IsActive = TRUE Then
                                                iCount = iCount + 1
                                            End If
                                        Next iPlayer
                                        If iCount > 1 Then
                                            ThisGame.iGameMode = cInPlay

                                            ' RESET SCORES
                                            For iPlayer = 1 To 4
                                                arrPlayer(iPlayer).Score = 0
                                            Next iPlayer
                                            ThisGame.iScore1 = 0
                                            ThisGame.iScore2 = 0

                                            ' UPDATE INSTRUCTIONS
                                            DrawInstructionsLayer ThisGame

                                            ' SERVE BALL
                                            ServeBall ThisGame, arrBall(1), arrPlayer(), 0

                                        End If
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF "3" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_3 Then
                                    If ThisGame.iGameMode = cInPlay Or ThisGame.iGameMode = cPaused Then
                                        ThisGame.iGameMode = cAttractMode
                                        InitGame ThisGame, arrPlayer(), arrBall()
                                        DrawInstructionsLayer ThisGame
                                    Else
                                        ' (ignore)
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF "4" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_4 Then
                                    ' TOGGLE SHOW VALUES MODE
                                    If ThisGame.bShowValues = FALSE Then
                                        ThisGame.bShowValues = TRUE
                                        ThisGame.sMessage4 = "bShowValues=TRUE"
                                    Else
                                        ThisGame.bShowValues = FALSE
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF "5" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_5 Then
                                    ' TOGGLE TEST MODE
                                    If ThisGame.bTestMode = FALSE Then
                                        ThisGame.bTestMode = TRUE
                                    Else
                                        ThisGame.bTestMode = FALSE
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF PAUSE "P" KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_P Then
                                    If ThisGame.iGameMode = cInPlay Then
                                        ThisGame.iGameMode = cPaused
                                        DrawInstructionsLayer ThisGame
                                    ElseIf ThisGame.iGameMode = cPaused Then
                                        ThisGame.iGameMode = cInPlay
                                        DrawInstructionsLayer ThisGame
                                    Else
                                        ' (ignore)
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF - (MINUS) KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_OEM_MINUS Then
                                    ThisGame.iSpeed = ThisGame.iSpeed - 1
                                    If ThisGame.iSpeed < ThisGame.MinSpeed Then
                                        ThisGame.iSpeed = ThisGame.MinSpeed
                                    Else
                                        ThisGame.bSpeedChanged = TRUE
                                    End If
                                End If

                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                ' CHECK IF =/+ KEY IS PRESSED
                                ' KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK
                                If iLastKeyDown = VK_OEM_PLUS Then
                                    ThisGame.iSpeed = ThisGame.iSpeed + 1
                                    If ThisGame.iSpeed > ThisGame.MaxSpeed Then
                                        ThisGame.iSpeed = ThisGame.MaxSpeed
                                    Else
                                        ThisGame.bSpeedChanged = TRUE
                                    End If
                                End If

                                ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                                ' END PROCESS KEYBOARD INPUT
                                ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                            Else
                                ' INVALID KEYBOARD INPUT = NOT A NUMBER
                                iLastKeyDown = 0
                                sLastKeyDown = Chr$(34) + sValue + Chr$(34) + " IS NOT A NUMBER"
                            End If
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' END KEY DOWN
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

                        ElseIf sMessageType = "u:" Then
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' BEGIN KEY UP
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            sValue = Right$(sInput, Len(sInput) - 2)
                            If IsNumber%(sValue) Then
                                ' KEY WAS RELEASED
                                iLastKeyUp = Val(sValue)
                                sLastKeyUp = VirtualKeyCodeToString$(iLastKeyUp)
                            Else
                                ' INVALID KEYBOARD INPUT = NOT A NUMBER
                                iLastKeyUp = 0
                                sLastKeyUp = Chr$(34) + sValue + Chr$(34) + " IS NOT A NUMBER"
                            End If
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' END KEY UP
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

                        ElseIf sMessageType = "m:" Then
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' BEGIN MOUSE MOVED
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

                            ' SPLIT INPUT INTO VALUES
                            ' cForPlayer cMouseDX cMouseDY cMousePosX cMousePosY cMouseWheel cMouseLeftDown cMouseMiddleDown cMouseRightDown
                            ' {mouse #}\t{dx}\t{dy}\t{pos x}\t{pos y}\t{wheel}\t{leftDown}\t{middleDown}\t{rightDown}\n
                            sValue = Right$(sInput, Len(sInput) - 2)
                            split sValue, Chr$(9), arrValue() ' SPLIT OUTPUT INTO PAGES

                            ' PROCESS VALUES
                            iIndex = -1
                            iValuePosition = 0

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

                                ' GET VALUE
                                sNextValue = _Trim$(arrValue(iLoop1))

                                ' 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)
                                            'DebugLog "MOUSE " + sNextValue + " INPUT, iIndex = " + _Trim$(Str$(iIndex))

                                            If iIndex >= 0 And iIndex < 4 Then
                                                iIndex = iIndex + 1
                                            Else
                                                'DebugLog "SKIPPING THIS MOUSE"
                                                Exit For
                                            End If

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

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

                                        Case cMousePosX:
                                            ' READ RAW VALUE
                                            arrPlayer(iIndex).px = Val(sNextValue)
                                            If arrPlayer(iIndex).px < arrPlayer(iIndex).minX Then
                                                arrPlayer(iIndex).x = arrPlayer(iIndex).minX
                                            ElseIf arrPlayer(iIndex).px > arrPlayer(iIndex).maxX Then
                                                arrPlayer(iIndex).x = arrPlayer(iIndex).maxX
                                            Else
                                                arrPlayer(iIndex).x = arrPlayer(iIndex).px
                                            End If

                                        Case cMousePosY:
                                            ' READ RAW VALUE
                                            arrPlayer(iIndex).py = Val(sNextValue)
                                            If arrPlayer(iIndex).py < arrPlayer(iIndex).minY Then
                                                arrPlayer(iIndex).y = arrPlayer(iIndex).minY
                                            ElseIf arrPlayer(iIndex).py > arrPlayer(iIndex).maxY Then
                                                arrPlayer(iIndex).y = arrPlayer(iIndex).maxY
                                            Else
                                                arrPlayer(iIndex).y = arrPlayer(iIndex).py
                                            End If

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

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

                                            ' DID VALUE CHANGE?
                                            If arrPlayer(iIndex).LeftDown <> arrPlayer(iIndex).LeftDownOld Then
                                                If arrPlayer(iIndex).LeftDown = TRUE Then
                                                    'DebugLog "arrPlayer(" + _Trim$(Str$(iIndex)) + ").LeftDown = TRUE"

                                                    ' CLICKED BUTTON
                                                    If ThisGame.iGameMode = cAttractMode Then
                                                        'DebugLog "MOUSE " + _Trim$(Str$(iIndex)) + " LEFT CLICKED: arrPlayer(" + _Trim$(Str$(iIndex)) + ").IsActive = TRUE"
                                                        If arrPlayer(iIndex).IsAvailable = TRUE Then
                                                            arrPlayer(iIndex).IsActive = TRUE
                                                        Else
                                                            arrPlayer(iIndex).IsActive = FALSE
                                                        End If
                                                    End If
                                                Else
                                                    ' RELEASED BUTTON
                                                End If
                                                arrPlayer(iIndex).LeftDownOld = arrPlayer(iIndex).LeftDown
                                            End If

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

                                            ' DID VALUE CHANGE?
                                            If arrPlayer(iIndex).MiddleDown <> arrPlayer(iIndex).MiddleDownOld Then
                                                If arrPlayer(iIndex).MiddleDown = TRUE Then
                                                    ' CLICKED BUTTON
                                                Else
                                                    ' RELEASED BUTTON
                                                End If
                                                arrPlayer(iIndex).MiddleDownOld = arrPlayer(iIndex).MiddleDown
                                            End If

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

                                            ' DID VALUE CHANGE?
                                            If arrPlayer(iIndex).RightDown <> arrPlayer(iIndex).RightDownOld Then
                                                If arrPlayer(iIndex).RightDown = TRUE Then
                                                    ' CLICKED BUTTON
                                                    If ThisGame.iGameMode = cAttractMode Then
                                                        'DebugLog "MOUSE " + _Trim$(Str$(iIndex)) + " RIGHT CLICKED: arrPlayer(" + _Trim$(Str$(iIndex)) + ").IsActive = FALSE"
                                                        arrPlayer(iIndex).IsActive = FALSE
                                                    End If
                                                Else
                                                    ' RELEASED BUTTON
                                                End If
                                                arrPlayer(iIndex).RightDownOld = arrPlayer(iIndex).RightDown
                                            End If

                                        Case Else:
                                            ' Unknown
                                    End Select
                                Else
                                    ' (VALUE ISN'T A NUMBER)
                                    ' (DO NOTHING)
                                    'DebugPrint "                " + _
                                    '    "Value " + _
                                    '    chr$(34) + sNextValue + chr$(34) + " " + _
                                    '    "at position " + _
                                    '    _Trim$(Str$(iLoop1)) + " " + _
                                    '    "(" + MouseDataPositionToString(iLoop1) + ") " + _
                                    '    "is not a number."
                                End If

                            Next iLoop1
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' END MOUSE MOVED
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

                        ElseIf sMessageType = "e:" Then
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' BEGIN INPUT ERROR
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            sValue = Right$(sInput, Len(sInput) - 2)
                            'DebugLog "Received error message from subprogram: " + sValue
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                            ' END INPUT ERROR
                            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
                        Else
                            'DebugLog "Message type from subprogram not recognized: " + sInput
                        End If
                    Next iLoop2
                Else
                    ' RETRIEVED DATA IS EMPTY
                    ' (JUST IGNORE FOR NOW)
                    ' DebugPrint "    DATA IS EMPTY"
                    ' TODO:
                    ' WE SHOULD QUIT IF NO DATA IS RETRIEVED IN A CERTAIN THRESHOLD OF TIME...
                End If
            Else
                ' ERROR RETRIEVING DATA...
                'DebugLog "ERROR RETRIEVING DATA FROM CONNECTION: " + m_sError
                ErrorClear
                ' (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
                ' TODO:
                ' WE SHOULD QUIT HERE, AND RESTART THE CLIENT...
            End If
            ' ================================================================================================================================================================
            ' END PROCESS INPUT FROM SUBPROGRAM
            ' ================================================================================================================================================================

            ' ================================================================================================================================================================
            ' BEGIN GAME LOGIC
            ' ================================================================================================================================================================
            ' *** THIS PART NEEDS WORK - THE MATH OF THE BALL ANGLES SEEMS WRONG ***

            If ThisGame.iGameMode = cInPlay Then
                '' LOOK AHEAD
                'arrBall(1).nextX = arrBall(1).x + arrBall(1).dx
                'arrBall(1).nextY = arrBall(1).y + arrBall(1).dy
                '(test first for look ahead)

                ' MOVE BALL HORIZONTALLY
                arrBall(1).x = arrBall(1).x + arrBall(1).dx

                If arrBall(1).x > arrBall(1).maxX Then
                    ' ****************************************************************************************************************************************************************
                    ' TEAM 1 SCORES
                    ' ****************************************************************************************************************************************************************
                    ScoreSound1
                    _Delay 2 ' WAIT A COUPLE SECONDS
                    ThisGame.iScore1 = ThisGame.iScore1 + 1 ' AWARD THEM A POINT

                    ' IF THEY WON THEN END GAME
                    If ThisGame.iScore1 >= ThisGame.iScoreToWin Then
                        ThisGame.iGameMode = cAttractMode
                        'EXIT DO
                    Else
                        ' The person who misses the ball always receives the next serve
                        ServeBall ThisGame, arrBall(1), arrPlayer(), 2
                    End If

                ElseIf arrBall(1).x < arrBall(1).minX Then
                    ' ****************************************************************************************************************************************************************
                    ' TEAM 2 SCORES
                    ' ****************************************************************************************************************************************************************
                    ScoreSound1
                    _Delay 2 ' WAIT A COUPLE SECONDS
                    ThisGame.iScore2 = ThisGame.iScore2 + 1 ' AWARD THEM A POINT

                    ' IF THEY WON THEN END GAME
                    If ThisGame.iScore2 >= ThisGame.iScoreToWin Then
                        ThisGame.iGameMode = cAttractMode
                        'EXIT DO
                    Else
                        ' The person who misses the ball always receives the next serve
                        ServeBall ThisGame, arrBall(1), arrPlayer(), 1
                    End If

                ElseIf (arrBall(1).x < arrPlayer(1).x) And (arrPlayer(1).IsActive = TRUE) And (ThisGame.bTestMode = TRUE) Then
                    ' ****************************************************************************************************************************************************************
                    ' TEST MODE BOUNCE BACK FOR PADDLE #1
                    ' ****************************************************************************************************************************************************************
                    BounceBack ThisGame, arrBall(1), arrPlayer(1), cLeftDir

                ElseIf (arrBall(1).x > arrPlayer(2).x) And (arrPlayer(2).IsActive = TRUE) And (ThisGame.bTestMode = TRUE) Then
                    ' ****************************************************************************************************************************************************************
                    ' TEST MODE BOUNCE BACK FOR PADDLE #2
                    ' ****************************************************************************************************************************************************************
                    BounceBack ThisGame, arrBall(1), arrPlayer(2), cRightDir

                ElseIf (arrBall(1).x < arrPlayer(3).x) And (arrPlayer(3).IsActive = TRUE) And (ThisGame.bTestMode = TRUE) Then
                    ' ****************************************************************************************************************************************************************
                    ' TEST MODE BOUNCE BACK FOR PADDLE #3
                    ' ****************************************************************************************************************************************************************
                    BounceBack ThisGame, arrBall(1), arrPlayer(3), cLeftDir

                ElseIf (arrBall(1).x > arrPlayer(4).x) And (arrPlayer(4).IsActive = TRUE) And (ThisGame.bTestMode = TRUE) Then
                    ' ****************************************************************************************************************************************************************
                    ' TEST MODE BOUNCE BACK FOR PADDLE #4
                    ' ****************************************************************************************************************************************************************
                    BounceBack ThisGame, arrBall(1), arrPlayer(4), cRightDir

                ELSEIF ( (arrBall(1).x >= arrPlayer(1).x) AND (arrBall(1).x < (arrPlayer(1).x + arrPlayer(1).width)) ) AND _
                       ( (arrBall(1).y >= arrPlayer(1).y) AND (arrBall(1).y <= (arrPlayer(1).y + arrPlayer(1).height)) ) AND _
                       (arrPlayer(1).IsActive = true) then

                    ' ****************************************************************************************************************************************************************
                    ' BALL HIT PADDLE #1
                    ' ****************************************************************************************************************************************************************
                    BallHitPaddle ThisGame, arrBall(1), arrPlayer(1), arrZone()

                ELSEIF ( (arrBall(1).x >= arrPlayer(2).x) AND (arrBall(1).x < (arrPlayer(2).x + arrPlayer(2).width)) ) AND _
                       ( (arrBall(1).y >= arrPlayer(2).y) AND (arrBall(1).y <= (arrPlayer(2).y + arrPlayer(2).height)) ) AND _
                       (arrPlayer(2).IsActive = true) then

                    ' ****************************************************************************************************************************************************************
                    ' BALL HIT PADDLE #2
                    ' ****************************************************************************************************************************************************************
                    BallHitPaddle ThisGame, arrBall(1), arrPlayer(2), arrZone()

                ELSEIF ( (arrBall(1).x >= arrPlayer(3).x) AND (arrBall(1).x < (arrPlayer(3).x + arrPlayer(3).width)) ) AND _
                       ( (arrBall(1).y >= arrPlayer(3).y) AND (arrBall(1).y <= (arrPlayer(3).y + arrPlayer(3).height)) ) AND _
                       (arrPlayer(3).IsActive = true) then

                    ' ****************************************************************************************************************************************************************
                    ' BALL HIT PADDLE #3
                    ' ****************************************************************************************************************************************************************
                    BallHitPaddle ThisGame, arrBall(1), arrPlayer(3), arrZone()

                ELSEIF ( (arrBall(1).x >= arrPlayer(4).x) AND (arrBall(1).x < (arrPlayer(4).x + arrPlayer(4).width)) ) AND _
                       ( (arrBall(1).y >= arrPlayer(4).y) AND (arrBall(1).y <= (arrPlayer(4).y + arrPlayer(4).height)) ) AND _
                       (arrPlayer(4).IsActive = true) then

                    ' ****************************************************************************************************************************************************************
                    ' BALL HIT PADDLE #4
                    ' ****************************************************************************************************************************************************************
                    BallHitPaddle ThisGame, arrBall(1), arrPlayer(4), arrZone()

                Else
                    ' COUNT VOLLEY IF BALL CROSSES CENTER LINE

                    ' IS BALL TRAVELING IN THE RIGHT DIRECTION?
                    If ((ThisGame.iVolleyDir < 0) And (arrBall(1).dx < 0)) Or ((ThisGame.iVolleyDir > 0) And (arrBall(1).dx > 0)) Then

                        If arrBall(1).dx < 0 Then
                            ' MOVING TO THE LEFT
                            If arrBall(1).x < (m_iScreenWidth / 2) Then
                                ' INCREASE VOLLEY #
                                ThisGame.iVolleyNum = ThisGame.iVolleyNum + 1

                                ' NOW WAIT FOR BALL TO CROSS IN OTHER DIRECTION
                                ThisGame.iVolleyDir = 0 - ThisGame.iVolleyDir

                                If ThisGame.iVolleyNum >= ThisGame.iIncreaseSpeedAtVolley Then
                                    ' SPEED UP
                                    ThisGame.iSpeed = ThisGame.iSpeed + 1
                                    ThisGame.bSpeedChanged = TRUE

                                    ' PAST MAX SPEED?
                                    If ThisGame.iSpeed > ThisGame.MaxSpeed Then
                                        ThisGame.iSpeed = ThisGame.MinSpeed
                                    End If

                                    ' GET VOLLEY # FOR NEXT SPEED INCREASE
                                    ThisGame.iIncreaseSpeedAtVolley = ThisGame.iVolleyNum + ThisGame.iIncreaseSpeedEvery
                                End If

                            End If
                        ElseIf arrBall(1).dx > 0 Then
                            ' MOVING TO THE RIGHT
                            If arrBall(1).x > (m_iScreenWidth / 2) Then
                                ' INCREASE VOLLEY #
                                ThisGame.iVolleyNum = ThisGame.iVolleyNum + 1

                                ' NOW WAIT FOR BALL TO CROSS IN OTHER DIRECTION
                                ThisGame.iVolleyDir = 0 - ThisGame.iVolleyDir

                                If ThisGame.iVolleyNum >= ThisGame.iIncreaseSpeedAtVolley Then
                                    ' SPEED UP
                                    ThisGame.iSpeed = ThisGame.iSpeed + 1
                                    ThisGame.bSpeedChanged = TRUE

                                    ' PAST MAX SPEED?
                                    If ThisGame.iSpeed > ThisGame.MaxSpeed Then
                                        ThisGame.iSpeed = ThisGame.MinSpeed
                                    End If

                                    ' GET VOLLEY # FOR NEXT SPEED INCREASE
                                    ThisGame.iIncreaseSpeedAtVolley = ThisGame.iVolleyNum + ThisGame.iIncreaseSpeedEvery
                                End If

                            End If
                        End If
                    End If
                End If

                ' MOVE BALL VERTICALLY
                arrBall(1).y = arrBall(1).y + arrBall(1).dy
                If arrBall(1).y < arrBall(1).minY Then
                    arrBall(1).y = arrBall(1).minY
                    arrBall(1).dy = 0 - arrBall(1).dy
                    WallSound1
                ElseIf arrBall(1).y > arrBall(1).maxY Then
                    arrBall(1).y = arrBall(1).maxY
                    arrBall(1).dy = 0 - arrBall(1).dy
                    WallSound2
                End If

                ' SHOW IN-GAME VALUES
                If ThisGame.bShowValues = TRUE Then
                    ' SHOW BALL SPEED
                    ThisGame.sMessage2 = "BALL: DX=" + cstr$(arrBall(1).dx) + " DY=" + cstr$(arrBall(1).dy)

                    ' SHOW VOLLEY # NEXT SPEED INCREASE AT
                    ThisGame.sMessage3 = "INCREASE AT: " + cstr$(ThisGame.iIncreaseSpeedAtVolley)
                End If

                ' DID GAME END?
                If ThisGame.iGameMode = cAttractMode Then
                    ' DEACTIVATE PLAYERS
                    For iPlayer = 1 To 4
                        arrPlayer(iPlayer).IsActive = FALSE
                    Next iPlayer

                    ' UPDATE INSTRUCTIONS
                    DrawInstructionsLayer ThisGame
                End If

            End If

            ' ================================================================================================================================================================
            ' END GAME LOGIC
            ' ================================================================================================================================================================

            ' ================================================================================================================================================================
            ' BEGIN UPDATE DISPLAY
            ' ================================================================================================================================================================

            ' DRAW THE SCORE LAYER
            DrawScoreLayer ThisGame

            ' DRAW THE OBJECTS LAYER
            DrawObjectsLayer ThisGame, arrPlayer(), arrBall()

            ' COPY LAYERS TO SCREEN AND UPDATE DISPLAY
            RenderScreen ThisGame

            ' ================================================================================================================================================================
            ' END UPDATE DISPLAY
            ' ================================================================================================================================================================

            ' ================================================================================================================================================================
            ' QUIT ONCE PLAYER PRESSES ESC KEY
            ' ================================================================================================================================================================
            If iLastKeyDown = VK_ESCAPE Then
                'DebugLog "USER HIT ESCAPE, DELETING TRIGGER FILE"
                DeleteFile m_sTriggerFile
            End If

            ' ================================================================================================================================================================
            ' QUIT IF TRIGGER FILE IS GONE
            ' ================================================================================================================================================================
            If _FileExists(m_sTriggerFile) = FALSE Then
                'DebugLog "_FileExists(" + chr$(34) + m_sTriggerFile + chr$(34) + ") = FALSE, exiting"
                Exit Do
            End If

            ' SET GAME SPEED IN FPS
            '_Limit 60 ' run 60 fps
        Loop While _Connected(lngConn)

        ' RETURN TO AUTODISPLAY
        _AutoDisplay

        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
        ' END MAIN LOOP
        ' XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

        'DebugLog "EXITED MAIN LOOP"
    End If

    If Len(m_sError) > 0 Then
        _AutoDisplay
        'DebugLog "ERROR: " + m_sError

        Screen 0
        Print m_sError
        Input "PRESS ENTER TO CONTINUE TEST"; in$
    End If

End Sub ' main

' /////////////////////////////////////////////////////////////////////////////
' COPY LAYERS TO SCREEN AND UPDATE DISPLAY

Sub RenderScreen (ThisGame As GameType)
    ' CLEAR THE SCREEN
    _Dest 0: Cls , cEmpty

    ' Add the background
    _PutImage , ThisGame.imgBackground, 0

    ' Add the walls
    _PutImage , ThisGame.imgWalls, 0

    ' Add the instructions
    _PutImage , ThisGame.imgText, 0

    ' Add the score
    _PutImage , ThisGame.imgScore, 0

    ' Add the players
    _PutImage , ThisGame.imgPlayers, 0

    ' update screen with changes
    _Display
End Sub ' RenderScreen

' /////////////////////////////////////////////////////////////////////////////
' DRAW THE INSTRUCTIONS LAYER

' TEXT SCREEN AT 1024 x  768 = 128 x 48 (m_iTextCols x m_iTextRows)

' DrawInstructionsLayer ThisGame

Sub DrawInstructionsLayer (ThisGame As GameType)
    Dim sMessage As String
    Dim iCol As Integer

    _Dest ThisGame.imgText: Cls , cEmpty

    Select Case ThisGame.sGameName
        Case "Pong Doubles", "Pong Volleyball":
            Select Case ThisGame.iGameMode
                Case cAttractMode:
                    ' ROW 1: PLUG MOUSE / TITLE
                    Color cWhite, cEmpty
                    sMessage = "MULTI-MOUSE PONG BY SOFTINTHEHEADWARE"
                    iCol = (m_iTextCols / 2) - (Len(sMessage) / 2)
                    Locate 2, iCol: Print sMessage; ' CENTERED TEXT

                    Color cYellow, cEmpty
                    sMessage = "PLUG IN A USB MOUSE FOR EACH PLAYER"
                    iCol = (m_iTextCols / 2) - (Len(sMessage) / 2)
                    Locate 3, iCol: Print sMessage; ' CENTERED TEXT

                    ' ROW 2: SCORE

                    ' ROW 47: CLICK MOUSE
                    Color cYellow, cEmpty
                    sMessage = "EACH PLAYER: CLICK MOUSE TO JOIN GAME, RIGHT-CLICK TO LEAVE"
                    Locate 47, ((m_iTextCols / 2) - (Len(sMessage) / 2)): Print sMessage; ' CENTERED TEXT
                    'Locate 47, 64 - (Len(sMessage) / 2): Print sMessage;

                    ' ROW 48: GAME OPTIONS
                    Color cWhite, cEmpty
                    sMessage = "[1] SELECT GAME"
                    iCol = (((m_iTextCols / 3) / 2) - (Len(sMessage) / 2))
                    Locate 48, iCol: Print sMessage;
                    Color cCyan, cEmpty
                    Locate 48, iCol + Len(sMessage) + 2: Print ThisGame.sGameName;

                    Color cWhite, cEmpty
                    sMessage = "[2] START  GAME"
                    Locate 48, ((m_iTextCols / 2) - (Len(sMessage) / 2)): Print sMessage ' MIDDLE THIRD

                    sMessage = "[ESC] EXIT"
                    Locate 48, (m_iTextCols - ((m_iTextCols / 3) / 2)) - (Len(sMessage) / 2): Print sMessage; ' RIGHT THIRD

                Case cInPlay:
                    ' PRINT TITLE ROW 1
                    Color cWhite, cEmpty
                    sMessage = "MULTI-MOUSE PONG BY SOFTINTHEHEADWARE"
                    iCol = (m_iTextCols / 2) - (Len(sMessage) / 2)
                    Locate 1, iCol: Print sMessage; ' CENTERED TEXT
                    'Locate 1, 64 - (Len(sMessage) / 2): Print sMessage; ' CENTER

                    ' SHOW MORE INSTRUCTIONS ROW 47
                    Color cWhite, cEmpty
                    sMessage = "AVOID MISSING BALL FOR HIGH SCORE"
                    Locate 46, 64 - (Len(sMessage) / 2): Print sMessage; ' CENTER

                    ' ROW 48: GAME OPTIONS
                    Color cWhite, cEmpty
                    'sMessage = "[2] START  GAME"
                    sMessage = "[3] CANCEL GAME"
                    iCol = (((m_iTextCols / 3) / 2) - (Len(sMessage) / 2))
                    Locate 48, iCol: Print sMessage;
                    Color cCyan, cEmpty
                    Locate 48, iCol + Len(sMessage) + 2: Print ThisGame.sGameName;

                    sMessage = "[ESC] EXIT"
                    Locate 48, (m_iTextCols - ((m_iTextCols / 3) / 2)) - (Len(sMessage) / 2): Print sMessage; ' RIGHT THIRD

                Case cPaused:
                    ' PRINT TITLE ROW 1
                    Color cWhite, cEmpty
                    sMessage = "MULTI-MOUSE PONG BY SOFTINTHEHEADWARE"
                    iCol = (m_iTextCols / 2) - (Len(sMessage) / 2)
                    Locate 1, iCol: Print sMessage; ' CENTERED TEXT
                    'Locate 1, 64 - (Len(sMessage) / 2): Print sMessage;

                    ' SHOW MORE INSTRUCTIONS ROW 47
                    Color cWhite, cEmpty
                    sMessage = "PAUSED - PRESS " + Chr$(34) + "P" + Chr$(34) + " TO CONTINUE"
                    Locate 47, 64 - (Len(sMessage) / 2): Print sMessage;

                    ' ROW 48: GAME OPTIONS
                    Color cWhite, cEmpty
                    'sMessage = "[2] START  GAME"
                    'sMessage = "    PLAYING GAME"
                    sMessage = "[P]  RESUME GAME"
                    iCol = (((m_iTextCols / 3) / 2) - (Len(sMessage) / 2))
                    Locate 48, iCol: Print sMessage;
                    Color cCyan, cEmpty
                    Locate 48, iCol + Len(sMessage) + 2: Print ThisGame.sGameName;

                    sMessage = "[ESC] EXIT"
                    Locate 48, (m_iTextCols - ((m_iTextCols / 3) / 2)) - (Len(sMessage) / 2): Print sMessage; ' RIGHT THIRD
            End Select
    End Select

End Sub ' DrawInstructionsLayer

' /////////////////////////////////////////////////////////////////////////////
' DRAW THE SCORE LAYER

' TEXT SCREEN AT 1024 x  768 = 128 x 48 (m_iTextCols x m_iTextRows)

' DrawScoreLayer ThisGame

Sub DrawScoreLayer (ThisGame As GameType)
    Dim sMessage As String
    Dim iCol As Integer

    _Dest ThisGame.imgScore: Cls , cEmpty

    Select Case ThisGame.sGameName
        Case "Pong Doubles", "Pong Volleyball":
            ' PRINT SCORES
            If ThisGame.iScore1 < ThisGame.iScoreToWin Then
                Color cCyan, cEmpty
            Else
                Color ThisGame.ulngScreenColor, cCyan
            End If
            Locate ThisGame.iTeam1ScoreRow, ThisGame.iTeam1ScoreColumn: Print _Trim$(Str$(ThisGame.iScore1));
            If ThisGame.iScore2 < ThisGame.iScoreToWin Then
                Color cCyan, cEmpty
            Else
                Color ThisGame.ulngScreenColor, cCyan
            End If
            Locate ThisGame.iTeam2ScoreRow, ThisGame.iTeam2ScoreColumn: Print _Trim$(Str$(ThisGame.iScore2));

            ' PRINT OTHER INFO
            If ThisGame.iGameMode = cInPlay Or ThisGame.iGameMode = cPaused Then
                Color cWhite, cEmpty
                Locate 46, 32: Print "SPEED: " + cstr$(ThisGame.iSpeed); ' SHOW SPEED
                Locate 46, 96: Print "VOLLEY: " + cstr$(ThisGame.iVolleyNum); ' SHOW VOLLEY #
            End If
        Case Else:
            ' FOR INDIVIDUAL COMPETITION GAMES:
            'for iPlayer = 1 to 4
            '   if arrPlayer(iPlayer).IsActive = true then
            '       IF arrPlayer(iPlayer).score < ThisGame.iScoreToWin THEN
            '           color arrPlayer(iPlayer).color, cEmpty
            '       else
            '           color ThisGame.ulngScreenColor, arrPlayer(iPlayer).color
            '       end if
            '       LOCATE arrPlayer(iPlayer).nameRow, arrPlayer(iPlayer).nameCol: PRINT arrPlayer(iPlayer).ID + ": ";
            '       LOCATE arrPlayer(iPlayer).scoreRow, arrPlayer(iPlayer).scoreCol: PRINT _Trim$(Str$(arrPlayer(1).score));
            '   end if
            'next iPlayer
    End Select

    ' SHOW TEST VALUES?
    If ThisGame.bShowValues = TRUE Then
        Color cHotPink, cEmpty
        Locate 47, 1: Print ThisGame.sMessage1;
        Locate 47, 32: Print ThisGame.sMessage2;
        Locate 47, 64: Print ThisGame.sMessage3;
        Locate 47, 96: Print ThisGame.sMessage4;
    End If

    ' INDICATE IF WE'RE IN TEST MODE
    If ThisGame.bTestMode = TRUE Then
        Color cHotPink, cEmpty
        Locate 47, 120: Print "TEST MODE";
    End If

End Sub ' DrawScoreLayer

' /////////////////////////////////////////////////////////////////////////////
' DRAW THE OBJECTS LAYER

' DrawObjectsLayer ThisGame, arrPlayer(), arrBall()

Sub DrawObjectsLayer (ThisGame As GameType, arrPlayer() As PlayerType, arrBall() As BallType)
    Dim iPlayer As Integer
    Dim iBall As Integer

    _Dest ThisGame.imgPlayers: Cls , cEmpty

    Select Case ThisGame.sGameName
        Case "Pong Doubles", "Pong Volleyball":

            For iPlayer = 1 To 4
                If arrPlayer(iPlayer).IsActive = TRUE Then
                    DrawRectSolid arrPlayer(iPlayer).x, arrPlayer(iPlayer).y, arrPlayer(iPlayer).width, arrPlayer(iPlayer).height, arrPlayer(iPlayer).color
                End If
            Next iPlayer

            For iBall = 1 To 1
                If ThisGame.iGameMode = cInPlay Or ThisGame.iGameMode = cPaused Then
                    DrawBoxSolid arrBall(iBall).x, arrBall(iBall).y, arrBall(iBall).size, arrBall(iBall).color
                End If
            Next iBall
    End Select
End Sub ' DrawObjectsLayer' /////////////////////////////////////////////////////////////////////////////
' arrInitPlayer({iNumPlayers}, [iPlayer}) as InitPlayerType

' NOTE: scoreCol, scoreRow, nameCol, nameRow are for future use
'       such as games where players compete individually,
'       currently all game options use team play

Sub InitGame (ThisGame As GameType, arrPlayer() As PlayerType, arrBall() As BallType)
    Dim arrInitPlayer(1 To 4) As InitPlayerType ' (# players): for now support upto 4 players, later we'll add 8 or 16
    Dim iNumPlayers As Integer
    Dim iPlayer As Integer
    Dim iNextX As Integer
    Dim iNextY As Integer

    ' ENABLE/DISABLE DEBUGGING
    'ThisGame.bTestMode = FALSE
    'ThisGame.bShowValues = FALSE
    'ThisGame.sMessage1 = ""
    'ThisGame.sMessage2 = ""
    'ThisGame.sMessage3 = ""
    'ThisGame.sMessage4 = ""

    ' ================================================================================================================================================================
    ' SETUP COMMON TO ALL GAMES
    ' ================================================================================================================================================================
    ' PROGRAM STATUS
    ThisGame.iGameMode = cAttractMode ' cAttractMode=wait for player input to change options or start game; cInPlay=game in session; cPaused=game paused, wait for P key or escape

    ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
    ' INIT LAYERS
    ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
    _Dest 0: Cls , cEmpty
    If ThisGame.imgBackground = -1 Or ThisGame.imgBackground = 0 Then
        ThisGame.imgBackground = _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
    End If
    If ThisGame.imgWalls = -1 Or ThisGame.imgWalls = 0 Then
        ThisGame.imgWalls = _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
    End If
    If ThisGame.imgText = -1 Or ThisGame.imgText = 0 Then
        ThisGame.imgText = _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
    End If
    If ThisGame.imgPlayers = -1 Or ThisGame.imgPlayers = 0 Then
        ThisGame.imgPlayers = _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
        _Dest ThisGame.imgPlayers: Cls , cEmpty
    End If
    If ThisGame.imgScore = -1 Or ThisGame.imgScore = 0 Then
        ThisGame.imgScore = _NewImage(m_iScreenWidth, m_iScreenHeight, 32)
        _Dest ThisGame.imgScore: Cls , cEmpty
    End If
    'If imgScreen& < -1 Or imgScreen& > 0 Then
    '   _FreeImage imgScreen&
    'End If
    '' update screen with changes
    '_Display

    ' ================================================================================================================================================================
    ' SPECIFIC GAME SETUP
    ' ================================================================================================================================================================
    Select Case ThisGame.sGameName

        Case "Pong Doubles":
            ' COLORS
            ThisGame.ulngScreenColor = cBlack
            ThisGame.ulngWallColor = cWhite

            ' FOR PLACING TEXT ON SCREEN: currently 1024 x  768 = 128 columns x 48 rows of text
            ThisGame.iTeam1ScoreColumn = 32
            ThisGame.iTeam2ScoreColumn = 96
            ThisGame.iTeam1ScoreRow = 2
            ThisGame.iTeam2ScoreRow = 2

            ' GAME PARAMETERS
            ThisGame.iScoreToWin = 11
            ThisGame.MinSpeed = 1
            ThisGame.MaxSpeed = 32
            ThisGame.iIncreaseSpeedEvery = 4 ' # times ball crosses net before increasing speed (add +1 to speed)

            ' GAME VARIABLES
            ThisGame.iScore1 = 0
            ThisGame.iScore2 = 0
            ThisGame.iVolleyNum = 0
            ThisGame.iSpeed = ThisGame.MinSpeed
            ThisGame.bSpeedChanged = TRUE
            ThisGame.iIncreaseSpeedAtVolley = ThisGame.iIncreaseSpeedEvery ' next iVolleyNum to increase speed at
            ThisGame.iVolleyDir = 0 ' (set at serveball)
            'ThisGame.iWhoLastHitBall = 0 ' FOR MULTIPLAYER NON-TEAM SCORING:

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' DRAW THE BACKGROUND LAYER
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            _Dest ThisGame.imgBackground: Cls , ThisGame.ulngScreenColor

            ' DRAW THE NET
            For iNextY = 44 To (m_iScreenHeight - 48) Step 20
                Line ((m_iScreenWidth / 2) - 2, iNextY)-((m_iScreenWidth / 2) + 2, iNextY + 10), ThisGame.ulngWallColor, BF
            Next iNextY
            'LINE (0, 0)-(iScreenWidth, iScreenHeight), ulngScreenColor, BF ' Draw a solid box

            ' DRAW LINE BETWEEN PLAYERS
            For iNextX = 7 To m_iScreenWidth Step 20
                Line (iNextX, (m_iScreenHeight / 2) - 2)-(iNextX + 10, (m_iScreenHeight / 2) + 2), ThisGame.ulngWallColor, BF
            Next iNextX

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' DRAW THE WALLS LAYER
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            _Dest ThisGame.imgWalls: Cls , cEmpty

            ' DRAW WALLS AROUND EDGES
            For iNextX = 7 To m_iScreenWidth Step 20
                Line (iNextX, 40)-(iNextX + 10, 44), ThisGame.ulngWallColor, BF
                Line (iNextX, m_iScreenHeight - 50)-(iNextX + 10, m_iScreenHeight - 54), ThisGame.ulngWallColor, BF
            Next iNextX
            'DrawRectSolid (iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long)
            'DrawBoxSolid (iX As Integer, iY As Integer, iSize As Integer, fgColor As _Unsigned Long)

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 1 (TEAM #1 ON LEFT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 1
            arrInitPlayer(iPlayer).ID = "Player" + _Trim$(Str$(iPlayer))
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 1
            arrInitPlayer(iPlayer).color = cRed
            arrInitPlayer(iPlayer).x = 64 ' LEFT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 4 ' MIDDLE OF TOP HALF OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(1).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(1).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = (m_iScreenHeight / 2) - arrInitPlayer(iPlayer).height ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_A
            arrInitPlayer(iPlayer).DownKey = VK_Z
            'arrPlayer(iPlayer).scoreCol = 32
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(1).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(1).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 2 (TEAM #2 ON RIGHT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 2
            arrInitPlayer(iPlayer).ID = "Player" + _Trim$(Str$(iPlayer))
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 2
            arrInitPlayer(iPlayer).color = cBlue
            arrInitPlayer(iPlayer).x = m_iScreenWidth - 64 ' RIGHT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 4 ' MIDDLE OF TOP HALF OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = (m_iScreenHeight / 2) - arrInitPlayer(iPlayer).height ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_UP
            arrInitPlayer(iPlayer).DownKey = VK_DOWN
            'arrPlayer(iPlayer).scoreCol = 54 '96
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 3 (TEAM #1 ON LEFT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 3
            arrInitPlayer(iPlayer).ID = "Player" + _Trim$(Str$(iPlayer))
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 1
            arrInitPlayer(iPlayer).color = cYellow
            arrInitPlayer(iPlayer).x = 64 ' LEFT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight - (m_iScreenHeight / 4) ' MIDDLE OF BOTTOM HALF OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = (m_iScreenHeight / 2) + 1 ' MIDDLE OF SCREEN +1px
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(iPlayer).height + 1)
            arrInitPlayer(iPlayer).UpKey = VK_I
            arrInitPlayer(iPlayer).DownKey = VK_K
            'arrPlayer(iPlayer).scoreCol = 76 '32
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 4 (TEAM #2 ON RIGHT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 4
            arrInitPlayer(iPlayer).ID = "Player" + _Trim$(Str$(iPlayer))
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 2
            arrInitPlayer(iPlayer).color = cLime
            arrInitPlayer(iPlayer).x = m_iScreenWidth - 64 ' RIGHT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight - (m_iScreenHeight / 4) ' MIDDLE OF BOTTOM HALF OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = (m_iScreenHeight / 2) + 1 ' MIDDLE OF SCREEN +1px
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(iPlayer).height + 1) ' BOTTOM OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_SUBTRACT ' numeric keypad -
            arrInitPlayer(iPlayer).DownKey = VK_ADD ' numeric keypad +
            'arrPlayer(iPlayer).scoreCol = 98 '96
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' INITIALIZE BALL
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            arrBall(1).size = 8
            arrBall(1).color = cCyan
            arrBall(1).x = 0
            arrBall(1).y = 0
            'arrBall(1).nextX = 0
            'arrBall(1).nextY = 0
            arrBall(1).dx = 0
            arrBall(1).dy = 0
            arrBall(1).minX = 1
            arrBall(1).maxX = m_iScreenWidth
            arrBall(1).minY = 45
            arrBall(1).maxY = m_iScreenHeight - (54 + arrBall(1).size + 1)


            ' ================================================================================================================================================================
        Case "Pong Volleyball":
            ' COLORS
            ThisGame.ulngScreenColor = cBlack
            ThisGame.ulngWallColor = cWhite

            ' FOR PLACING TEXT ON SCREEN: currently 1024 x  768 = 128 columns x 48 rows of text
            ThisGame.iTeam1ScoreColumn = 32
            ThisGame.iTeam2ScoreColumn = 96
            ThisGame.iTeam1ScoreRow = 2
            ThisGame.iTeam2ScoreRow = 2

            ' GAME PARAMETERS
            ThisGame.iScoreToWin = 11
            ThisGame.MinSpeed = 1
            ThisGame.MaxSpeed = 32
            ThisGame.iIncreaseSpeedEvery = 4 ' # times ball crosses net before increasing speed (add +1 to speed)

            ' GAME VARIABLES
            ThisGame.iScore1 = 0
            ThisGame.iScore2 = 0
            ThisGame.iVolleyNum = 0
            ThisGame.iSpeed = ThisGame.MinSpeed
            ThisGame.bSpeedChanged = TRUE
            ThisGame.iIncreaseSpeedAtVolley = ThisGame.iIncreaseSpeedEvery ' next iVolleyNum to increase speed at
            ThisGame.iVolleyDir = 0 ' (set at serveball)
            'ThisGame.iWhoLastHitBall = 0 ' FOR MULTIPLAYER NON-TEAM SCORING:

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' DRAW THE BACKGROUND LAYER
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            _Dest ThisGame.imgBackground: Cls , ThisGame.ulngScreenColor

            ' DRAW THE NET
            For iNextY = 44 To (m_iScreenHeight - 48) Step 20
                Line ((m_iScreenWidth / 2) - 2, iNextY)-((m_iScreenWidth / 2) + 2, iNextY + 10), ThisGame.ulngWallColor, BF
            Next iNextY
            'LINE (0, 0)-(iScreenWidth, iScreenHeight), ulngScreenColor, BF ' Draw a solid box

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' DRAW THE WALLS LAYER
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            _Dest ThisGame.imgWalls: Cls , cEmpty

            ' DRAW WALLS AROUND EDGES
            For iNextX = 7 To m_iScreenWidth Step 20
                Line (iNextX, 40)-(iNextX + 10, 44), ThisGame.ulngWallColor, BF
                Line (iNextX, m_iScreenHeight - 50)-(iNextX + 10, m_iScreenHeight - 54), ThisGame.ulngWallColor, BF
            Next iNextX
            'DrawRectSolid (iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long)
            'DrawBoxSolid (iX As Integer, iY As Integer, iSize As Integer, fgColor As _Unsigned Long)

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 1 (TEAM #1 ON LEFT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 1
            arrInitPlayer(iPlayer).ID = "Player1"
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 1
            arrInitPlayer(iPlayer).color = cRed
            arrInitPlayer(iPlayer).x = 64 ' LEFT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 2 ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(1).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(1).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(1).height + 1) ' BOTTOM OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_A
            arrInitPlayer(iPlayer).DownKey = VK_Z
            'arrPlayer(iPlayer).scoreCol = 32
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(1).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(1).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 2 (TEAM #2 ON RIGHT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 2
            arrInitPlayer(iPlayer).ID = "Player2"
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 2
            arrInitPlayer(iPlayer).color = cBlue
            arrInitPlayer(iPlayer).x = m_iScreenWidth - 64 ' RIGHT SIDE OF SCREEN
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 2 ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(iPlayer).height + 1) ' BOTTOM OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_UP
            arrInitPlayer(iPlayer).DownKey = VK_DOWN
            'arrPlayer(iPlayer).scoreCol = 54 '96
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 3 (TEAM #1 ON LEFT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 3
            arrInitPlayer(iPlayer).ID = "Player3"
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 3
            arrInitPlayer(iPlayer).color = cYellow
            arrInitPlayer(iPlayer).x = 64 * 5 ' TO THE LEFT OF THE NET
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 2 ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(iPlayer).height + 1) ' BOTTOM OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_I
            arrInitPlayer(iPlayer).DownKey = VK_K
            'arrPlayer(iPlayer).scoreCol = 76 '32
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            ' PLAYER 4 (TEAM #2 ON RIGHT) SETTINGS
            ' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
            iPlayer = 4
            arrInitPlayer(iPlayer).ID = "Player4"
            arrInitPlayer(iPlayer).IsAvailable = TRUE
            arrInitPlayer(iPlayer).TeamNumber = 4
            arrInitPlayer(iPlayer).color = cLime
            arrInitPlayer(iPlayer).x = m_iScreenWidth - (64 * 5) ' TO THE RIGHT OF THE NET
            arrInitPlayer(iPlayer).y = m_iScreenHeight / 2 ' MIDDLE OF SCREEN
            arrInitPlayer(iPlayer).width = 16
            arrInitPlayer(iPlayer).height = 64
            arrInitPlayer(iPlayer).minX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).x
            arrInitPlayer(iPlayer).minY = 45 ' TOP OF SCREEN
            arrInitPlayer(iPlayer).maxY = m_iScreenHeight - (54 + arrInitPlayer(iPlayer).height + 1) ' BOTTOM OF SCREEN
            arrInitPlayer(iPlayer).UpKey = VK_SUBTRACT ' numeric keypad -
            arrInitPlayer(iPlayer).DownKey = VK_ADD ' numeric keypad +
            'arrPlayer(iPlayer).scoreCol = 98 '96
            'arrPlayer(iPlayer).scoreRow = 2
            'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).scoreCol - 8
            'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).scoreRow

    End Select

    ' ================================================================================================================================================================
    ' INITIALIZE PLAYERS WITH SETTINGS FOR CURRENT GAME
    ' ================================================================================================================================================================
    For iPlayer = 1 To 4
        ' GAME OPTION SPECIFIC PLAYER PARAMETERS
        arrPlayer(iPlayer).IsAvailable = arrInitPlayer(iPlayer).IsAvailable
        arrPlayer(iPlayer).TeamNumber = arrInitPlayer(iPlayer).TeamNumber
        arrPlayer(iPlayer).ID = arrInitPlayer(iPlayer).ID
        arrPlayer(iPlayer).color = arrInitPlayer(iPlayer).color
        arrPlayer(iPlayer).x = arrInitPlayer(iPlayer).x
        arrPlayer(iPlayer).y = arrInitPlayer(iPlayer).y
        arrPlayer(iPlayer).width = arrInitPlayer(iPlayer).width
        arrPlayer(iPlayer).height = arrInitPlayer(iPlayer).height
        arrPlayer(iPlayer).minX = arrInitPlayer(iPlayer).minX
        arrPlayer(iPlayer).maxX = arrInitPlayer(iPlayer).maxX
        arrPlayer(iPlayer).minY = arrInitPlayer(iPlayer).minY
        arrPlayer(iPlayer).maxY = arrInitPlayer(iPlayer).maxY
        arrPlayer(iPlayer).UpKey = arrInitPlayer(iPlayer).UpKey
        arrPlayer(iPlayer).DownKey = arrInitPlayer(iPlayer).DownKey
        'arrPlayer(iPlayer).scoreCol = arrPlayer(iPlayer).scoreCol
        'arrPlayer(iPlayer).scoreRow = arrPlayer(iPlayer).scoreRow
        'arrPlayer(iPlayer).nameCol = arrPlayer(iPlayer).nameCol
        'arrPlayer(iPlayer).nameRow = arrPlayer(iPlayer).nameRow

        ' CALCULATED BASED ON GAME PARAMETERS
        arrPlayer(iPlayer).BounceSegmentWidth = arrPlayer(iPlayer).width / 7
        arrPlayer(iPlayer).BounceSegmentHeight = arrPlayer(iPlayer).height / 7

        ' PLAYER PARAMETERS THAT ARE THE SAME FOR ALL GAMES
        arrPlayer(iPlayer).Score = 0
        'arrPlayer(iPlayer).IsActive = FALSE ' WAIT UNTIL GIVEN MOUSE IS MOVED TO ACTIVATE EACH PLAYER (IsActive = TRUE)
        'arrPlayer(iPlayer).px = 0
        'arrPlayer(iPlayer).py = 0
        'arrPlayer(iPlayer).LeftDown = FALSE
        'arrPlayer(iPlayer).LeftDownOld = FALSE
        'arrPlayer(iPlayer).MiddleDown = FALSE
        'arrPlayer(iPlayer).MiddleDownOld = FALSE
        'arrPlayer(iPlayer).RightDown = FALSE
        'arrPlayer(iPlayer).RightDownOld = FALSE
    Next iPlayer

End Sub ' InitGame

' /////////////////////////////////////////////////////////////////////////////
' BALL HITS PADDLE SOUND

Sub BallPaddleSound (iSpeed As Integer)
    Dim iNote As Integer
    iNote = iSpeed * 100
    If iNote > 4186 Then
        iNote = 4186
    End If
    Sound iNote, .75
End Sub ' BallPaddleSound

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

Sub ScoreSound1
    Sound 123, 4
End Sub ' ScoreSound1

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

Sub WallSound1
    Sound 932, .75
End Sub ' WallSound1

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

Sub WallSound2
    Sound 123, .75
End Sub ' WallSound2

' /////////////////////////////////////////////////////////////////////////////
'
' Calculates the angle of trajectory of puck based on where it hits
' the paddle and its current trajectory.
'
' (variables passed in by reference)
'
' PucksY!   - puck's current y location
' PlayersY% - current player's paddle y location
'
' REMEMBER: If you modify a variable in a SUB/FUNCTION that has been passed
'           by reference, that modification will also be seen in the variable
'           used when calling the SUB/FUNCTION.
'
'           A function's name acts like a variable, returning a value to the
'           calling routine.
'
' NOTE    : For less deflection increase the last value on the next code line.
'           (default = 2)

Function TRAJECTORY% (ball_y%, player_y%, ball_size%, player_height%)
    TRAJECTORY% = (((ball_y% - player_y%) + ball_size% - (player_height% + ball_size%) \ 2) / ((player_height% + ball_size%) / 2)) / 2
End Function ' TRAJECTORY%

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

'SUB ServeBall (to_player%, ball_x%, ball_x1%, ball_x2%, ball_y%, ball_y1%, ball_y2%, ball_size%, screen_width%, screen_height%, ball_dx%, ball_dy%, iVolleyNum)
Sub ServeBall (ThisGame As GameType, MyBall As BallType, arrPlayer() As PlayerType, iToPlayer As Integer)
    Dim iGuess As Integer

    ' START IN THE MIDDLE OF THE COURT
    MyBall.x = m_iScreenWidth / 2

    ' PICK A RANDOM STARTING POINT
    MyBall.y = RandomNumber%(20, m_iScreenHeight - 20)

    ' CHOOSE HORIZONTAL DIRECTION?
    If iToPlayer = 0 Then
        ' PICK A RANDOM HORIZONTAL DIRECTION
        iGuess = RandomNumber%(1, 2)
        If iGuess = 1 Then
            ' 1 = right
            MyBall.dx = MyBall.size / 2
        Else
            ' 2 = left
            MyBall.dx = 0 - (MyBall.size / 2)
        End If

        If MyBall.dx < 1 Then
            ThisGame.iVolleyDir = -1
        Else
            ThisGame.iVolleyDir = 1
        End If
    Else
        ' (BALL DX RESUMES FROM BEFORE)
    End If

    ' CHOOSE VERTICAL DIRECTION?
    If iToPlayer = 0 Then
        ' PICK A RANDOM VERTICAL DIRECTION & SPEED
        iGuess = RandomNumber%(1, 2)
        If iGuess = 1 Then
            ' 1 = down
            iGuess = RandomNumber%(0, 6)
            MyBall.dy = iGuess
        Else
            ' 2 = up
            iGuess = RandomNumber%(0, 6)
            MyBall.dy = 0 - iGuess
        End If
    Else
        ' (BALL DY RESUMES FROM BEFORE)
    End If

    ' RESTART COUNTER
    ThisGame.iVolleyNum = 0
    ThisGame.iIncreaseSpeedAtVolley = ThisGame.iIncreaseSpeedEvery

    ' PLAY SOUND
    BallPaddleSound ThisGame.iSpeed
End Sub ' ServeBall

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

Sub BallHitPaddle (ThisGame As GameType, MyBall As BallType, MyPlayer As PlayerType, arrZone() As Integer)
    Dim iDeflectionZone As Integer

    BallPaddleSound ThisGame.iSpeed

    ' TURN BALL AROUND
    MyBall.dx = 0 - MyBall.dx

    ' TIME TO SPEED UP BALL?
    If ThisGame.bSpeedChanged = TRUE Then
        If MyBall.dx < 0 Then
            If Abs(MyBall.dx) < ThisGame.iSpeed Then
                MyBall.dx = 0 - ThisGame.iSpeed
            End If
        ElseIf MyBall.dx > 0 Then
            If MyBall.dx < ThisGame.iSpeed Then
                MyBall.dx = ThisGame.iSpeed
            End If
        Else
            ' (BALL DX SHOULD NEVER BE ZERO FOR RIGHT<->LEFT PONG GAMES)
        End If
        ThisGame.bSpeedChanged = FALSE
    End If

    ' MOVE BALL NEXT TO PADDLE (NOT ON IT)
    If MyBall.dx < 0 Then
        MyBall.x = (MyPlayer.x - 1)
    Else
        MyBall.x = (MyPlayer.x + MyPlayer.width)
    End If

    ' DETERMINE WHAT SEGMENT OF THE PADDLE THE BALL HIT
    ' WHICH TELLS US WHAT ANGLE IT BOUNCES AT
    ' (MIDDLE SEGMENT = STRAIGHT)
    If MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 6) Then
        iDeflectionZone = 7
    ElseIf MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 5) Then
        iDeflectionZone = 6
    ElseIf MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 4) Then
        iDeflectionZone = 5
    ElseIf MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 3) Then
        iDeflectionZone = 4
    ElseIf MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 2) Then
        iDeflectionZone = 3
    ElseIf MyBall.y > MyPlayer.y + (MyPlayer.BounceSegmentHeight * 1) Then
        iDeflectionZone = 2
    Else
        iDeflectionZone = 1
    End If

    MyBall.dy = arrZone(ThisGame.iSpeed, iDeflectionZone)
End Sub ' BallHitPaddle

' /////////////////////////////////////////////////////////////////////////////
' Bounces the ball back even if it doesn't hit player (for test mode)

Sub BounceBack (ThisGame As GameType, MyBall As BallType, MyPlayer As PlayerType, iDirection As Integer)
    If iDirection = cLeftDir Then
        BallPaddleSound ThisGame.iSpeed

        ' TURN BALL AROUND
        MyBall.dx = 0 - MyBall.dx

        ' TIME TO SPEED UP BALL?
        If ThisGame.bSpeedChanged = TRUE Then
            If MyBall.dx < 0 Then
                'If Abs(MyBall.dx) < ThisGame.iSpeed Then
                MyBall.dx = 0 - ThisGame.iSpeed
                'End If
            ElseIf MyBall.dx > 0 Then
                'If MyBall.dx < ThisGame.iSpeed Then
                MyBall.dx = ThisGame.iSpeed
                'End If
            Else
                ' (BALL DX SHOULD NEVER BE ZERO FOR RIGHT<->LEFT PONG GAMES)
            End If
            ThisGame.bSpeedChanged = FALSE
        End If

        ' MOVE BALL NEXT TO PADDLE (NOT ON IT)
        If MyBall.dx < 0 Then
            MyBall.x = (MyPlayer.x - 1)
        Else
            MyBall.x = (MyPlayer.x + MyPlayer.width)
        End If

    Else ' iDirection = cRightDir
        ' ****************************************************************************************************************************************************************
        ' TEST MODE BOUNCE BACK FOR PADDLE #2
        ' ****************************************************************************************************************************************************************
        BallPaddleSound ThisGame.iSpeed

        ' TURN BALL AROUND
        MyBall.dx = 0 - MyBall.dx

        ' TIME TO SPEED UP BALL?
        If ThisGame.bSpeedChanged = TRUE Then
            If MyBall.dx < 0 Then
                If Abs(MyBall.dx) < ThisGame.iSpeed Then
                    MyBall.dx = 0 - ThisGame.iSpeed
                End If
            ElseIf MyBall.dx > 0 Then
                If MyBall.dx < ThisGame.iSpeed Then
                    MyBall.dx = ThisGame.iSpeed
                End If
            Else
                ' (BALL DX SHOULD NEVER BE ZERO FOR RIGHT<->LEFT PONG GAMES)
            End If
            ThisGame.bSpeedChanged = FALSE
        End If

        ' MOVE BALL NEXT TO PADDLE (NOT ON IT)
        If MyBall.dx < 0 Then
            MyBall.x = (MyPlayer.x - 1)
        Else
            MyBall.x = (MyPlayer.x + MyPlayer.width)
        End If
    End If
End Sub ' BounceBack

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END PONG ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN PROGRAM METADATA ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Parses out the version # from the filename
' {filename}{version #}.{ext}
' and returns it as a string.

Function GetVersionNum$ (sFileName$)
    Const cDigits = "1234567890"
    Dim sResult As String
    Dim sProgName As String
    Dim iLoop As Integer
    sResult$ = ""
    sProgName$ = NoExt$(sFileName$)
    For iLoop = Len(sProgName) To 1 Step -1
        If InStr(1, cDigits, Mid$(sProgName, iLoop, 1)) = 0 Then
            Exit For
        Else
            sResult = Mid$(sProgName, iLoop, 1) + sResult
        End If
    Next iLoop
    GetVersionNum$ = sResult
End Function ' GetVersionNum$
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END PROGRAM METADATA ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
'
' iRow% and iCol% are 0-based in this version
'
' See also: PrintString, PrintString1, PutCharXY

Sub PrintAt (iRow%, iCol%, sText$)
    _PrintString (iCol% * 8, iRow% * 16), sText$
    '_PrintString (iCol%, iRow%), sText$
End Sub ' PrintAt

' /////////////////////////////////////////////////////////////////////////////
' 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: PrintString

Sub PrintString1 (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 ' 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 COLOR FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Function cRed~& ()
    cRed = _RGB32(255, 0, 0)
End Function

Function cOrangeRed~& ()
    cOrangeRed = _RGB32(255, 69, 0)
End Function ' cOrangeRed~&

Function cDarkOrange~& ()
    cDarkOrange = _RGB32(255, 140, 0)
End Function ' cDarkOrange~&

Function cOrange~& ()
    cOrange = _RGB32(255, 165, 0)
End Function ' cOrange~&

Function cGold~& ()
    cGold = _RGB32(255, 215, 0)
End Function ' cGold~&

Function cYellow~& ()
    cYellow = _RGB32(255, 255, 0)
End Function ' cYellow~&

' LONG-HAIRED FRIENDS OF JESUS OR NOT,
' THIS IS NOT YELLOW ENOUGH (TOO CLOSE TO LIME)
' TO USE FOR OUR COMPLEX RAINBOW SEQUENCE:
Function cChartreuse~& ()
    cChartreuse = _RGB32(127, 255, 0)
End Function ' cChartreuse~&

' WE SUBSTITUTE THIS CSS3 COLOR FOR INBETWEEN LIME AND YELLOW:
Function cOliveDrab1~& ()
    cOliveDrab1 = _RGB32(192, 255, 62)
End Function ' cOliveDrab1~&

Function cLime~& ()
    cLime = _RGB32(0, 255, 0)
End Function ' cLime~&

Function cMediumSpringGreen~& ()
    cMediumSpringGreen = _RGB32(0, 250, 154)
End Function ' cMediumSpringGreen~&

' ADDED THIS FOR THE GAUGE COLOR:
Function cSpringGreen~& ()
    cSpringGreen = _RGB32(0, 255, 160)
End Function ' cSpringGreen~&

Function cCyan~& ()
    cCyan = _RGB32(0, 255, 255)
End Function ' cCyan~&

Function cDeepSkyBlue~& ()
    cDeepSkyBlue = _RGB32(0, 191, 255)
End Function ' cDeepSkyBlue~&

Function cDodgerBlue~& ()
    cDodgerBlue = _RGB32(30, 144, 255)
End Function ' cDodgerBlue~&

Function cSeaBlue~& ()
    cSeaBlue = _RGB32(0, 64, 255)
End Function ' cSeaBlue~&

Function cBlue~& ()
    cBlue = _RGB32(0, 0, 255)
End Function ' cBlue~&

Function cBluePurple~& ()
    cBluePurple = _RGB32(64, 0, 255)
End Function ' cBluePurple~&

Function cDeepPurple~& ()
    cDeepPurple = _RGB32(96, 0, 255)
End Function ' cDeepPurple~&

Function cPurple~& ()
    cPurple = _RGB32(128, 0, 255)
End Function ' cPurple~&

Function cPurpleRed~& ()
    cPurpleRed = _RGB32(128, 0, 192)
End Function ' cPurpleRed~&

Function cDarkRed~& ()
    cDarkRed = _RGB32(160, 0, 64)
End Function ' cDarkRed~&

Function cBrickRed~& ()
    cBrickRed = _RGB32(192, 0, 32)
End Function ' cBrickRed~&

Function cDarkGreen~& ()
    cDarkGreen = _RGB32(0, 100, 0)
End Function ' cDarkGreen~&

Function cGreen~& ()
    cGreen = _RGB32(0, 128, 0)
End Function ' cGreen~&

Function cOliveDrab~& ()
    cOliveDrab = _RGB32(107, 142, 35)
End Function ' cOliveDrab~&

Function cLightPink~& ()
    cLightPink = _RGB32(255, 182, 193)
End Function ' cLightPink~&

Function cHotPink~& ()
    cHotPink = _RGB32(255, 105, 180)
End Function ' cHotPink~&

Function cDeepPink~& ()
    cDeepPink = _RGB32(255, 20, 147)
End Function ' cDeepPink~&

Function cMagenta~& ()
    cMagenta = _RGB32(255, 0, 255)
End Function ' cMagenta~&

Function cBlack~& ()
    cBlack = _RGB32(0, 0, 0)
End Function ' cBlack~&

Function cDimGray~& ()
    cDimGray = _RGB32(105, 105, 105)
End Function ' cDimGray~&

Function cGray~& ()
    cGray = _RGB32(128, 128, 128)
End Function ' cGray~&

Function cDarkGray~& ()
    cDarkGray = _RGB32(169, 169, 169)
End Function ' cDarkGray~&

Function cSilver~& ()
    cSilver = _RGB32(192, 192, 192)
End Function ' cSilver~&

Function cLightGray~& ()
    cLightGray = _RGB32(211, 211, 211)
End Function ' cLightGray~&

Function cGainsboro~& ()
    cGainsboro = _RGB32(220, 220, 220)
End Function ' cGainsboro~&

Function cWhiteSmoke~& ()
    cWhiteSmoke = _RGB32(245, 245, 245)
End Function ' cWhiteSmoke~&

Function cWhite~& ()
    cWhite = _RGB32(255, 255, 255)
    'cWhite = _RGB32(254, 254, 254)
End Function ' cWhite~&

Function cDarkBrown~& ()
    cDarkBrown = _RGB32(128, 64, 0)
End Function ' cDarkBrown~&

Function cLightBrown~& ()
    cLightBrown = _RGB32(196, 96, 0)
End Function ' cLightBrown~&

Function cKhaki~& ()
    cKhaki = _RGB32(240, 230, 140)
End Function ' cKhaki~&

Function cEmpty~& ()
    'cEmpty~& = -1
    cEmpty = _RGB32(0, 0, 0, 0)
End Function ' cEmpty~&

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END COLOR FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN FILE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'WORKS IN QB64PE BUT NOT QB64:
'' /////////////////////////////////////////////////////////////////////////////
'' QB64 Phoenix Edition › QB64 Rising › Code and Stuff › Help Me!
'' Using shell to delete a file
'' https://qb64phoenix.com/forum/showthread.php?tid=2618&pid=24683#pid24683
'' a740g
'' #5
'' 04-24-2024, 06:05 AM
''
'' There are no commands to directly make copies or backup of files.
'' But you could write one with a few lines of code like:
''
'' Copies src to dst
'' Set overwite to true if dst should be overwritten if present
'Sub CopyFile (src As String, dst As String, overwrite As _Byte)
'    If _FileExists(src) Then
'        If Not _FileExists(dst) Or (_FileExists(dst) And overwrite) Then
'            _WriteFile dst, _ReadFile$(src)
'        End If
'    End If
'End Sub ' CopyFile

' /////////////////////////////////////////////////////////////////////////////
' QB64 Phoenix Edition › QB64 Rising › Code and Stuff › Help Me!
' Using shell to delete a file
' https://qb64phoenix.com/forum/showthread.php?tid=2618

Sub DeleteFile (sFile As String)
    If _FileExists(sFile) Then
        'Shell "DELETE " + sFile
        'Shell "del " + sFile
        Kill sFile
    End If
End Sub ' DeleteFile

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

Function FileExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                FileExt$ = Right$(sFile, Len(sFile) - iPos)
            Else
                ' dot is first character, return everything after it
                FileExt$ = Right$(sFile, Len(sFile) - 1)
            End If
        Else
            ' file only has one character, the dot, the file extension is blank
            FileExt$ = ""
        End If
    Else
        ' no dot found, the file extension is blank
        FileExt$ = ""
    End If
End Function ' FileExt$

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

Function NameOnly$ (sFile As String, sSlash As String)
    Dim iPos As Integer
    'sFile = Replace$(sFile, "/", "\")

    iPos = _InStrRev(sFile, sSlash)
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NameOnly$ = Right$(sFile, Len(sFile) - iPos)
            Else
                ' slash is first character, return everything after it
                NameOnly$ = Right$(sFile, Len(sFile) - 1)
            End If
        Else
            ' file only has one character, the slash, name is blank
            NameOnly$ = ""
        End If
    Else
        ' slash not found, return the entire thing
        NameOnly$ = sFile
    End If
End Function ' NameOnly$

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

Function NoExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NoExt$ = Left$(sFile, iPos - 1)
            Else
                ' dot is first character, removing it returns blank!
                ' our version will just return the name unchanged
                ' but you can return blank if you prefer
                NoExt$ = sFile
            End If
        Else
            ' file only has one character, the dot, removing it returns blank!
            ' our version will just return the name unchanged
            ' but you can return blank if you prefer
            NoExt$ = sFile
        End If
    Else
        ' no dot found
        ' return the name unchanged
        NoExt$ = sFile
    End If
End Function ' NoExt$

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

Function PathOnly$ (sFile As String, sSlash As String)
    Dim iPos As Integer
    'sFile = Replace$(sFile, "/", "\")

    iPos = _InStrRev(sFile, sSlash)
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                PathOnly$ = Left$(sFile, iPos)
            Else
                ' slash is first character, so not much of a path, return blank
                PathOnly$ = ""
            End If
        Else
            ' file only has one character, the slash, name is blank
            PathOnly$ = ""
        End If
    Else
        ' slash not found, so not a path, return blank
        PathOnly$ = ""
    End If
End Function ' PathOnly$

' /////////////////////////////////////////////////////////////////////////////
' Writes sText to file sFileName.
' If bAppend=TRUE appends to file, else overwrites it.

' Returns blank if successful else returns error message.

' Example:
' ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
' ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)
' sFileName = ProgramPath$ + ProgramName$ + ".OUT.txt"
' sText = "This is a test." + chr$(13) + "Here is line 2." + chr$(13) + "End."
' sError = PrintFile$(sFileName, sText, FALSE)

Function PrintFile$ (sFileName As String, sText As String, bAppend As Integer)
    Dim sError As String: sError = ""

    If (bAppend = TRUE) Then
        If _FileExists(sFileName) Then
            Open sFileName For Append As #1 ' opens an existing file for appending
        Else
            sError = "Error in PrintFile$ : File not found. Cannot append."
        End If
    Else
        Open sFileName For Output As #1 ' opens and clears an existing file or creates new empty file
    End If

    If Len(sError) = 0 Then
        ' NOTE: WRITE places text in quotes in the file
        'WRITE #1, x, y, z$
        'WRITE #1, sText

        ' PRINT does not put text inside quotes
        Print #1, sText

        Close #1
    End If

    PrintFile$ = sError
End Function ' PrintFile$

' /////////////////////////////////////////////////////////////////////////////
' Fastest way is always to just read the whole life at once and then parse it.

Function ReadTextFile$ (sFileName As String, sDefault As String)
    Dim x$
    If _FileExists(sFileName) Then
        Open sFileName For Binary As #1
        x$ = Space$(LOF(1))
        Get #1, 1, x$
        Close #1
        ReadTextFile$ = x$
    Else
        ReadTextFile$ = sDefault
    End If
End Function ' ReadTextFile$

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END FILE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN CONSTANT TO STRING FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

Function MouseDataPositionToString$ (MyInteger As Integer)
    Dim Mystring As String
    Select Case MyInteger
        Case cForPlayer:
            Mystring = "cForPlayer"
        Case cMouseDX:
            Mystring = "cMouseDX"
        Case cMouseDY:
            Mystring = "cMouseDY"
        Case cMousePosX:
            Mystring = "cMousePosX"
        Case cMousePosY:
            Mystring = "cMousePosY"
        Case cMouseWheel:
            Mystring = "cMouseWheel"
        Case cMouseLeftDown:
            Mystring = "cMouseLeftDown"
        Case cMouseMiddleDown:
            Mystring = "cMouseMiddleDown"
        Case cMouseRightDown:
            Mystring = "cMouseRightDown"
        Case Else:
            Mystring = _Trim$(Str$(MyInteger))
    End Select
    MouseDataPositionToString$ = Mystring
End Function ' MouseDataPositionToString$


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

Function VirtualKeyCodeToString$ (MyInteger As Integer)
    Dim Mystring As String

    Select Case MyInteger
        Case VK_LBUTTON:
            Mystring = "VK_LBUTTON"
        Case VK_RBUTTON:
            Mystring = "VK_RBUTTON"
        Case VK_CANCEL:
            Mystring = "VK_CANCEL"
        Case VK_MBUTTON:
            Mystring = "VK_MBUTTON"
        Case VK_XBUTTON1:
            Mystring = "VK_XBUTTON1"
        Case VK_XBUTTON2:
            Mystring = "VK_XBUTTON2"
        Case VK_BACK:
            Mystring = "VK_BACK"
        Case VK_TAB:
            Mystring = "VK_TAB"
        Case VK_CLEAR:
            Mystring = "VK_CLEAR"
        Case VK_RETURN:
            Mystring = "VK_RETURN"
        Case VK_SHIFT:
            Mystring = "VK_SHIFT"
        Case VK_CONTROL:
            Mystring = "VK_CONTROL"
        Case VK_MENU:
            Mystring = "VK_MENU"
        Case VK_PAUSE:
            Mystring = "VK_PAUSE"
        Case VK_CAPITAL:
            Mystring = "VK_CAPITAL"
        Case VK_KANA:
            Mystring = "VK_KANA"
        Case VK_HANGUL:
            Mystring = "VK_HANGUL"
        Case VK_IME_ON:
            Mystring = "VK_IME_ON"
        Case VK_JUNJA:
            Mystring = "VK_JUNJA"
        Case VK_FINAL:
            Mystring = "VK_FINAL"
        Case VK_HANJA:
            Mystring = "VK_HANJA"
        Case VK_KANJI:
            Mystring = "VK_KANJI"
        Case VK_IME_OFF:
            Mystring = "VK_IME_OFF"
        Case VK_ESCAPE:
            Mystring = "VK_ESCAPE"
        Case VK_CONVERT:
            Mystring = "VK_CONVERT"
        Case VK_NONCONVERT:
            Mystring = "VK_NONCONVERT"
        Case VK_ACCEPT:
            Mystring = "VK_ACCEPT"
        Case VK_MODECHANGE:
            Mystring = "VK_MODECHANGE"
        Case VK_SPACE:
            Mystring = "VK_SPACE"
        Case VK_PRIOR:
            Mystring = "VK_PRIOR"
        Case VK_NEXT:
            Mystring = "VK_NEXT"
        Case VK_END:
            Mystring = "VK_END"
        Case VK_HOME:
            Mystring = "VK_HOME"
        Case VK_LEFT:
            Mystring = "VK_LEFT"
        Case VK_UP:
            Mystring = "VK_UP"
        Case VK_RIGHT:
            Mystring = "VK_RIGHT"
        Case VK_DOWN:
            Mystring = "VK_DOWN"
        Case VK_SELECT:
            Mystring = "VK_SELECT"
        Case VK_PRINT:
            Mystring = "VK_PRINT"
        Case VK_EXECUTE:
            Mystring = "VK_EXECUTE"
        Case VK_SNAPSHOT:
            Mystring = "VK_SNAPSHOT"
        Case VK_INSERT:
            Mystring = "VK_INSERT"
        Case VK_DELETE:
            Mystring = "VK_DELETE"
        Case VK_HELP:
            Mystring = "VK_HELP"
        Case VK_0:
            Mystring = "VK_0"
        Case VK_1:
            Mystring = "VK_1"
        Case VK_2:
            Mystring = "VK_2"
        Case VK_3:
            Mystring = "VK_3"
        Case VK_4:
            Mystring = "VK_4"
        Case VK_5:
            Mystring = "VK_5"
        Case VK_6:
            Mystring = "VK_6"
        Case VK_7:
            Mystring = "VK_7"
        Case VK_8:
            Mystring = "VK_8"
        Case VK_9:
            Mystring = "VK_9"
        Case VK_A:
            Mystring = "VK_A"
        Case VK_B:
            Mystring = "VK_B"
        Case VK_C:
            Mystring = "VK_C"
        Case VK_D:
            Mystring = "VK_D"
        Case VK_E:
            Mystring = "VK_E"
        Case VK_F:
            Mystring = "VK_F"
        Case VK_G:
            Mystring = "VK_G"
        Case VK_H:
            Mystring = "VK_H"
        Case VK_I:
            Mystring = "VK_I"
        Case VK_J:
            Mystring = "VK_J"
        Case VK_K:
            Mystring = "VK_K"
        Case VK_L:
            Mystring = "VK_L"
        Case VK_M:
            Mystring = "VK_M"
        Case VK_N:
            Mystring = "VK_N"
        Case VK_O:
            Mystring = "VK_O"
        Case VK_P:
            Mystring = "VK_P"
        Case VK_Q:
            Mystring = "VK_Q"
        Case VK_R:
            Mystring = "VK_R"
        Case VK_S:
            Mystring = "VK_S"
        Case VK_T:
            Mystring = "VK_T"
        Case VK_U:
            Mystring = "VK_U"
        Case VK_V:
            Mystring = "VK_V"
        Case VK_W:
            Mystring = "VK_W"
        Case VK_X:
            Mystring = "VK_X"
        Case VK_Y:
            Mystring = "VK_Y"
        Case VK_Z:
            Mystring = "VK_Z"
        Case VK_LWIN:
            Mystring = "VK_LWIN"
        Case VK_RWIN:
            Mystring = "VK_RWIN"
        Case VK_APPS:
            Mystring = "VK_APPS"
        Case VK_SLEEP:
            Mystring = "VK_SLEEP"
        Case VK_NUMPAD0:
            Mystring = "VK_NUMPAD0"
        Case VK_NUMPAD1:
            Mystring = "VK_NUMPAD1"
        Case VK_NUMPAD2:
            Mystring = "VK_NUMPAD2"
        Case VK_NUMPAD3:
            Mystring = "VK_NUMPAD3"
        Case VK_NUMPAD4:
            Mystring = "VK_NUMPAD4"
        Case VK_NUMPAD5:
            Mystring = "VK_NUMPAD5"
        Case VK_NUMPAD6:
            Mystring = "VK_NUMPAD6"
        Case VK_NUMPAD7:
            Mystring = "VK_NUMPAD7"
        Case VK_NUMPAD8:
            Mystring = "VK_NUMPAD8"
        Case VK_NUMPAD9:
            Mystring = "VK_NUMPAD9"
        Case VK_MULTIPLY:
            Mystring = "VK_MULTIPLY"
        Case VK_ADD:
            Mystring = "VK_ADD"
        Case VK_SEPARATOR:
            Mystring = "VK_SEPARATOR"
        Case VK_SUBTRACT:
            Mystring = "VK_SUBTRACT"
        Case VK_DECIMAL:
            Mystring = "VK_DECIMAL"
        Case VK_DIVIDE:
            Mystring = "VK_DIVIDE"
        Case VK_F1:
            Mystring = "VK_F1"
        Case VK_F2:
            Mystring = "VK_F2"
        Case VK_F3:
            Mystring = "VK_F3"
        Case VK_F4:
            Mystring = "VK_F4"
        Case VK_F5:
            Mystring = "VK_F5"
        Case VK_F6:
            Mystring = "VK_F6"
        Case VK_F7:
            Mystring = "VK_F7"
        Case VK_F8:
            Mystring = "VK_F8"
        Case VK_F9:
            Mystring = "VK_F9"
        Case VK_F10:
            Mystring = "VK_F10"
        Case VK_F11:
            Mystring = "VK_F11"
        Case VK_F12:
            Mystring = "VK_F12"
        Case VK_F13:
            Mystring = "VK_F13"
        Case VK_F14:
            Mystring = "VK_F14"
        Case VK_F15:
            Mystring = "VK_F15"
        Case VK_F16:
            Mystring = "VK_F16"
        Case VK_F17:
            Mystring = "VK_F17"
        Case VK_F18:
            Mystring = "VK_F18"
        Case VK_F19:
            Mystring = "VK_F19"
        Case VK_F20:
            Mystring = "VK_F20"
        Case VK_F21:
            Mystring = "VK_F21"
        Case VK_F22:
            Mystring = "VK_F22"
        Case VK_F23:
            Mystring = "VK_F23"
        Case VK_F24:
            Mystring = "VK_F24"
        Case VK_NUMLOCK:
            Mystring = "VK_NUMLOCK"
        Case VK_SCROLL:
            Mystring = "VK_SCROLL"
        Case VK_LSHIFT:
            Mystring = "VK_LSHIFT"
        Case VK_RSHIFT:
            Mystring = "VK_RSHIFT"
        Case VK_LCONTROL:
            Mystring = "VK_LCONTROL"
        Case VK_RCONTROL:
            Mystring = "VK_RCONTROL"
        Case VK_LMENU:
            Mystring = "VK_LMENU"
        Case VK_RMENU:
            Mystring = "VK_RMENU"
        Case VK_BROWSER_BACK:
            Mystring = "VK_BROWSER_BACK"
        Case VK_BROWSER_FORWARD:
            Mystring = "VK_BROWSER_FORWARD"
        Case VK_BROWSER_REFRESH:
            Mystring = "VK_BROWSER_REFRESH"
        Case VK_BROWSER_STOP:
            Mystring = "VK_BROWSER_STOP"
        Case VK_BROWSER_SEARCH:
            Mystring = "VK_BROWSER_SEARCH"
        Case VK_BROWSER_FAVORITES:
            Mystring = "VK_BROWSER_FAVORITES"
        Case VK_BROWSER_HOME:
            Mystring = "VK_BROWSER_HOME"
        Case VK_VOLUME_MUTE:
            Mystring = "VK_VOLUME_MUTE"
        Case VK_VOLUME_DOWN:
            Mystring = "VK_VOLUME_DOWN"
        Case VK_VOLUME_UP:
            Mystring = "VK_VOLUME_UP"
        Case VK_MEDIA_NEXT_TRACK:
            Mystring = "VK_MEDIA_NEXT_TRACK"
        Case VK_MEDIA_PREV_TRACK:
            Mystring = "VK_MEDIA_PREV_TRACK"
        Case VK_MEDIA_STOP:
            Mystring = "VK_MEDIA_STOP"
        Case VK_MEDIA_PLAY_PAUSE:
            Mystring = "VK_MEDIA_PLAY_PAUSE"
        Case VK_LAUNCH_MAIL:
            Mystring = "VK_LAUNCH_MAIL"
        Case VK_LAUNCH_MEDIA_SELECT:
            Mystring = "VK_LAUNCH_MEDIA_SELECT"
        Case VK_LAUNCH_APP1:
            Mystring = "VK_LAUNCH_APP1"
        Case VK_LAUNCH_APP2:
            Mystring = "VK_LAUNCH_APP2"
        Case VK_OEM_1:
            Mystring = "VK_OEM_1"
        Case VK_OEM_PLUS:
            Mystring = "VK_OEM_PLUS"
        Case VK_OEM_COMMA:
            Mystring = "VK_OEM_COMMA"
        Case VK_OEM_MINUS:
            Mystring = "VK_OEM_MINUS"
        Case VK_OEM_PERIOD:
            Mystring = "VK_OEM_PERIOD"
        Case VK_OEM_2:
            Mystring = "VK_OEM_2"
        Case VK_OEM_3:
            Mystring = "VK_OEM_3"
        Case VK_OEM_4:
            Mystring = "VK_OEM_4"
        Case VK_OEM_5:
            Mystring = "VK_OEM_5"
        Case VK_OEM_6:
            Mystring = "VK_OEM_6"
        Case VK_OEM_7:
            Mystring = "VK_OEM_7"
        Case VK_OEM_8:
            Mystring = "VK_OEM_8"
        Case VK_OEM_102:
            Mystring = "VK_OEM_102"
        Case VK_PROCESSKEY:
            Mystring = "VK_PROCESSKEY"
        Case VK_PACKET:
            Mystring = "VK_PACKET"
        Case VK_ATTN:
            Mystring = "VK_ATTN"
        Case VK_CRSEL:
            Mystring = "VK_CRSEL"
        Case VK_EXSEL:
            Mystring = "VK_EXSEL"
        Case VK_EREOF:
            Mystring = "VK_EREOF"
        Case VK_PLAY:
            Mystring = "VK_PLAY"
        Case VK_ZOOM:
            Mystring = "VK_ZOOM"
        Case VK_NONAME:
            Mystring = "VK_NONAME"
        Case VK_PA1:
            Mystring = "VK_PA1"
        Case VK_OEM_CLEAR:
            Mystring = "VK_OEM_CLEAR"
        Case Else:
            Mystring = _Trim$(Str$(MyInteger))
    End Select
    VirtualKeyCodeToString$ = Mystring
End Function ' VirtualKeyCodeToString$

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CONSTANT TO STRING FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN DRAWING ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D BOX (SOLID)
' https://www.qb64.org/wiki/LINE

' Renamed DrawBox/DrawBoxLine to DrawSolidBox

Sub DrawBoxSolid (iX As Integer, iY As Integer, iSize As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + iSize, iY + iSize), fgColor, BF ' Draw a solid box
End Sub ' DrawBoxSolid

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D RECTANGLE (SOLID)

Sub DrawRectSolid (iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + iSizeW, iY + iSizeH), fgColor, BF ' Draw a solid rectangle
End Sub ' DrawRectSolid

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END DRAWING ROUTINES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GENERAL PURPOSE ROUTINES #GENERAL
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

Function AppendString$ (MyString As String, NewString As String, MyDelimiter As String)
    Dim sResult As String: sResult = MyString
    If Len(MyString) > 0 Then
        sResult = sResult + MyDelimiter
    End If
    sResult = sResult + NewString
    AppendString$ = sResult
End Function ' AppendString$

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

Sub AppendToStringArray (MyStringArray$(), MyString$)
    ReDim _Preserve MyStringArray$(LBound(MyStringArray$) To UBound(MyStringArray$) + 1)
    MyStringArray$(UBound(MyStringArray$)) = MyString$
End Sub ' AppendToStringArray

' /////////////////////////////////////////////////////////////////////////////
' See also StringTo2dArray

Function Array2dToString$ (MyArray() As String)
    Dim MyString As String
    Dim iY As Integer
    Dim iX As Integer
    Dim sLine As String
    MyString = ""
    For iY = LBound(MyArray, 1) To UBound(MyArray, 1)
        sLine = ""
        For iX = LBound(MyArray, 2) To UBound(MyArray, 2)
            sLine = sLine + MyArray(iY, iX)
        Next iX
        MyString = MyString + sLine + Chr$(13)
    Next iY
    Array2dToString$ = MyString
End Function ' Array2dToString$

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

'Function Array2dToStringTest$ (MyArray() As String)
'    Dim MyString As String
'    Dim iY As Integer
'    Dim iX As Integer
'    Dim sLine As String
'    MyString = ""
'    MyString = MyString + "           11111111112222222222333" + Chr$(13)
'    MyString = MyString + "  12345678901234567890123456789012" + Chr$(13)
'    For iY = LBound(MyArray, 1) To UBound(MyArray, 1)
'        sLine = ""
'        sLine = sLine + Right$("  " + cstr$(iY), 2)
'        For iX = LBound(MyArray, 2) To UBound(MyArray, 2)
'            sLine = sLine + MyArray(iY, iX)
'        Next iX
'        sLine = sLine + Right$("  " + cstr$(iY), 2)
'        MyString = MyString + sLine + Chr$(13)
'    Next iY
'    MyString = MyString + "  12345678901234567890123456789012" + Chr$(13)
'    MyString = MyString + "           11111111112222222222333" + Chr$(13)
'    Array2dToStringTest$ = MyString
'End Function ' Array2dToStringTest$

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function CosD (degrees)
    CosD = Cos(_D2R(degrees))
End Function ' CosD

' /////////////////////////////////////////////////////////////////////////////
' Integer to string

Function cstr$ (myValue)
    'cstr$ = LTRIM$(RTRIM$(STR$(myValue)))
    cstr$ = _Trim$(Str$(myValue))
End Function ' cstr$

' /////////////////////////////////////////////////////////////////////////////
' Long to string

Function cstrl$ (myValue As Long)
    cstrl$ = _Trim$(Str$(myValue))
End Function ' cstrl$

' /////////////////////////////////////////////////////////////////////////////
' Single to string

Function cstrs$ (myValue As Single)
    ''cstr$ = LTRIM$(RTRIM$(STR$(myValue)))
    cstrs$ = _Trim$(Str$(myValue))
End Function ' cstrs$

' /////////////////////////////////////////////////////////////////////////////
' Unsigned Long to string

Function cstrul$ (myValue As _Unsigned Long)
    cstrul$ = _Trim$(Str$(myValue))
End Function ' cstrul$

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function

Function CurrentDateTime$
    CurrentDateTime$ = Mid$(Date$, 7, 4) + "-" + _
        Mid$(Date$, 1, 5) + " " + _
        Time$
End Function ' CurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function DAtan2 (x1, y1, x2, y2) ' The angle in degrees a 2nd point (x2, y2)  makes to a first point (x1, y1)
    Dim deltaX As Integer
    Dim deltaY As Integer
    Dim rtn As Integer

    ' Delta means change between 1 measure and another for example x2 - x1
    deltaX = x2 - x1
    deltaY = y2 - y1

    ' To find the angle point(x2, y2) makes to (x1, y1) in Degrees
    ' Take DegreeAngle = DAtan2(y2 - y1, x2 - x1)
    rtn = _R2D(_Atan2(deltaY, deltaX))
    If rtn < 0 Then
        DAtan2 = rtn + 360
    Else
        DAtan2 = rtn
    End If
End Function ' DAtan2

' /////////////////////////////////////////////////////////////////////////////
' Hack function, to be replaced with something better

Function DblToInt% (dblOld As Double)
    Dim dblNew As Double
    Dim sValue As String
    Dim iPos As Integer
    dblNew = RoundDouble#(dblOld, 0)
    sValue = DblToStr$(dblNew)
    DblToInt% = Val(sValue)
End Function ' DblToInt%

' /////////////////////////////////////////////////////////////////////////////
' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example: A string function that displays extremely small or large exponential decimal values.

Function DblToStr$ (n#)
    Dim result$: result$ = ""
    Dim value$
    Dim Xpos%
    Dim expo%
    Dim sign$
    Dim valu$
    Dim dot%
    Dim L%
    Dim add$
    Dim min$
    Dim DP$
    Dim n%
    Dim num$: num$ = ""

    value$ = UCase$(LTrim$(Str$(n#)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") ' only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n% = 1 To L%
            If Mid$(valu$, n%, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n%, 1)
            End If
        Next n%
        result$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
    Else
        result$ = value$
    End If

    DblToStr$ = result$
End Function ' DblToStr$

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

Function DblRoundedToStr$ (dblValue As Double, intNumPlaces As Integer)
    Dim dblNew As Double
    dblNew = RoundDouble#(dblValue, intNumPlaces)
    DblRoundedToStr$ = DblToStr$(dblNew)
End Function ' DblRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Receives an {sDelim} delimited list {sInput}
' returns the list with all duplicate entries removed.

Function DedupeDelimList$ (sInput As String, sDelim As String)
    ReDim arrLines(-1) As String
    Dim sOutput As String
    Dim iLoop As Integer

    split sInput, sDelim, arrLines()
    sOutput = sDelim
    For iLoop = LBound(arrLines) To UBound(arrLines)
        If InStr(1, sOutput, sDelim + arrLines(iLoop) + sDelim) = 0 Then
            sOutput = sOutput + arrLines(iLoop) + sDelim
        End If
    Next iLoop

    DedupeDelimList$ = sOutput
End Function ' DedupeDelimList$

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

Function DoubleABS# (dblValue As Double)
    If Sgn(dblValue) = -1 Then
        DoubleABS# = 0 - dblValue
    Else
        DoubleABS# = dblValue
    End If
End Function ' DoubleABS#

' /////////////////////////////////////////////////////////////////////////////
' bplus: The Gold Standard is even better than THE QB64 CIRCLE sub in this respect!
' https://forum.qb64.org/index.php?topic=1044.135

' from Steve Gold standard
' Renamed fcirc to DrawCircleSolid

' Not as fast as DrawCircleTopLeft but pretty fast.

' Example:
' Screen _NewImage(800, 600, 32)
' _ScreenMove 250, 60
' For r = 250 To 0 Step -60
'     DrawCircleSolid 400, 300, r, _RGBA(255, 255, 255, 100)
' Next r

Sub DrawCircleSolid (CX As Integer, CY As Integer, R As Integer, C As _Unsigned Long)
    Dim Radius As Integer, RadiusError As Integer
    Dim X As Integer, Y As Integer

    Radius = Abs(R)
    RadiusError = -Radius
    X = Radius
    Y = 0

    If Radius = 0 Then PSet (CX, CY), C: Exit Sub

    ' Draw the middle span here so we don't draw it twice in the main loop,
    ' which would be a problem with blending turned on.
    Line (CX - X, CY)-(CX + X, CY), C, BF

    While X > Y
        RadiusError = RadiusError + Y * 2 + 1
        If RadiusError >= 0 Then
            If X <> Y + 1 Then
                Line (CX - Y, CY - X)-(CX + Y, CY - X), C, BF
                Line (CX - Y, CY + X)-(CX + Y, CY + X), C, BF
            End If
            X = X - 1
            RadiusError = RadiusError - X * 2
        End If
        Y = Y + 1
        Line (CX - X, CY - Y)-(CX + X, CY - Y), C, BF
        Line (CX - X, CY + Y)-(CX + X, CY + Y), C, BF
    Wend
End Sub ' DrawCircleSolid

' /////////////////////////////////////////////////////////////////////////////
' Draws scaled + rotated text to screen
' by BPlus

' Scale rotate font text strings by B+
' https://qb64phoenix.com/forum/showthread.php?tid=414&highlight=rotate+text

' INPUT:
' S$ is the string to display
' c is the color (will have a transparent background)
' midX and midY is the center of where you want to display the string
' xScale would multiply 8 pixel width of default font
' yScale would multiply the 16 pixel height of the default font
' Rotation is in Radian units, use _D2R to convert Degree units to Radian units

' DEPENDENCIES:
' drwString needs sub RotoZoom2, intended for graphics screens using the default font.

Sub drwString (S$, c As _Unsigned Long, midX, midY, xScale, yScale, Rotation As Single)
    Dim I&
    Dim storeDest&

    I& = _NewImage(_PrintWidth(S$), _FontHeight, 32)
    _Dest I&
    Color c, _RGBA32(0, 0, 0, 0)
    _PrintString (0, 0), S$
    _Dest storeDest&
    RotoZoom2 midX, midY, I&, xScale, yScale, Rotation
    _FreeImage I&
End Sub ' drwString

' /////////////////////////////////////////////////////////////////////////////
' 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 sy%
    Dim dx%
    Dim dy%

    'bError% = FALSE
    'LOCATE 2, 2: PRINT "(" + STR$(x%) + "," + STR$(y%) + ") to (" + STR$(x2%) + "," + STR$(y2%) + ") of " + CHR$(34) + c$ + CHR$(34);

    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$;
        Else
            'PSET (x%, y%), c%
            Locate x%, y%
            Print 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$;

End Sub ' DrawTextLine

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

Sub DumpScreenAndFontSize ()
    Dim iCols As Integer
    Dim iRows As Integer
    'Screen _NewImage(1024, 768, 32): _ScreenMove 0, 0
    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight
    Print "_Width(0)  =" + _Trim$(Str$(_Width(0)))
    Print "_Height(0) =" + _Trim$(Str$(_Height(0)))
    Print "_FontWidth =" + _Trim$(Str$(_FontWidth))
    Print "_FontHeight=" + _Trim$(Str$(_FontHeight))
    Print "iCols = _Width(0)  \ _FontWidth  = " + _Trim$(Str$(iCols))
    Print "iRows = _Height(0) \ _FontHeight = " + _Trim$(Str$(iRows))
End Sub ' DumpScreenAndFontSize

' /////////////////////////////////////////////////////////////////////////////
' Use with timer functions to avoid "after midnight" bug.

' SMcNeill
' More efficient version of ExtendedTimer.
' This stores our day values and only updates them when necessary.
' We really don't need to figure out how many seconds are in today over and over endlessly
' -- just count them once, and when the clock swaps back to 0:0:0, add 24*60*60 seconds to the count.

' Re: how to time something (ie do loop for n seconds)
' https://forum.qb64.org/index.php?topic=4682.0

' SMcNeill, QB64 Developer
' Reply #1 on: Today at 11:26:52 am
'
' One caveat here:  You *can* experience bugs with this after midnight.
'
' Program starts at 23:59:59.
' Add three seconds -- 24:00:02...  (In seconds, and not hours and minutes like this, though hours and minutes are easier to visualize.)
' Clock hits midnight:  0:00:00
'
' At no point will you ever have TIMER become greater than t#.
'
' If you're going to have a program which might run into this issue,
' I'd suggest just plugging in my ExtendedTimer and use it instead:
'
' Most of us write time code to test little snippets for which method might
' be faster for us while we're coding.  The clock resetting on us isn't
' normally such a big deal.  When it is, however, all you have to do is
' swap to the ExtendedTimer function [below]
'
' Returns a value for you based off DAY + TIME, rather than just time alone!
' No midnight clock issues with something like that in our programs.  ;)

' Example using regular Timer:
'    t# = Timer + 3
'    Do
'         '(SOMETHING)
'    Loop Until Timer > t#

' Usage:
'     ' DO SOMETHING FOR 3 SECONDS
'     t# = ExtendedTimer1 + 3
'     Do
'         '(SOMETHING)
'     Loop Until Timer > t#

$If EXTENDEDTIMER = UNDEFINED Then
    $Let EXTENDEDTIMER = TRUE

    Function ExtendedTimer##
        'modified extendedtimer to store the old day's count, and not have to recalculate it every time the routine is called.

        Static olds As _Float, old_day As _Float, oldt As _Float
        Dim m As Integer, d As Integer, y As Integer
        Dim s As _Float, day As String
        If olds = 0 Then 'calculate the day the first time the extended timer runs
            day = Date$
            m = Val(Left$(day, 2))
            d = Val(Mid$(day, 4, 2))
            y = Val(Right$(day, 4)) - 1970
            Select Case m 'Add the number of days for each previous month passed
                Case 2: d = d + 31
                Case 3: d = d + 59
                Case 4: d = d + 90
                Case 5: d = d + 120
                Case 6: d = d + 151
                Case 7: d = d + 181
                Case 8: d = d + 212
                Case 9: d = d + 243
                Case 10: d = d + 273
                Case 11: d = d + 304
                Case 12: d = d + 334
            End Select
            If (y Mod 4) = 2 And m > 2 Then d = d + 1 'add a day if this is leap year and we're past february
            d = (d - 1) + 365 * y 'current month days passed + 365 days per each standard year
            d = d + (y + 2) \ 4 'add in days for leap years passed
            s = d * 24 * 60 * 60 'Seconds are days * 24 hours * 60 minutes * 60 seconds
            old_day = s
        End If
        If Timer < oldt Then 'we went from 23:59:59 (a second before midnight) to 0:0:0 (midnight)
            old_day = s + 83400 'add another worth of seconds to our counter
        End If
        oldt = Timer
        olds = old_day + oldt
        ExtendedTimer## = olds
    End Function ' ExtendedTimer##
$End If

' /////////////////////////////////////////////////////////////////////////////
' Fill a 2D integer array with a specified value

Sub FillIntegerArray2D (MyArray() As Integer, MyValue As Integer)
    Dim iLoop1 As Integer
    Dim iLoop2 As Integer
    For iLoop1 = 1 To UBound(MyArray, 1)
        For iLoop2 = 1 To UBound(MyArray, 2)
            'Print "iLoop1=" + cstr$(iLoop1) + ", iLoop2=" + cstr$(iLoop2)
            MyArray(iLoop1, iLoop2) = MyValue
        Next iLoop2
    Next iLoop1
End Sub ' FillIntegerArray2D

' /////////////////////////////////////////////////////////////////////////////
' Fill a 2D long array with a specified value

Sub FillLongArray2D (MyArray() As Long, MyValue As Long)
    Dim iLoop1 As Integer
    Dim iLoop2 As Integer
    For iLoop1 = 1 To UBound(MyArray, 1)
        For iLoop2 = 1 To UBound(MyArray, 2)
            'Print "iLoop1=" + cstr$(iLoop1) + ", iLoop2=" + cstr$(iLoop2)
            MyArray(iLoop1, iLoop2) = MyValue
        Next iLoop2
    Next iLoop1
End Sub ' FillLongArray2D

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

Function FloatRoundedToStr$ (fValue As _Float, intNumPlaces As Integer)
    Dim fNew As _Float
    fNew = RoundNatural##(fValue, intNumPlaces)
    FloatRoundedToStr$ = FloatToStr$(fNew)
End Function ' DblRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example: A string function that displays extremely small or large exponential decimal values.

Function FloatToStr$ (n##)
    Dim value$
    Dim Xpos%
    Dim expo%
    Dim sign$
    Dim valu$
    Dim dot%
    Dim L%
    Dim add$
    Dim min$
    Dim DP$
    Dim n As Integer
    Dim num$: num$ = ""

    value$ = UCase$(LTrim$(Str$(n##)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") 'only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n = 1 To L%
            If Mid$(valu$, n, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n, 1)
            End If
        Next n
    Else
        FloatToStr$ = value$
        Exit Function
    End If
    FloatToStr$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
End Function ' FloatToStr$

' /////////////////////////////////////////////////////////////////////////////
' TODO: find the newer formatting function?

Function FormatNumber$ (myValue, iDigits As Integer)
    Dim strValue As String
    strValue = DblToStr$(myValue) + String$(iDigits, " ")
    If myValue < 1 Then
        If myValue < 0 Then
            strValue = Replace$(strValue, "-.", "-0.")
        ElseIf myValue > 0 Then
            strValue = "0" + strValue
        End If
    End If
    FormatNumber$ = Left$(strValue, iDigits)
End Function ' FormatNumber$

' /////////////////////////////////////////////////////////////////////////////
' From: Bitwise Manipulations By Steven Roman
' http://www.romanpress.com/Articles/Bitwise_R/Bitwise.htm

' Returns the 8-bit binary representation
' of an integer iInput where 0 <= iInput <= 255

Function GetBinary$ (iInput1 As Integer)
    Dim sResult As String
    Dim iLoop As Integer
    Dim iInput As Integer: iInput = iInput1

    sResult = ""

    If iInput >= 0 And iInput <= 255 Then
        For iLoop = 1 To 8
            sResult = LTrim$(RTrim$(Str$(iInput Mod 2))) + sResult
            iInput = iInput \ 2
            'If iLoop = 4 Then sResult = " " + sResult
        Next iLoop
    End If

    GetBinary$ = sResult
End Function ' GetBinary$

' /////////////////////////////////////////////////////////////////////////////
' wonderfully inefficient way to read if a bit is set
' ival = GetBit256%(int we are comparing, int containing the bits we want to read)

' See also: GetBit256%, SetBit256%

Function GetBit256% (iNum1 As Integer, iBit1 As Integer)
    Dim iResult As Integer
    Dim sNum As String
    Dim sBit As String
    Dim iLoop As Integer
    Dim bContinue As Integer
    'DIM iTemp AS INTEGER
    Dim iNum As Integer: iNum = iNum1
    Dim iBit As Integer: iBit = iBit1

    iResult = FALSE
    bContinue = TRUE

    If iNum < 256 And iBit <= 128 Then
        sNum = GetBinary$(iNum)
        sBit = GetBinary$(iBit)
        For iLoop = 1 To 8
            If Mid$(sBit, iLoop, 1) = "1" Then
                'if any of the bits in iBit are false, return false
                If Mid$(sNum, iLoop, 1) = "0" Then
                    iResult = FALSE
                    bContinue = FALSE
                    Exit For
                End If
            End If
        Next iLoop
        If bContinue = TRUE Then
            iResult = TRUE
        End If
    End If

    GetBit256% = iResult
End Function ' GetBit256%

' /////////////////////////////////////////////////////////////////////////////
' Returns the text character at positon x%, y%

' Does the same as:
'   Locate y%, x%
'   GetCharXY% = Screen(CsrLin, Pos(0))

' See also: GetColorXY&

Function GetCharXY% (x%, y%)
    GetCharXY% = Screen(y%, x%, 0) ' when 3rd parameter = 0 returns character code
End Function ' GetCharXY%

' /////////////////////////////////////////////////////////////////////////////
' Returns the text color at positon x%, y%

' See also: GetCharXY%

Function GetColorXY& (x%, y%)
    GetColorXY& = Screen(y%, x%, 1) ' when 3rd parameter = 1 returns character color
End Function ' GetColorXY

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function
' Format: {YYYY}-{MM}-{DD} {hh}:[mm}:{ss}

' Uses:
'     TIME$
'         The TIME$ Function returns a STRING representation
'         of the current computer time in a 24 hour format.
'         https://qb64phoenix.com/qb64wiki/index.php/TIME$
'     DATE$
'         The DATE$ function returns the current computer date
'         as a string in the format "mm-dd-yyyy".
'         https://qb64phoenix.com/qb64wiki/index.php/DATE$
'
' TODO: support template where
'       {yyyy} = 4 digit year
'       {mm}   = 2 digit month
'       {dd}   = 2 digit day
'       {hh}   = 2 digit hour (12-hour)
'       {rr}   = 2 digit hour (24-hour)
'       {nn}   = 2 digit minute
'       {ss}   = 2 digit second
'       {ampm} = AM/PM

' We got the nn for minute from Microsoft > Office VBA Reference > DateDiff function
' https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/datediff-function

' PRINT "Current date time (simple format) = " + Chr$(34) + GetCurrentDateTime$("{yyyy}-{mm}-{dd} {rr}:{nn}:{ss}") + Chr$(34)
' PRINT "Current date time (US format)     = " + Chr$(34) + GetCurrentDateTime$("{mm}/{dd}/{yyyy} {hh}:{nn}:{ss} {ampm}") + Chr$(34)
' PRINT "Filename timestamp                = " + Chr$(34) + GetCurrentDateTime$("{yyyy}{mm}{dd}_{rr}{nn}{ss}") + Chr$(34)

Function GetCurrentDateTime$ (sTemplate$)
    Dim sDate$: sDate$ = Date$
    Dim sTime$: sTime$ = Time$
    Dim sYYYY$: sYYYY$ = Mid$(sDate$, 7, 4)
    Dim sMM$: sMM$ = Mid$(sDate$, 1, 2)
    Dim sDD$: sDD$ = Mid$(sDate$, 4, 2)
    Dim sHH24$: sHH24$ = Mid$(sTime$, 1, 2)
    Dim sHH$: sHH$ = ""
    Dim sMI$: sMI$ = Mid$(sTime$, 4, 2)
    Dim sSS$: sSS$ = Mid$(sTime$, 7, 2)
    Dim iHour%: iHour% = Val(sHH24$)
    Dim sAMPM$: sAMPM$ = ""
    Dim result$: result$ = ""

    ' FIGURE OUT AM/PM
    If InStr(sTemplate$, "{ampm}") > 0 Then
        If iHour% = 0 Then
            sAMPM$ = "AM"
            iHour% = 12
        ElseIf iHour% > 0 And iHour% < 12 Then
            sAMPM$ = "AM"
        ElseIf iHour% = 12 Then
            sAMPM$ = "PM"
        Else
            sAMPM$ = "PM"
            iHour% = iHour% - 12
        End If
        sHH$ = Right$("00" + _Trim$(Str$(iHour%)), 2)
    End If

    ' POPULATE TEMPLATE
    result$ = sTemplate$
    result$ = Replace$(result$, "{yyyy}", sYYYY$)
    result$ = Replace$(result$, "{mm}", sMM$)
    result$ = Replace$(result$, "{dd}", sDD$)
    result$ = Replace$(result$, "{hh}", sHH$)
    result$ = Replace$(result$, "{rr}", sHH24$)
    result$ = Replace$(result$, "{nn}", sMI$)
    result$ = Replace$(result$, "{ss}", sSS$)
    result$ = Replace$(result$, "{ampm}", sAMPM$)

    ' RETURN RESULT
    GetCurrentDateTime$ = result$
End Function ' GetCurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' From: Bitwise Manipulations By Steven Roman
' http://www.romanpress.com/Articles/Bitwise_R/Bitwise.htm

' Returns the integer that corresponds to a binary string of length 8

Function GetIntegerFromBinary% (sBinary1 As String)
    Dim iResult As Integer
    Dim iLoop As Integer
    Dim strBinary As String
    Dim sBinary As String: sBinary = sBinary1

    iResult = 0
    strBinary = Replace$(sBinary, " ", "") ' Remove any spaces
    For iLoop = 0 To Len(strBinary) - 1
        iResult = iResult + 2 ^ iLoop * Val(Mid$(strBinary, Len(strBinary) - iLoop, 1))
    Next iLoop

    GetIntegerFromBinary% = iResult
End Function ' GetIntegerFromBinary%

' /////////////////////////////////////////////////////////////////////////////
' Receives a {sDelimeter} delimited list of numbers {MyString}
' and splits it up into an integer array arrInteger()
' beginning at index {iMinIndex}.

Sub GetIntegerArrayFromDelimList (MyString As String, sDelimiter As String, iMinIndex As Integer, arrInteger() As Integer)
    ReDim arrString(-1) As String
    Dim CleanString As String
    Dim iLoop As Integer
    Dim iCount As Integer: iCount = iMinIndex - 1

    ReDim arrInteger(-1) As Integer

    'DebugPrint "GetIntegerArrayFromDelimList " + _
    '    "MyString=" + chr$(34) + MyString + chr$(34) + ", " + _
    '    "sDelimiter=" + chr$(34) + sDelimiter + chr$(34) + ", " + _
    '    "iMinIndex=" + cstr$(iMinIndex) + ", " + _
    '    "arrInteger()"

    If Len(sDelimiter) > 0 Then
        CleanString = MyString
        If sDelimiter <> " " Then
            CleanString = Replace$(CleanString, " ", "")
        End If

        split CleanString, sDelimiter, arrString()
        iCount = iMinIndex - 1
        For iLoop = LBound(arrString) To UBound(arrString)
            If IsNum%(arrString(iLoop)) = TRUE Then
                iCount = iCount + 1
                ReDim _Preserve arrInteger(iMinIndex To iCount) As Integer
                arrInteger(iCount) = Val(arrString(iLoop))
                'DebugPrint "5633 arrInteger(" + cstr$(iCount) + ") = VAL(arrString(" + cstr$(iLoop) + ")) = " + cstr$(arrInteger(iCount))

            End If
        Next iLoop
    Else
        If IsNum%(MyString) = TRUE Then
            ReDim _Preserve arrInteger(iMinIndex To iMinIndex) As Integer
            arrInteger(iMinIndex) = Val(MyString)
        End If
    End If

    'CleanString=""
    'for iLoop=lbound(arrInteger) to ubound(arrInteger)
    'CleanString = CleanString + iifstr$(iLoop=lbound(arrInteger), "", ",") + cstr$(arrInteger(iLoop))
    'next iLoop
    'DebugPrint "arrInteger=(" + CleanString + ")"
End Sub ' GetIntegerArrayFromDelimList

' /////////////////////////////////////////////////////////////////////////////
' Gets the # of seconds so far today

Function GetTimeSeconds& ()
    Dim result&: result& = 0
    Dim sTime$: sTime$ = Time$
    Dim sHH24$: sHH24$ = Mid$(sTime$, 1, 2)
    Dim sMI$: sMI$ = Mid$(sTime$, 4, 2)
    Dim sSS$: sSS$ = Mid$(sTime$, 7, 2)

    result& = result& + Val(sSS$)
    result& = result& + (Val(sMI$) * 60)
    result& = result& + ((Val(sHH24$) * 60) * 60)

    ' RETURN RESULT
    GetTimeSeconds& = result&
End Function ' GetTimeSeconds&

' /////////////////////////////////////////////////////////////////////////////
' 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%

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for integers

Function IIF (Condition, IfTrue, IfFalse)
    If Condition Then IIF = IfTrue Else IIF = IfFalse
End Function

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for strings

Function IIFS$ (Condition, IfTrue$, IfFalse$)
    If Condition Then IIFS$ = IfTrue$ Else IIFS$ = IfFalse$
End Function

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

Function IntPadLeft$ (iValue As Integer, iWidth As Integer)
    IntPadLeft$ = Right$(String$(iWidth, " ") + _Trim$(Str$(iValue)), iWidth)
End Function ' IntPadLeft$

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

Function IntPadRight$ (iValue As Integer, iWidth As Integer)
    IntPadRight$ = Left$(_Trim$(Str$(iValue)) + String$(iWidth, " "), iWidth)
End Function ' IntPadRight$

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if number n is even
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/
' see also: IsOdd%

Function IsEven% (n)
    If n Mod 2 = 0 Then
        IsEven% = TRUE
    Else
        IsEven% = FALSE
    End If
End Function ' IsEven%

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

Function IsNum% (text$)
    IsNum% = IsNumber%(text$)
End Function ' IsNum%

'' OLD IsNum% CHECK FAILS FOR NUMBERS LIKE "002" AND "2.000":
'' from https://www.qb64.org/forum/index.php?topic=896.0
'Function IsNum% (text$)
'    Dim a$
'    Dim b$
'    a$ = _Trim$(text$)
'    b$ = _Trim$(Str$(Val(text$)))
'    If a$ = b$ Then
'        IsNum% = TRUE
'    Else
'        IsNum% = FALSE
'    End If
'End Function ' IsNum%

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if value OriginalString$ is numeric else returns FALSE.

' 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 iDecimalCount%
    Dim sNextChar$

    If Len(OriginalString$) > 0 Then
        TestString$ = ""
        If Left$(OriginalString$, 1) = "+" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
        ElseIf Left$(OriginalString$, 1) = "-" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
        Else
            TestString$ = OriginalString$
        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%

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

'Sub IsNumberTest
'    Dim in$
'    Cls
'    IsNumberTest1 "1"
'    IsNumberTest1 "01"
'    IsNumberTest1 "001"
'    IsNumberTest1 "-1"
'    IsNumberTest1 "-01"
'    IsNumberTest1 "-001"
'    IsNumberTest1 "+1"
'    IsNumberTest1 "+01"
'    IsNumberTest1 "+001"
'    IsNumberTest1 ".1"
'    IsNumberTest1 ".01"
'    IsNumberTest1 ".001"
'    IsNumberTest1 ".10"
'    IsNumberTest1 ".100"
'    IsNumberTest1 "..100"
'    IsNumberTest1 "100."
'    Input "PRESS ENTER TO CONTINUE TEST";in$
'    Cls
'    IsNumberTest1 "0.10"
'    IsNumberTest1 "00.100"
'    IsNumberTest1 "000.1000"
'    IsNumberTest1 "000..1000"
'    IsNumberTest1 "000.1000.00"
'    IsNumberTest1 "+1.00"
'    IsNumberTest1 "++1.00"
'    IsNumberTest1 "+-1.00"
'    IsNumberTest1 "-1.00"
'    IsNumberTest1 "-+1.00"
'    IsNumberTest1 " 1"
'    IsNumberTest1 "1 "
'    IsNumberTest1 "1. 01"
'    IsNumberTest1 "+1 "
'End Sub ' IsNumberTest
'Sub IsNumberTest1(MyString As String)
'    Const cWidth = 16
'    Dim sInput As String : sInput = left$(Chr$(34) + MyString + Chr$(34) + String$(cWidth, " "), cWidth)
'    Dim sResult As String : sResult = right$(String$(2, " ") + _Trim$(Str$(IsNumber%(MyString))), 2)
'    Print "IsNumber%(" + sInput + ") returns " + sResult
'End Sub ' IsNumberTest1

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if number n is odd
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/
' see also: IsEven%

Function IsOdd% (n)
    If n Mod 2 = 1 Then
        IsOdd% = TRUE
    Else
        IsOdd% = FALSE
    End If
End Function ' IsOdd%

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

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

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

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

Function LeftPadString$ (myString$, toWidth%, padChar$)
    LeftPadString$ = Right$(String$(toWidth%, padChar$) + myString$, toWidth%)
End Function ' LeftPadString$

' /////////////////////////////////////////////////////////////////////////////
' ABS was returning strange values with type LONG
' so I created this which does not.

' See also: UnsignedLongABS

Function LongABS& (lngValue As Long)
    If Sgn(lngValue) = -1 Then
        LongABS& = 0 - lngValue
    Else
        LongABS& = lngValue
    End If
End Function ' LongABS&

' /////////////////////////////////////////////////////////////////////////////
' Remove scientific Notation to String (~40 LOC)
' SMcNeill Jan 7, 2020
' https://www.qb64.org/forum/index.php?topic=1555.msg112989#msg112989

' Last Function in code marked Best Answer (removed debug comments and
' blank lines added these 2 lines.)

Function N2S$ (EXP$)
    ReDim t$, sign$, l$, r$, r&&
    ReDim dp As Long, dm As Long, ep As Long, em As Long, check1 As Long, l As Long, i As Long
    t$ = LTrim$(RTrim$(EXP$))
    If Left$(t$, 1) = "-" Or Left$(t$, 1) = "N" Then sign$ = "-": t$ = Mid$(t$, 2)
    dp = InStr(t$, "D+"): dm = InStr(t$, "D-")
    ep = InStr(t$, "E+"): em = InStr(t$, "E-")
    check1 = Sgn(dp) + Sgn(dm) + Sgn(ep) + Sgn(em)
    If check1 < 1 Or check1 > 1 Then N2S = _Trim$(EXP$): Exit Function ' If no scientic notation is found, or if we find more than 1 type, it's not SN!
    Select Case l ' l now tells us where the SN starts at.
        Case Is < dp: l = dp
        Case Is < dm: l = dm
        Case Is < ep: l = ep
        Case Is < em: l = em
    End Select
    l$ = Left$(t$, l - 1) ' The left of the SN
    r$ = Mid$(t$, l + 1): r&& = Val(r$) ' The right of the SN, turned into a workable long
    If InStr(l$, ".") Then ' Location of the decimal, if any
        If r&& > 0 Then
            r&& = r&& - Len(l$) + 2
        Else
            r&& = r&& + 1
        End If
        l$ = Left$(l$, 1) + Mid$(l$, 3)
    End If
    Select Case r&&
        Case 0 ' what the heck? We solved it already?
            ' l$ = l$
        Case Is < 0
            For i = 1 To -r&&
                l$ = "0" + l$
            Next
            l$ = "." + l$
        Case Else
            For i = 1 To r&&
                l$ = l$ + "0"
            Next
            l$ = l$
    End Select
    N2S$ = sign$ + l$
End Function ' N2S$

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

Function PadLeft$ (MyString As String, iLength As Integer)
    Dim sValue As String
    sValue = String$(iLength, " ") + MyString
    sValue = Right$(sValue, iLength)
    PadLeft$ = sValue
End Function ' PadLeft$

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

Function PadRight$ (MyString As String, iLength As Integer)
    Dim sValue As String
    sValue = MyString + String$(iLength, " ")
    sValue = Left$(sValue, iLength)
    PadRight$ = sValue
End Function ' PadRight$

' /////////////////////////////////////////////////////////////////////////////
' Pauses for iDS deciseconds (iDS * 100 ms)

Sub PauseDecisecond (iDS As Integer)
    Dim iCount As Integer
    iCount = 0
    Do
        iCount = iCount + 1
        _Limit 10 ' run 10x every second
    Loop Until iCount = iDS
End Sub ' PauseDecisecond

' /////////////////////////////////////////////////////////////////////////////
' Returns TRUE if point (x1%, y1%) is adjacent to point (x2%, y2%)

Function PointsAreAdjacent% (x1%, y1%, x2%, y2%)
    Dim bResult%: bResult% = FALSE

    ' x or y can be the same, but not both
    If (x1% <> x2%) Or (y1% <> y2%) Then
        If (x1% = x2%) Or ((x1% = (x2% + 1)) Or (x2% = (x1% + 1))) Then
            If (y1% = y2%) Or ((y1% = (y2% + 1)) Or (y2% = (y1% + 1))) Then
                bResult% = TRUE
            End If
        End If
    End If
    PointsAreAdjacent% = bResult%
End Function ' PointsAreAdjacent%



' /////////////////////////////////////////////////////////////////////////////
' Prints MyString$, iLinesPerPage% lines at a time,
' then waits for user to press a key to continue,
' before printing the next iLinesPerPage% lines.

Sub PrintPaged (MyString$, iLinesPerPage%)
    Dim delim$
    ReDim arrTest$(0)
    Dim iLoop%
    Dim iCount%
    Dim in$
    _KeyClear ' CLEAR KEYBOARD BUFFER

    delim$ = Chr$(13)
    split MyString$, delim$, arrTest$()
    iCount% = 0
    For iLoop% = LBound(arrTest$) To UBound(arrTest$)
        iCount% = iCount% + 1
        If iCount% > iLinesPerPage% Then
            Print
            Print "PRESS ANY KEY TO CONTINUE"
            Sleep: _KeyClear ' CLEAR KEYBOARD BUFFER
            Print
            iCount% = 0
        End If
        Print arrTest$(iLoop%)
    Next iLoop%

    _KeyClear ' CLEAR KEYBOARD BUFFER
End Sub ' PrintPaged

' /////////////////////////////////////////////////////////////////////////////
' iNum% = PromptForIntegerInRange%("Please type a number between {min} and {max} (or blank to quit).", 1, 4, 0) ' prompt, min, max, default

Function PromptForIntegerInRange% (sPrompt$, iMin%, iMax%, iDefault%)
    Dim iValue%
    Dim bFinished%
    Dim sPrompt1$
    Dim in$

    If Len(sPrompt$) > 0 Then
        sPrompt1$ = sPrompt$
    Else
        sPrompt1$ = "Please type a number between {min} and {max} (or blank to quit)."
    End If

    sPrompt1$ = Replace$(sPrompt1$, "{min}", cstr$(iMin%))
    sPrompt1$ = Replace$(sPrompt1$, "{max}", cstr$(iMax%))

    bFinished% = FALSE
    Do
        Print sPrompt1$

        Input in$
        in$ = _Trim$(in$)
        If Len(in$) > 0 Then
            If IsNumber(in$) Then
                iValue% = Val(in$)
                If iValue% >= iMin% And iValue% <= iMax% Then
                    'bFinished% = TRUE
                    Exit Do
                Else
                    Print "Number out of range."
                    Print
                End If
            Else
                Print "Not a valid number."
                Print
            End If
        Else
            iValue% = iDefault%
            Exit Do
            'bFinished% = TRUE
        End If
    Loop Until bFinished% = TRUE

    PromptForIntegerInRange% = iValue%
End Function ' PromptForIntegerInRange%

' /////////////////////////////////////////////////////////////////////////////
' Prints text character char$ at positoin x%,y% in color mycolor~&.

Sub PutCharXY (x%, y%, char$, myColor~&)
    Color myColor~&
    Locate y%, x%
    Print char$;
End Sub ' PutCharXY

' /////////////////////////////////////////////////////////////////////////////
' Initializes random number generator seed.

' NOTE:
' using # of seconds so far in the day
' may be slightly more random than Randomize Timer
' unless user runs program at exact same time every day

Sub InitializeRandom
    Dim t9#
    t9# = (Timer * 1000000) Mod 32767
    Randomize t9#
End Sub ' InitializeRandom

' /////////////////////////////////////////////////////////////////////////////
' Initializes random number generator seed.

' *** NOT SURE IF THIS ONE WORKS ***

Sub InitializeRandom1
    Dim iSeed As Integer
    iSeed = GetTimeSeconds& Mod 32767
    Randomize iSeed
End Sub ' InitializeRandom1

' /////////////////////////////////////////////////////////////////////////////
' Generate random value between Min and Max inclusive.

' Note: random-number generator should be initialized with
'       InitializeRandom or Randomize Timer

Function RandomNumber% (Min%, Max%)
    Dim NumSpread%

    '' SET RANDOM SEED
    ''Randomize ' Initialize random-number generator.
    'Randomize Timer

    NumSpread% = (Max% - Min%) + 1
    RandomNumber% = Int(Rnd * NumSpread%) + Min% ' GET RANDOM # BETWEEN Max% AND Min%
End Function ' RandomNumber%

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

$If  Then
    Sub RandomNumberTest
    Dim iCols As Integer: iCols = 10
    Dim iRows As Integer: iRows = 20
    Dim iLoop As Integer
    Dim iX As Integer
    Dim iY As Integer
    Dim sError As String
    Dim sFileName As String
    Dim sText As String
    Dim bAppend As Integer
    Dim iMin As Integer
    Dim iMax As Integer
    Dim iNum As Integer
    Dim iErrorCount As Integer
    Dim sInput$

    sFileName = "c:\temp\maze_test_1.txt"
    sText = "Count" + Chr$(9) + "Min" + Chr$(9) + "Max" + Chr$(9) + "Random"
    bAppend = FALSE
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) = 0 Then
    bAppend = TRUE
    iErrorCount = 0

    iMin = 0
    iMax = iCols - 1
    For iLoop = 1 To 100
    iNum = RandomNumber%(iMin, iMax)
    sText = Str$(iLoop) + Chr$(9) + Str$(iMin) + Chr$(9) + Str$(iMax) + Chr$(9) + Str$(iNum)
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) > 0 Then
    iErrorCount = iErrorCount + 1
    Print Str$(iLoop) + ". ERROR"
    Print "    " + "iMin=" + Str$(iMin)
    Print "    " + "iMax=" + Str$(iMax)
    Print "    " + "iNum=" + Str$(iNum)
    Print "    " + "Could not write to file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print "    " + sError
    End If
    Next iLoop

    iMin = 0
    iMax = iRows - 1
    For iLoop = 1 To 100
    iNum = RandomNumber%(iMin, iMax)
    sText = Str$(iLoop) + Chr$(9) + Str$(iMin) + Chr$(9) + Str$(iMax) + Chr$(9) + Str$(iNum)
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) > 0 Then
    iErrorCount = iErrorCount + 1
    Print Str$(iLoop) + ". ERROR"
    Print "    " + "iMin=" + Str$(iMin)
    Print "    " + "iMax=" + Str$(iMax)
    Print "    " + "iNum=" + Str$(iNum)
    Print "    " + "Could not write to file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print "    " + sError
    End If
    Next iLoop

    Print "Finished generating numbers. Errors: " + Str$(iErrorCount)
    Else
    Print "Error creating file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print sError
    End If

    Input "Press <ENTER> to continue", sInput$
    End Sub ' RandomNumberTest
$End If

' /////////////////////////////////////////////////////////////////////////////
' [Replace$] replaces all instances of the [Find] sub-string
' with the [Add] sub-string within the [Text] string.

' SUMMARY:
'   Purpose:  A library of custom functions that transform strings.
'   Author:   Dustinian Camburides (dustinian@gmail.com)
'   Platform: QB64 (www.qb64.org)
'   Revision: 1.6
'   Updated:  5/28/2012

' INPUT:
'   Text: The input string; the text that's being manipulated.
'   Find: The specified sub-string; the string sought within the [Text] string.
'   Add: The sub-string that's being added to the [Text] string.

' FROM: String Manipulation
' found at abandoned, outdated and now likely malicious qb64 dot net website
' http://www.qb64.[net]/forum/index_topic_5964-0/

Function Replace$ (Text1 As String, Find1 As String, Add1 As String)
    ' VARIABLES:
    Dim Text2 As String
    Dim Find2 As String
    Dim Add2 As String
    Dim lngLocation As Long ' The address of the [Find] substring within the [Text] string.
    Dim strBefore As String ' The characters before the string to be replaced.
    Dim strAfter As String ' The characters after the string to be replaced.

    ' INITIALIZE:
    ' MAKE COPIES SO THE ORIGINAL IS NOT MODIFIED (LIKE ByVal IN VBA)
    Text2 = Text1
    Find2 = Find1
    Add2 = Add1

    lngLocation = InStr(1, Text2, Find2)

    ' PROCESSING:
    ' While [Find2] appears in [Text2]...
    While lngLocation
        ' Extract all Text2 before the [Find2] substring:
        strBefore = Left$(Text2, lngLocation - 1)

        ' Extract all text after the [Find2] substring:
        strAfter = Right$(Text2, ((Len(Text2) - (lngLocation + Len(Find2) - 1))))

        ' Return the substring:
        Text2 = strBefore + Add2 + strAfter

        ' Locate the next instance of [Find2]:
        lngLocation = InStr(1, Text2, Find2)

        ' Next instance of [Find2]...
    Wend

    ' OUTPUT:
    Replace$ = Text2
End Function ' Replace$

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

$If  Then
    Sub ReplaceTest
    Dim in$

    Print "-------------------------------------------------------------------------------"
    Print "ReplaceTest"
    Print

    Print "Original value"
    in$ = "Thiz iz a teZt."
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "Replacing lowercase " + Chr$(34) + "z" + Chr$(34) + " with " + Chr$(34) + "s" + Chr$(34) + "..."
    in$ = Replace$(in$, "z", "s")
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "Replacing uppercase " + Chr$(34) + "Z" + Chr$(34) + " with " + Chr$(34) + "s" + Chr$(34) + "..."
    in$ = Replace$(in$, "Z", "s")
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "ReplaceTest finished."
    End Sub ' ReplaceTest
$End If

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

Function RightPadString$ (myString$, toWidth%, padChar$)
    RightPadString$ = Left$(myString$ + String$(toWidth%, padChar$), toWidth%)
End Function ' RightPadString$

' /////////////////////////////////////////////////////////////////////////////
' This sub gives really nice control over displaying an Image.
' by BPlus

' Scale rotate font text strings by B+
' https://qb64phoenix.com/forum/showthread.php?tid=414&highlight=rotate+text

' USED BY: drwString

Sub RotoZoom2 (centerX As Long, centerY As Long, Image As Long, xScale As Single, yScale As Single, Rotation As Single)
    Dim px(3) As Single: Dim py(3) As Single
    Dim W&
    Dim H&
    Dim sinr!
    Dim cosr!
    Dim i&
    Dim x2&
    Dim y2&

    W& = _Width(Image&): H& = _Height(Image&)
    px(0) = -W& / 2: py(0) = -H& / 2: px(1) = -W& / 2: py(1) = H& / 2
    px(2) = W& / 2: py(2) = H& / 2: px(3) = W& / 2: py(3) = -H& / 2
    sinr! = Sin(-Rotation): cosr! = Cos(-Rotation)
    For i& = 0 To 3
        x2& = (px(i&) * cosr! + sinr! * py(i&)) * xScale + centerX
        y2& = (py(i&) * cosr! - px(i&) * sinr!) * yScale + centerY
        px(i&) = x2&: py(i&) = y2&
    Next
    _MapTriangle (0, 0)-(0, H& - 1)-(W& - 1, H& - 1), Image& To(px(0), py(0))-(px(1), py(1))-(px(2), py(2))
    _MapTriangle (0, 0)-(W& - 1, 0)-(W& - 1, H& - 1), Image& To(px(0), py(0))-(px(3), py(3))-(px(2), py(2))
End Sub ' RotoZoom2

'' /////////////////////////////////////////////////////////////////////////////
'' https://qb64phoenix.com/forum/showthread.php?tid=644
'' From: bplus
'' Date: 07-18-2022, 03:16 PM
'' Here is a Round$ that acts the way you'd expect in under 100 LOC
'' b = b + ...
'
'Function Round$ (anyNumber, dp As Long)
'    ' 5 and up at decimal place dp+1 > +1 at decimal place   4 and down  > +0 at dp
'    ' 2 1 0.-1 -2 -3 -4 ...  pick dp like this for this Round$ Function
'    sn$ = N2S$(Str$(anyNumber + .5 * 10 ^ dp)) ' get rid of sci notation, steve trims it so next find dot
'    dot = InStr(sn$, ".")
'    If dot Then
'        predot = dot - 1
'        postdot = Len(sn$) - (dot + 1)
'    Else
'        predot = Len(sn$)
'        postdot = 0
'    End If
'    ' xxx.yyyyyy  dp = -2
'    '      ^ dp
'    If dp >= 0 Then
'        Rtn$ = Mid$(sn$, 1, predot - dp) + String$(dp, "0")
'    Else
'        Rtn$ = Mid$(sn$, 1, predot) + "." + Mid$(sn$, dot + 1, -dp)
'    End If
'    If Rtn$ = "" Then
'        Round$ = "0"
'    Else
'        Round$ = Rtn$
'    End If
'End Function ' Round$
'
'' /////////////////////////////////////////////////////////////////////////////
''
''Sub RoundTest
''   Print Round$(.15, 0) '  0
''   Print Round$(.15, -1) ' .2
''   Print Round$(.15, -2) ' .15
''   Print Round$(.15, -3) ' .150
''   Print
''   Print Round$(3555, 0) ' 3555
''   Print Round$(3555, 1) ' 3560
''   Print Round$(3555, 2) ' 3600 'good
''   Print Round$(3555, 3) ' 4000
''   Print
''   Print Round$(23.149999, -1) ' 23.1
''   Print Round$(23.149999, -2) ' 23.15
''   Print Round$(23.149999, -3) ' 23.150
''   Print Round$(23.149999, -4) ' 23.1500
''   Print
''   Print Round$(23.143335, -1) ' 23.1 OK?
''   Print Round$(23.143335, -2) ' 23.14
''   Print Round$(23.143335, -3) ' 23.143
''   Print Round$(23.143335, -4) ' 23.1433
''   Print Round$(23.143335, -5) ' 23.14334
''   Print
''   Dim float31 As _Float
''   float31 = .310000000000009
''   Print Round$(.31, -2) ' .31
''   Print Round$(.31##, -2)
''   Print Round$(float31, -2)
''End Sub ' RoundTest

' /////////////////////////////////////////////////////////////////////////////
' Rounding functions.

' FROM:
' https://www.qb64.org/forum/index.php?topic=3605.0

' Quote from: SMcNeill on Today at 03:53:48 PM
'
' Sometimes, you guys make things entirely too  complicated.
' There ya go!  Three functions to either round naturally,
' always round down, or always round up, to whatever number of digits you desire.
' EDIT:  Modified to add another option to round scientific,
' since you had it's description included in your example.

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE _FLOAT

' Receives + returns _FLOAT myVar## (-1.18E-4932 to +1.18E+4932)

Function RoundNatural## (num##, digits%)
    RoundNatural## = Int(num## * 10 ^ digits% + .5) / 10 ^ digits%
End Function

Function RoundUp## (num##, digits%)
    RoundUp## = _Ceil(num## * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDown## (num##, digits%)
    RoundDown## = Int(num## * 10 ^ digits%) / 10 ^ digits%
End Function

' old name: Round_Scientific##
Function RoundScientific## (num##, digits%)
    RoundScientific## = _Round(num## * 10 ^ digits%) / 10 ^ digits%
End Function

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE DOUBLE

Function RoundDouble# (num#, digits%)
    RoundDouble# = Int(num# * 10 ^ digits% + .5) / 10 ^ digits%
End Function

Function RoundUpDouble# (num#, digits%)
    RoundUpDouble# = _Ceil(num# * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDownDouble# (num#, digits%)
    RoundDownDouble# = Int(num# * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundScientificDouble# (num#, digits%)
    RoundScientificDouble# = _Round(num# * 10 ^ digits%) / 10 ^ digits%
End Function

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE SINGLE

Function RoundSingle! (num!, digits%)
    RoundSingle! = Int(num! * 10 ^ digits% + .5) / 10 ^ digits%
End Function

' NOTE: not sure this one works: when digits%=3, it rounds .31 to .32
Function RoundUpSingle! (num!, digits%)
    RoundUpSingle! = _Ceil(num! * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDownSingle! (num!, digits%)
    RoundDownSingle! = Int(num! * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundScientificSingle! (num!, digits%)
    RoundScientificSingle! = _Round(num! * 10 ^ digits%) / 10 ^ digits%
End Function

' /////////////////////////////////////////////////////////////////////////////
' fantastically inefficient way to set a bit

' example use: arrMaze(iX, iY) = SetBit256%(arrMaze(iX, iY), cS, FALSE)

' See also: GetBit256%, SetBit256%

' newint=SetBit256%(oldint, int containing the bits we want to set, value to set them to)
Function SetBit256% (iNum1 As Integer, iBit1 As Integer, bVal1 As Integer)
    Dim sNum As String
    Dim sBit As String
    Dim sVal As String
    Dim iLoop As Integer
    Dim strResult As String
    Dim iResult As Integer
    Dim iNum As Integer: iNum = iNum1
    Dim iBit As Integer: iBit = iBit1
    Dim bVal As Integer: bVal = bVal1

    If iNum < 256 And iBit <= 128 Then
        sNum = GetBinary$(iNum)
        sBit = GetBinary$(iBit)
        If bVal = TRUE Then
            sVal = "1"
        Else
            sVal = "0"
        End If
        strResult = ""
        For iLoop = 1 To 8
            If Mid$(sBit, iLoop, 1) = "1" Then
                strResult = strResult + sVal
            Else
                strResult = strResult + Mid$(sNum, iLoop, 1)
            End If
        Next iLoop
        iResult = GetIntegerFromBinary%(strResult)
    Else
        iResult = iNum
    End If

    SetBit256% = iResult
End Function ' SetBit256%

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

$If  Then
    Sub ShowDegreesAndRadians
    Dim iDegree As Integer
    Dim sngRadian As Single

    DebugPrint "Degree   Radian"
    DebugPrint "------   ------"
    For iDegree = 0 To 360
    sngRadian = _D2R(iDegree)

    'DebugPrint LeftPadString$(cstr$(iDegree), 3, " ") + "   " + LeftPadString$(cstr$(iRadian), 3, " ")

    DebugPrint LeftPadString$(cstr$(iDegree), 3, " ") + "   " + SngToStr$(sngRadian)

    'Print "SngToStr$(MyValue)           =" + SngToStr$(MyValue)
    'Print "SngRoundedToStr$(MyValue, 12)=" + SngRoundedToStr$(MyValue, 12)

    Next iDegree
    End Sub ' ShowDegreesAndRadians
$End If

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function SinD (degrees)
    SinD = Sin(_D2R(degrees))
End Function ' SinD

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

Function SmallestOf3% (i1%, i2%, i3%)
    Dim iMin%
    iMin% = i1%
    If i2% < iMin% Then iMin% = i2%
    If i3% < iMin% Then iMin% = i3%
    SmallestOf3% = iMin%
End Function ' SmallestOf3

' /////////////////////////////////////////////////////////////////////////////
' Receives a Single, rounds it to intNumPlaces places,
' and returns the result as a string.

Function SngRoundedToStr$ (sngValue As Single, intNumPlaces As Integer)
    Dim sngNew As Single
    sngNew = RoundSingle!(sngValue, intNumPlaces)
    SngRoundedToStr$ = SngToStr$(sngNew)
End Function ' SngRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Receives a Single, rounds it to 0 places,
' and returns the result as an Integer.

' NOTE: Hack function, to be replaced with something better?

Function SngToInt% (sngOld As Single)
    Dim sngNew As Single
    Dim sValue As String
    Dim iPos As Integer
    sngNew = RoundSingle!(sngOld, 0)
    sValue = SngToStr$(sngNew)
    SngToInt% = Val(sValue)
End Function ' SngToInt%

' /////////////////////////////////////////////////////////////////////////////
' Converts a Single to a string, formatted without scientific notation.

' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example usage:
' A string function that displays extremely small or large exponential
' decimal values.

Function SngToStr$ (n!)
    Dim result$: result$ = ""
    Dim value$
    Dim Xpos%
    Dim expo%
    Dim sign$
    Dim valu$
    Dim dot%
    Dim L%
    Dim add$
    Dim min$
    Dim DP$
    Dim n%
    Dim num$: num$ = ""

    value$ = UCase$(LTrim$(Str$(n!)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") ' only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n% = 1 To L%
            If Mid$(valu$, n%, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n%, 1)
            End If
        Next n%
        result$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
    Else
        result$ = value$
    End If

    SngToStr$ = result$
End Function ' SngToStr$

' /////////////////////////////////////////////////////////////////////////////
' Splits a string in$ by delimeter delimiter$
' into an array result$().

' 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, iDelimLen) = delimiter$
            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

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

$If  Then
    Sub SplitTest
    Dim in$
    Dim delim$
    ReDim arrTest$(0)
    Dim iLoop%

    delim$ = Chr$(10)
    in$ = "this" + delim$ + "is" + delim$ + "a" + delim$ + "test"
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print "delim$ = " + Chr$(34) + delim$ + Chr$(34)
    split in$, delim$, arrTest$()

    For iLoop% = LBound(arrTest$) To UBound(arrTest$)
    Print "arrTest$(" + LTrim$(RTrim$(Str$(iLoop%))) + ") = " + Chr$(34) + arrTest$(iLoop%) + Chr$(34)
    Next iLoop%

    Print
    Print "Split test finished."
    End Sub ' SplitTest
$End If

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

$If  Then
    Sub SplitAndReplaceTest
    Dim in$
    Dim out$
    Dim iLoop%
    ReDim arrTest$(0)

    Print "-------------------------------------------------------------------------------"
    Print "SplitAndReplaceTest"
    Print

    Print "Original value"
    in$ = "This line 1 " + Chr$(13) + Chr$(10) + "and line 2" + Chr$(10) + "and line 3 " + Chr$(13) + "finally THE END."
    out$ = in$
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
    Print

    Print "Fixing linebreaks..."
    in$ = Replace$(in$, Chr$(13) + Chr$(10), Chr$(13))
    in$ = Replace$(in$, Chr$(10), Chr$(13))
    out$ = in$
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
    Print

    Print "Splitting up..."
    split in$, Chr$(13), arrTest$()

    For iLoop% = LBound(arrTest$) To UBound(arrTest$)
    out$ = arrTest$(iLoop%)
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "arrTest$(" + _Trim$(Str$(iLoop%)) + ") = " + Chr$(34) + out$ + Chr$(34)
    Next iLoop%
    Print

    Print "SplitAndReplaceTest finished."
    End Sub ' SplitAndReplaceTest
$End If

' /////////////////////////////////////////////////////////////////////////////
' Converts a chr$(13) delimited string
' into a 2-dimensional array.

' Usage:
' Dim StringArray(1 To 48, 1 To 128) As String
' StringTo2dArray StringArray(), GetMap$

' Version 2 with indexed array(row, columm)
' Renamed StringToArray to StringTo2dArray.

' See also: Array2dToString$

Sub StringTo2dArray (MyArray() As String, MyString As String)
    Dim sDelim As String
    ReDim arrLines(0) As String
    Dim iRow As Integer
    Dim iCol As Integer
    Dim sChar As String
    Dim iDim1 As Integer
    Dim iDim2 As Integer
    Dim iIndex1 As Integer
    Dim iIndex2 As Integer

    iDim1 = LBound(MyArray, 1)
    iDim2 = LBound(MyArray, 2)
    sDelim = Chr$(13)
    split MyString, sDelim, arrLines()
    For iRow = LBound(arrLines) To UBound(arrLines)
        If iRow <= UBound(MyArray, 1) Then
            For iCol = 1 To Len(arrLines(iRow))
                If iCol <= UBound(MyArray, 2) Then
                    sChar = Mid$(arrLines(iRow), iCol, 1)

                    If Len(sChar) > 1 Then
                        sChar = Left$(sChar, 1)
                    Else
                        If Len(sChar) = 0 Then
                            sChar = "."
                        End If
                    End If

                    iIndex1 = iRow + iDim1
                    iIndex2 = (iCol - 1) + iDim2
                    MyArray(iIndex1, iIndex2) = sChar
                    'DebugPrint "MyArray(" + cstr$(iIndex1) + ", " + cstr$(iIndex2) + " = " + chr$(34) + sChar + chr$(34)
                Else
                    ' Exit if out of bounds
                    Exit For
                End If
            Next iCol
        Else
            ' Exit if out of bounds
            Exit For
        End If
    Next iRow
End Sub ' StringTo2dArray

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

Function StrPadLeft$ (sValue As String, iWidth As Integer)
    StrPadLeft$ = Right$(String$(iWidth, " ") + sValue, iWidth)
End Function ' StrPadLeft$

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

Function StrJustifyRight$ (sValue As String, iWidth As Integer)
    StrJustifyRight$ = Right$(String$(iWidth, " ") + sValue, iWidth)
End Function ' StrJustifyRight$

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

Function StrPadRight$ (sValue As String, iWidth As Integer)
    StrPadRight$ = Left$(sValue + String$(iWidth, " "), iWidth)
End Function ' StrPadRight$

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

Function StrJustifyLeft$ (sValue As String, iWidth As Integer)
    StrJustifyLeft$ = Left$(sValue + String$(iWidth, " "), iWidth)
End Function ' StrJustifyLeft$

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

Function StrJustifyCenter$ (sValue As String, iWidth As Integer)
    Dim iLen0 As Integer
    Dim iLen1 As Integer
    Dim iLen2 As Integer
    Dim iExtra As Integer

    iLen0 = Len(sValue)
    If iWidth = iLen0 Then
        ' no extra space: return unchanged
        StrJustifyCenter$ = sValue
    ElseIf iWidth > iLen0 Then
        If IsOdd%(iWidth) Then
            iWidth = iWidth - 1
        End If

        ' center
        iExtra = iWidth - iLen0
        iLen1 = iExtra \ 2
        iLen2 = iLen1 + (iExtra Mod 2)
        StrJustifyCenter$ = String$(iLen1, " ") + sValue + String$(iLen2, " ")
    Else
        ' string is too long: truncate
        StrJustifyCenter$ = Left$(sValue, iWidth)
    End If
End Function ' StrJustifyCenter$

' /////////////////////////////////////////////////////////////////////////////
' Use to pretty print TRUE and FALSE values.

Function TrueFalse$ (myValue)
    If myValue = TRUE Then
        TrueFalse$ = "TRUE"
    Else
        TrueFalse$ = "FALSE"
    End If
End Function ' TrueFalse$

' /////////////////////////////////////////////////////////////////////////////
' ABS was returning strange values with type _UNSIGNED LONG
' so I created this which does not.

' See also: LongABS

Function UnsignedLongABS~& (ulValue As _Unsigned Long)
    If Sgn(ulValue) = -1 Then
        UnsignedLongABS~& = 0 - ulValue
    Else
        UnsignedLongABS~& = ulValue
    End If
End Function ' UnsignedLongABS~&

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

Sub WaitForEnter
    Dim in$
    Input "Press <ENTER> to continue", in$
End Sub ' WaitForEnter

' /////////////////////////////////////////////////////////////////////////////
' WaitForKey "Press <ESC> to continue", 27, 0
' WaitForKey "Press <ENTER> to begin;", 13, 0
' waitforkey "", 65, 5

Sub WaitForKey (prompt$, KeyCode&, DelaySeconds%)
    ' SHOW PROMPT (IF SPECIFIED)
    If Len(prompt$) > 0 Then
        If Right$(prompt$, 1) <> ";" Then
            Print prompt$
        Else
            Print Right$(prompt$, Len(prompt$) - 1);
        End If
    End If

    ' WAIT FOR KEY
    Do: Loop Until _KeyDown(KeyCode&) ' leave loop when specified key pressed

    ' PAUSE AFTER (IF SPECIFIED)
    If DelaySeconds% < 1 Then
        _KeyClear: '_DELAY 1
    Else
        _KeyClear: _Delay DelaySeconds%
    End If
End Sub ' WaitForKey

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GENERAL PURPOSE ROUTINES #GENERAL
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

Sub DebugLog (sText As String)
    If cDebugEnabled = TRUE Then
        Dim sTime As String
        Dim sResult As String
        ReDim arrLines(0) As String
        Dim iLoop As Integer
        Dim sNextLine As String

        If _FileExists(m_sDebugFile) = FALSE Then
            sResult = PrintFile$(m_sDebugFile, "", FALSE)
        End If

        If Len(sResult) = 0 Then
            sTime = GetCurrentDateTime$("{mm}/{dd}/{yyyy} {hh}:{nn}:{ss} {ampm}")
            split sText, Chr$(13), arrLines()
            For iLoop = LBound(arrLines) To UBound(arrLines)
                sNextLine = sTime + " " + arrLines(iLoop)
                sResult = PrintFile$(m_sDebugFile, sNextLine, TRUE)
            Next iLoop
        End If
    End If
End Sub ' DebugLog

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

Sub DebugLog1 (sText As String)
    If cDebugEnabled = TRUE Then
        Dim sResult As String
        If _FileExists(m_sDebugFile) Then
            sResult = PrintFile$(m_sDebugFile, sText, TRUE)
        Else
            sResult = PrintFile$(m_sDebugFile, sText, FALSE)
        End If
    End If
End Sub ' DebugLog

' /////////////////////////////////////////////////////////////////////////////
' 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

' /////////////////////////////////////////////////////////////////////////////
' Writes sText to a debug file in the EXE folder.
' Debug file is named the same thing as the program EXE name with ".txt" at the end.
' For example the program "C:\QB64\MyProgram.BAS" running as
' "C:\QB64\MyProgram.EXE" would have an output file "C:\QB64\MyProgram.EXE.txt".
' If the file doesn't exist, it is created, otherwise it is appended to.

Sub PrintDebugFile (sText As String)
    Dim sFileName As String
    Dim sError As String
    Dim sOut As String

    sFileName = m_ProgramPath$ + m_ProgramName$ + ".txt"
    sError = ""
    If _FileExists(sFileName) = FALSE Then
        sOut = ""
        sOut = sOut + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + Chr$(13) + Chr$(10)
        sOut = sOut + "PROGRAM : " + m_ProgramName$ + Chr$(13) + Chr$(10)
        sOut = sOut + "RUN DATE: " + CurrentDateTime$ + Chr$(13) + Chr$(10)
        sOut = sOut + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + Chr$(13) + Chr$(10)
        sError = PrintFile$(sFileName, sOut, FALSE)
    End If
    If Len(sError) = 0 Then
        sError = PrintFile$(sFileName, sText, TRUE)
    End If
    If Len(sError) <> 0 Then
        Print CurrentDateTime$ + " PrintDebugFile FAILED: " + sError
    End If
End Sub ' PrintDebugFile

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' 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

"PongSub33.bas":
Code: (Select All)
' ################################################################################################################################################################
' MULTI-MOUSE PONG sub-program "PongSub"
' ################################################################################################################################################################

' MULTI-MOUSE + KEYBOARD READER FOR MULTI MOUSE PONG v0.33 by Softintheheadware

' -------------------------------------------------------------------------------
' ABOUT
' -------------------------------------------------------------------------------
' Subprogram for "PONGMAIN.BAS"

' *****************************************************************************
' NOTES:
'
' The following header files must be in same folder as this program:
' "makeint.h"
' "winproc.h"
' *****************************************************************************

' -------------------------------------------------------------------------------
' TO DO
' -------------------------------------------------------------------------------
' Some issues and things to fix:
'
' * Hide the real mouse cursor and reactivate it when program closes.
'
' * Detect moving the scroll wheel.
'
' * Get it working with _FullScreen _SquarePixels.
'   When I tried that mode things got really weird.
'
' * Read input from multiple keyboards - need to figure out how to do the
'   unions in the RAWINPUT structure type in "Pongsub".
'   (Just need someone to figure this out, I have tried.)
'
' * Figure out how to get the same functionality for Mac & Linux.

' -------------------------------------------------------------------------------
' THANK YOU
' -------------------------------------------------------------------------------
' For the multi-mouse function, 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   Now we get mice coordinates from subprogram via TCPIP.
'                        A lot faster than using files!
'                        Thanks to DSMan195276 for the networking code.
' 2024-06-19  madscijr   Now we get absolute mouse coordinates not just dx, dy
'                        and can read the keyboard (just a single keyboard).
'                        Updated the graphics / display code to use layers.

' -------------------------------------------------------------------------------
' RAW INPUT 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 "PongSub"
$NoPrefix
'$Console:Only
'Console Off

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

' FORM ALPHA VALUES
Const cInvisible = 1 ' for some reason a value of 0 (fully invisible) doesn't let the window get the focus, so we use 1
Const cTransparent = 160
Const cVisible = 255

' MIN/MAX VALUES FOR MOUSE TEST
Const cMinX = 1
Const cMaxX = 354 ' 160 ' 79 ' 80
Const cMinY = 1 ' 16
Const cMaxY = 45 ' 30 24
Const cMinWheel = 0
Const cMaxWheel = 255
Const cMinPX = 1
Const cMaxPX = 1024
Const cMinPY = 1
Const cMaxPY = 768

' CONSTANT FOR 2ND DIMENSION OF arrFile ARRAY
Const cFileName = 0
Const cFileData = 1
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CONSTANTS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ================================================================================================================================================================
' BEGIN API CONSTANTS
' ================================================================================================================================================================

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' 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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR RAW INPUT API
Const COLOR_WINDOW = 5
Const CS_HREDRAW = &H0002
Const CS_VREDRAW = &H0001
Const CW_USEDEFAULT = &H80000000
Const DT_CENTER = &H00000001
Const DT_LEFT = &H00000000
Const DT_RIGHT = &H00000002
Const DT_VCENTER = &H00000004
Const DT_WORDBREAK = &H00000010
Const DT_SINGLELINE = &H00000020

Const Edit = 101
Const EM_GETSEL = &H00B0
Const EM_SETSEL = &H00B1
Const EN_CHANGE = &H0300
Const EN_KILLFOCUS = &H0200
Const EN_SETFOCUS = &H0100
Const GCL_HICON = -14
Const GCL_HICONSM = -34
Const Hid_Bottom = 66
Const Hid_Left = 33
Const Hid_Right = 34
Const HWND_DESKTOP = 0
Const ICON_BIG = 1
Const ICON_SMALL = 0
Const IDC_ARROW = 32512
Const IDI_APPLICATION = 32512
Const KEYEVENTF_KEYUP = &H0002
Const KL_NAMELENGTH = 9
Const LabelInfo = 201
Const MOUSE_ATTRIBUTES_CHANGED = &H04
Const MOUSE_MOVE_ABSOLUTE = &H01
Const MOUSE_MOVE_NOCOALESCE = &H08
Const MOUSE_MOVE_RELATIVE = &H00
Const MOUSE_VIRTUAL_DESKTOP = &H02
Const NULL = 0
Const RI_KEY_BREAK = 1
Const RI_KEY_E0 = 2
Const RI_KEY_E1 = 4
Const RI_KEY_MAKE = 0
Const RI_KEY_TERMSRV_SET_LED = 8
Const RI_KEY_TERMSRV_SHADOW = &H10
Const RID_INPUT = &H10000003
Const RIDEV_EXINPUTSINK = &H00001000
Const RIDI_DEVICEINFO = &H2000000B

Const RIM_TYPEMOUSE = 0
Const RIM_TYPEKEYBOARD = 1
Const RIM_TYPEHID = 2
Const RIM_TYPEUNKNOWN = -1 ' just a made up value to indicate type unknown

Const SIZE_MINIMIZED = 1
Const SW_SHOW = 5

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' BEGIN Virtual-Key Codes
' https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' NOTE: raw.data.Keyboard.vKey may require set extended bit
Const VK_LBUTTON = &H01 ' dec = 1, Left mouse button
Const VK_RBUTTON = &H02 ' dec = 2, Right mouse button
Const VK_CANCEL = &H03 ' dec = 3, Control-break processing
Const VK_MBUTTON = &H04 ' dec = 4, Middle mouse button
Const VK_XBUTTON1 = &H05 ' dec = 5, X1 mouse button
Const VK_XBUTTON2 = &H06 ' dec = 6, X2 mouse button
'??? = &H07 ' dec = 7, Reserved
Const VK_BACK = &H08 ' dec = 8, BACKSPACE key
Const VK_TAB = &H09 ' dec = 9, TAB key
'??? = &H0A-0B ' dec = 10-11, Reserved
Const VK_CLEAR = &H0C ' dec = 12, CLEAR key
Const VK_RETURN = &H0D ' dec = 13, ENTER key
'??? = &H0E-0F ' dec = 14-15, Unassigned
Const VK_SHIFT = &H10 ' dec = 16, SHIFT key
Const VK_CONTROL = &H11 ' dec = 17, CTRL key
Const VK_MENU = &H12 ' dec = 18, ALT key
Const VK_PAUSE = &H13 ' dec = 19, PAUSE key
Const VK_CAPITAL = &H14 ' dec = 20, CAPS LOCK key
Const VK_KANA = &H15 ' dec = 21, IME Kana mode
Const VK_HANGUL = &H15 ' dec = 21, IME Hangul mode
Const VK_IME_ON = &H16 ' dec = 22, IME On
Const VK_JUNJA = &H17 ' dec = 23, IME Junja mode
Const VK_FINAL = &H18 ' dec = 24, IME final mode
Const VK_HANJA = &H19 ' dec = 25, IME Hanja mode
Const VK_KANJI = &H19 ' dec = 25, IME Kanji mode
Const VK_IME_OFF = &H1A ' dec = 26, IME Off
Const VK_ESCAPE = &H1B ' dec = 27, ESC key
Const VK_CONVERT = &H1C ' dec = 28, IME convert
Const VK_NONCONVERT = &H1D ' dec = 29, IME nonconvert
Const VK_ACCEPT = &H1E ' dec = 30, IME accept
Const VK_MODECHANGE = &H1F ' dec = 31, IME mode change request
Const VK_SPACE = &H20 ' dec = 32, SPACEBAR
Const VK_PRIOR = &H21 ' dec = 33, PAGE UP key
Const VK_NEXT = &H22 ' dec = 34, PAGE DOWN key
Const VK_END = &H23 ' dec = 35, END key
Const VK_HOME = &H24 ' dec = 36, HOME key
Const VK_LEFT = &H25 ' dec = 37, LEFT ARROW key
Const VK_UP = &H26 ' dec = 38, UP ARROW key
Const VK_RIGHT = &H27 ' dec = 39, RIGHT ARROW key
Const VK_DOWN = &H28 ' dec = 40, DOWN ARROW key
Const VK_SELECT = &H29 ' dec = 41, SELECT key
Const VK_PRINT = &H2A ' dec = 42, PRINT key
Const VK_EXECUTE = &H2B ' dec = 43, EXECUTE key
Const VK_SNAPSHOT = &H2C ' dec = 44, PRINT SCREEN key
Const VK_INSERT = &H2D ' dec = 45, INS key
Const VK_DELETE = &H2E ' dec = 46, DEL key
Const VK_HELP = &H2F ' dec = 47, HELP key

' MADE OUR OWN CONSTANTS FOR THESE:
Const VK_0 = &H30 ' dec = 48, 0 key
Const VK_1 = &H31 ' dec = 49, 1 key
Const VK_2 = &H32 ' dec = 50, 2 key
Const VK_3 = &H33 ' dec = 51, 3 key
Const VK_4 = &H34 ' dec = 52, 4 key
Const VK_5 = &H35 ' dec = 53, 5 key
Const VK_6 = &H36 ' dec = 54, 6 key
Const VK_7 = &H37 ' dec = 55, 7 key
Const VK_8 = &H38 ' dec = 56, 8 key
Const VK_9 = &H39 ' dec = 57, 9 key
'??? = &H3A-40 ' dec = 58-64, Undefined
Const VK_A = &H41 ' dec = 65, A key
Const VK_B = &H42 ' dec = 66, B key
Const VK_C = &H43 ' dec = 67, C key
Const VK_D = &H44 ' dec = 68, D key
Const VK_E = &H45 ' dec = 69, E key
Const VK_F = &H46 ' dec = 70, F key
Const VK_G = &H47 ' dec = 71, G key
Const VK_H = &H48 ' dec = 72, H key
Const VK_I = &H49 ' dec = 73, I key
Const VK_J = &H4A ' dec = 74, J key
Const VK_K = &H4B ' dec = 75, K key
Const VK_L = &H4C ' dec = 76, L key
Const VK_M = &H4D ' dec = 77, M key
Const VK_N = &H4E ' dec = 78, N key
Const VK_O = &H4F ' dec = 79, O key
Const VK_P = &H50 ' dec = 80, P key
Const VK_Q = &H51 ' dec = 81, Q key
Const VK_R = &H52 ' dec = 82, R key
Const VK_S = &H53 ' dec = 83, S key
Const VK_T = &H54 ' dec = 84, T key
Const VK_U = &H55 ' dec = 85, U key
Const VK_V = &H56 ' dec = 86, V key
Const VK_W = &H57 ' dec = 87, W key
Const VK_X = &H58 ' dec = 88, X key
Const VK_Y = &H59 ' dec = 89, Y key
Const VK_Z = &H5A ' dec = 90, Z key

' Microsoft's Virtual-Key Codes constants (continued):
Const VK_LWIN = &H5B ' dec = 91, Left Windows key
Const VK_RWIN = &H5C ' dec = 92, Right Windows key
Const VK_APPS = &H5D ' dec = 93, Applications key
'??? = &H5E ' dec = 94, Reserved
Const VK_SLEEP = &H5F ' dec = 95, Computer Sleep key
Const VK_NUMPAD0 = &H60 ' dec = 96, Numeric keypad 0 key
Const VK_NUMPAD1 = &H61 ' dec = 97, Numeric keypad 1 key
Const VK_NUMPAD2 = &H62 ' dec = 98, Numeric keypad 2 key
Const VK_NUMPAD3 = &H63 ' dec = 99, Numeric keypad 3 key
Const VK_NUMPAD4 = &H64 ' dec = 100, Numeric keypad 4 key
Const VK_NUMPAD5 = &H65 ' dec = 101, Numeric keypad 5 key
Const VK_NUMPAD6 = &H66 ' dec = 102, Numeric keypad 6 key
Const VK_NUMPAD7 = &H67 ' dec = 103, Numeric keypad 7 key
Const VK_NUMPAD8 = &H68 ' dec = 104, Numeric keypad 8 key
Const VK_NUMPAD9 = &H69 ' dec = 105, Numeric keypad 9 key
Const VK_MULTIPLY = &H6A ' dec = 106, Multiply key
Const VK_ADD = &H6B ' dec = 107, Add key
Const VK_SEPARATOR = &H6C ' dec = 108, Separator key
Const VK_SUBTRACT = &H6D ' dec = 109, Subtract key
Const VK_DECIMAL = &H6E ' dec = 110, Decimal key
Const VK_DIVIDE = &H6F ' dec = 111, Divide key
Const VK_F1 = &H70 ' dec = 112, F1 key
Const VK_F2 = &H71 ' dec = 113, F2 key
Const VK_F3 = &H72 ' dec = 114, F3 key
Const VK_F4 = &H73 ' dec = 115, F4 key
Const VK_F5 = &H74 ' dec = 116, F5 key
Const VK_F6 = &H75 ' dec = 117, F6 key
Const VK_F7 = &H76 ' dec = 118, F7 key
Const VK_F8 = &H77 ' dec = 119, F8 key
Const VK_F9 = &H78 ' dec = 120, F9 key
Const VK_F10 = &H79 ' dec = 121, F10 key
Const VK_F11 = &H7A ' dec = 122, F11 key
Const VK_F12 = &H7B ' dec = 123, F12 key
Const VK_F13 = &H7C ' dec = 124, F13 key
Const VK_F14 = &H7D ' dec = 125, F14 key
Const VK_F15 = &H7E ' dec = 126, F15 key
Const VK_F16 = &H7F ' dec = 127, F16 key
Const VK_F17 = &H80 ' dec = 128, F17 key
Const VK_F18 = &H81 ' dec = 129, F18 key
Const VK_F19 = &H82 ' dec = 130, F19 key
Const VK_F20 = &H83 ' dec = 131, F20 key
Const VK_F21 = &H84 ' dec = 132, F21 key
Const VK_F22 = &H85 ' dec = 133, F22 key
Const VK_F23 = &H86 ' dec = 134, F23 key
Const VK_F24 = &H87 ' dec = 135, F24 key
'??? = &H88-8F ' dec = 136-143, Reserved
Const VK_NUMLOCK = &H90 ' dec = 144, NUM LOCK key
Const VK_SCROLL = &H91 ' dec = 145, SCROLL LOCK key
'??? = &H92-96 ' dec = 146-150, OEM specific
'??? = &H97-9F ' dec = 151-159, Unassigned
Const VK_LSHIFT = &HA0 ' dec = 160, Left SHIFT key
Const VK_RSHIFT = &HA1 ' dec = 161, Right SHIFT key
Const VK_LCONTROL = &HA2 ' dec = 162, Left CONTROL key
Const VK_RCONTROL = &HA3 ' dec = 163, Right CONTROL key
Const VK_LMENU = &HA4 ' dec = 164, Left ALT key
Const VK_RMENU = &HA5 ' dec = 165, Right ALT key
Const VK_BROWSER_BACK = &HA6 ' dec = 166, Browser Back key
Const VK_BROWSER_FORWARD = &HA7 ' dec = 167, Browser Forward key
Const VK_BROWSER_REFRESH = &HA8 ' dec = 168, Browser Refresh key
Const VK_BROWSER_STOP = &HA9 ' dec = 169, Browser Stop key
Const VK_BROWSER_SEARCH = &HAA ' dec = 170, Browser Search key
Const VK_BROWSER_FAVORITES = &HAB ' dec = 171, Browser Favorites key
Const VK_BROWSER_HOME = &HAC ' dec = 172, Browser Start and Home key
Const VK_VOLUME_MUTE = &HAD ' dec = 173, Volume Mute key
Const VK_VOLUME_DOWN = &HAE ' dec = 174, Volume Down key
Const VK_VOLUME_UP = &HAF ' dec = 175, Volume Up key
Const VK_MEDIA_NEXT_TRACK = &HB0 ' dec = 176, Next Track key
Const VK_MEDIA_PREV_TRACK = &HB1 ' dec = 177, Previous Track key
Const VK_MEDIA_STOP = &HB2 ' dec = 178, Stop Media key
Const VK_MEDIA_PLAY_PAUSE = &HB3 ' dec = 179, Play/Pause Media key
Const VK_LAUNCH_MAIL = &HB4 ' dec = 180, Start Mail key
Const VK_LAUNCH_MEDIA_SELECT = &HB5 ' dec = 181, Select Media key
Const VK_LAUNCH_APP1 = &HB6 ' dec = 182, Start Application 1 key
Const VK_LAUNCH_APP2 = &HB7 ' dec = 183, Start Application 2 key
'??? = &HB8-B9 ' dec = 184-137, Reserved
Const VK_OEM_1 = &HBA ' dec = 186, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ;: key
Const VK_OEM_PLUS = &HBB ' dec = 187, For any country/region, the + key
Const VK_OEM_COMMA = &HBC ' dec = 188, For any country/region, the , key
Const VK_OEM_MINUS = &HBD ' dec = 189, For any country/region, the - key
Const VK_OEM_PERIOD = &HBE ' dec = 190, For any country/region, the . key
Const VK_OEM_2 = &HBF ' dec = 191, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the /? key
Const VK_OEM_3 = &HC0 ' dec = 192, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the `~ key
'??? = &HC1-DA ' dec = 193-218, Reserved
Const VK_OEM_4 = &HDB ' dec = 219, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the [{ key
Const VK_OEM_5 = &HDC ' dec = 220, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the \\| key
Const VK_OEM_6 = &HDD ' dec = 221, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ]} key
Const VK_OEM_7 = &HDE ' dec = 222, Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '" key
Const VK_OEM_8 = &HDF ' dec = 223, Used for miscellaneous characters; it can vary by keyboard.
'??? = &HE0 ' dec = 224, Reserved
'??? = &HE1 ' dec = 225, OEM specific
Const VK_OEM_102 = &HE2 ' dec = 226, The <> keys on the US standard keyboard, or the \\| key on the non-US 102-key keyboard
'??? = &HE3-E4 ' dec = 227-228, OEM specific
Const VK_PROCESSKEY = &HE5 ' dec = 229, IME PROCESS key
'??? = &HE6 ' dec = 230, OEM specific
Const VK_PACKET = &HE7 ' dec = 231, Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
'??? = &HE8 ' dec = 232, Unassigned
'??? = &HE9-F5 ' dec = 233-245, OEM specific
Const VK_ATTN = &HF6 ' dec = 246, Attn key
Const VK_CRSEL = &HF7 ' dec = 247, CrSel key
Const VK_EXSEL = &HF8 ' dec = 248, ExSel key
Const VK_EREOF = &HF9 ' dec = 249, Erase EOF key
Const VK_PLAY = &HFA ' dec = 250, Play key
Const VK_ZOOM = &HFB ' dec = 251, Zoom key
Const VK_NONAME = &HFC ' dec = 252, Reserved
Const VK_PA1 = &HFD ' dec = 253, PA1 key
Const VK_OEM_CLEAR = &HFE ' dec = 254, Clear key
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' END Virtual-Key Codes
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------

' Messages a window receives through or sends from its WindowProc function:
' DefWindowProcA function (winuser.h)
' https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowproca
Const WM_APP = &H08000 ' dec=32768
Const WM_APPCOMMAND = &H0319 ' dec=793
Const WM_CHAR = &H0102 ' dec=258
Const WM_COMMAND = &H0111 ' dec=273
Const WM_DEADCHAR = &H0103 ' dec=259
Const WM_DESTROY = &H0002 ' dec=2
Const WM_INITDIALOG = &H0110 ' dec=272
Const WM_INPUT = &H00FF ' dec=255
Const WM_KEYDOWN = &H0100 ' dec=256
Const WM_KEYUP = &H0101 ' dec=257
Const WM_MOUSEMOVE = &H0200 ' dec=512
Const WM_NCACTIVATE = &H0086 ' dec=134
Const WM_NEXTDLGCTL = &H28 ' dec=40
Const WM_PAINT = &H000F ' dec=15
Const WM_SETICON = &H0080 ' dec=128
Const WM_SIZE = &H0005 ' dec=5
Const WM_SYSCHAR = &H0106 ' dec=262
Const WM_SYSDEADCHAR = &H0107 ' dec=263
Const WM_SYSKEYDOWN = &H0104 ' dec=260
Const WM_SYSKEYUP = &H0105 ' dec=261
Const WM_UNICHAR = &H0109 ' dec=265

' CONSTANTS USED FOR WINDOWS STYLES & FEATURES, SEE:
' Window Styles
' https://learn.microsoft.com/en-us/windows/win32/winmsg/window-styles
' Window Features
' https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features
Const WS_CAPTION = &H00C00000 ' dec=12582912
Const WS_CHILD = &H40000000 ' dec=1073741824
Const WS_MAXIMIZEBOX = &H00010000 ' dec=65536
Const WS_MINIMIZEBOX = &H00020000 ' dec=131072
Const WS_OVERLAPPED = &H00000000 ' dec=0
Const WS_SYSMENU = &H00080000 ' dec=524288
Const WS_THICKFRAME = &H00040000 ' dec=262144
Const WS_VISIBLE = &H10000000 ' dec=268435456
Const WS_OVERLAPPEDWINDOW = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_THICKFRAME Or WS_MINIMIZEBOX Or WS_MAXIMIZEBOX

' CONSTANTS USED BY MapVirtualKey FOR PARAMETER uMapType
' https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-mapvirtualkeya
Const MAPVK_VK_TO_VSC = 0 ' The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If there is no translation, the function returns 0.
Const MAPVK_VSC_TO_VK = 1 ' The uCode parameter is a scan code and is translated into a virtual-key code that does not distinguish between left- and right-hand keys. If there is no translation, the function returns 0. Windows Vista and later: the high byte of the uCode value can contain either 0xe0 or 0xe1 to specify the extended scan code.
Const MAPVK_VK_TO_CHAR = 2 ' The uCode parameter is a virtual-key code and is translated into an unshifted character value in the low order word of the return value. Dead keys (diacritics) are indicated by setting the top bit of the return value. If there is no translation, the function returns 0. See Remarks.
Const MAPVK_VSC_TO_VK_EX = 3 ' The uCode parameter is a scan code and is translated into a virtual-key code that distinguishes between left- and right-hand keys. If there is no translation, the function returns 0. Windows Vista and later: the high byte of the uCode value can contain either 0xe0 or 0xe1 to specify the extended scan code.
Const MAPVK_VK_TO_VSC_EX = 4 ' Windows Vista and later: The uCode parameter is a virtual-key code and is translated into a scan code. If it is a virtual-key code that does not distinguish between left- and right-hand keys, the left-hand scan code is returned. If the scan code is an extended scan code, the high byte of the returned value will contain either 0xe0 or 0xe1 to specify the extended scan code. If there is no translation, the function returns 0.

' ================================================================================================================================================================
' END API CONSTANTS
' ================================================================================================================================================================

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN RAW INPUT TYPES
' FOR TYPE CONVERSION SEE: "QB64PE C Libraries" at:
' https://qb64phoenix.com/qb64wiki/index.php/C_Libraries
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWINPUTDEVICE structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputdevice
'typedef struct tagRAWINPUTDEVICE {
'  USHORT usUsagePage;
'  USHORT usUsage;
'  DWORD  dwFlags;
'  HWND   hwndTarget;
'} RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;

' Spriggsy's version:
Type RAWINPUTDEVICE
    As Unsigned Integer usUsagePage, usUsage
    As Unsigned Long dwFlags
    As Offset hwndTarget
End Type

' ^^^ Should "Unsigned Integer" be "_UNSIGNED INTEGER"
'     and    "Unsigned Long" be "_UNSIGNED LONG"
'     and    "Offset" be "_OFFSET" like this?:
'
'TYPE RAWINPUTDEVICE
'    usUsagePage AS _UNSIGNED INTEGER ' WORD
'    usUsage     AS _UNSIGNED INTEGER ' WORD
'    dwFlags     AS _UNSIGNED LONG ' DWORD
'    hwndTarget  AS _OFFSET ' DWORD
'END TYPE

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWINPUTDEVICELIST structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputdevicelist
'typedef struct tagRAWINPUTDEVICELIST {
'  HANDLE hDevice;
'  DWORD  dwType;
'} RAWINPUTDEVICELIST, *PRAWINPUTDEVICELIST;

' Spriggsy's version:
Type RAWINPUTDEVICELIST
    As Offset hDevice
    As Unsigned Long dwType
    $If 64BIT Then
        As String * 4 alignment
    $End If
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'POINT structure (windef.h)
'https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-point
'typedef struct tagPOINT {
'  LONG x;
'  LONG y;
'} POINT, *PPOINT, *NPPOINT, *LPPOINT;

Type POINT
    As Long x, y
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'MSG structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
'typedef struct tagMSG {
'  HWND   hwnd;
'  UINT   message;
'  WPARAM wParam;
'  LPARAM lParam;
'  DWORD  time;
'  POINT  pt;
'  DWORD  lPrivate;
'} MSG, *PMSG, *NPMSG, *LPMSG;

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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'WNDCLASSEXA structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexa
'typedef struct WNDCLASSEXA {
'  UINT      cbSize;
'  UINT      style;
'  WNDPROC   lpfnWndProc;
'  int       cbClsExtra;
'  int       cbWndExtra;
'  HINSTANCE hInstance;
'  HICON     hIcon;
'  HCURSOR   hCursor;
'  HBRUSH    hbrBackground;
'  LPCSTR    lpszMenuName;
'  LPCSTR    lpszClassName;
'  HICON     hIconSm;
'} WNDCLASSEXA, *PWNDCLASSEXA, *NPWNDCLASSEXA, *LPWNDCLASSEXA;
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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RECT structure (windef.h)
'https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
'typedef struct tagRECT {
'  LONG left;     Specifies the x-coordinate of the upper-left corner of the rectangle.
'  LONG top;      Specifies the y-coordinate of the upper-left corner of the rectangle.
'  LONG right;    Specifies the x-coordinate of the lower-right corner of the rectangle.
'  LONG bottom;   Specifies the y-coordinate of the lower-right corner of the rectangle.
'} RECT, *PRECT, *NPRECT, *LPRECT;
Type RECT
    As Long left, top, right, bottom
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'PAINTSTRUCT structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-paintstruct
'typedef struct tagPAINTSTRUCT {
'  HDC  hdc;
'  BOOL fErase;
'  RECT rcPaint;
'  BOOL fRestore;
'  BOOL fIncUpdate;
'  BYTE rgbReserved[32];
'} PAINTSTRUCT, *PPAINTSTRUCT, *NPPAINTSTRUCT, *LPPAINTSTRUCT;
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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWINPUTHEADER structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinputheader
'typedef struct tagRAWINPUTHEADER {
'  DWORD  dwType;
'  DWORD  dwSize;
'  HANDLE hDevice;
'  WPARAM wParam;
'} RAWINPUTHEADER, *PRAWINPUTHEADER, *LPRAWINPUTHEADER;

' Spriggsy's version:
Type RAWINPUTHEADER
    As Unsigned Long dwType, dwSize
    As Offset hDevice
    As Unsigned Offset wParam
End Type

' ^^^ Doesn't match the types I expected, should it be these?:
'TYPE RAWINPUTHEADER
'    dwType  AS _UNSIGNED LONG ' DWORD
'    dwSize  AS _UNSIGNED LONG ' DWORD
'    hDevice AS _UNSIGNED LONG ' DWORD <- should this be _OFFSET ?
'    wParam  AS LONG
'END TYPE

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWMOUSE structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawmouse
'typedef struct tagRAWMOUSE {
'  USHORT usFlags;
'  union {
'    ULONG ulButtons;
'    struct {
'      USHORT usButtonFlags;
'      USHORT usButtonData;
'    } DUMMYSTRUCTNAME;
'  } DUMMYUNIONNAME2;
'  ULONG  ulRawButtons;
'  LONG   lLastX;
'  LONG   lLastY;
'  ULONG  ulExtraInformation;
'} RAWMOUSE, *PRAWMOUSE, *LPRAWMOUSE;

' Spriggsy's simplified version:
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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWKEYBOARD structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawkeyboard
'typedef struct tagRAWKEYBOARD {
'  USHORT MakeCode;
'  USHORT Flags;
'  USHORT Reserved;
'  USHORT VKey;
'  UINT   Message;
'  ULONG  ExtraInformation;
'} RAWKEYBOARD, *PRAWKEYBOARD, *LPRAWKEYBOARD;
Type RAWKEYBOARD
    MakeCode As _Unsigned Integer ' USHORT
    Flags As _Unsigned Integer ' USHORT
    Reserved As _Unsigned Integer ' USHORT
    VKey As _Unsigned Integer ' USHORT
    Message As _Unsigned Long ' UINT
    ExtraInformation As _Unsigned _Offset ' ULONG
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWINPUT structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawinput
'typedef struct tagRAWINPUT {
'  RAWINPUTHEADER header;
'  union {
'    RAWMOUSE    mouse;
'    RAWKEYBOARD keyboard;
'    RAWHID      hid;
'  } data;
'} RAWINPUT, *PRAWINPUT, *LPRAWINPUT;

' Spriggsy's simplified version:
Type RAWINPUT
    As RAWINPUTHEADER header
    As RAWMOUSE mouse
    'As RAWKEYBOARD keyboard <- ADDING THIS CAUSES THE PROGRAM TO CRASH ON MOUSE INPUT
End Type

' Simplified copy for keyboard:
Type RAWINPUT_K
    As RAWINPUTHEADER header
    As RAWKEYBOARD keyboard
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RID_DEVICE_INFO_MOUSE structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rid_device_info_mouse
'typedef struct tagRID_DEVICE_INFO_MOUSE {
'  DWORD dwId;
'  DWORD dwNumberOfButtons;
'  DWORD dwSampleRate;
'  BOOL  fHasHorizontalWheel;
'} RID_DEVICE_INFO_MOUSE, *PRID_DEVICE_INFO_MOUSE;
Type RID_DEVICE_INFO_MOUSE
    dwId As _Unsigned Long
    dwNumberOfButtons As _Unsigned Long
    dwSampleRate As _Unsigned Long
    fHasHorizontalWheel As Integer
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RID_DEVICE_INFO_KEYBOARD structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rid_device_info_keyboard
'typedef struct tagRID_DEVICE_INFO_KEYBOARD {
'  DWORD dwType;
'  DWORD dwSubType;
'  DWORD dwKeyboardMode;
'  DWORD dwNumberOfFunctionKeys;
'  DWORD dwNumberOfIndicators;
'  DWORD dwNumberOfKeysTotal;
'} RID_DEVICE_INFO_KEYBOARD, *PRID_DEVICE_INFO_KEYBOARD;
Type RID_DEVICE_INFO_KEYBOARD
    dwType As _Unsigned Long ' DWORD
    dwSubType As _Unsigned Long ' DWORD
    dwKeyboardMode As _Unsigned Long ' DWORD
    dwNumberOfFunctionKeys As _Unsigned Long ' DWORD
    dwNumberOfIndicators As _Unsigned Long ' DWORD
    dwNumberOfKeysTotal As _Unsigned Long ' DWORD
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RID_DEVICE_INFO_HID structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rid_device_info_hid
'typedef struct tagRID_DEVICE_INFO_HID {
'  DWORD  dwVendorId;
'  DWORD  dwProductId;
'  DWORD  dwVersionNumber;
'  USHORT usUsagePage;
'  USHORT usUsage;
'} RID_DEVICE_INFO_HID, *PRID_DEVICE_INFO_HID;
Type RID_DEVICE_INFO_HID
    dwVendorId As _Unsigned Long ' DWORD
    dwProductId As _Unsigned Long ' DWORD
    dwVersionNumber As _Unsigned Long ' DWORD
    usUsagePage As _Unsigned Integer ' USHORT
    usUsage As _Unsigned Integer ' USHORT
End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' NEEDS FIXING:

'RID_DEVICE_INFO structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rid_device_info
'typedef struct tagRID_DEVICE_INFO {
'  DWORD cbSize;
'  DWORD dwType;
'  union {
'    RID_DEVICE_INFO_MOUSE    mouse;
'    RID_DEVICE_INFO_KEYBOARD keyboard;
'    RID_DEVICE_INFO_HID      hid;
'  } DUMMYUNIONNAME1;
'} RID_DEVICE_INFO, *PRID_DEVICE_INFO, *LPRID_DEVICE_INFO;

' ^^^ NOT SURE HOW TO DEFINE THIS, SHOULD IT BE SOMETHING LIKE THIS?:

'Type DUMMYUNIONNAME1
'    My_RID_DEVICE_INFO_MOUSE As _Offset ' pointer to VAR A1
'    My_RID_DEVICE_INFO_KEYBOARD As _Offset ' pointer to VAR A2
'    My_RID_DEVICE_INFO_HID As _Offset ' pointer to VAR A3
'End Type
'Type RID_DEVICE_INFO
'    cbSize As _Unsigned Long ' DWORD
'    dwType As _Unsigned Long ' DWORD
'    My_DUMMYUNIONNAME1 As _Offset ' pointer to DUMMYUNIONNAME1
'End Type

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'RAWHID structure (winuser.h)
'https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawhid
'typedef struct tagRAWHID {
'  DWORD dwSizeHid;
'  DWORD dwCount;
'  BYTE  bRawData[1];
'} RAWHID, *PRAWHID, *LPRAWHID;
Type RAWHID
    dwSizeHid As _Unsigned Long ' DWORD
    dwCount As _Unsigned Long ' DWORD
    bRawData As _Unsigned _Byte ' bRawData[1] AS BYTE
End Type

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END RAW INPUT TYPES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN CUSTOM TYPES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' UDT TO HOLD THE INFO FOR EACH MOUSE (READ MICE SUB)
Type MouseInfoType
    ID As String ' player identifier or mouse device ID

    char As String ' cursor character

    dx As Integer ' mouse x movement -1=left, 1=right, 0=none
    dy As Integer ' mouse y movement -1=up  , 1=down , 0=none

    px As Long ' pointer x position (hires) for absolute position of mouse from raw input api
    py As Long ' pointer y position (hires) for absolute position of mouse from raw input api

    ' Multimouse:
    pdx As Long ' mouse x movement (hires) can be greater than just -1 or +1
    pdy As Long ' mouse y movement (hires) can be greater than just -1 or +1

    wheel As Integer ' mouse wheel value
    'wheelOld As Integer ' old mouse wheel value

    LeftDown As Integer ' tracks left mouse button state, TRUE=down
    'LeftDownOld As Integer ' old left mouse button state, TRUE=down

    MiddleDown As Integer ' tracks middle mouse button state, TRUE=down
    'MiddleDownOld As Integer ' old middle mouse button state, TRUE=down

    RightDown As Integer ' tracks right mouse button state, TRUE=down
    'RightDownOld As Integer ' old right mouse button state, TRUE=down
End Type ' MouseInfoType

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CUSTOM TYPES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN API DECLARATIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ================================================================================================================================================================
' BEGIN CustomType Library definitions
' ================================================================================================================================================================
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' FOR RAW INPUT API
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
Declare CustomType Library
    'DECLARE FUNCTION GetRawInputDeviceList LIB "USER32.DLL" ALIAS "GetRawInputDeviceList"( _
    '    BYREF pRawInputDeviceList AS RAWINPUTDEVICELIST, _
    '    BYREF puiNumDevices AS _UNSIGNED LONG, _
    '    BYVAL cbSize AS _UNSIGNED LONG _
    '    ) AS _UNSIGNED LONG
    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)

    'DECLARE FUNCTION RegisterRawInputDevices LIB "USER32.DLL" ALIAS "RegisterRawInputDevices"( _
    '    BYREF pRawInputDevices AS RAWINPUTDEVICE, _
    '    BYVAL uiNumDevices AS _UNSIGNED LONG, _
    '    BYVAL cbSize AS _UNSIGNED LONG _
    '    ) AS 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)

    'DECLARE FUNCTION GetRawInputData LIB "USER32.DLL" ALIAS "GetRawInputData"( _
    '    BYVAL hRawInput AS _UNSIGNED LONG, _
    '    BYVAL uiCommand AS _UNSIGNED LONG, _
    '    BYREF pData AS _OFFSET, _
    '    BYREF pcbSize AS _UNSIGNED LONG, _
    '    BYVAL cbSizeHeader AS _UNSIGNED LONG _
    '    ) AS _UNSIGNED LONG
    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)

    'DECLARE FUNCTION SendMessage LIB "USER32.DLL" ALIAS "SendMessageA"( _
    '    BYVAL hWnd AS _OFFSET, _
    '    BYVAL Msg AS _UNSIGNED LONG, _
    '    BYVAL wParam AS _UNSIGNED LONG, _
    '    BYVAL lParam AS LONG _
    '    ) 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)

    'Public Declare Function GetClientRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
    Sub GetClientRect (ByVal hWnd As Offset, Byval lpRect As Offset)

    'Public Declare Function DrawText Lib "user32" Alias "DrawTextA" (ByVal hdc As Long, ByVal lpStr As String, ByVal nCount As Long, lpRect As RECT, ByVal wFormat As Long) As Long
    Sub DrawText (ByVal hdc As Offset, Byval lpchText As Offset, Byval cchText As Long, Byval lprc As Offset, Byval format As Unsigned Long)

    'Public Declare Function OffsetRect Lib "user32" (lpRect As RECT, ByVal X As Long, ByVal Y As Long) As 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 ' CustomType Library

' 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

' ================================================================================================================================================================
' END CustomType Library definitions
' ================================================================================================================================================================

' ================================================================================================================================================================
' BEGIN Library definitions
' ================================================================================================================================================================
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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' Needed for acquiring the hWnd of the window
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
Declare Library
    Function FindWindow& (ByVal ClassName As _Offset, WindowName$) ' To get hWnd handle
End Declare

' ================================================================================================================================================================
' END Library definitions
' ================================================================================================================================================================

' ================================================================================================================================================================
' BEGIN Dynamic Library definitions
' ================================================================================================================================================================

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

    ' To make window invisible
    Function SetLayeredWindowAttributes& (ByVal hwnd As _Offset, Byval crKey As Long, Byval bAlpha As _Unsigned _Byte, Byval dwFlags As Long)
    Function GetWindowLong& Alias "GetWindowLongA" (ByVal hwnd As _Offset, Byval nIndex As Long)
    Function SetWindowLong& Alias "SetWindowLongA" (ByVal hwnd As _Offset, Byval nIndex As Long, Byval dwNewLong As Long)

    '' To hide the cursor
    'Function ShowCursor& (ByVal bShow As Long)

End Declare

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

' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
' To keep focus on window
' ----------------------------------------------------------------------------------------------------------------------------------------------------------------
'Declare Dynamic Library "user32"
'    Sub ShowWindow (ByVal hWnd As _Offset, Byval nCmdShow As Long)
'End Declare

' ================================================================================================================================================================
' END Dynamic Library definitions
' ================================================================================================================================================================

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

' 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)
Dim Shared m_sTriggerFile As String: m_sTriggerFile = m_ProgramPath$ + "Pong.DELETE-TO-CLOSE"
Dim Shared m_sDebugFile As String: m_sDebugFile = m_ProgramPath$ + m_ProgramName$ + ".txt"


' RAW INPUT VARIABLES
Dim Shared rawinputdevices As String
Dim Shared hDlg As _Unsigned Long ' DWORD

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

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

' KEYBOARD VARIABLES
'Dim Shared arrKeyIndex(8) As String ' STORES KEYBOARD ID
'Dim Shared arrLastKeyDown(8) As Integer ' STORES LAST KEY PRESSED
'Dim Shared arrLastKeyUp(8) As Integer ' STORES LAST KEY RELEASED
'Dim Shared iKeyBoardCount As Integer ' # OF KEYBOARDS ATTACHED
Dim Shared iLastKeyDown As Integer
Dim Shared iLastKeyUp As Integer

' 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$

' HANDLE FOR THE PROGRAM WINDOW
Dim Shared MyHwnd As _Offset ' _Integer64 hwnd%&
Dim Shared hwndMain As _Offset

' SCREEN SIZE
Dim Shared lngScreenWidth As Long
Dim Shared lngScreenHeight As Long
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END GLOBAL VARIABLES
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

' INITIALIZE
iMinX = 0
iMaxX = 1024 '_DesktopWidth '3583
iMinY = 0
iMaxY = 768 '_DesktopHeight ' 8202
lngScreenWidth = 1024 ' _DESKTOPWIDTH
lngScreenHeight = 768 ' _DESKTOPHEIGHT

' START THE MAIN ROUTINE
main

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

' EXIT PROGRAM
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
    Dim sPort As String
    Dim iLoop As Integer
    Dim in$
    Dim MyTime##

    ' 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
            DebugLog "lngConn = _OpenClient(" + Chr$(34) + "tcp/ip:" + Chr$(34) + "+ _Trim$(Str$(uintPort)) + " + Chr$(34) + ":localhost" + Chr$(34) + ") = " + _Trim$(Str$(lngConn))

            ' INITIALIZE
            iMinX = 0
            iMaxX = 1024 '3583
            iMinY = 0
            iMaxY = 768 '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?

            '' ATTEMPT FULLSCREEN <- NOT REALLY WORKING
            '_FULLSCREEN _STRETCH, _SMOOTH
            'IF _FULLSCREEN = 0 THEN _FULLSCREEN _OFF 'check that a full screen mode initialized

            ' MAXIMIZE WINDOW
            'DOESN'T WORK: $RESIZE:STRETCH
            'DOESN'T WORK: $RESIZE:SMOOTH

            ' TRY JUST SAVING THE DESKTOP SIZE AND USING THAT WHEN DOING A NEW SCREEN
            ' Use _DESKTOPWIDTH and _DESKTOPHEIGHT to find the current desktop resolution to place the program’s window.
            lngScreenWidth = 1024 '_DesktopWidth
            lngScreenHeight = 768 '_DesktopHeight

            ' CLICK ON SCREEN TO GIVE IT THE FOCUS
            _ScreenClick 0, 0
            ''_SCREENCLICK column%, row%[, button%]

            ' CREATE TRIGGER FILE
            Open m_sTriggerFile For Output As #1
            Print #1, "Deleting this file will cause program " + m_ProgramName$ + " to stop running."
            Close #1

            ' WAIT UNTIL FILE IS CREATED
            ' (time out after 10 seconds)
            MyTime## = ExtendedTimer + 10
            Do
                If _FileExists(m_sTriggerFile) = TRUE Then Exit Do
            Loop Until Timer > MyTime##
            If _FileExists(m_sTriggerFile) = FALSE Then
                m_sError = "Trigger file not found: " + Chr$(34) + m_sTriggerFile + Chr$(34)
            End If

            If Len(m_sError) = 0 Then
                ' GET HANDLE TO THE PROGRAM WINDOW
                Do
                    MyHwnd = _WindowHandle
                Loop Until MyHwnd

                ' GIVE CONTROL TO THE EVENT-ORIENTED CODE
                System Val(Str$(WinMain))
            Else
                Print "ERROR: " + m_sError
            End If
        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
    Dim As Offset hInst
    Dim As Offset hWndTop

    Dim As MSG msg
    Dim As WNDCLASSEX wndclass
    Dim As String szMainWndClass
    Dim As String szWinTitle
    Dim As Unsigned Integer reg
    Dim sData As String

    Dim iKeyLoop As Integer
    Dim iKeyCode As Integer

    Dim iLoop As Integer

    Dim sOperation As String

    Dim sResult As String

    Dim sDebug As String

    Dim iResult As Long

    'DEBUG: TRY FULL SCREEN <- PROGRAM CRASHES!
    '_FullScreen _SquarePixels

    hInst = GetModuleHandle(0)
    szMainWndClass = "WinTestWin" + 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

    ' INITIALIZE RAW INPUT
    InitRawInput

    If Len(m_sError) = 0 Then
        ' SET WINDOW SiZE + INITIALIZE WINDOW
        'Q: HOW CAN WE USE THE QB64PE PROGRAM'S WINDOW HANDLE e.g. _WindowHandle ?
        hwndMain = CreateWindowEx( _
            0, _
            MAKELPARAM(reg, 0), _
            Offset(szWinTitle), _
            WS_OVERLAPPEDWINDOW, _
            0, _
            0, _
            lngScreenWidth, _
            lngScreenHeight, _
            0, _
            0, _
            hInst, _
            0)
        ShowWindow hwndMain, SW_SHOW

        ' TURN SUB WINDOW INVISIBLE
        ''SetWindowOpacity MyHwnd, cInvisible
        'SetWindowOpacity hwndMain, cTransparent ' <- USE THIS FOR TESTING
        'SetWindowOpacity hwndMain, 50 ' <- USE THIS FOR TESTING
        SetWindowOpacity hwndMain, cInvisible

        ' KEEP WINDOW VISIBLE
        'DEBUG: SUBSTITUTE _WindowHandle
        'UpdateWindow _WindowHandle
        UpdateWindow hwndMain

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

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

        ' MOVE TO TOP
        'Function SetWindowPos& (ByVal hWnd%&, Byval hWndInsertAfter%&, Byval X&, Byval Y&, Byval cx&, Byval cy&, Byval uFlags~&)
        'If SetWindowPos(hwndMain, HWND_TOPMOST, 200, 200, 0, 0, SWP_NOSIZE Or SWP_NOACTIVATE) = 0 Then
        If SetWindowPos(hwndMain, HWND_TOPMOST, 0, 0, 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

        '' DEBUG
        'DebugLog ""
        'DebugLog "AFTER InitRawInput:"
        'For iLoop = LBound(arrMouse) To UBound(arrMouse)
        '   DebugLog "    arrMouse(" + _Trim$(Str$(iLoop)) + ").ID = " + chr$(34) + arrMouse(iLoop).ID + chr$(34)
        'Next iLoop
        'DebugLog ""

        ' IF EVERYTHING IS WORKING, CONTINUE
        If Len(m_sError) = 0 Then
            ' START THE INPUT ROUTINES
            InitInputVars

            '' DEBUG
            'DebugLog "AFTER InitInputVars:"
            'For iLoop = LBound(arrMouse) To UBound(arrMouse)
            '   DebugLog "    arrMouse(" + _Trim$(Str$(iLoop)) + ").ID = " + chr$(34) + arrMouse(iLoop).ID + chr$(34)
            'Next iLoop
            'DebugLog ""

            ' SEND SUB WINDOW HANDLE BACK TO MAIN
            _Delay 2

            sData = "w:" + _Trim$(Str$(hwndMain)) + Chr$(13)
            sDebug = "w:" + _Trim$(Str$(hwndMain)) + "\n"

            _Delay 2

            DebugLog "Put #lngConn, , sData"
            DebugLog "Put #" + _Trim$(Str$(lngConn)) + ", , " + Chr$(34) + sDebug + Chr$(34)

            ' SET ERROR TRAPPING
            On Error GoTo ErrorHandler

            ' TRY RETURNING WINDOW HANDLE TO MAIN PROGRAM
            Put #lngConn, , sData

            ' RESUME NORMAL ERROR TRAPPING
            On Error GoTo 0

            If Len(m_sError) = 0 Then
                _Delay 2
                DebugLog "MAIN PROGRAM LOOP..."

                ' *** STOPS THE MOUSE FROM WORKING! ***
                '' HIDE THE MOUSE CURSOR
                'iResult = ShowCursor(0) ' hide cursor

                ' MAIN PROGRAM LOOP
                While GetMessage(Offset(msg), 0, 0, 0)
                    TranslateMessage Offset(msg)
                    DispatchMessage Offset(msg)

                    ' QUIT IF TRIGGER FILE IS GONE
                    If _FileExists(m_sTriggerFile) = FALSE Then
                        '' SHOW THE MOUSE CURSOR AGAIN
                        'iResult = ShowCursor(1) ' show cursor

                        ' EXIT
                        System
                    End If

                    ' KEEP WINDOW ON TOP
                    If _WindowHasFocus = 0 Then
                        _ScreenIcon
                        ''ShowWindow MyHwnd, 1
                        'ShowWindow hwndMain, 1
                        ShowWindow hwndMain, SW_SHOW
                    End If
                Wend
            Else
                m_sError = "Error, can't return hwndMain: " + m_sError
                DebugLog m_sError
            End If
        Else
            m_sError = "Error, can't setup subprogram window: " + m_sError
            DebugLog m_sError
        End If
    End If

    ' SEND ANY ERROR MESSAGES BACK TO MAIN
    If Len(m_sError) > 0 Then
        ' SHOW EROR MESSAGE
        If cDebugEnabled Then
            Screen 0
            Cls
            Print m_ProgramName$ + " failed:"
            Print m_sError
            Print
        End If

        ' TRY TO RETURN ERROR
        _Delay 2
        sData = "e:" + m_sError + Chr$(13)

        m_sError = ""

        ' SET ERROR TRAPPING
        On Error GoTo ErrorHandler

        ' TRY RETURNING ERROR MESSAGE TO MAIN PROGRAM
        Put #lngConn, , sData

        ' RESUME NORMAL ERROR TRAPPING
        On Error GoTo 0

        _Delay 2

        ' SHOW EROR MESSAGE
        If cDebugEnabled Then
            If Len(m_sError) > 0 Then
                Print "Could not return error to main program: " + m_sError
                Print
            End If

            Print "PRESS ANY KEY TO CONTINUE"
            Sleep
        End If
    End If

    ' RETURN A VALUE
    WinMain = msg.wParam
End Function ' WinMain

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

' MESSAGE TYPES FOR READING THE KEYBOARD:
' WM_CHAR
' WM_KEYDOWN
' WM_KEYUP
' WM_SYSCHAR
' WM_SYSKEYDOWN
' WM_SYSKEYUP

Function MainWndProc%& (hwnd As Offset, nMsg As Unsigned Long, wParam As Unsigned Offset, lParam As Offset)
    ' EVENT HANDLER VARIABLES PART 1
    Static As Offset hwndButton
    Static As Long cx, cy
    Dim As Offset hdc
    Dim As PAINTSTRUCT ps
    Dim As RECT rc
    Dim As RECT TargetRect
    Dim As MEM lpb
    Dim As Unsigned Long dwSize
    Dim As RAWINPUT rawm ' MOUSE VERSION
    Dim As RAWINPUT_K rawk ' KEYBOARD VERSION
    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 iLine As Integer
    Dim iLen As Integer
    Dim iCount As Integer
    Dim sCount As String
    Dim sText As String
    Dim sX As String
    Dim sY As String
    Dim sPX As String
    Dim sPY 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
    Dim iInputType As Integer

    ' EVENT HANDLER VARIABLES PART 2
    Dim pRawInput As _Offset ' RAWINPUT POINTER
    Dim zKeyName As String ' ASCIIZ * 50 = NULL-terminated string
    Dim sRawInput As String
    Dim sBuffer As String
    Dim ScanCode As _Unsigned Long ' DWORD
    Static hFocusBak As _Unsigned Long ' DWORD
    Dim RawInputDevCount As Long
    Dim KeyboardTypeCount As Long
    Dim RawInputDeviceIndex As Long
    Dim ByteCount As Long
    Dim int_wParam As Integer
    Dim vbCrLf As String: vbCrLf = Chr$(13) + Chr$(10)
    Dim vbCr As String: vbCr = Chr$(13)
    Dim vbLf As String: vbLf = Chr$(10)
    ReDim arrText$(0)

    ' HANDLE EVENT MESSAGES
    Select Case nMsg
        Case WM_DESTROY:
            'DebugPrint "nMsg = WM_DESTROY"
            PostQuitMessage 0
            MainWndProc = 0
            Exit Function

        Case WM_INPUT:
            'DebugPrint "nMsg = WM_INPUT"

            ' MOUSE VERSION:
            GetRawInputData lParam, RID_INPUT, 0, Offset(dwSize), Len(rih)

            ' KEYBOARD VERSION:
            'GetRawInputData(CBLPARAM, %RID_INPUT, BYVAL %NULL, ByteCount, SIZEOF(RAWINPUTHEADER)) ' Get size of raw input buffer

            lpb = MemNew(dwSize)
            If lpb.SIZE = 0 Then
                MainWndProc = 0
                Exit Function
            End If

            ' GET THE RAW INPUT
            If GetRawInputData(lParam, RID_INPUT, lpb.OFFSET, Offset(dwSize), Len(rih)) <> dwSize Then
                'TODO: BUBBLE UP THE ERROR MESSAGE?
                Print "GetRawInputData doesn't return correct size!"
                'DebugPrint "WRONG SIZE: GetRawInputData doesn't return correct size!"
            End If

            ' IDENTIFY TYPE OF INPUT
            Select Case dwSize
                Case Len(rawm):
                    ' MOUSE INPUT
                    'DebugPrint "dwSize = Len(rawm) so MOUSE INPUT DETECTED"
                    iInputType = RIM_TYPEMOUSE
                    MemGet lpb, lpb.OFFSET, rawm
                Case Len(rawk):
                    ' KEYBOARD INPUT
                    'DebugPrint "dwSize = Len(rawk) so KEYBOARD INPUT DETECTED"
                    iInputType = RIM_TYPEKEYBOARD
                    MemGet lpb, lpb.OFFSET, rawk
                Case Else:
                    ' SOME OTHER TYPE (MAYBE HID) BUT ONE WE CAN'T PROCESS
                    'DebugPrint "dwSize = SOME OTHER TYPE (MAYBE HID)"
                    iInputType = RIM_TYPEUNKNOWN
            End Select

            If iInputType = RIM_TYPEMOUSE Then
                'DebugLog "iInputType = RIM_TYPEMOUSE"

                If rawm.header.dwType = RIM_TYPEMOUSE Then
                    'DebugLog "    rawm.header.dwType = RIM_TYPEMOUSE"

                    tmpx = rawm.mouse.lLastX
                    tmpy = rawm.mouse.lLastY
                    maxx = tmpx

                    ' 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$(rawm.header.hDevice))
                    'DebugPrint "    strNextID = " + Chr$(34) + strNextID + Chr$(34)
                    'DebugLog "        strNextID = " + Chr$(34) + strNextID + Chr$(34)

                    ' GET ARRAY INDEX FROM strnextID
                    iIndex = GetMouseIndex%(strNextID)
                    'DebugPrint "    iIndex =  GetMouseIndex%(" + chr$(34) + strNextID + chr$(34) + ") = " + _Trim$(Str$(iIndex))
                    'DebugLog "    iIndex =  GetMouseIndex%(" + chr$(34) + strNextID + chr$(34) + ") = " + _Trim$(Str$(iIndex))

                    ' DETECT INPUT
                    If iIndex >= LBound(arrMouse) Then
                        'DebugLog "            iIndex >= LBound(arrMouse)"

                        If iIndex <= UBound(arrMouse) Then
                            'DebugLog "                iIndex <= UBound(arrMouse)"

                            'DebugLog "                    iIndex = " + _Trim$(Str$(iIndex))


                            ' INCREMENT/DECREMENT FIXED DELTA X
                            If rawm.mouse.lLastX < 0 Then
                                arrMouse(iIndex).dx = -1
                                'arrMouse(iIndex).x = arrMouse(iIndex).x - 1
                            ElseIf rawm.mouse.lLastX > 0 Then
                                arrMouse(iIndex).dx = 1
                                'arrMouse(iIndex).x = arrMouse(iIndex).x + 1
                            End If
                            'DebugLog "                    arrMouse(iIndex).dx = " + _Trim$(Str$(arrMouse(iIndex).dx))

                            ' INCREMENT/DECREMENT FIXED DELTA Y
                            If rawm.mouse.lLastY < 0 Then
                                arrMouse(iIndex).dy = -1
                                'arrMouse(iIndex).y = arrMouse(iIndex).y - 1
                            ElseIf rawm.mouse.lLastY > 0 Then
                                arrMouse(iIndex).dy = 1
                                'arrMouse(iIndex).y = arrMouse(iIndex).y + 1
                            End If
                            'DebugLog "                    arrMouse(iIndex).dy = " + _Trim$(Str$(arrMouse(iIndex).dy))


                            ' INCREMENT/DECREMENT TRUE DELTA
                            arrMouse(iIndex).pdx = rawm.mouse.lLastX
                            arrMouse(iIndex).pdy = rawm.mouse.lLastY
                            arrMouse(iIndex).px = arrMouse(iIndex).px + arrMouse(iIndex).pdx
                            arrMouse(iIndex).py = arrMouse(iIndex).py + arrMouse(iIndex).pdy

                            ' CHECK HIRES CURSOR BOUNDARIES
                            If arrMouse(iIndex).px < cMinPX Then arrMouse(iIndex).px = cMinPX
                            If arrMouse(iIndex).px > cMaxPX Then arrMouse(iIndex).px = cMaxPX
                            If arrMouse(iIndex).py < cMinPY Then arrMouse(iIndex).py = cMinPY
                            If arrMouse(iIndex).py > cMaxPY Then arrMouse(iIndex).py = cMaxPY

                            'DebugLog "                    arrMouse(iIndex).px = " + _Trim$(Str$(arrMouse(iIndex).px))
                            'DebugLog "                    arrMouse(iIndex).py = " + _Trim$(Str$(arrMouse(iIndex).py))

                            ' =============================================================================
                            ' left button = 1 when down, 2 when released
                            If ((rawm.mouse.usButtonFlags And 1) = 1) Then
                                arrMouse(iIndex).LeftDown = TRUE
                            ElseIf ((rawm.mouse.usButtonFlags And 2) = 2) Then
                                arrMouse(iIndex).LeftDown = FALSE
                            End If
                            'DebugLog "                    arrMouse(iIndex).LeftDown = " + TrueFalse$(arrMouse(iIndex).LeftDown)

                            ' =============================================================================
                            ' middle button = 16 when down, 32 when released
                            If ((rawm.mouse.usButtonFlags And 16) = 16) Then
                                arrMouse(iIndex).MiddleDown = TRUE
                            ElseIf ((rawm.mouse.usButtonFlags And 32) = 32) Then
                                arrMouse(iIndex).MiddleDown = FALSE
                            End If
                            'DebugLog "                    arrMouse(iIndex).MiddleDown = " + TrueFalse$(arrMouse(iIndex).MiddleDown)

                            ' =============================================================================
                            ' right button = 4 when down, 8 when released
                            If ((rawm.mouse.usButtonFlags And 4) = 4) Then
                                arrMouse(iIndex).RightDown = TRUE
                            ElseIf ((rawm.mouse.usButtonFlags And 8) = 8) Then
                                arrMouse(iIndex).RightDown = FALSE
                            End If
                            'DebugLog "                    arrMouse(iIndex).RightDown = " + TrueFalse$(arrMouse(iIndex).RightDown)

                            ' =============================================================================
                            ' scroll wheel = ???
                            'Hex$(rawm.mouse.usButtonFlags)
                            '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{dx}\t{dy}\t{wheel}\t{leftDown}\t{middleDown}\t{rightDown}\n
                            sOutput = "m:"
                            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).px)) + Chr$(9)
                            sOutput = sOutput + _Trim$(Str$(arrMouse(iIndex).py)) + 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
                            'DebugLog "                    Put #lngConn, , sOutput"
                            'DebugLog "                    Put #" + _Trim$(Str$(lngConn)) + ", , " + chr$(34) + sOutput + chr$(34)

                            Put #lngConn, , sOutput

                            'DebugLog "                    m_sError = " + chr$(34) + m_sError + chr$(34)

                            ' CLEAR MOVEMENT
                            arrMouse(iIndex).dx = 0
                            arrMouse(iIndex).dy = 0
                            'arrMouse(iIndex).wheelOld = arrMouse(iIndex).wheel
                            'arrMouse(iIndex).LeftDownOld = arrMouse(iIndex).LeftDown
                            'arrMouse(iIndex).MiddleDownOld = arrMouse(iIndex).MiddleDown
                            'arrMouse(iIndex).RightDownOld = arrMouse(iIndex).RightDown
                        End If

                    End If

                    '' INVOKE PAINT
                    'InvalidateRect hwnd, 0, -1
                    'SendMessage hwnd, WM_PAINT, 0, 0
                End If
            ElseIf iInputType = RIM_TYPEKEYBOARD Then
                ' *** FOR NOW RAW KEYBOARD INPUT NOT WORKING
                'DebugPrint "iInputType = RIM_TYPEKEYBOARD"
                'If rawk.header.dwType = RIM_TYPEKEYBOARD Then
                '    DebugPrint "* FOUND RAW INPUT KEYBOARD *"
                '
                '    ' HOW DO WE READ THE KEYBOARD USING RawInputAPI ???
                '    DebugPrint "rawk.header.dwType = RIM_TYPEKEYBOARD"
                '
                '    ' IDENTIFY WHICH KEYBOARD IT IS
                '    strNextID = _Trim$(Str$(rawk.header.hDevice))
                '    DebugPrint "    strNextID = " + Chr$(34) + strNextID + Chr$(34)
                '
                '    '' GET ARRAY INDEX FROM strnextID
                '    'iIndex = GetKeyboardIndex%(strNextID)
                '    'DebugPrint "    iIndex = " + _Trim$(Str$(iIndex))
                '
                'End If
            End If

            ' FINISHUP WM_INPUT
            MemFree lpb
            MainWndProc = 0
            Exit Function

        Case WM_MOUSEMOVE:
            'DebugPrint "nMsg = WM_MOUSEMOVE"
            Exit Function

        Case WM_PAINT:
            'DebugPrint "nMsg = WM_PAINT"

            'hdc = BeginPaint(hwnd, Offset(ps))
            'GetClientRect hwnd, Offset(rc)
            '
            '' -----------------------------------------------------------------------------
            '' DISPLAY MOUSE INFO ON SCREEN AT MOUSE POSITIONS
            'iCount = 0
            'For iIndex = LBound(arrMouse) To UBound(arrMouse)
            '    iCount = iCount + 1
            '
            '    If Len(arrMouse(iIndex).ID) > 0 Then
            '        ' CHECK CURSOR BOUNDARIES
            '        If arrMouse(iIndex).x < cMinX Then arrMouse(iIndex).x = cMinX
            '        If arrMouse(iIndex).x > cMaxX Then arrMouse(iIndex).x = cMaxX
            '        If arrMouse(iIndex).y < cMinY Then arrMouse(iIndex).y = cMinY
            '        If arrMouse(iIndex).y > cMaxY Then arrMouse(iIndex).y = cMaxY
            '
            '        ' CHECK HIRES CURSOR BOUNDARIES
            '        If arrMouse(iIndex).px < cMinPX Then arrMouse(iIndex).px = cMinPX
            '        If arrMouse(iIndex).px > cMaxPX Then arrMouse(iIndex).px = cMaxPX
            '        If arrMouse(iIndex).py < cMinPY Then arrMouse(iIndex).py = cMinPY
            '        If arrMouse(iIndex).py > cMaxPY Then arrMouse(iIndex).py = cMaxPY
            '
            '        ' DEFINE TARGET RECT FOR WHERE TO DRAW ON SCREEN
            '        TargetRect.left = rc.left + arrMouse(iIndex).px
            '        TargetRect.top = rc.top + arrMouse(iIndex).py
            '        TargetRect.right = rc.right + arrMouse(iIndex).px
            '        TargetRect.bottom = rc.bottom + arrMouse(iIndex).py
            '
            '        ' COLLECT VALUES FOR THIS MOUSE IN A STRING
            '        sText = ""
            '        sText = sText + _Trim$(Str$(iCount))
            '        sText = sText + " ("
            '        sText = sText + _Trim$(Str$(arrMouse(iIndex).px))
            '        sText = sText + ","
            '        sText = sText + _Trim$(Str$(arrMouse(iIndex).py))
            '        sText = sText + ") "
            '        sText = sText + IIFS$(arrMouse(iIndex).LeftDown, "1", " ")
            '        sText = sText + IIFS$(arrMouse(iIndex).MiddleDown, "2", " ")
            '        sText = sText + IIFS$(arrMouse(iIndex).RightDown, "3", " ")
            '
            '        'arrMouse(iIndex).wheel
            '        'arrMouse(iIndex).char
            '        'arrMouse(iIndex).y
            '        'arrMouse(iIndex).x
            '
            '        ' DRAW VALUES FOR THIS MOUSE TO SCREEN AT POINTER POSITION
            '        DrawText hdc, Offset(sText), Len(sText), Offset(TargetRect), DT_LEFT
            '        OffsetRect Offset(TargetRect), arrMouse(iIndex).px, arrMouse(iIndex).px
            '    End If
            'Next iIndex
            '
            '' -----------------------------------------------------------------------------
            '' DISPLAY INSTRUCTIONS ON SCREEN
            '' DEFINE TARGET RECT FOR WHERE TO DRAW ON SCREEN
            'TargetRect.left = rc.left + 100
            'TargetRect.top = rc.top + 500
            'TargetRect.right = rc.right + 100
            'TargetRect.bottom = rc.bottom + 500
            '
            '' COLLECT VALUES FOR THIS KEYBOARD IN A STRING
            'sText = ""
            'sText = sText + "Raw Input API multi-mouse demo:"
            'sText = sText + Chr$(13)
            'sText = sText + Chr$(13)
            'sText = sText + "1. Plug in 2 or more USB mice"
            'sText = sText + Chr$(13)
            'sText = sText + "2. Move them around and click the buttons."
            'sText = sText + Chr$(13)
            'sText = sText + "3. Try pressing some keys on the keyboard."
            'sText = sText + Chr$(13)
            'sText = sText + Chr$(13)
            'sText = sText + "Press ESC to exit."
            '
            '' DRAW VALUES FOR THIS KEYBOARD TO SCREEN AT NEXT POSITION
            'DrawText hdc, Offset(sText), Len(sText), Offset(TargetRect), DT_LEFT
            'OffsetRect Offset(TargetRect), 0, 0 ' y,x
            '
            '' -----------------------------------------------------------------------------
            '' DISPLAY KEYBOARD INFO ON SCREEN
            '' DEFINE TARGET RECT FOR WHERE TO DRAW ON SCREEN
            'TargetRect.left = rc.left + 400
            'TargetRect.top = rc.top + 100
            'TargetRect.right = rc.right + 400
            'TargetRect.bottom = rc.bottom + 100
            '
            '' COLLECT VALUES FOR THIS KEYBOARD IN A STRING
            'sText = ""
            'sText = sText + "Keyboard: "
            'sText = sText + IIFS$(iLastKeyDown > 0, VirtualKeyCodeToString$(iLastKeyDown) + " (" + _Trim$(Str$(iLastKeyDown)) + ")", "")
            ''sText = sText + Chr$(13)
            ''sText = sText + "  LAST DOWN="
            ''sText = sText + IIFS$(iLastKeyDown > 0, VirtualKeyCodeToString$(iLastKeyDown), "")
            ''sText = sText + Chr$(13)
            ''sText = sText + "  LAST UP  ="
            ''sText = sText + IIFS$(iLastKeyUp > 0, VirtualKeyCodeToString$(iLastKeyUp), "")
            '
            '' DRAW VALUES FOR THIS KEYBOARD TO SCREEN AT NEXT POSITION
            'DrawText hdc, Offset(sText), Len(sText), Offset(TargetRect), DT_LEFT
            'OffsetRect Offset(TargetRect), 0, 0 ' y,x
            '
            '' -----------------------------------------------------------------------------
            '' FINISH PAINT
            'EndPaint hwnd, Offset(ps)
            '
            MainWndProc = 0
            Exit Function

        Case WM_CHAR:
            'DebugPrint "nMsg = WM_CHAR"

            '' GET AN INTEGER FROM WPARAM
            'If wParam < 32768 Then
            '    int_wParam = Val(_Trim$(Str$(wParam)))
            'Else
            '    int_wParam = -1
            'End If
            '
            '' WM_CHAR message
            '' https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-char
            '' Posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed.
            'DebugPrint "nMsg = WM_CHAR"
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)
            '
            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            'MainWndProc = 0

            Exit Function

        Case WM_KEYDOWN:
            'DebugPrint "nMsg = WM_KEYDOWN"

            ' GET AN INTEGER FROM WPARAM
            If wParam < 32768 Then
                int_wParam = Val(_Trim$(Str$(wParam)))
            Else
                int_wParam = -1
            End If

            ' REMEMBER KEY
            iLastKeyDown = int_wParam

            ' WM_KEYDOWN message
            ' Posted to the window with the keyboard focus when a nonsystem key is pressed. A nonsystem key is a key that is pressed when the ALT key is not pressed.
            ' https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-keydown
            'DebugPrint "nMsg = WM_KEYDOWN"
            'DebugPrint "    strNextID  =" + Chr$(34) + strNextID + Chr$(34)
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)

            ' SEND KEYDOWN EVENT TO HOST
            sOutput = "d:" + _Trim$(Str$(int_wParam)) + Chr$(13)
            Put #lngConn, , sOutput

            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            MainWndProc = 0

            Exit Function

        Case WM_KEYUP:
            'DebugPrint "nMsg = WM_KEYUP"

            ' GET AN INTEGER FROM WPARAM
            If wParam < 32768 Then
                int_wParam = Val(_Trim$(Str$(wParam)))
            Else
                int_wParam = -1
            End If

            ' REMEMBER KEY
            iLastKeyUp = int_wParam
            iLastKeyDown = 0

            ' WM_KEYUP message
            ' https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-keyup
            ' Posted to the window with the keyboard focus when a nonsystem key is released. A nonsystem key is a key that is pressed when the ALT key is not pressed, or a keyboard key that is pressed when a window has the keyboard focus.
            'DebugPrint "nMsg = WM_KEYUP"
            'DebugPrint "    strNextID  =" + Chr$(34) + strNextID + Chr$(34)
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)

            ' SEND KEYUP EVENT TO HOST
            sOutput = "u:" + _Trim$(Str$(int_wParam)) + Chr$(13)
            Put #lngConn, , sOutput

            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            MainWndProc = 0

            '' EXIT WHEN USER RELEASES ESCAPE KEY
            'If int_wParam = 27 Then System

            Exit Function

        Case WM_SYSCHAR:
            'DebugPrint "nMsg = WM_SYSCHAR"

            '' GET AN INTEGER FROM WPARAM
            'If wParam < 32768 Then
            '    int_wParam = Val(_Trim$(Str$(wParam)))
            'Else
            '    int_wParam = -1
            'End If
            '
            '' WM_SYSCHAR message
            '' https://learn.microsoft.com/en-us/windows/win32/menurc/wm-syschar
            '' Posted to the window with the keyboard focus when a WM_SYSKEYDOWN message is translated by the TranslateMessage function. It specifies the character code of a system character key that is, a character key that is pressed while the ALT key is down.
            'DebugPrint "nMsg = WM_SYSCHAR"
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)
            '
            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            'MainWndProc = 0

            Exit Function

        Case WM_SYSKEYDOWN:
            'DebugPrint "nMsg = WM_SYSKEYDOWN"

            ' GET AN INTEGER FROM WPARAM
            If wParam < 32768 Then
                int_wParam = Val(_Trim$(Str$(wParam)))
            Else
                int_wParam = -1
            End If

            ' REMEMBER KEY
            iLastKeyDown = int_wParam

            ' WM_SYSKEYDOWN message
            ' https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-syskeydown
            ' Posted to the window with the keyboard focus when the user presses the F10 key (which activates the menu bar) or holds down the ALT key and then presses another key. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYDOWN message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter.
            'DebugPrint "nMsg = WM_SYSKEYDOWN"
            'DebugPrint "    strNextID  =" + Chr$(34) + strNextID + Chr$(34)
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)

            ' SEND KEYDOWN EVENT TO HOST
            sOutput = "d:" + _Trim$(Str$(int_wParam)) + Chr$(13)
            Put #lngConn, , sOutput

            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            MainWndProc = 0

            Exit Function

        Case WM_SYSKEYUP:
            'DebugPrint "nMsg = WM_SYSKEYUP"

            ' GET AN INTEGER FROM WPARAM
            If wParam < 32768 Then
                int_wParam = Val(_Trim$(Str$(wParam)))
            Else
                int_wParam = -1
            End If

            ' REMEMBER KEY
            iLastKeyUp = int_wParam
            iLastKeyDown = 0

            ' WM_SYSKEYUP message
            ' https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-syskeyup
            ' Posted to the window with the keyboard focus when the user releases a key that was pressed while the ALT key was held down. It also occurs when no window currently has the keyboard focus; in this case, the WM_SYSKEYUP message is sent to the active window. The window that receives the message can distinguish between these two contexts by checking the context code in the lParam parameter.
            ' A window receives this message through its WindowProc function.
            'DebugPrint "nMsg = WM_SYSKEYUP"
            'DebugPrint "    strNextID  =" + Chr$(34) + strNextID + Chr$(34)
            'DebugPrint "    Offset hwnd=" + _Trim$(Str$(hwnd)) + ", Unsigned Long nMsg=" + _Trim$(Str$(nMsg)) + ", Unsigned Offset wParam=" + _Trim$(Str$(wParam)) + ", Offset lParam=" + _Trim$(Str$(lParam))
            'DebugPrint "    Pressed key: " + VirtualKeyCodeToString$(int_wParam)

            ' SEND KEYUP EVENT TO HOST
            sOutput = "u:" + _Trim$(Str$(int_wParam)) + Chr$(13)
            Put #lngConn, , sOutput

            '' INVOKE PAINT
            'InvalidateRect hwnd, 0, -1
            'SendMessage hwnd, WM_PAINT, 0, 0
            MainWndProc = 0

            Exit Function

        Case Else:
            ' some other message
            MainWndProc = DefWindowProc(hwnd, nMsg, wParam, lParam)
    End Select
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 iLoop2 As Integer
    Dim strNextID As String

    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 DEVICE INFO
    rawinputdevices = "Number of raw input devices:" + Str$(nDevices) + Chr$(13)
    iMouseCount = 0
    'iKeyBoardCount = 0
    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 = RIM_TYPEMOUSE Then
            iMouseCount = iMouseCount + 1 ' INCREMENT THE MOUSE COUNT
            strNextID = _Trim$(Str$(rawdevs(x).hDevice)) ' GET THE MOUSE DEVICE ID
            arrMouse(iMouseCount - 1).ID = strNextID ' SAVE THE MOUSE DEVICE ID
            'arrMouse(iMouseCount - 1).UpdateCount = 0
        ElseIf rawdevs(x).dwType = RIM_TYPEKEYBOARD Then
            'iKeyBoardCount = iKeyBoardCount + 1 ' INCREMENT THE KEYBAORD COUNT
            'strNextID = _Trim$(Str$(rawdevs(x).hDevice)) ' GET THE KEYBOARD DEVICE ID
            'arrKeyIndex(iKeyBoardCount - 1) = strNextID ' SAVE THE KEYBOARD DEVICE ID
            'arrLastKeyDown(iKeyBoardCount - 1) = 0
        End If
    Next x

    ' FOR NOW KEYBOARD INFO IS NOT RAW INPUT, UNTIL WE FIGURE IT OUT:
    iLastKeyDown = 0
    iLastKeyUp = 0

    rawinputdevices = rawinputdevices + Chr$(0)
    MemFree pRawInputDeviceList

    Rid(0).usUsagePage = &H01
    Rid(0).usUsage = &H02
    Rid(0).dwFlags = 0
    Rid(0).hwndTarget = 0

    If RegisterRawInputDevices(Offset(Rid()), 1, Len(Rid(0))) = 0 Then
        m_sError = "RawInput init failed" + Chr$(0)
    End If

End Sub ' InitRawInput

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN WINDOW FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' hWnd = handle to window to set opacity for
' Level = 0 TO 255, 0=totally invisible, 128=transparent, 255=100% solid

Sub SetWindowOpacity (hWnd As _Offset, Level As _Unsigned _Byte)
    Const cIndex = -20
    Const LWA_ALPHA = &H2
    Const WS_EX_LAYERED = &H80000

    Dim lngMsg As Long
    Dim lngValue As Long

    'Function GetWindowLong& Alias "GetWindowLongA" (ByVal hwnd As _Offset, Byval nIndex As Long)
    lngMsg = GetWindowLong(hWnd, cIndex)
    lngMsg = lngMsg Or WS_EX_LAYERED

    'Function SetWindowLong& Alias "SetWindowLongA" (ByVal hwnd As _Offset, Byval nIndex As Long, Byval dwNewLong As Long)
    lngValue = SetWindowLong(hWnd, cIndex, lngMsg)

    'Function SetLayeredWindowAttributes& (ByVal hwnd As _Offset, Byval crKey As Long, Byval bAlpha As _Unsigned _Byte, Byval dwFlags As Long)
    lngValue = SetLayeredWindowAttributes(hWnd, 0, Level, LWA_ALPHA)
End Sub ' SetWindowOpacity

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END WINDOW FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN RAW INPUT VARIABLE FUNCTIONS #1
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' Initialize variables that store mouse + keyboard input

Sub InitInputVars
    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.
    iIndex = LBound(arrMouse) - 1
    For iLoop = 1 To iMouseCount
        iIndex = iIndex + 1
        'DON'T ERASE THE ID!: arrMouse(iIndex).ID = ""
        'arrMouse(iIndex).UpdateCount = 1
        arrMouse(iIndex).dx = 0
        arrMouse(iIndex).dy = 0
        arrMouse(iIndex).px = cMaxPX / 2 ' 0
        arrMouse(iIndex).py = cMaxPY / 2 ' 0
        arrMouse(iIndex).pdx = 0 ' 100
        arrMouse(iIndex).pdy = 0 ' 100
        arrMouse(iIndex).wheel = 0
        'arrMouse(iIndex).wheelOld = 0
        arrMouse(iIndex).LeftDown = FALSE
        'arrMouse(iIndex).LeftDownOld = FALSE
        arrMouse(iIndex).MiddleDown = FALSE
        'arrMouse(iIndex).MiddleDownOld = FALSE
        arrMouse(iIndex).RightDown = FALSE
        'arrMouse(iIndex).RightDownOld = FALSE
    Next iLoop

    ' INITIALIZE KEYBOARD STATE VARIABLES
    iLastKeyDown = 0
    iLastKeyUp = 0
End Sub ' InitInputVars

' /////////////////////////////////////////////////////////////////////////////
' 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
        End If
    Next iLoop
    GetMouseIndex% = iIndex%
End Function ' GetMouseIndex%

'' /////////////////////////////////////////////////////////////////////////////
'' Finds position in array arrKeyIndex containing KeyboardID
'
'Function GetKeyboardIndex% (KeyboardID As String)
'    Dim iLoop As Integer
'    Dim iIndex%
'    iIndex% = LBound(arrKeyIndex) - 1
'    For iLoop = LBound(arrKeyIndex) To UBound(arrKeyIndex)
'        If arrKeyIndex(iLoop) = KeyboardID Then
'            iIndex% = iLoop
'            Exit For
'        End If
'    Next iLoop
'    GetKeyboardIndex% = iIndex%
'End Function ' GetKeyboardIndex%

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END RAW INPUT VARIABLE FUNCTIONS #1
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN FILE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
'WORKS IN QB64PE BUT NOT QB64:
'' /////////////////////////////////////////////////////////////////////////////
'' QB64 Phoenix Edition › QB64 Rising › Code and Stuff › Help Me!
'' Using shell to delete a file
'' https://qb64phoenix.com/forum/showthread.php?tid=2618&pid=24683#pid24683
'' a740g
'' #5
'' 04-24-2024, 06:05 AM
''
'' There are no commands to directly make copies or backup of files.
'' But you could write one with a few lines of code like:
''
'' Copies src to dst
'' Set overwite to true if dst should be overwritten if present
'Sub CopyFile (src As String, dst As String, overwrite As _Byte)
'    If _FileExists(src) Then
'        If Not _FileExists(dst) Or (_FileExists(dst) And overwrite) Then
'            _WriteFile dst, _ReadFile$(src)
'        End If
'    End If
'End Sub ' CopyFile

' /////////////////////////////////////////////////////////////////////////////
' Convert a value to string and trim it (because normal Str$ adds spaces)

Function cstr$ (myValue)
    'cstr$ = LTRIM$(RTRIM$(STR$(myValue)))
    cstr$ = _Trim$(Str$(myValue))
End Function ' cstr$

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

Sub DebugLog1 (sText As String)
    Dim sResult As String
    If _FileExists(m_sDebugFile) Then
        sResult = PrintFile$(m_sDebugFile, sText, TRUE)
    Else
        sResult = PrintFile$(m_sDebugFile, sText, FALSE)
    End If
End Sub ' DebugLog

' /////////////////////////////////////////////////////////////////////////////
' QB64 Phoenix Edition › QB64 Rising › Code and Stuff › Help Me!
' Using shell to delete a file
' https://qb64phoenix.com/forum/showthread.php?tid=2618

Sub DeleteFile (sFile As String)
    If _FileExists(sFile) Then
        'Shell "DELETE " + sFile
        'Shell "del " + sFile
        Kill sFile
    End If
End Sub ' DeleteFile

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

Function FileExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                FileExt$ = Right$(sFile, Len(sFile) - iPos)
            Else
                ' dot is first character, return everything after it
                FileExt$ = Right$(sFile, Len(sFile) - 1)
            End If
        Else
            ' file only has one character, the dot, the file extension is blank
            FileExt$ = ""
        End If
    Else
        ' no dot found, the file extension is blank
        FileExt$ = ""
    End If
End Function ' FileExt$

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

Function NameOnly$ (sFile As String, sSlash As String)
    Dim iPos As Integer
    'sFile = Replace$(sFile, "/", "\")

    iPos = _InStrRev(sFile, sSlash)
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NameOnly$ = Right$(sFile, Len(sFile) - iPos)
            Else
                ' slash is first character, return everything after it
                NameOnly$ = Right$(sFile, Len(sFile) - 1)
            End If
        Else
            ' file only has one character, the slash, name is blank
            NameOnly$ = ""
        End If
    Else
        ' slash not found, return the entire thing
        NameOnly$ = sFile
    End If
End Function ' NameOnly$

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

Function NoExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NoExt$ = Left$(sFile, iPos - 1)
            Else
                ' dot is first character, removing it returns blank!
                ' our version will just return the name unchanged
                ' but you can return blank if you prefer
                NoExt$ = sFile
            End If
        Else
            ' file only has one character, the dot, removing it returns blank!
            ' our version will just return the name unchanged
            ' but you can return blank if you prefer
            NoExt$ = sFile
        End If
    Else
        ' no dot found
        ' return the name unchanged
        NoExt$ = sFile
    End If
End Function ' NoExt$

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

Function PathOnly$ (sFile As String, sSlash As String)
    Dim iPos As Integer
    'sFile = Replace$(sFile, "/", "\")

    iPos = _InStrRev(sFile, sSlash)
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                PathOnly$ = Left$(sFile, iPos)
            Else
                ' slash is first character, so not much of a path, return blank
                PathOnly$ = ""
            End If
        Else
            ' file only has one character, the slash, name is blank
            PathOnly$ = ""
        End If
    Else
        ' slash not found, so not a path, return blank
        PathOnly$ = ""
    End If
End Function ' PathOnly$

' /////////////////////////////////////////////////////////////////////////////
' Writes sText to file sFileName.
' If bAppend=TRUE appends to file, else overwrites it.

' Returns blank if successful else returns error message.

' Example:
' ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
' ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)
' sFileName = ProgramPath$ + ProgramName$ + ".OUT.txt"
' sText = "This is a test." + chr$(13) + "Here is line 2." + chr$(13) + "End."
' sError = PrintFile$(sFileName, sText, FALSE)

Function PrintFile$ (sFileName As String, sText As String, bAppend As Integer)
    Dim sError As String: sError = ""

    If (bAppend = TRUE) Then
        If _FileExists(sFileName) Then
            Open sFileName For Append As #1 ' opens an existing file for appending
        Else
            sError = "Error in PrintFile$ : File not found. Cannot append."
        End If
    Else
        Open sFileName For Output As #1 ' opens and clears an existing file or creates new empty file
    End If

    If Len(sError) = 0 Then
        ' NOTE: WRITE places text in quotes in the file
        'WRITE #1, x, y, z$
        'WRITE #1, sText

        ' PRINT does not put text inside quotes
        Print #1, sText

        Close #1
    End If

    PrintFile$ = sError
End Function ' PrintFile$

' /////////////////////////////////////////////////////////////////////////////
' Fastest way is always to just read the whole life at once and then parse it.

Function ReadTextFile$ (sFileName As String, sDefault As String)
    Dim x$
    If _FileExists(sFileName) Then
        Open sFileName For Binary As #1
        x$ = Space$(LOF(1))
        Get #1, 1, x$
        Close #1
        ReadTextFile$ = x$
    Else
        ReadTextFile$ = sDefault
    End If
End Function ' ReadTextFile$

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END FILE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN CONSTANT TO STRING FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

Function VirtualKeyCodeToString$ (MyInteger As Integer)
    Dim Mystring As String

    Select Case MyInteger
        Case VK_LBUTTON:
            Mystring = "VK_LBUTTON"
        Case VK_RBUTTON:
            Mystring = "VK_RBUTTON"
        Case VK_CANCEL:
            Mystring = "VK_CANCEL"
        Case VK_MBUTTON:
            Mystring = "VK_MBUTTON"
        Case VK_XBUTTON1:
            Mystring = "VK_XBUTTON1"
        Case VK_XBUTTON2:
            Mystring = "VK_XBUTTON2"
        Case VK_BACK:
            Mystring = "VK_BACK"
        Case VK_TAB:
            Mystring = "VK_TAB"
        Case VK_CLEAR:
            Mystring = "VK_CLEAR"
        Case VK_RETURN:
            Mystring = "VK_RETURN"
        Case VK_SHIFT:
            Mystring = "VK_SHIFT"
        Case VK_CONTROL:
            Mystring = "VK_CONTROL"
        Case VK_MENU:
            Mystring = "VK_MENU"
        Case VK_PAUSE:
            Mystring = "VK_PAUSE"
        Case VK_CAPITAL:
            Mystring = "VK_CAPITAL"
        Case VK_KANA:
            Mystring = "VK_KANA"
        Case VK_HANGUL:
            Mystring = "VK_HANGUL"
        Case VK_IME_ON:
            Mystring = "VK_IME_ON"
        Case VK_JUNJA:
            Mystring = "VK_JUNJA"
        Case VK_FINAL:
            Mystring = "VK_FINAL"
        Case VK_HANJA:
            Mystring = "VK_HANJA"
        Case VK_KANJI:
            Mystring = "VK_KANJI"
        Case VK_IME_OFF:
            Mystring = "VK_IME_OFF"
        Case VK_ESCAPE:
            Mystring = "VK_ESCAPE"
        Case VK_CONVERT:
            Mystring = "VK_CONVERT"
        Case VK_NONCONVERT:
            Mystring = "VK_NONCONVERT"
        Case VK_ACCEPT:
            Mystring = "VK_ACCEPT"
        Case VK_MODECHANGE:
            Mystring = "VK_MODECHANGE"
        Case VK_SPACE:
            Mystring = "VK_SPACE"
        Case VK_PRIOR:
            Mystring = "VK_PRIOR"
        Case VK_NEXT:
            Mystring = "VK_NEXT"
        Case VK_END:
            Mystring = "VK_END"
        Case VK_HOME:
            Mystring = "VK_HOME"
        Case VK_LEFT:
            Mystring = "VK_LEFT"
        Case VK_UP:
            Mystring = "VK_UP"
        Case VK_RIGHT:
            Mystring = "VK_RIGHT"
        Case VK_DOWN:
            Mystring = "VK_DOWN"
        Case VK_SELECT:
            Mystring = "VK_SELECT"
        Case VK_PRINT:
            Mystring = "VK_PRINT"
        Case VK_EXECUTE:
            Mystring = "VK_EXECUTE"
        Case VK_SNAPSHOT:
            Mystring = "VK_SNAPSHOT"
        Case VK_INSERT:
            Mystring = "VK_INSERT"
        Case VK_DELETE:
            Mystring = "VK_DELETE"
        Case VK_HELP:
            Mystring = "VK_HELP"
        Case VK_0:
            Mystring = "VK_0"
        Case VK_1:
            Mystring = "VK_1"
        Case VK_2:
            Mystring = "VK_2"
        Case VK_3:
            Mystring = "VK_3"
        Case VK_4:
            Mystring = "VK_4"
        Case VK_5:
            Mystring = "VK_5"
        Case VK_6:
            Mystring = "VK_6"
        Case VK_7:
            Mystring = "VK_7"
        Case VK_8:
            Mystring = "VK_8"
        Case VK_9:
            Mystring = "VK_9"
        Case VK_A:
            Mystring = "VK_A"
        Case VK_B:
            Mystring = "VK_B"
        Case VK_C:
            Mystring = "VK_C"
        Case VK_D:
            Mystring = "VK_D"
        Case VK_E:
            Mystring = "VK_E"
        Case VK_F:
            Mystring = "VK_F"
        Case VK_G:
            Mystring = "VK_G"
        Case VK_H:
            Mystring = "VK_H"
        Case VK_I:
            Mystring = "VK_I"
        Case VK_J:
            Mystring = "VK_J"
        Case VK_K:
            Mystring = "VK_K"
        Case VK_L:
            Mystring = "VK_L"
        Case VK_M:
            Mystring = "VK_M"
        Case VK_N:
            Mystring = "VK_N"
        Case VK_O:
            Mystring = "VK_O"
        Case VK_P:
            Mystring = "VK_P"
        Case VK_Q:
            Mystring = "VK_Q"
        Case VK_R:
            Mystring = "VK_R"
        Case VK_S:
            Mystring = "VK_S"
        Case VK_T:
            Mystring = "VK_T"
        Case VK_U:
            Mystring = "VK_U"
        Case VK_V:
            Mystring = "VK_V"
        Case VK_W:
            Mystring = "VK_W"
        Case VK_X:
            Mystring = "VK_X"
        Case VK_Y:
            Mystring = "VK_Y"
        Case VK_Z:
            Mystring = "VK_Z"
        Case VK_LWIN:
            Mystring = "VK_LWIN"
        Case VK_RWIN:
            Mystring = "VK_RWIN"
        Case VK_APPS:
            Mystring = "VK_APPS"
        Case VK_SLEEP:
            Mystring = "VK_SLEEP"
        Case VK_NUMPAD0:
            Mystring = "VK_NUMPAD0"
        Case VK_NUMPAD1:
            Mystring = "VK_NUMPAD1"
        Case VK_NUMPAD2:
            Mystring = "VK_NUMPAD2"
        Case VK_NUMPAD3:
            Mystring = "VK_NUMPAD3"
        Case VK_NUMPAD4:
            Mystring = "VK_NUMPAD4"
        Case VK_NUMPAD5:
            Mystring = "VK_NUMPAD5"
        Case VK_NUMPAD6:
            Mystring = "VK_NUMPAD6"
        Case VK_NUMPAD7:
            Mystring = "VK_NUMPAD7"
        Case VK_NUMPAD8:
            Mystring = "VK_NUMPAD8"
        Case VK_NUMPAD9:
            Mystring = "VK_NUMPAD9"
        Case VK_MULTIPLY:
            Mystring = "VK_MULTIPLY"
        Case VK_ADD:
            Mystring = "VK_ADD"
        Case VK_SEPARATOR:
            Mystring = "VK_SEPARATOR"
        Case VK_SUBTRACT:
            Mystring = "VK_SUBTRACT"
        Case VK_DECIMAL:
            Mystring = "VK_DECIMAL"
        Case VK_DIVIDE:
            Mystring = "VK_DIVIDE"
        Case VK_F1:
            Mystring = "VK_F1"
        Case VK_F2:
            Mystring = "VK_F2"
        Case VK_F3:
            Mystring = "VK_F3"
        Case VK_F4:
            Mystring = "VK_F4"
        Case VK_F5:
            Mystring = "VK_F5"
        Case VK_F6:
            Mystring = "VK_F6"
        Case VK_F7:
            Mystring = "VK_F7"
        Case VK_F8:
            Mystring = "VK_F8"
        Case VK_F9:
            Mystring = "VK_F9"
        Case VK_F10:
            Mystring = "VK_F10"
        Case VK_F11:
            Mystring = "VK_F11"
        Case VK_F12:
            Mystring = "VK_F12"
        Case VK_F13:
            Mystring = "VK_F13"
        Case VK_F14:
            Mystring = "VK_F14"
        Case VK_F15:
            Mystring = "VK_F15"
        Case VK_F16:
            Mystring = "VK_F16"
        Case VK_F17:
            Mystring = "VK_F17"
        Case VK_F18:
            Mystring = "VK_F18"
        Case VK_F19:
            Mystring = "VK_F19"
        Case VK_F20:
            Mystring = "VK_F20"
        Case VK_F21:
            Mystring = "VK_F21"
        Case VK_F22:
            Mystring = "VK_F22"
        Case VK_F23:
            Mystring = "VK_F23"
        Case VK_F24:
            Mystring = "VK_F24"
        Case VK_NUMLOCK:
            Mystring = "VK_NUMLOCK"
        Case VK_SCROLL:
            Mystring = "VK_SCROLL"
        Case VK_LSHIFT:
            Mystring = "VK_LSHIFT"
        Case VK_RSHIFT:
            Mystring = "VK_RSHIFT"
        Case VK_LCONTROL:
            Mystring = "VK_LCONTROL"
        Case VK_RCONTROL:
            Mystring = "VK_RCONTROL"
        Case VK_LMENU:
            Mystring = "VK_LMENU"
        Case VK_RMENU:
            Mystring = "VK_RMENU"
        Case VK_BROWSER_BACK:
            Mystring = "VK_BROWSER_BACK"
        Case VK_BROWSER_FORWARD:
            Mystring = "VK_BROWSER_FORWARD"
        Case VK_BROWSER_REFRESH:
            Mystring = "VK_BROWSER_REFRESH"
        Case VK_BROWSER_STOP:
            Mystring = "VK_BROWSER_STOP"
        Case VK_BROWSER_SEARCH:
            Mystring = "VK_BROWSER_SEARCH"
        Case VK_BROWSER_FAVORITES:
            Mystring = "VK_BROWSER_FAVORITES"
        Case VK_BROWSER_HOME:
            Mystring = "VK_BROWSER_HOME"
        Case VK_VOLUME_MUTE:
            Mystring = "VK_VOLUME_MUTE"
        Case VK_VOLUME_DOWN:
            Mystring = "VK_VOLUME_DOWN"
        Case VK_VOLUME_UP:
            Mystring = "VK_VOLUME_UP"
        Case VK_MEDIA_NEXT_TRACK:
            Mystring = "VK_MEDIA_NEXT_TRACK"
        Case VK_MEDIA_PREV_TRACK:
            Mystring = "VK_MEDIA_PREV_TRACK"
        Case VK_MEDIA_STOP:
            Mystring = "VK_MEDIA_STOP"
        Case VK_MEDIA_PLAY_PAUSE:
            Mystring = "VK_MEDIA_PLAY_PAUSE"
        Case VK_LAUNCH_MAIL:
            Mystring = "VK_LAUNCH_MAIL"
        Case VK_LAUNCH_MEDIA_SELECT:
            Mystring = "VK_LAUNCH_MEDIA_SELECT"
        Case VK_LAUNCH_APP1:
            Mystring = "VK_LAUNCH_APP1"
        Case VK_LAUNCH_APP2:
            Mystring = "VK_LAUNCH_APP2"
        Case VK_OEM_1:
            Mystring = "VK_OEM_1"
        Case VK_OEM_PLUS:
            Mystring = "VK_OEM_PLUS"
        Case VK_OEM_COMMA:
            Mystring = "VK_OEM_COMMA"
        Case VK_OEM_MINUS:
            Mystring = "VK_OEM_MINUS"
        Case VK_OEM_PERIOD:
            Mystring = "VK_OEM_PERIOD"
        Case VK_OEM_2:
            Mystring = "VK_OEM_2"
        Case VK_OEM_3:
            Mystring = "VK_OEM_3"
        Case VK_OEM_4:
            Mystring = "VK_OEM_4"
        Case VK_OEM_5:
            Mystring = "VK_OEM_5"
        Case VK_OEM_6:
            Mystring = "VK_OEM_6"
        Case VK_OEM_7:
            Mystring = "VK_OEM_7"
        Case VK_OEM_8:
            Mystring = "VK_OEM_8"
        Case VK_OEM_102:
            Mystring = "VK_OEM_102"
        Case VK_PROCESSKEY:
            Mystring = "VK_PROCESSKEY"
        Case VK_PACKET:
            Mystring = "VK_PACKET"
        Case VK_ATTN:
            Mystring = "VK_ATTN"
        Case VK_CRSEL:
            Mystring = "VK_CRSEL"
        Case VK_EXSEL:
            Mystring = "VK_EXSEL"
        Case VK_EREOF:
            Mystring = "VK_EREOF"
        Case VK_PLAY:
            Mystring = "VK_PLAY"
        Case VK_ZOOM:
            Mystring = "VK_ZOOM"
        Case VK_NONAME:
            Mystring = "VK_NONAME"
        Case VK_PA1:
            Mystring = "VK_PA1"
        Case VK_OEM_CLEAR:
            Mystring = "VK_OEM_CLEAR"
        Case Else:
            Mystring = _Trim$(Str$(MyInteger))
    End Select
    VirtualKeyCodeToString$ = Mystring
End Function ' VirtualKeyCodeToString$

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END CONSTANT TO STRING FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

' /////////////////////////////////////////////////////////////////////////////
' SMcNeill
' More efficient version of ExtendedTimer.
' This stores our day values and only updates them when necessary.
' We really don't need to figure out how many seconds are in today over and over endlessly
' -- just count them once, and when the clock swaps back to 0:0:0, add 24*60*60 seconds to the count.
' Re: how to time something (ie do loop for n seconds)
' https://forum.qb64.org/index.php?topic=4682.0

Function ExtendedTimer##
    'modified extendedtimer to store the old day's count, and not have to recalculate it every time the routine is called.

    Static olds As _Float, old_day As _Float
    Dim m As Integer, d As Integer, y As Integer
    Dim s As _Float, day As String
    Dim oldt As Single
    If olds = 0 Then 'calculate the day the first time the extended timer runs
        day = Date$
        m = Val(Left$(day, 2))
        d = Val(Mid$(day, 4, 2))
        y = Val(Right$(day, 4)) - 1970
        Select Case m 'Add the number of days for each previous month passed
            Case 2: d = d + 31
            Case 3: d = d + 59
            Case 4: d = d + 90
            Case 5: d = d + 120
            Case 6: d = d + 151
            Case 7: d = d + 181
            Case 8: d = d + 212
            Case 9: d = d + 243
            Case 10: d = d + 273
            Case 11: d = d + 304
            Case 12: d = d + 334
        End Select
        If (y Mod 4) = 2 And m > 2 Then d = d + 1 'add a day if this is leap year and we're past february
        d = (d - 1) + 365 * y 'current month days passed + 365 days per each standard year
        d = d + (y + 2) \ 4 'add in days for leap years passed
        s = d * 24 * 60 * 60 'Seconds are days * 24 hours * 60 minutes * 60 seconds
        old_day = s
    End If
    If Timer < oldt Then 'we went from 23:59:59 (a second before midnight) to 0:0:0 (midnight)
        old_day = s + 83400 'add another worth of seconds to our counter
    End If
    oldt = Timer
    olds = old_day + oldt
    ExtendedTimer## = olds
End Function ' ExtendedTimer##

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function
' Format: {YYYY}-{MM}-{DD} {hh}:[mm}:{ss}

' Uses:
'     TIME$
'         The TIME$ Function returns a STRING representation
'         of the current computer time in a 24 hour format.
'         https://qb64phoenix.com/qb64wiki/index.php/TIME$
'     DATE$
'         The DATE$ function returns the current computer date
'         as a string in the format "mm-dd-yyyy".
'         https://qb64phoenix.com/qb64wiki/index.php/DATE$
'
' TODO: support template where
'       {yyyy} = 4 digit year
'       {mm}   = 2 digit month
'       {dd}   = 2 digit day
'       {hh}   = 2 digit hour (12-hour)
'       {rr}   = 2 digit hour (24-hour)
'       {nn}   = 2 digit minute
'       {ss}   = 2 digit second
'       {ampm} = AM/PM

' We got the nn for minute from Microsoft > Office VBA Reference > DateDiff function
' https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/datediff-function

' PRINT "Current date time (simple format) = " + Chr$(34) + GetCurrentDateTime$("{yyyy}-{mm}-{dd} {rr}:{nn}:{ss}") + Chr$(34)
' PRINT "Current date time (US format)     = " + Chr$(34) + GetCurrentDateTime$("{mm}/{dd}/{yyyy} {hh}:{nn}:{ss} {ampm}") + Chr$(34)
' PRINT "Filename timestamp                = " + Chr$(34) + GetCurrentDateTime$("{yyyy}{mm}{dd}_{rr}{nn}{ss}") + Chr$(34)

Function GetCurrentDateTime$ (sTemplate$)
    Dim sDate$: sDate$ = Date$
    Dim sTime$: sTime$ = Time$
    Dim sYYYY$: sYYYY$ = Mid$(sDate$, 7, 4)
    Dim sMM$: sMM$ = Mid$(sDate$, 1, 2)
    Dim sDD$: sDD$ = Mid$(sDate$, 4, 2)
    Dim sHH24$: sHH24$ = Mid$(sTime$, 1, 2)
    Dim sHH$: sHH$ = ""
    Dim sMI$: sMI$ = Mid$(sTime$, 4, 2)
    Dim sSS$: sSS$ = Mid$(sTime$, 7, 2)
    Dim iHour%: iHour% = Val(sHH24$)
    Dim sAMPM$: sAMPM$ = ""
    Dim result$: result$ = ""

    ' FIGURE OUT AM/PM
    If InStr(sTemplate$, "{ampm}") > 0 Then
        If iHour% = 0 Then
            sAMPM$ = "AM"
            iHour% = 12
        ElseIf iHour% > 0 And iHour% < 12 Then
            sAMPM$ = "AM"
        ElseIf iHour% = 12 Then
            sAMPM$ = "PM"
        Else
            sAMPM$ = "PM"
            iHour% = iHour% - 12
        End If
        sHH$ = Right$("00" + _Trim$(Str$(iHour%)), 2)
    End If

    ' POPULATE TEMPLATE
    result$ = sTemplate$
    result$ = Replace$(result$, "{yyyy}", sYYYY$)
    result$ = Replace$(result$, "{mm}", sMM$)
    result$ = Replace$(result$, "{dd}", sDD$)
    result$ = Replace$(result$, "{hh}", sHH$)
    result$ = Replace$(result$, "{rr}", sHH24$)
    result$ = Replace$(result$, "{nn}", sMI$)
    result$ = Replace$(result$, "{ss}", sSS$)
    result$ = Replace$(result$, "{ampm}", sAMPM$)

    ' RETURN RESULT
    GetCurrentDateTime$ = result$
End Function ' GetCurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' 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%

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for integers

Function IIF (Condition, IfTrue, IfFalse)
    If Condition Then IIF = IfTrue Else IIF = IfFalse
End Function

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for strings

Function IIFS$ (Condition, IfTrue$, IfFalse$)
    If Condition Then IIFS$ = IfTrue$ Else IIFS$ = IfFalse$
End Function

' /////////////////////////////////////////////////////////////////////////////
' 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%

' /////////////////////////////////////////////////////////////////////////////
' FROM: String Manipulation
' found at abandoned, outdated and now likely malicious qb64 dot net website
' http://www.qb64.[net]/forum/index_topic_5964-0/
'
'SUMMARY:
'   Purpose:  A library of custom functions that transform strings.
'   Author:   Dustinian Camburides (dustinian@gmail.com)
'   Platform: QB64 (www.qb64.org)
'   Revision: 1.6
'   Updated:  5/28/2012

'SUMMARY:
'[Replace$] replaces all instances of the [Find] sub-string with the [Add] sub-string within the [Text] string.
'INPUT:
'Text: The input string; the text that's being manipulated.
'Find: The specified sub-string; the string sought within the [Text] string.
'Add: The sub-string that's being added to the [Text] string.

Function Replace$ (Text1 As String, Find1 As String, Add1 As String)
    ' VARIABLES:
    Dim Text2 As String
    Dim Find2 As String
    Dim Add2 As String
    Dim lngLocation As Long ' The address of the [Find] substring within the [Text] string.
    Dim strBefore As String ' The characters before the string to be replaced.
    Dim strAfter As String ' The characters after the string to be replaced.

    ' INITIALIZE:
    ' MAKE COPIESSO THE ORIGINAL IS NOT MODIFIED (LIKE ByVal IN VBA)
    Text2 = Text1
    Find2 = Find1
    Add2 = Add1

    lngLocation = InStr(1, Text2, Find2)

    ' PROCESSING:
    ' While [Find2] appears in [Text2]...
    While lngLocation
        ' Extract all Text2 before the [Find2] substring:
        strBefore = Left$(Text2, lngLocation - 1)

        ' Extract all text after the [Find2] substring:
        strAfter = Right$(Text2, ((Len(Text2) - (lngLocation + Len(Find2) - 1))))

        ' Return the substring:
        Text2 = strBefore + Add2 + strAfter

        ' Locate the next instance of [Find2]:
        lngLocation = InStr(1, Text2, Find2)

        ' Next instance of [Find2]...
    Wend

    ' OUTPUT:
    Replace$ = Text2
End Function ' Replace$

' /////////////////////////////////////////////////////////////////////////////
' 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 As Integer
    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

'' /////////////////////////////////////////////////////////////////////////////
'
'Sub SplitTest
'    Dim in$
'    Dim delim$
'    ReDim arrText$(0)
'    Dim iLoop%
'
'    delim$ = Chr$(10)
'    in$ = "this" + delim$ + "is" + delim$ + "a" + delim$ + "test"
'    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
'    Print "delim$ = " + Chr$(34) + delim$ + Chr$(34)
'    split in$, delim$, arrText$()
'
'    For iLoop% = LBound(arrText$) To UBound(arrText$)
'        Print "arrText$(" + _Trim$(Str$(iLoop%)) + ") = " + Chr$(34) + arrText$(iLoop%) + Chr$(34)
'    Next iLoop%
'    Print
'    Print "Split test finished."
'End Sub ' SplitTest

'' /////////////////////////////////////////////////////////////////////////////
'
'Sub SplitAndReplaceTest
'    Dim in$
'    Dim out$
'    Dim iLoop%
'    ReDim arrText$(0)
'
'    Print "-------------------------------------------------------------------------------"
'    Print "SplitAndReplaceTest"
'    Print
'
'    Print "Original value"
'    in$ = "This line 1 " + Chr$(13) + Chr$(10) + "and line 2" + Chr$(10) + "and line 3 " + Chr$(13) + "finally THE END."
'    out$ = in$
'    out$ = Replace$(out$, Chr$(13), "\r")
'    out$ = Replace$(out$, Chr$(10), "\n")
'    out$ = Replace$(out$, Chr$(9), "\t")
'    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
'    Print
'
'    Print "Fixing linebreaks..."
'    in$ = Replace$(in$, Chr$(13) + Chr$(10), Chr$(13))
'    in$ = Replace$(in$, Chr$(10), Chr$(13))
'    out$ = in$
'    out$ = Replace$(out$, Chr$(13), "\r")
'    out$ = Replace$(out$, Chr$(10), "\n")
'    out$ = Replace$(out$, Chr$(9), "\t")
'    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
'    Print
'
'    Print "Splitting up..."
'    split in$, Chr$(13), arrText$()
'
'    For iLoop% = LBound(arrText$) To UBound(arrText$)
'        out$ = arrText$(iLoop%)
'        out$ = Replace$(out$, Chr$(13), "\r")
'        out$ = Replace$(out$, Chr$(10), "\n")
'        out$ = Replace$(out$, Chr$(9), "\t")
'        Print "arrText$(" + cstr$(iLoop%) + ") = " + Chr$(34) + out$ + Chr$(34)
'    Next iLoop%
'    Print
'
'    Print "SplitAndReplaceTest finished."
'End Sub ' SplitAndReplaceTest

' /////////////////////////////////////////////////////////////////////////////
' Use to pretty print TRUE and FALSE values.

Function TrueFalse$ (myValue)
    If myValue = TRUE Then
        TrueFalse$ = "TRUE"
    Else
        TrueFalse$ = "FALSE"
    End If
End Function ' TrueFalse$

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN handle MEM for any type
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' QB64 GPT Just Rewrote My Code
' https://qb64phoenix.com/forum/showthread.php?tid=2728

' And the revisions QB64 GPT made (after minor changes and me asking it to redo some syntax errors):
' It separated out a lot of processing out to separate subs.
' It is quite impressive how little input I had to give it to fix its mistakes.
' The code actually worked just as well as it did before the changes, which blows my mind.
' It actually even listened to me when I told it that it would need to cast an OFFSET type by using VAL(STR$(var)).
' To be fair, I had to tell it "ByRef" was invalid and a couple of other things.
' I also had to declare "y" each time it was used. But the last iteration only required me to declare "y".
' I think that is a decent enough result. Too bad I can't get it to be this good every time.
' 1) This is the paid version of GPT4. I am on the plus plan, so whatever that one has.
' 2) I think I deleted the session. Sorry. I only used it for as long as I needed it.
' 3) I don't know what the hard limit is. It's in "tokens", which I have no idea how those are calculated.
' I got a pretty large source code from one output and it can take a lot of input. I would just say it can handle quite a bit.
' The GPT I used was one I trained using the Wiki, sample code, etc. At the time, it used GPT4.
' Custom GPTs now use 4o. I will probably need to republish it to take advantage of 4o for it.
' I guess training is the wrong word. A custom GPT has a "knowledge base".
' You can have a maximum of 20 files.
' It can use those files to create an answer. Even a zip folder can be used.
' It will basically only use the knowledge base when specifically asked. Otherwise, it is using whatever it already had in its model.
' As for testing code and such, you can create "actions" for your GPT that allow it to do things outside of ChatGPT, including REST API.
' So if dbox ever made a REST API for QBJS, you could definitely have it write QBJS code and then ask it to run it.

Sub anyArg (args() As _MEM)
    Dim As _Unsigned Integer x, y
    Dim As _Unsigned _Offset z
    Dim As _Unsigned Long size, elementsize

    For x = LBound(args) To UBound(args)
        If _MemExists(args(x)) Then
            z = 0
            size = Val(Str$(args(x).SIZE))
            elementsize = Val(Str$(args(x).ELEMENTSIZE))

            If _ReadBit(args(x).TYPE, 7) And _ReadBit(args(x).TYPE, 13) = 0 Then
                HandleNumericType args(x), size, elementsize, z
            ElseIf _ReadBit(args(x).TYPE, 8) Then
                HandleFloatingType args(x), size, elementsize, z
            ElseIf _ReadBit(args(x).TYPE, 9) Then
                HandleStringType args(x), size, elementsize
            ElseIf _ReadBit(args(x).TYPE, 13) And _ReadBit(args(x).TYPE, 7) Then
                HandleOffsetType args(x), size, elementsize, z
            ElseIf args(x).TYPE = 0 And args(x).SIZE > 0 Then
                HandleSoundType args(x)
            ElseIf _ReadBit(args(x).TYPE, 14) Then
                Print args(x).SIZE, "MEM"
                ' TODO: Handle other types if necessary
            End If

            If _ReadBit(args(x).TYPE, 11) Then
                Screen args(x).IMAGE
            End If
        End If
    Next
End Sub ' anyArg

' Subroutines for handling specific types
Sub HandleNumericType (arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset)
    If _ReadBit(arg.TYPE, 10) Then
        If _ReadBit(arg.TYPE, 16) Then
            Select Case elementsize
                Case 1
                    Dim As _Unsigned _Byte unsignedbytearray(1 To (size / elementsize))
                    ProcessArray_UByte unsignedbytearray(), arg, size, elementsize, z, "UBYTE ARRAY"
                Case 2
                    Dim As _Unsigned Integer unsignedintarray(1 To (size / elementsize))
                    ProcessArray_UInteger unsignedintarray(), arg, size, elementsize, z, "USHORT ARRAY"
                Case 4
                    Dim As _Unsigned Long unsignedlongarray(1 To (size / elementsize))
                    ProcessArray_ULong unsignedlongarray(), arg, size, elementsize, z, "ULONG ARRAY"
                Case 8
                    Dim As _Unsigned _Integer64 unsignedint64array(1 To (size / elementsize))
                    ProcessArray_UInt64 unsignedint64array(), arg, size, elementsize, z, "UINT64 ARRAY"
            End Select
        Else
            PrintSingleValue arg, size, elementsize
        End If
    Else
        If _ReadBit(arg.TYPE, 16) Then
            Select Case elementsize
                Case 1
                    Dim As _Byte bytearray(1 To (size / elementsize))
                    ProcessArray_Byte bytearray(), arg, size, elementsize, z, "BYTE ARRAY"
                Case 2
                    Dim As Integer intarray(1 To (size / elementsize))
                    ProcessArray_Integer intarray(), arg, size, elementsize, z, "SHORT ARRAY"
                Case 4
                    Dim As Long longarray(1 To (size / elementsize))
                    ProcessArray_Long longarray(), arg, size, elementsize, z, "LONG ARRAY"
                Case 8
                    Dim As _Integer64 int64array(1 To (size / elementsize))
                    ProcessArray_Int64 int64array(), arg, size, elementsize, z, "INT64 ARRAY"
            End Select
        Else
            PrintSingleValue arg, size, elementsize
        End If
    End If
End Sub ' HandleNumericType

Sub HandleFloatingType (arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset)
    If _ReadBit(arg.TYPE, 16) Then
        Select Case elementsize
            Case 4
                Dim As Single singlearray(1 To (size / elementsize))
                ProcessArray_Single singlearray(), arg, size, elementsize, z, "SINGLE ARRAY"
            Case 8
                Dim As Double doublearray(1 To (size / elementsize))
                ProcessArray_Double doublearray(), arg, size, elementsize, z, "DOUBLE ARRAY"
            Case 32
                Dim As _Float floatarray(1 To (size / elementsize))
                ProcessArray_Float floatarray(), arg, size, elementsize, z, "FLOAT ARRAY"
        End Select
    Else
        Select Case size
            Case 4
                Print _MemGet(arg, arg.OFFSET, Single), "SINGLE"
            Case 8
                Print _MemGet(arg, arg.OFFSET, Double), "DOUBLE"
            Case 32
                Print _MemGet(arg, arg.OFFSET, _Float), "FLOAT"
        End Select
    End If
End Sub ' HandleFloatingType

Sub HandleStringType (arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long)
    If _ReadBit(arg.TYPE, 16) Then
        Dim As String stringarray(1 To (size / elementsize))
        Dim As _Unsigned Long y
        For y = LBound(stringarray) To UBound(stringarray)
            stringarray(y) = Space$(elementsize)
            _MemGet arg, (arg.OFFSET) + (y * elementsize - elementsize), stringarray(y)
            Print stringarray(y), "STRING ARRAY"
        Next
    Else
        Dim As String stringtest: stringtest = Space$(elementsize)
        _MemGet arg, arg.OFFSET, stringtest
        Print stringtest
    End If
End Sub ' HandleStringType

Sub HandleOffsetType (arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset)
    If _ReadBit(arg.TYPE, 10) Then
        If _ReadBit(arg.TYPE, 16) Then
            Dim As _Unsigned _Offset unsignedoffsetarray(1 To (size / elementsize))
            ProcessArray_UOffset unsignedoffsetarray(), arg, size, elementsize, z, "ULONG_PTR ARRAY"
        Else
            Print _MemGet(arg, arg.OFFSET, _Unsigned _Offset), "ULONG_PTR"
        End If
    Else
        If _ReadBit(arg.TYPE, 16) Then
            Dim As _Offset offsetarray(1 To (size / elementsize))
            ProcessArray_Offset offsetarray(), arg, size, elementsize, z, "LONG_PTR ARRAY"
        Else
            Print _MemGet(arg, arg.OFFSET, _Offset), "LONG_PTR"
        End If
    End If
End Sub ' HandleOffsetType

Sub HandleSoundType (arg As _MEM)
    If Not _SndPlaying(arg.SOUND) Then
        _SndPlay (arg.SOUND)
    End If
    Print "SOUND", arg.SIZE, arg.ELEMENTSIZE
End Sub ' HandleSoundType

' Subroutines for processing arrays
Sub ProcessArray_UByte (unsignedbytearray() As _Unsigned _Byte, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(unsignedbytearray) To UBound(unsignedbytearray)
        _MemGet arg, arg.OFFSET + z, unsignedbytearray(y)
        z = z + elementsize
        Print unsignedbytearray(y), typeName
    Next
End Sub ' ProcessArray_UByte

Sub ProcessArray_UInteger (unsignedintarray() As _Unsigned Integer, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(unsignedintarray) To UBound(unsignedintarray)
        _MemGet arg, arg.OFFSET + z, unsignedintarray(y)
        z = z + elementsize
        Print unsignedintarray(y), typeName
    Next
End Sub ' ProcessArray_UInteger

Sub ProcessArray_ULong (unsignedlongarray() As _Unsigned Long, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(unsignedlongarray) To UBound(unsignedlongarray)
        _MemGet arg, arg.OFFSET + z, unsignedlongarray(y)
        z = z + elementsize
        Print unsignedlongarray(y), typeName
    Next
End Sub ' ProcessArray_ULong

Sub ProcessArray_UInt64 (unsignedint64array() As _Unsigned _Integer64, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(unsignedint64array) To UBound(unsignedint64array)
        _MemGet arg, arg.OFFSET + z, unsignedint64array(y)
        z = z + elementsize
        Print unsignedint64array(y), typeName
    Next
End Sub ' ProcessArray_UInt64

Sub ProcessArray_Byte (bytearray() As _Byte, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(bytearray) To UBound(bytearray)
        _MemGet arg, arg.OFFSET + z, bytearray(y)
        z = z + elementsize
        Print bytearray(y), typeName
    Next
End Sub ' ProcessArray_Byte

Sub ProcessArray_Integer (intarray() As Integer, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(intarray) To UBound(intarray)
        _MemGet arg, arg.OFFSET + z, intarray(y)
        z = z + elementsize
        Print intarray(y), typeName
    Next
End Sub ' ProcessArray_Integer

Sub ProcessArray_Long (longarray() As Long, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(longarray) To UBound(longarray)
        _MemGet arg, arg.OFFSET + z, longarray(y)
        z = z + elementsize
        Print longarray(y), typeName
    Next
End Sub ' ProcessArray_Long

Sub ProcessArray_Int64 (int64array() As _Integer64, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(int64array) To UBound(int64array)
        _MemGet arg, arg.OFFSET + z, int64array(y)
        z = z + elementsize
        Print int64array(y), typeName
    Next
End Sub ' ProcessArray_Int64

Sub ProcessArray_Single (singlearray() As Single, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(singlearray) To UBound(singlearray)
        _MemGet arg, arg.OFFSET + z, singlearray(y)
        z = z + elementsize
        Print singlearray(y), typeName
    Next
End Sub ' ProcessArray_Single

Sub ProcessArray_Double (doublearray() As Double, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(doublearray) To UBound(doublearray)
        _MemGet arg, arg.OFFSET + z, doublearray(y)
        z = z + elementsize
        Print doublearray(y), typeName
    Next
End Sub ' ProcessArray_Double

Sub ProcessArray_Float (floatarray() As _Float, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(floatarray) To UBound(floatarray)
        _MemGet arg, arg.OFFSET + z, floatarray(y)
        z = z + elementsize / 2
        Print floatarray(y), typeName
    Next
End Sub ' ProcessArray_Float

Sub ProcessArray_UOffset (unsignedoffsetarray() As _Unsigned _Offset, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(unsignedoffsetarray) To UBound(unsignedoffsetarray)
        _MemGet arg, arg.OFFSET + z, unsignedoffsetarray(y)
        z = z + elementsize
        Print unsignedoffsetarray(y), typeName
    Next
End Sub ' ProcessArray_UOffset

Sub ProcessArray_Offset (offsetarray() As _Offset, arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long, z As _Unsigned _Offset, typeName As String)
    Dim As _Unsigned Long y
    For y = LBound(offsetarray) To UBound(offsetarray)
        _MemGet arg, arg.OFFSET + z, offsetarray(y)
        z = z + elementsize
        Print offsetarray(y), typeName
    Next
End Sub ' ProcessArray_Offset

Sub PrintSingleValue (arg As _MEM, size As _Unsigned Long, elementsize As _Unsigned Long)
    Select Case size
        Case 1
            Print _MemGet(arg, arg.OFFSET, _Byte), "BYTE"
        Case 2
            Print _MemGet(arg, arg.OFFSET, Integer), "SHORT"
        Case 4
            Print _MemGet(arg, arg.OFFSET, Long), "LONG"
        Case 8
            Print _MemGet(arg, arg.OFFSET, _Integer64), "INT64"
    End Select
End Sub ' PrintSingleValue
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END handle MEM for any type
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

Sub DebugLog (sText As String)
    If cDebugEnabled = TRUE Then
        Dim sTime As String
        Dim sResult As String
        ReDim arrLines(0) As String
        Dim iLoop As Integer
        Dim sNextLine As String

        If _FileExists(m_sDebugFile) = FALSE Then
            sResult = PrintFile$(m_sDebugFile, "", FALSE)
        End If

        If Len(sResult) = 0 Then
            sTime = GetCurrentDateTime$("{mm}/{dd}/{yyyy} {hh}:{nn}:{ss} {ampm}")
            split sText, Chr$(13), arrLines()
            For iLoop = LBound(arrLines) To UBound(arrLines)
                sNextLine = sTime + " " + arrLines(iLoop)
                sResult = PrintFile$(m_sDebugFile, sNextLine, TRUE)
            Next iLoop
        End If
    End If
End Sub ' DebugLog

' /////////////////////////////////////////////////////////////////////////////
' 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
' ################################################################################################################################################################

' ################################################################################################################################################################
' BEGIN REFERENCE #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 REFERENCE @REFERENCE
' ################################################################################################################################################################
' @END

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

"winproc.h":
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;
}
Reply




Users browsing this thread: 1 Guest(s)