PlayerScripts/StarterPlayerScripts/ControlScript.lua
--[[
	// FileName: ControlScript.lua
	// Version 1.1
	// Written by: jmargh and jeditkacheff
	// Description: Manages in game controls for both touch and keyboard/mouse devices.
	
	// This script will be inserted into PlayerScripts under each player by default. If you want to
	// create your own custom controls or modify these controls, you must place a script with this
	// name, ControlScript, under StarterPlayer -> PlayerScripts.
	
	// Required Modules:
		ClickToMove
		DPad
		KeyboardMovement
		Thumbpad
		Thumbstick
		TouchJump
		MasterControl
		VehicleController
--]]

--[[ Services ]]--
local ContextActionService = game:GetService('ContextActionService')
local Players = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local VRService = game:GetService('VRService')
-- Settings and GameSettings are read only
local Settings = UserSettings()
local GameSettings = Settings.GameSettings

-- Issue with play solo? (F6)
while not UserInputService.KeyboardEnabled and not UserInputService.TouchEnabled and not UserInputService.GamepadEnabled do
	wait()
end

--[[ Script Variables ]]--
while not Players.LocalPlayer do
	wait()
end

local lastInputType = nil
local LocalPlayer = Players.LocalPlayer
local PlayerGui = LocalPlayer:WaitForChild('PlayerGui')
local IsTouchDevice = UserInputService.TouchEnabled
local UserMovementMode = IsTouchDevice and GameSettings.TouchMovementMode or GameSettings.ComputerMovementMode
local DevMovementMode = IsTouchDevice and LocalPlayer.DevTouchMovementMode or LocalPlayer.DevComputerMovementMode
local IsUserChoice = (IsTouchDevice and DevMovementMode == Enum.DevTouchMovementMode.UserChoice) or (DevMovementMode == Enum.DevComputerMovementMode.UserChoice)
local TouchGui = nil
local TouchControlFrame = nil
local IsModalEnabled = UserInputService.ModalEnabled
local BindableEvent_OnFailStateChanged = nil
local isJumpEnabled = false

-- register what control scripts we are using
do
	local PlayerScripts = LocalPlayer:WaitForChild("PlayerScripts")
	local canRegisterControls = pcall(function() PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Default) end)

	if canRegisterControls then
		PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbstick)
		PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DPad)
		PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.Thumbpad)
		PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.ClickToMove)
		PlayerScripts:RegisterTouchMovementMode(Enum.TouchMovementMode.DynamicThumbstick)

		PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.Default)
		PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.KeyboardMouse)
		PlayerScripts:RegisterComputerMovementMode(Enum.ComputerMovementMode.ClickToMove)
	end
end

local DynamicThumbstickAvailable = pcall(function() 
	return Enum.DevTouchMovementMode.DynamicThumbstick and Enum.TouchMovementMode.DynamicThumbstick
end)

local FFlagUserNoCameraClickToMoveSuccess, FFlagUserNoCameraClickToMoveResult = pcall(function() return UserSettings():IsUserFeatureEnabled("UserNoCameraClickToMove") end)
local FFlagUserNoCameraClickToMove = FFlagUserNoCameraClickToMoveSuccess and FFlagUserNoCameraClickToMoveResult

--[[ Modules ]]--
local ClickToMoveTouchControls = nil
local ControlModules = {}

local ControlState = {}
ControlState.Current = nil
function ControlState:SwitchTo(newControl)
	if ControlState.Current == newControl then return end
	
	if ControlState.Current then
		ControlState.Current:Disable()
	end
	
	ControlState.Current = newControl
	
	if ControlState.Current then
		ControlState.Current:Enable()
	end
end

function ControlState:IsTouchJumpModuleUsed()
	return isJumpEnabled
end

local MasterControl = require(script:WaitForChild('MasterControl'))
--MasterControl needs access to ControlState in order to be able to fully enable and disable control
MasterControl.ControlState = ControlState

local DynamicThumbstickModule = require(script.MasterControl:WaitForChild('DynamicThumbstick'))
local ThumbstickModule = require(script.MasterControl:WaitForChild('Thumbstick'))
local ThumbpadModule = require(script.MasterControl:WaitForChild('Thumbpad'))
local DPadModule = require(script.MasterControl:WaitForChild('DPad'))
local DefaultModule = ControlModules.Thumbstick
local TouchJumpModule = require(script.MasterControl:WaitForChild('TouchJump'))
local ClickToMoveModule = FFlagUserNoCameraClickToMove and require(script.MasterControl:WaitForChild('ClickToMoveController')) or nil

