Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
game input mapping system v1.0 for gamepad, keyboard
#1
This is the first version of my joystick & keyboard mapping + input library (original post).
Version 2 followed, which added a simple GUI (what a pain and interesting project THAT was!)
(I'm currently working on Version 3 which removes the GUI abomination and simplifies things.)

madscijr
game input mapping system v1.0 for gamepad, keyboard
« on: January 07, 2022, 06:43:22 pm »

I needed some basic reusable functionality to map game input from USB game controllers and the keyboard, which could be saved to a config file and reused.
Everything is working now, you can map controls, load/save/edit mappings, test the mapping, or test game controllers.
The input is menu driven, and the menus are very rudimentary, text-based, but it works (at least as far as I can tell).

1. Basic controller test
2. Load controller mapping
3. View controller mapping
4. Edit  controller mapping for 1 or more players
5. Reset controller mapping for 1 or more players
6. Map controllers for 1 or more players
7. Test controller mappings to move around screen
8. Save controller mappings

Code is below and fully self-contained.

Enjoy.

Code: (Select All)
' ################################################################################################################################################################
' #TOP

' Game Input Mapping Test
' Version 1.0 by madscijr

' BASED ON CODE BY SMcNeill FROM:
'     Simple Joystick Detection and Interaction  (Read 316 times)
'     https://www.qb64.org/forum/index.php?topic=2160.msg129051#msg129051
'     https://qb64forum.alephc.xyz/index.php?topic=2160.msg129083#msg129083
' and others (sources cited throughout).

' ################################################################################################################################################################
' #CONSTANTS = GLOBAL CONSTANTS

' boolean constants:
Const FALSE = 0
Const TRUE = Not FALSE

' BEGIN GAME CONTROLLER MAPPING CONSTANTS
Const cInputNone = 0
Const cInputKey = 1
Const cInputButton = 2
Const cInputAxis = 3

Const cMaxButtons = 12
Const cMaxAxis = 8
Const cMaxControllers = 8
Const cMaxPlayers = 8

' Use as index for array of ControlInputType
Const cInputUp = 1
Const cInputDown = 2
Const cInputLeft = 3
Const cInputRight = 4
Const cInputButton1 = 5
Const cInputButton2 = 6
Const cInputButton3 = 7
Const cInputButton4 = 8

Const c_iKeyDown_F10 = 17408
Const c_iKeyHit_AltLeft = -30764
Const c_iKeyHit_AltRight = -30765
' END GAME CONTROLLER MAPPING CONSTANTS

' ################################################################################################################################################################
' #UDT #TYPES = USER DEFINED TYPES

' UDT TO HOLD THE INFO FOR A PLAYER
Type PlayerType
    x As Integer ' player x position
    y As Integer ' player y position
    c As Integer ' character to display on screen
    xOld As Integer
    yOld As Integer

    ' control buffer
    moveX As Integer
    moveY As Integer

    moveUp As Integer
    moveDown As Integer
    moveLeft As Integer
    moveRight As Integer
    button1 As Integer
    button2 As Integer
    button3 As Integer
    button4 As Integer

    ' control previous move
    'lastMoveX As Integer
    'lastMoveY As Integer
    lastMoveUp As Integer
    lastMoveDown As Integer
    lastMoveLeft As Integer
    lastMoveRight As Integer
    lastButton1 As Integer
    lastButton2 As Integer
    lastButton3 As Integer
    lastButton4 As Integer

    'repeat As Integer
End Type ' PlayerType

' UDT TO HOLD THE INFO FOR A GAME CONTROLLER
Type ControllerType
    buttonCount As Integer
    axisCount As Integer
End Type ' ControllerType

' UDT TO HOLD THE INFO FOR A GAME CONTROLLER
Type ControlInputType
    device As Integer
    typ As Integer ' cInputKey, cInputButton, cInputAxis
    code As Integer
    value As Integer
    repeat As Integer
End Type ' ControlInputType

' ################################################################################################################################################################
' #VARS = GLOBAL VARIABLES

' ENABLE / DISABLE DEBUG CONSOLE
Dim Shared m_bTesting As Integer: m_bTesting = TRUE

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

' GAME CONTROLLER MAPPING
Dim Shared m_ControlMapFileName$: m_ControlMapFileName$ = Left$(m_ProgramName$, _InStrRev(m_ProgramName$, ".")) + "map.txt"
ReDim Shared m_arrControlMap(1 To 8, 1 To 8) As ControlInputType ' holds control mapping for each player (player #, direction)
ReDim Shared m_arrController(1 To 8) As ControllerType ' holds info for each game controller
ReDim Shared m_arrButtonCode(1 To 99) As Integer ' Long
ReDim Shared m_arrButtonKey(1 To 99) As String
ReDim Shared m_arrButtonKeyDesc(0 To 512) As String
Dim Shared m_bInitialized As Integer: m_bInitialized = FALSE
Dim Shared m_bHaveMapping As Integer: m_bHaveMapping = FALSE

' USE TO GLOBALLY ENABLE/DISABLE REPEATING INPUT PER FUNCTION
' To enable override set m_bRepeatOverride=TRUE,
' otherwise this can be configured for each individual controller
' when you map the functions.
Dim Shared m_bRepeatOverride As Integer: m_bRepeatOverride = TRUE
Dim Shared m_bRepeatUp As Integer: m_bRepeatUp = TRUE
Dim Shared m_bRepeatDown As Integer: m_bRepeatDown = TRUE
Dim Shared m_bRepeatLeft As Integer: m_bRepeatLeft = FALSE
Dim Shared m_bRepeatRight As Integer: m_bRepeatRight = FALSE
Dim Shared m_bRepeatButton1 As Integer: m_bRepeatButton1 = TRUE
Dim Shared m_bRepeatButton2 As Integer: m_bRepeatButton2 = TRUE
Dim Shared m_bRepeatButton3 As Integer: m_bRepeatButton3 = FALSE
Dim Shared m_bRepeatButton4 As Integer: m_bRepeatButton4 = FALSE

' VARIABLES FOR GRAPHIC PRINTING ROUTINES
Dim Shared m_NumColumns As Integer: m_NumColumns = 1
Dim Shared m_PrintRow As Integer: m_PrintRow = 0
Dim Shared m_PrintCol As Integer: m_PrintCol = 0
Dim Shared m_StartRow As Integer: m_StartRow = 0
Dim Shared m_EndRow As Integer: m_EndRow = 0
Dim Shared m_StartCol As Integer: m_StartCol = 0
Dim Shared m_EndCol As Integer: m_EndCol = 0

' DEMO GAME / TESTING
ReDim Shared m_arrPlayer(1 To 8) As PlayerType ' holds info for each player

' =============================================================================
' LOCAL VARIABLES
Dim in$

' ****************************************************************************************************************************************************************
' ACTIVATE DEBUGGING WINDOW
If m_bTesting = TRUE Then
    $Console
    _Delay 4
    _Console On
    _Echo "Started " + m_ProgramName$
    _Echo "Debugging on..."
End If
' ****************************************************************************************************************************************************************

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

' =============================================================================
' FINISH
Screen 0
Print m_ProgramName$ + " finished."
Input "Press <ENTER> to continue", in$

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

System ' return control to the operating system
End

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

Sub main
    Dim RoutineName As String: RoutineName = "main"
    Dim in$
    Dim result$: result$ = ""

    ' SET UP SCREEN
    Screen _NewImage(1280, 1024, 32): _ScreenMove 0, 0

    Do
        If Len(result$) = 0 Then
            Cls
        Else
            Print
        End If

        Print m_ProgramName$
        Print
        Print "Game Input Mapping Test"
        Print "v1.0, by Softintheheadware (Jan, 2022)"
        Print

        Print "1. Basic controller test"
        Print "2. Load controller mapping"
        Print "3. View controller mapping"
        Print "4. Edit  controller mapping for 1 or more players"
        Print "5. Reset controller mapping for 1 or more players"
        Print "6. Map controllers for 1 or more players"
        Print "7. Test controller mappings to move around screen"
        Print "8. Save controller mappings"
        Print
        Print "What to do? ('q' to exit)"

        Input in$: in$ = LCase$(Left$(in$, 1))

        If in$ = "1" Then
            result$ = TestJoysticks$
        ElseIf in$ = "2" Then
            result$ = LoadMappings$
            If Len(result$) = 0 Then result$ = "Loaded mappings."
        ElseIf in$ = "3" Then
            result$ = ViewMappings$
        ElseIf in$ = "4" Then
            result$ = EditMappings$
        ElseIf in$ = "5" Then
            result$ = ResetMapping$
        ElseIf in$ = "6" Then
            result$ = MapInput$
        ElseIf in$ = "7" Then
            result$ = TestMappings$
        ElseIf in$ = "8" Then
            result$ = SaveMappings$
        End If

        If Len(result$) > 0 Then
            Print result$
        End If

    Loop Until in$ = "q"

    ' RETURN TO TEXT SCREEN
    Screen 0

End Sub ' main

' /////////////////////////////////////////////////////////////////////////////
' TODO: get keyboard input working
' TODO: get continuous movement working for digital joysticks
' TODO: adjust analog joystick sensitivity

Function TestMappings$
    Dim sResult As String: sResult = ""
    Dim sError As String: sError = ""

    Dim iDeviceCount As Integer
    Dim iDevice As Integer
    Dim iNumControllers As Integer
    Dim iController As Integer
    Dim iValue As Integer
    Dim iWhichInput As Integer

    Dim arrButton(32, 16) As Integer ' number of buttons on the joystick
    Dim arrButtonNew(32, 16) As Integer ' tracks when to initialize values
    Dim arrAxis(32, 16) As Double ' number of axis on the joystick
    Dim arrAxisNew(32, 16) As Integer ' tracks when to initialize values

    Dim iCols As Integer
    Dim iRows As Integer

    Dim iPlayer As Integer
    Dim iNextY As Integer
    Dim iNextX As Integer
    Dim iNextC As Integer

    Dim iMinX As Integer
    Dim iMaxX As Integer
    Dim iMinY As Integer
    Dim iMaxY As Integer

    Dim bHaveInput As Integer
    Dim bFinished As Integer
    Dim bFoundWho As Integer
    Dim bRepeat As Integer

    Dim in$

    ' MAKE SURE WE HAVE MAPPING
    If m_bHaveMapping = TRUE Then
        ' INITIALIZE
        Cls
        InitKeyboardButtonCodes
        iCols = _Width(0) \ _FontWidth
        iRows = _Height(0) \ _FontHeight
        iMinX = 0: iMaxX = iCols
        iMinY = 0: iMaxY = iRows

        ' INITIALIZE PLAYER COORDINATES AND SCREEN CHARACTERS
        iNextY = 1
        iNextX = -3
        iNextC = 64

        For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
            iNextX = iNextX + 4
            If iNextX >= iMaxX Then
                iNextX = iMinX
                iNextY = iNextY + 4
                If iNextY > iMaxY Then
                    iNextY = iMinY
                End If
            End If
            iNextC = iNextC + 1
            m_arrPlayer(iPlayer).x = iNextX
            m_arrPlayer(iPlayer).y = iNextY
            m_arrPlayer(iPlayer).c = iNextC
            m_arrPlayer(iPlayer).xOld = iNextX
            m_arrPlayer(iPlayer).yOld = iNextY

            m_arrPlayer(iPlayer).moveX = 0
            m_arrPlayer(iPlayer).moveY = 0

            m_arrPlayer(iPlayer).moveUp = FALSE
            m_arrPlayer(iPlayer).moveDown = FALSE
            m_arrPlayer(iPlayer).moveLeft = FALSE
            m_arrPlayer(iPlayer).moveRight = FALSE
            m_arrPlayer(iPlayer).button1 = FALSE
            m_arrPlayer(iPlayer).button2 = FALSE
            m_arrPlayer(iPlayer).button3 = FALSE
            m_arrPlayer(iPlayer).button4 = FALSE

            m_arrPlayer(iPlayer).lastMoveUp = FALSE
            m_arrPlayer(iPlayer).lastMoveDown = FALSE
            m_arrPlayer(iPlayer).lastMoveLeft = FALSE
            m_arrPlayer(iPlayer).lastMoveRight = FALSE
            m_arrPlayer(iPlayer).lastButton1 = FALSE
            m_arrPlayer(iPlayer).lastButton2 = FALSE
            m_arrPlayer(iPlayer).lastButton3 = FALSE
            m_arrPlayer(iPlayer).lastButton4 = FALSE
        Next iPlayer

        ' COUNT # OF JOYSTICKS
        ' TODO: find out the right way to count joysticks
        If Len(sError) = 0 Then
            ' D= _DEVICES ' MUST be read in order for other 2 device functions to work!
            iDeviceCount = _Devices ' Find the number of devices on someone's system

            If iDeviceCount > 2 Then
                ' LIMIT # OF DEVICES, IF THERE IS A LIMIT DEFINED
                iNumControllers = iDeviceCount - 2
                If cMaxControllers > 0 Then
                    If iNumControllers > cMaxControllers Then
                        iNumControllers = cMaxControllers
                    End If
                End If
            Else
                ' ONLY 2 FOUND (KEYBOARD, MOUSE)
                'sError = "No game controllers found."
                iNumControllers = 0
            End If
        End If

        ' INITIALIZE CONTROLLER DATA
        If Len(sError) = 0 Then
            For iController = 1 To iNumControllers
                m_arrController(iController).buttonCount = cMaxButtons
                m_arrController(iController).axisCount = cMaxAxis
                For iLoop = 1 To cMaxButtons
                    arrButtonNew(iController, iLoop) = TRUE
                Next iLoop
                For iLoop = 1 To cMaxAxis
                    arrAxisNew(iController, iLoop) = TRUE
                Next iLoop
            Next iController
        End If

        ' INITIALIZE CONTROLLER INPUT
        If Len(sError) = 0 Then
            _KeyClear: _Delay 1
            For iController = 1 To iNumControllers
                iDevice = iController + 2
                While _DeviceInput(iDevice) ' clear and update the device buffer
                    For iLoop = 1 To _LastButton(iDevice)
                        If (iLoop > cMaxButtons) Then Exit For
                        m_arrController(iController).buttonCount = iLoop
                        arrButton(iController, iLoop) = FALSE
                    Next iLoop
                    For iLoop = 1 To _LastAxis(iDevice) ' this loop checks all my axis
                        If (iLoop > cMaxAxis) Then Exit For
                        m_arrController(iController).axisCount = iLoop
                        arrAxis(iController, iLoop) = 0
                    Next iLoop
                Wend ' clear and update the device buffer
            Next iController
        End If

        ' GET INPUT AND MOVE PLAYERS AROUND ON SCREEN
        _KeyClear: _Delay 1
        bFinished = FALSE
        Do
            ' Clear control buffer for players
            For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
                m_arrPlayer(iPlayer).moveUp = FALSE
                m_arrPlayer(iPlayer).moveDown = FALSE
                m_arrPlayer(iPlayer).moveLeft = FALSE
                m_arrPlayer(iPlayer).moveRight = FALSE
                m_arrPlayer(iPlayer).button1 = FALSE
                m_arrPlayer(iPlayer).button2 = FALSE
                m_arrPlayer(iPlayer).button3 = FALSE
                m_arrPlayer(iPlayer).button4 = FALSE
            Next iPlayer

            ' -----------------------------------------------------------------------------
            ' BEGIN CHECK FOR CONTROLLER INPUT
            If iNumControllers > 0 Then
                For iController = 1 To iNumControllers
                    iDevice = iController + 2

                    ' Check all devices
                    While _DeviceInput(iDevice)
                    Wend ' clear and update the device buffer

                    ' Check each button
                    For iLoop = 1 To _LastButton(iDevice)
                        If (iLoop > cMaxButtons) Then Exit For

                        ' update button array to indicate if a button is up or down currently.
                        'if TRUE=TRUE then
                        If _ButtonChange(iLoop) Then
                            iValue = _Button(iLoop)
                            If iValue <> arrButton(iController, iLoop) Then
                                ' *****************************************************************************
                                ' PRESSED BUTTON

                                ' BEGIN find who this is mapped for
                                bFoundWho = FALSE
                                For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
                                    For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                                        If m_arrControlMap(iPlayer, iWhichInput).device = iDevice Then
                                            If m_arrControlMap(iPlayer, iWhichInput).typ = cInputButton Then
                                                If m_arrControlMap(iPlayer, iWhichInput).code = iLoop Then
                                                    'if m_arrControlMap(iPlayer, iWhichInput).value = iValue then
                                                    bFoundWho = TRUE
                                                    Select Case iWhichInput
                                                        Case cInputUp:
                                                            m_arrPlayer(iPlayer).moveUp = TRUE
                                                        Case cInputDown:
                                                            m_arrPlayer(iPlayer).moveDown = TRUE
                                                        Case cInputLeft:
                                                            m_arrPlayer(iPlayer).moveLeft = TRUE
                                                        Case cInputRight:
                                                            m_arrPlayer(iPlayer).moveRight = TRUE
                                                        Case cInputButton1:
                                                            m_arrPlayer(iPlayer).button1 = TRUE
                                                        Case cInputButton2:
                                                            m_arrPlayer(iPlayer).button2 = TRUE
                                                        Case cInputButton3:
                                                            m_arrPlayer(iPlayer).button3 = TRUE
                                                        Case cInputButton4:
                                                            m_arrPlayer(iPlayer).button4 = TRUE
                                                        Case Else:
                                                            '(IGNORE)
                                                    End Select
                                                    Exit For
                                                    'end if
                                                End If
                                            End If
                                        End If
                                    Next iWhichInput
                                    If bFoundWho = TRUE Then Exit For
                                Next iPlayer
                                ' END find who this is mapped for

                            End If
                        End If
                    Next iLoop

                    ' Check each axis
                    For iLoop = 1 To _LastAxis(iDevice)
                        If (iLoop > cMaxAxis) Then Exit For
                        dblNextAxis = _Axis(iLoop)
                        dblNextAxis = RoundUpDouble#(dblNextAxis, 3)

                        ' I like to give a little "jiggle" resistance to my controls, as I have an old joystick
                        ' which is prone to always give minute values and never really center on true 0.
                        ' A value of 1 means my axis is pushed fully in one direction.
                        ' A value greater than 0.1 means it's been partially pushed in a direction (such as at a 45 degree diagional angle).
                        ' A value of less than 0.1 means we count it as being centered. (As if it was 0.)

                        ' Set sensitivity:
                        'These are way too sensitive for analog:
                        'IF ABS(_AXIS(iLoop)) <= 1 AND ABS(_AXIS(iLoop)) >= .1 THEN
                        'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= .01 THEN
                        'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= .001 THEN
                        ''For digital input, we'll use a big picture:
                        'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= 0.75 THEN
                        If Abs(dblNextAxis) <= 1 And Abs(dblNextAxis) >= 0.5 Then

                            ' WE WANT CONTINUOUS MOVEMENT (DISABLE FOR NOT)
                            'if TRUE=TRUE then
                            If dblNextAxis <> arrAxis(iController, iLoop) Then
                                ' *****************************************************************************
                                ' MOVED STICK

                                ' convert to a digital value
                                If dblNextAxis < 0 Then
                                    iValue = -1
                                Else
                                    iValue = 1
                                End If

                                ' BEGIN find who this is mapped for
                                bFoundWho = FALSE
                                For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
                                    For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                                        If m_arrControlMap(iPlayer, iWhichInput).device = iDevice Then
                                            If m_arrControlMap(iPlayer, iWhichInput).typ = cInputAxis Then
                                                If m_arrControlMap(iPlayer, iWhichInput).code = iLoop Then
                                                    If m_arrControlMap(iPlayer, iWhichInput).value = iValue Then
                                                        bFoundWho = TRUE
                                                        Select Case iWhichInput
                                                            Case cInputUp:
                                                                m_arrPlayer(iPlayer).moveUp = TRUE
                                                            Case cInputDown:
                                                                m_arrPlayer(iPlayer).moveDown = TRUE
                                                            Case cInputLeft:
                                                                m_arrPlayer(iPlayer).moveLeft = TRUE
                                                            Case cInputRight:
                                                                m_arrPlayer(iPlayer).moveRight = TRUE
                                                            Case cInputButton1:
                                                                m_arrPlayer(iPlayer).button1 = TRUE
                                                            Case cInputButton2:
                                                                m_arrPlayer(iPlayer).button2 = TRUE
                                                            Case cInputButton3:
                                                                m_arrPlayer(iPlayer).button3 = TRUE
                                                            Case cInputButton4:
                                                                m_arrPlayer(iPlayer).button4 = TRUE
                                                            Case Else:
                                                                '(IGNORE)
                                                        End Select
                                                        Exit For
                                                    End If
                                                End If
                                            End If
                                        End If
                                    Next iWhichInput
                                    If bFoundWho = TRUE Then Exit For
                                Next iPlayer
                                ' END find who this is mapped for

                            End If
                        End If
                    Next iLoop

                Next iController
            End If
            ' END CHECK FOR CONTROLLER INPUT
            ' -----------------------------------------------------------------------------

            ' -----------------------------------------------------------------------------
            ' BEGIN CHECK FOR KEYBOARD INPUT #1
            '_KEYCLEAR: _DELAY 1
            While _DeviceInput(1): Wend ' clear and update the keyboard buffer

            ' Detect changed key state
            iDevice = 1 ' keyboard
            For iLoop = LBound(m_arrButtonCode) To UBound(m_arrButtonCode)
                iCode = m_arrButtonCode(iLoop)
                If _Button(iCode) <> FALSE Then
                    ' *****************************************************************************
                    ' PRESSED KEYBOARD
                    'PRINT "PRESSED " + m_arrButtonKey(iLoop)

                    ' BEGIN find who this is mapped for
                    bFoundWho = FALSE
                    For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
                        For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                            If m_arrControlMap(iPlayer, iWhichInput).device = iDevice Then
                                If m_arrControlMap(iPlayer, iWhichInput).typ = cInputKey Then
                                    'if m_arrControlMap(iPlayer, iWhichInput).code = iLoop then
                                    If m_arrControlMap(iPlayer, iWhichInput).code = iCode Then
                                        'if m_arrControlMap(iPlayer, iWhichInput).value = iValue then
                                        bFoundWho = TRUE
                                        Select Case iWhichInput
                                            Case cInputUp:
                                                m_arrPlayer(iPlayer).moveUp = TRUE
                                            Case cInputDown:
                                                m_arrPlayer(iPlayer).moveDown = TRUE
                                            Case cInputLeft:
                                                m_arrPlayer(iPlayer).moveLeft = TRUE
                                            Case cInputRight:
                                                m_arrPlayer(iPlayer).moveRight = TRUE
                                            Case cInputButton1:
                                                m_arrPlayer(iPlayer).button1 = TRUE
                                            Case cInputButton2:
                                                m_arrPlayer(iPlayer).button2 = TRUE
                                            Case cInputButton3:
                                                m_arrPlayer(iPlayer).button3 = TRUE
                                            Case cInputButton4:
                                                m_arrPlayer(iPlayer).button4 = TRUE
                                            Case Else:
                                                '(IGNORE)
                                        End Select
                                        Exit For
                                        'end if
                                    End If
                                End If
                            End If
                        Next iWhichInput
                        If bFoundWho = TRUE Then Exit For
                    Next iPlayer
                    ' END find who this is mapped for

                End If
            Next iLoop
            ' END CHECK FOR KEYBOARD INPUT #1
            ' -----------------------------------------------------------------------------

            ' NOW DRAW PLAYERS ON SCREEN
            For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)

                ' -----------------------------------------------------------------------------
                ' BEGIN UPDATE MOVEMENT CONTROL STATES
                ' If repeating keys are disabled then
                ' disable until the key has been released

                If m_arrControlMap(iPlayer, cInputUp).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).moveUp = TRUE Then
                        If m_arrPlayer(iPlayer).lastMoveUp = TRUE Then
                            m_arrPlayer(iPlayer).moveUp = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastMoveUp = FALSE
                    End If
                End If

                If m_arrControlMap(iPlayer, cInputDown).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).moveDown = TRUE Then
                        If m_arrPlayer(iPlayer).lastMoveDown = TRUE Then
                            m_arrPlayer(iPlayer).moveDown = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastMoveDown = FALSE
                    End If
                End If

                If m_arrControlMap(iPlayer, cInputLeft).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).moveLeft = TRUE Then
                        If m_arrPlayer(iPlayer).lastMoveLeft = TRUE Then
                            m_arrPlayer(iPlayer).moveLeft = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastMoveLeft = FALSE
                    End If
                End If

                If m_arrControlMap(iPlayer, cInputRight).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).moveRight = TRUE Then
                        If m_arrPlayer(iPlayer).lastMoveRight = TRUE Then
                            m_arrPlayer(iPlayer).moveRight = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastMoveRight = FALSE
                    End If
                End If
                ' END UPDATE MOVEMENT CONTROL STATES
                ' -----------------------------------------------------------------------------

                ' -----------------------------------------------------------------------------
                ' BEGIN MOVEMENT ACTIONS

                m_arrPlayer(iPlayer).moveY = 0
                m_arrPlayer(iPlayer).moveX = 0

                If m_arrPlayer(iPlayer).moveUp = TRUE Then
                    m_arrPlayer(iPlayer).moveY = -1
                    m_arrPlayer(iPlayer).lastMoveUp = TRUE
                End If

                If m_arrPlayer(iPlayer).moveDown = TRUE Then
                    m_arrPlayer(iPlayer).moveY = 1
                    m_arrPlayer(iPlayer).lastMoveDown = TRUE
                End If

                If m_arrPlayer(iPlayer).moveLeft = TRUE Then
                    m_arrPlayer(iPlayer).moveX = -1
                    m_arrPlayer(iPlayer).lastMoveLeft = TRUE
                End If

                If m_arrPlayer(iPlayer).moveRight = TRUE Then
                    m_arrPlayer(iPlayer).moveX = 1
                    m_arrPlayer(iPlayer).lastMoveRight = TRUE
                End If
                ' END MOVEMENT ACTIONS
                ' -----------------------------------------------------------------------------


                ' -----------------------------------------------------------------------------
                ' BEGIN MOVEMENT

                ' MOVE RIGHT/LEFT
                m_arrPlayer(iPlayer).x = m_arrPlayer(iPlayer).x + m_arrPlayer(iPlayer).moveX
                If m_arrPlayer(iPlayer).x < iMinX Then
                    m_arrPlayer(iPlayer).x = m_arrPlayer(iPlayer).xOld ' iMinX
                ElseIf m_arrPlayer(iPlayer).x > iMaxX Then
                    m_arrPlayer(iPlayer).x = m_arrPlayer(iPlayer).xOld ' iMaxX
                End If

                ' MOVE UP/DOWN
                m_arrPlayer(iPlayer).y = m_arrPlayer(iPlayer).y + m_arrPlayer(iPlayer).moveY
                If m_arrPlayer(iPlayer).y < iMinY Then
                    m_arrPlayer(iPlayer).y = m_arrPlayer(iPlayer).yOld ' iMinY
                ElseIf m_arrPlayer(iPlayer).y > iMaxY Then
                    m_arrPlayer(iPlayer).y = m_arrPlayer(iPlayer).yOld ' iMaxY
                End If

                ' UPDATE SCREEN
                '_PRINTSTRING (m_arrPlayer(iPlayer).xOld, m_arrPlayer(iPlayer).yOld), " "
                '_PRINTSTRING (m_arrPlayer(iPlayer).x, m_arrPlayer(iPlayer).y), CHR$(m_arrPlayer(iPlayer).c)
                PrintString m_arrPlayer(iPlayer).xOld, m_arrPlayer(iPlayer).yOld, " "
                PrintString m_arrPlayer(iPlayer).x, m_arrPlayer(iPlayer).y, Chr$(m_arrPlayer(iPlayer).c)
                m_arrPlayer(iPlayer).xOld = m_arrPlayer(iPlayer).x
                m_arrPlayer(iPlayer).yOld = m_arrPlayer(iPlayer).y

                ' END MOVEMENT
                ' -----------------------------------------------------------------------------







                ' -----------------------------------------------------------------------------
                ' BEGIN UPDATE BUTTON STATES
                ' If repeating keys are disabled then
                ' disable until the key has been released

                'if m_bRepeatButton1 = FALSE then
                If m_arrControlMap(iPlayer, cInputButton1).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).button1 = TRUE Then
                        If m_arrPlayer(iPlayer).lastButton1 = TRUE Then
                            m_arrPlayer(iPlayer).button1 = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastButton1 = FALSE
                    End If
                End If
                If m_arrControlMap(iPlayer, cInputButton2).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).button2 = TRUE Then
                        If m_arrPlayer(iPlayer).lastButton2 = TRUE Then
                            m_arrPlayer(iPlayer).button2 = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastButton2 = FALSE
                    End If
                End If
                If m_arrControlMap(iPlayer, cInputButton3).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).button3 = TRUE Then
                        If m_arrPlayer(iPlayer).lastButton3 = TRUE Then
                            m_arrPlayer(iPlayer).button3 = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastButton3 = FALSE
                    End If
                End If
                If m_arrControlMap(iPlayer, cInputButton4).repeat = FALSE Then
                    If m_arrPlayer(iPlayer).button4 = TRUE Then
                        If m_arrPlayer(iPlayer).lastButton4 = TRUE Then
                            m_arrPlayer(iPlayer).button4 = FALSE
                        End If
                    Else
                        m_arrPlayer(iPlayer).lastButton4 = FALSE
                    End If
                End If
                ' END UPDATE BUTTON STATES
                ' -----------------------------------------------------------------------------



                ' -----------------------------------------------------------------------------
                ' BEGIN BUTTON ACTIONS
                If m_arrPlayer(iPlayer).button1 = TRUE Then
                    MakeSound iPlayer, 1
                    m_arrPlayer(iPlayer).lastButton1 = TRUE
                End If

                If m_arrPlayer(iPlayer).button2 = TRUE Then
                    MakeSound iPlayer, 2
                    m_arrPlayer(iPlayer).lastButton2 = TRUE
                End If

                If m_arrPlayer(iPlayer).button3 = TRUE Then
                    MakeSound iPlayer, 3
                    m_arrPlayer(iPlayer).lastButton3 = TRUE
                End If

                If m_arrPlayer(iPlayer).button4 = TRUE Then
                    MakeSound iPlayer, 4
                    m_arrPlayer(iPlayer).lastButton4 = TRUE
                End If
                ' END BUTTON ACTIONS
                ' -----------------------------------------------------------------------------

            Next iPlayer

            _Limit 30
        Loop Until _KeyHit = 27 ' ESCAPE to quit
        _KeyClear: _Delay 1

        sResult = sError
    Else
        sResult = "No mapping loaded. Please load a mapping or map keys."
    End If

    TestMappings$ = sResult
