PlayerScripts/StarterPlayerScripts_NewStructure/RobloxPlayerScript/ControlScript.lua
--[[
	ControlScript - This module manages the selection of the current character control module
	and calls update() on the active module on RenderStepped
	
	2018 PlayerScripts Update - AllYourBlox		
--]]
local ControlScript = {}

--[[ Roblox Services ]]--
local a = shared
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local Settings = UserSettings()
local GameSettings = Settings.GameSettings

local activeControlModule = nil	-- Used to prevent unnecessarily expensive checks on each input event
local activeController = nil
local touchJumpController = nil
local moveFunction = Players.LocalPlayer.Move
local humanoid = nil
local lastInputType = Enum.UserInputType.None
local cameraRelative = true

-- For Roblox VehicleController
local humanoidSeatedConn = nil
local vehicleController = nil

local touchControlFrame = nil

-- Modules - each returns a new() constructor function used to create controllers as needed
local Keyboard = require(script:WaitForChild("Keyboard"))
local Gamepad = require(script:WaitForChild("Gamepad"))
local TouchDPad = require(script:WaitForChild("TouchDPad"))
local DynamicThumbstick = require(script:WaitForChild("DynamicThumbstick"))

-- These controllers handle only walk/run movement, jumping is handled by the
-- TouchJump controller if any of these are active
local ClickToMove = require(script:WaitForChild("ClickToMoveController"))
local TouchThumbstick = require(script:WaitForChild("TouchThumbstick"))
local TouchThumbpad = require(script:WaitForChild("TouchThumbpad"))
local TouchJump = require(script:WaitForChild("TouchJump"))

-- Old control script notes that this must be required but is not used like a control module?
local VehicleController = require(script:WaitForChild("VehicleController"))

-- The Modules above are used to construct controller instances as-needed, and this
-- table is a map from Module to the instance created from it
local controllers = {}