MasterControl.TouchJumpModule = TouchJumpModule
local VRNavigationModule = require(script.MasterControl:WaitForChild('VRNavigation'))
local keyboardModule = require(script.MasterControl:WaitForChild('KeyboardMovement'))
ControlModules.Gamepad = require(script.MasterControl:WaitForChild('Gamepad'))

function getTouchModule()

	local module = nil
	if not IsUserChoice then
		if DynamicThumbstickAvailable and DevMovementMode == Enum.DevTouchMovementMode.DynamicThumbstick then
			module = DynamicThumbstickModule
			isJumpEnabled = true
		elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbstick then
			module = ThumbstickModule
			isJumpEnabled = true
		elseif DevMovementMode == Enum.DevTouchMovementMode.Thumbpad then
			module = ThumbpadModule
			isJumpEnabled = true
		elseif DevMovementMode == Enum.DevTouchMovementMode.DPad then
			module = DPadModule
			isJumpEnabled = false
		elseif DevMovementMode == Enum.DevTouchMovementMode.ClickToMove then
			if FFlagUserNoCameraClickToMove then
				module = ClickToMoveModule
				isJumpEnabled = false -- TODO: What should this be, true or false?
			else
				module = nil
			end
		elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
			module = nil
		end
	else
		if DynamicThumbstickAvailable and UserMovementMode == Enum.TouchMovementMode.DynamicThumbstick then
			module = DynamicThumbstickModule
			isJumpEnabled = true
		elseif UserMovementMode == Enum.TouchMovementMode.Default or UserMovementMode == Enum.TouchMovementMode.Thumbstick then
			module = ThumbstickModule
			isJumpEnabled = true
		elseif UserMovementMode == Enum.TouchMovementMode.Thumbpad then
			module = ThumbpadModule
			isJumpEnabled = true
		elseif UserMovementMode == Enum.TouchMovementMode.DPad then
			module = DPadModule
			isJumpEnabled = false
		elseif UserMovementMode == Enum.TouchMovementMode.ClickToMove then
			if FFlagUserNoCameraClickToMove then
				module = ClickToMoveModule
				isJumpEnabled = false -- TODO: What should this be, true or false?
			else
				module = nil
			end
		end
	end
	return module
end

function setJumpModule(isEnabled)
	if not isEnabled then
		TouchJumpModule:Disable()
	elseif ControlState.Current == ControlModules.Touch then
		TouchJumpModule:Enable()
	end
end

function setClickToMove()
	if DevMovementMode == Enum.DevTouchMovementMode.ClickToMove or DevMovementMode == Enum.DevComputerMovementMode.ClickToMove or
		UserMovementMode == Enum.ComputerMovementMode.ClickToMove or UserMovementMode == Enum.TouchMovementMode.ClickToMove then
		if lastInputType == Enum.UserInputType.Touch then
			ClickToMoveTouchControls = ControlState.Current
		end
	elseif ClickToMoveTouchControls then
		ClickToMoveTouchControls:Disable()
		ClickToMoveTouchControls = nil
	end
end

ControlModules.Touch = {}
ControlModules.Touch.Current = nil
ControlModules.Touch.LocalPlayerChangedCon = nil
ControlModules.Touch.GameSettingsChangedCon = nil

function ControlModules.Touch:RefreshControlStyle()
	if ControlModules.Touch.Current then
		ControlModules.Touch.Current:Disable()
	end
	setJumpModule(false)
	TouchJumpModule:Disable()
	ControlModules.Touch:Enable()
end
function ControlModules.Touch:DisconnectEvents()
	if ControlModules.Touch.LocalPlayerChangedCon then
		ControlModules.Touch.LocalPlayerChangedCon:disconnect()
		ControlModules.Touch.LocalPlayerChangedCon = nil
	end
	if ControlModules.Touch.GameSettingsChangedCon then
		ControlModules.Touch.GameSettingsChangedCon:disconnect()
		ControlModules.Touch.GameSettingsChangedCon = nil
	end
end
function ControlModules.Touch:Enable()
	DevMovementMode = LocalPlayer.DevTouchMovementMode
	IsUserChoice = DevMovementMode == Enum.DevTouchMovementMode.UserChoice
	if IsUserChoice then
		UserMovementMode = GameSettings.TouchMovementMode
	end
		
	local newModuleToEnable = getTouchModule()
	if newModuleToEnable then
		setClickToMove()
		setJumpModule(isJumpEnabled)
		
		newModuleToEnable:Enable()
		ControlModules.Touch.Current = newModuleToEnable
				
		if isJumpEnabled then TouchJumpModule:Enable() end
	end
	
	-- This being within the above if statement was causing issues with ClickToMove, which isn't a module within this script.
	ControlModules.Touch:DisconnectEvents()
	ControlModules.Touch.LocalPlayerChangedCon = LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):connect(function()
		ControlModules.Touch:RefreshControlStyle()
	end)
	ControlModules.Touch.GameSettingsChangedCon = GameSettings:GetPropertyChangedSignal("TouchMovementMode"):connect(function()
		ControlModules.Touch:RefreshControlStyle()
	end)