End Function ' TestMappings$

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

Sub MakeSound (iPlayer As Integer, iButton As Integer)
    Dim note%
    If iPlayer < 1 Then
        iPlayer = 1
    ElseIf iPlayer > 8 Then
        iPlayer = 8
    End If
    If iButton < 1 Then
        iButton = 1
    ElseIf iButton > 4 Then
        iButton = 4
    End If

    note% = iPlayer * 100 + (iButton * 25)
    If note% > 4186 Then
        note% = 4186
    End If
    Sound note%, .75
End Sub ' MakeSound

' /////////////////////////////////////////////////////////////////////////////
' V2 prints in 2 columns.

Sub PrintControllerMap2
    Dim RoutineName As String:: RoutineName = "PrintControllerMap2"
    Dim iPlayer As Integer
    Dim iWhichInput As Integer
    Dim iCount As Integer
    Dim sLine As String
    Dim iHalf As Integer
    Dim sColumn1 As String: sColumn1 = ""
    Dim sColumn2 As String: sColumn2 = ""
    ReDim arrColumn1(-1) As String
    ReDim arrColumn2(-1) As String
    Dim iLoop As Integer
    Dim iColWidth As Integer: iColWidth = 75
    Dim sValue As String
    Dim in$

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' START OUTPUT
    Print "Controller mapping:"
    'Print "Player#  Input      Device#  Type     Code              Value"
    '       1        button #2  x        unknown  x                 x
    '       9        11         9        9        18                9
    '       12345678912345678901123456789123456789123456789012345678123456789
    '       12345678901234567890123456789012345678901234567890123456789012345678901234567890
    '       00000000011111111112222222222333333333344444444445555555555666666666677777777778

    If m_bHaveMapping = TRUE Then
        ' THIS IS A LAZY WAY TO GET 2 COLUMNS!
        iHalf = UBound(m_arrControlMap, 1) / 2

        sLine = "Player#  Input      Device#  Type     Code              Value    Repeat"
        sColumn1 = sColumn1 + StrPadRight$(sLine, iColWidth) + Chr$(13)
        sLine = "-----------------------------------------------------------------------"
        sColumn1 = sColumn1 + StrPadRight$(sLine, iColWidth) + Chr$(13)
        For iPlayer = LBound(m_arrControlMap, 1) To iHalf
            iCount = 0
            For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                    iCount = iCount + 1
                End If
            Next iWhichInput
            If iCount > 0 Then
                For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                    If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                        sLine = IntPadRight$(iPlayer, 9)
                        sLine = sLine + StrPadRight$(InputToString$(iWhichInput), 11)
                        sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).device, 9)
                        sLine = sLine + StrPadRight$(InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ), 9)

                        'sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).code, 9)
                        If m_arrControlMap(iPlayer, iWhichInput).typ = cInputKey Then
                            sValue = GetKeyboardButtonCodeText$(m_arrControlMap(iPlayer, iWhichInput).code)
                            sValue = StrPadRight$(sValue, 18)
                        Else
                            sValue = IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).code, 18)
                        End If
                        sLine = sLine + sValue

                        sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).value, 9)

                        sValue = TrueFalse$(m_arrControlMap(iPlayer, iWhichInput).repeat)
                        sLine = sLine + StrPadRight$(sValue, 6)

                        'Print sLine
                        sLine = StrPadRight$(sLine, iColWidth)
                        sColumn1 = sColumn1 + sLine + Chr$(13)
                    End If
                Next iWhichInput
            Else
                sLine = IntPadRight$(iPlayer, 9) + "(NONE)"
                'Print sLine
                sLine = StrPadRight$(sLine, iColWidth)
                sColumn1 = sColumn1 + sLine + Chr$(13)
            End If
        Next iPlayer

        'sLine = "Player#  Input      Device#  Type     Code              Value"
        sLine = "Player#  Input      Device#  Type     Code              Value    Repeat"
        sColumn2 = sColumn2 + StrPadRight$(sLine, iColWidth) + Chr$(13)
        'sLine = "-------------------------------------------------------------"
        sLine = "-----------------------------------------------------------------------"
        sColumn2 = sColumn2 + StrPadRight$(sLine, iColWidth) + Chr$(13)
        For iPlayer = iHalf + 1 To UBound(m_arrControlMap, 1)
            iCount = 0
            For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                    iCount = iCount + 1
                End If
            Next iWhichInput
            If iCount > 0 Then
                For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                    If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                        sLine = IntPadRight$(iPlayer, 9)
                        sLine = sLine + StrPadRight$(InputToString$(iWhichInput), 11)
                        sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).device, 9)
                        sLine = sLine + StrPadRight$(InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ), 9)

                        'sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).code, 9)
                        If m_arrControlMap(iPlayer, iWhichInput).typ = cInputKey Then
                            sValue = GetKeyboardButtonCodeText$(m_arrControlMap(iPlayer, iWhichInput).code)
                            sValue = StrPadRight$(sValue, 18)
                        Else
                            sValue = IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).code, 18)
                        End If
                        sLine = sLine + sValue

                        sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).value, 9)

                        sValue = TrueFalse$(m_arrControlMap(iPlayer, iWhichInput).repeat)
                        sLine = sLine + StrPadRight$(sValue, 6)

                        'Print sLine
                        sLine = StrPadRight$(sLine, iColWidth)
                        sColumn2 = sColumn2 + sLine + Chr$(13)
                    End If
                Next iWhichInput
            Else
                sLine = IntPadRight$(iPlayer, 9) + "(NONE)"
                'Print sLine
                sLine = StrPadRight$(sLine, iColWidth)
                sColumn2 = sColumn2 + sLine + Chr$(13)
            End If
        Next iPlayer

        split sColumn1, Chr$(13), arrColumn1()
        split sColumn2, Chr$(13), arrColumn2()
        If UBound(arrColumn1) > UBound(arrColumn2) Then
            iCount = UBound(arrColumn1)
        Else
            iCount = UBound(arrColumn2)
        End If
        For iLoop = 0 To iCount
            sLine = ""
            If UBound(arrColumn1) >= iLoop Then
                sLine = sLine + arrColumn1(iLoop)
            Else
                sLine = sLine + String$(iColWidth, " ")
            End If
            sLine = sLine + "     "
            If UBound(arrColumn2) >= iLoop Then
                sLine = sLine + arrColumn2(iLoop)
            Else
                sLine = sLine + String$(iColWidth, " ")
            End If
            Print sLine
        Next iLoop
    Else
        Print "No mapping loaded. Please load a mapping or map keys."
    End If

