From c3ca73618449cfd753b79787accb1989cd040c5d Mon Sep 17 00:00:00 2001 From: DogLooksGood Date: Tue, 27 Feb 2024 13:02:20 +0800 Subject: [PATCH] Fixes to make simple game works again --- api/src/effect.rs | 2 - core/src/context.rs | 17 ++-- js/sdk-core/src/base-client.ts | 140 ++++++++++++++------------ js/sdk-core/src/connection.ts | 13 ++- js/sdk-core/src/effect.ts | 5 - js/sdk-core/src/game-context.ts | 15 ++- js/sdk-core/src/handler.ts | 2 +- js/sdk-core/src/sub-client.ts | 2 +- js/sdk-core/src/types.ts | 6 +- test/src/client_helpers.rs | 2 +- test/src/handler_helpers.rs | 10 -- transactor/src/component/event_bus.rs | 2 +- 12 files changed, 116 insertions(+), 100 deletions(-) diff --git a/api/src/effect.rs b/api/src/effect.rs index 38f962f7..d1c19dd8 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -208,7 +208,6 @@ pub struct Effect { pub init_random_states: Vec, pub revealed: HashMap>, pub answered: HashMap, - pub is_checkpoint: bool, pub checkpoint: Option>, pub settles: Vec, pub handler_state: Option>, @@ -463,7 +462,6 @@ mod tests { error: Some(HandleError::NoEnoughPlayers), allow_exit: true, transfers: vec![], - is_checkpoint: false, checkpoint: None, launch_sub_games: vec![], bridge_events: vec![], diff --git a/core/src/context.rs b/core/src/context.rs index 0b93c769..5566b684 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -188,7 +188,7 @@ impl GameContext { .as_ref() .ok_or(Error::GameNotServed)?; - let nodes = game_account + let mut nodes: Vec = game_account .servers .iter() .map(|s| { @@ -203,13 +203,12 @@ impl GameContext { ) }) .collect(); - - let players = game_account - .players - .iter() - .filter(|p| p.access_version <= game_account.access_version) - .map(|p| GamePlayer::new(p.access_version, p.balance, p.position)) - .collect(); + let mut players = vec![]; + game_account.players.iter() + .for_each(|p| { + players.push(GamePlayer::new(p.access_version, p.balance, p.position)); + nodes.push(Node::new_pending(p.addr.clone(), p.access_version, ClientMode::Player)) + }); Ok(Self { game_addr: game_account.addr.clone(), @@ -234,6 +233,7 @@ impl GameContext { } pub fn id_to_addr(&self, id: u64) -> Result { + println!("Nodes: {:?}", self.nodes); self.nodes .iter() .find(|n| n.id == id) @@ -716,7 +716,6 @@ impl GameContext { init_random_states: Vec::new(), revealed, answered, - is_checkpoint: false, checkpoint: None, settles: Vec::new(), handler_state: Some(self.handler_state.clone()), diff --git a/js/sdk-core/src/base-client.ts b/js/sdk-core/src/base-client.ts index ed49d055..8a4ce9e1 100644 --- a/js/sdk-core/src/base-client.ts +++ b/js/sdk-core/src/base-client.ts @@ -23,7 +23,7 @@ import { PlayerConfirming } from './tx-state'; import { Client } from './client'; import { Custom, GameEvent, ICustomEvent } from './events'; import { DecryptionCache } from './decryption-cache'; -import { ConnectionStateCallbackFunction, ErrorCallbackFunction, ErrorKind, EventCallbackFunction, GameInfo, LoadProfileCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; +import { ConnectionStateCallbackFunction, ErrorCallbackFunction, ErrorKind, EventCallbackFlags, EventCallbackFunction, GameInfo, LoadProfileCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; const MAX_RETRIES = 3; @@ -194,7 +194,7 @@ export class BaseClient { console.log('Game context created,', this.__gameContext); for (const p of gameAccount.players) this.__onLoadProfile(p.accessVersion, p.addr); await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); - await this.__initializeState(initAccount); + await this.__initializeState(initAccount, true); } catch (e) { console.error('Attaching game failed', e); this.__invokeErrorCallback('attach-failed') @@ -213,17 +213,20 @@ export class BaseClient { } } - async __invokeEventCallback(event: GameEvent | undefined, isHistory: boolean) { + async __invokeEventCallback(event: GameEvent | undefined, flags: EventCallbackFlags) { const snapshot = new GameContextSnapshot(this.__gameContext); const state = this.__gameContext.handlerState; - this.__onEvent(snapshot, state, event, isHistory); + this.__onEvent(snapshot, state, event, flags); } - async __initializeState(initAccount: InitAccount): Promise { + async __initializeState(initAccount: InitAccount, isHistory: boolean): Promise { console.log('Initialize state with', initAccount); - await this.__handler.initState(this.__gameContext, initAccount); + const effects = await this.__handler.initState(this.__gameContext, initAccount); console.log('State initialized'); - await this.__invokeEventCallback(undefined, true); + await this.__invokeEventCallback(undefined, { + isCheckpoint: effects.checkpoint !== undefined, + isHistory, + }); } async __getGameAccount(): Promise { @@ -262,6 +265,71 @@ export class BaseClient { } } + async __handleBroadcastFrameEvent(frame: BroadcastFrameEvent) { + + const { event, timestamp } = frame; + console.groupCollapsed('Handle event: ' + event.kind() + ' at timestamp: ' + new Date(Number(timestamp)).toLocaleString()); + console.log('Event: ', event); + let state: Uint8Array | undefined; + let err: ErrorKind | undefined; + let effects: EventEffects | undefined; + const isHistory = frame.remain !== 0; + + try { // For log group + try { + this.__gameContext.prepareForNextEvent(timestamp); + console.log('Game context before:', this.__gameContext); + effects = await this.__handler.handleEvent(this.__gameContext, event); + console.log('Game context after:', this.__gameContext); + console.log('Output:', effects); + state = this.__gameContext.handlerState; + const sha = await sha256(this.__gameContext.handlerState) + console.log('SHA:', sha); + if (sha !== frame.stateSha) { + const remoteState = await this.__connection.getState(); + console.log('Remote state:', remoteState); + console.log('Local state:', state); + err = 'state-sha-mismatch' + } + } catch (e: any) { + console.error(e); + err = 'handle-event-error'; + } + + if (!err) { + await this.__invokeEventCallback(event, { + isCheckpoint: effects?.checkpoint !== undefined, + isHistory, + }); + } + + if ((!err) && effects?.checkpoint) { + console.log('Rebuild state for checkpoint:', effects.checkpoint); + const initData = this.__gameContext.initData; + if (initData === undefined) { + err = 'state-sha-mismatch' + } else { + const initAccount = new InitAccount({ + entryType: this.__gameContext.entryType, + data: initData, + players: this.__gameContext.players, + checkpoint: effects.checkpoint, + maxPlayers: this.__gameContext.maxPlayers, + }); + await this.__initializeState(initAccount, isHistory); + } + } + + if (err) { + this.__invokeErrorCallback(err, state); + throw new Error(`An error occurred in event loop: ${err}`); + } + + } finally { + console.groupEnd() + } + } + async __handleBroadcastFrame(frame: BroadcastFrame) { if (frame instanceof BroadcastFrameMessage) { console.groupCollapsed('Receive message broadcast'); @@ -304,61 +372,7 @@ export class BaseClient { console.groupEnd(); } } else if (frame instanceof BroadcastFrameEvent) { - const { event, timestamp } = frame; - console.groupCollapsed('Handle event: ' + event.kind() + ' at timestamp: ' + new Date(Number(timestamp)).toLocaleString()); - console.log('Event: ', event); - let state: Uint8Array | undefined; - let err: ErrorKind | undefined; - let effects: EventEffects | undefined; - - try { // For log group - - try { - this.__gameContext.prepareForNextEvent(timestamp); - effects = await this.__handler.handleEvent(this.__gameContext, event); - state = this.__gameContext.handlerState; - const sha = await sha256(this.__gameContext.handlerState) - if (sha !== frame.stateSha) { - const remoteState = await this.__connection.getState(); - console.log('Remote state:', remoteState); - console.log('Local state:', state); - err = 'state-sha-mismatch' - } - } catch (e: any) { - console.error(e); - err = 'handle-event-error'; - } - - if (!err) { - await this.__invokeEventCallback(event, frame.remain !== 0); - } - - if ((!err) && effects?.checkpoint) { - console.log('Rebuild state for checkpoint'); - const initData = this.__gameContext.initData; - if (initData === undefined) { - err = 'state-sha-mismatch' - } else { - const initAccount = new InitAccount({ - entryType: this.__gameContext.entryType, - data: initData, - players: this.__gameContext.players, - checkpoint: effects.checkpoint, - maxPlayers: this.__gameContext.maxPlayers, - }); - await this.__initializeState(initAccount); - await this.__invokeEventCallback(event, frame.remain !== 0); - } - } - - if (err) { - this.__invokeErrorCallback(err, state); - throw new Error(`An error occurred in event loop: ${err}`); - } - - } finally { - console.groupEnd() - } + await this.__handleBroadcastFrameEvent(frame); } } @@ -373,7 +387,7 @@ export class BaseClient { const initAccount = InitAccount.createFromGameAccount(gameAccount); this.__gameContext.applyCheckpoint(gameAccount.checkpointAccessVersion, this.__gameContext.settleVersion); await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); - await this.__initializeState(initAccount); + await this.__initializeState(initAccount, true); } else if (state === 'connected') { if (this.__onConnectionState !== undefined) { this.__onConnectionState('connected') diff --git a/js/sdk-core/src/connection.ts b/js/sdk-core/src/connection.ts index d4bb50a3..cd19ab86 100644 --- a/js/sdk-core/src/connection.ts +++ b/js/sdk-core/src/connection.ts @@ -97,6 +97,7 @@ export class BroadcastFrameEvent extends BroadcastFrame { constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameEvent); } } @@ -109,6 +110,7 @@ export class BroadcastFrameMessage extends BroadcastFrame { constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameMessage); } } @@ -119,6 +121,7 @@ export class BroadcastFrameTxState extends BroadcastFrame { constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameTxState); } } @@ -135,6 +138,7 @@ export class BroadcastFrameSync extends BroadcastFrame { constructor(fields: any) { super(); Object.assign(this, fields) + Object.setPrototypeOf(this, BroadcastFrameSync); } } @@ -339,11 +343,14 @@ export class Connection implements IConnection { await this.waitSocketReady(); this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); while (true) { - if (this.streamMessageQueue.length > 0) { - yield this.streamMessageQueue.shift()!; + while (this.streamMessageQueue.length > 0) { + yield this.streamMessageQueue.shift(); + } + if (this.streamResolve === undefined) { + this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); + yield this.streamMessagePromise; } else { yield this.streamMessagePromise; - this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); } } } diff --git a/js/sdk-core/src/effect.ts b/js/sdk-core/src/effect.ts index 4200515e..1bcc25f3 100644 --- a/js/sdk-core/src/effect.ts +++ b/js/sdk-core/src/effect.ts @@ -201,9 +201,6 @@ export class Effect { @field(map('usize', 'string')) answered!: Map; - @field('bool') - isCheckpoint!: boolean; - @field(option('u8-array')) checkpoint!: Uint8Array | undefined; @@ -255,7 +252,6 @@ export class Effect { const releases: Release[] = []; const reveals: Reveal[] = []; const initRandomStates: RandomSpec[] = []; - const isCheckpoint = false; const checkpoint = undefined; const settles: Settle[] = []; const handlerState = context.handlerState; @@ -281,7 +277,6 @@ export class Effect { initRandomStates, revealed, answered, - isCheckpoint, checkpoint, settles, handlerState, diff --git a/js/sdk-core/src/game-context.ts b/js/sdk-core/src/game-context.ts index d8d1bcfb..8038096a 100644 --- a/js/sdk-core/src/game-context.ts +++ b/js/sdk-core/src/game-context.ts @@ -407,6 +407,8 @@ export class GameContext { } applyEffect(effect: Effect): EventEffects { + console.log('Apply effect:', effect); + if (effect.startGame) { this.startGame(); } else if (effect.stopGame) { @@ -435,26 +437,33 @@ export class GameContext { let settles: Settle[] = []; - if (effect.isCheckpoint) { + if (effect.checkpoint !== undefined) { + // Reset random states this.randomStates = []; this.decisionStates = []; + + // Sort settles and track player states + settles.push(...effect.settles); settles = effect.settles; settles = settles.sort((s1, s2) => s1.compare(s2)); for (let s of settles) { if (s.op instanceof SettleAdd) { - this.playerSubBalance(s.id, s.op.amount); - } else if (s.op instanceof SettleSub) { this.playerAddBalance(s.id, s.op.amount); + } else if (s.op instanceof SettleSub) { + this.playerSubBalance(s.id, s.op.amount); } else if (s.op instanceof SettleEject) { this.removePlayer(s.id); } } + this.checkpoint = effect.checkpoint; this.status = 'idle'; } if (effect.handlerState !== undefined) { this.handlerState = effect.handlerState; + } else { + console.warn('Effect has no handler state'); } this.subGames.push(...effect.launchSubGames); diff --git a/js/sdk-core/src/handler.ts b/js/sdk-core/src/handler.ts index f1a79350..f458df49 100644 --- a/js/sdk-core/src/handler.ts +++ b/js/sdk-core/src/handler.ts @@ -87,7 +87,7 @@ export class Handler implements IHandler { const addr = context.idToAddr(sender); context.lock(addr, randomId, ciphertextsAndDigests); } else if (event instanceof Join) { - // No op here + event.players.forEach(p => context.addPlayer(p)); } else if (event instanceof Leave) { if (!context.allowExit) { throw new Error('Leave is not allowed') diff --git a/js/sdk-core/src/sub-client.ts b/js/sdk-core/src/sub-client.ts index d28e8a06..a8593080 100644 --- a/js/sdk-core/src/sub-client.ts +++ b/js/sdk-core/src/sub-client.ts @@ -58,7 +58,7 @@ export class SubClient extends BaseClient { await this.__client.attachGame(); sub = this.__connection.subscribeEvents(); await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); - await this.__initializeState(this.__initAccount); + await this.__initializeState(this.__initAccount, true); } catch (e) { console.error('Attaching game failed', e); throw e; diff --git a/js/sdk-core/src/types.ts b/js/sdk-core/src/types.ts index 30ed076e..140d6719 100644 --- a/js/sdk-core/src/types.ts +++ b/js/sdk-core/src/types.ts @@ -30,12 +30,16 @@ export type PlayerProfileWithPfp = { nick: string, }; +export type EventCallbackFlags = { + isHistory: boolean, + isCheckpoint: boolean, +} export type EventCallbackFunction = ( context: GameContextSnapshot, state: Uint8Array, event: GameEvent | undefined, - isHistory: boolean, + flags: EventCallbackFlags, ) => void; export type ErrorKind = diff --git a/test/src/client_helpers.rs b/test/src/client_helpers.rs index 4844ac2c..7f91cdeb 100644 --- a/test/src/client_helpers.rs +++ b/test/src/client_helpers.rs @@ -182,7 +182,7 @@ mod tests { #[tokio::test] async fn test_dummy_connection() -> Result<()> { let conn = DummyConnection::default(); - let event = Event::GameStart { access_version: 1 }; + let event = Event::GameStart; conn.submit_event( "", SubmitEventParams { diff --git a/test/src/handler_helpers.rs b/test/src/handler_helpers.rs index 8ad81318..53b5117d 100644 --- a/test/src/handler_helpers.rs +++ b/test/src/handler_helpers.rs @@ -1,6 +1,5 @@ use std::mem::swap; -use race_api::effect::Effect; use race_api::engine::GameHandler; use race_api::error::Result; use race_api::event::Event; @@ -11,13 +10,6 @@ use race_encryptor::Encryptor; use crate::client_helpers::TestClient; -fn parse_effect_checkpoint(effect: &mut Effect) -> Result<()> { - if effect.is_checkpoint { - effect.__set_checkpoint_raw(vec![]); - } - Ok(()) -} - /// A wrapped handler for testing /// This handler includes the general event handling, which is necessary for integration test. pub struct TestHandler @@ -45,8 +37,6 @@ impl TestHandler { general_handle_event(&mut new_context, event, &encryptor)?; let mut effect = new_context.derive_effect(); self.handler.handle_event(&mut effect, event.to_owned())?; - parse_effect_checkpoint::(&mut effect)?; - new_context.apply_effect(effect)?; swap(context, &mut new_context); Ok(()) } diff --git a/transactor/src/component/event_bus.rs b/transactor/src/component/event_bus.rs index 3ad17b8d..adc7dc96 100644 --- a/transactor/src/component/event_bus.rs +++ b/transactor/src/component/event_bus.rs @@ -129,7 +129,7 @@ mod tests { #[async_trait] impl Component for TestProducer { - fn name(&self) -> &str { + fn name() -> &'static str { "Test Producer" }