-- Mapping from movement mode and lastInputType enum values to control modules to avoid huge if elseif switching
local movementEnumToModuleMap = {
	[Enum.TouchMovementMode.DPad] = TouchDPad,
	[Enum.DevTouchMovementMode.DPad] = TouchDPad,
	[Enum.TouchMovementMode.Thumbpad] = TouchThumbpad,
	[Enum.DevTouchMovementMode.Thumbpad] = TouchThumbpad,
	[Enum.TouchMovementMode.Thumbstick] = TouchThumbstick,
	[Enum.DevTouchMovementMode.Thumbstick] = TouchThumbstick,
	[Enum.TouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
	[Enum.DevTouchMovementMode.DynamicThumbstick] = DynamicThumbstick,
	[Enum.TouchMovementMode.ClickToMove] = ClickToMove,
	[Enum.DevTouchMovementMode.ClickToMove] = ClickToMove,

	-- Current default
	[Enum.TouchMovementMode.Default] = TouchThumbstick,

	[Enum.ComputerMovementMode.Default] = Keyboard,
	[Enum.ComputerMovementMode.KeyboardMouse] = Keyboard,
	[Enum.DevComputerMovementMode.KeyboardMouse] = Keyboard,
	[Enum.DevComputerMovementMode.Scriptable] = nil,
	[Enum.ComputerMovementMode.ClickToMove] = ClickToMove,
	[Enum.DevComputerMovementMode.ClickToMove] = ClickToMove,
}

-- Keyboard controller is really keyboard and mouse controller
local computerInputTypeToModuleMap = {
	[Enum.UserInputType.Keyboard] = Keyboard,
	[Enum.UserInputType.MouseButton1] = Keyboard,
	[Enum.UserInputType.MouseButton2] = Keyboard,
	[Enum.UserInputType.MouseButton3] = Keyboard,
	[Enum.UserInputType.MouseWheel] = Keyboard,
	[Enum.UserInputType.MouseMovement] = Keyboard,
	[Enum.UserInputType.Gamepad1] = Gamepad,
	[Enum.UserInputType.Gamepad2] = Gamepad,
	[Enum.UserInputType.Gamepad3] = Gamepad,
	[Enum.UserInputType.Gamepad4] = Gamepad,
}

-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
local function SelectComputerMovementModule()
	if not (UserInputService.KeyboardEnabled or UserInputService.GamepadEnabled) then
		return nil, false
	end
	local computerModule = nil
	local DevMovementMode = Players.LocalPlayer.DevComputerMovementMode
	if DevMovementMode == Enum.DevComputerMovementMode.UserChoice then
		computerModule = computerInputTypeToModuleMap[lastInputType]
		if not computerModule then
			computerModule = movementEnumToModuleMap[GameSettings.ComputerMovementMode]
		end
	elseif DevMovementMode == Enum.DevComputerMovementMode.Scriptable then
		return nil, true
	elseif DevMovementMode == Enum.DevComputerMovementMode.ClickToMove then
		computerModule = ClickToMove
	else
		computerModule = computerInputTypeToModuleMap[lastInputType]
		if not computerModule then
			computerModule = movementEnumToModuleMap[DevMovementMode]
		end
	end
	return computerModule, true
end

-- Choose current Touch control module based on settings (user, dev)
-- Returns module (possibly nil) and success code to differentiate returning nil due to error vs Scriptable
local function SelectTouchModule()
	if not UserInputService.TouchEnabled then
		return nil, false
	end
	local touchModule = nil
	local DevMovementMode = Players.LocalPlayer.DevTouchMovementMode
	if DevMovementMode == Enum.DevTouchMovementMode.UserChoice then
		touchModule = movementEnumToModuleMap[GameSettings.TouchMovementMode]
	elseif DevMovementMode == Enum.DevTouchMovementMode.Scriptable then
		return nil, true	
	else
		touchModule = movementEnumToModuleMap[DevMovementMode]
	end
	return touchModule, true
end

local function OnRenderStepped()
	if activeController and activeController.enabled and humanoid then
		local moveVector = activeController:GetMoveVector()

		local vehicleConsumedInput = false
		if vehicleController then
			moveVector, vehicleConsumedInput = vehicleController:Update(moveVector, activeControlModule==Gamepad)
		end

		-- User of vehicleConsumedInput is commented out to preserve legacy behavior, in case some game relies on Humanoid.MoveDirection still being set while in a VehicleSeat
		--if not vehicleConsumedInput then
			moveFunction(Players.LocalPlayer, moveVector, cameraRelative)
		--end

		humanoid.Jump = activeController:GetIsJumping() or (touchJumpController and touchJumpController:GetIsJumping())
	end
end

local function OnHumanoidSeated(active, currentSeatPart)
	if active then
		if currentSeatPart and currentSeatPart:IsA("VehicleSeat") then
			if not vehicleController then
				vehicleController = VehicleController.new()
			end
			vehicleController:Enable(true, currentSeatPart)
		end
	else
		if vehicleController then
			vehicleController:Enable(false, currentSeatPart)
		end
	end
end

local function OnCharacterAdded(char)
	humanoid = char:FindFirstChildOfClass("Humanoid")
	while not humanoid do
		char.ChildAdded:wait()
		humanoid = char:FindFirstChildOfClass("Humanoid")
	end

	if humanoidSeatedConn then
		humanoidSeatedConn:Disconnect()
		humanoidSeatedConn = nil
	end
	humanoidSeatedConn = humanoid.Seated:connect(OnHumanoidSeated)
end

local function OnCharacterRemoving(char)
	humanoid = nil
end

-- Helper function to lazily instantiate a controller if it does not yet exist,
-- disable the active controller if it is different from the on being switched to,
-- and then enable the requested controller. The argument to this function must be
-- a reference to one of the control modules, i.e. Keyboard, Gamepad, etc.
local function SwitchToController(controlModule)
	if not controlModule then
		if activeController then
			activeController:Enable(false)
		end
		activeController = nil
		activeControlModule = nil
	else
		if not controllers[controlModule] then
			controllers[controlModule] = controlModule.new()
		end

		if activeController ~= controllers[controlModule] then
			if activeController then
				activeController:Enable(false)
			end
			activeController = controllers[controlModule]
			activeControlModule = controlModule -- Only used to check if controller switch is necessary
			if touchControlFrame then
				activeController:Enable(true, touchControlFrame)
			else
				activeController:Enable(true)
			end
			if touchControlFrame and (activeControlModule == TouchThumbpad
									or activeControlModule == TouchThumbstick
									or activeControlModule == ClickToMove
									or activeControlModule == DynamicThumbstick) then
				touchJumpController = controllers[TouchJump]
				if not touchJumpController then
					touchJumpController = TouchJump.new()
				end
				touchJumpController:Enable(true, touchControlFrame)
			else
				if touchJumpController then
					touchJumpController:Enable(false)
				end
			end
		end
	end
end

local function OnLastInputTypeChanged(newLastInputType)
	if lastInputType == newLastInputType then
		warn("LastInputType Change listener called with current type.")
	end
	lastInputType = newLastInputType

	if lastInputType == Enum.UserInputType.Touch then
		-- TODO: Check if touch module already active
		local touchModule, success = SelectTouchModule()
		if success then
			while not touchControlFrame do
				wait()
			end
			SwitchToController(touchModule)
		end
	else
		local computerModule = computerInputTypeToModuleMap[lastInputType]
		if computerModule then
			SwitchToController(computerModule)
		end
	end
end

-- Called when any relevant values of GameSettings or LocalPlayer change, forcing re-evalulation of
-- current control scheme
local function OnComputerMovementModeChange()
	local controlModule, success =  SelectComputerMovementModule()
	if success then
		SwitchToController(controlModule)
	end
end

local function OnTouchMovementModeChange()
	local touchModule, success = SelectTouchModule()
	if success then
		while not touchControlFrame do
			wait()
		end
		SwitchToController(touchModule)
	end
end

Players.LocalPlayer.CharacterAdded:Connect(OnCharacterAdded)
Players.LocalPlayer.CharacterRemoving:Connect(OnCharacterRemoving)
if Players.LocalPlayer.Character then
	OnCharacterAdded(Players.LocalPlayer.Character)
end

RunService:BindToRenderStep("ControlScriptRenderstep", Enum.RenderPriority.Input.Value, OnRenderStepped)

UserInputService.LastInputTypeChanged:Connect(OnLastInputTypeChanged)

local propertyChangeListeners = {
	GameSettings:GetPropertyChangedSignal("TouchMovementMode"):Connect(OnTouchMovementModeChange),
	Players.LocalPlayer:GetPropertyChangedSignal("DevTouchMovementMode"):Connect(OnTouchMovementModeChange),

	GameSettings:GetPropertyChangedSignal("ComputerMovementMode"):Connect(OnComputerMovementModeChange),
	Players.LocalPlayer:GetPropertyChangedSignal("DevComputerMovementMode"):Connect(OnComputerMovementModeChange),
}

--[[ Touch Device UI ]]--
local PlayerGui = nil
local touchGui = nil
local playerGuiAddedConn = nil

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

	touchControlFrame = Instance.new("Frame")
	touchControlFrame.Name = "TouchControlFrame"
	touchControlFrame.Size = UDim2.new(1, 0, 1, 0)
	touchControlFrame.BackgroundTransparency = 1
	touchControlFrame.Parent = touchGui

	touchGui.Parent = PlayerGui
end

if UserInputService.TouchEnabled then
	PlayerGui = Players.LocalPlayer:FindFirstChildOfClass("PlayerGui")
	if PlayerGui then
		createTouchGuiContainer()
		OnLastInputTypeChanged(UserInputService:GetLastInputType())
	else
		playerGuiAddedConn = Players.LocalPlayer.ChildAdded:Connect(function(child)
			if child:IsA("PlayerGui") then
				PlayerGui = child
				createTouchGuiContainer()
				playerGuiAddedConn:Disconnect()
				playerGuiAddedConn = nil
				OnLastInputTypeChanged(UserInputService:GetLastInputType())
			end
		end)
	end
end

return ControlScript
