--[[ Created by Maelstorm_1973 Copyright (C)2023 Dracolynxis Technologies Inc. Library Scripts Context Action Service This replaces Roblox's ContextActionService system which is limited to 7 events. The number of events for this is unlimited. This uses lookup tables to determine what events have handlers attached to them, so this is quite fast in processing events and getting them to their handlers. ******** Functions/Methods **** bindAction Binds one or more input events to an action. bindAction(actionName, bindFunc, priority, touchButton, keyList, inputIndex): number actionName Name of the action (Required). bindFunc Reference to the callback function (Required). priority The priority to bind the function to. If not specified, then the default is medium priority. touchButton If a table, then it will create a touchbutton on the screen according to the parameters in the table. See below for table format. keyList
Can be a either a single Enum.KeyCode value or a table of Enum.KeyCode values, or nil. inputIndex A unique number for this action that allows enabling or disabling of the action without unbinding the action entirely. Used for turning actions on or off when the same keys or buttons are used for different purposes. Notes: One of touchButton or keyList is required. Table format for the touchButton parameter of bindAction: { Position UDim2 GUI position for touchscreen button. Size Size in Pixles of the touchscreen button. Image A Roblox asset Id --> "rbxassetid://"; Text Renders a text button with the string. BaseImage If the button base image and/or surroundng ring is to be rendered. false or enums.CASImageCode.Both - Do not render either background image or backround ring. enums.CASImageCode.Background - Do not render the background image. enums.CASImageCode.Ring - Do not render the background ring. BaseImageColor Colors the background ring only. Touch If false, then this button is not rendered. Existing If set, then this button is not rendered, but is attached to the given instance. } The requirements for the callback function has the same requirements as the callback function for Roblox's ContextActionService. The function definition is as follows: local function callback(actionName, inputState, inputObject) This returns a handle (number) that represents the callback function that can be used to remove the callback function using removeCallbackHandle() at a later time. If you wish to use a grid for the icons, then the utility function setGridPosition is provided. If you wish to use a standardized size of your onscreen touch icons, then the setIconSize utility function is provided. **** bindActionMulti Binds multiple actions in a single function call. bindActionMulti(buttonList): table buttonList A table of tables which contains all the information that is needed to bind actions. Table format for the buttonList parameter of bindActionMulti: { Action1 = {}; Action2 = {}; Action3 = {}; ... } Action1, Action2, and Action3 are subtables that have the same format as the one used for the touchButton parameter of bindAction above, with addtions. The names of the actions (Action1, Action2, Action3, etc...) does not matter. You can name them whatever makes sense to you. The additions for the action table format are as follows: { ActionName The name of the action to bind. (Required) Callback The function reference to call when a corresponding input event occurs. (Required) Priority If nil or not specified, then the default priority is Medium. Index The inputIndex of this action. If specified, it must be a number that is greather than zero (> 0) and it must be unique. KeyBind The index of the action to set the enabled status of. state The enabled state to set the action to. true - The action is enabled. false - The action is disabled. showWarn Sets the ability to display a warning if an attempt is made to set the status of an non-existant inputIndex. true - The warning is shown. false - The warning is disabled. Note: showWarn was added due to the realization that an action can be unbound bettwen enabling and disabling states. This eliminates the need to check for that situation. **** getActionEnabledStatus Returns the current enabled status of the specified inputIndex. getActionEnabledStatus(inputIndex): boolean inputIndex The index of the action to to return the enabled status of. **** unbindAction Unbinds the previous bound action by the given name. unbindAction(actionName) actionName The name of the previously bound action. Will not throw an error or warning if the action does not exist. **** unbindActionAll Unbinds all previously bound actions. unbindActionAll() **** getAllBindings Returns a table of all current bindings. getAllBindings(): Table The table that is returned is an array of tables with the following information: { ActionName The name of the previously bound action. KeyBind
A table of the keybindings for this action. Touch True if a touch button is associated with this action. False if not. } **** removeCallbackFunction Removes a callback function by actionName and function reference. removeCallbackFunction(actionName, func) actionName The name of the action to use. func The function reference to remove. Note: If the remaining number of callback functions for the action is zero, then the action itself is unbound. **** removeCallbackHandle Removes a callback function by it's handle. removeCallbackHandle(handle) handle The handle of the function to remove. Note: If the remaining number of callback functions for the associated action is zero, then the action itself is unbound. **** hideTouchButtons Shows or hides the touch buttons. hideTouchButtons(state) state If true, then the touch screen buttons are hidden from view. If false, then the touch screen buttons are visible. **** setGridPosition Sets the position of a touch button according to a grid layout. setGridPosition(x, y, horiz, vert): UDim2 x Horizontal grid position offset (not pixles). y Vertical grid position offset (not pixles). horiz Horizontal screen region reference indicator. 0 is from the left edge of the screen. If x = 0, then the left half of the touch button will be displayed off screen. Positive x values will move the touch button to the right. 0.5 is from the center of the screen. If x = 0, then the touch button is rendered in the middle of the screen. Positive x values move the touch button to the right while negative x values move the touch button to the left. 1 is from the right edge of the screen. As with 0 above, if x = 0, then right half of the touch button will be displayed off screen. Negative x values will move the touch button to the left. vert Vertical screen retion reference indicator. Similar to the horiz and x values above, this is for the y value. Note that the GUI Inset of 36 pixles is ignored for vertical placements. 0 is from the top edge of the screen. If y = 0, then the top half of the touch button will be displayed off screen. Positive y values will move the touch button down. 0.5 is from the center of the screen. If y = 0, then the touch button is rendered in the middle of the screen. Positive y values will move the touch button down while negative y values will move the touch button up. 1 is from the bottom edge of the screen. If y = 0 then the bottom half of the touch button will be rendered off screen. Negative y values will move the touch button up. The default values for horiz is 1 and for vert it's 0.5. This means that touch buttons that use these values for horiz and vert will have their positional reference at the middle right edge of the screen. The default grid values are 60x60 pixles as defined in scaleFactorX and scaleFactorY. **** setIconSize Returns the size of an icon from the given size. setIconSize(size): Number size Size scale for the icon (not pixles). The size is a multiple of the icon scale size which is currently 25 pixles as defined in scaleFactorS. ******** Enumerations This module utilizes custom enumerations called enums which is just a dictionary of name = value pairs. The definition of which is below: local enums = { -- Enumeration: CAS Image Frame Codes CASImageCode = { Both = 0; -- Remove both backround image and ring. Background = 1; -- Remove background only. Ring = 2; -- Remove ring only. }; } The module has included with the require the enum definitions for your convenience. To utilize these definitions: local moduleName = require(ContextActionService) local both = moduleName.enums.CASImageCode.Both ******** Keybinds There are some non-standard keybinds when dealing with the mouse and gamepads. These keybinds use the World codes from Enum.KeyCode. These codes are part of Roblox's API but my research has revealed that nothing uses them. Those definitions are as follows: Enum.KeyCode.World80 Mouse Wheel Forward Enum.KeyCode.World81 Mouse Wheel Backward Enum.KeyCode.World91 Mouse Wheel Moved Enum.Keycode.World92 Mouse Movement Enum.Keycode.World93 Mouse Button 1 Enum.Keycode.World94 Mouse Button 2 Enum.Keycode.World95 Mouse Button 3 Binding these keycodes will fire the defined callback functions for these actions. NOTE: As of 14 November 2023, I have just acquired an X-Box gamepad, so expect updates as I figure out how to code for it. --]] -- ******** Requirements -- Required Game Services and Facilities local playerService = game:GetService("Players") local userInputService = game:GetService("UserInputService") local starterGui = game:GetService("StarterGui") -- Scripting Support local localPlayer = playerService.LocalPlayer -- ******** Local Data local enums = { -- Enumeration: CAS Image Frame Codes CASImageCode = { Both = 0; -- Remove both backround image and ring. Background = 1; -- Remove background only. Ring = 2; -- Remove ring only. }; -- Enumeration: CAS Input Type Class CASInputTypeClass = { Touch = 0; Mouse = 1; Keyboard = 2; Gamepad = 3; }; } local playerGui = localPlayer.PlayerGui local clientGui = nil local CASGuiName = "CASGui" local CASSetEnableName = "CASSetEnableStatus" local setEnableEvent = nil local buttonFrame = nil local buttonImage = nil local buttonText = nil local moduleReady = false local mutex = false local humanDiedEvent1 = nil local humanDiedEvent2 = nil local humanDiedEvent3 = nil local humanDiedEvent4 = nil local diedDebounce = false local DEBUG = true local DEBUG_INDEX = false local INPUT_TEST_MODE = false local scaleFactorX = 60 -- X axis coordinate scale local scaleFactorY = 60 -- Y axis coordinate scale local scaleFactorS = 25 -- Size scale factor -- Input Type Classification -- 0 Touch -- 1 Mouse -- 2 Keyboard -- 3 Gamepad -- Everything else is undefined. local inputTypeClass = { [Enum.UserInputType.Touch] = 0; [Enum.UserInputType.MouseMovement] = 1; [Enum.UserInputType.MouseButton1] = 1; [Enum.UserInputType.MouseButton2] = 1; [Enum.UserInputType.MouseButton3] = 1; [Enum.UserInputType.MouseWheel] = 1; [Enum.UserInputType.Keyboard] = 2; [Enum.UserInputType.Gamepad1] = 3; [Enum.UserInputType.Gamepad2] = 3; [Enum.UserInputType.Gamepad3] = 3; [Enum.UserInputType.Gamepad4] = 3; [Enum.UserInputType.Gamepad5] = 3; [Enum.UserInputType.Gamepad6] = 3; [Enum.UserInputType.Gamepad7] = 3; [Enum.UserInputType.Gamepad8] = 3; } -- Lookup by action name. local nameList = {} -- Lookup by keybindings. local bindList = {} -- Action Index List local actionIndexList = {} -- Template for priority actions. local callbackTemplate = { priority = Enum.ContextActionPriority.Medium; handle = nil; callfunc = nil; } local actionTemplate = { name = ""; index = 0; callback = { high = {}; medium = {}; low = {}; }; touch = { frame = { instance = nil; }; buttonText = nil; buttonImage = nil; }; bindings = nil; } -- ******** Functions/Methods: Utility -- Recursively copies a table into a new table. local function recursiveCopy(dataTable) local tableCopy = {} for index, value in pairs(dataTable) do if type(value) == "table" then value = recursiveCopy(value) end tableCopy[index] = value end return tableCopy end -- Returns the player's parts. local function getPlayerParts(player) local char = player.Character while char == nil do task.wait() char = player.Character end local human = char:WaitForChild("Humanoid") local root = char:WaitForChild("HumanoidRootPart") return char, human, root end -- Waits for as long as necessary for the module -- initialization routine to complete. local function waitModule() while moduleReady == false do task.wait() end end -- Locks the mutex for mutual exclusion. local function lockMutex() local flag = true while flag == true do while mutex == true do task.wait(0.1) end if mutex == false then mutex = true flag = false end end end -- Unlocks the mutial exclusion mutex. local function unlockMutex() mutex = false end -- Counts the number of active callbacks for an action. local function countActionCallbacks(name) local data = nameList[name] if data == nil then return 0 end local count = 0 for _, priority in pairs(data.callback) do for _, callback in ipairs(priority) do count += 1 end end end -- ******** Functions/Methods: Function Handles -- Each callback function has a handle (number) assigned -- to it. This is to keep track of what callback functions -- are tied to which bindings which allows the removal -- of a callback function. local functionHandle = { list = {}; nextIndex = 1; } -- Adds the specified function and associated data to -- the function handler list. Returns the handle. functionHandle.add = function(func, action, priority) local handle = functionHandle.nextIndex functionHandle.nextIndex += 1 local data = { func = func; action = action; priority = priority; handle = handle; } functionHandle.list[handle] = data return handle end -- Removes the specified handle and associated function -- from the internal data. functionHandle.remove = function(handle) local hdata = functionHandle.list[handle] if hdata == nil then return end -- Retrieve Data, if possible... local adata = nameList[hdata.action] local plist = nil if hdata.priority == Enum.ContextActionPriority.High then plist = adata.callback.high elseif hdata.priority == Enum.ContextActionPriority.Medium then plist = adata.callback.medium elseif hdata.priority == Enum.ContextActionPriority.Low then plist = adata.callback.low else return end -- Search the list for the function. If found, then -- remove it. for i = 1, #plist, 1 do if plist[i].func == hdata.func then table.remove(plist, i) functionHandle.list[handle] = nil break end end end -- Checks to see if the function handle is valid. functionHandle.check = function(handle) if functionHandle.list[handle] ~= nil then return true end return false end -- Returns a copy of the data associated with the given -- function handle. Returns nil if not found. functionHandle.get = function(handle) if functionHandle.list[handle] == nil then return nil end return recursiveCopy(functionHandle.list[handle]) end -- Clears the function handle list. functionHandle.clear = function() functionHandle.list = {} end -- ******** Functions/Methods: Common Event Handlers -- Runs a function call list. local function eventCommonRunList(list, object, data) local name = data.name unlockMutex() for _, item in ipairs(list) do local status, message = pcall(function() return item.func(name, object.UserInputState, object) end) if status == false then warn("ContextActionService Callback Error:", message) warn(name, item) print(debug.traceback()) lockMutex() return false end if message ~= Enum.ContextActionResult.Pass then lockMutex() return false end end lockMutex() return true end -- Runs the priority context action lists. local function eventCommonPriority(object, actionList) -- Since the keybindings are a list of actions, we need to run -- the list. local result for _, data in pairs(actionList) do -- Check if the action has been disabled. if data.index > 0 and actionIndexList[data.index] ~= true then continue end -- Runs all defined high priority callbacks for the action. result = eventCommonRunList(data.callback.high, object, data) if result == false then continue end -- Runs all defined medium priority callbacks for the action. result = eventCommonRunList(data.callback.medium, object, data) if result == false then continue end -- Runs all defined low priority callbacks for the action. result = eventCommonRunList(data.callback.low, object, data) if result == false then continue end end end -- Common event code to get the data, if it exists. Also checks -- if a keyboard is enabled and if there is a text box that has -- focus. If it does, then we return nil because the user is -- most likely typing something in the chat. local function eventCommonDetermine(object, filter) -- Looks for GUI objects at the given location. local function guiObjectCheck(object) local list = playerGui:GetGuiObjectsAtPosition(object.Position.X, object.Position.Y) local name = nil local itemList = {} for _, item in pairs(list) do if item:IsA("Frame") then name = item:GetAttribute("Action") if name ~= nil then itemList[name] = nameList[name] end end end return itemList end -- Touch Input Handler local function inputHandlerTouch(object) if userInputService.TouchEnabled == true then if object.UserInputType == Enum.UserInputType.Touch then return guiObjectCheck(object) end else return nil end end -- Mouse Input Handler -- This converts mouse inputs to world key codes. -- The world key codes are no longer used per SDL 2.0. -- This is discussed in this post: -- https://devforum.roblox.com/t/what-are-the-inputs-world0-to-world95-used-for/222976 -- More information about SDL can be found here: -- https://wiki.libsdl.org/SDL2/FrontPage -- Since these are no longer used, we are reusing them for -- our purpose. Mouse wheel forward is Enum.KeyCode.World80 -- and mouse wheel backward is Enum.KeyCode.World81 -- We can alter the InputObject because this function -- is guaranteed to only be called when we have a mouse -- input event which means that there is no keycode -- assigned. local function inputHandlerMouse(object) if userInputService.MouseEnabled == true then local keyCode = Enum.KeyCode.Unknown local result = nil -- Go through all the possible mouse states and -- assign key codes for each one. if object.UserInputType == Enum.UserInputType.MouseMovement then keyCode = Enum.KeyCode.World92 object.KeyCode = keyCode result = bindList[keyCode] elseif object.UserInputType == Enum.UserInputType.MouseButton1 then keyCode = Enum.KeyCode.World93 object.KeyCode = keyCode result = guiObjectCheck(object) if result == nil then result = bindList[keyCode] end elseif object.UserInputType == Enum.UserInputType.MouseButton2 then keyCode = Enum.KeyCode.World94 object.KeyCode = keyCode result = guiObjectCheck(object) if result == nil then result = bindList[keyCode] end elseif object.UserInputType == Enum.UserInputType.MouseButton3 then keyCode = Enum.KeyCode.World95 object.KeyCode = keyCode result = guiObjectCheck(object) if result == nil then result = bindList[keyCode] end elseif object.UserInputType == Enum.UserInputType.MouseWheel then -- Mouse wheel is a special case since it can morph into -- different codes depending on what has been bound. keyCode = Enum.KeyCode.World91 object.KeyCode = keyCode result = bindList[keyCode] if result == nil then if object.Position.Z == 1 then -- Mouse Wheel Forward keyCode = Enum.KeyCode.World80 object.KeyCode = keyCode result = bindList[keyCode] elseif object.Position.Z == -1 then -- Mouse Wheel Backward keyCode = Enum.KeyCode.World81 object.KeyCode = keyCode result = bindList[keyCode] else warn("Invalid psoition value received for mouse wheel.") return nil end end end return result else return nil end end -- Keyboard Input Handler local function inputHandlerKeyboard(object) if userInputService.KeyboardEnabled == true then local keyCode = Enum.KeyCode.Unknown if userInputService:GetFocusedTextBox() == nil then if object.KeyCode ~= Enum.KeyCode.Unknown then keyCode = object.KeyCode end end return bindList[keyCode] else return nil end end -- Gamepad Input Handler local function inputHandlerGamepad(object) if userInputService.GamepadEnabled == true then local keyCode = Enum.KeyCode.Unknown if object.KeyCode ~= Enum.KeyCode.Unknown then keyCode = object.KeyCode end return bindList[keyCode] else return nil end end -- Make sure that the state is the correct one. if object.UserInputState ~= filter then return nil end -- Setup local keyCode = Enum.KeyCode.Unknown local data = nil -- Classify the input type broadly. local inputType = inputTypeClass[object.UserInputType] if inputType == nil then -- If the input type is not in the table, then -- we ignore it. return nil end -- Set the input type as an attribute on the object. object:SetAttribute("InputType", inputType) -- Process input according to class. if inputType == 0 then -- Touch Input return inputHandlerTouch(object) elseif inputType == 1 then -- Mouse Input return inputHandlerMouse(object) elseif inputType == 2 then -- Keyboard Input return inputHandlerKeyboard(object) elseif inputType == 3 then -- Gamepad Input return inputHandlerGamepad(object) else warn("ContextActionService: Unknown input type.") end -- We shouldn't get here, but if we do then -- return nil return nil end -- Common event handler for input beginning. local function eventCommonBegin(object) -- Get the data list associated with the input -- object and input state. lockMutex() local actionList = eventCommonDetermine(object, Enum.UserInputState.Begin) if actionList == nil then unlockMutex() return end -- Process Action. eventCommonPriority(object, actionList) unlockMutex() end -- Common event handler for input changing. local function eventCommonChange(object) -- Get the data list associated with the input -- object and input state. lockMutex() local actionList = eventCommonDetermine(object, Enum.UserInputState.Change) if actionList == nil then unlockMutex() return end -- Process Action. eventCommonPriority(object, actionList) unlockMutex() end -- Common event handler for input ending. local function eventCommonEnd(object) -- Get the data list associated with the input -- object and input state. lockMutex() local actionList = eventCommonDetermine(object, Enum.UserInputState.End) if actionList == nil then unlockMutex() return end -- Process Action. eventCommonPriority(object, actionList) unlockMutex() end -- ******** Functions/Methods: Main -- Sets up the touch button frame. local function setButtonFrame(data) local frame = buttonFrame:Clone() data.touch.frame.instance = frame frame:SetAttribute("Action", data.name) frame.Name = data.name frame.Parent = playerGui:FindFirstChild(CASGuiName) return frame end -- Checks the callbacks. local function checkCallbacks(list, func) for _, item in pairs(list) do if item.func == func then return true end end return false end -- Sets up the data for the action to be bound. local function bindActionSetup(name, bindFunc) local data local control if nameList[name] ~= nil then data = nameList[name] control = true -- Check if the callback function was already registered. local result = false result = if checkCallbacks(data.callback.high, bindFunc) == true then true else result result = if checkCallbacks(data.callback.medium, bindFunc) == true then true else result result = if checkCallbacks(data.callback.low, bindFunc) == true then true else result if result == true then warn("funcBind function was already registered for this action:", name) print(debug.traceback()) return nil end else data = recursiveCopy(actionTemplate) data.name = name nameList[name] = data control = false end return data, control end -- Adds the callback function to the list with -- the specified priority. local function bindActionCallback(data, bindFunc, priority) -- If priority wasn't specified, then set it to medium. if priority == nil then priority = Enum.ContextActionPriority.Medium end -- Add the callback to the function handle. local handle = functionHandle.add(bindFunc, data.name, priority) -- Fill out the callback table entry. local callback = recursiveCopy(callbackTemplate) callback.priority = priority callback.func = bindFunc callback.handle = handle -- Add the callback table entry to the appropreiate -- priority list. if priority == Enum.ContextActionPriority.High then table.insert(data.callback.high, callback) elseif priority == Enum.ContextActionPriority.Medium then table.insert(data.callback.medium, callback) elseif priority == Enum.ContextActionPriority.Low then table.insert(data.callback.low, callback) else warn("Invalid priority specified. Must be of Enum.ContextActionPriority or nil.") return false end -- Return the function handle. return handle end -- Handles the bind action touchscreen button. local function bindActionTouchscreen(data, button) if button == true then waitModule() setButtonFrame(data) elseif type(button) == "table" then -- If the touch flag is false, then we do not -- create a touch button. Exit. if button.Touch == false then return end waitModule() if button.Existing == nil then local frame = setButtonFrame(data) if button.Position ~= nil then frame.Position = button.Position end if button.Size ~= nil then frame.Size = button.Size end if button.BaseImage ~= nil then local image if button.BaseImage == false or button.BaseImage == enums.CASImageCode.Both then image = frame:FindFirstChild("BaseImage") image:Destroy() image = frame:FindFirstChild("RingImage") image:Destroy() elseif button.BaseImage == enums.CASImageCode.Background then image = frame:FindFirstChild("BaseImage") image:Destroy() elseif button.BaseImage == enums.CASImageCode.Ring then image = frame:FindFirstChild("RingImage") image:Destroy() end end if button.BaseImageColor ~= nil then if typeof(button.BaseImageColor) == "BrickColor" then local image = frame:FindFirstChild("RingImage") if image ~= nil then image.ImageColor3 = button.BaseImageColor.Color end elseif typeof(button.BaseImageColor) == "Color3" then local image = frame:FindFirstChild("RingImage") if image ~= nil then image.ImageColor3 = button.BaseImageColor end end end if button.Image ~= nil then local image = buttonImage:Clone() image.Image = button.Image image.Parent = frame data.touch.buttonImage = image if button.ImageSizeFactor ~= nil then local x = image.Size.X.Offset + button.ImageSizeFactor local y = image.Size.Y.Offset + button.ImageSizeFactor image.Size = UDim2.new(1, x, 1, y) end end if button.Text ~= nil then local text = buttonText:Clone() text.Text = button.Text text.Parent = frame data.touch.buttonText = text if button.TextSizeFactor ~= nil then local x = text.Size.X.Offset + button.TextSizeFactor local y = text.Size.Y.Offset + button.TextSizeFactor text.Size = UDim2.new(1, x, 1, y) end end else button.Existing:SetAttribute("Action", data.name) end end end -- Handles the bind action for keyboard/gamepad buttons. local function bindActionKeyboard(data, keybindList) if keybindList ~= nil then if type(keybindList) == "table" then -- Multiple Enum.KeyCode in a list. data.bindings = recursiveCopy(keybindList) for _, keycode in pairs(keybindList) do if bindList[keycode] == nil then bindList[keycode] = {} end bindList[keycode][data.name] = data end else -- Single Enum.KeyCode if bindList[keybindList] == nil then bindList[keybindList] = {} end bindList[keybindList][data.name] = data data.bindings = { keybindList } end end end -- ******** Functions/Methods: Public -- Binds one or more keycodes to an action. local function bindAction(actionName, bindFunc, priority, touchButton, keyList, inputIndex) lockMutex() -- Check Input if type(actionName) ~= "string" or actionName == "" then warn("The actionName parameter must be specified.") unlockMutex() return nil end if type(bindFunc) ~= "function" then warn("The bindFunc parameter must be specified.") unlockMutex() return nil end if touchButton == nil and keyList == nil then warn("One or more of of touchButton or bindList must be specified.") unlockMutex() return nil end if inputIndex ~= nil and type(inputIndex) ~= "number" then if inputIndex <= 0 then warn("The inputIndex parameter must be a number that's greater than 0.") else warn("The inputIndex parameter must be a number or nil.") end unlockMutex() return nil end -- Set Input Index if inputIndex == nil then inputIndex = 0 else if inputIndex > 0 then if actionIndexList[inputIndex] ~= nil then warn("The inputIndex parameter has already been assigned:", inputIndex) unlockMutex() return nil end actionIndexList[inputIndex] = true end end -- Setup local data, control = bindActionSetup(actionName, bindFunc) if data == nil then actionIndexList[inputIndex] = nil unlockMutex() return nil end -- Bind Function local handle = bindActionCallback(data, bindFunc, priority) if handle == false then if control == false then nameList[actionName] = nil end actionIndexList[inputIndex] = nil unlockMutex() return nil end -- Set Bindings if DEBUG ~= true then -- Touchscreen if userInputService.TouchEnabled == true then bindActionTouchscreen(data, touchButton) end -- Keyboard/Gamepad if userInputService.KeyboardEnabled == true or userInputService.GamepadEnabled == true then bindActionKeyboard(data, keyList) end else bindActionTouchscreen(data, touchButton) bindActionKeyboard(data, keyList) end -- Unlock Mutex unlockMutex() -- Returns the function handle. return handle end -- This is used if multiple buttons are to be created at -- once. Returns a table of handles. local function bindActionMulti(buttonList) -- Check Input if type(buttonList) ~= "table" then warn("Input data must be a table.") return nil end -- Setup local data local handle local control local handleList = {} lockMutex() -- Run the loop for each button defined. for _, button in pairs(buttonList) do -- Check Input if type(button.ActionName) ~= "string" or button.ActionName == "" then warn("The button.ActionName parameter must be specified.") continue end if type(button.Callback) ~= "function" then warn("The button.Callback parameter must be specified and must be a function.") continue end if (button.Image == nil or button.Image == "") and (button.Text == nil or button.Text == "") and button.KeyBind == nil then warn("One or more of of touchButton or bindList must be specified.") continue end if button.Index ~= nil and type(button.Index) ~= "number" then if button.Index <= 0 then warn("The button.Index parameter must be a number that's greater than 0.") else warn("The button.Index parameter must be a number or nil.") end unlockMutex() return nil end -- Set Input Index local inputIndex if button.Index == nil then inputIndex = 0 else inputIndex = button.Index if inputIndex > 0 then if actionIndexList[inputIndex] ~= nil then if actionIndexList[inputIndex] == true then -- Debugging code for conflicting indices. if DEBUG_INDEX ~= false then print(debug.traceback()) print(button) end warn("The inputIndex parameter has already been assigned:", inputIndex) unlockMutex() return nil end else actionIndexList[inputIndex] = true end end end -- Setup data, control = bindActionSetup(button.ActionName, button.Callback) if data == nil then actionIndexList[inputIndex] = nil continue end data.index = inputIndex -- Bind Function handle = bindActionCallback(data, button.Callback, button.Priority) if handle == false then if control == false then nameList[button.ActionName] = nil actionIndexList[inputIndex] = nil continue end end -- Set Bindings if DEBUG ~= true then if userInputService.TouchEnabled == true then bindActionTouchscreen(data, button) end if userInputService.KeyboardEnabled == true or userInputService.GamepadEnabled == true then bindActionKeyboard(data, button.KeyBind) end else bindActionTouchscreen(data, button) bindActionKeyboard(data, button.KeyBind) end table.insert(handleList, { handle = handle; func = button.Callback; action = button.ActionName; }) end -- Unlock Mutex unlockMutex() -- Returns the handle list. return handleList end -- Sets the enabled status of the specified index. -- True mean that the action for the associated index is enabled -- while false means that it's disabled. If the index is not -- assigned, a warning will be thrown according to how showWarn -- is set. local function setActionEnabledStatus(inputIndex, state, showWarn) if inputIndex == nil then warn("The inputIndex parameter must be specified.") return end if type(inputIndex) ~= "number" then warn("The input index parameter must be a number.") return end if inputIndex <= 0 then warn("The inputIndex parameter must be greater than 0.") return end if type(state) ~= "boolean" then warn("The state parameter must be either 'true' or 'false'.") return end if actionIndexList[inputIndex] == nil then if showWarn == true then warn("The input index parameter is not assigned:", inputIndex) end else lockMutex() actionIndexList[inputIndex] = state unlockMutex() end local event = localPlayer:FindFirstChild(CASSetEnableName) event:Fire(inputIndex, state) end -- Returns the enabled status of the specified index. local function getActionEnabledStatus(inputIndex) if inputIndex == nil then warn("The inputIndex parameter must be specified.") return nil end if type(inputIndex) ~= "number" then warn("The input index parameter must be a number.") return nil end if inputIndex <= 0 then warn("The inputIndex parameter must be greater than 0.") return nil end lockMutex() local result = actionIndexList[inputIndex] unlockMutex() return result end -- Unbinds all actions with the given name. Does not throw -- an error if the action name is not defined. local function unbindAction(actionName) -- Check Input if actionName == nil or actionName == "" then warn("The actionName parameter must be specified.") return end lockMutex() if nameList[actionName] == nil then unlockMutex() return end -- Setup local data = nameList[actionName] -- Remove touch button events and instances. if data.touch.frame.eventBegin ~= nil then data.touch.frame.eventBegin:Disconnect() data.touch.frame.eventBegin = nil end if data.touch.frame.eventChange ~= nil then data.touch.frame.eventChange:Disconnect() data.touch.frame.eventChange = nil end if data.touch.frame.eventEnd ~= nil then data.touch.frame.eventEnd:Disconnect() data.touch.frame.eventEnd = nil end if data.touch.frame.instance ~= nil then data.touch.frame.instance:Destroy() data.touch.frame.instance = nil end -- Removes all keybindings from the key binding list. for _, key in pairs(data.bindings) do bindList[key] = nil end -- Removes all assigned functions from the -- function handle list. for _, priority in pairs(data.callback) do for _, index in ipairs(priority) do functionHandle.remove(index.handle) end end -- Removes actionIndexList entry, if needed. if data.index > 0 then actionIndexList[data.index] = nil end -- Removes the action name. nameList[actionName] = nil -- Unlock Mutex unlockMutex() end -- Removes all bindings local function unbindActionAll() lockMutex() for name, data in pairs(nameList) do local data = nameList[name] if data.touch.frame.eventBegin ~= nil then data.touch.frame.eventBegin:Disconnect() data.touch.frame.eventBegin = nil end if data.touch.frame.eventChange ~= nil then data.touch.frame.eventChange:Disconnect() data.touch.frame.eventChange = nil end if data.touch.frame.eventEnd ~= nil then data.touch.frame.eventEnd:Disconnect() data.touch.frame.eventEnd = nil end if data.touch.frame.instance ~= nil then data.touch.frame.instance:Destroy() data.touch.frame.instance = nil end end -- Clears the tables. nameList = {} bindList = {} actionIndexList = {} functionHandle.clear() unlockMutex() end -- Returns a table of all bindings. The table includes the -- name of the binding, the keycodes, and if there is a touch -- screen button. local function getAllBindings() lockMutex() local button local keys local tab = {} for name, data in pairs(nameList) do if data.touch.frame.instance ~= nil then button = true else button = false end keys = recursiveCopy(data.bindings) table.insert(tab, { ActionName = name; KeyBind = keys; Touch = button; }) end unlockMutex() return tab end -- Removes a function from an action handler. local function removeCallbackFunction(actionName, func) -- Check Input if actionName == nil or actionName == "" then warn("The actionName parameter must be specified.") return end lockMutex() if nameList[actionName] == nil then warn("The specified action name does not exist.") unlockMutex() return end -- Search for function local data = nameList[actionName] local found = false for _, priority in pairs(data.callback) do for _, callback in ipairs(priority) do if callback.func == func then functionHandle.remove(callback.handle) found = true callback = nil break end end end -- Check Result if found == false then warn("The specified function reference was not found.") end -- If the action has 0 handlers, then we unbind the action. if countActionCallbacks(actionName) == 0 then unbindAction(actionName) end unlockMutex() end -- Removes a callback function from an action handler by the -- function handle. local function removeCallbackHandle(handle) -- Check Input if type(handle) ~= "number" then warn("The handle parameter must be a number") return end -- Get Handle Data lockMutex() local hdata = functionHandle.get(handle) if hdata == nil then warn("Callback function handle does not exist.") unlockMutex() return end -- Get Action Data local data = nameList[hdata.action] if data == nil then functionHandle.remove(handle) unlockMutex() return end -- Remove handled function from action. local list = data.callback[hdata.priority] for _, item in ipairs(list) do if item.handle == handle then item = nil end end functionHandle.remove(handle) -- If the action has 0 handlers, then we unbind the action. if countActionCallbacks(hdata.action) == 0 then unbindAction(hdata.action) end unlockMutex() end -- Hides/Shows screen touchbuttons. local function hideTouchButtons(state) local clientGui = playerGui:FindFirstChild(CASGuiName) if clientGui ~= nil then if state == true then clientGui.Enabled = false elseif state == false or state == nil then clientGui.Enabled = true end end end -- Returns a UDim2 based on a grid position. local function setGridPosition(x, y, horiz, vert) -- Setup local hsc local vsc if horiz ~= nil then hsc = horiz else hsc = 1 end if vert ~= nil then vsc = vert else vsc = 0.5 end local pos = UDim2.new(hsc, x * scaleFactorX, vsc, y * scaleFactorY) return pos end -- Returns a UDim2 size. local function setIconSize(size) if size == nil then size = 1 end local final = size * scaleFactorS return UDim2.new(0, final, 0, final) end -- ******** Events -- Called when the player dies so we can reset everything. local function humanoidDied() if diedDebounce == false then diedDebounce = true unbindActionAll() -- Not an issue unless the respawn time < 1 second or so. task.delay(1, function() humanDiedEvent1 = nil humanDiedEvent2 = nil humanDiedEvent3 = nil humanDiedEvent4 = nil diedDebounce = false end) end end -- Connects the death events so we can reset if the -- player dies. local function connectDeathEvents() if humanDiedEvent1 == nil then local char = localPlayer.Character while char == nil do task.wait() char = localPlayer.Character end local human = char:WaitForChild("Humanoid") humanDiedEvent1 = human.Died:Once(function() humanoidDied() end) humanDiedEvent2 = human.StateChanged:Once(function(old, new) if new == Enum.HumanoidStateType.Dead then humanoidDied() end end) humanDiedEvent3 = human.HealthChanged:Once(function(health) if health <= 0 then humanoidDied() end end) humanDiedEvent4 = human:GetPropertyChangedSignal("Health"):Once(function() if human.Health <= 0 then humanoidDied() end end) end end -- ******** Debugging -- Prints data from the input object depending on what the input -- mode is. local function printInputObjectTestMode(object, eventMode) if INPUT_TEST_MODE == 0 then -- Touch Test if inputTypeClass[object.UserInputType] == INPUT_TEST_MODE then print(string.format("Touch %s: Position:", eventMode), object.Position, "Delta:", object.Delta) end elseif INPUT_TEST_MODE == 1 then -- Mouse Test if inputTypeClass[object.UserInputType] == INPUT_TEST_MODE then if object.UserInputType == Enum.UserInputType.MouseMovement then print(string.format("Mouse %s: Position:", eventMode), object.Position, "Delta:", object.Delta) else print(string.format("Mouse %s: Type:", eventMode), object.UserInputType, "Position:", object.Position) end end elseif INPUT_TEST_MODE == 2 then -- Keyboard if inputTypeClass[object.UserInputType] == INPUT_TEST_MODE then print(string.format("Keyboard %s: KeyCode:", eventMode), object.KeyCode) end elseif INPUT_TEST_MODE == 3 then -- Gamepad if inputTypeClass[object.UserInputType] == INPUT_TEST_MODE then print(string.format("Gamepad %s: KeyCode:", eventMode), object.KeyCode, "Position:", object.Position) end else print(string.format("Unknown %s Input Type", eventMode), "\n\tInput Type:", object.UserInputType, '\n\tPosition:', object.Position, '\n\tDelta:', object.Delta, '\n\tKeyCode:', object.KeyCode) end end -- ******** Events -- Called when a user input is started. userInputService.InputBegan:Connect(function(object, event) if INPUT_TEST_MODE == false then eventCommonBegin(object) else printInputObjectTestMode(object, "Begin") end end) -- Called when a user input is changed. userInputService.InputChanged:Connect(function(object, event) if INPUT_TEST_MODE == false then eventCommonChange(object) else printInputObjectTestMode(object, "Changed") end end) -- Called when a user input has ended. userInputService.InputEnded:Connect(function(object, event) if INPUT_TEST_MODE == false then eventCommonEnd(object) else printInputObjectTestMode(object, "End") end end) -- Clones the GUI from StarterGUI to PlayerGUI when the -- player's character loads, if needed. localPlayer.CharacterAdded:Connect(function(char) connectDeathEvents() if playerGui:FindFirstChild(CASGuiName) == nil then unbindActionAll() local gui = starterGui:FindFirstChild(CASGuiName) local newGui = gui:Clone() newGui.Parent = playerGui clientGui = newGui end end) -- ******** Initialization -- We have to do some strange things with this because the -- module can be loaded in different LUA memory spaces. So -- the initialization will run for each seperate memory -- space the module is loaded in. We have to make sure that -- only one GUI is present in both StarterGui and PlayerGui. -- Creates the screen GUI for the module. Because this module -- can be included in different memory spaces, we need to check -- to make sure that there is only one GUI. local function createGui() -- Creates and returns a new ScreenGui with parameters set. local function create() -- Create Gui local gui = Instance.new("ScreenGui") gui.Name = CASGuiName gui.IgnoreGuiInset = true gui.ResetOnSpawn = true gui.ZIndexBehavior = Enum.ZIndexBehavior.Global gui.DisplayOrder = 20 gui.Enabled = true return gui end -- Check to see if the GUI is in PlayerGui. If not, then -- we create it and place it there. There can be only -- one instance in PlayerGui. local clientGui = playerGui:FindFirstChild(CASGuiName) if clientGui == nil then clientGui = create() clientGui.Parent = playerGui end -- Checks to see if the GUI is in StarterGui. If not, -- then we clone clientGui into it. There can be only -- one instance in StarterGui. local sourceGui = starterGui:FindFirstChild(CASGuiName) if sourceGui == nil then sourceGui = clientGui:Clone() sourceGui.Parent = starterGui end end -- Creates the template for the basic touchscreen -- button. local function createTouchButtonFrame() -- Button Frame local frame = Instance.new("Frame") frame.Name = "Frame" frame.Active = true frame.AnchorPoint = Vector2.new(0.5, 0.5) frame.Position = UDim2.new(0.5, 0, 0.5, 0) frame.Size = UDim2.new(0, 35, 0, 35) frame.BackgroundTransparency = 1 frame.ZIndex = 16300 -- Image Button -- This image is always present unless it's specified -- to be deleted. local button = Instance.new("ImageButton") button.Name = "BaseImage" button.AnchorPoint = Vector2.new(0.5, 0.5) button.Active = false button.Position = UDim2.new(0.5, 0, 0.5, 0) button.Size = UDim2.new(1, 0, 1, 0) button.BackgroundTransparency = 1 button.BorderSizePixel = 0 button.ImageColor3 = BrickColor.new("Institutional white").Color button.Image = "rbxassetid://7185003058" button.ZIndex = 16301 button.Parent = frame -- This is overlaid on top of the previous image. -- This image is always present unless it's specified -- to be deleted. local button = Instance.new("ImageButton") button.Name = "RingImage" button.AnchorPoint = Vector2.new(0.5, 0.5) button.Active = false button.Position = UDim2.new(0.5, 0, 0.5, 0) button.Size = UDim2.new(1, 0, 1, 0) button.BackgroundTransparency = 1 button.BorderSizePixel = 0 button.Image = "rbxassetid://429500449" button.ZIndex = 16302 button.Parent = frame return frame end -- Create a image button template. local function createTouchButtonImage() local button = Instance.new("ImageButton") button.Name = "ButtonImage" button.Active = false button.AnchorPoint = Vector2.new(0.5, 0.5) button.Position = UDim2.new(0.5, 0, 0.5, 0) button.Size = UDim2.new(1, -5, 1, -5) button.BackgroundTransparency = 1 button.BorderSizePixel = 0 button.ImageColor3 = BrickColor.new("Institutional white").Color button.Image = "" button.ZIndex = 16302 return button end -- Create a text button template. local function createTouchButtonText() local button = Instance.new("TextButton") button.Name = "ButtonText" button.Active = false button.AnchorPoint = Vector2.new(0.5, 0.5) button.Position = UDim2.new(0.5, 0, 0.5, 0) button.Size = UDim2.new(1, -2, 1, -2) button.BackgroundTransparency = 1 button.BorderSizePixel = 0 button.TextScaled = true button.TextColor = BrickColor.new("Institutional white") button.Text = "" button.ZIndex = 16303 return button end -- Create a BindableEvent under Player that allows cross -- actor signaling and then binds an event to it. local function createBindEvent() -- Check for the event. If not present then -- create it. local event = localPlayer:FindFirstChild(CASSetEnableName) if event == nil then event = Instance.new("BindableEvent") event.Name = CASSetEnableName event.Parent = localPlayer end -- Bind to the event. setEnableEvent = event.Event:Connect(function(index, status) if actionIndexList[index] ~= nil then actionIndexList[index] = status end end) end -- Initialize buttonFrame = createTouchButtonFrame() buttonImage = createTouchButtonImage() buttonText = createTouchButtonText() createGui() connectDeathEvents() createBindEvent() moduleReady = true print("**** Context Action Service Module Ready") -- ******** Module local module = { enums = enums; } module.bindAction = bindAction module.bindActionMulti = bindActionMulti module.setActionEnabledStatus = setActionEnabledStatus module.getActionEnabledStatus = getActionEnabledStatus module.unbindAction = unbindAction module.unbindActionAll = unbindActionAll module.removeCallbackFunction = removeCallbackFunction module.removeCallbackHandle = removeCallbackHandle module.getAllBindings = getAllBindings module.hideTouchButtons = hideTouchButtons module.setGridPosition = setGridPosition module.setIconSize = setIconSize return module