@hexium-softworks/inputservice 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +724 -0
- package/default.project.json +14 -0
- package/package.json +38 -0
- package/src/Client/InputServiceClient.luau +354 -0
- package/src/Client/InputServiceClient.spec.luau +71 -0
- package/src/Server/InputService.luau +186 -0
- package/src/Shared/InputServiceConfigUtils.luau +168 -0
- package/src/Shared/InputServiceConfigUtils.spec.luau +86 -0
- package/src/Shared/InputServiceConstants.luau +28 -0
- package/src/Shared/InputServiceInstanceUtils.luau +194 -0
- package/src/Shared/InputServiceInstanceUtils.spec.luau +100 -0
- package/src/Shared/InputServiceTypes.luau +63 -0
- package/src/jest.config.lua +3 -0
- package/src/node_modules.project.json +7 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Validation helpers for InputService configs.
|
|
4
|
+
|
|
5
|
+
@class InputServiceConfigUtils
|
|
6
|
+
]=]
|
|
7
|
+
|
|
8
|
+
local require = require(script.Parent.loader).load(script)
|
|
9
|
+
|
|
10
|
+
local Table = require("Table")
|
|
11
|
+
|
|
12
|
+
local InputServiceConfigUtils = {}
|
|
13
|
+
|
|
14
|
+
type FieldMap = { [string]: string }
|
|
15
|
+
|
|
16
|
+
local CONTEXT_FIELDS: FieldMap = Table.readonly({
|
|
17
|
+
Name = "string",
|
|
18
|
+
Enabled = "boolean",
|
|
19
|
+
Priority = "number",
|
|
20
|
+
Sink = "boolean",
|
|
21
|
+
Actions = "table",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
local ACTION_FIELDS: FieldMap = Table.readonly({
|
|
25
|
+
Name = "string",
|
|
26
|
+
DisplayName = "string",
|
|
27
|
+
Enabled = "boolean",
|
|
28
|
+
Type = "InputActionType",
|
|
29
|
+
Bindings = "table",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
local BINDING_FIELDS: FieldMap = Table.readonly({
|
|
33
|
+
Name = "string",
|
|
34
|
+
Type = "InputBindingType",
|
|
35
|
+
KeyCode = "KeyCode",
|
|
36
|
+
UIButton = "GuiButton",
|
|
37
|
+
UIModifier = "GuiButton",
|
|
38
|
+
PrimaryModifier = "KeyCode",
|
|
39
|
+
SecondaryModifier = "KeyCode",
|
|
40
|
+
DisplayName = "string",
|
|
41
|
+
DisplayImage = "Content",
|
|
42
|
+
PressedThreshold = "number",
|
|
43
|
+
ReleasedThreshold = "number",
|
|
44
|
+
ResponseCurve = "number",
|
|
45
|
+
Scale = "number",
|
|
46
|
+
Vector2Scale = "Vector2",
|
|
47
|
+
Vector3Scale = "Vector3",
|
|
48
|
+
PointerIndex = "number",
|
|
49
|
+
ClampMagnitudeToOne = "boolean",
|
|
50
|
+
Up = "KeyCode",
|
|
51
|
+
Down = "KeyCode",
|
|
52
|
+
Left = "KeyCode",
|
|
53
|
+
Right = "KeyCode",
|
|
54
|
+
Forward = "KeyCode",
|
|
55
|
+
Backward = "KeyCode",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
local function validateName(value: any, path: string): ()
|
|
59
|
+
assert(typeof(value) == "string", string.format("%s.Name must be a string", path))
|
|
60
|
+
assert(#value > 0, string.format("%s.Name must not be empty", path))
|
|
61
|
+
assert(#value <= 100, string.format("%s.Name must be 100 characters or fewer", path))
|
|
62
|
+
assert(not string.find(value, "[%c/]"), string.format("%s.Name contains unsupported characters", path))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local function validateEnumItem(value: any, enumType: any, fieldName: string): ()
|
|
66
|
+
assert(typeof(value) == "EnumItem", string.format("%s must be an EnumItem", fieldName))
|
|
67
|
+
assert(value.EnumType == enumType, string.format("%s must be a member of %s", fieldName, tostring(enumType)))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
local function validateGuiButton(value: any, fieldName: string): ()
|
|
71
|
+
assert(typeof(value) == "Instance", string.format("%s must be a GuiButton", fieldName))
|
|
72
|
+
assert(value:IsA("GuiButton"), string.format("%s must be a GuiButton", fieldName))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
local function validateField(value: any, expectedType: string, fieldName: string): ()
|
|
76
|
+
if expectedType == "KeyCode" then
|
|
77
|
+
validateEnumItem(value, Enum.KeyCode, fieldName)
|
|
78
|
+
elseif expectedType == "InputActionType" then
|
|
79
|
+
validateEnumItem(value, Enum.InputActionType, fieldName)
|
|
80
|
+
elseif expectedType == "InputBindingType" then
|
|
81
|
+
validateEnumItem(value, Enum.InputBindingType, fieldName)
|
|
82
|
+
elseif expectedType == "GuiButton" then
|
|
83
|
+
validateGuiButton(value, fieldName)
|
|
84
|
+
elseif expectedType == "Content" then
|
|
85
|
+
assert(typeof(value) == "string" or typeof(value) == "Content", string.format("%s must be Content", fieldName))
|
|
86
|
+
else
|
|
87
|
+
assert(typeof(value) == expectedType, string.format("%s must be %s", fieldName, expectedType))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
local function validateKnownFields(config: { [any]: any }, allowedFields: FieldMap, path: string): ()
|
|
92
|
+
for key, value in config do
|
|
93
|
+
assert(typeof(key) == "string", string.format("%s contains a non-string key", path))
|
|
94
|
+
|
|
95
|
+
local expectedType = allowedFields[key]
|
|
96
|
+
assert(expectedType ~= nil, string.format("%s.%s is not a supported config field", path, key))
|
|
97
|
+
|
|
98
|
+
if value ~= nil then
|
|
99
|
+
validateField(value, expectedType, string.format("%s.%s", path, key))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
function InputServiceConfigUtils.validateBindingConfig(config: any, path: string?): ()
|
|
105
|
+
local bindingPath = path or "Binding"
|
|
106
|
+
assert(typeof(config) == "table", string.format("%s must be a table", bindingPath))
|
|
107
|
+
validateKnownFields(config, BINDING_FIELDS, bindingPath)
|
|
108
|
+
validateName(config.Name, bindingPath)
|
|
109
|
+
|
|
110
|
+
if config.PressedThreshold ~= nil and config.ReleasedThreshold ~= nil then
|
|
111
|
+
assert(
|
|
112
|
+
config.PressedThreshold >= config.ReleasedThreshold,
|
|
113
|
+
string.format("%s.PressedThreshold must be greater than or equal to ReleasedThreshold", bindingPath)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
function InputServiceConfigUtils.validateActionConfig(config: any, path: string?): ()
|
|
119
|
+
local actionPath = path or "Action"
|
|
120
|
+
assert(typeof(config) == "table", string.format("%s must be a table", actionPath))
|
|
121
|
+
validateKnownFields(config, ACTION_FIELDS, actionPath)
|
|
122
|
+
validateName(config.Name, actionPath)
|
|
123
|
+
|
|
124
|
+
if config.Bindings ~= nil then
|
|
125
|
+
for index, bindingConfig in config.Bindings do
|
|
126
|
+
assert(typeof(index) == "number", string.format("%s.Bindings must be an array", actionPath))
|
|
127
|
+
InputServiceConfigUtils.validateBindingConfig(
|
|
128
|
+
bindingConfig,
|
|
129
|
+
string.format("%s.Bindings[%d]", actionPath, index)
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
function InputServiceConfigUtils.validateContextConfig(config: any, path: string?): ()
|
|
136
|
+
local contextPath = path or "Context"
|
|
137
|
+
assert(typeof(config) == "table", string.format("%s must be a table", contextPath))
|
|
138
|
+
validateKnownFields(config, CONTEXT_FIELDS, contextPath)
|
|
139
|
+
validateName(config.Name, contextPath)
|
|
140
|
+
|
|
141
|
+
if config.Actions ~= nil then
|
|
142
|
+
for index, actionConfig in config.Actions do
|
|
143
|
+
assert(typeof(index) == "number", string.format("%s.Actions must be an array", contextPath))
|
|
144
|
+
InputServiceConfigUtils.validateActionConfig(
|
|
145
|
+
actionConfig,
|
|
146
|
+
string.format("%s.Actions[%d]", contextPath, index)
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
function InputServiceConfigUtils.assertCanCreateInputClass(className: string): ()
|
|
153
|
+
local ok, instance = pcall(Instance.new, className, nil)
|
|
154
|
+
if ok and typeof(instance) == "Instance" then
|
|
155
|
+
instance:Destroy()
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
error(
|
|
160
|
+
string.format(
|
|
161
|
+
"%s is not available in this Roblox runtime. Enable the Input Action System beta or run in a newer engine.",
|
|
162
|
+
className
|
|
163
|
+
),
|
|
164
|
+
2
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
return InputServiceConfigUtils
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class InputServiceConfigUtils.spec
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local InputServiceConfigUtils = require("InputServiceConfigUtils")
|
|
10
|
+
|
|
11
|
+
local describe = Jest.Globals.describe
|
|
12
|
+
local expect = Jest.Globals.expect
|
|
13
|
+
local it = Jest.Globals.it
|
|
14
|
+
|
|
15
|
+
local function getBoolInputActionType(): any
|
|
16
|
+
local ok, value = pcall(function()
|
|
17
|
+
return Enum.InputActionType.Bool
|
|
18
|
+
end)
|
|
19
|
+
|
|
20
|
+
if ok then
|
|
21
|
+
return value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe("InputServiceConfigUtils", function()
|
|
28
|
+
it("accepts a valid context config", function()
|
|
29
|
+
expect(function()
|
|
30
|
+
InputServiceConfigUtils.validateContextConfig({
|
|
31
|
+
Name = "PlayContext",
|
|
32
|
+
Enabled = true,
|
|
33
|
+
Priority = 2000,
|
|
34
|
+
Sink = true,
|
|
35
|
+
Actions = {
|
|
36
|
+
{
|
|
37
|
+
Name = "Sprint",
|
|
38
|
+
Bindings = {
|
|
39
|
+
{
|
|
40
|
+
Name = "Keyboard",
|
|
41
|
+
KeyCode = Enum.KeyCode.LeftShift,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
end).never.toThrow()
|
|
48
|
+
end)
|
|
49
|
+
|
|
50
|
+
if getBoolInputActionType() ~= nil then
|
|
51
|
+
it("accepts InputActionType enum values", function()
|
|
52
|
+
expect(function()
|
|
53
|
+
InputServiceConfigUtils.validateActionConfig({
|
|
54
|
+
Name = "Sprint",
|
|
55
|
+
Type = getBoolInputActionType(),
|
|
56
|
+
})
|
|
57
|
+
end).never.toThrow()
|
|
58
|
+
end)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it("rejects unknown fields", function()
|
|
62
|
+
expect(function()
|
|
63
|
+
InputServiceConfigUtils.validateContextConfig({
|
|
64
|
+
Name = "PlayContext",
|
|
65
|
+
RemoteEventName = "Nope",
|
|
66
|
+
})
|
|
67
|
+
end).toThrow()
|
|
68
|
+
end)
|
|
69
|
+
|
|
70
|
+
it("rejects unsafe names", function()
|
|
71
|
+
expect(function()
|
|
72
|
+
InputServiceConfigUtils.validateActionConfig({
|
|
73
|
+
Name = "Bad/Action",
|
|
74
|
+
})
|
|
75
|
+
end).toThrow()
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
it("rejects mismatched enum types", function()
|
|
79
|
+
expect(function()
|
|
80
|
+
InputServiceConfigUtils.validateActionConfig({
|
|
81
|
+
Name = "Sprint",
|
|
82
|
+
Type = Enum.KeyCode.E,
|
|
83
|
+
})
|
|
84
|
+
end).toThrow()
|
|
85
|
+
end)
|
|
86
|
+
end)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Constants for the input action system wrapper.
|
|
4
|
+
|
|
5
|
+
@class InputServiceConstants
|
|
6
|
+
]=]
|
|
7
|
+
|
|
8
|
+
local require = require(script.Parent.loader).load(script)
|
|
9
|
+
|
|
10
|
+
local Table = require("Table")
|
|
11
|
+
|
|
12
|
+
export type InputServiceConstants = {
|
|
13
|
+
ROOT_FOLDER_NAME: string,
|
|
14
|
+
ROOT_FOLDER_CLASS_NAME: string,
|
|
15
|
+
CONTEXT_CLASS_NAME: string,
|
|
16
|
+
ACTION_CLASS_NAME: string,
|
|
17
|
+
BINDING_CLASS_NAME: string,
|
|
18
|
+
DEFAULT_CONTEXT_PRIORITY: number,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return Table.readonly({
|
|
22
|
+
ROOT_FOLDER_NAME = "Inputs",
|
|
23
|
+
ROOT_FOLDER_CLASS_NAME = "Folder",
|
|
24
|
+
CONTEXT_CLASS_NAME = "InputContext",
|
|
25
|
+
ACTION_CLASS_NAME = "InputAction",
|
|
26
|
+
BINDING_CLASS_NAME = "InputBinding",
|
|
27
|
+
DEFAULT_CONTEXT_PRIORITY = 0,
|
|
28
|
+
} :: InputServiceConstants)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Instance creation and upsert utilities for InputService.
|
|
4
|
+
|
|
5
|
+
@class InputServiceInstanceUtils
|
|
6
|
+
]=]
|
|
7
|
+
|
|
8
|
+
local require = require(script.Parent.loader).load(script)
|
|
9
|
+
|
|
10
|
+
local InputServiceConfigUtils = require("InputServiceConfigUtils")
|
|
11
|
+
local InputServiceConstants = require("InputServiceConstants")
|
|
12
|
+
local InputServiceTypes = require("InputServiceTypes")
|
|
13
|
+
|
|
14
|
+
type InputContextConfig = InputServiceTypes.InputContextConfig
|
|
15
|
+
type InputActionConfig = InputServiceTypes.InputActionConfig
|
|
16
|
+
type InputBindingConfig = InputServiceTypes.InputBindingConfig
|
|
17
|
+
|
|
18
|
+
local InputServiceInstanceUtils = {}
|
|
19
|
+
|
|
20
|
+
local CONTEXT_PROPERTIES = { "Enabled", "Priority", "Sink" }
|
|
21
|
+
local ACTION_PROPERTIES = { "DisplayName", "Enabled", "Type" }
|
|
22
|
+
local BINDING_PROPERTIES = {
|
|
23
|
+
"Type",
|
|
24
|
+
"KeyCode",
|
|
25
|
+
"UIButton",
|
|
26
|
+
"UIModifier",
|
|
27
|
+
"PrimaryModifier",
|
|
28
|
+
"SecondaryModifier",
|
|
29
|
+
"DisplayName",
|
|
30
|
+
"DisplayImage",
|
|
31
|
+
"PressedThreshold",
|
|
32
|
+
"ReleasedThreshold",
|
|
33
|
+
"ResponseCurve",
|
|
34
|
+
"Scale",
|
|
35
|
+
"Vector2Scale",
|
|
36
|
+
"Vector3Scale",
|
|
37
|
+
"PointerIndex",
|
|
38
|
+
"ClampMagnitudeToOne",
|
|
39
|
+
"Up",
|
|
40
|
+
"Down",
|
|
41
|
+
"Left",
|
|
42
|
+
"Right",
|
|
43
|
+
"Forward",
|
|
44
|
+
"Backward",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
local function findTypedChild(parent: Instance, name: string, className: string): Instance?
|
|
48
|
+
local child = parent:FindFirstChild(name)
|
|
49
|
+
if child == nil then
|
|
50
|
+
return nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
assert(
|
|
54
|
+
child:IsA(className),
|
|
55
|
+
string.format("%s exists but is a %s, not %s", child:GetFullName(), child.ClassName, className)
|
|
56
|
+
)
|
|
57
|
+
return child
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local function upsertChild(parent: Instance, name: string, className: string): Instance
|
|
61
|
+
local child = findTypedChild(parent, name, className)
|
|
62
|
+
if child then
|
|
63
|
+
return child
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
InputServiceConfigUtils.assertCanCreateInputClass(className)
|
|
67
|
+
|
|
68
|
+
local instance = Instance.new(className)
|
|
69
|
+
instance.Name = name
|
|
70
|
+
instance.Parent = parent
|
|
71
|
+
return instance
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
local function applyProperties(instance: Instance, config: { [string]: any }, propertyNames: { string }): ()
|
|
75
|
+
local writableInstance = instance :: any
|
|
76
|
+
for _, propertyName in propertyNames do
|
|
77
|
+
local value = config[propertyName]
|
|
78
|
+
if value ~= nil then
|
|
79
|
+
writableInstance[propertyName] = value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
function InputServiceInstanceUtils.getOrCreateRootFolder(parent: Instance): Folder
|
|
85
|
+
local child = parent:FindFirstChild(InputServiceConstants.ROOT_FOLDER_NAME)
|
|
86
|
+
if child then
|
|
87
|
+
assert(
|
|
88
|
+
child:IsA(InputServiceConstants.ROOT_FOLDER_CLASS_NAME),
|
|
89
|
+
string.format(
|
|
90
|
+
"%s must be a %s",
|
|
91
|
+
child:GetFullName(),
|
|
92
|
+
InputServiceConstants.ROOT_FOLDER_CLASS_NAME
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return child :: Folder
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
local folder = Instance.new(InputServiceConstants.ROOT_FOLDER_CLASS_NAME)
|
|
99
|
+
folder.Name = InputServiceConstants.ROOT_FOLDER_NAME
|
|
100
|
+
folder.Parent = parent
|
|
101
|
+
return folder :: Folder
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
function InputServiceInstanceUtils.getContext(rootFolder: Instance, contextName: string): InputContext?
|
|
105
|
+
local context = findTypedChild(rootFolder, contextName, InputServiceConstants.CONTEXT_CLASS_NAME)
|
|
106
|
+
return context :: InputContext?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
function InputServiceInstanceUtils.getAction(context: Instance, actionName: string): InputAction?
|
|
110
|
+
assert(context:IsA(InputServiceConstants.CONTEXT_CLASS_NAME), "Context must be an InputContext")
|
|
111
|
+
|
|
112
|
+
local action = findTypedChild(context, actionName, InputServiceConstants.ACTION_CLASS_NAME)
|
|
113
|
+
return action :: InputAction?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
function InputServiceInstanceUtils.getBinding(action: Instance, bindingName: string): InputBinding?
|
|
117
|
+
assert(action:IsA(InputServiceConstants.ACTION_CLASS_NAME), "Action must be an InputAction")
|
|
118
|
+
|
|
119
|
+
local binding = findTypedChild(action, bindingName, InputServiceConstants.BINDING_CLASS_NAME)
|
|
120
|
+
return binding :: InputBinding?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
function InputServiceInstanceUtils.upsertBinding(action: Instance, config: InputBindingConfig): InputBinding
|
|
124
|
+
assert(action:IsA(InputServiceConstants.ACTION_CLASS_NAME), "Binding parent must be an InputAction")
|
|
125
|
+
InputServiceConfigUtils.validateBindingConfig(config)
|
|
126
|
+
|
|
127
|
+
local binding = upsertChild(action, config.Name, InputServiceConstants.BINDING_CLASS_NAME) :: InputBinding
|
|
128
|
+
applyProperties(binding, config :: any, BINDING_PROPERTIES)
|
|
129
|
+
return binding
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
function InputServiceInstanceUtils.upsertAction(context: Instance, config: InputActionConfig): InputAction
|
|
133
|
+
assert(context:IsA(InputServiceConstants.CONTEXT_CLASS_NAME), "Action parent must be an InputContext")
|
|
134
|
+
InputServiceConfigUtils.validateActionConfig(config)
|
|
135
|
+
|
|
136
|
+
local action = upsertChild(context, config.Name, InputServiceConstants.ACTION_CLASS_NAME) :: InputAction
|
|
137
|
+
applyProperties(action, config :: any, ACTION_PROPERTIES)
|
|
138
|
+
|
|
139
|
+
if config.Bindings then
|
|
140
|
+
for _, bindingConfig in config.Bindings do
|
|
141
|
+
InputServiceInstanceUtils.upsertBinding(action, bindingConfig)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
return action
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
function InputServiceInstanceUtils.upsertContext(rootFolder: Instance, config: InputContextConfig): InputContext
|
|
149
|
+
assert(rootFolder:IsA(InputServiceConstants.ROOT_FOLDER_CLASS_NAME), "Context parent must be the input root Folder")
|
|
150
|
+
InputServiceConfigUtils.validateContextConfig(config)
|
|
151
|
+
|
|
152
|
+
local context = upsertChild(rootFolder, config.Name, InputServiceConstants.CONTEXT_CLASS_NAME) :: InputContext
|
|
153
|
+
applyProperties(context, config :: any, CONTEXT_PROPERTIES)
|
|
154
|
+
|
|
155
|
+
if config.Actions then
|
|
156
|
+
for _, actionConfig in config.Actions do
|
|
157
|
+
InputServiceInstanceUtils.upsertAction(context, actionConfig)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return context
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
function InputServiceInstanceUtils.removeContext(rootFolder: Instance, contextName: string): boolean
|
|
165
|
+
local context = InputServiceInstanceUtils.getContext(rootFolder, contextName)
|
|
166
|
+
if not context then
|
|
167
|
+
return false
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context:Destroy()
|
|
171
|
+
return true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
function InputServiceInstanceUtils.removeAction(context: Instance, actionName: string): boolean
|
|
175
|
+
local action = InputServiceInstanceUtils.getAction(context, actionName)
|
|
176
|
+
if not action then
|
|
177
|
+
return false
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
action:Destroy()
|
|
181
|
+
return true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
function InputServiceInstanceUtils.removeBinding(action: Instance, bindingName: string): boolean
|
|
185
|
+
local binding = InputServiceInstanceUtils.getBinding(action, bindingName)
|
|
186
|
+
if not binding then
|
|
187
|
+
return false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
binding:Destroy()
|
|
191
|
+
return true
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
return InputServiceInstanceUtils
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class InputServiceInstanceUtils.spec
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local InputServiceConstants = require("InputServiceConstants")
|
|
9
|
+
local InputServiceInstanceUtils = require("InputServiceInstanceUtils")
|
|
10
|
+
local Jest = require("Jest")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
local function canCreateInputActionInstances(): boolean
|
|
17
|
+
local ok, instance = pcall(Instance.new, InputServiceConstants.CONTEXT_CLASS_NAME, nil)
|
|
18
|
+
if ok and typeof(instance) == "Instance" then
|
|
19
|
+
instance:Destroy()
|
|
20
|
+
return true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
return false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe("InputServiceInstanceUtils", function()
|
|
27
|
+
it("guards unsupported runtimes", function()
|
|
28
|
+
expect(typeof(canCreateInputActionInstances())).toEqual("boolean")
|
|
29
|
+
end)
|
|
30
|
+
|
|
31
|
+
if canCreateInputActionInstances() then
|
|
32
|
+
it("upserts contexts without duplicating actions or bindings", function()
|
|
33
|
+
local root = Instance.new("Folder")
|
|
34
|
+
root.Name = InputServiceConstants.ROOT_FOLDER_NAME
|
|
35
|
+
|
|
36
|
+
local context = InputServiceInstanceUtils.upsertContext(root, {
|
|
37
|
+
Name = "PlayContext",
|
|
38
|
+
Enabled = true,
|
|
39
|
+
Priority = 10,
|
|
40
|
+
Sink = false,
|
|
41
|
+
Actions = {
|
|
42
|
+
{
|
|
43
|
+
Name = "Sprint",
|
|
44
|
+
Type = Enum.InputActionType.Bool,
|
|
45
|
+
Bindings = {
|
|
46
|
+
{
|
|
47
|
+
Name = "Keyboard",
|
|
48
|
+
KeyCode = Enum.KeyCode.LeftShift,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
InputServiceInstanceUtils.upsertContext(root, {
|
|
56
|
+
Name = "PlayContext",
|
|
57
|
+
Priority = 25,
|
|
58
|
+
Actions = {
|
|
59
|
+
{
|
|
60
|
+
Name = "Sprint",
|
|
61
|
+
DisplayName = "Run",
|
|
62
|
+
Bindings = {
|
|
63
|
+
{
|
|
64
|
+
Name = "Keyboard",
|
|
65
|
+
KeyCode = Enum.KeyCode.E,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
local sprint = context:FindFirstChild("Sprint")
|
|
73
|
+
expect(context.Priority).toEqual(25)
|
|
74
|
+
expect(#root:GetChildren()).toEqual(1)
|
|
75
|
+
expect(sprint ~= nil).toEqual(true)
|
|
76
|
+
expect(#(sprint :: Instance):GetChildren()).toEqual(1)
|
|
77
|
+
|
|
78
|
+
root:Destroy()
|
|
79
|
+
end)
|
|
80
|
+
|
|
81
|
+
it("removes only targeted objects", function()
|
|
82
|
+
local root = Instance.new("Folder")
|
|
83
|
+
root.Name = InputServiceConstants.ROOT_FOLDER_NAME
|
|
84
|
+
|
|
85
|
+
local context = InputServiceInstanceUtils.upsertContext(root, {
|
|
86
|
+
Name = "PlayContext",
|
|
87
|
+
Actions = {
|
|
88
|
+
{ Name = "Sprint" },
|
|
89
|
+
{ Name = "Jump" },
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
expect(InputServiceInstanceUtils.removeAction(context, "Sprint")).toEqual(true)
|
|
94
|
+
expect(context:FindFirstChild("Sprint")).toEqual(nil)
|
|
95
|
+
expect(context:FindFirstChild("Jump") ~= nil).toEqual(true)
|
|
96
|
+
|
|
97
|
+
root:Destroy()
|
|
98
|
+
end)
|
|
99
|
+
end
|
|
100
|
+
end)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Shared public types for InputService.
|
|
4
|
+
|
|
5
|
+
@class InputServiceTypes
|
|
6
|
+
]=]
|
|
7
|
+
|
|
8
|
+
local InputServiceTypes = {}
|
|
9
|
+
|
|
10
|
+
export type InputActionState = boolean | number | Vector2 | Vector3
|
|
11
|
+
|
|
12
|
+
export type InputBindingConfig = {
|
|
13
|
+
Name: string,
|
|
14
|
+
Type: Enum.InputBindingType?,
|
|
15
|
+
KeyCode: Enum.KeyCode?,
|
|
16
|
+
UIButton: GuiButton?,
|
|
17
|
+
UIModifier: GuiButton?,
|
|
18
|
+
PrimaryModifier: Enum.KeyCode?,
|
|
19
|
+
SecondaryModifier: Enum.KeyCode?,
|
|
20
|
+
DisplayName: string?,
|
|
21
|
+
DisplayImage: any?,
|
|
22
|
+
PressedThreshold: number?,
|
|
23
|
+
ReleasedThreshold: number?,
|
|
24
|
+
ResponseCurve: number?,
|
|
25
|
+
Scale: number?,
|
|
26
|
+
Vector2Scale: Vector2?,
|
|
27
|
+
Vector3Scale: Vector3?,
|
|
28
|
+
PointerIndex: number?,
|
|
29
|
+
ClampMagnitudeToOne: boolean?,
|
|
30
|
+
Up: Enum.KeyCode?,
|
|
31
|
+
Down: Enum.KeyCode?,
|
|
32
|
+
Left: Enum.KeyCode?,
|
|
33
|
+
Right: Enum.KeyCode?,
|
|
34
|
+
Forward: Enum.KeyCode?,
|
|
35
|
+
Backward: Enum.KeyCode?,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type InputActionConfig = {
|
|
39
|
+
Name: string,
|
|
40
|
+
DisplayName: string?,
|
|
41
|
+
Enabled: boolean?,
|
|
42
|
+
Type: Enum.InputActionType?,
|
|
43
|
+
Bindings: { InputBindingConfig }?,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type InputContextConfig = {
|
|
47
|
+
Name: string,
|
|
48
|
+
Enabled: boolean?,
|
|
49
|
+
Priority: number?,
|
|
50
|
+
Sink: boolean?,
|
|
51
|
+
Actions: { InputActionConfig }?,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ActionSignalBundle = {
|
|
55
|
+
Pressed: any,
|
|
56
|
+
Released: any,
|
|
57
|
+
StateChanged: any,
|
|
58
|
+
EnabledChanged: any,
|
|
59
|
+
PreferredBindingChanged: any,
|
|
60
|
+
Destroy: (ActionSignalBundle) -> (),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return InputServiceTypes
|