Initial Commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* State management functionality for GameBitPacker
|
||||
*/
|
||||
|
||||
import type { BitPackerSchema } from "@suaveplan/types";
|
||||
import type { StateDelta } from "../types.js";
|
||||
import type { GameStateManagerInterface } from "./state-manager.types.js";
|
||||
|
||||
export class GameStateManager<T extends BitPackerSchema>
|
||||
implements GameStateManagerInterface<T>
|
||||
{
|
||||
private currentState: unknown | undefined;
|
||||
private lastSentState: unknown | undefined;
|
||||
private sequence = 0;
|
||||
private lastReceivedSequence = -1;
|
||||
public static readonly MAX_SEQUENCE = 15; // 4 bits for sequence
|
||||
|
||||
/**
|
||||
* Update the current state with new data
|
||||
*/
|
||||
public update(data: unknown, isInitialState = false): void {
|
||||
this.currentState = this.deepClone(data);
|
||||
|
||||
if (isInitialState) {
|
||||
this.lastSentState = this.deepClone(data);
|
||||
this.sequence = 0;
|
||||
this.lastReceivedSequence = 0; // Start at 0 to receive sequence 1
|
||||
} else {
|
||||
this.sequence = (this.sequence + 1) % GameStateManager.MAX_SEQUENCE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state
|
||||
*/
|
||||
public getState(): unknown | undefined {
|
||||
return this.currentState
|
||||
? this.deepClone(this.currentState)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last sent state
|
||||
*/
|
||||
public getLastSentState(): unknown | undefined {
|
||||
return this.lastSentState
|
||||
? this.deepClone(this.lastSentState)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last sent state
|
||||
*/
|
||||
public updateLastSentState(state: unknown): void {
|
||||
this.lastSentState = this.deepClone(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current sequence number
|
||||
*/
|
||||
public getSequence(): number {
|
||||
return this.sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last received sequence number
|
||||
*/
|
||||
public getLastReceivedSequence(): number {
|
||||
return this.lastReceivedSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update last received sequence
|
||||
*/
|
||||
public updateLastReceivedSequence(sequence: number): void {
|
||||
this.lastReceivedSequence = sequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset state
|
||||
*/
|
||||
public reset(): void {
|
||||
this.currentState = undefined;
|
||||
this.lastSentState = undefined;
|
||||
this.sequence = 0;
|
||||
this.lastReceivedSequence = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if initialized
|
||||
*/
|
||||
public isInitialized(): boolean {
|
||||
return this.currentState !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute delta between two states
|
||||
*/
|
||||
public computeDelta(
|
||||
current: unknown,
|
||||
previous: unknown
|
||||
): StateDelta<unknown> {
|
||||
const delta: Record<string, unknown> = {};
|
||||
const changedPaths: string[] = [];
|
||||
let hasChanges = false;
|
||||
|
||||
const compareAndAdd = (curr: unknown, prev: unknown, path: string) => {
|
||||
if (curr !== prev) {
|
||||
hasChanges = true;
|
||||
changedPaths.push(path);
|
||||
this.setNestedValue(delta, path, curr);
|
||||
}
|
||||
};
|
||||
|
||||
this.traverseObject(current, previous, "", compareAndAdd);
|
||||
|
||||
return { hasChanges, delta, changedPaths };
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone an object
|
||||
*/
|
||||
private deepClone<U>(obj: U): U {
|
||||
if (obj === null || typeof obj !== "object") return obj;
|
||||
if (obj instanceof Date) return new Date(obj.getTime()) as unknown as U;
|
||||
if (Array.isArray(obj)) {
|
||||
const clonedArr: unknown[] = [];
|
||||
for (const item of obj) {
|
||||
clonedArr.push(this.deepClone(item));
|
||||
}
|
||||
return clonedArr as unknown as U;
|
||||
}
|
||||
if (obj instanceof Object) {
|
||||
const clonedObj: Record<string, unknown> = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
clonedObj[key] = this.deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj as U;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse object recursively
|
||||
*/
|
||||
private traverseObject(
|
||||
current: unknown,
|
||||
previous: unknown,
|
||||
path: string,
|
||||
callback: (curr: unknown, prev: unknown, path: string) => void
|
||||
): void {
|
||||
if (typeof current !== "object" || current === null) {
|
||||
callback(current, previous, path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
const prevArray = Array.isArray(previous) ? previous : [];
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
this.traverseObject(
|
||||
current[i],
|
||||
prevArray[i],
|
||||
path ? `${path}[${i}]` : `[${i}]`,
|
||||
callback
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const prevObj =
|
||||
typeof previous === "object" && previous !== null
|
||||
? previous
|
||||
: {};
|
||||
for (const key in current) {
|
||||
if (Object.hasOwn(current, key)) {
|
||||
this.traverseObject(
|
||||
(current as Record<string, unknown>)[key],
|
||||
(prevObj as Record<string, unknown>)[key],
|
||||
path ? `${path}.${key}` : key,
|
||||
callback
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set nested value in object using path
|
||||
*/
|
||||
private setNestedValue(obj: unknown, path: string, value: unknown): void {
|
||||
const keys = path.split(/\.|\[|\]/).filter((k) => k);
|
||||
let current = obj as Record<string, unknown>;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const key = keys[i];
|
||||
if (!key) continue;
|
||||
if (!(key in current)) {
|
||||
const nextKey = keys[i + 1];
|
||||
current[key] = nextKey && /^\d+$/.test(nextKey) ? [] : {};
|
||||
}
|
||||
current = current[key] as Record<string, unknown>;
|
||||
}
|
||||
|
||||
const lastKey = keys[keys.length - 1];
|
||||
if (lastKey) {
|
||||
current[lastKey] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user