@gamerstake/game-core 0.1.0 → 0.2.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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Client SDK Types
3
+ *
4
+ * Type definitions for the game-core client SDK.
5
+ */
6
+
7
+ import type { Socket } from 'socket.io-client';
8
+
9
+ /**
10
+ * Game client configuration
11
+ */
12
+ export interface GameClientConfig {
13
+ /** Server URL to connect to */
14
+ serverUrl: string;
15
+
16
+ /** Enable client-side prediction */
17
+ enablePrediction?: boolean;
18
+
19
+ /** Enable server reconciliation */
20
+ enableReconciliation?: boolean;
21
+
22
+ /** Enable entity interpolation */
23
+ enableInterpolation?: boolean;
24
+
25
+ /** Interpolation delay in milliseconds */
26
+ interpolationDelay?: number;
27
+
28
+ /** Maximum interpolation delay in milliseconds */
29
+ maxInterpolationDelay?: number;
30
+
31
+ /** Minimum interpolation delay in milliseconds */
32
+ minInterpolationDelay?: number;
33
+
34
+ /** Number of reconnection attempts */
35
+ reconnectAttempts?: number;
36
+
37
+ /** Delay between reconnection attempts in milliseconds */
38
+ reconnectDelay?: number;
39
+
40
+ /** Heartbeat interval in milliseconds */
41
+ heartbeatInterval?: number;
42
+
43
+ /** Heartbeat timeout in milliseconds */
44
+ heartbeatTimeout?: number;
45
+
46
+ /** Socket.io connection options */
47
+ socketOptions?: Partial<{
48
+ autoConnect: boolean;
49
+ reconnection: boolean;
50
+ reconnectionAttempts: number;
51
+ reconnectionDelay: number;
52
+ transports: string[];
53
+ }>;
54
+ }
55
+
56
+ /**
57
+ * Command sent from client to server
58
+ */
59
+ export interface Command {
60
+ /** Command type */
61
+ type: string;
62
+
63
+ /** Action sequence number */
64
+ seq?: number;
65
+
66
+ /** Timestamp when command was created */
67
+ timestamp?: number;
68
+
69
+ /** Additional command data */
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ /**
74
+ * Move command
75
+ */
76
+ export interface MoveCommand extends Command {
77
+ type: 'move';
78
+ targetX: number;
79
+ targetZ: number;
80
+ actionId?: number;
81
+ }
82
+
83
+ /**
84
+ * Stop command
85
+ */
86
+ export interface StopCommand extends Command {
87
+ type: 'stop';
88
+ }
89
+
90
+ /**
91
+ * Network statistics
92
+ */
93
+ export interface NetworkStats {
94
+ /** Round-trip time in milliseconds */
95
+ rtt: number;
96
+
97
+ /** Network jitter in milliseconds */
98
+ jitter: number;
99
+
100
+ /** Estimated packet loss percentage */
101
+ packetLoss: number;
102
+
103
+ /** Total messages sent */
104
+ messagesSent: number;
105
+
106
+ /** Total messages received */
107
+ messagesReceived: number;
108
+
109
+ /** Whether connected to server */
110
+ connected: boolean;
111
+
112
+ /** Whether currently reconnecting */
113
+ reconnecting: boolean;
114
+
115
+ /** Number of unacknowledged inputs */
116
+ inputBufferSize: number;
117
+
118
+ /** Last state update time */
119
+ lastStateUpdateTime: number;
120
+ }
121
+
122
+ /**
123
+ * Time sync statistics
124
+ */
125
+ export interface TimeSyncStats {
126
+ /** Current clock offset */
127
+ offset: number;
128
+
129
+ /** Round-trip time */
130
+ rtt: number;
131
+
132
+ /** Network jitter */
133
+ jitter: number;
134
+
135
+ /** Number of samples */
136
+ samples: number;
137
+ }
138
+
139
+ /**
140
+ * Event callbacks
141
+ */
142
+ export interface GameClientEvents {
143
+ /** Called when connected to server */
144
+ onConnected?: (data?: unknown) => void;
145
+
146
+ /** Called when disconnected from server */
147
+ onDisconnected?: (data: unknown) => void;
148
+
149
+ /** Called when connection is lost */
150
+ onConnectionLost?: (data: unknown) => void;
151
+
152
+ /** Called when reconnected after disconnect */
153
+ onReconnect?: (data?: unknown) => void;
154
+
155
+ /** Called on connection error */
156
+ onError?: (data: unknown) => void;
157
+
158
+ /** Called when state update received */
159
+ onStateUpdate?: (data: unknown) => void;
160
+
161
+ /** Called when move acknowledgment received */
162
+ onMoveAck?: (data: unknown) => void;
163
+
164
+ /** Custom event handlers */
165
+ [key: string]: ((data?: unknown) => void) | undefined;
166
+ }
167
+
168
+ /**
169
+ * Input buffer entry
170
+ */
171
+ export interface BufferedInput {
172
+ /** Sequence number */
173
+ seq: number;
174
+
175
+ /** Command data */
176
+ command: Command;
177
+
178
+ /** Timestamp */
179
+ timestamp: number;
180
+
181
+ /** Whether acknowledged by server */
182
+ acknowledged: boolean;
183
+ }
184
+
185
+ /**
186
+ * State snapshot for interpolation
187
+ */
188
+ export interface StateSnapshot {
189
+ /** Timestamp */
190
+ timestamp: number;
191
+
192
+ /** Server tick number */
193
+ tick?: number;
194
+
195
+ /** Entity states */
196
+ entities?: any[];
197
+
198
+ /** Custom state data */
199
+ [key: string]: unknown;
200
+ }
201
+
202
+ /**
203
+ * Transport interface (abstraction for future protocols)
204
+ */
205
+ export interface Transport {
206
+ connect(): Promise<void>;
207
+ disconnect(): void;
208
+ send(event: string, data: unknown): void;
209
+ on(event: string, handler: (data: unknown) => void): void;
210
+ off(event: string, handler: (data: unknown) => void): void;
211
+ once(event: string, handler: (data: unknown) => void): void;
212
+ }
213
+
214
+ /**
215
+ * Socket.io transport implementation
216
+ */
217
+ export class SocketIOTransport implements Transport {
218
+ constructor(private socket: Socket) {}
219
+
220
+ async connect(): Promise<void> {
221
+ return new Promise((resolve, reject) => {
222
+ this.socket.once('connect', () => resolve());
223
+ this.socket.once('connect_error', (error) => reject(error));
224
+ this.socket.connect();
225
+ });
226
+ }
227
+
228
+ disconnect(): void {
229
+ this.socket.disconnect();
230
+ }
231
+
232
+ send(event: string, data: unknown): void {
233
+ this.socket.emit(event, data);
234
+ }
235
+
236
+ on(event: string, handler: (data: unknown) => void): void {
237
+ this.socket.on(event, handler);
238
+ }
239
+
240
+ off(event: string, handler: (data: unknown) => void): void {
241
+ this.socket.off(event, handler);
242
+ }
243
+
244
+ once(event: string, handler: (data: unknown) => void): void {
245
+ this.socket.once(event, handler);
246
+ }
247
+ }
package/src/index.ts CHANGED
@@ -37,6 +37,14 @@ export type {
37
37
  export { Network } from './network/Network.js';
38
38
  export { Snapshot } from './network/Snapshot.js';
39
39
 
40
+ // Adapters
41
+ export { SocketIOAdapter } from './adapters/SocketIOAdapter.js';
42
+ export type {
43
+ SocketIOAdapterConfig,
44
+ AdapterContext,
45
+ RoomMetadata,
46
+ } from './adapters/SocketIOAdapter.js';
47
+
40
48
  // Utils
41
49
  export { RingBuffer } from './utils/RingBuffer.js';
42
50
  export {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * InputBuffer Tests
3
+ */
4
+
5
+ import { InputBuffer } from '../../src/client/InputBuffer';
6
+
7
+ describe('InputBuffer', () => {
8
+ let buffer: InputBuffer;
9
+
10
+ beforeEach(() => {
11
+ buffer = new InputBuffer(10); // Small buffer for testing
12
+ });
13
+
14
+ describe('addInput', () => {
15
+ it('should add input and return sequence number', () => {
16
+ const seq = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
17
+ expect(seq).toBe(1);
18
+ expect(buffer.size()).toBe(1);
19
+ });
20
+
21
+ it('should increment sequence numbers', () => {
22
+ const seq1 = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
23
+ const seq2 = buffer.addInput({
24
+ type: 'move',
25
+ targetX: 200,
26
+ targetZ: 100,
27
+ });
28
+ expect(seq2).toBe(seq1 + 1);
29
+ });
30
+
31
+ it('should prevent buffer overflow', () => {
32
+ // Add more than maxSize
33
+ for (let i = 0; i < 15; i++) {
34
+ buffer.addInput({ type: 'move', targetX: i, targetZ: i });
35
+ }
36
+
37
+ expect(buffer.size()).toBeLessThanOrEqual(10);
38
+ });
39
+ });
40
+
41
+ describe('acknowledge', () => {
42
+ it('should acknowledge input', () => {
43
+ const seq = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
44
+ buffer.acknowledge(seq);
45
+
46
+ expect(buffer.getUnackedCount()).toBe(0);
47
+ });
48
+
49
+ it('should remove acknowledged inputs', () => {
50
+ buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
51
+ const seq2 = buffer.addInput({
52
+ type: 'move',
53
+ targetX: 200,
54
+ targetZ: 100,
55
+ });
56
+ buffer.addInput({ type: 'move', targetX: 300, targetZ: 150 });
57
+
58
+ buffer.acknowledge(seq2);
59
+
60
+ // Should remove seq1 and seq2
61
+ expect(buffer.getUnackedCount()).toBe(1);
62
+ });
63
+ });
64
+
65
+ describe('getUnacknowledgedInputs', () => {
66
+ it('should return unacknowledged inputs in order', () => {
67
+ buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
68
+ buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
69
+ buffer.addInput({ type: 'move', targetX: 300, targetZ: 150 });
70
+
71
+ const unacked = buffer.getUnacknowledgedInputs();
72
+ expect(unacked.length).toBe(3);
73
+ expect(unacked[0].seq).toBe(1);
74
+ expect(unacked[1].seq).toBe(2);
75
+ expect(unacked[2].seq).toBe(3);
76
+ });
77
+
78
+ it('should not return acknowledged inputs', () => {
79
+ const seq1 = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
80
+ buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
81
+
82
+ buffer.acknowledge(seq1);
83
+
84
+ const unacked = buffer.getUnacknowledgedInputs();
85
+ expect(unacked.length).toBe(1);
86
+ expect(unacked[0].seq).toBe(2);
87
+ });
88
+ });
89
+
90
+ describe('clear', () => {
91
+ it('should clear all inputs', () => {
92
+ buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
93
+ buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
94
+
95
+ buffer.clear();
96
+
97
+ expect(buffer.size()).toBe(0);
98
+ expect(buffer.isEmpty()).toBe(true);
99
+ });
100
+ });
101
+
102
+ describe('removeOlderThan', () => {
103
+ it('should remove old inputs', () => {
104
+ const now = Date.now();
105
+
106
+ buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
107
+
108
+ // Wait a bit
109
+ setTimeout(() => {
110
+ buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
111
+
112
+ buffer.removeOlderThan(now + 50);
113
+
114
+ expect(buffer.size()).toBe(1);
115
+ }, 100);
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * TimeSync Tests
3
+ */
4
+
5
+ import { TimeSync } from '../../src/client/TimeSync';
6
+
7
+ describe('TimeSync', () => {
8
+ let mockSocket: { emit: jest.Mock; on: jest.Mock };
9
+ let timeSync: TimeSync;
10
+
11
+ beforeEach(() => {
12
+ mockSocket = {
13
+ emit: jest.fn(),
14
+ on: jest.fn(),
15
+ };
16
+ timeSync = new TimeSync(mockSocket as any);
17
+ });
18
+
19
+ describe('start/stop', () => {
20
+ it('should start sending pings', () => {
21
+ timeSync.start();
22
+ expect(mockSocket.emit).toHaveBeenCalledWith('timeSyncPing', {
23
+ clientTime: expect.any(Number),
24
+ });
25
+ });
26
+
27
+ it('should stop sending pings', (done) => {
28
+ timeSync.start();
29
+ const initialCount = mockSocket.emit.mock.calls.length;
30
+ timeSync.stop();
31
+
32
+ // Wait a bit and verify no more pings sent
33
+ setTimeout(() => {
34
+ const finalCount = mockSocket.emit.mock.calls.length;
35
+ expect(finalCount).toBe(initialCount);
36
+ done();
37
+ }, 100);
38
+ });
39
+ });
40
+
41
+ describe('handlePong', () => {
42
+ it('should calculate offset correctly', () => {
43
+ const now = Date.now();
44
+ timeSync.handlePong({
45
+ clientTime: now - 50,
46
+ serverTime: now + 100,
47
+ });
48
+
49
+ const offset = timeSync.getOffset();
50
+ expect(offset).toBeGreaterThan(0);
51
+ });
52
+
53
+ it('should calculate RTT correctly', () => {
54
+ const now = Date.now();
55
+ timeSync.handlePong({
56
+ clientTime: now - 100,
57
+ serverTime: now,
58
+ });
59
+
60
+ const rtt = timeSync.getRTT();
61
+ expect(rtt).toBeGreaterThanOrEqual(100);
62
+ });
63
+
64
+ it('should use median for offset (robust to outliers)', () => {
65
+ // Add multiple samples with one outlier
66
+ const baseTime = Date.now();
67
+
68
+ for (let i = 0; i < 5; i++) {
69
+ timeSync.handlePong({
70
+ clientTime: baseTime - 50,
71
+ serverTime: baseTime + 100, // ~125ms offset
72
+ });
73
+ }
74
+
75
+ // Add outlier
76
+ timeSync.handlePong({
77
+ clientTime: baseTime - 50,
78
+ serverTime: baseTime + 1000, // ~1000ms offset (outlier)
79
+ });
80
+
81
+ const offset = timeSync.getOffset();
82
+ // Should be close to 125, not affected by outlier
83
+ expect(offset).toBeLessThan(500);
84
+ });
85
+ });
86
+
87
+ describe('getServerTime', () => {
88
+ it('should return server time estimate', () => {
89
+ const now = Date.now();
90
+ timeSync.handlePong({
91
+ clientTime: now - 100,
92
+ serverTime: now + 50,
93
+ });
94
+
95
+ const serverTime = timeSync.getServerTime();
96
+ expect(serverTime).toBeGreaterThan(now);
97
+ });
98
+ });
99
+
100
+ describe('reset', () => {
101
+ it('should clear all statistics', () => {
102
+ timeSync.handlePong({
103
+ clientTime: Date.now() - 100,
104
+ serverTime: Date.now(),
105
+ });
106
+
107
+ expect(timeSync.getRTT()).toBeGreaterThan(0);
108
+
109
+ timeSync.reset();
110
+
111
+ expect(timeSync.getRTT()).toBe(0);
112
+ expect(timeSync.getOffset()).toBe(0);
113
+ expect(timeSync.getJitter()).toBe(0);
114
+ });
115
+ });
116
+ });
package/tsup.config.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { defineConfig } from 'tsup';
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts'],
4
+ entry: {
5
+ index: 'src/index.ts',
6
+ 'client/index': 'src/client/index.ts',
7
+ },
5
8
  format: ['esm'],
6
9
  dts: true,
7
10
  sourcemap: true,