Initial commit: ROW Client source code

Game client codebase including:
- CharacterActionControl: Character and creature management
- GlobalScript: Network, items, skills, quests, utilities
- RYLClient: Main client application with GUI and event handlers
- Engine: 3D rendering engine (RYLGL)
- MemoryManager: Custom memory allocation
- Library: Third-party dependencies (DirectX, boost, etc.)
- Tools: Development utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 16:24:34 +09:00
commit e067522598
5135 changed files with 1745744 additions and 0 deletions

View File

@@ -0,0 +1,639 @@
Attribute VB_Name = "modAirHockey"
Option Explicit
Public dx As New DirectX8
Public Type HockeyPlayerInfo
Score As Long 'Current score of this player
PlayerName As String ' The name of the player
Latency As Long 'Average latency (ping time) of this player
End Type
'Declare for timeGetTime
Public Declare Function timeGetTime Lib "winmm.dll" () As Long
'Registry constants (for saving and retreiving information)
Public Const gsKeyName As String = "vbAirHockey"
Public Const gsSubKey As String = "Defaults"
Public Const gsSubKeyAudio As String = "Audio"
Public Const gsSubKeyInput As String = "Input"
Public Const gsSubKeyGraphics As String = "Graphics"
Public Const glMaxPuckSpeedConstant As Long = 10.23
'The wall locations, use these for easier collision detection
Public Const gnSideRightWallEdge As Single = -5
Public Const gnSideLeftWallEdge As Single = 5
Public Const gnNearWallEdge As Single = 9.92
Public Const gnFarWallEdge As Single = -9.92
'We also need the dimensions for the 'scoring area' so we can tell when we score
Public Const gnScoringEdgeLeft As Single = 1.35
Public Const gnScoringEdgeRight As Single = -1.35
Public Const gnPuckScored As Single = 1.15
'Radius constants for the puck and paddle
Public Const gnPuckRadius As Single = 0.46046
Public Const gnPaddleRadius As Single = 0.6
'ComputerAI Maximum velocity
Public Const gnComputerMaximumVelocity As Single = 0.43
'Winning score
Public Const glDefaultWinningScore As Long = 7
'We will ensure that we have at least a particular number of physics calculations per second
'We will lower frame rate to ensure we can calculate these physic calculations if necessary
'Number of physics calculations per second
Public Const glNumPhysicCalcPerSec As Long = 100
'Ticks between physic calcs
Public Const glNumTickForPhysicCalcs As Long = 1000 \ glNumPhysicCalcPerSec
'Minimum delay before allowing another paddle hit
Public Const glMinDelayPaddleHit = 100
'Delay time (ms) before 'helper' text appears
Public Const glDefaultDelayTime As Long = 3000
'Delay time (ms) before 'helper' text disappears
Public Const glDefaultDelayTimeGone As Long = 10000
Public Const gnVelocityBoost As Single = 1.1
'The objects that can appear in the scene
Public goCamera As cCamera 'Doesn't really appear in the scene, but it does control what we see in the scene
Public goPuck As cPuck 'The puck. Pretty important
Public goPaddle(1) As cPaddle 'There are two paddles
Public goTable As cTable 'The table will never have a destination or a velocity, but we may need to move it's position
Public goRoom As cRoom 'The room information
Public goAudio As cAudio 'All of the audio information will be stored here
Public goInput As cInput 'All of the input (mouse,keyboard, joystick,etc) will be stored here
Public goFade As cFade 'The 'Fading' class
'Text variables
Public goTextLittle As cText
Public goTextBig As cText
'Main 'Select device' form
Public goDev As frmSelectDevice
'Which paddle am I controlling (Used mainly for multiplayer mode)
Public glMyPaddleID As Long
Public gfScored As Boolean 'Is the puck in the scored state
Public gfMultiplayer As Boolean 'Is this a multiplayer game
Public gfHost As Boolean 'Am I the host of this game?
Public gfGameCanBeStarted As Boolean 'Can the game be started
Public gPlayer(1) As HockeyPlayerInfo 'Current information of all the players
Public gfRecentlyHitPaddle As Boolean 'Have we recently hit a paddle?
'Current time for all objects
Public glTimeCompPaddle As Long
'Is the game over (ie, has someone won the game)
Public gfGameOver As Boolean
'The user defined winning score
Public glUserWinningScore As Long
Public glPaddleCollideTime As Long
'We'll maintain a slight dampening factor for realism as the puck bounces off
'the wall
Public gnVelocityDamp As Single
'Paddle mass
Public gnPaddleMass As Single
'Time the puck was last scored
Public glTimePuckScored As Long
'Time the game was over
Public glTimeGameOver As Long
'Time the F1 help was displayed
Public glTimeNoRoom As Long
'Is the system paused
Public gfSystemPause As Boolean
Public gfDrawRoomText As Boolean
Public glScreenWidth As Long
Public glScreenHeight As Long
Public gfObjectsLoaded As Boolean
'Extra misc vars
Public gfWireFrame As Boolean
Public Sub MainGameLoop()
Dim lTime As Long
Dim lLastPhysicsTime As Long
'Start the render loop
lTime = timeGetTime
Do While True
Do While Not gfSystemPause
'In each frame we need to do a few things
If (timeGetTime - lTime > 100) And (Not gfDrawRoomText) And (goRoom.DrawRoom) Then
'We want to maintain a reasonable frame rate (10fps is on
'the low end), so if we start taking too long between updates,
'tell them they can get rid of the room
gfDrawRoomText = True
glTimeNoRoom = timeGetTime
End If
lTime = timeGetTime
'Check to see if the game is over
CheckGameOver
'We need to update any objects that are in the scene
UpdateObjects
'Get and handle any input
goInput.GetAndHandleInput goPaddle(glMyPaddleID), goPuck
If (Not gfScored) And (Not gfGameOver) Then
'Next we need to check for any collisions that may have happened
goPuck.CheckCollisions goPaddle, goAudio
If Not gfMultiplayer Then 'Only on single player mode
'Let the Computer AI do it's thing
goPaddle(Abs(glMyPaddleID - 1)).DoComputerAI goPuck
End If
End If
'We need to update the game state on the other machine
If gfMultiplayer Then
UpdateNetworkSettings
Else
If gfScored Then goPaddle(Abs(glMyPaddleID - 1)).UpdateTime
End If
'Only redraw the world if we're keeping up with our physic calculations
If timeGetTime - lLastPhysicsTime < glNumTickForPhysicCalcs Then
'We should fade if necessary
If goFade.AmFading Then goFade.UpdateFade goPuck, goPaddle, goTable, goRoom
'Now we need to render the frame
Render
End If
lLastPhysicsTime = timeGetTime
DoEvents
Loop
'Now give the CPU a chance
DoEvents
Loop
End Sub
Public Sub LoadDefaultStartPositions()
'Our camera will start away from the table, and zoom in on it
With goCamera
.Position = vec3(0, 35, -40)
.LastPosition = .Position
End With
goCamera.SetCameraPosition 0, glMyPaddleID
'The puck's initial position should be on top of the table
With goPaddle(0)
.Position = vec3(0, 2.5, -6.8)
.LastPosition = .Position
End With
With goPaddle(1)
.Position = vec3(0, 2.5, 6.8)
.LastPosition = .Position
End With
With goTable
.Position = vec3(0, -5, 0)
End With
goPuck.DefaultStartPosition
End Sub
Public Sub UpdateObjects()
'We need a timer for each of the objects we're updating
Dim lCount As Long
If gfMultiplayer And gfNoSendData Then 'Uh oh! We've been disconnected sometime, no need to process anything
Exit Sub
End If
'Update the camera's position based on it's velocity
goCamera.UpdatePosition
'Update the puck's position
goPuck.UpdatePosition
End Sub
Public Sub CheckGameOver()
Dim lCount As Long
If gfGameOver Then Exit Sub
For lCount = 0 To 1
If gPlayer(lCount).Score >= glUserWinningScore Then
'Make sure we're leading the other player by 2 or more
If gPlayer(lCount).Score > gPlayer(Abs(lCount - 1)).Score + 1 Then
gfGameOver = True
glTimeGameOver = timeGetTime
End If
End If
Next
End Sub
Public Sub ShowStartup()
'Now 'zoom' in with our camera
Do While ((goCamera.Dest.Y <> goCamera.Position.Y) Or (goCamera.Dest.z <> goCamera.Position.z))
goCamera.UpdatePosition
Render
DoEvents
Loop
End Sub
Public Sub LoadObjects()
If gfObjectsLoaded Then Exit Sub
'Initialize the objects
Set goPuck = New cPuck
Set goPaddle(0) = New cPaddle
goPaddle(0).PaddleID = 0
Set goPaddle(1) = New cPaddle
goPaddle(1).PaddleID = 1
If goCamera Is Nothing Then Set goCamera = New cCamera
Set goTable = New cTable
Set goRoom = New cRoom
If goInput Is Nothing Then Set goInput = New cInput
If goAudio Is Nothing Then Set goAudio = New cAudio
If goFade Is Nothing Then Set goFade = New cFade
If goDev Is Nothing Then Set goDev = New frmSelectDevice
D3DEnum_BuildAdapterList frmAir
'Get any defaults from the registry we might need
goTable.DrawTable = GetSetting(gsKeyName, gsSubKey, "DrawTable", True)
goRoom.DrawRoom = GetSetting(gsKeyName, gsSubKey, "DrawRoom", True)
goRoom.barRoom = GetSetting(gsKeyName, gsSubKey, "RoomIsBarRoom", True)
'Audio options
goAudio.PlayMusic = GetSetting(gsKeyName, gsSubKeyAudio, "UseBackgroundMusic", False)
goAudio.PlaySounds = GetSetting(gsKeyName, gsSubKeyAudio, "UseSound", True)
goAudio.MusicVolume = GetSetting(gsKeyName, gsSubKeyAudio, "MusicVolume", 0)
goAudio.SoundVolume = GetSetting(gsKeyName, gsSubKeyAudio, "SoundVolume", 0)
'Input options
goInput.UseMouse = GetSetting(gsKeyName, gsSubKeyInput, "UseMouse", True)
goInput.UseKeyboard = GetSetting(gsKeyName, gsSubKeyInput, "UseKeyboard", True)
goInput.UseJoystick = GetSetting(gsKeyName, gsSubKeyInput, "UseJoystick", False)
goInput.JoystickGuid = GetSetting(gsKeyName, gsSubKeyInput, "JoystickGuid", vbNullString)
goInput.JoystickSensitivity = GetSetting(gsKeyName, gsSubKeyInput, "JoystickSensitivity", 0.00025)
goInput.MouseSensitivity = GetSetting(gsKeyName, gsSubKeyInput, "MouseSensitivity", 0.02)
goInput.KeyboardSensitivity = GetSetting(gsKeyName, gsSubKeyInput, "KeyboardSensitivity", 0.002)
'D3D options
goDev.Windowed = GetSetting(gsKeyName, gsSubKeyGraphics, "Windowed", True)
goDev.Adapter = GetSetting(gsKeyName, gsSubKeyGraphics, "AdapterID", 0)
goDev.Mode = GetSetting(gsKeyName, gsSubKeyGraphics, "Mode", 0)
gfObjectsLoaded = True
End Sub
Public Sub PauseSystem(ByVal fPause As Boolean)
gfSystemPause = fPause
If Not fPause Then
glTimeCompPaddle = timeGetTime
End If
If Not (goPuck Is Nothing) Then
goPuck.PauseSystem fPause
End If
End Sub
Public Sub Cleanup(Optional fFinalCleanup As Boolean = False, Optional fOnlyD3D As Boolean = False)
'Getting rid of the objects will clean up the internal objects
If fFinalCleanup Then
Set goPuck = Nothing
Set goPaddle(0) = Nothing
Set goPaddle(1) = Nothing
Set goTable = Nothing
Set goRoom = Nothing
Set goTextBig = Nothing
Set goTextLittle = Nothing
If Not fOnlyD3D Then
Set goInput = Nothing
Set goAudio = Nothing
Set goFade = Nothing
Set goDev = Nothing
End If
gfObjectsLoaded = False
Else
goPuck.CleanupFrame
goPaddle(0).CleanupFrame
goPaddle(1).CleanupFrame
goTable.CleanupFrame
goRoom.CleanupFrame
End If
End Sub
Public Sub InitGeometry()
LoadObjects
'First set up the media
D3DUtil_SetMediaPath AddDirSep(App.path) & "models\"
goRoom.Init g_mediaPath, "room.x", "lobby_skybox.x"
frmAir.IncreaseProgressBar
goPaddle(1).Init g_mediaPath, "paddle.x"
frmAir.IncreaseProgressBar
goPaddle(0).Init g_mediaPath, "paddle.x"
frmAir.IncreaseProgressBar
goPuck.Init g_mediaPath, "puck.x"
frmAir.IncreaseProgressBar
goTable.Init g_mediaPath, "table.x"
frmAir.IncreaseProgressBar
End Sub
Public Sub Render()
Dim lCount As Long
On Error Resume Next
If gfSystemPause Then Exit Sub
If g_dev Is Nothing Then Exit Sub
' Clear the backbuffer to a black color
If gfMultiplayer And gfNoSendData Then 'Uh oh! We've been disconnected sometime, notify the user
D3DUtil_ClearAll &HFF0000FF 'Clear with a blue background
Else
D3DUtil_ClearAll 0
' Setup the view and projection matrices
SetupMatrices
' Begin the scene
g_dev.BeginScene
'Draw everything in either a solid fillmode, or wireframe
If gfWireFrame Then
g_dev.SetRenderState D3DRS_FILLMODE, D3DFILL_WIREFRAME
Else
g_dev.SetRenderState D3DRS_FILLMODE, D3DFILL_SOLID
End If
If goFade.AmFading Then
g_dev.SetRenderState D3DRS_ALPHABLENDENABLE, 1 'TRUE
g_dev.SetRenderState D3DRS_SRCBLEND, D3DBLEND_SRCALPHA
g_dev.SetRenderState D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA
End If
'Render the room
goRoom.Render g_dev
'Render the table
goTable.Render g_dev
'Now Paddle (0)
goPaddle(0).Render g_dev
'Now Paddle (1)
goPaddle(1).Render g_dev
'And finally the puck
goPuck.Render g_dev
'Now lets draw whatever text we need
End If
'We can draw text (don't draw text if we're currently fading)
If Not goFade.AmFading Then
goTextLittle.BeginText
If gfGameCanBeStarted Then
'If the game can be started, then draw the scores at the top of the screen
If gfMultiplayer Then
If glMyPaddleID = 0 Then
goTextLittle.DrawText gsUserName & ":" & gPlayer(0).Score, 10, 5, &HFFFFFF00
Else
goTextLittle.DrawText "Opponent:" & gPlayer(0).Score, 10, 5, &HFFFFFFFF
End If
If glMyPaddleID = 1 Then
goTextLittle.DrawText gsUserName & ":" & gPlayer(1).Score, glScreenWidth - 75, 5, &HFFFFFF00
Else
goTextLittle.DrawText "Opponent:" & gPlayer(1).Score, glScreenWidth - 75, 5, &HFFFFFFFF
End If
Else
goTextLittle.DrawText "Player:" & gPlayer(0).Score, 10, 5, &HFFFFFF00
goTextLittle.DrawText "Computer:" & gPlayer(1).Score, glScreenWidth - 75, 5, &HFFFFFFFF
End If
Else
'The game can't be started yet (only in multiplayer) Let the host know
goTextLittle.DrawText "Waiting for the game to be started...", (glScreenWidth / 2) - 50, 5, &HFFFFFFFF
End If
'Here is a little helper text letting the user know to press Space
'to launch the puck (will show up after 3 seconds, and stay on for 10 seconds)
If (timeGetTime - glTimePuckScored > glDefaultDelayTime) And gfScored And Not gfGameOver And ((timeGetTime - glTimePuckScored < glDefaultDelayTimeGone + glDefaultDelayTime)) Then
goPuck.DefaultStartPosition
goPuck.Spinning = True
goTextLittle.DrawText "Press <Space> to launch puck...", (glScreenWidth / 2) - 50, 25, &HFF0000FF
End If
'Here is a little helper text letting the user know to press F1
'to turn of the room (will show up after 3 seconds, and stay on for 10 seconds)
If (gfDrawRoomText And goRoom.DrawRoom) And (timeGetTime - glTimeNoRoom < glDefaultDelayTimeGone) Then
goTextLittle.DrawText "You can press F1 to turn off the drawing " & vbCrLf & " of the room, which will increase performance.", -15, glScreenHeight - 50, &HFFFF00FF
End If
If gfGameOver And ((timeGetTime - glTimeGameOver) > glDefaultDelayTime) And ((timeGetTime - glTimeGameOver < glDefaultDelayTimeGone + glDefaultDelayTime)) Then
goTextLittle.DrawText "Press F3 to restart...", (glScreenWidth / 2) - 50, 25, &HFF0000FF
End If
goTextLittle.EndText
goTextBig.BeginText
If gfGameOver Then
If gfMultiplayer Then
If gPlayer(glMyPaddleID).Score > gPlayer(Abs(glMyPaddleID - 1)).Score Then
goTextBig.DrawText "Game over!!" & vbCrLf & "You win!!", (glScreenWidth / 2) - (glScreenWidth / 4), (glScreenHeight / 2) - (glScreenHeight / 4), &HFFDD11AA
Else
goTextBig.DrawText "Game over!!" & vbCrLf & "You lose!!", (glScreenWidth / 2) - (glScreenWidth / 4), (glScreenHeight / 2) - (glScreenHeight / 4), &HFFDD11AA
End If
Else
If gPlayer(0).Score > gPlayer(1).Score Then
goTextBig.DrawText "Game over!!" & vbCrLf & "You win!!", (glScreenWidth / 2) - (glScreenWidth / 4), (glScreenHeight / 2) - (glScreenHeight / 4), &HFFDD11AA
Else
goTextBig.DrawText "Game over!!" & vbCrLf & "You lose!!", (glScreenWidth / 2) - (glScreenWidth / 4), (glScreenHeight / 2) - (glScreenHeight / 4), &HFFDD11AA
End If
End If
End If
If gfMultiplayer And gfNoSendData Then 'Uh oh! We've been disconnected sometime, notify the user
goTextBig.DrawText "The connection with the other" & vbCrLf & "system was lost.", 5, (glScreenHeight / 2) - (glScreenHeight / 4), &HFFFFFF00
'This message isn't on a timer to go away
End If
goTextBig.EndText
End If
' End the scene
g_dev.EndScene
' Present the backbuffer contents to the front buffer (screen)
D3DUtil_PresentAll 0
End Sub
Public Sub SetupMatrices()
Dim matView As D3DMATRIX
Dim matProj As D3DMATRIX
D3DXMatrixLookAtLH matView, goCamera.Position, vec3(0#, 0#, 0#), vec3(0#, 1#, 0#)
g_dev.SetTransform D3DTS_VIEW, matView
D3DXMatrixPerspectiveFovLH matProj, g_pi / 4, 1, 1, 110
g_dev.SetTransform D3DTS_PROJECTION, matProj
End Sub
Public Sub RestoreDeviceObjects()
' Set miscellaneous render states
With g_dev
' Set world transform
Dim matWorld As D3DMATRIX
D3DXMatrixIdentity matWorld
.SetTransform D3DTS_WORLD, matWorld
.SetTextureStageState 0, D3DTSS_COLORARG2, D3DTA_DIFFUSE
.SetTextureStageState 0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1
.SetTextureStageState 0, D3DTSS_ALPHAARG1, D3DTA_DIFFUSE
.SetTextureStageState 0, D3DTSS_MINFILTER, D3DTEXF_LINEAR
.SetTextureStageState 0, D3DTSS_MAGFILTER, D3DTEXF_LINEAR
' Set default render states
.SetRenderState D3DRS_ZENABLE, 1 'True
End With
Set goTextBig = Nothing
Set goTextLittle = Nothing
'Now create a new text object
Set goTextLittle = New cText
goTextLittle.InitText g_d3dx, g_dev, "Times New Roman", 8, True
Set goTextBig = New cText
goTextBig.InitText g_d3dx, g_dev, "Times New Roman", 18, True
End Sub
Public Sub InitDefaultLights(ByVal lNumLights As Long)
With g_dev
' Set ambient light
.SetRenderState D3DRS_AMBIENT, &HFFFFFFFF
If lNumLights < 3 Then Exit Sub 'Nothing to do
' Set ambient light
'We will slowly lower the ambient light as each new light gets added
.SetRenderState D3DRS_AMBIENT, &HFFBBBBBB
' Turn on lighting
.SetRenderState D3DRS_LIGHTING, 1
'Turn on two lights one on each end of the table
Dim light As D3DLIGHT8
If lNumLights > 0 Then
With light
.Type = D3DLIGHT_DIRECTIONAL
.diffuse.r = 1
.diffuse.g = 1
.diffuse.b = 1
.Direction.X = 0
.Direction.Y = -10
.Direction.z = 0
.Range = 1.84467435229094E+19 'User defined.
.Position.X = 0
.Position.Y = 3
.Position.z = 0
End With
.SetLight 0, light 'let d3d know about the light
.LightEnable 0, 1 'turn it on
End If
If lNumLights > 1 Then
.SetRenderState D3DRS_AMBIENT, &HFFAAAAAA
'Now turn on the second light if we can
With light
.Type = D3DLIGHT_DIRECTIONAL
.Direction.X = 5
.Direction.Y = -3
.Direction.z = -5
.Position.X = -5
.Position.Y = 3
.Position.z = 5
End With
.SetLight 1, light 'let d3d know about the light
.LightEnable 1, 1 'turn it on
End If
If lNumLights > 3 Then
.SetRenderState D3DRS_AMBIENT, 0
'Now turn on the third light if we can
With light
.Type = D3DLIGHT_DIRECTIONAL
.Direction.X = -5
.Direction.Y = 3
.Direction.z = 5
.Position.X = 5
.Position.Y = -3
.Position.z = -5
End With
.SetLight 2, light 'let d3d know about the light
.LightEnable 2, 1 'turn it on
End If
End With
End Sub
Public Sub SaveOrRestoreObjectSettings(ByVal fSave As Boolean)
'Puck
Static LastPuckPosition As D3DVECTOR
Static PuckPosition As D3DVECTOR
Static MaxPuckVel As Single
Static PuckSpinning As Boolean
Static PuckVelocity As D3DVECTOR
If fSave Then
LastPuckPosition = goPuck.LastPosition
MaxPuckVel = goPuck.MaximumPuckVelocity
PuckPosition = goPuck.Position
PuckSpinning = goPuck.Spinning
PuckVelocity = goPuck.Velocity
Else
goPuck.LastPosition = LastPuckPosition
goPuck.MaximumPuckVelocity = MaxPuckVel
goPuck.Position = PuckPosition
goPuck.Spinning = PuckSpinning
goPuck.Velocity = PuckVelocity
End If
'paddles
Static LastPaddlePosition(1) As D3DVECTOR
Static LastPaddleVelTick(1) As Long
Static PaddleID(1) As Long
Static PaddlePosition(1) As D3DVECTOR
Static PaddleTrans(1) As Boolean
Static PaddleVelocity(1) As D3DVECTOR
Dim i As Integer
If fSave Then
For i = 0 To 1
LastPaddlePosition(i) = goPaddle(i).LastPosition
LastPaddleVelTick(i) = goPaddle(i).LastVelocityTick
PaddleID(i) = goPaddle(i).PaddleID
PaddlePosition(i) = goPaddle(i).Position
PaddleTrans(i) = goPaddle(i).Transparent
PaddleVelocity(i) = goPaddle(i).Velocity
Next
Else
For i = 0 To 1
goPaddle(i).LastPosition = LastPaddlePosition(i)
goPaddle(i).LastVelocityTick = LastPaddleVelTick(i)
goPaddle(i).PaddleID = PaddleID(i)
goPaddle(i).Position = PaddlePosition(i)
goPaddle(i).Transparent = PaddleTrans(i)
goPaddle(i).Velocity = PaddleVelocity(i)
Next
End If
'Room
Static barRoom As Boolean
Static DrawRoom As Boolean
If fSave Then
barRoom = goRoom.barRoom
DrawRoom = goRoom.DrawRoom
Else
goRoom.barRoom = barRoom
goRoom.DrawRoom = DrawRoom
End If
'Table
Static DrawTable As Boolean
Static TablePosition As D3DVECTOR
Static TableTrans As Boolean
If fSave Then
DrawTable = goTable.DrawTable
TablePosition = goTable.Position
TableTrans = goTable.Transparent
Else
goTable.DrawTable = DrawTable
goTable.Position = TablePosition
goTable.Transparent = TableTrans
End If
End Sub