Skip to content

Commit

Permalink
First working synchronised net game.
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Jan 19, 2025
1 parent 4026ed2 commit de7e456
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 104 deletions.
36 changes: 36 additions & 0 deletions spec/unit/net/netfloat.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,40 @@ describe('Netfloat', () => {
}
});
});
test('test parsing object arrays', () => {
expect(
fromNetObject(toNetObject({
a: [{
b: 1.2345
}]
}))
).toEqual({
a: [{
b: 1.2345
}]
});
});
test('test parsing multiple values', () => {
expect(
fromNetObject(toNetObject({
a: {
b: 1.2345,
c: 4.5123
},
d: {
e: 5.123,
f: 231.12
}
}))
).toEqual({
a: {
b: 1.2345,
c: 4.5123
},
d: {
e: 5.123,
f: 231.12
}
});
});
});
4 changes: 3 additions & 1 deletion src/@types/matrix-js-sdk.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
GameActionEventType,
GameClientReadyEvent,
GameClientReadyEventType,
GameStateIncrementalEvent,
GameStateIncrementalEventType,
} from "../net/models";

// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
Expand All @@ -32,7 +34,7 @@ declare module "matrix-js-sdk" {
}
export interface TimelineEvents {
[PlayerAckEventType]: PlayerAckEvent["content"];
[GameStateEventType]: FullGameStateEvent["content"];
[GameStateIncrementalEventType]: GameStateIncrementalEvent["content"];
[GameActionEventType]: GameActionEvent["content"];
[GameClientReadyEventType]: GameClientReadyEvent["content"];
}
Expand Down
10 changes: 8 additions & 2 deletions src/entities/phys/physicsEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { AssetPack } from "../../assets";
import type { RecordedEntityState } from "../../state/model";
import { CameraLockPriority } from "../../camera";
import { BehaviorSubject, distinct, Observable } from "rxjs";
import Logger from "../../log";


const log = new Logger('PhysicsEntity');