End Sub ' PrintControllerMap2

' /////////////////////////////////////////////////////////////////////////////
' Original (simple) routine

Sub PrintControllerMap1
    Dim RoutineName As String:: RoutineName = "PrintControllerMap1"
    Dim iPlayer As Integer
    Dim iWhichInput As Integer
    Dim sLine As String
    Dim iCount As Integer
    Dim in$

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' OUTPUT MAPPING
    Print "Controller mapping:"
    Print "Player#  Input      Device#  Type     Code     Value"
    '      1        button #2  x        unknown  x        x
    '      9        11         9        9        9        9
    '      12345678912345678901123456789123456789123456789123456789
    '      12345678901234567890123456789012345678901234567890123456789012345678901234567890
    For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
        iCount = 0
        For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
            If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                iCount = iCount + 1
            End If
        Next iWhichInput
        If iCount > 0 Then
            For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                    sLine = IntPadRight$(iPlayer, 9)
                    sLine = sLine + StrPadRight$(InputToString$(iWhichInput), 11)
                    sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).device, 9)
                    sLine = sLine + StrPadRight$(InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ), 9)
                    sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).code, 9)
                    sLine = sLine + IntPadRight$(m_arrControlMap(iPlayer, iWhichInput).value, 9)
                    Print sLine
                End If
            Next iWhichInput
        Else
            sLine = IntPadRight$(iPlayer, 9) + "(NONE)"
            Print sLine
        End If
    Next iPlayer
End Sub ' PrintControllerMap1

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

Function LoadMappings$
    Dim sResult As String: sResult = ""

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' Try loading map
    sResult = LoadControllerMap$

    LoadMappings$ = sResult
End Function ' LoadMappings$

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

Function SaveMappings$
    Dim sResult As String: sResult = ""
    Dim sError As String: sError = ""

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' Try saving map
    sResult = SaveControllerMap$

    SaveMappings$ = sResult
End Function ' SaveMappings$


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

Function ViewMappings$
    ' INITIALIZE
    InitKeyboardButtonCodes

    PrintControllerMap2
    Print
    Input "PRESS <ENTER> TO CONTINUE", in$
    Print
    ViewMappings$ = ""
End Function ' ViewMappings$

' /////////////////////////////////////////////////////////////////////////////
' TODO: test this

Function EditMappings$
    Dim RoutineName As String: RoutineName = "EditMappings$"
    Dim in$
    Dim iPlayer As Integer
    Dim iWhichInput As Integer
    Dim iDevice As Integer
    Dim iType As Integer
    Dim iCode As Integer
    Dim iValue As Integer
    Dim iRepeat As Integer
    Dim iItem As Integer
    Dim sResult As String: sResult = ""
    Dim bContinue1 As Integer: bContinue1 = TRUE
    Dim bContinue2 As Integer: bContinue2 = TRUE
    Dim bContinue3 As Integer: bContinue3 = TRUE
    Dim bContinue4 As Integer: bContinue4 = TRUE

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' EDIT
    Do
        PrintControllerMap2
        Print "To edit a mapping, enter a player number: " _
            "1-" + cstr$(cMaxPlayers) + ", " + _
            cstr$(cMaxPlayers+1) + ") or q to exit."
        Input "Edit mapping for player"; in$
        If IsNum%(in$) Then
            iPlayer = Val(in$)
            If iPlayer > 0 And iPlayer <= cMaxPlayers Then
                bContinue2 = TRUE
                Do
                    Print "Editing mappings for player " + cstr$(iPlayer) + "."
                    For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                        'Print right$("  " + cstr$(iWhichInput), 2) + ". " + InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ)
                        Print Right$("  " + cstr$(iWhichInput), 2) + ". " + InputToString$(iWhichInput)
                    Next iWhichInput
                    Input "Type # of control to edit or q to quit editing player"; in$
                    If IsNum%(in$) Then
                        iWhichInput = Val(in$)
                        If iWhichInput >= LBound(m_arrControlMap, 2) And m_arrControlMap <= UBound(m_arrControlMap, 2) Then
                            bContinue3 = TRUE
                            Do
                                Print "Settings for " + InputToString$(iWhichInput) + ":"
                                Print "1. Device #     : " + cstr$(m_arrControlMap(iPlayer, iWhichInput).device)
                                Print "2. Device type  : " + InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ)

                                If m_arrControlMap(iPlayer, iWhichInput).typ = cInputKey Then
                                    Print "3. Input code   : " + GetKeyboardButtonCodeText$(m_arrControlMap(iPlayer, iWhichInput).code) + _
                                        " (" + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).code)) + ")"
                                Else
                                    Print "3. Input code   : " + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).code))
                                End If

                                Print "4. Input value  : " + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).value))
                                Print "5. Enable repeat: " + TrueFalse$(m_arrControlMap(iPlayer, iWhichInput).repeat)
                                Input "Change item? (1-5 or q to quit editing control)"; in$
                                If IsNum%(in$) Then
                                    iItem = Val(in$)
                                    Select Case iItem
                                        Case 1:
                                            Print "Change the device number."
                                            Input "Type a new device #, 0 for none (disabled), or blank to leave it unchanged"; in$
                                            If IsNum%(in$) Then
                                                iDevice = Val(in$)
                                                m_arrControlMap(iPlayer, iWhichInput).device = iDevice
                                                Print "Updated device number. Remember to save mappings when done."
                                            Else
                                                Print "(No change.)"
                                            End If
                                        Case 2:
                                            bContinue4 = TRUE
                                            Do
                                                Print "Change the device type."
                                                Print cstr$(cInputKey) + "=keyboard"
                                                Print cstr$(cInputButton) + "=game controller button"
                                                Print cstr$(cInputAxis) + "=game controller joystick/axis"
                                                Print cstr$(cInputNone) + "=none"
                                                Input "Device type or blank to leave it unchanged"; in$
                                                If IsNum%(in$) Then
                                                    iType = Val(in$)
                                                    if iType=cInputKey or iType=cInputButton or _
                                                        iType=cInputAxis or iType=cInputNone then

                                                        m_arrControlMap(iPlayer, iWhichInput).typ = iType
                                                        Print "Updated device type. Remember to save mappings when done."
                                                        bContinue4 = FALSE: Exit Do
                                                    Else
                                                        Print "Please choose one of the listed values."
                                                    End If
                                                Else
                                                    Print "(No change.)"
                                                    bContinue4 = FALSE: Exit Do
                                                End If
                                            Loop Until bContinue4 = FALSE
                                        Case 3:
                                            Print "Change the input code."
                                            Input "Type a new input code, or blank to leave it unchanged"; in$
                                            If IsNum%(in$) Then
                                                iCode = Val(in$)
                                                m_arrControlMap(iPlayer, iWhichInput).code = iCode
                                                Print "Updated input code. Remember to save mappings when done."
                                            Else
                                                Print "(No change.)"
                                            End If
                                        Case 4:
                                            Print "Change the input value."
                                            Input "Type a new input value, or blank to leave it unchanged"; in$
                                            If IsNum%(in$) Then
                                                iValue = Val(in$)
                                                m_arrControlMap(iPlayer, iWhichInput).value = iValue
                                                Print "Updated input value. Remember to save mappings when done."
                                            Else
                                                Print "(No change.)"
                                            End If
                                        Case 5:
                                            Print "Change the repeat setting."
                                            Input "Type 1 to enable or 0 to disable, or blank to leave it unchanged"; in$
                                            If IsNum%(in$) Then
                                                iRepeat = Val(in$)
                                                If iRepeat = 0 Then
                                                    m_arrControlMap(iPlayer, iWhichInput).repeat = FALSE
                                                    Print "Repeat disabled. Remember to save mappings when done."
                                                ElseIf iRepeat = 1 Then
                                                    m_arrControlMap(iPlayer, iWhichInput).repeat = TRUE
                                                    Print "Repeat enabled. Remember to save mappings when done."
                                                Else
                                                    Print "(No change.)"
                                                End If
                                            Else
                                                Print "(No change.)"
                                            End If
                                        Case Else:
                                            Print "Please choose a number between 1 and 4."
                                    End Select
                                Else
                                    bContinue3 = FALSE: Exit Do
                                End If
                            Loop Until bContinue3 = FALSE
                        Else
                            Print "Please choose a number between " + cstr$(LBound(m_arrControlMap, 2)) + " and " + cstr$(UBound(m_arrControlMap, 2)) + "."
                        End If
                    Else
                        bContinue2 = FALSE: Exit Do
                    End If
                Loop Until bContinue2 = FALSE
                If bContinue1 = FALSE Then Exit Do
            Else
                Print "Please choose a number between 1 and " + cstr$(cMaxPlayers) + "."
            End If
        Else
            If Len(sResult) = 0 Then sResult = "(Cancelled.)"
            bContinue1 = FALSE: Exit Do
        End If
    Loop Until bContinue1 = FALSE

    _KeyClear: _Delay 1

    EditMappings$ = sResult
