SOURCE / PINNED RELEASE
Made of little things.
Pickle Rustlers
- Release
- 039e51f55e34…
- Author-recorded commit
- 40608586a8a1…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35
Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { gamepad, type GamepadApi, type GamepadState } from '@napplet/sdk';
import { runtimeHasDomain } from '../domain-availability.js';
import type { GamepadSample } from './gamepad.js';
interface ControllerSource {
read: () => ArrayLike<GamepadSample | null>;
active: () => boolean;
dispose(): void;
}
/**
* Hosts without NAP-GAMEPAD (soyLI 0.31 included) still let the frame read the
* browser Gamepad API, which soyLI's controller guide recommends. Only an absent
* domain falls back here; a host that injects the domain but denies it stays denied.
*/
function createNativeGamepadSource(): ControllerSource {
return {
read: () => {
if (typeof navigator === 'undefined' || !navigator.getGamepads) throw new Error('Gamepad API unavailable');
return navigator.getGamepads();
},
active: () => typeof document !== 'undefined' && !document.hidden && document.hasFocus(),
dispose() {},
};
}
/** Cache shell snapshots; rendering never sends controller requests. */
export function createShellGamepadSource(
reset: () => void,
supported = runtimeHasDomain('gamepad'),
api: GamepadApi = gamepad,
): ControllerSource {
if (!supported) return createNativeGamepadSource();
let state: GamepadState = { available: false, focused: false, pads: [] };
let subscription: { close(): void } | undefined;
let closed = false;
const update = (next: GamepadState) => {
if (closed) return;
const before = new Map(state.pads.filter(pad => pad !== null).map(pad => [pad.index, pad]));
const after = new Map(next.pads.filter(pad => pad !== null).map(pad => [pad.index, pad]));
const changed = before.size !== after.size || [...before].some(([index, pad]) =>
after.get(index)?.id !== pad.id || after.get(index)?.mapping !== pad.mapping);
// Observe focus loss even when loss and regain happen between render frames.
if (changed || state.focused !== next.focused || state.available !== next.available) reset();
state = next;
};
try {
update({ available: api.available, focused: api.focused, pads: api.getGamepads() });
subscription = api.onChange(update);
} catch {
update({ available: false, reason: 'unavailable', focused: false, pads: [] });
}
return {
read: () => {
if (!state.available) {
const error = new Error('Controller input unavailable');
if (state.reason === 'blocked') error.name = 'SecurityError';
throw error;
}
return state.pads;
},
active: () => !closed && state.available && state.focused,
dispose() {
if (closed) return;
closed = true;
state = { available: false, focused: false, pads: [] };
try { subscription?.close(); } catch { /* The host may already have ended the session. */ }
reset();
},
};
}
