Skip to content

Commit

Permalink
Reworking network state engine
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Jan 19, 2025
1 parent 630d282 commit 11211b4
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 31 deletions.
6 changes: 6 additions & 0 deletions src/entities/phys/physicsEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export abstract class PhysicsEntity<
this.physObject.body.applyImpulse(force, true);
}

applyState(state: T): void {
this.body.setTranslation(state.tra, 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();
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export function Menu({
} else if (currentMenu === GameMenu.Lobby) {
const onOpenIngame = (gameInstance: RunningNetGameInstance) => {
// TODO: Hardcoded level.
onNewGame("netGameTest", gameInstance, "levels_testing");
onNewGame("netGame", gameInstance, "levels_testing");
};
if (!currentLobbyId) {
throw Error("Current Lobby ID must be set!");
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/components/menus/lobby.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export function ActiveLobby({
const viableToStart = useMemo(
() =>
gameInstance.isHost &&
members.length >= 2 &&
members.length >= 1 &&
proposedTeams.length >= 2 &&
Object.keys(
proposedTeams.reduce<Partial<Record<TeamGroup, number>>>(
Expand Down
19 changes: 12 additions & 7 deletions src/net/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { BehaviorSubject, map, Observable } from "rxjs";
import Logger from "../log";
import { StoredTeam, WORMGINE_STORAGE_KEY_CLIENT_CONFIG } from "../settings";
import { MatrixStateReplay } from "../state/player";
import { toNetObject, toNetworkFloat } from "./netfloat";
import { fromNetObject, toNetObject, toNetworkFloat } from "./netfloat";

const logger = new Logger("NetClient");

Expand Down Expand Up @@ -69,7 +69,7 @@ export class NetGameInstance {
private readonly _proposedTeams: BehaviorSubject<
Record<string, ProposedTeam>
>;
public readonly proposedTeams: Observable<ProposedTeam[]>;
public readonly proposedTeams: Observable<ProposedTeam[]>;
private readonly _rules: BehaviorSubject<GameRules>;
public readonly proposedRules: Observable<GameRules>;

Expand Down Expand Up @@ -239,7 +239,6 @@ export class NetGameInstance {
teams,
ents: [],
iteration: 0,
bitmap_hash: "",
} satisfies FullGameStateEvent["content"]);
await this.client.client.sendStateEvent(this.roomId, GameStageEventType, {
stage: GameStage.InProgress,
Expand Down Expand Up @@ -531,7 +530,7 @@ export class NetGameClient extends EventEmitter {
}

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

Expand All @@ -558,9 +557,15 @@ export class RunningNetGameInstance extends NetGameInstance {
}
const stateKey = event.getStateKey();
const type = event.getType();
if (stateKey && type === GameStateEventType) {
const content = event.getContent() as FullGameStateEvent["content"];
this._gameState.next(content);
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"]);
}
});
this.player = new MatrixStateReplay();
Expand Down
6 changes: 3 additions & 3 deletions src/net/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InputKind } from "../input";
import { GameRules } from "../logic/gamestate";
import { Team, TeamGroup } from "../logic/teams";
import { StoredTeam } from "../settings";
import { RecordedEntityState } from "../state/model";

export interface EntityDescriptor {
pos: { x: number; y: number };
Expand Down Expand Up @@ -41,10 +42,9 @@ export const GameStateEventType = "uk.half-shot.wormgine.game_state";
export interface FullGameStateEvent {
type: typeof GameStateEventType;
content: {
teams?: Team[];
iteration: number;
bitmap_hash: string;
ents: EntityDescriptor[];
teams: Team[];
ents: (RecordedEntityState & { uuid: string })[];
};
}

Expand Down
39 changes: 33 additions & 6 deletions src/net/netGameWorld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,72 @@ import { RecordedEntityState } from "../state/model";
import { PhysicsEntity } from "../entities/phys/physicsEntity";
import Logger from "../log";
import { RunningNetGameInstance } from "./client";
import { toNetObject } from "./netfloat";
import { IPhysicalEntity } from "../entities/entity";

Check failure on line 9 in src/net/netGameWorld.ts

View workflow job for this annotation

GitHub Actions / ci

'IPhysicalEntity' is defined but never used. Allowed unused vars must match /^_/u

const TICK_EVERY_MS = 200;
const TICK_EVERY_MS = 350;

const logger = new Logger("NetGameWorld");

export class NetGameWorld extends GameWorld {
private broadcasting = false;
private msSinceLastTick = 0;
private entStateHash = new Map<string, string>();
private iteration = 0;

constructor(
rapierWorld: RAPIER.World,
ticker: Ticker,
private readonly instance: RunningNetGameInstance,
) {
super(rapierWorld, ticker);
instance.gameState.subscribe(s => {
logger.info("Game state update");
if (this.broadcasting) {
return;
}
s.ents.forEach(e => {
const ent = this.entities.get(e.uuid);
if (!ent) {
logger.warning(`Could not find entity ${e.uuid} but got state update`);
return;
}
(ent as PhysicsEntity).applyState(e);
});
})
}

public setBroadcasting(isBroadcasting: boolean) {
this.broadcasting = isBroadcasting;
if (this.broadcasting) {
if (this.broadcasting === isBroadcasting) {
return;
}
if (isBroadcasting) {
logger.info("Enabled broadcasting from this client");
this.ticker.add(this.onTick, undefined, UPDATE_PRIORITY.HIGH);
} else {
logger.info("Disabled broadcasting from this client");
this.ticker.remove(this.onTick);
}
this.broadcasting = isBroadcasting;
}

public collectEntityState() {
const state: (RecordedEntityState & { uuid: string })[] = [];
for (const [uuid, ent] of this.entities.entries()) {
if ("recordState" in ent === false) {
// Not recordable.
break;
continue;
}
const data = (ent as PhysicsEntity).recordState();
const hashData = JSON.stringify(data);
if (this.entStateHash.get(uuid) === hashData) {
// No updates - skip.
break;
continue;
}
this.entStateHash.set(uuid, hashData);
state.push({
uuid,
...data,
...toNetObject(data as any) as any,

Check failure on line 73 in src/net/netGameWorld.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type

Check failure on line 73 in src/net/netGameWorld.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected any. Specify a different type
});
}
return state;
Expand All @@ -63,12 +83,19 @@ export class NetGameWorld extends GameWorld {
}
this.msSinceLastTick -= TICK_EVERY_MS;

logger.debug("Tick!");
// Fetch all entities and look for state changes.
const collectedState = this.collectEntityState();
if (collectedState.length === 0) {
// Nothing to send, skip.
return;
}
logger.info(`Found ${collectedState.length} entity updates to send`);
this.instance.sendGameState({
iteration: ++this.iteration,
ents: collectedState,
}).catch((ex) => {

Check failure on line 97 in src/net/netGameWorld.ts

View workflow job for this annotation

GitHub Actions / ci

'ex' is defined but never used. Allowed unused args must match /^_/u
logger.warning("Failed to send game state");
})
};
}
33 changes: 20 additions & 13 deletions src/scenarios/netGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,16 @@ export default async function runScenario(game: Game) {
wormInst.replayAim(dir, parseFloat(angle));
});

player.on("wormActionMove", ({ id, action, cycles }) => {
const wormInst = wormInstances.get(id);
if (!wormInst) {
throw Error("Worm not found");
}
if (wormInst instanceof RemoteWorm === false) {
return;
}
wormInst.replayMovement(action, cycles);
});
// player.on("wormActionMove", ({ id, action, cycles }) => {
// const wormInst = wormInstances.get(id);
// if (!wormInst) {
// throw Error("Worm not found");
// }
// if (wormInst instanceof RemoteWorm === false) {
// return;
// }
// wormInst.replayMovement(action, cycles);
// });

player.on("wormActionFire", ({ id, duration }) => {
const wormInst = wormInstances.get(id);
Expand All @@ -118,9 +118,11 @@ export default async function runScenario(game: Game) {
level.terrain.destructible,
);

const initialTeams = gameInstance.gameStateImmediate.teams;


const initialTeams = gameInstance.gameStateImmediate.teams!;

for (const team of gameInstance.gameStateImmediate.teams) {
for (const team of initialTeams) {
if (team.flag) {
Assets.add({ alias: `team-flag-${team.name}`, src: team.flag });
await Assets.load(`team-flag-${team.name}`);
Expand Down Expand Up @@ -350,8 +352,13 @@ export default async function runScenario(game: Game) {
}),
)
.subscribe(([roundState, worm]) => {
if (worm?.team.playerUserId === gameInstance.myUserId && roundState === RoundState.Preround) {
world.setBroadcasting(true);
} else if (roundState === RoundState.Finished) {
world.setBroadcasting(false);
}
log.info("Round state sub fired for", roundState, worm);
if (worm === undefined && RoundState.Finished && gameInstance.isHost) {
if (worm === undefined && roundState === RoundState.Finished && gameInstance.isHost) {
log.info("Starting first round as worm was undefined");
gameState.advanceRound();
return;
Expand Down

0 comments on commit 11211b4

Please sign in to comment.