End Function ' EditMappings$

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

Function ResetMapping$
    Dim RoutineName As String: RoutineName = "ResetMapping$"
    Dim in$
    Dim iPlayer As Integer
    Dim sResult As String: sResult = ""

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' RESET
    Do
        PrintControllerMap2

        Print "To delete mapping, enter a player number: " _
            "1-" + cstr$(cMaxPlayers) + ", " + _
            cstr$(cMaxPlayers+1) + " for all, or 0 to exit."
        Input "Delete mapping for player? "; iPlayer

        If iPlayer > 0 And iPlayer <= cMaxPlayers Then
            Print "Delete mappings for player " + cstr$(iPlayer) + "."
            Input "Delete (y/n)"; in$: in$ = LCase$(_Trim$(in$))
            If in$ = "y" Then
                For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                    If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                        m_arrControlMap(iPlayer, iWhichInput).device = 0
                        m_arrControlMap(iPlayer, iWhichInput).typ = 0
                        m_arrControlMap(iPlayer, iWhichInput).code = 0
                        m_arrControlMap(iPlayer, iWhichInput).value = 0
                        m_arrControlMap(iPlayer, iWhichInput).repeat = 0 ' GetGlobalInputRepeatSetting%(iWhichInput)
                    End If
                Next iWhichInput
                sResult = "Mappings deleted for player " + cstr$(iPlayer) + "."
                Print sResult
            End If
        ElseIf iPlayer = (cMaxPlayers + 1) Then
            Input "Delete all mappings (y/n)"; in$: in$ = LCase$(_Trim$(in$))
            If in$ = "y" Then
                For iPlayer = 1 To cMaxPlayers
                    For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
                        If InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) <> "unknown" Then
                            m_arrControlMap(iPlayer, iWhichInput).device = 0
                            m_arrControlMap(iPlayer, iWhichInput).typ = 0
                            m_arrControlMap(iPlayer, iWhichInput).code = 0
                            m_arrControlMap(iPlayer, iWhichInput).value = 0
                            m_arrControlMap(iPlayer, iWhichInput).repeat = 0 ' GetGlobalInputRepeatSetting%(iWhichInput)
                        End If
                    Next iWhichInput
                Next iPlayer
                sResult = "All mappings deleted."
                Print sResult
            End If
        Else
            If Len(sResult) = 0 Then sResult = "(Cancelled.)"
            Exit Do
        End If
    Loop
    ResetMapping$ = sResult
End Function ' ResetMapping$

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

Function MapInput$
    Dim RoutineName As String: RoutineName = "MapInput$"
    Dim in$
    Dim iDeviceCount As Integer
    Dim iPlayer As Integer
    Dim sResult As String
    Dim sError As String

    ' INITIALIZE
    InitKeyboardButtonCodes

    ' SET UP SCREEN
    Screen _NewImage(1280, 1024, 32): _ScreenMove 0, 0

    ' MAKE SURE WE HAVE DEVICES
    ' 1 is the keyboard
    ' 2 is the mouse
    ' 3 is the joystick
    ' unless someone has a strange setup with multiple mice/keyboards/ect...
    ' In that case, you can use _DEVICE$(i) to look for "KEYBOARD", "MOUSE", "JOYSTICK", if necessary.
    ' I've never actually found it necessary, but I figure it's worth mentioning, just in case...
    iDeviceCount = _Devices ' Find the number of devices on someone's system
    If iDeviceCount > 2 Then
        '' Try loading map
        'sError = LoadControllerMap$
        'if len(sError) = 0 then
        '    print "Previous controller mapping loaded."
        'else
        '    print "*******************************************************************************"
        '    print "There were errors loading the controller mapping file:"
        '    print sError
        '    print
        '    print "Try remapping - a new file will be created."
        '    print "*******************************************************************************"
        'end if
        Do
            PrintControllerMap2
            Print "To edit mapping, enter a player number (1-" + cstr$(cMaxPlayers) + ") or 0 to exit."
            Input "Get input for player? "; iPlayer
            If iPlayer > 0 And iPlayer <= cMaxPlayers Then
                sResult = MapInput1$(iPlayer)
                If Len(sResult) = 0 Then
                    Print "Remember to save mappings when done."
                Else
                    Print sResult
                End If
            Else
                sResult = "(Cancelled.)"
                Exit Do
            End If
        Loop
    Else
        sResult = "No controller devices found."
        Input "PRESS <ENTER> TO CONTINUE", in$
    End If
    MapInput$ = sResult

End Function ' MapInput$

' /////////////////////////////////////////////////////////////////////////////
' Detect controls
' THIS VERSION SUPPORTS UPTO 8 JOYSTICKS, WITH UPTO 2 BUTTONS AND 2 AXES EACH
' (THIS IS FOR ATARI 2600 JOYSTICKS)

' The following shared arrays must be declared:
'     ReDim Shared m_arrButtonCode(1 To 99) As Long
'     ReDim Shared m_arrButtonKey(1 To 99) As String

Function MapInput1$ (iPlayer As Integer)
    Dim RoutineName As String:: RoutineName = "MapInput1$"
    Dim sResult As String: sResult = ""
    Dim sError As String: sError = ""

    Dim in$

    Dim iDeviceCount As Integer
    Dim iDevice As Integer
    Dim iNumControllers As Integer
    Dim iController As Integer

    Dim iLoop As Integer
    Dim strValue As String
    Dim strAxis As String
    Dim dblNextAxis
    Dim iCount As Long
    Dim iValue As Integer
    Dim iCode As Integer

    Dim arrButton(32, 16) As Integer ' number of buttons on the joystick
    Dim arrButtonNew(32, 16) As Integer ' tracks when to initialize values
    Dim arrAxis(32, 16) As Double ' number of axis on the joystick
    Dim arrAxisNew(32, 16) As Integer ' tracks when to initialize values

    'Dim arrInput(1 To 8) As ControlInputType
    Dim iWhichInput As Integer
    Dim bFinished As Integer
    Dim bHaveInput As Integer
    Dim bMoveNext As Integer
    Dim bCancel As Integer
    Dim iNextInput As Integer

    ' FOR PRINTING OUTPUT
    Dim iDigits As Integer ' # digits to display (values are truncated to this length)
    Dim iColCount As Integer
    Dim iGroupCount As Integer
    Dim sLine As String
    Dim iCols As Integer
    Dim iRows As Integer
    Dim iMaxCols As Integer

    ' INITIALIZE
    If Len(sError) = 0 Then
        iDigits = 4 ' 11
        iColCount = 3
        iGroupCount = 0 ' re-initialized at the top of every loop
        iCols = _Width(0) \ _FontWidth
        iRows = _Height(0) \ _FontHeight
    End If

    ' COUNT # OF JOYSTICKS
    ' TODO: find out the right way to count joysticks
    If Len(sError) = 0 Then
        ' D= _DEVICES ' MUST be read in order for other 2 device functions to work!
        iDeviceCount = _Devices ' Find the number of devices on someone's system

        If iDeviceCount > 2 Then
            ' LIMIT # OF DEVICES, IF THERE IS A LIMIT DEFINED
            iNumControllers = iDeviceCount - 2
            If cMaxControllers > 0 Then
                If iNumControllers > cMaxControllers Then
                    iNumControllers = cMaxControllers
                End If
            End If
        Else
            ' ONLY 2 FOUND (KEYBOARD, MOUSE)
            sError = "No game controllers found."
        End If
    End If

    ' INITIALIZE CONTROLLER DATA
    If Len(sError) = 0 Then
        For iController = 1 To iNumControllers
            m_arrController(iController).buttonCount = cMaxButtons
            m_arrController(iController).axisCount = cMaxAxis
            For iLoop = 1 To cMaxButtons
                arrButtonNew(iController, iLoop) = TRUE
            Next iLoop
            For iLoop = 1 To cMaxAxis
                arrAxisNew(iController, iLoop) = TRUE
            Next iLoop
        Next iController
    End If

    ' INITIALIZE CONTROLLER INPUT
    If Len(sError) = 0 Then
        Cls
        Print "We will now detect controllers."
        Print "Do not touch any keys or game controllers during detection."
        Input "Press <ENTER> to begin"; in$
        _KeyClear: Print
        sLine = "Initializing controllers": Print sLine;
        iMaxCols = (iCols - Len(sLine)) - 1
        iCount = 0
        Do
            iCount = iCount + 1
            If iCount < iMaxCols Then
                Print ".";
            Else
                Print ".": Print sLine: iCount = 0
            End If
            For iController = 1 To iNumControllers
                iDevice = iController + 2
                While _DeviceInput(iDevice) ' clear and update the device buffer
                    For iLoop = 1 To _LastButton(iDevice)
                        If (iLoop > cMaxButtons) Then Exit For
                        m_arrController(iController).buttonCount = iLoop
                        'IF _BUTTONCHANGE(iLoop) THEN
                        '    arrButton(iController, iLoop) = _BUTTON(iLoop)
                        'END IF
                        arrButton(iController, iLoop) = FALSE
                    Next iLoop
                    For iLoop = 1 To _LastAxis(iDevice) ' this loop checks all my axis
                        If (iLoop > cMaxAxis) Then Exit For
                        m_arrController(iController).axisCount = iLoop
                        arrAxis(iController, iLoop) = 0
                    Next iLoop
                Wend ' clear and update the device buffer
            Next iController
            _Limit 30
        Loop Until iCount > 60 ' quit after 2 seconds
        Print: Print
    End If

    ' WAIT FOR INPUT
    If Len(sError) = 0 Then
        Cls
        Print "Press <ESCAPE> to cancel at any time."
        Print

        _KeyClear: _Delay 1
        bCancel = FALSE
        bFinished = FALSE
        iLastPressed = 0
        For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
            'print "iWhichInput=" + cstr$(iWhichInput)
            Print "Player #" + cstr$(iPlayer) + " press control for " + InputToString$(iWhichInput) + " or ESC to skip: ";

            ' =============================================================================
            ' BEGIN LOOK FOR NEXT INPUT
            bMoveNext = FALSE
            Do
                ' -----------------------------------------------------------------------------
                ' BEGIN CHECK FOR CONTROLLER INPUT
                For iController = 1 To iNumControllers
                    iDevice = iController + 2

                    ' Check all devices
                    While _DeviceInput(iDevice)

                        ' Check each button
                        If bMoveNext = FALSE Then
                            For iLoop = 1 To _LastButton(iDevice)
                                If (iLoop > cMaxButtons) Then Exit For
                                'm_arrController(iController).buttonCount = iLoop

                                ' update button array to indicate if a button is up or down currently.
                                If _ButtonChange(iLoop) Then
                                    iValue = _Button(iLoop)
                                    If iValue <> arrButton(iController, iLoop) Then
                                        ' *****************************************************************************
                                        ' PRESSED BUTTON

                                        ' make sure this isn't already mapped
                                        bHaveInput = TRUE
                                        If iWhichInput > LBound(m_arrControlMap, 2) Then
                                            ' is input unique?
                                            For iNextInput = LBound(m_arrControlMap, 2) To iWhichInput - 1
                                                If m_arrControlMap(iPlayer, iNextInput).device = iDevice Then
                                                    If m_arrControlMap(iPlayer, iNextInput).typ = cInputButton Then
                                                        If m_arrControlMap(iPlayer, iNextInput).code = iLoop Then
                                                            If m_arrControlMap(iPlayer, iNextInput).value = iValue Then
                                                                bHaveInput = FALSE
                                                            End If
                                                        End If
                                                    End If
                                                End If
                                            Next iNextInput
                                        End If

                                        If bHaveInput Then
                                            m_arrControlMap(iPlayer, iWhichInput).device = iDevice
                                            m_arrControlMap(iPlayer, iWhichInput).typ = cInputButton
                                            m_arrControlMap(iPlayer, iWhichInput).code = iLoop
                                            m_arrControlMap(iPlayer, iWhichInput).value = iValue
                                            bMoveNext = TRUE
                                        End If

                                    End If
                                End If
                            Next iLoop
                        End If

                        ' Check each axis
                        If bMoveNext = FALSE Then
                            For iLoop = 1 To _LastAxis(iDevice)
                                If (iLoop > cMaxAxis) Then Exit For
                                'm_arrController(iController).axisCount = iLoop

                                dblNextAxis = _Axis(iLoop)
                                dblNextAxis = RoundUpDouble#(dblNextAxis, 3)

                                ' I like to give a little "jiggle" resistance to my controls, as I have an old joystick
                                ' which is prone to always give minute values and never really center on true 0.
                                ' A value of 1 means my axis is pushed fully in one direction.
                                ' A value greater than 0.1 means it's been partially pushed in a direction (such as at a 45 degree diagional angle).
                                ' A value of less than 0.1 means we count it as being centered. (As if it was 0.)

                                'These are way too sensitive for analog:
                                'IF ABS(_AXIS(iLoop)) <= 1 AND ABS(_AXIS(iLoop)) >= .1 THEN
                                'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= .01 THEN
                                'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= .001 THEN

                                'For digital input, we'll use a big picture:
                                If Abs(dblNextAxis) <= 1 And Abs(dblNextAxis) >= 0.75 Then
                                    If dblNextAxis <> arrAxis(iController, iLoop) Then
                                        ' *****************************************************************************
                                        ' MOVED STICK

                                        ' convert to a digital value
                                        If dblNextAxis < 0 Then
                                            iValue = -1
                                        Else
                                            iValue = 1
                                        End If

                                        ' make sure this isn't already mapped
                                        bHaveInput = TRUE
                                        If iWhichInput > LBound(m_arrControlMap, 2) Then
                                            ' is input unique?
                                            For iNextInput = LBound(m_arrControlMap, 2) To iWhichInput - 1
                                                If m_arrControlMap(iPlayer, iNextInput).device = iDevice Then
                                                    If m_arrControlMap(iPlayer, iNextInput).typ = cInputAxis Then
                                                        If m_arrControlMap(iPlayer, iNextInput).code = iLoop Then
                                                            If m_arrControlMap(iPlayer, iNextInput).value = iValue Then
                                                                bHaveInput = FALSE
                                                            End If
                                                        End If
                                                    End If
                                                End If
                                            Next iNextInput
                                        End If

                                        If bHaveInput Then
                                            m_arrControlMap(iPlayer, iWhichInput).device = iDevice
                                            m_arrControlMap(iPlayer, iWhichInput).typ = cInputAxis
                                            m_arrControlMap(iPlayer, iWhichInput).code = iLoop
                                            m_arrControlMap(iPlayer, iWhichInput).value = iValue
                                            bMoveNext = TRUE
                                        End If

                                    End If
                                End If
                            Next iLoop
                        End If

                    Wend ' clear and update the device buffer

                Next iController
                ' END CHECK FOR CONTROLLER INPUT
                ' -----------------------------------------------------------------------------

                ' -----------------------------------------------------------------------------
                ' BEGIN CHECK FOR KEYBOARD INPUT #1
                If bMoveNext = FALSE Then
                    '_KEYCLEAR: _DELAY 1
                    While _DeviceInput(1): Wend ' clear and update the keyboard buffer

                    ' Detect changed key state
                    For iLoop = LBound(m_arrButtonCode) To UBound(m_arrButtonCode)
                        iCode = m_arrButtonCode(iLoop)
                        If _Button(iCode) <> FALSE Then
                            ' *****************************************************************************
                            ' PRESSED KEYBOARD
                            'PRINT "PRESSED " + m_arrButtonKey(iLoop)

                            ' make sure this isn't already mapped
                            bHaveInput = TRUE
                            If iWhichInput > LBound(m_arrControlMap, 2) Then
                                ' is input unique?
                                For iNextInput = LBound(m_arrControlMap, 2) To iWhichInput - 1
                                    If m_arrControlMap(iPlayer, iNextInput).device = 1 Then ' .device 1 = keyboard
                                        If m_arrControlMap(iPlayer, iNextInput).typ = cInputKey Then
                                            If m_arrControlMap(iPlayer, iNextInput).code = iCode Then
                                                'if m_arrControlMap(iPlayer, iNextInput).value = TRUE then
                                                bHaveInput = FALSE
                                                'end if
                                            End If
                                        End If
                                    End If
                                Next iNextInput
                            End If

                            If bHaveInput Then
                                m_arrControlMap(iPlayer, iWhichInput).device = 1 ' .device 1 = keyboard
                                m_arrControlMap(iPlayer, iWhichInput).typ = cInputKey
                                m_arrControlMap(iPlayer, iWhichInput).code = iCode
                                m_arrControlMap(iPlayer, iWhichInput).value = TRUE
                                bMoveNext = TRUE
                            End If

                        End If
                    Next iLoop
                End If
                ' END CHECK FOR KEYBOARD INPUT #1
                ' -----------------------------------------------------------------------------

                If bMoveNext = TRUE Then Exit Do
                _Limit 30
            Loop Until _KeyHit = 27 ' ESCAPE to quit
            ' END LOOK FOR NEXT INPUT
            ' =============================================================================

            If bMoveNext = TRUE Then
                Print "Device #" + cstr$(m_arrControlMap(iPlayer, iWhichInput).device) + " " + _
                    InputTypeToString$(m_arrControlMap(iPlayer, iWhichInput).typ) + " " + _
                    cstr$(m_arrControlMap(iPlayer, iWhichInput).code) + " = " + _
                    cstr$(m_arrControlMap(iPlayer, iWhichInput).value)

                ' Only ask user to select repeat if no override.
                If m_bRepeatOverride = FALSE Then
                    Input "Enable repeat (y/n)"; in$: in$ = LCase$(_Trim$(in$))
                    If in$ = "y" Then
                        m_arrControlMap(iPlayer, iWhichInput).repeat = TRUE
                    Else
                        m_arrControlMap(iPlayer, iWhichInput).repeat = FALSE
                    End If
                Else
                    m_arrControlMap(iPlayer, iWhichInput).repeat = GetGlobalInputRepeatSetting%(iWhichInput)
                End If
            Else
                Print "(Skipped)"
                bCancel = TRUE
                bFinished = TRUE
            End If

            If bFinished = TRUE Then Exit For
        Next iWhichInput
    End If

    If Len(sError) = 0 Then
        m_bHaveMapping = TRUE
    Else
        sResult = "ERRORS: " + sError
    End If

    _KeyClear: _Delay 1
    MapInput1$ = sResult
