@martini-kit/devtools
Development tools for martini-kit multiplayer SDK - debug your multiplayer games with ease.
Features
- Real-time State Inspection - Diff-based snapshots with automatic throttling
- Action History Tracking - Aggregated timelines that avoid spammy actions
- Statistics & Metrics - Track action frequency and state changes
- Event Listeners - React to state changes and actions programmatically
- Memory-Efficient - Configurable limits for history size
Installation
pnpm add -D @martini-kit/devtools
Quick Start
import { StateInspector } from '@martini-kit/devtools';
import { GameRuntime } from '@martini-kit/core';
const runtime = new GameRuntime(myGame, transport, config);
const inspector = new StateInspector();
inspector.attach(runtime);
console.log(inspector.getSnapshots());
console.log(inspector.getActionHistory());
console.log(inspector.getStats());
inspector.onStateChange((snapshot) => {
console.log('State changed:', snapshot.state);
});
inspector.onAction((action) => {
console.log('Action submitted:', action.actionName, action.input);
});
inspector.detach();
API Reference
Constructor
new StateInspector(options?: StateInspectorOptions)
Options:
maxSnapshots (default: 100) - Maximum number of state snapshots to keep
maxActions (default: 1000) - Maximum number of actions to keep in history
snapshotIntervalMs (default: 250) - Minimum time between automatic snapshots
actionAggregationWindowMs (default: 200) - Time window for grouping identical actions
ignoreActions (default: []) - Array of action names to drop entirely (e.g. ['tick'])
Methods
attach(runtime: GameRuntime): void
Attach the inspector to a GameRuntime instance. Only one runtime can be attached at a time.
inspector.attach(runtime);
detach(): void
Detach the inspector from the current runtime and stop tracking.
inspector.detach();
isAttached(): boolean
Check if the inspector is currently attached to a runtime.
if (inspector.isAttached()) {
console.log('Inspector is active');
}
getRuntime(): GameRuntime | null
Get the currently attached runtime, or null if not attached.
const runtime = inspector.getRuntime();
getSnapshots(): StateSnapshot[]
Get all captured state snapshots.
const snapshots = inspector.getSnapshots();
snapshots.forEach(snapshot => {
console.log(snapshot.timestamp, snapshot.state);
});
Returns an array of:
interface StateSnapshot {
id: number;
timestamp: number;
state?: any;
diff?: Patch[];
lastActionId?: number;
}
getActionHistory(): ActionRecord[]
Get all tracked actions.
const history = inspector.getActionHistory();
history.forEach(action => {
console.log(action.actionName, action.input);
});
Returns an array of:
interface ActionRecord {
id: number;
timestamp: number;
actionName: string;
input: any;
playerId?: string;
targetId?: string;
count?: number;
duration?: number;
snapshotId?: number;
excludedActionsTotal?: number;
}
getStats(): InspectorStats
Get statistics about tracked actions and state changes.
const stats = inspector.getStats();
console.log(`Total actions: ${stats.totalActions}`);
console.log(`Total state changes: ${stats.totalStateChanges}`);
console.log(`Increment action called ${stats.actionsByName.increment} times`);
Returns:
interface InspectorStats {
totalActions: number;
totalStateChanges: number;
actionsByName: Record<string, number>;
excludedActions: number;
}
onStateChange(listener: (snapshot: StateSnapshot) => void): () => void
Listen for state changes. Returns an unsubscribe function.
const unsubscribe = inspector.onStateChange((snapshot) => {
console.log('New state:', snapshot.state);
});
unsubscribe();
onAction(listener: (action: ActionRecord) => void): () => void
Listen for actions. Returns an unsubscribe function.
const unsubscribe = inspector.onAction((action) => {
console.log(`${action.actionName} submitted by ${action.playerId}`);
});
unsubscribe();
clear(): void
Clear all snapshots, action history, and statistics.
inspector.clear();
Use Cases
Debug State Issues
const inspector = new StateInspector();
inspector.attach(runtime);
runtime.submitAction('move', { x: 100, y: 200 });
runtime.submitAction('attack', { targetId: 'enemy1' });
const snapshots = inspector.getSnapshots();
console.log('Initial state:', snapshots[0].state);
console.log('Final state:', snapshots[snapshots.length - 1].state);
Track Performance
const inspector = new StateInspector();
inspector.attach(runtime);
let actionCount = 0;
inspector.onAction(() => {
actionCount++;
if (actionCount % 100 === 0) {
const stats = inspector.getStats();
console.log(`Actions per second: ${actionCount / ((Date.now() - startTime) / 1000)}`);
}
});
Build Custom DevTools UI
const inspector = new StateInspector({ maxSnapshots: 50 });
inspector.attach(runtime);
function DevToolsPanel() {
const [snapshots, setSnapshots] = useState([]);
const [actions, setActions] = useState([]);
useEffect(() => {
const unsubState = inspector.onStateChange((snapshot) => {
setSnapshots(prev => [...prev, snapshot]);
});
const unsubAction = inspector.onAction((action) => {
setActions(prev => [...prev, action]);
});
return () => {
unsubState();
unsubAction();
};
}, []);
return (
<div>
<StateViewer snapshots={snapshots} />
<ActionHistory actions={actions} />
<Statistics stats={inspector.getStats()} />
</div>
);
}
Monitor Multiplayer Sync
const inspector = new StateInspector();
inspector.attach(runtime);
inspector.onAction((action) => {
console.log(`[${action.timestamp}] ${action.playerId} -> ${action.actionName}`);
});
inspector.onStateChange((snapshot) => {
if (snapshot.state.players[myId]?.health < 0) {
console.warn('Invalid state detected:', snapshot);
}
});
Testing with StateInspector
import { describe, it, expect } from 'vitest';
import { StateInspector } from '@martini-kit/devtools';
describe('MyGame', () => {
it('should increment counter on action', () => {
const inspector = new StateInspector();
inspector.attach(runtime);
const initialState = inspector.getSnapshots()[0].state;
expect(initialState.count).toBe(0);
runtime.submitAction('increment', {});
const finalSnapshots = inspector.getSnapshots();
const finalState = finalSnapshots[finalSnapshots.length - 1].state;
expect(finalState.count).toBe(1);
inspector.detach();
});
});
Best Practices
- Use in Development Only - Don't ship StateInspector to production (it's a devDependency)
- Set Limits - Configure
maxSnapshots and maxActions to prevent memory issues
- Detach When Done - Always call
detach() to clean up listeners
- Deep Clone Awareness - Snapshots are deep clones, so large states may impact performance
Next Steps
License
MIT