/**
* Abstract class for any physical object in the world. The
Expand Down Expand Up @@ -143,15 +147,17 @@ export abstract class PhysicsEntity<
}

applyState(state: T): void {
log.debug("Applying state", state);
this.body.setTranslation(state.tra, true);
this.body.setLinvel(state.vel, true);
this.body.setRotation(state.rot, true);
// this.body.setLinvel(state.vel, true);
// this.body.setRotation(state.rot, true);
}

recordState(): T {
const translation = this.body.translation();
const rotation = this.body.rotation();
const linvel = this.body.linvel();
log.debug("Recording state", translation, rotation, linvel);
return {
type: -1,
tra: {
Expand Down
114 changes: 54 additions & 60 deletions src/net/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {
GameStageEventType,
GameConfigEventType,
PlayerAckEventType,
GameStateEventType,
FullGameStateEvent,
ProposedTeam,
GameProposedTeamEventType,
GameActionEventType,
GameClientReadyEventType,
GameStateIncrementalEventType,
GameStateIncrementalEvent,
} from "./models";
import { EventEmitter } from "pixi.js";
import { StateRecordLine } from "../state/model";
Expand Down Expand Up @@ -164,9 +164,28 @@ export class NetGameInstance {
});
}

public async updateGameConfig(newRules: GameRules) {
public async updateGameConfig() {
const teams: Team[] = Object.values(this._proposedTeams.value).map((v) => ({
name: v.name,
flag: v.flagb64,
group: v.group,
playerUserId: v.playerUserId,
uuid: v.uuid,
// Needs to come from rules.
ammo: this._rules.value.ammoSchema,
worms: v.worms.slice(0, v.wormCount).map(
(w) =>
({
name: w,
health: this._rules.value.wormHealth,
maxHealth: this._rules.value.wormHealth,
uuid: globalThis.crypto.randomUUID(),
}) satisfies WormIdentity,
),
}));
await this.client.client.sendStateEvent(this.roomId, GameConfigEventType, {
rules: newRules,
rules: this._rules.value,
teams,
} satisfies GameConfigEvent["content"]);
}

Expand Down Expand Up @@ -216,30 +235,7 @@ export class NetGameInstance {
}

public async startGame() {
const teams: Team[] = Object.values(this._proposedTeams.value).map((v) => ({
name: v.name,
flag: v.flagb64,
group: v.group,
playerUserId: v.playerUserId,
uuid: v.uuid,
// Needs to come from rules.
ammo: this._rules.value.ammoSchema,
worms: v.worms.slice(0, v.wormCount).map(
(w) =>
({
name: w,
health: this._rules.value.wormHealth,
maxHealth: this._rules.value.wormHealth,
uuid: globalThis.crypto.randomUUID(),
}) satisfies WormIdentity,
),
}));
// Initial full state of the game.
await this.client.client.sendStateEvent(this.roomId, GameStateEventType, {
teams,
ents: [],
iteration: 0,
} satisfies FullGameStateEvent["content"]);
this.updateGameConfig();
await this.client.client.sendStateEvent(this.roomId, GameStageEventType, {
stage: GameStage.InProgress,
} satisfies GameStageEvent["content"]);
Expand All @@ -255,8 +251,8 @@ export class NetGameInstance {
} satisfies PlayerAckEvent["content"]);
}

public async sendGameState(data: FullGameStateEvent["content"]) {
await this.client.client.sendEvent(this.roomId, GameStateEventType, data);
public async sendGameState(data: GameStateIncrementalEvent["content"]) {
await this.client.client.sendEvent(this.roomId, GameStateIncrementalEventType, data);
}
}

Expand Down Expand Up @@ -511,17 +507,17 @@ export class NetGameClient extends EventEmitter {
};

if (gameStage === GameStage.InProgress) {
const fullStateEvent = stateEvents.find(
(s) => s.type === GameStateEventType,
) as unknown as FullGameStateEvent;
if (!fullStateEvent) {
const configEvent = stateEvents.find(
(s) => s.type === GameConfigEventType,
) as unknown as GameConfigEvent;
if (!configEvent) {
throw Error("In progress game had no state");
}
return new RunningNetGameInstance(
room,
this,
initialConfig,
fullStateEvent["content"],
configEvent["content"],
);
}

Expand All @@ -530,12 +526,14 @@ export class NetGameClient extends EventEmitter {
}

export class RunningNetGameInstance extends NetGameInstance {
private readonly _gameState: BehaviorSubject<FullGameStateEvent["content"]>;
public readonly gameState: Observable<FullGameStateEvent["content"]>;
private readonly _gameConfig: BehaviorSubject<GameConfigEvent["content"]>;
public readonly gameConfig: Observable<GameConfigEvent["content"]>;
private readonly _gameState: BehaviorSubject<GameStateIncrementalEvent["content"]>;
public readonly gameState: Observable<GameStateIncrementalEvent["content"]>;
public readonly player: MatrixStateReplay;

public get gameStateImmediate() {
return this._gameState.value;
public get gameConfigImmediate() {
return this._gameConfig.value;
}

public get rules() {
Expand All @@ -546,41 +544,37 @@ export class RunningNetGameInstance extends NetGameInstance {
room: Room,
client: NetGameClient,
private readonly initialConfig: NetGameConfiguration,
currentState: FullGameStateEvent["content"],
currentState: GameConfigEvent["content"],
) {
super(room, client, initialConfig);
this._gameState = new BehaviorSubject(currentState);
this._gameConfig = new BehaviorSubject(currentState);
this.gameConfig = this._gameConfig.asObservable();
this._gameState = new BehaviorSubject({ iteration: 0, ents: [] as GameStateIncrementalEvent["content"]["ents"]});
this.gameState = this._gameState.asObservable();
client.client.on(RoomStateEvent.Events, (event, state) => {
if (state.roomId !== this.roomId) {
return;
}
this.player = new MatrixStateReplay();
room.on(RoomStateEvent.Events, (event) => {
const stateKey = event.getStateKey();
const type = event.getType();
if (stateKey && type === GameStateEventType && event.getSender() !== this.myUserId) {
logger.info("Got new gme ")
const content = fromNetObject(event.getContent() as FullGameStateEvent["content"]);
this._gameState.next(content as FullGameStateEvent["content"]);
}
if (type === GameStateEventType && event.getSender() !== this.myUserId) {
logger.info("Got new gme ")
const content = fromNetObject(event.getContent() as FullGameStateEvent["content"]);
this._gameState.next(content as FullGameStateEvent["content"]);
if (stateKey && type === GameConfigEventType && event.getSender() !== this.myUserId) {
const content = fromNetObject(event.getContent() as GameConfigEvent["content"]);
this._gameConfig.next(content as GameConfigEvent["content"]);
}
});
this.player = new MatrixStateReplay();
client.client.on(RoomEvent.Timeline, (event, r) => {
if (r?.roomId !== this.roomId) {
return;
}
room.on(RoomEvent.Timeline, (event) => {
const type = event.getType();
// Filter our won events out.
if (
event.getType() === GameActionEventType &&
type === GameActionEventType &&
!event.isState() &&
event.getSender() !== this.myUserId
) {
void this.player.handleEvent(event.getContent());
}
if (type === GameStateIncrementalEventType && event.getSender() !== this.myUserId) {
const content = fromNetObject(event.getContent() as GameStateIncrementalEvent["content"]);
logger.info("Got new incremental event", content)
this._gameState.next(content as GameStateIncrementalEvent["content"]);
}
});
}

Expand Down
17 changes: 5 additions & 12 deletions src/net/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,16 @@ export interface GameActionEvent {
};
}

export const GameStateEventType = "uk.half-shot.wormgine.game_state";
export interface FullGameStateEvent {
type: typeof GameStateEventType;
export const GameStateIncrementalEventType = "uk.half-shot.wormgine.game_state";

export interface GameStateIncrementalEvent {
type: typeof GameStateIncrementalEventType;
content: {
teams?: Team[];
iteration: number;
ents: (RecordedEntityState & { uuid: string })[];
};
}

export interface GameControlEvent {
type: typeof GameStateEventType;
content: {
input: InputKind;
entity: number; // ?
};
}

export interface BitmapEvent {
type: "uk.half-shot.uk.wormgine.bitmap";
content: {
Expand Down Expand Up @@ -87,6 +79,7 @@ export interface GameConfigEvent {
type: typeof GameConfigEventType;
content: {
rules: GameRules;
teams: Team[];
// Need to decide on some config.
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/net/netGameWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class NetGameWorld extends GameWorld {
) {
super(rapierWorld, ticker);
instance.gameState.subscribe(s => {
logger.info("Game state update");
logger.info("Remote state update", s.iteration);
if (this.broadcasting) {
return;
}
Expand Down
23 changes: 14 additions & 9 deletions src/net/netfloat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,29 @@ export function toNetObject(
);
}

export function fromNetObject(o: Record<string, unknown>): unknown {
export function fromNetObject(o: unknown): unknown {
function isNF(v: unknown): v is NetworkFloat {
return (
(v !== null && typeof v === "object" && "nf" in v && v.nf === true) ||
false
);
}

if (typeof o !== "object" || o === null) {
return o;
}

if (Array.isArray(o)) {
return o.map(o => fromNetObject(o));
}

if (isNF(o)) {
return fromNetworkFloat(o);
}

return Object.fromEntries(
Object.entries(o).map<[string, unknown | NetworkFloat]>(([key, v]) => {
if (isNF(v)) {
return [key, fromNetworkFloat(v)];
} else if (Array.isArray(v)) {
return [key, v.map((v2) => (isNF(v2) ? fromNetworkFloat(v2) : v2))];
} else if (typeof v === "object") {
return [key, fromNetObject(v as Record<string, unknown>)];
}
return [key, v];
return [key, fromNetObject(v)];
}),
);
}
Loading

0 comments on commit de7e456

Please sign in to comment.