End Function ' MapInput1$

' /////////////////////////////////////////////////////////////////////////////
' Receives which input contstant and returns a text description

Function InputToString$ (iWhich As Integer)
    Select Case iWhich
        Case cInputUp:
            InputToString$ = "up"
        Case cInputDown:
            InputToString$ = "down"
        Case cInputLeft:
            InputToString$ = "left"
        Case cInputRight:
            InputToString$ = "right"
        Case cInputButton1:
            InputToString$ = "button #1"
        Case cInputButton2:
            InputToString$ = "button #2"
        Case cInputButton3:
            InputToString$ = "button #3"
        Case cInputButton4:
            InputToString$ = "button #4"
        Case Else:
            InputToString$ = "unknown"
    End Select
End Function ' InputToString$

' /////////////////////////////////////////////////////////////////////////////
' Receives which input contstant and returns its global "repeat" setting

' usage:
'     m_arrControlMap(iPlayer, iWhichInput).repeat = GetGlobalInputRepeatSetting%(cInputUp)

Function GetGlobalInputRepeatSetting% (iWhich As Integer)
    Select Case iWhich
        Case cInputUp:
            GetGlobalInputRepeatSetting% = m_bRepeatUp
        Case cInputDown:
            GetGlobalInputRepeatSetting% = m_bRepeatDown
        Case cInputLeft:
            GetGlobalInputRepeatSetting% = m_bRepeatLeft
        Case cInputRight:
            GetGlobalInputRepeatSetting% = m_bRepeatRight
        Case cInputButton1:
            GetGlobalInputRepeatSetting% = m_bRepeatButton1
        Case cInputButton2:
            GetGlobalInputRepeatSetting% = m_bRepeatButton2
        Case cInputButton3:
            GetGlobalInputRepeatSetting% = m_bRepeatButton3
        Case cInputButton4:
            GetGlobalInputRepeatSetting% = m_bRepeatButton4
        Case Else:
            GetGlobalInputRepeatSetting% = FALSE
    End Select
End Function ' GetGlobalInputRepeatSetting%

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

Function InputTypeToString$ (iCode As Integer)
    Select Case iCode
        Case cInputNone:
            InputTypeToString$ = "none"
        Case cInputKey:
            InputTypeToString$ = "key"
        Case cInputButton:
            InputTypeToString$ = "button"
        Case cInputAxis:
            InputTypeToString$ = "axis"
        Case Else:
            InputTypeToString$ = "unknown"
    End Select
End Function ' InputTypeToString$

' /////////////////////////////////////////////////////////////////////////////
' METHOD v2 = faster

Function GetKeyboardButtonCodeText$ (iCode As Integer)
    Dim sResult As String: sResult = ""
    If LBound(m_arrButtonKeyDesc) <= iCode Then
        If UBound(m_arrButtonKeyDesc) >= iCode Then
            sResult = m_arrButtonKeyDesc(iCode)
        End If
    End If
    If Len(sResult) = 0 Then
        sResult = _Trim$(Str$(iCode)) + " (?)"
    End If
    GetKeyboardButtonCodeText$ = sResult
End Function ' GetKeyboardButtonCodeText$

' /////////////////////////////////////////////////////////////////////////////
' METHOD v2
' Faster lookup - a dictionary with a hash lookup would be best
' but this is a quick way to do it since the values never change.

' The following shared arrays must be declared:
'     ReDim Shared m_arrButtonCode(1 To 99) As Long
'     ReDim Shared m_arrButtonKey(1 To 99) As String
'     ReDim Shared m_arrButtonKeyDesc(0 To 512) As String

