@jgengine/shell 0.18.1 → 0.21.0
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/CHANGELOG.md +504 -7
- package/dist/GameHost.d.ts +12 -1
- package/dist/GameHost.js +14 -1
- package/dist/GamePlayerShell.js +132 -17
- package/dist/Shell3dPresentation.d.ts +5 -11
- package/dist/Shell3dPresentation.js +73 -11
- package/dist/ShellChrome.d.ts +2 -1
- package/dist/ShellChrome.js +22 -6
- package/dist/ShellHudPresentation.d.ts +20 -2
- package/dist/ShellHudPresentation.js +41 -6
- package/dist/audio/AudioComponents.js +6 -2
- package/dist/audio/audioEngine.d.ts +24 -0
- package/dist/audio/audioEngine.js +231 -83
- package/dist/audio/audioWire.d.ts +3 -0
- package/dist/audio/audioWire.js +9 -3
- package/dist/audio/loopParams.d.ts +39 -0
- package/dist/audio/loopParams.js +43 -0
- package/dist/audio/synthEngine.d.ts +7 -1
- package/dist/audio/synthEngine.js +103 -38
- package/dist/camera/GameCameraRig.js +3 -2
- package/dist/camera/GameFirstPersonCamera.d.ts +6 -4
- package/dist/camera/GameFirstPersonCamera.js +68 -27
- package/dist/camera/Viewports.d.ts +34 -0
- package/dist/camera/Viewports.js +123 -0
- package/dist/camera/cameraRigs.js +76 -62
- package/dist/camera/chaseHeading.d.ts +9 -0
- package/dist/camera/chaseHeading.js +10 -0
- package/dist/camera/customMuzzle.d.ts +14 -0
- package/dist/camera/customMuzzle.js +68 -0
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/orbitCameraMath.js +2 -2
- package/dist/camera/rigMath.d.ts +82 -4
- package/dist/camera/rigMath.js +133 -15
- package/dist/camera/rigResolve.d.ts +3 -0
- package/dist/camera/rigResolve.js +11 -0
- package/dist/combat/ProjectileModels.d.ts +31 -0
- package/dist/combat/ProjectileModels.js +70 -0
- package/dist/commandSink.d.ts +1 -2
- package/dist/commandSink.js +3 -2
- package/dist/defineGame.d.ts +19 -3
- package/dist/defineGame.js +39 -7
- package/dist/devtools/DevtoolsOverlay.d.ts +2 -69
- package/dist/devtools/DevtoolsOverlay.js +15 -167
- package/dist/devtools/DevtoolsRuntime.d.ts +7 -0
- package/dist/devtools/DevtoolsRuntime.js +61 -0
- package/dist/devtools/agentBridge.d.ts +6 -0
- package/dist/devtools/agentBridge.js +16 -2
- package/dist/devtools/devtoolsReports.d.ts +68 -0
- package/dist/devtools/devtoolsReports.js +100 -0
- package/dist/devtools/latencyInstrumentation.d.ts +3 -0
- package/dist/devtools/latencyInstrumentation.js +11 -0
- package/dist/devtools/playtestProbe.d.ts +4 -0
- package/dist/devtools/playtestProbe.js +22 -0
- package/dist/diagnostics/FpsCounter.d.ts +17 -0
- package/dist/diagnostics/FpsCounter.js +32 -0
- package/dist/drivers/FrameDriver.js +35 -20
- package/dist/drivers/FrameRateLimiter.d.ts +9 -0
- package/dist/drivers/FrameRateLimiter.js +39 -0
- package/dist/drivers/HudOnlyDriver.d.ts +6 -1
- package/dist/drivers/HudOnlyDriver.js +32 -14
- package/dist/environment/DayNightSky.js +5 -5
- package/dist/environment/Daylight.d.ts +35 -1
- package/dist/environment/Daylight.js +98 -28
- package/dist/environment/EnvironmentScene.js +11 -10
- package/dist/environment/VolumetricClouds.js +20 -2
- package/dist/environment/daylightCycle.d.ts +20 -0
- package/dist/environment/daylightCycle.js +59 -7
- package/dist/environment/sunShadowMath.d.ts +45 -0
- package/dist/environment/sunShadowMath.js +50 -0
- package/dist/input/gamepadPoll.d.ts +47 -0
- package/dist/input/gamepadPoll.js +142 -0
- package/dist/input/gamepadSource.d.ts +11 -1
- package/dist/input/gamepadSource.js +117 -71
- package/dist/input/mouseLook.js +2 -1
- package/dist/input/padHaptics.d.ts +17 -0
- package/dist/input/padHaptics.js +32 -0
- package/dist/input/pointerLock.d.ts +11 -0
- package/dist/input/pointerLock.js +19 -0
- package/dist/inputSink.d.ts +6 -6
- package/dist/inputSink.js +60 -21
- package/dist/lazyShell3dPresentation.d.ts +9 -0
- package/dist/lazyShell3dPresentation.js +24 -0
- package/dist/materialOverride.d.ts +42 -0
- package/dist/materialOverride.js +95 -36
- package/dist/multiplayer.d.ts +33 -3
- package/dist/multiplayer.js +105 -22
- package/dist/pointer/pointerService.d.ts +2 -0
- package/dist/pointer/pointerService.js +12 -2
- package/dist/postfx/PostProcessing.d.ts +11 -1
- package/dist/postfx/PostProcessing.js +14 -42
- package/dist/postfx/gradeShader.d.ts +1 -1
- package/dist/postfx/gradeShader.js +30 -2
- package/dist/postfx/shadowPrepass.d.ts +3 -0
- package/dist/postfx/shadowPrepass.js +18 -0
- package/dist/postfx/stylizePass.d.ts +23 -0
- package/dist/postfx/stylizePass.js +216 -0
- package/dist/presentationRecovery.d.ts +75 -0
- package/dist/presentationRecovery.js +204 -0
- package/dist/presentationRecovery3d.d.ts +4 -0
- package/dist/presentationRecovery3d.js +32 -0
- package/dist/presentationResolve.d.ts +8 -1
- package/dist/presentationResolve.js +5 -2
- package/dist/render/CascadedShadows.d.ts +7 -2
- package/dist/render/CascadedShadows.js +98 -20
- package/dist/render/EnvironmentLighting.js +74 -35
- package/dist/render/HairCards.d.ts +7 -0
- package/dist/render/HairCards.js +23 -0
- package/dist/render/SceneModels.js +52 -78
- package/dist/render/SkinnedInstances.d.ts +91 -0
- package/dist/render/SkinnedInstances.js +249 -0
- package/dist/render/TextureFiltering.d.ts +16 -0
- package/dist/render/TextureFiltering.js +64 -0
- package/dist/render/authoredSurfaceMaterial.d.ts +43 -0
- package/dist/render/authoredSurfaceMaterial.js +108 -0
- package/dist/render/displayColor.d.ts +33 -0
- package/dist/render/displayColor.js +138 -0
- package/dist/render/materialAppearanceSignals.d.ts +23 -0
- package/dist/render/materialAppearanceSignals.js +58 -0
- package/dist/render/materialAsset.d.ts +98 -0
- package/dist/render/materialAsset.js +454 -0
- package/dist/render/measureBounds.js +16 -3
- package/dist/render/measureCollisionMesh.js +8 -1
- package/dist/render/modelAssets.d.ts +8 -0
- package/dist/render/modelAssets.js +43 -0
- package/dist/render/modelBindPose.d.ts +5 -0
- package/dist/render/modelBindPose.js +35 -0
- package/dist/render/modelLoad.d.ts +32 -0
- package/dist/render/modelLoad.js +70 -1
- package/dist/render/modelNodeVisibility.d.ts +6 -0
- package/dist/render/modelNodeVisibility.js +18 -0
- package/dist/render/modelRender.d.ts +18 -1
- package/dist/render/modelRender.js +81 -3
- package/dist/render/projectedOverdraw.d.ts +35 -0
- package/dist/render/projectedOverdraw.js +204 -0
- package/dist/render/resolveModel.d.ts +12 -2
- package/dist/render/resolveModel.js +25 -4
- package/dist/render/rigNode.d.ts +26 -0
- package/dist/render/rigNode.js +32 -0
- package/dist/render/spriteRender.d.ts +4 -0
- package/dist/render/spriteRender.js +5 -0
- package/dist/render/staticShapeInstances.d.ts +34 -0
- package/dist/render/staticShapeInstances.js +78 -0
- package/dist/render/useFootIk.d.ts +53 -4
- package/dist/render/useFootIk.js +262 -70
- package/dist/render/useModelAnimation.d.ts +50 -0
- package/dist/render/useModelAnimation.js +245 -99
- package/dist/render/useModelInstance.d.ts +25 -0
- package/dist/render/useModelInstance.js +43 -0
- package/dist/scatter/scatterModels.d.ts +3 -2
- package/dist/scatter/scatterModels.js +15 -7
- package/dist/scene/AuthoredScene.d.ts +23 -6
- package/dist/scene/AuthoredScene.js +46 -9
- package/dist/scene/CityRenderer.js +6 -6
- package/dist/scene/GeneratedAssetRenderer.js +11 -42
- package/dist/settings/QuickControls.js +1 -1
- package/dist/settings/SettingsMenu.js +11 -18
- package/dist/settings/SettingsRuntime.d.ts +5 -0
- package/dist/settings/SettingsRuntime.js +29 -2
- package/dist/settings/appliedSettings.d.ts +9 -3
- package/dist/settings/appliedSettings.js +6 -5
- package/dist/settings/settingsController.d.ts +3 -0
- package/dist/settings/settingsController.js +47 -3
- package/dist/shellControlSuspension.d.ts +18 -0
- package/dist/shellControlSuspension.js +28 -0
- package/dist/shellInputPublication.d.ts +28 -0
- package/dist/shellInputPublication.js +49 -0
- package/dist/shellPointerInput.d.ts +18 -0
- package/dist/shellPointerInput.js +49 -0
- package/dist/structures/BuildingKitBatch.d.ts +3 -1
- package/dist/structures/BuildingKitBatch.js +35 -56
- package/dist/structures/BuildingRenderBudget.d.ts +23 -0
- package/dist/structures/BuildingRenderBudget.js +54 -0
- package/dist/structures/GeneratedBuilding.d.ts +3 -15
- package/dist/structures/GeneratedBuilding.js +33 -22
- package/dist/structures/buildingKitMaterials.d.ts +20 -0
- package/dist/structures/buildingKitMaterials.js +70 -0
- package/dist/structures/buildingSpatialBatch.d.ts +11 -0
- package/dist/structures/buildingSpatialBatch.js +45 -0
- package/dist/structures/buildingSurface.d.ts +21 -0
- package/dist/structures/buildingSurface.js +134 -0
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +59 -5
- package/dist/terrain/grassGeometry.d.ts +10 -0
- package/dist/terrain/grassGeometry.js +55 -0
- package/dist/terrain/grassMaterial.js +2 -2
- package/dist/terrain/terrainDetailMaterial.d.ts +3 -3
- package/dist/terrain/terrainDetailMaterial.js +7 -7
- package/dist/touch/TouchControlsOverlay.js +4 -4
- package/dist/useShellMultiplayerSync.d.ts +1 -1
- package/dist/useShellMultiplayerSync.js +9 -3
- package/dist/vfx/ParticleField.d.ts +23 -20
- package/dist/vfx/ParticleField.js +168 -50
- package/dist/visibility/CullingProvider.js +2 -62
- package/dist/visibility/cullingDriver.d.ts +9 -0
- package/dist/visibility/cullingDriver.js +68 -0
- package/dist/weather/AuthoredWeatherLayer.d.ts +16 -0
- package/dist/weather/AuthoredWeatherLayer.js +31 -0
- package/dist/weather/DustField.d.ts +2 -1
- package/dist/weather/DustField.js +17 -3
- package/dist/weather/FireSpreadLayer.d.ts +4 -2
- package/dist/weather/FireSpreadLayer.js +5 -11
- package/dist/weather/LightningStrike.d.ts +4 -1
- package/dist/weather/LightningStrike.js +13 -17
- package/dist/weather/RainField.d.ts +2 -1
- package/dist/weather/RainField.js +17 -3
- package/dist/weather/RainImpactField.d.ts +16 -0
- package/dist/weather/RainImpactField.js +89 -0
- package/dist/weather/SnowField.d.ts +2 -1
- package/dist/weather/SnowField.js +17 -3
- package/dist/weather/WeatherLayer.d.ts +11 -2
- package/dist/weather/WeatherLayer.js +30 -6
- package/dist/weather/index.d.ts +2 -0
- package/dist/weather/index.js +2 -0
- package/dist/weather/weatherMath.d.ts +2 -0
- package/dist/weather/weatherMath.js +8 -0
- package/dist/weather/weatherShelter.d.ts +18 -0
- package/dist/weather/weatherShelter.js +75 -0
- package/dist/weather/weatherUniforms.d.ts +40 -1
- package/dist/weather/weatherUniforms.js +29 -9
- package/dist/world/AuthoredFlocks.d.ts +22 -0
- package/dist/world/AuthoredFlocks.js +87 -0
- package/dist/world/StaticObjectModels.d.ts +8 -0
- package/dist/world/StaticObjectModels.js +75 -0
- package/dist/world/WorldHud.d.ts +14 -6
- package/dist/world/WorldHud.js +25 -12
- package/dist/world/WorldParticles.d.ts +10 -23
- package/dist/world/WorldParticles.js +210 -62
- package/dist/world/WorldScene.d.ts +3 -1
- package/dist/world/WorldScene.js +41 -26
- package/dist/world/staticObjectBatches.d.ts +35 -0
- package/dist/world/staticObjectBatches.js +163 -0
- package/dist/world/worldBarSamples.d.ts +37 -33
- package/dist/world/worldBarSamples.js +117 -75
- package/package.json +4 -4
- package/skills/ce-handoff/SKILL.md +2 -2
- package/skills/game-design/SKILL.md +2 -2
- package/skills/jgengine/SKILL.md +28 -6
- package/skills/jgengine/api.md +139 -99
- package/skills/jgengine/capabilities.md +28 -0
- package/skills/jgengine/recipes/minimal-game.md +10 -2
- package/skills/jgengine-ui/SKILL.md +16 -17
- package/skills/jgengine-ui/api.md +603 -365
- package/skills/jgengine-ui/capabilities.md +131 -1
- package/skills/jgengine-ui/reference-primitives.md +179 -0
- package/skills/jgengine-ui/reference.md +4 -81
- package/skills/jgengine-world/SKILL.md +25 -4
- package/skills/jgengine-world/api.md +1045 -655
- package/skills/jgengine-world/capabilities.md +246 -9
- package/skills/jgengine-world/recipes/character-animation.md +62 -0
- package/skills/jgengine-world/recipes/controls.md +48 -0
- package/skills/jgengine-world/recipes/flight-feel.md +76 -0
- package/skills/jgengine-world/recipes/npc-senses.md +55 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +87 -0
- package/skills/jgengine-world/reference.md +180 -4
- package/skills/jgengine-world/visibility.md +139 -0
- package/skills/level-design/SKILL.md +1 -1
- package/dist/render/testFixtures/proceduralSpriteAtlas.d.ts +0 -15
- package/dist/render/testFixtures/proceduralSpriteAtlas.js +0 -30
- package/dist/render/testFixtures/proceduralTileLayer.d.ts +0 -13
- package/dist/render/testFixtures/proceduralTileLayer.js +0 -30
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type GamepadBindings, type GamepadFrame, type GamepadSample, type ResolveGamepadFrameOptions } from "@jgengine/core/input/gamepadModel";
|
|
2
|
+
import type { ActionCodesMap, ActionStateTracker } from "@jgengine/core/input/actionBindings";
|
|
3
|
+
import type { LocalPlayerSlot, LocalPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
4
|
+
/** Buffers one shell gamepad poll reuses across frames. */
|
|
5
|
+
export interface GamepadPoll {
|
|
6
|
+
held: Set<string>;
|
|
7
|
+
nextHeld: Set<string>;
|
|
8
|
+
frame: GamepadFrame;
|
|
9
|
+
merged: Record<string, number>;
|
|
10
|
+
base: Readonly<Record<string, number>> | null;
|
|
11
|
+
}
|
|
12
|
+
/** Fresh buffers for {@link stepGamepadPoll}. */
|
|
13
|
+
export declare function emptyGamepadPoll(): GamepadPoll;
|
|
14
|
+
/** The pad-only slice of an action binding map, in the shape {@link resolveGamepadFrame} reads. */
|
|
15
|
+
export declare function gamepadCodes(bindings: ActionCodesMap): GamepadBindings;
|
|
16
|
+
/**
|
|
17
|
+
* Swap the pad bindings mid-game (a context push) without touching keyboard state: release the codes
|
|
18
|
+
* of pad-held actions whose pad codes changed (the next poll presses them under the new map), and
|
|
19
|
+
* keep the rest held.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rebindGamepadPoll(poll: GamepadPoll, previous: GamepadBindings, next: GamepadBindings, tracker: ActionStateTracker<string>): void;
|
|
22
|
+
/**
|
|
23
|
+
* One poll: resolve every connected pad, press/release tracker codes for actions whose pad state
|
|
24
|
+
* changed, and return the analog map to publish (pad values max-merged over the other source's).
|
|
25
|
+
* Allocation-free after the first frame.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stepGamepadPoll(poll: GamepadPoll, pads: ArrayLike<GamepadSample | null | undefined>, bindings: GamepadBindings, options: ResolveGamepadFrameOptions, tracker: ActionStateTracker<string>, analogIn: Readonly<Record<string, number>> | null): Readonly<Record<string, number>> | null;
|
|
28
|
+
/** Where each connected pad's input goes this frame; buffers are reused across frames. */
|
|
29
|
+
export interface GamepadRoute {
|
|
30
|
+
/** Pads driving the primary player, by pad index (`null` for pads routed elsewhere). */
|
|
31
|
+
primary: (GamepadSample | null)[];
|
|
32
|
+
/** Pads driving another local seat; entries past `seatCount` are stale pooled slots. */
|
|
33
|
+
seats: {
|
|
34
|
+
slotId: string;
|
|
35
|
+
pad: GamepadSample;
|
|
36
|
+
}[];
|
|
37
|
+
seatCount: number;
|
|
38
|
+
/** Seats opened this frame. */
|
|
39
|
+
joined: LocalPlayerSlot[];
|
|
40
|
+
}
|
|
41
|
+
/** Fresh buffers for {@link routeGamepads}. */
|
|
42
|
+
export declare function emptyGamepadRoute(): GamepadRoute;
|
|
43
|
+
/**
|
|
44
|
+
* Route pads to local seats: a claimed pad goes to its seat, an unclaimed pad hot-joins on a button
|
|
45
|
+
* press (stick drift never opens a seat), and with one seat every pad drives the primary player.
|
|
46
|
+
*/
|
|
47
|
+
export declare function routeGamepads(pads: ArrayLike<GamepadSample | null | undefined>, seats: Pick<LocalPlayers, "assign" | "slotForDevice" | "config">, route: GamepadRoute): GamepadRoute;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { resolveGamepadFrame, } from "@jgengine/core/input/gamepadModel";
|
|
2
|
+
/** Fresh buffers for {@link stepGamepadPoll}. */
|
|
3
|
+
export function emptyGamepadPoll() {
|
|
4
|
+
return { held: new Set(), nextHeld: new Set(), frame: { held: [], analog: {} }, merged: {}, base: null };
|
|
5
|
+
}
|
|
6
|
+
/** The pad-only slice of an action binding map, in the shape {@link resolveGamepadFrame} reads. */
|
|
7
|
+
export function gamepadCodes(bindings) {
|
|
8
|
+
const result = {};
|
|
9
|
+
for (const [action, config] of Object.entries(bindings)) {
|
|
10
|
+
const codes = Array.isArray(config)
|
|
11
|
+
? config
|
|
12
|
+
: [...(config.hold ?? []), ...(config.toggle ?? [])];
|
|
13
|
+
const padCodes = codes.filter((code) => code.startsWith("pad:") || code.startsWith("padaxis:"));
|
|
14
|
+
if (padCodes.length > 0)
|
|
15
|
+
result[action] = padCodes;
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function syncTracker(tracker, bindings, previous, next) {
|
|
20
|
+
for (const action in bindings) {
|
|
21
|
+
const wasDown = previous.has(action);
|
|
22
|
+
const isDown = next.has(action);
|
|
23
|
+
if (isDown === wasDown)
|
|
24
|
+
continue;
|
|
25
|
+
for (const code of bindings[action]) {
|
|
26
|
+
if (isDown)
|
|
27
|
+
tracker.handleDown(code);
|
|
28
|
+
else
|
|
29
|
+
tracker.handleUp(code);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Swap the pad bindings mid-game (a context push) without touching keyboard state: release the codes
|
|
35
|
+
* of pad-held actions whose pad codes changed (the next poll presses them under the new map), and
|
|
36
|
+
* keep the rest held.
|
|
37
|
+
*/
|
|
38
|
+
export function rebindGamepadPoll(poll, previous, next, tracker) {
|
|
39
|
+
for (const action of poll.held) {
|
|
40
|
+
const before = (previous[action] ?? []);
|
|
41
|
+
const after = next[action];
|
|
42
|
+
if (after !== undefined && after.length === before.length && after.every((code, index) => code === before[index]))
|
|
43
|
+
continue;
|
|
44
|
+
for (const code of before)
|
|
45
|
+
tracker.handleUp(code);
|
|
46
|
+
poll.held.delete(action);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One poll: resolve every connected pad, press/release tracker codes for actions whose pad state
|
|
51
|
+
* changed, and return the analog map to publish (pad values max-merged over the other source's).
|
|
52
|
+
* Allocation-free after the first frame.
|
|
53
|
+
*/
|
|
54
|
+
export function stepGamepadPoll(poll, pads, bindings, options, tracker, analogIn) {
|
|
55
|
+
// The analog slot is shared with the touch dock: anything other than our own merged record is a
|
|
56
|
+
// fresh touch publish (or null) and becomes the base the pad values merge over.
|
|
57
|
+
if (analogIn !== poll.merged)
|
|
58
|
+
poll.base = analogIn;
|
|
59
|
+
const merged = poll.merged;
|
|
60
|
+
for (const key in merged)
|
|
61
|
+
delete merged[key];
|
|
62
|
+
let any = false;
|
|
63
|
+
if (poll.base !== null) {
|
|
64
|
+
for (const action in poll.base) {
|
|
65
|
+
merged[action] = poll.base[action];
|
|
66
|
+
any = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const nextHeld = poll.nextHeld;
|
|
70
|
+
nextHeld.clear();
|
|
71
|
+
for (let index = 0; index < pads.length; index += 1) {
|
|
72
|
+
const pad = pads[index];
|
|
73
|
+
if (pad === null || pad === undefined)
|
|
74
|
+
continue;
|
|
75
|
+
const frame = resolveGamepadFrame(pad, bindings, options, poll.frame);
|
|
76
|
+
for (const action of frame.held)
|
|
77
|
+
nextHeld.add(action);
|
|
78
|
+
for (const action in frame.analog) {
|
|
79
|
+
const value = frame.analog[action];
|
|
80
|
+
if (value > (merged[action] ?? 0)) {
|
|
81
|
+
merged[action] = value;
|
|
82
|
+
any = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
syncTracker(tracker, bindings, poll.held, nextHeld);
|
|
87
|
+
poll.nextHeld = poll.held;
|
|
88
|
+
poll.held = nextHeld;
|
|
89
|
+
return any ? merged : null;
|
|
90
|
+
}
|
|
91
|
+
/** Fresh buffers for {@link routeGamepads}. */
|
|
92
|
+
export function emptyGamepadRoute() {
|
|
93
|
+
return { primary: [], seats: [], seatCount: 0, joined: [] };
|
|
94
|
+
}
|
|
95
|
+
function anyButtonPressed(pad) {
|
|
96
|
+
for (let index = 0; index < pad.buttons.length; index += 1) {
|
|
97
|
+
if (pad.buttons[index]?.pressed === true)
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Route pads to local seats: a claimed pad goes to its seat, an unclaimed pad hot-joins on a button
|
|
104
|
+
* press (stick drift never opens a seat), and with one seat every pad drives the primary player.
|
|
105
|
+
*/
|
|
106
|
+
export function routeGamepads(pads, seats, route) {
|
|
107
|
+
route.primary.length = pads.length;
|
|
108
|
+
route.seatCount = 0;
|
|
109
|
+
route.joined.length = 0;
|
|
110
|
+
const single = seats.config().maxSlots === 1;
|
|
111
|
+
for (let index = 0; index < pads.length; index += 1) {
|
|
112
|
+
route.primary[index] = null;
|
|
113
|
+
const pad = pads[index];
|
|
114
|
+
if (pad === null || pad === undefined || !pad.connected)
|
|
115
|
+
continue;
|
|
116
|
+
const deviceId = `gamepad:${index}`;
|
|
117
|
+
let slot = seats.slotForDevice(deviceId);
|
|
118
|
+
if (slot === null && (single || anyButtonPressed(pad))) {
|
|
119
|
+
const assigned = seats.assign(deviceId);
|
|
120
|
+
if (assigned !== null) {
|
|
121
|
+
slot = assigned.slot;
|
|
122
|
+
if (assigned.joined)
|
|
123
|
+
route.joined.push(slot);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (slot === null)
|
|
127
|
+
continue;
|
|
128
|
+
if (slot.index === 0)
|
|
129
|
+
route.primary[index] = pad;
|
|
130
|
+
else {
|
|
131
|
+
const entry = route.seats[route.seatCount];
|
|
132
|
+
if (entry === undefined)
|
|
133
|
+
route.seats.push({ slotId: slot.slotId, pad });
|
|
134
|
+
else {
|
|
135
|
+
entry.slotId = slot.slotId;
|
|
136
|
+
entry.pad = pad;
|
|
137
|
+
}
|
|
138
|
+
route.seatCount += 1;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return route;
|
|
142
|
+
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
import { type GamepadFeelConfig } from "@jgengine/core/input/gamepadModel";
|
|
2
|
+
import type { LocalPlayerSlot, LocalPlayers } from "@jgengine/core/runtime/localPlayers";
|
|
1
3
|
import type { ActionCodesMap, ActionStateTracker } from "@jgengine/core/input/actionBindings";
|
|
2
4
|
import type { InputSnapshot } from "@jgengine/core/runtime/inputSnapshot";
|
|
3
5
|
export { mergeGamepadFrame, mergeGamepadInput } from "./gamepadMerge.js";
|
|
4
6
|
/** Poll browser gamepads and feed semantic actions into the shell tracker. */
|
|
5
|
-
export declare function GamepadSource({ tracker, bindings, analogRef, input, }: {
|
|
7
|
+
export declare function GamepadSource({ tracker, bindings, analogRef, input, feel, seats, onSeatJoin, seatsActive, }: {
|
|
6
8
|
tracker: ActionStateTracker<string>;
|
|
7
9
|
bindings: ActionCodesMap;
|
|
8
10
|
analogRef: {
|
|
9
11
|
current: Readonly<Record<string, number>> | null;
|
|
10
12
|
};
|
|
11
13
|
input: InputSnapshot;
|
|
14
|
+
/** Game-level pad feel (`defineGame({ gamepad })`); unset keeps the shell defaults. */
|
|
15
|
+
feel?: GamepadFeelConfig;
|
|
16
|
+
/** Local seats; pads claimed by a seat other than the primary publish to that seat's input. */
|
|
17
|
+
seats?: LocalPlayers;
|
|
18
|
+
/** A pad just opened a new seat; the shell spawns its player here. */
|
|
19
|
+
onSeatJoin?: (slot: LocalPlayerSlot) => void;
|
|
20
|
+
/** False while play controls are gated (menus, orientation lock): seats publish nothing held. */
|
|
21
|
+
seatsActive?: () => boolean;
|
|
12
22
|
}): null;
|
|
@@ -1,64 +1,69 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
|
-
import { resolveGamepadFrame, } from "@jgengine/core/input/gamepadModel";
|
|
4
|
-
import {
|
|
3
|
+
import { gamepadFeelOptions, resolveGamepadFrame, } from "@jgengine/core/input/gamepadModel";
|
|
4
|
+
import { emptyGamepadPoll, emptyGamepadRoute, gamepadCodes, rebindGamepadPoll, routeGamepads, stepGamepadPoll, } from "./gamepadPoll.js";
|
|
5
|
+
import { PAD_HAPTIC_EFFECT_MS, emptyPadHapticState, stepPadHaptics } from "./padHaptics.js";
|
|
5
6
|
export { mergeGamepadFrame, mergeGamepadInput } from "./gamepadMerge.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
axes: Array.from(gamepad.axes),
|
|
10
|
-
buttons: Array.from(gamepad.buttons, (button) => ({ pressed: button.pressed, value: button.value })),
|
|
11
|
-
connected: gamepad.connected,
|
|
12
|
-
};
|
|
7
|
+
const NO_PADS = [];
|
|
8
|
+
function browserPads() {
|
|
9
|
+
return typeof navigator === "undefined" || navigator.getGamepads === undefined ? NO_PADS : navigator.getGamepads();
|
|
13
10
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
for (const [action, config] of Object.entries(bindings)) {
|
|
17
|
-
const codes = Array.isArray(config)
|
|
18
|
-
? config
|
|
19
|
-
: [...(config.hold ?? []), ...(config.toggle ?? [])];
|
|
20
|
-
const padCodes = codes.filter((code) => code.startsWith("pad:") || code.startsWith("padaxis:"));
|
|
21
|
-
if (padCodes.length > 0)
|
|
22
|
-
result.set(action, padCodes);
|
|
23
|
-
}
|
|
24
|
-
return result;
|
|
11
|
+
function actuatorOf(pad) {
|
|
12
|
+
return pad === null || pad === undefined || !("vibrationActuator" in pad) ? null : (pad.vibrationActuator ?? null);
|
|
25
13
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
14
|
+
function ignore() { }
|
|
15
|
+
/** Pad index a user's rumble goes to: their seat's pad, else the first connected pad for the primary seat. */
|
|
16
|
+
function padIndexForUser(userId, seats, pads) {
|
|
17
|
+
const legacy = /^gamepad:(\d+)$/.exec(userId)?.[1];
|
|
18
|
+
if (legacy !== undefined)
|
|
19
|
+
return Number(legacy);
|
|
20
|
+
const slot = seats?.slots().find((candidate) => candidate.userId === userId);
|
|
21
|
+
const claimed = slot?.deviceId === null || slot?.deviceId === undefined ? undefined : /^gamepad:(\d+)$/.exec(slot.deviceId)?.[1];
|
|
22
|
+
if (claimed !== undefined)
|
|
23
|
+
return Number(claimed);
|
|
24
|
+
if (slot !== undefined && slot.index !== 0)
|
|
25
|
+
return -1;
|
|
26
|
+
for (let index = 0; index < pads.length; index += 1) {
|
|
27
|
+
if (pads[index]?.connected === true)
|
|
28
|
+
return index;
|
|
36
29
|
}
|
|
30
|
+
return -1;
|
|
37
31
|
}
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
const NO_HELD = [];
|
|
33
|
+
function hasAnalog(analog) {
|
|
34
|
+
for (const _ in analog)
|
|
35
|
+
return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
// `?gamepad` (or `?gamepad=N` for N pads) injects synthetic pads holding Start (button 9), since
|
|
39
|
+
// headless browsers cannot attach a real one; N > 1 hot-joins local seats.
|
|
40
|
+
function syntheticPads() {
|
|
41
|
+
if (typeof window === "undefined")
|
|
42
|
+
return null;
|
|
43
|
+
const param = new URLSearchParams(window.location.search).get("gamepad");
|
|
44
|
+
if (param === null)
|
|
40
45
|
return null;
|
|
41
|
-
|
|
46
|
+
const count = Math.max(1, Math.min(4, Number(param) || 1));
|
|
47
|
+
const buttons = Array.from({ length: 10 }, (_, index) => ({ pressed: index === 9, value: index === 9 ? 1 : 0 }));
|
|
48
|
+
return Array.from({ length: count }, () => ({ axes: [0, 0, 0, 0], buttons, connected: true }));
|
|
42
49
|
}
|
|
43
50
|
/** Poll browser gamepads and feed semantic actions into the shell tracker. */
|
|
44
|
-
export function GamepadSource({ tracker, bindings, analogRef, input, }) {
|
|
45
|
-
const
|
|
46
|
-
const previousAnalog = useRef(new Set());
|
|
51
|
+
export function GamepadSource({ tracker, bindings, analogRef, input, feel, seats, onSeatJoin, seatsActive, }) {
|
|
52
|
+
const poll = useRef(emptyGamepadPoll());
|
|
47
53
|
const padBindings = useRef(gamepadCodes(bindings));
|
|
54
|
+
const options = useMemo(() => gamepadFeelOptions(feel), [feel]);
|
|
55
|
+
const synthetic = useMemo(syntheticPads, []);
|
|
48
56
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
tracker.reset();
|
|
57
|
+
const next = gamepadCodes(bindings);
|
|
58
|
+
rebindGamepadPoll(poll.current, padBindings.current, next, tracker);
|
|
59
|
+
padBindings.current = next;
|
|
53
60
|
}, [bindings, tracker]);
|
|
54
61
|
useEffect(() => {
|
|
55
62
|
const inputWithRumble = input;
|
|
56
63
|
inputWithRumble.rumble = async (userId, options) => {
|
|
57
|
-
const pads =
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const actuator = pad?.vibrationActuator;
|
|
61
|
-
if (actuator === undefined)
|
|
64
|
+
const pads = browserPads();
|
|
65
|
+
const actuator = actuatorOf(pads[padIndexForUser(userId, seats, pads)]);
|
|
66
|
+
if (actuator === null)
|
|
62
67
|
return false;
|
|
63
68
|
try {
|
|
64
69
|
await actuator.playEffect("dual-rumble", {
|
|
@@ -75,33 +80,74 @@ export function GamepadSource({ tracker, bindings, analogRef, input, }) {
|
|
|
75
80
|
return () => {
|
|
76
81
|
delete inputWithRumble.rumble;
|
|
77
82
|
};
|
|
78
|
-
}, [input]);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
for (let index = 0; index <
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
}, [input, seats]);
|
|
84
|
+
const hapticStates = useRef(new Map());
|
|
85
|
+
const driveHaptics = (dt, pads) => {
|
|
86
|
+
if (seats === undefined)
|
|
87
|
+
return;
|
|
88
|
+
const nowMs = performance.now();
|
|
89
|
+
const slots = seats.slots();
|
|
90
|
+
for (let index = 0; index < slots.length; index += 1) {
|
|
91
|
+
const level = input.haptics(slots[index].userId).mix(dt);
|
|
92
|
+
const padIndex = padIndexForUser(slots[index].userId, seats, pads);
|
|
93
|
+
const actuator = actuatorOf(pads[padIndex]);
|
|
94
|
+
if (actuator === null)
|
|
95
|
+
continue;
|
|
96
|
+
let state = hapticStates.current.get(padIndex);
|
|
97
|
+
if (state === undefined) {
|
|
98
|
+
state = emptyPadHapticState();
|
|
99
|
+
hapticStates.current.set(padIndex, state);
|
|
100
|
+
}
|
|
101
|
+
const command = stepPadHaptics(state, level, nowMs);
|
|
102
|
+
if (command === "play") {
|
|
103
|
+
actuator
|
|
104
|
+
.playEffect("dual-rumble", { duration: PAD_HAPTIC_EFFECT_MS, strongMagnitude: level.strong, weakMagnitude: level.weak })
|
|
105
|
+
.catch(ignore);
|
|
91
106
|
}
|
|
107
|
+
else if (command === "reset") {
|
|
108
|
+
actuator.reset?.().catch(ignore);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const route = useRef(emptyGamepadRoute());
|
|
113
|
+
const seatFrame = useRef({ held: [], analog: {} });
|
|
114
|
+
const seatsPublished = useRef(new Set());
|
|
115
|
+
useFrame((_state, dt) => {
|
|
116
|
+
const pads = synthetic ?? browserPads();
|
|
117
|
+
driveHaptics(dt, pads);
|
|
118
|
+
if (seats === undefined) {
|
|
119
|
+
analogRef.current = stepGamepadPoll(poll.current, pads, padBindings.current, options, tracker, analogRef.current);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const routed = routeGamepads(pads, seats, route.current);
|
|
123
|
+
for (const slot of routed.joined)
|
|
124
|
+
onSeatJoin?.(slot);
|
|
125
|
+
analogRef.current = stepGamepadPoll(poll.current, routed.primary, padBindings.current, options, tracker, analogRef.current);
|
|
126
|
+
const active = seatsActive?.() ?? true;
|
|
127
|
+
const published = seatsPublished.current;
|
|
128
|
+
for (const slotId of published) {
|
|
129
|
+
let routedNow = false;
|
|
130
|
+
for (let index = 0; index < routed.seatCount; index += 1)
|
|
131
|
+
routedNow ||= routed.seats[index].slotId === slotId;
|
|
132
|
+
if (routedNow && active)
|
|
133
|
+
continue;
|
|
134
|
+
const seat = seats.local(slotId);
|
|
135
|
+
seat?.input.publish(NO_HELD);
|
|
136
|
+
seat?.input.publishAnalog(null);
|
|
137
|
+
published.delete(slotId);
|
|
138
|
+
}
|
|
139
|
+
if (!active)
|
|
140
|
+
return;
|
|
141
|
+
for (let index = 0; index < routed.seatCount; index += 1) {
|
|
142
|
+
const { slotId, pad } = routed.seats[index];
|
|
143
|
+
const seat = seats.local(slotId);
|
|
144
|
+
if (seat === null)
|
|
145
|
+
continue;
|
|
146
|
+
const frame = resolveGamepadFrame(pad, padBindings.current, options, seatFrame.current);
|
|
147
|
+
seat.input.publish(frame.held);
|
|
148
|
+
seat.input.publishAnalog(hasAnalog(frame.analog) ? frame.analog : null);
|
|
149
|
+
published.add(slotId);
|
|
92
150
|
}
|
|
93
|
-
const baseAnalog = { ...(analogRef.current ?? {}) };
|
|
94
|
-
for (const action of previousAnalog.current)
|
|
95
|
-
delete baseAnalog[action];
|
|
96
|
-
const merged = mergeGamepadInput(frames, { held: [], analog: baseAnalog });
|
|
97
|
-
const nextHeld = new Set(merged.held);
|
|
98
|
-
syncTracker(tracker, padBindings.current, previousHeld.current, nextHeld);
|
|
99
|
-
previousHeld.current = nextHeld;
|
|
100
|
-
analogRef.current = Object.keys(merged.analog).length === 0 ? null : merged.analog;
|
|
101
|
-
previousAnalog.current = new Set(Object.keys(mergeGamepadInput(frames).analog));
|
|
102
151
|
});
|
|
103
152
|
return null;
|
|
104
153
|
}
|
|
105
|
-
function padBindingsToGamepad(bindings) {
|
|
106
|
-
return Object.fromEntries(bindings.entries());
|
|
107
|
-
}
|
package/dist/input/mouseLook.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestRawPointerLock } from "./pointerLock.js";
|
|
1
2
|
export function createMouseLookTracker(element, options = {}) {
|
|
2
3
|
const sensitivity = options.sensitivity ?? 0.0024;
|
|
3
4
|
const maxPitch = options.maxPitch ?? 1.15;
|
|
@@ -10,7 +11,7 @@ export function createMouseLookTracker(element, options = {}) {
|
|
|
10
11
|
if (window.matchMedia?.("(pointer: coarse)").matches)
|
|
11
12
|
return;
|
|
12
13
|
if (document.pointerLockElement !== element)
|
|
13
|
-
|
|
14
|
+
requestRawPointerLock(element);
|
|
14
15
|
};
|
|
15
16
|
const onMove = (event) => {
|
|
16
17
|
if (pointerLock && document.pointerLockElement !== element)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HapticLevel } from "@jgengine/core/input/haptics";
|
|
2
|
+
/** How long each rumble command lasts; refreshed before it runs out so a held level feels continuous. */
|
|
3
|
+
export declare const PAD_HAPTIC_EFFECT_MS = 120;
|
|
4
|
+
/** Last command sent to one pad. */
|
|
5
|
+
export interface PadHapticState {
|
|
6
|
+
strong: number;
|
|
7
|
+
weak: number;
|
|
8
|
+
sentAtMs: number;
|
|
9
|
+
active: boolean;
|
|
10
|
+
}
|
|
11
|
+
/** Fresh state for {@link stepPadHaptics}. */
|
|
12
|
+
export declare function emptyPadHapticState(): PadHapticState;
|
|
13
|
+
/**
|
|
14
|
+
* Decide what to send a pad this frame for a mixed level: `"play"` when the level changed or the
|
|
15
|
+
* last effect is about to run out, `"reset"` when it fell silent, `null` otherwise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function stepPadHaptics(state: PadHapticState, level: HapticLevel, nowMs: number): "play" | "reset" | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** How long each rumble command lasts; refreshed before it runs out so a held level feels continuous. */
|
|
2
|
+
export const PAD_HAPTIC_EFFECT_MS = 120;
|
|
3
|
+
const REFRESH_MS = 80;
|
|
4
|
+
const CHANGE = 0.03;
|
|
5
|
+
const SILENT = 0.01;
|
|
6
|
+
/** Fresh state for {@link stepPadHaptics}. */
|
|
7
|
+
export function emptyPadHapticState() {
|
|
8
|
+
return { strong: 0, weak: 0, sentAtMs: -Infinity, active: false };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Decide what to send a pad this frame for a mixed level: `"play"` when the level changed or the
|
|
12
|
+
* last effect is about to run out, `"reset"` when it fell silent, `null` otherwise.
|
|
13
|
+
*/
|
|
14
|
+
export function stepPadHaptics(state, level, nowMs) {
|
|
15
|
+
const silent = level.strong < SILENT && level.weak < SILENT;
|
|
16
|
+
if (silent) {
|
|
17
|
+
if (!state.active)
|
|
18
|
+
return null;
|
|
19
|
+
state.active = false;
|
|
20
|
+
state.strong = 0;
|
|
21
|
+
state.weak = 0;
|
|
22
|
+
return "reset";
|
|
23
|
+
}
|
|
24
|
+
const changed = Math.abs(level.strong - state.strong) > CHANGE || Math.abs(level.weak - state.weak) > CHANGE;
|
|
25
|
+
if (state.active && !changed && nowMs - state.sentAtMs < REFRESH_MS)
|
|
26
|
+
return null;
|
|
27
|
+
state.active = true;
|
|
28
|
+
state.strong = level.strong;
|
|
29
|
+
state.weak = level.weak;
|
|
30
|
+
state.sentAtMs = nowMs;
|
|
31
|
+
return "play";
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface LockableElement {
|
|
2
|
+
requestPointerLock?: (options?: {
|
|
3
|
+
unadjustedMovement?: boolean;
|
|
4
|
+
}) => Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Request pointer lock with raw (unaccelerated) mouse deltas, falling back to a plain lock when
|
|
8
|
+
* the browser or OS rejects `unadjustedMovement`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestRawPointerLock(element: LockableElement): void;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request pointer lock with raw (unaccelerated) mouse deltas, falling back to a plain lock when
|
|
3
|
+
* the browser or OS rejects `unadjustedMovement`.
|
|
4
|
+
*/
|
|
5
|
+
export function requestRawPointerLock(element) {
|
|
6
|
+
const request = element.requestPointerLock;
|
|
7
|
+
if (request === undefined)
|
|
8
|
+
return;
|
|
9
|
+
let pending;
|
|
10
|
+
try {
|
|
11
|
+
pending = request.call(element, { unadjustedMovement: true });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
pending = request.call(element);
|
|
15
|
+
}
|
|
16
|
+
if (pending !== undefined && typeof pending.catch === "function") {
|
|
17
|
+
pending.catch(() => request.call(element)?.catch?.(() => undefined));
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/inputSink.d.ts
CHANGED
|
@@ -2,17 +2,17 @@ import { type InputFrame } from "@jgengine/core/runtime/hostedGameRunner";
|
|
|
2
2
|
import type { LiveGameBackend } from "@jgengine/core/runtime/transport";
|
|
3
3
|
/** Where the local player's per-frame input goes: discarded in single-player, sent to the authoritative host under `authority: "server"`. */
|
|
4
4
|
export interface InputSink {
|
|
5
|
-
send(frame: InputFrame
|
|
5
|
+
send(frame: InputFrame, options?: {
|
|
6
|
+
urgent?: boolean;
|
|
7
|
+
reset?: boolean;
|
|
8
|
+
}): void;
|
|
6
9
|
}
|
|
7
10
|
/** Discards input — the single-player / client-authoritative default, where the client integrates movement itself.
|
|
8
11
|
* @internal
|
|
9
12
|
*/
|
|
10
13
|
export declare function noopInputSink(): InputSink;
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
* {@link INPUT_COMMAND}. Sends are sequenced one-in-flight-at-a-time per `serverId` and stamped with a monotonic
|
|
14
|
-
* seq: a frame sent while one is already in flight replaces the pending frame rather than racing it, so the
|
|
15
|
-
* latest intent always wins and a stale frame can never resolve after (and overwrite) a newer one.
|
|
14
|
+
/** Coalesces continuous frames. Discrete release/reset intents bypass a pending ACK;
|
|
15
|
+
* the host's monotonic sequence admission rejects any older input arriving afterwards.
|
|
16
16
|
* @internal
|
|
17
17
|
*/
|
|
18
18
|
export declare function remoteInputSink(backend: Pick<LiveGameBackend, "transport">, serverId: string): InputSink;
|