@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
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hexium-softworks/inputservice",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Game-agnostic IAS wrapper",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Roblox",
|
|
7
|
+
"Nevermore",
|
|
8
|
+
"Luau",
|
|
9
|
+
"Input"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"default.project.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"preinstall": "npx only-allow pnpm",
|
|
19
|
+
"build": "echo \"No build step required; publishing Roblox/Luau source files.\""
|
|
20
|
+
},
|
|
21
|
+
"contributors": [
|
|
22
|
+
"HexedEthan"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@quenty/loader": "10.11.0",
|
|
26
|
+
"@quenty/maid": "^3.12.0",
|
|
27
|
+
"@quenty/servicebag": "^11.21.0",
|
|
28
|
+
"@quenty/signal": "^7.13.2",
|
|
29
|
+
"@quenty/table": "^3.9.2"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@quenty/nevermore-test-runner": "1.4.0",
|
|
33
|
+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Client-side helpers for Roblox Input Action System contexts.
|
|
4
|
+
|
|
5
|
+
Client mutations are local convenience only. They are non-authoritative and
|
|
6
|
+
are not replicated to the server through this package.
|
|
7
|
+
|
|
8
|
+
@class InputServiceClient
|
|
9
|
+
]=]
|
|
10
|
+
|
|
11
|
+
local require = require(script.Parent.loader).load(script)
|
|
12
|
+
|
|
13
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
14
|
+
|
|
15
|
+
local Maid = require("Maid")
|
|
16
|
+
local ServiceBag = require("ServiceBag")
|
|
17
|
+
local Signal = require("Signal")
|
|
18
|
+
|
|
19
|
+
local InputServiceConstants = require("InputServiceConstants")
|
|
20
|
+
local InputServiceInstanceUtils = require("InputServiceInstanceUtils")
|
|
21
|
+
local InputServiceTypes = require("InputServiceTypes")
|
|
22
|
+
|
|
23
|
+
type ServiceBag = ServiceBag.ServiceBag
|
|
24
|
+
type InputContextConfig = InputServiceTypes.InputContextConfig
|
|
25
|
+
type InputActionConfig = InputServiceTypes.InputActionConfig
|
|
26
|
+
type InputBindingConfig = InputServiceTypes.InputBindingConfig
|
|
27
|
+
type InputActionState = InputServiceTypes.InputActionState
|
|
28
|
+
type ActionSignalBundle = InputServiceTypes.ActionSignalBundle
|
|
29
|
+
|
|
30
|
+
local InputServiceClient = {}
|
|
31
|
+
InputServiceClient.ServiceName = "InputServiceClient"
|
|
32
|
+
|
|
33
|
+
export type InputServiceClient = typeof(setmetatable(
|
|
34
|
+
{} :: {
|
|
35
|
+
_serviceBag: ServiceBag,
|
|
36
|
+
_maid: Maid.Maid,
|
|
37
|
+
_rootFolder: Folder?,
|
|
38
|
+
_actionSignals: { [InputAction]: ActionSignalBundle },
|
|
39
|
+
},
|
|
40
|
+
{} :: typeof({ __index = InputServiceClient })
|
|
41
|
+
))
|
|
42
|
+
|
|
43
|
+
local function findRootFolder(): Folder?
|
|
44
|
+
local child = ReplicatedStorage:FindFirstChild(InputServiceConstants.ROOT_FOLDER_NAME)
|
|
45
|
+
if child == nil then
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
assert(
|
|
50
|
+
child:IsA(InputServiceConstants.ROOT_FOLDER_CLASS_NAME),
|
|
51
|
+
string.format("%s must be a %s", child:GetFullName(), InputServiceConstants.ROOT_FOLDER_CLASS_NAME)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return child :: Folder
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
local function createActionSignalBundle(action: InputAction): ActionSignalBundle
|
|
58
|
+
local maid = Maid.new()
|
|
59
|
+
|
|
60
|
+
local bundle = {
|
|
61
|
+
Pressed = Signal.new(),
|
|
62
|
+
Released = Signal.new(),
|
|
63
|
+
StateChanged = Signal.new(),
|
|
64
|
+
EnabledChanged = Signal.new(),
|
|
65
|
+
PreferredBindingChanged = Signal.new(),
|
|
66
|
+
} :: any
|
|
67
|
+
|
|
68
|
+
maid:GiveTask(bundle.Pressed)
|
|
69
|
+
maid:GiveTask(bundle.Released)
|
|
70
|
+
maid:GiveTask(bundle.StateChanged)
|
|
71
|
+
maid:GiveTask(bundle.EnabledChanged)
|
|
72
|
+
maid:GiveTask(bundle.PreferredBindingChanged)
|
|
73
|
+
|
|
74
|
+
maid:GiveTask(action.Pressed:Connect(function()
|
|
75
|
+
bundle.Pressed:Fire()
|
|
76
|
+
end))
|
|
77
|
+
|
|
78
|
+
maid:GiveTask(action.Released:Connect(function()
|
|
79
|
+
bundle.Released:Fire()
|
|
80
|
+
end))
|
|
81
|
+
|
|
82
|
+
maid:GiveTask(action.StateChanged:Connect(function(value: any)
|
|
83
|
+
bundle.StateChanged:Fire(value)
|
|
84
|
+
end))
|
|
85
|
+
|
|
86
|
+
maid:GiveTask(action:GetPropertyChangedSignal("Enabled"):Connect(function()
|
|
87
|
+
bundle.EnabledChanged:Fire(action.Enabled)
|
|
88
|
+
end))
|
|
89
|
+
|
|
90
|
+
maid:GiveTask(action:GetPropertyChangedSignal("PreferredBinding"):Connect(function()
|
|
91
|
+
bundle.PreferredBindingChanged:Fire(action.PreferredBinding)
|
|
92
|
+
end))
|
|
93
|
+
|
|
94
|
+
function bundle:Destroy()
|
|
95
|
+
maid:DoCleaning()
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return bundle :: ActionSignalBundle
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
function InputServiceClient.Init(self: InputServiceClient, serviceBag: ServiceBag): ()
|
|
102
|
+
assert(not self._serviceBag, "Already initialized")
|
|
103
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
104
|
+
self._maid = Maid.new()
|
|
105
|
+
self._rootFolder = findRootFolder()
|
|
106
|
+
self._actionSignals = {}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
function InputServiceClient.Start(_self: InputServiceClient): () end
|
|
110
|
+
|
|
111
|
+
function InputServiceClient._getRootFolder(self: InputServiceClient, createIfMissing: boolean): Folder?
|
|
112
|
+
local rootFolder = self._rootFolder
|
|
113
|
+
if rootFolder and rootFolder.Parent then
|
|
114
|
+
return rootFolder
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
rootFolder = findRootFolder()
|
|
118
|
+
if rootFolder then
|
|
119
|
+
self._rootFolder = rootFolder
|
|
120
|
+
return rootFolder
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if not createIfMissing then
|
|
124
|
+
return nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
self._rootFolder = InputServiceInstanceUtils.getOrCreateRootFolder(ReplicatedStorage)
|
|
128
|
+
return self._rootFolder
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
function InputServiceClient.GetRootFolder(self: InputServiceClient): Folder
|
|
132
|
+
local rootFolder = self:_getRootFolder(true)
|
|
133
|
+
assert(rootFolder, "Input root folder is not available")
|
|
134
|
+
|
|
135
|
+
return rootFolder
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
function InputServiceClient.GetContext(self: InputServiceClient, contextName: string): InputContext?
|
|
139
|
+
assert(typeof(contextName) == "string", "Context name must be a string")
|
|
140
|
+
|
|
141
|
+
local rootFolder = self:_getRootFolder(false)
|
|
142
|
+
if not rootFolder then
|
|
143
|
+
return nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return InputServiceInstanceUtils.getContext(rootFolder, contextName)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
function InputServiceClient.WaitForContext(
|
|
150
|
+
self: InputServiceClient,
|
|
151
|
+
contextName: string,
|
|
152
|
+
timeoutSeconds: number?
|
|
153
|
+
): InputContext?
|
|
154
|
+
assert(typeof(contextName) == "string", "Context name must be a string")
|
|
155
|
+
if timeoutSeconds ~= nil then
|
|
156
|
+
assert(typeof(timeoutSeconds) == "number", "Timeout must be a number")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
local context = self:GetContext(contextName)
|
|
160
|
+
if context then
|
|
161
|
+
return context
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
local rootFolder = self:_getRootFolder(false)
|
|
165
|
+
if not rootFolder then
|
|
166
|
+
local child = ReplicatedStorage:WaitForChild(InputServiceConstants.ROOT_FOLDER_NAME, timeoutSeconds)
|
|
167
|
+
if child == nil then
|
|
168
|
+
return nil
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
assert(
|
|
172
|
+
child:IsA(InputServiceConstants.ROOT_FOLDER_CLASS_NAME),
|
|
173
|
+
string.format("%s must be a %s", child:GetFullName(), InputServiceConstants.ROOT_FOLDER_CLASS_NAME)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
rootFolder = child :: Folder
|
|
177
|
+
self._rootFolder = rootFolder
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
local child = rootFolder:WaitForChild(contextName, timeoutSeconds)
|
|
181
|
+
if child == nil then
|
|
182
|
+
return nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
assert(child:IsA("InputContext"), string.format("%s must be an InputContext", child:GetFullName()))
|
|
186
|
+
return child :: InputContext
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
function InputServiceClient.DefineLocalContext(self: InputServiceClient, config: InputContextConfig): InputContext
|
|
190
|
+
return InputServiceInstanceUtils.upsertContext(self:GetRootFolder(), config)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
function InputServiceClient.ConfigureAction(
|
|
194
|
+
self: InputServiceClient,
|
|
195
|
+
contextName: string,
|
|
196
|
+
actionConfig: InputActionConfig
|
|
197
|
+
): InputAction
|
|
198
|
+
local context = self:GetContext(contextName)
|
|
199
|
+
assert(context, string.format("InputContext %q does not exist", contextName))
|
|
200
|
+
|
|
201
|
+
return InputServiceInstanceUtils.upsertAction(context, actionConfig)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
function InputServiceClient.ConfigureBinding(
|
|
205
|
+
self: InputServiceClient,
|
|
206
|
+
contextName: string,
|
|
207
|
+
actionName: string,
|
|
208
|
+
bindingConfig: InputBindingConfig
|
|
209
|
+
): InputBinding
|
|
210
|
+
local action = self:GetAction(contextName, actionName)
|
|
211
|
+
assert(action, string.format("InputAction %q.%q does not exist", contextName, actionName))
|
|
212
|
+
|
|
213
|
+
return InputServiceInstanceUtils.upsertBinding(action, bindingConfig)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
function InputServiceClient.GetAction(self: InputServiceClient, contextName: string, actionName: string): InputAction?
|
|
217
|
+
assert(typeof(actionName) == "string", "Action name must be a string")
|
|
218
|
+
|
|
219
|
+
local context = self:GetContext(contextName)
|
|
220
|
+
if not context then
|
|
221
|
+
return nil
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
return InputServiceInstanceUtils.getAction(context, actionName)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
function InputServiceClient.GetBinding(
|
|
228
|
+
self: InputServiceClient,
|
|
229
|
+
contextName: string,
|
|
230
|
+
actionName: string,
|
|
231
|
+
bindingName: string
|
|
232
|
+
): InputBinding?
|
|
233
|
+
assert(typeof(bindingName) == "string", "Binding name must be a string")
|
|
234
|
+
|
|
235
|
+
local action = self:GetAction(contextName, actionName)
|
|
236
|
+
if not action then
|
|
237
|
+
return nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
return InputServiceInstanceUtils.getBinding(action, bindingName)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
function InputServiceClient.GetActionSignals(
|
|
244
|
+
self: InputServiceClient,
|
|
245
|
+
contextName: string,
|
|
246
|
+
actionName: string
|
|
247
|
+
): ActionSignalBundle
|
|
248
|
+
local action = self:GetAction(contextName, actionName)
|
|
249
|
+
assert(action, string.format("InputAction %q.%q does not exist", contextName, actionName))
|
|
250
|
+
|
|
251
|
+
local existing = self._actionSignals[action]
|
|
252
|
+
if existing then
|
|
253
|
+
return existing
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
local bundle = createActionSignalBundle(action)
|
|
257
|
+
self._actionSignals[action] = bundle
|
|
258
|
+
self._maid:GiveTask(bundle)
|
|
259
|
+
self._maid:GiveTask(action.Destroying:Connect(function()
|
|
260
|
+
self._actionSignals[action] = nil
|
|
261
|
+
bundle:Destroy()
|
|
262
|
+
end))
|
|
263
|
+
|
|
264
|
+
return bundle
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
function InputServiceClient.BindPressed(
|
|
268
|
+
self: InputServiceClient,
|
|
269
|
+
contextName: string,
|
|
270
|
+
actionName: string,
|
|
271
|
+
callback: () -> ()
|
|
272
|
+
): any
|
|
273
|
+
assert(typeof(callback) == "function", "Callback must be a function")
|
|
274
|
+
|
|
275
|
+
return self:GetActionSignals(contextName, actionName).Pressed:Connect(callback)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
function InputServiceClient.BindReleased(
|
|
279
|
+
self: InputServiceClient,
|
|
280
|
+
contextName: string,
|
|
281
|
+
actionName: string,
|
|
282
|
+
callback: () -> ()
|
|
283
|
+
): any
|
|
284
|
+
assert(typeof(callback) == "function", "Callback must be a function")
|
|
285
|
+
|
|
286
|
+
return self:GetActionSignals(contextName, actionName).Released:Connect(callback)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
function InputServiceClient.BindStateChanged(
|
|
290
|
+
self: InputServiceClient,
|
|
291
|
+
contextName: string,
|
|
292
|
+
actionName: string,
|
|
293
|
+
callback: (InputActionState) -> ()
|
|
294
|
+
): any
|
|
295
|
+
assert(typeof(callback) == "function", "Callback must be a function")
|
|
296
|
+
|
|
297
|
+
return self:GetActionSignals(contextName, actionName).StateChanged:Connect(callback)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
function InputServiceClient.SetContextEnabled(self: InputServiceClient, contextName: string, enabled: boolean): ()
|
|
301
|
+
assert(typeof(enabled) == "boolean", "Enabled must be a boolean")
|
|
302
|
+
|
|
303
|
+
local context = self:GetContext(contextName)
|
|
304
|
+
assert(context, string.format("InputContext %q does not exist", contextName))
|
|
305
|
+
|
|
306
|
+
context.Enabled = enabled
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
function InputServiceClient.SetActionEnabled(
|
|
310
|
+
self: InputServiceClient,
|
|
311
|
+
contextName: string,
|
|
312
|
+
actionName: string,
|
|
313
|
+
enabled: boolean
|
|
314
|
+
): ()
|
|
315
|
+
assert(typeof(enabled) == "boolean", "Enabled must be a boolean")
|
|
316
|
+
|
|
317
|
+
local action = self:GetAction(contextName, actionName)
|
|
318
|
+
assert(action, string.format("InputAction %q.%q does not exist", contextName, actionName))
|
|
319
|
+
|
|
320
|
+
action.Enabled = enabled
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
function InputServiceClient.FireBinding(
|
|
324
|
+
self: InputServiceClient,
|
|
325
|
+
contextName: string,
|
|
326
|
+
actionName: string,
|
|
327
|
+
bindingName: string,
|
|
328
|
+
state: InputActionState
|
|
329
|
+
): ()
|
|
330
|
+
local binding = self:GetBinding(contextName, actionName, bindingName)
|
|
331
|
+
assert(binding, string.format("InputBinding %q.%q.%q does not exist", contextName, actionName, bindingName))
|
|
332
|
+
|
|
333
|
+
local ok, err = pcall(function()
|
|
334
|
+
binding:Fire(state)
|
|
335
|
+
end)
|
|
336
|
+
|
|
337
|
+
assert(
|
|
338
|
+
ok,
|
|
339
|
+
string.format(
|
|
340
|
+
"Failed to fire scriptable InputBinding %q.%q.%q: %s",
|
|
341
|
+
contextName,
|
|
342
|
+
actionName,
|
|
343
|
+
bindingName,
|
|
344
|
+
tostring(err)
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
function InputServiceClient.Destroy(self: InputServiceClient): ()
|
|
350
|
+
self._maid:DoCleaning()
|
|
351
|
+
table.clear(self._actionSignals)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
return InputServiceClient
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class InputServiceClient.spec
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
9
|
+
|
|
10
|
+
local InputServiceClient = require("InputServiceClient")
|
|
11
|
+
local InputServiceConstants = require("InputServiceConstants")
|
|
12
|
+
local Jest = require("Jest")
|
|
13
|
+
|
|
14
|
+
local describe = Jest.Globals.describe
|
|
15
|
+
local expect = Jest.Globals.expect
|
|
16
|
+
local it = Jest.Globals.it
|
|
17
|
+
|
|
18
|
+
local function canCreateInputActionInstances(): boolean
|
|
19
|
+
local ok, instance = pcall(Instance.new, InputServiceConstants.CONTEXT_CLASS_NAME, nil)
|
|
20
|
+
if ok and typeof(instance) == "Instance" then
|
|
21
|
+
instance:Destroy()
|
|
22
|
+
return true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
local function newClient()
|
|
29
|
+
local service = setmetatable({}, { __index = InputServiceClient }) :: any
|
|
30
|
+
service:Init({} :: any)
|
|
31
|
+
return service
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe("InputServiceClient", function()
|
|
35
|
+
it("guards unsupported runtimes", function()
|
|
36
|
+
expect(typeof(canCreateInputActionInstances())).toEqual("boolean")
|
|
37
|
+
end)
|
|
38
|
+
|
|
39
|
+
if canCreateInputActionInstances() then
|
|
40
|
+
it("caches action signals and disconnects them on destroy", function()
|
|
41
|
+
local existingRoot = ReplicatedStorage:FindFirstChild(InputServiceConstants.ROOT_FOLDER_NAME)
|
|
42
|
+
if existingRoot then
|
|
43
|
+
existingRoot:Destroy()
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
local service = newClient()
|
|
47
|
+
service:DefineLocalContext({
|
|
48
|
+
Name = "SpecContext",
|
|
49
|
+
Actions = {
|
|
50
|
+
{ Name = "SpecAction" },
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
local firstSignals = service:GetActionSignals("SpecContext", "SpecAction")
|
|
55
|
+
local secondSignals = service:GetActionSignals("SpecContext", "SpecAction")
|
|
56
|
+
local connection = service:BindPressed("SpecContext", "SpecAction", function() end)
|
|
57
|
+
|
|
58
|
+
expect(firstSignals == secondSignals).toEqual(true)
|
|
59
|
+
expect(connection:IsConnected()).toEqual(true)
|
|
60
|
+
|
|
61
|
+
service:Destroy()
|
|
62
|
+
|
|
63
|
+
expect(connection:IsConnected()).toEqual(false)
|
|
64
|
+
|
|
65
|
+
local root = ReplicatedStorage:FindFirstChild(InputServiceConstants.ROOT_FOLDER_NAME)
|
|
66
|
+
if root then
|
|
67
|
+
root:Destroy()
|
|
68
|
+
end
|
|
69
|
+
end)
|
|
70
|
+
end
|
|
71
|
+
end)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
Server-owned registry for Roblox Input Action System contexts.
|
|
4
|
+
|
|
5
|
+
This service intentionally exposes no remotes. Client-side input state is
|
|
6
|
+
only intent and must never be treated as gameplay authority by the server.
|
|
7
|
+
|
|
8
|
+
@class InputService
|
|
9
|
+
]=]
|
|
10
|
+
|
|
11
|
+
local require = require(script.Parent.loader).load(script)
|
|
12
|
+
|
|
13
|
+
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
14
|
+
|
|
15
|
+
local Maid = require("Maid")
|
|
16
|
+
local ServiceBag = require("ServiceBag")
|
|
17
|
+
|
|
18
|
+
local InputServiceInstanceUtils = require("InputServiceInstanceUtils")
|
|
19
|
+
local InputServiceTypes = require("InputServiceTypes")
|
|
20
|
+
|
|
21
|
+
type ServiceBag = ServiceBag.ServiceBag
|
|
22
|
+
type InputContextConfig = InputServiceTypes.InputContextConfig
|
|
23
|
+
type InputActionConfig = InputServiceTypes.InputActionConfig
|
|
24
|
+
type InputBindingConfig = InputServiceTypes.InputBindingConfig
|
|
25
|
+
|
|
26
|
+
local InputService = {}
|
|
27
|
+
InputService.ServiceName = "InputService"
|
|
28
|
+
|
|
29
|
+
export type InputService = typeof(setmetatable(
|
|
30
|
+
{} :: {
|
|
31
|
+
_serviceBag: ServiceBag,
|
|
32
|
+
_maid: Maid.Maid,
|
|
33
|
+
_rootFolder: Folder,
|
|
34
|
+
},
|
|
35
|
+
{} :: typeof({ __index = InputService })
|
|
36
|
+
))
|
|
37
|
+
|
|
38
|
+
function InputService.Init(self: InputService, serviceBag: ServiceBag): ()
|
|
39
|
+
assert(not self._serviceBag, "Already initialized")
|
|
40
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
41
|
+
self._maid = Maid.new()
|
|
42
|
+
self._rootFolder = InputServiceInstanceUtils.getOrCreateRootFolder(ReplicatedStorage)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
function InputService.Start(_self: InputService): () end
|
|
46
|
+
|
|
47
|
+
--[=[
|
|
48
|
+
Returns the shared input root folder in ReplicatedStorage.
|
|
49
|
+
|
|
50
|
+
@return Folder
|
|
51
|
+
]=]
|
|
52
|
+
function InputService.GetRootFolder(self: InputService): Folder
|
|
53
|
+
return self._rootFolder
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
--[=[
|
|
57
|
+
Registers or updates a server-owned shared input context.
|
|
58
|
+
|
|
59
|
+
@param config InputContextConfig
|
|
60
|
+
@return InputContext
|
|
61
|
+
]=]
|
|
62
|
+
function InputService.RegisterContext(self: InputService, config: InputContextConfig): InputContext
|
|
63
|
+
return InputServiceInstanceUtils.upsertContext(self._rootFolder, config)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
--[=[
|
|
67
|
+
Registers or updates multiple server-owned shared input contexts.
|
|
68
|
+
|
|
69
|
+
@param configs { InputContextConfig }
|
|
70
|
+
]=]
|
|
71
|
+
function InputService.RegisterContexts(self: InputService, configs: { InputContextConfig }): ()
|
|
72
|
+
assert(typeof(configs) == "table", "Configs must be a table")
|
|
73
|
+
|
|
74
|
+
for _, config in configs do
|
|
75
|
+
self:RegisterContext(config)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
function InputService.GetContext(self: InputService, contextName: string): InputContext?
|
|
80
|
+
assert(typeof(contextName) == "string", "Context name must be a string")
|
|
81
|
+
|
|
82
|
+
return InputServiceInstanceUtils.getContext(self._rootFolder, contextName)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
function InputService.GetAction(self: InputService, contextName: string, actionName: string): InputAction?
|
|
86
|
+
assert(typeof(actionName) == "string", "Action name must be a string")
|
|
87
|
+
|
|
88
|
+
local context = self:GetContext(contextName)
|
|
89
|
+
if not context then
|
|
90
|
+
return nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
return InputServiceInstanceUtils.getAction(context, actionName)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
function InputService.GetBinding(
|
|
97
|
+
self: InputService,
|
|
98
|
+
contextName: string,
|
|
99
|
+
actionName: string,
|
|
100
|
+
bindingName: string
|
|
101
|
+
): InputBinding?
|
|
102
|
+
assert(typeof(bindingName) == "string", "Binding name must be a string")
|
|
103
|
+
|
|
104
|
+
local action = self:GetAction(contextName, actionName)
|
|
105
|
+
if not action then
|
|
106
|
+
return nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return InputServiceInstanceUtils.getBinding(action, bindingName)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
function InputService.ConfigureAction(
|
|
113
|
+
self: InputService,
|
|
114
|
+
contextName: string,
|
|
115
|
+
actionConfig: InputActionConfig
|
|
116
|
+
): InputAction
|
|
117
|
+
local context = self:GetContext(contextName)
|
|
118
|
+
assert(context, string.format("InputContext %q does not exist", contextName))
|
|
119
|
+
|
|
120
|
+
return InputServiceInstanceUtils.upsertAction(context, actionConfig)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
function InputService.ConfigureBinding(
|
|
124
|
+
self: InputService,
|
|
125
|
+
contextName: string,
|
|
126
|
+
actionName: string,
|
|
127
|
+
bindingConfig: InputBindingConfig
|
|
128
|
+
): InputBinding
|
|
129
|
+
local action = self:GetAction(contextName, actionName)
|
|
130
|
+
assert(action, string.format("InputAction %q.%q does not exist", contextName, actionName))
|
|
131
|
+
|
|
132
|
+
return InputServiceInstanceUtils.upsertBinding(action, bindingConfig)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
function InputService.SetContextEnabled(self: InputService, contextName: string, enabled: boolean): ()
|
|
136
|
+
assert(typeof(enabled) == "boolean", "Enabled must be a boolean")
|
|
137
|
+
|
|
138
|
+
local context = self:GetContext(contextName)
|
|
139
|
+
assert(context, string.format("InputContext %q does not exist", contextName))
|
|
140
|
+
|
|
141
|
+
context.Enabled = enabled
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
function InputService.SetActionEnabled(self: InputService, contextName: string, actionName: string, enabled: boolean): ()
|
|
145
|
+
assert(typeof(enabled) == "boolean", "Enabled must be a boolean")
|
|
146
|
+
|
|
147
|
+
local action = self:GetAction(contextName, actionName)
|
|
148
|
+
assert(action, string.format("InputAction %q.%q does not exist", contextName, actionName))
|
|
149
|
+
|
|
150
|
+
action.Enabled = enabled
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
function InputService.RemoveContext(self: InputService, contextName: string): boolean
|
|
154
|
+
assert(typeof(contextName) == "string", "Context name must be a string")
|
|
155
|
+
|
|
156
|
+
return InputServiceInstanceUtils.removeContext(self._rootFolder, contextName)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
function InputService.RemoveAction(self: InputService, contextName: string, actionName: string): boolean
|
|
160
|
+
local context = self:GetContext(contextName)
|
|
161
|
+
if not context then
|
|
162
|
+
return false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
return InputServiceInstanceUtils.removeAction(context, actionName)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
function InputService.RemoveBinding(
|
|
169
|
+
self: InputService,
|
|
170
|
+
contextName: string,
|
|
171
|
+
actionName: string,
|
|
172
|
+
bindingName: string
|
|
173
|
+
): boolean
|
|
174
|
+
local action = self:GetAction(contextName, actionName)
|
|
175
|
+
if not action then
|
|
176
|
+
return false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
return InputServiceInstanceUtils.removeBinding(action, bindingName)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
function InputService.Destroy(self: InputService): ()
|
|
183
|
+
self._maid:DoCleaning()
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
return InputService
|