Sub InitKeyboardButtonCodes ()
    Dim iLoop As Integer

    If m_bInitialized = FALSE Then
        ' CODE(S) DETECTED WITH _BUTTON:
        m_arrButtonCode(1) = 2: m_arrButtonKey(1) = "Esc"
        m_arrButtonCode(2) = 60: m_arrButtonKey(2) = "F1"
        m_arrButtonCode(3) = 61: m_arrButtonKey(3) = "F2"
        m_arrButtonCode(4) = 62: m_arrButtonKey(4) = "F3"
        m_arrButtonCode(5) = 63: m_arrButtonKey(5) = "F4"
        m_arrButtonCode(6) = 64: m_arrButtonKey(6) = "F5"
        m_arrButtonCode(7) = 65: m_arrButtonKey(7) = "F6"
        m_arrButtonCode(8) = 66: m_arrButtonKey(8) = "F7"
        m_arrButtonCode(9) = 67: m_arrButtonKey(9) = "F8"
        m_arrButtonCode(10) = 68: m_arrButtonKey(10) = "F9"
        m_arrButtonCode(11) = 88: m_arrButtonKey(11) = "F11"
        m_arrButtonCode(12) = 89: m_arrButtonKey(12) = "F12"
        m_arrButtonCode(13) = 42: m_arrButtonKey(13) = "Tilde"
        m_arrButtonCode(14) = 3: m_arrButtonKey(14) = "1"
        m_arrButtonCode(15) = 4: m_arrButtonKey(15) = "2"
        m_arrButtonCode(16) = 5: m_arrButtonKey(16) = "3"
        m_arrButtonCode(17) = 6: m_arrButtonKey(17) = "4"
        m_arrButtonCode(18) = 7: m_arrButtonKey(18) = "5"
        m_arrButtonCode(19) = 8: m_arrButtonKey(19) = "6"
        m_arrButtonCode(20) = 9: m_arrButtonKey(20) = "7"
        m_arrButtonCode(21) = 10: m_arrButtonKey(21) = "8"
        m_arrButtonCode(22) = 11: m_arrButtonKey(22) = "9"
        m_arrButtonCode(23) = 12: m_arrButtonKey(23) = "0"
        m_arrButtonCode(24) = 13: m_arrButtonKey(24) = "Minus"
        m_arrButtonCode(25) = 14: m_arrButtonKey(25) = "Equal"
        m_arrButtonCode(26) = 15: m_arrButtonKey(26) = "BkSp"
        m_arrButtonCode(27) = 16: m_arrButtonKey(27) = "Tab"
        m_arrButtonCode(28) = 17: m_arrButtonKey(28) = "Q"
        m_arrButtonCode(29) = 18: m_arrButtonKey(29) = "W"
        m_arrButtonCode(30) = 19: m_arrButtonKey(30) = "E"
        m_arrButtonCode(31) = 20: m_arrButtonKey(31) = "R"
        m_arrButtonCode(32) = 21: m_arrButtonKey(32) = "T"
        m_arrButtonCode(33) = 22: m_arrButtonKey(33) = "Y"
        m_arrButtonCode(34) = 23: m_arrButtonKey(34) = "U"
        m_arrButtonCode(35) = 24: m_arrButtonKey(35) = "I"
        m_arrButtonCode(36) = 25: m_arrButtonKey(36) = "O"
        m_arrButtonCode(37) = 26: m_arrButtonKey(37) = "P"
        m_arrButtonCode(38) = 27: m_arrButtonKey(38) = "BracketLeft"
        m_arrButtonCode(39) = 28: m_arrButtonKey(39) = "BracketRight"
        m_arrButtonCode(40) = 44: m_arrButtonKey(40) = "Backslash"
        m_arrButtonCode(41) = 59: m_arrButtonKey(41) = "CapsLock"
        m_arrButtonCode(42) = 31: m_arrButtonKey(42) = "A"
        m_arrButtonCode(43) = 32: m_arrButtonKey(43) = "S"
        m_arrButtonCode(44) = 33: m_arrButtonKey(44) = "D"
        m_arrButtonCode(45) = 34: m_arrButtonKey(45) = "F"
        m_arrButtonCode(46) = 35: m_arrButtonKey(46) = "G"
        m_arrButtonCode(47) = 36: m_arrButtonKey(47) = "H"
        m_arrButtonCode(48) = 37: m_arrButtonKey(48) = "J"
        m_arrButtonCode(49) = 38: m_arrButtonKey(49) = "K"
        m_arrButtonCode(50) = 39: m_arrButtonKey(50) = "L"
        m_arrButtonCode(51) = 40: m_arrButtonKey(51) = "Semicolon"
        m_arrButtonCode(52) = 41: m_arrButtonKey(52) = "Apostrophe"
        m_arrButtonCode(53) = 29: m_arrButtonKey(53) = "Enter"
        m_arrButtonCode(54) = 43: m_arrButtonKey(54) = "ShiftLeft"
        m_arrButtonCode(55) = 45: m_arrButtonKey(55) = "Z"
        m_arrButtonCode(56) = 46: m_arrButtonKey(56) = "X"
        m_arrButtonCode(57) = 47: m_arrButtonKey(57) = "C"
        m_arrButtonCode(58) = 48: m_arrButtonKey(58) = "V"
        m_arrButtonCode(59) = 49: m_arrButtonKey(59) = "B"
        m_arrButtonCode(60) = 50: m_arrButtonKey(60) = "N"
        m_arrButtonCode(61) = 51: m_arrButtonKey(61) = "M"
        m_arrButtonCode(62) = 52: m_arrButtonKey(62) = "Comma"
        m_arrButtonCode(63) = 53: m_arrButtonKey(63) = "Period"
        m_arrButtonCode(64) = 54: m_arrButtonKey(64) = "Slash"
        m_arrButtonCode(65) = 55: m_arrButtonKey(65) = "ShiftRight"
        m_arrButtonCode(66) = 30: m_arrButtonKey(66) = "CtrlLeft"
        m_arrButtonCode(67) = 348: m_arrButtonKey(67) = "WinLeft"
        m_arrButtonCode(68) = 58: m_arrButtonKey(68) = "Spacebar"
        m_arrButtonCode(69) = 349: m_arrButtonKey(69) = "WinRight"
        m_arrButtonCode(70) = 350: m_arrButtonKey(70) = "Menu"
        m_arrButtonCode(71) = 286: m_arrButtonKey(71) = "CtrlRight"
        m_arrButtonCode(72) = 339: m_arrButtonKey(72) = "Ins"
        m_arrButtonCode(73) = 328: m_arrButtonKey(73) = "Home"
        m_arrButtonCode(74) = 330: m_arrButtonKey(74) = "PgUp"
        m_arrButtonCode(75) = 340: m_arrButtonKey(75) = "Del"
        m_arrButtonCode(76) = 336: m_arrButtonKey(76) = "End"
        m_arrButtonCode(77) = 338: m_arrButtonKey(77) = "PgDn"
        m_arrButtonCode(78) = 329: m_arrButtonKey(78) = "Up"
        m_arrButtonCode(79) = 332: m_arrButtonKey(79) = "Left"
        m_arrButtonCode(80) = 337: m_arrButtonKey(80) = "Down"
        m_arrButtonCode(81) = 334: m_arrButtonKey(81) = "Right"
        m_arrButtonCode(82) = 71: m_arrButtonKey(82) = "ScrollLock"
        m_arrButtonCode(83) = 326: m_arrButtonKey(83) = "NumLock"
        m_arrButtonCode(84) = 310: m_arrButtonKey(84) = "KeypadSlash"
        m_arrButtonCode(85) = 56: m_arrButtonKey(85) = "KeypadMultiply"
        m_arrButtonCode(86) = 75: m_arrButtonKey(86) = "KeypadMinus"
        m_arrButtonCode(87) = 72: m_arrButtonKey(87) = "Keypad7Home"
        m_arrButtonCode(88) = 73: m_arrButtonKey(88) = "Keypad8Up"
        m_arrButtonCode(89) = 74: m_arrButtonKey(89) = "Keypad9PgUp"
        m_arrButtonCode(90) = 79: m_arrButtonKey(90) = "KeypadPlus"
        m_arrButtonCode(91) = 76: m_arrButtonKey(91) = "Keypad4Left"
        m_arrButtonCode(92) = 77: m_arrButtonKey(92) = "Keypad5"
        m_arrButtonCode(93) = 78: m_arrButtonKey(93) = "Keypad6Right"
        m_arrButtonCode(94) = 80: m_arrButtonKey(94) = "Keypad1End"
        m_arrButtonCode(95) = 81: m_arrButtonKey(95) = "Keypad2Down"
        m_arrButtonCode(96) = 82: m_arrButtonKey(96) = "Keypad3PgDn"
        m_arrButtonCode(97) = 285: m_arrButtonKey(97) = "KeypadEnter"
        m_arrButtonCode(98) = 83: m_arrButtonKey(98) = "Keypad0Ins"
        m_arrButtonCode(99) = 84: m_arrButtonKey(99) = "KeypadPeriodDel"

        ' not sure if this works:
        '' CODE(S) DETECTED WITH _KEYDOWN:
        'm_arrButtonCode(100) = -1 : m_arrButtonCode(100) = "F10"

        ' not sure if this works:
        '' CODE(S) DETECTED WITH _KEYHIT:
        'm_arrButtonCode(101) = -2 : m_arrButtonCode(101) = "AltLeft"
        'm_arrButtonCode(102) = -3 : m_arrButtonCode(102) = "AltRight"

        ' DESCRIPTIONS BY KEYCODE
        For iLoop = LBound(m_arrButtonKeyDesc) To UBound(m_arrButtonKeyDesc)
            m_arrButtonKeyDesc(iLoop) = ""
        Next iLoop
        m_arrButtonKeyDesc(2) = "Esc"
        m_arrButtonKeyDesc(60) = "F1"
        m_arrButtonKeyDesc(61) = "F2"
        m_arrButtonKeyDesc(62) = "F3"
        m_arrButtonKeyDesc(63) = "F4"
        m_arrButtonKeyDesc(64) = "F5"
        m_arrButtonKeyDesc(65) = "F6"
        m_arrButtonKeyDesc(66) = "F7"
        m_arrButtonKeyDesc(67) = "F8"
        m_arrButtonKeyDesc(68) = "F9"
        m_arrButtonKeyDesc(88) = "F11"
        m_arrButtonKeyDesc(89) = "F12"
        m_arrButtonKeyDesc(42) = "Tilde"
        m_arrButtonKeyDesc(3) = "1"
        m_arrButtonKeyDesc(4) = "2"
        m_arrButtonKeyDesc(5) = "3"
        m_arrButtonKeyDesc(6) = "4"
        m_arrButtonKeyDesc(7) = "5"
        m_arrButtonKeyDesc(8) = "6"
        m_arrButtonKeyDesc(9) = "7"
        m_arrButtonKeyDesc(10) = "8"
        m_arrButtonKeyDesc(11) = "9"
        m_arrButtonKeyDesc(12) = "0"
        m_arrButtonKeyDesc(13) = "Minus"
        m_arrButtonKeyDesc(14) = "Equal"
        m_arrButtonKeyDesc(15) = "BkSp"
        m_arrButtonKeyDesc(16) = "Tab"
        m_arrButtonKeyDesc(17) = "Q"
        m_arrButtonKeyDesc(18) = "W"
        m_arrButtonKeyDesc(19) = "E"
        m_arrButtonKeyDesc(20) = "R"
        m_arrButtonKeyDesc(21) = "T"
        m_arrButtonKeyDesc(22) = "Y"
        m_arrButtonKeyDesc(23) = "U"
        m_arrButtonKeyDesc(24) = "I"
        m_arrButtonKeyDesc(25) = "O"
        m_arrButtonKeyDesc(26) = "P"
        m_arrButtonKeyDesc(27) = "BracketLeft"
        m_arrButtonKeyDesc(28) = "BracketRight"
        m_arrButtonKeyDesc(44) = "Backslash"
        m_arrButtonKeyDesc(59) = "CapsLock"
        m_arrButtonKeyDesc(31) = "A"
        m_arrButtonKeyDesc(32) = "S"
        m_arrButtonKeyDesc(33) = "D"
        m_arrButtonKeyDesc(34) = "F"
        m_arrButtonKeyDesc(35) = "G"
        m_arrButtonKeyDesc(36) = "H"
        m_arrButtonKeyDesc(37) = "J"
        m_arrButtonKeyDesc(38) = "K"
        m_arrButtonKeyDesc(39) = "L"
        m_arrButtonKeyDesc(40) = "Semicolon"
        m_arrButtonKeyDesc(41) = "Apostrophe"
        m_arrButtonKeyDesc(29) = "Enter"
        m_arrButtonKeyDesc(43) = "ShiftLeft"
        m_arrButtonKeyDesc(45) = "Z"
        m_arrButtonKeyDesc(46) = "X"
        m_arrButtonKeyDesc(47) = "C"
        m_arrButtonKeyDesc(48) = "V"
        m_arrButtonKeyDesc(49) = "B"
        m_arrButtonKeyDesc(50) = "N"
        m_arrButtonKeyDesc(51) = "M"
        m_arrButtonKeyDesc(52) = "Comma"
        m_arrButtonKeyDesc(53) = "Period"
        m_arrButtonKeyDesc(54) = "Slash"
        m_arrButtonKeyDesc(55) = "ShiftRight"
        m_arrButtonKeyDesc(30) = "CtrlLeft"
        m_arrButtonKeyDesc(348) = "WinLeft"
        m_arrButtonKeyDesc(58) = "Spacebar"
        m_arrButtonKeyDesc(349) = "WinRight"
        m_arrButtonKeyDesc(350) = "Menu"
        m_arrButtonKeyDesc(286) = "CtrlRight"
        m_arrButtonKeyDesc(339) = "Ins"
        m_arrButtonKeyDesc(328) = "Home"
        m_arrButtonKeyDesc(330) = "PgUp"
        m_arrButtonKeyDesc(340) = "Del"
        m_arrButtonKeyDesc(336) = "End"
        m_arrButtonKeyDesc(338) = "PgDn"
        m_arrButtonKeyDesc(329) = "Up"
        m_arrButtonKeyDesc(332) = "Left"
        m_arrButtonKeyDesc(337) = "Down"
        m_arrButtonKeyDesc(334) = "Right"
        m_arrButtonKeyDesc(71) = "ScrollLock"
        m_arrButtonKeyDesc(326) = "NumLock"
        m_arrButtonKeyDesc(310) = "KeypadSlash"
        m_arrButtonKeyDesc(56) = "KeypadMultiply"
        m_arrButtonKeyDesc(75) = "KeypadMinus"
        m_arrButtonKeyDesc(72) = "Keypad7Home"
        m_arrButtonKeyDesc(73) = "Keypad8Up"
        m_arrButtonKeyDesc(74) = "Keypad9PgUp"
        m_arrButtonKeyDesc(79) = "KeypadPlus"
        m_arrButtonKeyDesc(76) = "Keypad4Left"
        m_arrButtonKeyDesc(77) = "Keypad5"
        m_arrButtonKeyDesc(78) = "Keypad6Right"
        m_arrButtonKeyDesc(80) = "Keypad1End"
        m_arrButtonKeyDesc(81) = "Keypad2Down"
        m_arrButtonKeyDesc(82) = "Keypad3PgDn"
        m_arrButtonKeyDesc(285) = "KeypadEnter"
        m_arrButtonKeyDesc(83) = "Keypad0Ins"
        m_arrButtonKeyDesc(84) = "KeypadPeriodDel"

        m_bInitialized = TRUE
    End If
End Sub ' InitKeyboardButtonCodes

' /////////////////////////////////////////////////////////////////////////////
' not sure if this works

' Returns TRUE if the F10 key is held down.
' We use _KEYDOWN for this because _BUTTON doesn't detect F10.

' Constant must be declared globally:
' Const c_iKeyDown_F10 = 17408

Function KeydownF10%
    Dim iCode As Long
    '_KEYCLEAR: _DELAY 1
    If _KeyDown(c_iKeyDown_F10) = TRUE Then
        KeydownF10% = TRUE
    Else
        KeydownF10% = FALSE
    End If
    '_KEYCLEAR
End Function ' KeydownF10%

' /////////////////////////////////////////////////////////////////////////////
' not sure if this works

' Returns TRUE if the left ALT key is held down.
' We use _KEYHIT for this because _BUTTON doesn't detect ALT.

' Constant must be declared globally:
' Const c_iKeyHit_AltLeft = -30764

Function KeyhitAltLeft%
    '_KEYCLEAR: _DELAY 1
    If _KeyHit = c_iKeyHit_AltLeft Then
        KeyhitAltLeft% = TRUE
    Else
        KeyhitAltLeft% = FALSE
    End If
    '_KEYCLEAR
End Function ' KeyhitAltLeft%

' /////////////////////////////////////////////////////////////////////////////
' not sure if this works

' Returns TRUE if the right ALT key is held down.
' We use _KEYHIT for this because _BUTTON doesn't detect ALT.

' Constant must be declared globally:
' Const c_iKeyHit_AltRight = -30765

Function KeyhitAltRight%
    '_KEYCLEAR: _DELAY 1
    If _KeyHit = c_iKeyHit_AltRight Then
        KeyhitAltRight% = TRUE
    Else
        KeyhitAltRight% = FALSE
    End If
    '_KEYCLEAR
End Function ' KeyhitAltRight%

' /////////////////////////////////////////////////////////////////////////////
' DEVICES Button
' _LASTBUTTON(1) keyboards will normally return 512 buttons. One button is read per loop through all numbers.
' _BUTTONCHANGE(number) returns -1 when pressed, 1 when released and 0 when there is no event since the last read.
' _BUTTON(number) returns -1 when a button is pressed and 0 when released

' Detects most keys (where the codes are documented?)

' However, does not seem to detect:
' Key             Use
' ---             ---
' F10             Function KeydownF10%
' Left Alt        Function KeyhitAltLeft%
' Right Alt       Function KeyhitAltRight%
' Print Screen    (system API call?)
' Pause/Break     (system API call?)

Function KeyPressed% (iCode As Integer)
    '_KEYCLEAR: _DELAY 1
    While _DeviceInput(1): Wend ' clear and update the keyboard buffer
    If _Button(iCode) <> FALSE Then
        KeyPressed% = TRUE
    Else
        KeyPressed% = FALSE
    End If
    '_KEYCLEAR
End Function ' KeyPressed%

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

Function TestJoysticks$
    Dim RoutineName As String: RoutineName = "TestJoysticks$"
    Dim iDeviceCount As Integer
    Dim sResult As String

    ' 1 is the keyboard
    ' 2 is the mouse
    ' 3 is the joystick
    ' unless someone has a strange setup with multiple mice/keyboards/ect...
    ' In that case, you can use _DEVICE$(i) to look for "KEYBOARD", "MOUSE", "JOYSTICK", if necessary.
    ' I've never actually found it necessary, but I figure it's worth mentioning, just in case...

    iDeviceCount = _Devices ' Find the number of devices on someone's system
    If iDeviceCount > 2 Then
        TestJoysticks1
        sResult = ""
    Else
        sResult = "No joysticks found."
    End If

    _KeyClear

    TestJoysticks$ = sResult
End Function ' TestJoysticks$

' /////////////////////////////////////////////////////////////////////////////
' Reads controllers and displays values on screen.

' Currently this is set up to support up to 8 joysticks,
' with upto 4 buttons and 2 axes each
' Testing with an old USB Logitech RumblePad 2
' and Atari 2600 joysticks plugged into using
' iCode Atari Joystick, Paddle, Driving to USB Adapter 4 ports