end
function ControlModules.Touch:Disable()
	ControlModules.Touch:DisconnectEvents()
	
	local newModuleToDisable = getTouchModule()
	
	if newModuleToDisable == ThumbstickModule or
		newModuleToDisable == DPadModule or
		newModuleToDisable == ThumbpadModule or
		newModuleToDisable == DynamicThumbstickModule then
			newModuleToDisable:Disable()
			setJumpModule(false)
			TouchJumpModule:Disable()
	end
	
	-- UserMovementMode will still have the previous value at this point
	if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
		ClickToMoveModule:Disable()
	end
end

local function getKeyboardModule()
	-- NOTE: Click to move still uses keyboard. Leaving cases in case this ever changes.
	local whichModule = nil
	if not IsUserChoice then
		if DevMovementMode == Enum.DevComputerMovementMode.KeyboardMouse then
			whichModule = keyboardModule
		elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then
			whichModule = keyboardModule
		end 
	else
		if UserMovementMode == Enum.ComputerMovementMode.KeyboardMouse or UserMovementMode == Enum.ComputerMovementMode.Default then
			whichModule = keyboardModule
		elseif UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
			whichModule = keyboardModule
		end
	end

	return whichModule
end

ControlModules.Keyboard = {}
function ControlModules.Keyboard:RefreshControlStyle()
	ControlModules.Keyboard:Disable()
	ControlModules.Keyboard:Enable()
end
function ControlModules.Keyboard:Enable()
	DevMovementMode = LocalPlayer.DevComputerMovementMode
	IsUserChoice = DevMovementMode == Enum.DevComputerMovementMode.UserChoice
	if IsUserChoice then
		UserMovementMode = GameSettings.ComputerMovementMode
	end
		
	local newModuleToEnable = getKeyboardModule()
	if newModuleToEnable then
		newModuleToEnable:Enable()
	end
	
	if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
		ClickToMoveModule:Enable()
	end
	
	ControlModules.Keyboard:DisconnectEvents()
	ControlModules.Keyboard.LocalPlayerChangedCon = LocalPlayer.Changed:connect(function(property)
		if property == 'DevComputerMovementMode' then
			ControlModules.Keyboard:RefreshControlStyle()
		end
	end)
	
	ControlModules.Keyboard.GameSettingsChangedCon = GameSettings.Changed:connect(function(property)
		if property == 'ComputerMovementMode' then
			ControlModules.Keyboard:RefreshControlStyle()
		end
	end)
end
function ControlModules.Keyboard:DisconnectEvents()
	if ControlModules.Keyboard.LocalPlayerChangedCon then
		ControlModules.Keyboard.LocalPlayerChangedCon:disconnect()
		ControlModules.Keyboard.LocalPlayerChangedCon = nil
	end
	if ControlModules.Keyboard.GameSettingsChangedCon then
		ControlModules.Keyboard.GameSettingsChangedCon:disconnect()
		ControlModules.Keyboard.GameSettingsChangedCon = nil
	end
end
function ControlModules.Keyboard:Disable()
	ControlModules.Keyboard:DisconnectEvents()
	local newModuleToDisable = getKeyboardModule()
	if newModuleToDisable then
		newModuleToDisable:Disable()
	end
	
	-- UserMovementMode will still be set to previous movement type
	if FFlagUserNoCameraClickToMove and UserMovementMode == Enum.ComputerMovementMode.ClickToMove then
		ClickToMoveModule:Disable()
	end
end

ControlModules.VRNavigation = {}

function ControlModules.VRNavigation:Enable()
	VRNavigationModule:Enable()
end

function ControlModules.VRNavigation:Disable()
	VRNavigationModule:Disable()
end

if not FFlagUserNoCameraClickToMove and IsTouchDevice then
	BindableEvent_OnFailStateChanged = script.Parent:WaitForChild("OnClickToMoveFailStateChange")
end

-- not used, but needs to be required
local VehicleController = require(script.MasterControl:WaitForChild('VehicleController'))


