@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 ADDED
@@ -0,0 +1,724 @@
1
+ # InputService
2
+
3
+ A game-agnostic Nevermore package for Roblox's Input Action System.
4
+
5
+ InputService creates and edits real `InputContext`, `InputAction`, and
6
+ `InputBinding` instances in `ReplicatedStorage.Inputs`. The server owns shared
7
+ defaults, clients listen to actions and make local presentation changes, and no
8
+ client input is treated as server authority by this package.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ pnpm add @hexium-softworks/inputservice
14
+ ```
15
+
16
+ This package expects a Roblox runtime that can create `InputContext`,
17
+ `InputAction`, and `InputBinding` instances.
18
+
19
+ ## Model
20
+
21
+ InputService mirrors Roblox's Input Action System hierarchy:
22
+
23
+ ```txt
24
+ ReplicatedStorage
25
+ Inputs
26
+ PlayContext
27
+ Sprint
28
+ Keyboard
29
+ Gamepad
30
+ Jump
31
+ Keyboard
32
+ MenuContext
33
+ Confirm
34
+ Back
35
+ ```
36
+
37
+ - `InputContext` groups actions and controls priority, sinking, and enablement.
38
+ - `InputAction` represents something the player can do, such as sprint, jump,
39
+ confirm, or open inventory.
40
+ - `InputBinding` describes how an action is triggered, such as a key, UI button,
41
+ directional set, pointer index, or scriptable binding.
42
+
43
+ Server code should create shared gameplay defaults. Client code should connect to
44
+ signals, enable or disable local presentation contexts, add local-only bindings,
45
+ and fire scriptable bindings for UI or custom local input.
46
+
47
+ ## Server Usage
48
+
49
+ Register one context:
50
+
51
+ ```lua
52
+ local InputService = require("InputService")
53
+
54
+ local inputService = serviceBag:GetService(InputService)
55
+
56
+ inputService:RegisterContext({
57
+ Name = "PlayContext",
58
+ Enabled = true,
59
+ Priority = 2000,
60
+ Sink = true,
61
+ Actions = {
62
+ {
63
+ Name = "Sprint",
64
+ DisplayName = "Sprint",
65
+ Type = Enum.InputActionType.Bool,
66
+ Bindings = {
67
+ {
68
+ Name = "Keyboard",
69
+ KeyCode = Enum.KeyCode.LeftShift,
70
+ DisplayName = "Left Shift",
71
+ },
72
+ {
73
+ Name = "Gamepad",
74
+ KeyCode = Enum.KeyCode.ButtonL3,
75
+ DisplayName = "L3",
76
+ },
77
+ },
78
+ },
79
+ },
80
+ })
81
+ ```
82
+
83
+ Register multiple contexts at startup:
84
+
85
+ ```lua
86
+ inputService:RegisterContexts({
87
+ {
88
+ Name = "Gameplay",
89
+ Enabled = true,
90
+ Priority = 2000,
91
+ Sink = false,
92
+ Actions = {
93
+ {
94
+ Name = "Sprint",
95
+ DisplayName = "Sprint",
96
+ Type = Enum.InputActionType.Bool,
97
+ Bindings = {
98
+ {
99
+ Name = "Keyboard",
100
+ KeyCode = Enum.KeyCode.LeftShift,
101
+ },
102
+ {
103
+ Name = "Gamepad",
104
+ KeyCode = Enum.KeyCode.ButtonL3,
105
+ },
106
+ },
107
+ },
108
+ {
109
+ Name = "Jump",
110
+ DisplayName = "Jump",
111
+ Type = Enum.InputActionType.Bool,
112
+ Bindings = {
113
+ {
114
+ Name = "Keyboard",
115
+ KeyCode = Enum.KeyCode.Space,
116
+ },
117
+ {
118
+ Name = "Gamepad",
119
+ KeyCode = Enum.KeyCode.ButtonA,
120
+ },
121
+ },
122
+ },
123
+ {
124
+ Name = "Move",
125
+ DisplayName = "Move",
126
+ Type = Enum.InputActionType.Direction2D,
127
+ Bindings = {
128
+ {
129
+ Name = "Wasd",
130
+ Up = Enum.KeyCode.W,
131
+ Down = Enum.KeyCode.S,
132
+ Left = Enum.KeyCode.A,
133
+ Right = Enum.KeyCode.D,
134
+ Vector2Scale = Vector2.new(1, 1),
135
+ ClampMagnitudeToOne = true,
136
+ },
137
+ },
138
+ },
139
+ },
140
+ },
141
+ {
142
+ Name = "Menu",
143
+ Enabled = false,
144
+ Priority = 3000,
145
+ Sink = true,
146
+ Actions = {
147
+ {
148
+ Name = "Confirm",
149
+ DisplayName = "Confirm",
150
+ Type = Enum.InputActionType.Bool,
151
+ Bindings = {
152
+ {
153
+ Name = "Keyboard",
154
+ KeyCode = Enum.KeyCode.Return,
155
+ },
156
+ {
157
+ Name = "Gamepad",
158
+ KeyCode = Enum.KeyCode.ButtonA,
159
+ },
160
+ },
161
+ },
162
+ {
163
+ Name = "Back",
164
+ DisplayName = "Back",
165
+ Type = Enum.InputActionType.Bool,
166
+ Bindings = {
167
+ {
168
+ Name = "Keyboard",
169
+ KeyCode = Enum.KeyCode.Escape,
170
+ },
171
+ {
172
+ Name = "Gamepad",
173
+ KeyCode = Enum.KeyCode.ButtonB,
174
+ },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ {
180
+ Name = "Vehicle",
181
+ Enabled = false,
182
+ Priority = 2500,
183
+ Sink = true,
184
+ Actions = {
185
+ {
186
+ Name = "Throttle",
187
+ DisplayName = "Throttle",
188
+ Type = Enum.InputActionType.Direction1D,
189
+ Bindings = {
190
+ {
191
+ Name = "Keyboard",
192
+ Up = Enum.KeyCode.W,
193
+ Down = Enum.KeyCode.S,
194
+ Scale = 1,
195
+ },
196
+ },
197
+ },
198
+ },
199
+ },
200
+ })
201
+ ```
202
+
203
+ Update inputs later without rebuilding the whole tree:
204
+
205
+ ```lua
206
+ inputService:ConfigureAction("Gameplay", {
207
+ Name = "Interact",
208
+ DisplayName = "Interact",
209
+ Type = Enum.InputActionType.Bool,
210
+ Bindings = {
211
+ {
212
+ Name = "Keyboard",
213
+ KeyCode = Enum.KeyCode.E,
214
+ },
215
+ },
216
+ })
217
+
218
+ inputService:ConfigureBinding("Gameplay", "Jump", {
219
+ Name = "Keyboard",
220
+ KeyCode = Enum.KeyCode.Space,
221
+ DisplayName = "Space",
222
+ })
223
+ ```
224
+
225
+ Switch active modes by toggling contexts:
226
+
227
+ ```lua
228
+ local function setMenuOpen(isOpen)
229
+ inputService:SetContextEnabled("Gameplay", not isOpen)
230
+ inputService:SetContextEnabled("Menu", isOpen)
231
+ end
232
+
233
+ local function setInVehicle(isInVehicle)
234
+ inputService:SetContextEnabled("Vehicle", isInVehicle)
235
+ end
236
+ ```
237
+
238
+ Remove objects when a feature is unloaded:
239
+
240
+ ```lua
241
+ inputService:RemoveBinding("Gameplay", "Jump", "Keyboard")
242
+ inputService:RemoveAction("Gameplay", "Interact")
243
+ inputService:RemoveContext("Vehicle")
244
+ ```
245
+
246
+ ## Client Usage
247
+
248
+ Read shared contexts and bind to action signals:
249
+
250
+ ```lua
251
+ local InputServiceClient = require("InputServiceClient")
252
+
253
+ local inputServiceClient = serviceBag:GetService(InputServiceClient)
254
+
255
+ inputServiceClient:WaitForContext("Gameplay", 10)
256
+
257
+ local sprintPressed = inputServiceClient:BindPressed("Gameplay", "Sprint", function()
258
+ print("Sprint pressed")
259
+ end)
260
+
261
+ local sprintReleased = inputServiceClient:BindReleased("Gameplay", "Sprint", function()
262
+ print("Sprint released")
263
+ end)
264
+
265
+ local moveChanged = inputServiceClient:BindStateChanged("Gameplay", "Move", function(value)
266
+ print("Move changed", value)
267
+ end)
268
+ ```
269
+
270
+ `BindPressed`, `BindReleased`, and `BindStateChanged` return connections. Store
271
+ and disconnect them with your maid/janitor/cleanup pattern:
272
+
273
+ ```lua
274
+ maid:GiveTask(sprintPressed)
275
+ maid:GiveTask(sprintReleased)
276
+ maid:GiveTask(moveChanged)
277
+ ```
278
+
279
+ You can also access the full signal bundle:
280
+
281
+ ```lua
282
+ local signals = inputServiceClient:GetActionSignals("Gameplay", "Jump")
283
+
284
+ maid:GiveTask(signals.EnabledChanged:Connect(function(enabled)
285
+ print("Jump enabled:", enabled)
286
+ end))
287
+
288
+ maid:GiveTask(signals.PreferredBindingChanged:Connect(function(binding)
289
+ print("Preferred jump binding:", binding)
290
+ end))
291
+ ```
292
+
293
+ Toggle local action state:
294
+
295
+ ```lua
296
+ inputServiceClient:SetActionEnabled("Gameplay", "Sprint", false)
297
+ inputServiceClient:SetContextEnabled("Menu", true)
298
+ ```
299
+
300
+ ## Local Contexts And UI Bindings
301
+
302
+ Clients can define local-only contexts. These are useful for UI, tutorials,
303
+ debug tools, accessibility overlays, and rebinding screens. They do not send any
304
+ claims or config changes to the server through this package.
305
+
306
+ ```lua
307
+ local closeButton = playerGui.Inventory.CloseButton
308
+
309
+ inputServiceClient:DefineLocalContext({
310
+ Name = "InventoryUi",
311
+ Enabled = false,
312
+ Priority = 4000,
313
+ Sink = true,
314
+ Actions = {
315
+ {
316
+ Name = "Close",
317
+ DisplayName = "Close",
318
+ Type = Enum.InputActionType.Bool,
319
+ Bindings = {
320
+ {
321
+ Name = "Keyboard",
322
+ KeyCode = Enum.KeyCode.Escape,
323
+ },
324
+ {
325
+ Name = "Button",
326
+ UIButton = closeButton,
327
+ DisplayName = "Close",
328
+ },
329
+ },
330
+ },
331
+ },
332
+ })
333
+
334
+ inputServiceClient:BindPressed("InventoryUi", "Close", function()
335
+ inputServiceClient:SetContextEnabled("InventoryUi", false)
336
+ end)
337
+ ```
338
+
339
+ Configure a local binding at runtime:
340
+
341
+ ```lua
342
+ inputServiceClient:ConfigureBinding("InventoryUi", "Close", {
343
+ Name = "Gamepad",
344
+ KeyCode = Enum.KeyCode.ButtonB,
345
+ DisplayName = "Back",
346
+ })
347
+ ```
348
+
349
+ Fire a binding from custom client code when the underlying `InputBinding` type in
350
+ your Roblox runtime supports `:Fire()`:
351
+
352
+ ```lua
353
+ inputServiceClient:ConfigureAction("InventoryUi", {
354
+ Name = "Close",
355
+ Type = Enum.InputActionType.Bool,
356
+ Bindings = {
357
+ {
358
+ Name = "Script",
359
+ Type = Enum.InputBindingType.Scriptable,
360
+ },
361
+ },
362
+ })
363
+
364
+ inputServiceClient:FireBinding("InventoryUi", "Close", "Script", true)
365
+ inputServiceClient:FireBinding("InventoryUi", "Close", "Script", false)
366
+ ```
367
+
368
+ ## Blend And Nevermore UI Patterns
369
+
370
+ InputService does not depend on Blend, Rx, Binder, or BaseObject, but it fits
371
+ well with those Nevermore patterns:
372
+
373
+ - Use a service to define the shared input tree and expose game-specific state.
374
+ - Use `BaseObject` or a pane/controller object to own UI and input connections.
375
+ - Use `Maid` for every connection, mounted Blend tree, and temporary tool input.
376
+ - Use `Blend.State` for the current list of visible input hints.
377
+ - Use `PreferredBindingChanged` to update labels when the player's device changes.
378
+ - Use a short-lived maid for tool-specific actions so unequipping the tool removes
379
+ its input context and hint rows at the same time.
380
+
381
+ The pattern is to keep actions in the input tree, then render a separate list of
382
+ human-readable rows. General actions, such as `F` to interact, stay in the list
383
+ all the time. Tool actions are pushed while the tool is equipped and cleaned up
384
+ when it is unequipped.
385
+
386
+ ```lua
387
+ local Blend = require("Blend")
388
+ local Maid = require("Maid")
389
+
390
+ local InputHintPane = {}
391
+ InputHintPane.ClassName = "InputHintPane"
392
+ InputHintPane.__index = InputHintPane
393
+
394
+ function InputHintPane.new(inputServiceClient, playerGui)
395
+ local self = setmetatable({}, InputHintPane)
396
+
397
+ self._maid = Maid.new()
398
+ self._inputServiceClient = inputServiceClient
399
+ self._rows = Blend.State({})
400
+
401
+ self._maid:GiveTask(self:_render(playerGui):Subscribe())
402
+ self:_setAlwaysAvailableRows()
403
+
404
+ return self
405
+ end
406
+
407
+ function InputHintPane:_render(playerGui)
408
+ return Blend.New "ScreenGui" {
409
+ Name = "InputHints",
410
+ Parent = playerGui,
411
+ ResetOnSpawn = false,
412
+
413
+ Blend.New "Frame" {
414
+ AnchorPoint = Vector2.new(1, 1),
415
+ Position = UDim2.fromScale(0.98, 0.96),
416
+ Size = UDim2.fromOffset(280, 160),
417
+ BackgroundTransparency = 1,
418
+
419
+ Blend.New "UIListLayout" {
420
+ Padding = UDim.new(0, 6),
421
+ SortOrder = Enum.SortOrder.LayoutOrder,
422
+ },
423
+
424
+ Blend.ComputedPairs(self._rows, function(_, row)
425
+ return Blend.New "TextLabel" {
426
+ Size = UDim2.new(1, 0, 0, 28),
427
+ BackgroundTransparency = 0.25,
428
+ TextXAlignment = Enum.TextXAlignment.Left,
429
+ Text = string.format("[%s] %s", row.BindingText, row.DisplayName),
430
+ LayoutOrder = row.LayoutOrder,
431
+ }
432
+ end),
433
+ },
434
+ }
435
+ end
436
+
437
+ function InputHintPane:_setRows(rows)
438
+ self._rows.Value = rows
439
+ end
440
+
441
+ function InputHintPane:_setAlwaysAvailableRows()
442
+ self:_setRows({
443
+ {
444
+ ContextName = "Gameplay",
445
+ ActionName = "Interact",
446
+ DisplayName = "Interact",
447
+ BindingText = "F",
448
+ LayoutOrder = 100,
449
+ },
450
+ {
451
+ ContextName = "Gameplay",
452
+ ActionName = "Sprint",
453
+ DisplayName = "Sprint",
454
+ BindingText = "LeftShift",
455
+ LayoutOrder = 110,
456
+ },
457
+ })
458
+ end
459
+
460
+ function InputHintPane:Destroy()
461
+ self._maid:DoCleaning()
462
+ end
463
+
464
+ return InputHintPane
465
+ ```
466
+
467
+ For real UI, avoid hard-coding binding text forever. Start with a fallback, then
468
+ listen to each action's `PreferredBindingChanged` signal and rewrite that row
469
+ when Roblox chooses a better keyboard, gamepad, or touch binding for the player.
470
+
471
+ ```lua
472
+ local function getBindingText(binding)
473
+ if not binding then
474
+ return "?"
475
+ end
476
+
477
+ if binding.DisplayName ~= "" then
478
+ return binding.DisplayName
479
+ end
480
+
481
+ if binding.KeyCode ~= Enum.KeyCode.Unknown then
482
+ return binding.KeyCode.Name
483
+ end
484
+
485
+ if binding.UIButton then
486
+ return binding.UIButton.Name
487
+ end
488
+
489
+ return binding.Name
490
+ end
491
+
492
+ function InputHintPane:_watchPreferredBinding(row)
493
+ local signals = self._inputServiceClient:GetActionSignals(row.ContextName, row.ActionName)
494
+
495
+ local function update(binding)
496
+ local nextRows = table.clone(self._rows.Value)
497
+
498
+ for index, existing in nextRows do
499
+ if existing.ContextName == row.ContextName and existing.ActionName == row.ActionName then
500
+ local nextRow = table.clone(existing)
501
+ nextRow.BindingText = getBindingText(binding)
502
+ nextRows[index] = nextRow
503
+ break
504
+ end
505
+ end
506
+
507
+ self._rows.Value = nextRows
508
+ end
509
+
510
+ local action = self._inputServiceClient:GetAction(row.ContextName, row.ActionName)
511
+ if action then
512
+ update(action.PreferredBinding)
513
+ end
514
+
515
+ return signals.PreferredBindingChanged:Connect(update)
516
+ end
517
+ ```
518
+
519
+ Tool-specific input works best as a lifetime-scoped layer. When the user equips a
520
+ tool, define or enable a local context for the tool, add its rows to the hint
521
+ list, and store all of that work in a tool maid. When the tool is unequipped, the
522
+ maid cleans the UI rows and disables or removes the tool context.
523
+
524
+ ```lua
525
+ function InputHintPane:SetEquippedTool(tool)
526
+ if self._toolMaid then
527
+ self._toolMaid:DoCleaning()
528
+ end
529
+
530
+ self._toolMaid = Maid.new()
531
+ self._maid._toolMaid = self._toolMaid
532
+
533
+ if not tool then
534
+ self:_setAlwaysAvailableRows()
535
+ return
536
+ end
537
+
538
+ local contextName = "Tool:" .. tool.Name
539
+ local toolRows = {
540
+ {
541
+ ContextName = contextName,
542
+ ActionName = "Primary",
543
+ DisplayName = tool.Name .. " Primary",
544
+ BindingText = "MouseLeftButton",
545
+ LayoutOrder = 200,
546
+ },
547
+ {
548
+ ContextName = contextName,
549
+ ActionName = "Reload",
550
+ DisplayName = "Reload",
551
+ BindingText = "R",
552
+ LayoutOrder = 210,
553
+ },
554
+ }
555
+
556
+ self._inputServiceClient:DefineLocalContext({
557
+ Name = contextName,
558
+ Enabled = true,
559
+ Priority = 2600,
560
+ Sink = false,
561
+ Actions = {
562
+ {
563
+ Name = "Primary",
564
+ DisplayName = tool.Name .. " Primary",
565
+ Type = Enum.InputActionType.Bool,
566
+ Bindings = {
567
+ {
568
+ Name = "Mouse",
569
+ KeyCode = Enum.KeyCode.MouseLeftButton,
570
+ },
571
+ },
572
+ },
573
+ {
574
+ Name = "Reload",
575
+ DisplayName = "Reload",
576
+ Type = Enum.InputActionType.Bool,
577
+ Bindings = {
578
+ {
579
+ Name = "Keyboard",
580
+ KeyCode = Enum.KeyCode.R,
581
+ },
582
+ },
583
+ },
584
+ },
585
+ })
586
+
587
+ local rows = table.clone(self._rows.Value)
588
+ for _, row in toolRows do
589
+ table.insert(rows, row)
590
+ self._toolMaid:GiveTask(self:_watchPreferredBinding(row))
591
+ end
592
+
593
+ self:_setRows(rows)
594
+
595
+ self._toolMaid:GiveTask(function()
596
+ self._inputServiceClient:SetContextEnabled(contextName, false)
597
+ self:_setAlwaysAvailableRows()
598
+ end)
599
+ end
600
+ ```
601
+
602
+ In a full Nevermore game, the tool layer usually comes from a service or Binder
603
+ instead of from the pane directly:
604
+
605
+ ```lua
606
+ function ToolInputHintService:Start()
607
+ local inputHintPane = InputHintPane.new(
608
+ self._serviceBag:GetService(InputServiceClient),
609
+ Players.LocalPlayer:WaitForChild("PlayerGui")
610
+ )
611
+
612
+ self._maid:GiveTask(inputHintPane)
613
+
614
+ self._maid:GiveTask(self._equippedTool.Changed:Connect(function(tool)
615
+ inputHintPane:SetEquippedTool(tool)
616
+ end))
617
+ end
618
+ ```
619
+
620
+ That keeps the responsibilities clean: `InputServiceClient` owns input instances,
621
+ the tool service decides which tool is active, and the Blend pane only renders
622
+ the current rows. The same shape works for ability bars, contextual prompts,
623
+ vehicle controls, build-mode hotkeys, and accessibility overlays.
624
+
625
+ ## Config Reference
626
+
627
+ Every config requires a non-empty `Name` with no control characters or `/`.
628
+ Unknown fields are rejected so typos fail early.
629
+
630
+ Context fields:
631
+
632
+ | Field | Type | Notes |
633
+ | --- | --- | --- |
634
+ | `Name` | `string` | Required. Used as the `InputContext.Name`. |
635
+ | `Enabled` | `boolean?` | Sets whether the context is active. |
636
+ | `Priority` | `number?` | Higher-priority contexts can win over lower-priority contexts. |
637
+ | `Sink` | `boolean?` | Controls whether the context sinks input. |
638
+ | `Actions` | `{ InputActionConfig }?` | Actions to create or update under this context. |
639
+
640
+ Action fields:
641
+
642
+ | Field | Type | Notes |
643
+ | --- | --- | --- |
644
+ | `Name` | `string` | Required. Used as the `InputAction.Name`. |
645
+ | `DisplayName` | `string?` | Human-readable action name. |
646
+ | `Enabled` | `boolean?` | Sets whether the action is active. |
647
+ | `Type` | `Enum.InputActionType?` | The action value type, such as `Bool`, `Direction1D`, `Direction2D`, `Direction3D`, or `ViewportPosition`. |
648
+ | `Bindings` | `{ InputBindingConfig }?` | Bindings to create or update under this action. |
649
+
650
+ Binding fields:
651
+
652
+ | Field | Type | Notes |
653
+ | --- | --- | --- |
654
+ | `Name` | `string` | Required. Used as the `InputBinding.Name`. |
655
+ | `Type` | `Enum.InputBindingType?` | Binding type for runtimes that require it. |
656
+ | `KeyCode` | `Enum.KeyCode?` | Keyboard, mouse, or gamepad key/button binding. |
657
+ | `UIButton` | `GuiButton?` | UI button binding. |
658
+ | `UIModifier` | `GuiButton?` | UI modifier button. |
659
+ | `PrimaryModifier` | `Enum.KeyCode?` | Primary keyboard/gamepad modifier. |
660
+ | `SecondaryModifier` | `Enum.KeyCode?` | Secondary keyboard/gamepad modifier. |
661
+ | `DisplayName` | `string?` | Human-readable binding name. |
662
+ | `DisplayImage` | `Content?` | Binding icon/image content. |
663
+ | `PressedThreshold` | `number?` | Pressed threshold. Must be greater than or equal to `ReleasedThreshold` when both are set. |
664
+ | `ReleasedThreshold` | `number?` | Released threshold. |
665
+ | `ResponseCurve` | `number?` | Response curve value. |
666
+ | `Scale` | `number?` | Scalar output scale. |
667
+ | `Vector2Scale` | `Vector2?` | Vector2 output scale. |
668
+ | `Vector3Scale` | `Vector3?` | Vector3 output scale. |
669
+ | `PointerIndex` | `number?` | Pointer index for pointer-style bindings. |
670
+ | `ClampMagnitudeToOne` | `boolean?` | Clamp vector magnitude to one. |
671
+ | `Up` | `Enum.KeyCode?` | Up or positive direction key. |
672
+ | `Down` | `Enum.KeyCode?` | Down or negative direction key. |
673
+ | `Left` | `Enum.KeyCode?` | Left direction key. |
674
+ | `Right` | `Enum.KeyCode?` | Right direction key. |
675
+ | `Forward` | `Enum.KeyCode?` | Forward direction key for 3D actions. |
676
+ | `Backward` | `Enum.KeyCode?` | Backward direction key for 3D actions. |
677
+
678
+ ## API Reference
679
+
680
+ Server `InputService`:
681
+
682
+ | Method | Description |
683
+ | --- | --- |
684
+ | `GetRootFolder()` | Returns `ReplicatedStorage.Inputs`. |
685
+ | `RegisterContext(config)` | Creates or updates one shared context. |
686
+ | `RegisterContexts(configs)` | Creates or updates many shared contexts. |
687
+ | `GetContext(contextName)` | Returns an `InputContext?`. |
688
+ | `GetAction(contextName, actionName)` | Returns an `InputAction?`. |
689
+ | `GetBinding(contextName, actionName, bindingName)` | Returns an `InputBinding?`. |
690
+ | `ConfigureAction(contextName, actionConfig)` | Creates or updates one action under an existing context. |
691
+ | `ConfigureBinding(contextName, actionName, bindingConfig)` | Creates or updates one binding under an existing action. |
692
+ | `SetContextEnabled(contextName, enabled)` | Enables or disables a context. |
693
+ | `SetActionEnabled(contextName, actionName, enabled)` | Enables or disables an action. |
694
+ | `RemoveContext(contextName)` | Removes a context and returns whether one was removed. |
695
+ | `RemoveAction(contextName, actionName)` | Removes an action and returns whether one was removed. |
696
+ | `RemoveBinding(contextName, actionName, bindingName)` | Removes a binding and returns whether one was removed. |
697
+
698
+ Client `InputServiceClient`:
699
+
700
+ | Method | Description |
701
+ | --- | --- |
702
+ | `GetRootFolder()` | Returns or creates the local root folder. |
703
+ | `GetContext(contextName)` | Returns an `InputContext?`. |
704
+ | `WaitForContext(contextName, timeoutSeconds?)` | Waits for a shared context to replicate. |
705
+ | `DefineLocalContext(config)` | Creates or updates a local-only context. |
706
+ | `ConfigureAction(contextName, actionConfig)` | Creates or updates an action under an existing context. |
707
+ | `ConfigureBinding(contextName, actionName, bindingConfig)` | Creates or updates a binding under an existing action. |
708
+ | `GetAction(contextName, actionName)` | Returns an `InputAction?`. |
709
+ | `GetBinding(contextName, actionName, bindingName)` | Returns an `InputBinding?`. |
710
+ | `GetActionSignals(contextName, actionName)` | Returns cached pressed, released, state, enabled, and preferred-binding signals. |
711
+ | `BindPressed(contextName, actionName, callback)` | Connects to an action's pressed signal. |
712
+ | `BindReleased(contextName, actionName, callback)` | Connects to an action's released signal. |
713
+ | `BindStateChanged(contextName, actionName, callback)` | Connects to an action's state changed signal. |
714
+ | `SetContextEnabled(contextName, enabled)` | Enables or disables a context locally. |
715
+ | `SetActionEnabled(contextName, actionName, enabled)` | Enables or disables an action locally. |
716
+ | `FireBinding(contextName, actionName, bindingName, state)` | Fires a scriptable binding with `boolean`, `number`, `Vector2`, or `Vector3` state. |
717
+
718
+ ## Security
719
+
720
+ - No remotes are created for input claims, rebinding, or context changes.
721
+ - Server APIs accept configs only from server code.
722
+ - Client-side input signals should be treated as intent only.
723
+ - Client-created contexts and bindings are local convenience only.
724
+ - Games must validate gameplay effects on the server.