Sub TestJoysticks1 ()
    Dim RoutineName As String:: RoutineName = "TestJoysticks1"

    Dim in$
    Dim iDeviceCount As Integer
    Dim iDevice As Integer

    Dim arrButton(32, 16) As Integer ' number of buttons on the joystick
    Dim arrButtonMin(32, 16) As Integer ' stores the minimum value read
    Dim arrButtonMax(32, 16) As Integer ' stores the maximum value read
    Dim arrAxis(32, 16) As Double ' number of axis on the joystick
    Dim arrAxisMin(32, 16) As Double ' stores the minimum value read
    Dim arrAxisMax(32, 16) As Double ' stores the maximum value read
    Dim arrAxisAvg(32, 16) As Double ' stores the average value read in the last few measurements
    Dim arrButtonNew(32, 16) As Integer ' tracks when to initialize values
    Dim arrAxisNew(32, 16) As Integer ' tracks when to initialize values

    Dim arrController(8) As ControllerType ' holds info for each player
    Dim iNumControllers As Integer
    Dim iController As Integer
    Dim iNextY As Integer
    Dim iNextX As Integer
    Dim iNextC As Integer
    Dim iLoop As Integer
    Dim iDigits As Integer ' # digits to display (values are truncated to this length)
    Dim strValue As String
    Dim strAxis As String
    Dim dblNextAxis
    'DIM iMeasureCount AS INTEGER
    Dim dblAverage As Double
    Dim sngAverage As Single
    Dim sLine As String
    Dim iX As Integer
    Dim iY As Integer

    Dim iCol As Integer
    Dim iRow As Integer
    Dim iCols As Integer
    Dim iRows As Integer
    Dim iColWidth As Integer
    Dim iColCount As Integer
    Dim iGroupCount As Integer

    ' SET UP SCREEN
    Screen _NewImage(1280, 1024, 32): _ScreenMove 0, 0

    ' INITIALIZE
    iDigits = 4 ' 11
    iColCount = 3
    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight
    iColWidth = iCols \ iColCount

    ' COUNT # OF JOYSTICKS
    ' D= _DEVICES ' MUST be read in order for other 2 device functions to work!
    iDeviceCount = _Devices ' Find the number of devices on someone's system
    If iDeviceCount < 3 Then
        Cls
        Print "NO JOYSTICKS FOUND, EXITING..."
        Input "PRESS <ENTER>"; in$
        Exit Sub
    End If

    ' BASE # OF PLAYERS ON HOW MANY CONTROLLERS FOUND
    iNumControllers = iDeviceCount - 2 ' TODO: find out the right way to count joysticks
    If iNumControllers > cMaxControllers Then
        iNumControllers = cMaxControllers
    End If

    ' INITIALIZE PLAYER COORDINATES AND SCREEN CHARACTERS
    iNextY = 1
    iNextX = -3
    iNextC = 64
    For iController = 1 To iNumControllers
        iNextX = iNextX + 4
        If iNextX > 80 Then
            iNextX = 1
            iNextY = iNextY + 4
        End If
        iNextC = iNextC + 1
        arrController(iController).buttonCount = cMaxButtons
        arrController(iController).axisCount = cMaxAxis

        For iLoop = 1 To cMaxButtons
            arrButtonNew(iController, iLoop) = TRUE
        Next iLoop
        For iLoop = 1 To cMaxAxis
            arrAxisNew(iController, iLoop) = TRUE
            arrAxisAvg(iController, iLoop) = 0
        Next iLoop
    Next iController

    ' CLEAR THE SCREEN
    'iMeasureCount = 0
    Do
        For iController = 1 To iNumControllers
            iDevice = iController + 2

            While _DeviceInput(iDevice) ' clear and update the device buffer
                ''IF _DEVICEINPUT = 3 THEN ' this says we only care about joystick input values

                ' check all the buttons
                For iLoop = 1 To _LastButton(iDevice)
                    If (iLoop > cMaxButtons) Then
                        Exit For
                    End If
                    arrController(iController).buttonCount = iLoop

                    ' update button array to indicate if a button is up or down currently.
                    If _ButtonChange(iLoop) Then
                        '' _BUTTON(number) returns -1 when a button is pressed and 0 when released.
                        ''arrButton(iLoop) = NOT arrButton(iLoop)
                        arrButton(iController, iLoop) = _Button(iLoop)
                    End If

                    '' SAVE MINIMUM VALUE
                    'if arrButton(iController, iLoop) < arrButtonMin(iController, iLoop) then
                    '    arrButtonMin(iController, iLoop) = arrButton(iController, iLoop)
                    '
                    '    ' INITIALIZE THE MAX TO THE MINIMUM VALUE
                    '    IF arrButtonNew(iController, iLoop) = TRUE THEN
                    '        arrButtonMax(iController, iLoop) = arrButtonMin(iController, iLoop)
                    '        arrButtonNew(iController, iLoop) = FALSE
                    '    END IF
                    'end if
                    '
                    '' SAVE MAXIMUM VALUE
                    'if arrButton(iController, iLoop) > arrButtonMax(iController, iLoop) then
                    '    arrButtonMax(iController, iLoop) = arrButton(iController, iLoop)
                    'end if

                Next iLoop

                For iLoop = 1 To _LastAxis(iDevice) ' this loop checks all my axis
                    If (iLoop > cMaxAxis) Then
                        Exit For
                    End If
                    arrController(iController).axisCount = iLoop

                    ' I like to give a little "jiggle" resistance to my controls, as I have an old joystick
                    ' which is prone to always give minute values and never really center on true 0.
                    ' A value of 1 means my axis is pushed fully in one direction.
                    ' A value greater than 0.1 means it's been partially pushed in a direction (such as at a 45 degree diagional angle).
                    ' A value of less than 0.1 means we count it as being centered. (As if it was 0.)
                    'IF ABS(_AXIS(iLoop)) <= 1 AND ABS(_AXIS(iLoop)) >= .1 THEN

                    dblNextAxis = _Axis(iLoop)
                    dblNextAxis = RoundUpDouble#(dblNextAxis, 3)
                    'IF ABS(dblNextAxis) <= 1 AND ABS(dblNextAxis) >= .01 THEN
                    If Abs(dblNextAxis) <= 1 And Abs(dblNextAxis) >= .001 Then
                        arrAxis(iController, iLoop) = dblNextAxis
                    Else
                        arrAxis(iController, iLoop) = 0
                    End If

                    '' SAVE MINIMUM VALUE
                    'if arrAxis(iController, iLoop) < arrAxisMin(iController, iLoop) then
                    '    arrAxisMin(iController, iLoop) = arrAxis(iController, iLoop)
                    '
                    '    ' INITIALIZE THE MAX TO THE MINIMUM VALUE
                    '    IF arrAxisNew(iController, iLoop) = TRUE THEN
                    '        arrAxisMax(iController, iLoop) = arrAxisMin(iController, iLoop)
                    '        arrAxisNew(iController, iLoop) = FALSE
                    '    END IF
                    'end if
                    '
                    '' SAVE MAXIMUM VALUE
                    'if arrAxis(iController, iLoop) > arrAxisMax(iController, iLoop) then
                    '    arrAxisMax(iController, iLoop) = arrAxis(iController, iLoop)
                    'end if
                    '
                    '' ADD CURRENT VALUE TO AVERAGE SUM
                    'arrAxisAvg(iController, iLoop) = arrAxisAvg(iController, iLoop) + arrAxis(iController, iLoop)

                Next iLoop
            Wend ' clear and update the device buffer

        Next iController

        'PRINT "*** iNumControllers=" + cstr$(iNumControllers) + " ***"
        'iMeasureCount = iMeasureCount + 1
        'if iMeasureCount = 10 then
        'iMeasureCount = 0

        ' And below here is just the simple display routine which displays our values.
        ' If this was for a game, I'd choose something like arrAxis(1) = -1 for a left arrow style input,
        ' arrAxis(1) = 1 for a right arrow style input, rather than just using _KEYHIT or INKEY$.

        InitColumns iColCount
        m_StartRow = 6
        m_EndRow = iRows - 2
        'm_StartCol
        'm_EndCol

        Cls
        PrintString 1, 1, "Game controller test program."
        PrintString 1, 2, "This program is free to use and distribute per GNU GPLv3 license."
        PrintString 1, 3, "Tests up to 4 controllers with 2 axes / 2 buttons each."
        PrintString 1, 4, "Plug in controllers and move them & press buttons."
        PrintString 1, 5, "-------------------------------------------------------------------------------"

        iGroupCount = 0

        For iController = 1 To iNumControllers
            For iLoop = 1 To arrController(iController).axisCount ' A loop for each axis
                strAxis = Right$("  " + cstr$(iLoop), 2)

                sLine = ""

                ' display their status to the screen
                sLine = sLine + "Player " + cstr$(iController)

                strValue = FormatNumber$(arrAxis(iController, iLoop), iDigits)
                sLine = sLine + ",   Axis #" + strAxis + " = " + strValue

                'strValue = FormatNumber$(arrAxisMin(iController, iLoop), iDigits)
                'sLine = sLine + ", Min=" + strValue
                '
                'strValue = FormatNumber$(arrAxisMax(iController, iLoop), iDigits)
                'sLine = sLine + ", Max=" + strValue
                '
                '' COMPUTE AVERAGE
                'dblAverage = arrAxisAvg(iController, iLoop) / 10
                'dblAverage = RoundUpDouble# (dblAverage, 3)
                'strValue = FormatNumber$(dblAverage, iDigits)
                'sLine = sLine + ", Avg=" + strValue
                '
                '' CLEAR THE AVERAGE
                'arrAxisAvg(iController, iLoop) = 0

                PrintColumn sLine
            Next iLoop
            For iLoop = 1 To arrController(iController).buttonCount ' A loop for each button
                strAxis = Right$("  " + cstr$(iLoop), 2)

                sLine = ""

                ' display their status to the screen
                sLine = sLine + "Player " + cstr$(iController)

                strValue = FormatNumber$(arrButton(iController, iLoop), iDigits)
                sLine = sLine + ", Button #" + strAxis + " = " + strValue

                'strValue = FormatNumber$(arrButtonMin(iController, iLoop), iDigits)
                'sLine = sLine + ", Min=" + strValue
                '
                'strValue = FormatNumber$(arrButtonMax(iController, iLoop), iDigits)
                'sLine = sLine + ", Max=" + strValue

                PrintColumn sLine
            Next iLoop

            iGroupCount = iGroupCount + 1
            If iGroupCount = 2 Then
                ColumnBreak
                iGroupCount = 0
            End If

        Next iController

        PrintString 1, iRows - 1, "-------------------------------------------------------------------------------"
        PrintString 1, iRows - 0, "PRESS <ESC> TO EXIT"

        'end if

        _Limit 30
    Loop Until _KeyHit = 27 ' ESCAPE to quit

    ' RETURN TO TEXT SCREEN
    Screen 0
End Sub ' TestJoysticks1

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN FILE FUNCTIONS
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' /////////////////////////////////////////////////////////////////////////////
' File format is comma-delimited
' containing controller info for one action per line
' where each line contains the following in this order:

' TAB ORDER   INFO             TYPE      DESCRIPTION
' 1           {player #}       Integer   player # this mapping is for
' 2           {which action}   Integer   which action this mapping is for (up/down/right/left/button 1/button 2, etc.)
' 3           {device #}       Integer   number of the device this is mapped to
' 4           {type}           Integer   type of input (one of: cInputKey, cInputButton, cInputAxis)
' 5           {code}           Integer   if button the _BUTTON #, if axis the _AXIS #, if keyboard the _BUTTON #
' 6           {value}          Integer   if axis, the value (-1 or 1), else can be ignored
' 7           {repeat}         Integer   if TRUE, and repeating keys not controlled by global values (when m_bRepeatOverride=TRUE), controls repeating keys for this control

' These need to be declared globally and populated:
'     ReDim Shared m_arrControlMap(1 To 8, 1 To 8) As ControlInputType
'     Dim Shared m_ControlMapFileName$: m_ControlMapFileName$ = Left$(m_ProgramName$, _InStrRev(m_ProgramName$, ".")) + "map.txt"
'     Dim Shared m_bRepeatOverride As Integer

' If there is an error, returns error message,
' else returns blank string.

Function SaveControllerMap$
    Dim RoutineName As String:: RoutineName = "SaveControllerMap$"
    Dim sResult As String: sResult = ""
    Dim sError As String: sError = ""
    Dim sFile As String
    Dim in$
    Dim iPlayer As Integer
    Dim iWhichInput As Integer
    Dim sLine As String
    Dim iCount As Long: iCount = 0
    'Dim iError As Long: iError = 0
    Dim sDelim As String: sDelim = "," ' CHR$(9)

    'DebugPrint "--------------------------------------------------------------------------------"
    'DebugPrint "Started " + RoutineName
    'DebugPrint "--------------------------------------------------------------------------------"

    ' Get file name
    If Len(m_ControlMapFileName$) = 0 Then
        m_ControlMapFileName$ = Left$(m_ProgramName$, _InStrRev(m_ProgramName$, ".")) + "map.txt"
    End If
    sFile = Mid$(m_ControlMapFileName$, _InStrRev(m_ControlMapFileName$, "\") + 1)

    '_KeyClear
    'Cls
    'Print "SAVE CONTROLLER MAPPING:"
    'Print "Default file name is " + Chr$(34) + m_ControlMapFileName$ + Chr$(34) + "."
    'Input "Type save file name, or blank for default: ", in$
    'in$ = _Trim$(in$)
    'If Len(in$) > 0 Then
    '    m_ControlMapFileName$ = in$
    'End If
    'sFile = m_ProgramPath$ + m_ControlMapFileName$

    'DebugPrint "m_ControlMapFileName$=" + CHR$(34) + m_ControlMapFileName$ + CHR$(34)

    ' Save mapping to file
    Open m_ControlMapFileName$ For Output As #1

    For iPlayer = LBound(m_arrControlMap, 1) To UBound(m_arrControlMap, 1)
        For iWhichInput = LBound(m_arrControlMap, 2) To UBound(m_arrControlMap, 2)
            sLine = ""

            sLine = sLine + _Trim$(Str$(iPlayer))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(iWhichInput))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).device))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).typ))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).code))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).value))
            sLine = sLine + sDelim

            sLine = sLine + _Trim$(Str$(m_arrControlMap(iPlayer, iWhichInput).repeat))

            Print #1, sLine
            iCount = iCount + 1
        Next iWhichInput
    Next iPlayer

    Close #1

    'DebugPrint "Wrote   " + _Trim$(Str$(iCount)) + " lines."
    'Print "Skipped " + _Trim$(Str$(iError)) + " lines."
    'DebugPrint ""
    'Input "PRESS <ENTER> TO CONTINUE", in$

    If Len(sError) = 0 Then
        sResult = "Saved mapping file " + Chr$(34) + sFile + Chr$(34) + "."
    Else
        sResult = "ERRORS: " + sError
    End If

    SaveControllerMap$ = sResult
End Function ' SaveControllerMap$

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