--[[ Initialization/Setup ]]--
local function createTouchGuiContainer()
	if TouchGui then TouchGui:Destroy() end
	
	-- Container for all touch device guis
	TouchGui = Instance.new('ScreenGui')
	TouchGui.Name = "TouchGui"
	TouchGui.ResetOnSpawn = false
	TouchGui.Parent = PlayerGui
	
	TouchControlFrame = Instance.new('Frame')
	TouchControlFrame.Name = "TouchControlFrame"
	TouchControlFrame.Size = UDim2.new(1, 0, 1, 0)
	TouchControlFrame.BackgroundTransparency = 1
	TouchControlFrame.Parent = TouchGui
	
	ThumbstickModule:Create(TouchControlFrame)
	DPadModule:Create(TouchControlFrame)
	ThumbpadModule:Create(TouchControlFrame)
	TouchJumpModule:Create(TouchControlFrame)
	DynamicThumbstickModule:Create(TouchControlFrame)
end

--[[ Settings Changed Connections ]]--
LocalPlayer.Changed:connect(function(property)
	if lastInputType == Enum.UserInputType.Touch and property == 'DevTouchMovementMode' then
		ControlState:SwitchTo(ControlModules.Touch)
	elseif UserInputService.KeyboardEnabled and property == 'DevComputerMovementMode' then
		ControlState:SwitchTo(ControlModules.Keyboard)
	end
end)

GameSettings.Changed:connect(function(property)
	if not IsUserChoice then return end
	if property == 'TouchMovementMode' or property == 'ComputerMovementMode' then
		UserMovementMode = GameSettings[property]
		if property == 'TouchMovementMode' then
			ControlState:SwitchTo(ControlModules.Touch)
		elseif property == 'ComputerMovementMode' then
			ControlState:SwitchTo(ControlModules.Keyboard)
		end
	end
end)

--[[ Touch Events ]]--
UserInputService.Changed:connect(function(property)
	if property == 'ModalEnabled' then
		IsModalEnabled = UserInputService.ModalEnabled

		if lastInputType == Enum.UserInputType.Touch then
			if ControlState.Current == ControlModules.Touch and IsModalEnabled then
				ControlState:SwitchTo(nil)
			elseif ControlState.Current == nil and not IsModalEnabled then
				ControlState:SwitchTo(ControlModules.Touch)
			end
		end
	end
end)

if FFlagUserNoCameraClickToMove then
	BindableEvent_OnFailStateChanged = MasterControl:GetClickToMoveFailStateChanged()
end
if BindableEvent_OnFailStateChanged then
	BindableEvent_OnFailStateChanged.Event:connect(function(isOn)
		if lastInputType == Enum.UserInputType.Touch and ClickToMoveTouchControls then
			if isOn then
				ControlState:SwitchTo(ClickToMoveTouchControls)
			else
				ControlState:SwitchTo(nil)
			end
		end
	end)
end

local switchToInputType = function(newLastInputType)
	lastInputType = newLastInputType
	
	if VRService.VREnabled then
		ControlState:SwitchTo(ControlModules.VRNavigation)
		return
	end

	if lastInputType == Enum.UserInputType.Touch then
				ControlState:SwitchTo(ControlModules.Touch)
	elseif lastInputType == Enum.UserInputType.Keyboard or
			lastInputType == Enum.UserInputType.MouseButton1 or
			lastInputType == Enum.UserInputType.MouseButton2 or
			lastInputType == Enum.UserInputType.MouseButton3 or
			lastInputType == Enum.UserInputType.MouseWheel or
			lastInputType == Enum.UserInputType.MouseMovement  then
				ControlState:SwitchTo(ControlModules.Keyboard)
	elseif lastInputType == Enum.UserInputType.Gamepad1 or
			lastInputType == Enum.UserInputType.Gamepad2 or
			lastInputType == Enum.UserInputType.Gamepad3 or
			lastInputType == Enum.UserInputType.Gamepad4 then
				ControlState:SwitchTo(ControlModules.Gamepad)
	end
end

if IsTouchDevice then
	createTouchGuiContainer()
end

MasterControl:Init()

UserInputService.GamepadDisconnected:connect(function(gamepadEnum)
	local connectedGamepads = UserInputService:GetConnectedGamepads()
	if #connectedGamepads > 0 then return end
	
	if not VRService.VREnabled then
		if UserInputService.KeyboardEnabled then
			ControlState:SwitchTo(ControlModules.Keyboard)
		elseif IsTouchDevice then
			ControlState:SwitchTo(ControlModules.Touch)
		end
	end
end)

UserInputService.GamepadConnected:connect(function(gamepadEnum)
	if not VRService.VREnabled then
		ControlState:SwitchTo(ControlModules.Gamepad)
	end
end)

switchToInputType(UserInputService:GetLastInputType())
UserInputService.LastInputTypeChanged:connect(switchToInputType)

VRService:GetPropertyChangedSignal("VREnabled"):Connect(function()	
	if VRService.VREnabled then
		ControlState:SwitchTo(ControlModules.VRNavigation)
	end
end)