Function LoadControllerMap$
    Dim RoutineName As String:: RoutineName = "LoadControllerMap$"
    Dim sResult As String: sResult = ""
    Dim sError As String: sError = ""
    Dim sNextErr As String

    Dim sFile As String
    Dim sText As String
    Dim iTotal As Long: iTotal = 0

    Dim iRead As Long: iRead = 0
    Dim iValid As Long: iValid = 0
    Dim iBad As Long: iBad = 0
    Dim iBlank As Long: iBlank = 0

    Dim sLine As String
    ReDim arrNextLine(-1) As String
    Dim iNumValues As Integer
    Dim iAdjust As Integer

    Dim iPlayer As Integer
    Dim iWhichInput As Integer
    Dim iDevice As Integer
    Dim iType As Integer
    Dim iCode As Integer
    Dim iValue As Integer
    Dim bRepeat As Integer
    'Dim sDebugLine As String

    'DebugPrint "--------------------------------------------------------------------------------"
    'DebugPrint "Started " + RoutineName
    'DebugPrint "--------------------------------------------------------------------------------"

    ' Get file name
    If Len(sError) = 0 Then
        If Len(m_ControlMapFileName$) = 0 Then
            m_ControlMapFileName$ = Left$(m_ProgramName$, _InStrRev(m_ProgramName$, ".")) + "map.txt"
        End If
        sFile = Mid$(m_ControlMapFileName$, _InStrRev(m_ControlMapFileName$, "\") + 1)
    End If

    '' Get file name
    'If Len(sError) = 0 Then
    '    Cls
    '    If Len(m_ControlMapFileName$) = 0 Then
    '        m_ControlMapFileName$ = Left$(m_ProgramName$, _InStrRev(m_ProgramName$, ".")) + "map.txt"
    '    End If
    '    Print "LOAD CONTROLLER MAPPING:"
    '    Print "Default file name is " + Chr$(34) + m_ControlMapFileName$ + Chr$(34) + "."
    '    Input "Type name of file to open, or blank for default: ", in$
    '    in$ = _Trim$(in$)
    '    If Len(in$) > 0 Then
    '        m_ControlMapFileName$ = in$
    '    End If
    '    sFile = m_ProgramPath$ + m_ControlMapFileName$
    'End If

    ' Make sure file exists
    If Len(sError) = 0 Then
        If _FileExists(m_ControlMapFileName$) = FALSE Then
            sError = "File not found: " + Chr$(34) + m_ControlMapFileName$ + Chr$(34)
        Else
            'DebugPrint "Found file: " + chr$(34) + m_ControlMapFileName$ + chr$(34)
        End If
    End If

    ' Read data from file
    If Len(sError) = 0 Then
        'DebugPrint "OPEN m_ControlMapFileName$ FOR BINARY AS #1"

        Open m_ControlMapFileName$ For Binary As #1
        sText = Space$(LOF(1))
        Get #1, , sText
        Close #1
        iTotal = Len(sText) - Len(Replace$(sText, Chr$(13), ""))
        sText = ""

        Open m_ControlMapFileName$ For Input As #1
        While Not EOF(1)
            'INPUT #1, sLine
            Line Input #1, sLine ' read entire text file line

            iRead = iRead + 1
            'DebugPrint "Parsing line " + _Trim$(Str$(iRead)) + _
            '    " of " + _Trim$(Str$(iTotal))

            sLine = Replace$(sLine, " ", "") ' Remove spaces
            sLine = Replace$(sLine, Chr$(9), "") ' Remove tabs
            sLine = Replace$(sLine, Chr$(10), "") ' Remove line breaks
            sLine = Replace$(sLine, Chr$(13), "") ' Remove carriage returns
            'DebugPrint "    Trimmed=" + chr$(34) + sLine + chr$(34)

            If Len(sLine) > 0 Then
                split sLine, ",", arrNextLine()
                'DebugPrint "split into arrNextLine()"
                'DebugPrint "    lbound =" + _Trim$(Str$(lbound(arrNextLine))) '+ CHR$(10)
                'DebugPrint "    ubound =" + _Trim$(Str$(ubound(arrNextLine))) '+ CHR$(10)

                iNumValues = (UBound(arrNextLine) - LBound(arrNextLine)) + 1
                If iNumValues > 5 Then
                    iAdjust = -1 '- lbound(arrNextLine)

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(1 + iAdjust)) = TRUE Then
                            iPlayer = Val(arrNextLine(1 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 1: not a number"
                        End If
                    End If

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(2 + iAdjust)) = TRUE Then
                            iWhichInput = Val(arrNextLine(2 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 2: not a number"
                        End If
                    End If

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(3 + iAdjust)) = TRUE Then
                            iDevice = Val(arrNextLine(3 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 3: not a number"
                        End If
                    End If

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(4 + iAdjust)) = TRUE Then
                            iType = Val(arrNextLine(4 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 4: not a number"
                        End If
                    End If

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(5 + iAdjust)) = TRUE Then
                            iCode = Val(arrNextLine(5 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 5: not a number"
                        End If
                    End If

                    If Len(sNextErr) = 0 Then
                        If IsNum%(arrNextLine(6 + iAdjust)) = TRUE Then
                            iValue = Val(arrNextLine(6 + iAdjust))
                        Else
                            sNextErr = "Error on line " + cstr$(iRead) + ", value 6: not a number"
                        End If
                    End If

                    ' validate iPlayer
                    If Len(sNextErr) = 0 Then
                        If iPlayer < LBound(m_arrControlMap, 1) Then
                            sNextErr = "Player value " + _Trim$(Str$(iPlayer)) + _
                                " is outside lbound(m_arrControlMap, 1) " + _
                                " which is " + _Trim$(Str$(lbound(m_arrControlMap, 1))) + "."
                        ElseIf iPlayer > UBound(m_arrControlMap, 1) Then
                            sNextErr = "Player value " + _Trim$(Str$(iPlayer)) + _
                                " is outside ubound(m_arrControlMap, 1) " + _
                                " which is " + _Trim$(Str$(ubound(m_arrControlMap, 1))) + "."
                        End If
                    End If

                    ' validate iWhichInput
                    If Len(sNextErr) = 0 Then
                        If iWhichInput < LBound(m_arrControlMap, 2) Then
                            sNextErr = "WhichInput value " + _Trim$(Str$(iWhichInput)) + _
                                " is outside lbound(m_arrControlMap, 2) " + _
                                " which is " + _Trim$(Str$(lbound(m_arrControlMap, 2))) + "."
                        ElseIf iWhichInput > UBound(m_arrControlMap, 2) Then
                            sNextErr = "WhichInput value " + _Trim$(Str$(iWhichInput)) + _
                                " is outside ubound(m_arrControlMap, 2) " + _
                                " which is " + _Trim$(Str$(ubound(m_arrControlMap, 2))) + "."
                        End If
                    End If

                    ' validate repeat setting
                    If iNumValues > 6 Then
                        If Len(sNextErr) = 0 Then
                            If IsNum%(arrNextLine(7 + iAdjust)) = TRUE Then
                                bRepeat = Val(arrNextLine(7 + iAdjust))
                            Else
                                sNextErr = "Error on line " + cstr$(iRead) + ", value 7: not a number"
                            End If
                        End If
                    Else
                        ' get values from global
                        'if m_bRepeatOverride = TRUE then
                        bRepeat = GetGlobalInputRepeatSetting%(iWhichInput)
                        'end if
                    End If
                Else
                    sNextErr = "Error on line " + cstr$(iRead) + ": detected " + cstr$(iNumValues) + " values, expected 6."
                End If

                If Len(sNextErr) = 0 Then
                    iValid = iValid + 1
                    m_arrControlMap(iPlayer, iWhichInput).device = iDevice
                    m_arrControlMap(iPlayer, iWhichInput).typ = iType
                    m_arrControlMap(iPlayer, iWhichInput).code = iCode
                    m_arrControlMap(iPlayer, iWhichInput).value = iValue
                    m_arrControlMap(iPlayer, iWhichInput).repeat = bRepeat
                Else
                    iBad = iBad + 1
                    DebugPrint sNextErr
                End If
            Else
                'DebugPrint "    Line is blank: skipped"
                iBlank = iBlank + 1
            End If ' LEN(sLine) > 0

        Wend
        Close #1
    End If

    'DebugPrint ""
    'DebugPrint "Lines read: " + _Trim$(Str$(iRead))
    'DebugPrint "Valid     : " + _Trim$(Str$(iValid))
    'DebugPrint "Invalid   : " + _Trim$(Str$(iErrors))
    'DebugPrint "Blank     : " + _Trim$(Str$(iBlank))
    'DebugPrint ""
    'Input "PRESS <ENTER> TO CONTINUE", in$

    If Len(sError) = 0 Then
        sResult = "Loaded mapping file " + Chr$(34) + sFile + Chr$(34) + "."
        m_bHaveMapping = TRUE
    Else
        sResult = "ERRORS: " + sError
    End If

    LoadControllerMap$ = sResult
End Function ' LoadControllerMap$

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

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

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

' /////////////////////////////////////////////////////////////////////////////
' A way to automatically print to columns.

' Depends on the following shared variables:
'     Dim Shared m_NumColumns As Integer : m_NumColumns = 1
'     Dim Shared m_PrintRow As Integer : m_PrintRow = 0
'     Dim Shared m_PrintCol As Integer : m_PrintCol = 0
'     Dim Shared m_StartRow As Integer : m_StartRow = 0
'     Dim Shared m_EndRow As Integer : m_EndRow = 0
'     Dim Shared m_StartCol As Integer : m_StartCol = 0
'     Dim Shared m_EndCol As Integer : m_EndCol = 0

' InitColumns 2
' m_PrintRow = 5
' m_PrintCol = 2
' PrintColumn "Col 2, Row 5"
' PrintColumn "m_NumColumns=" + cstr$(m_NumColumns)

Sub PrintColumn (MyString As String)
    Dim iCols As Integer
    Dim iRows As Integer
    Dim iX As Integer
    Dim iY As Integer

    ReDim arrLines(-1) As String
    Dim iRow As Integer
    Dim iCol As Integer
    Dim sChar As String
    Dim sLine As String
    Dim iColWidth As Integer

    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight

    If m_NumColumns < 1 Or m_NumColumns > iCols Then
        m_NumColumns = 1
    End If

    If m_StartRow < 1 Or m_StartRow > iRows Then
        m_StartRow = 1
    End If
    If m_EndRow < m_StartRow Or m_EndRow > iRows Then
        m_EndRow = iRows
    End If
    If m_StartCol < 1 Or m_StartCol > m_NumColumns Then
        m_StartCol = 1
    End If
    If m_EndCol < m_StartCol Or m_EndCol > m_NumColumns Then
        m_EndCol = m_NumColumns
    End If

    If m_PrintRow < m_StartRow Then
        m_PrintRow = m_StartRow
    End If
    If m_PrintCol < m_StartCol Then
        m_PrintCol = m_StartCol
    End If

    iColWidth = iCols \ m_NumColumns

    If m_PrintRow <= m_EndRow Then
        If m_PrintCol <= m_EndCol Then
            split MyString, Chr$(13), arrLines()
            For iRow = 0 To UBound(arrlines)
                sLine = Left$(arrLines(iRow), iColWidth)
                'TODO: wrap remaining text
                iX = _FontWidth * ((m_PrintCol - 1) * iColWidth)
                iY = _FontHeight * (m_PrintRow - 1)
                _PrintString (iX, iY), sLine

                m_PrintRow = m_PrintRow + 1
                If m_PrintRow > m_EndRow Then
                    m_PrintRow = m_StartRow
                    m_PrintCol = m_PrintCol + 1
                    If m_PrintCol > m_NumColumns Then
                        'TODO: options for when we reach the bottom of the last column (stop printing, wrap around)
                        m_PrintCol = 1
                    End If
                End If
            Next iRow
        End If
    End If
End Sub ' PrintColumn

' /////////////////////////////////////////////////////////////////////////////
' A way to automatically print to columns.

Sub ColumnBreak ()
    m_PrintRow = m_StartRow
    m_PrintCol = m_PrintCol + 1
    If m_PrintCol > m_NumColumns Then
        'TODO: options for when we go past the last column (stop printing, wrap around)
    End If
End Sub ' ColumnBreak

' /////////////////////////////////////////////////////////////////////////////
' A way to automatically print to columns.

Sub InitColumns (iNumColumns As Integer)
    Dim iCols As Integer
    Dim iRows As Integer
    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight
    If iNumColumns < 1 Or iNumColumns > iCols Then
        m_NumColumns = 1
    Else
        m_NumColumns = iNumColumns
    End If

    If m_StartRow < 1 Or m_StartRow > iRows Then
        m_StartRow = 1
    End If
    If m_EndRow < m_StartRow Or m_EndRow > iRows Then
        m_EndRow = iRows
    End If
    If m_StartCol < 1 Or m_StartCol > m_NumColumns Then
        m_StartCol = 1
    End If
    If m_EndCol < m_StartCol Or m_EndCol > m_NumColumns Then
        m_EndCol = m_NumColumns
    End If

    m_PrintRow = 1
    m_PrintCol = 1
End Sub ' InitColumns

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

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN DEBUGGING ROUTINES #DEBUGGING
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Sub DebugPrint (s$)
    If m_bTesting = TRUE Then
        _Echo s$
        'ReDim arrLines$(0)
        'dim delim$ : delim$ = Chr$(13)
        'split MyString, delim$, arrLines$()
    End If
End Sub ' DebugPrint

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END DEBUGGING ROUTINES @DEBUGGING
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

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

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

' /////////////////////////////////////////////////////////////////////////////
' Convert a Long value to string and trim it (because normal Str$ adds spaces)

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

' /////////////////////////////////////////////////////////////////////////////
' Convert a Single value to string and trim it (because normal Str$ adds spaces)

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

' /////////////////////////////////////////////////////////////////////////////
' Convert an unsigned Long value to string and trim it (because normal Str$ adds spaces)

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

' /////////////////////////////////////////////////////////////////////////////
' 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#)
    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")
        If expo% < 0 Then min$ = String$(Abs(expo%) - (dot% - 1), "0"): DP$ = "."
        For n = 1 To L%
            If Mid$(valu$, n, 1) <> "." Then num$ = num$ + Mid$(valu$, n, 1)
        Next
    Else DblToStr$ = value$: Exit Function
    End If
    DblToStr$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
End Function ' DblToStr$

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

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%

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

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

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

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

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

' /////////////////////////////////////////////////////////////////////////////
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/

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

' /////////////////////////////////////////////////////////////////////////////
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/

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

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

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

' MWheatley
' « Reply #18 on: January 01, 2019, 11:24:30 AM »

' returns 1 if string is an integer, 0 if not
Function IsNumber (text$)
    Dim i As Integer

    IsNumber = 1
    For i = 1 To Len(text$)
        If Asc(Mid$(text$, i, 1)) < 45 Or Asc(Mid$(text$, i, 1)) >= 58 Then
            IsNumber = 0
            Exit For
        ElseIf Asc(Mid$(text$, i, 1)) = 47 Then
            IsNumber = 0
            Exit For
        End If
    Next i
End Function ' IsNumber

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

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

Function join$ (in$(), delimiter$)
    Dim result$
    Dim 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$

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

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

' /////////////////////////////////////////////////////////////////////////////
' 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 DebugPrintFile (sText As String)
    Dim sFileName As String
    Dim sError As String
    Dim sOut As String

    sFileName = ProgramPath$ + ProgramName$ + ".txt"
    sError = ""
    If _FileExists(sFileName) = FALSE Then
        sOut = ""
        sOut = sOut + "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" + Chr$(13) + Chr$(10)
        sOut = sOut + "PROGRAM : " + 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$ + " DebugPrintFile FAILED: " + sError
    End If
End Sub ' DebugPrintFile

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

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

' /////////////////////////////////////////////////////////////////////////////
' Returns blank if successful else returns error message.

Function PrintFile$ (sFileName As String, sText As String, bAppend As Integer)
    'x = 1: y = 2: z$ = "Three"

    Dim sError As String: sError = ""

    If Len(sError) = 0 Then
        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
    End If
    If Len(sError) = 0 Then
        ' 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

        'PRINT "File created with data. Press a key!"
        'K$ = INPUT$(1) 'press a key

        'OPEN sFileName FOR INPUT AS #2 ' opens a file to read it
        'INPUT #2, a, b, c$
        'CLOSE #2

        'PRINT a, b, c$
        'WRITE a, b, c$
    End If

    PrintFile$ = sError
End Function ' PrintFile$

' /////////////////////////////////////////////////////////////////////////////
' Generate random value between Min and Max.
Function RandomNumber% (Min%, Max%)
    Dim NumSpread%

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

    ' GET RANDOM # Min%-Max%
    'RandomNumber = Int((Max * Rnd) + Min) ' generate number

    NumSpread% = (Max% - Min%) + 1

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

End Function ' RandomNumber%

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

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

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

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

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

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

Function Round## (num##, digits%)
    Round## = 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

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

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

Function RoundUpSingle! (num!, digits%)
    RoundUpSingle! = _Ceil(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%

' /////////////////////////////////////////////////////////////////////////////
' 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 SngToStr$ (n!)
    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")
        If expo% < 0 Then min$ = String$(Abs(expo%) - (dot% - 1), "0"): DP$ = "."
        For n = 1 To L%
            If Mid$(valu$, n, 1) <> "." Then num$ = num$ + Mid$(valu$, n, 1)
        Next
    Else SngToStr$ = value$: Exit Function
    End If
    SngToStr$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
End Function ' SngToStr$

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

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

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$(" + cstr$(iLoop%) + ") = " + Chr$(34) + out$ + Chr$(34)
    Next iLoop%
    Print

    Print "SplitAndReplaceTest finished."
End Sub ' SplitAndReplaceTest

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

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

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

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

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

' #END
' ################################################################################################################################################################
Reply


Messages In This Thread
game input mapping system v1.0 for gamepad, keyboard - by madscijr - 05-12-2023, 03:20 PM



Users browsing this thread: 1 Guest(s)