import Redux from 'redux' ; import { MultiplayerEvent , MultiplayerEventBody , StatusEvent } from 'shared/multiplayer/Events' ; import { toClientKey } from 'shared/multiplayer/Session' ; import { handleFetchErrors } from 'shared/requests' ; import { Expansion } from 'shared/schema/Constants' ; import { openSnackbar } from '../actions/Snackbar' ; import { MULTIPLAYER_SETTINGS } from '../Constants' ; import { logEvent } from '../Logging' ; import { ConnectionHandler , getMultiplayerConnection } from '../multiplayer/Connection' ; import { getMultiplayerAction } from '../multiplayer/Remoteify' ; import { AppStateWithHistory , MultiplayerSessionMeta , MultiplayerState , UserState } from '../reducers/StateTypes' ; import { LocalAction , MultiplayerClientStatus , MultiplayerConnectedAction , MultiplayerMultiEventStartAction } from './ActionTypes' ; import { toCard } from './Card' ; export function local ( a ) { const inflight = ( a as any ) . _inflight ; return { type : 'LOCAL' , action : a , _inflight : inflight } as any as LocalAction ; } export function multiplayerDisconnect ( c = getMultiplayerConnection ( ) ) { c . disconnect ( ) ; return { type : 'MULTIPLAYER_DISCONNECT' } ; } function doConnect ( user , secret , dispatch , c = getMultiplayerConnection ( ) , fetch = window . fetch ) { let sessionID = '' ; const client = user . id . toString ( ) ; const instance = Date . now ( ) . toString ( ) ; return fetch ( MULTIPLAYER_SETTINGS . connectURI , { body : JSON . stringify ( { instance , secret } ) , credentials : 'include' , headers : new Headers ( { Accept : 'application/json' , } ) , method : 'POST' , mode : 'cors' , } ) . then ( handleFetchErrors ) . then ( ( response ) => response . json ( ) ) . then ( ( data ) => { if ( ! data . session ) { return dispatch ( openSnackbar ( Error ( 'Error parsing session' ) ) ) ; } sessionID = data . session ; } ) . then ( ( ) => { dispatch ( { type : 'MULTIPLAYER_SESSION' , session : { secret , id : sessionID } , client , instance } ) ; return dispatch ( toCard ( { name : 'REMOTE_PLAY_LOBBY' } ) ) ; } ) . then ( ( ) => { c . configure ( client , instance ) ; return c . connect ( sessionID , secret ) ; } ) ; } export function multiplayerNewSession ( user , c = getMultiplayerConnection ( ) , fetch = window . fetch ) { return ( dispatch ) : any => { return fetch ( MULTIPLAYER_SETTINGS . newSessionURI , { credentials : 'include' , headers : new Headers ( { Accept : 'text/html' , } ) , method : 'POST' , mode : 'cors' , } ) . then ( handleFetchErrors ) . then ( ( response ) => response . json ( ) ) . then ( ( data ) => { if ( ! data . secret ) { return dispatch ( openSnackbar ( Error ( 'Error parsing new session secret' ) ) ) ; } logEvent ( 'multiplayer' , 'new_session' , { label : data . secret } ) ; return doConnect ( user , data . secret , dispatch , c , fetch ) ; } ) . catch ( ( error ) => { logEvent ( 'multiplayer' , 'new_session_err' , { label : error . toString ( ) } ) ; dispatch ( openSnackbar ( Error ( 'Error creating session: ' + error . toString ( ) ) ) ) ; } ) ; } ; } export function multiplayerConnect ( user , secret , c = getMultiplayerConnection ( ) , fetch = window . fetch ) { return ( dispatch ) : any => { return doConnect ( user , secret , dispatch , c , fetch ) . catch ( ( error ) => { logEvent ( 'multiplayer' , 'connect_err' , { label : error . toString ( ) } ) ; console . error ( error ) ; dispatch ( openSnackbar ( Error ( 'Error connecting: ' + error . toString ( ) ) ) ) ; } ) ; } ; } export function loadMultiplayer ( user , fetch = window . fetch ) { return ( dispatch ) : any => { if ( ! user || ! user . id ) { throw new Error ( 'you are not logged in' ) ; } return fetch ( MULTIPLAYER_SETTINGS . firstLoadURI , { credentials : 'include' , method : 'GET' , mode : 'cors' , } ) . then ( ( response ) => response . json ( ) ) . then ( ( data ) => { dispatch ( { type : 'MULTIPLAYER_HISTORY' , history : data . history } ) ; dispatch ( toCard ( { name : 'REMOTE_PLAY_CONNECT' } ) ) ; } ) . catch ( ( error ) => { console . error ( error ) ; logEvent ( 'multiplayer' , 'init_err' , { label : error . toString ( ) } ) ; dispatch ( toCard ( { name : 'REMOTE_PLAY_CONNECT' } ) ) ; } ) ; } ; } export function setMultiplayerStatus ( ev , c = getMultiplayerConnection ( ) ) { return ( dispatch , getState ) : any => { const { multiplayer } = getState ( ) ; if ( ! multiplayer ) { return ; } dispatch ( sendStatus ( undefined , undefined , ev , c ) ) ; dispatch ( { client : multiplayer . client , instance : multiplayer . instance , status : ev , type : 'MULTIPLAYER_CLIENT_STATUS' , } as MultiplayerClientStatus ) ; return null ; } ; } export function setMultiplayerConnected ( connected ) { return { type : 'MULTIPLAYER_CONNECTED' , connected } ; } export function syncMultiplayer ( c = getMultiplayerConnection ( ) ) { return ( dispatch ) : any => { dispatch ( { type : 'CLEAR_HISTORY' } ) ; dispatch ( { type : 'MULTIPLAYER_SYNC' } ) ; c . sync ( ) ; dispatch ( sendStatus ( undefined , undefined , undefined , c ) ) ; dispatch ( openSnackbar ( new Error ( 'Was there a bug?' ) , true ) ) ; } ; } export function sendStatus ( client ? , instance ? , partialStatus ? , c = getMultiplayerConnection ( ) ) { return ( dispatch , getState ) : Promise < void > => { const { multiplayer , settings , commitID , quest , user } = getState ( ) ; const elem = ( quest && quest . node && quest . node . elem ) ; const combat = ( quest && quest . node && quest . node . ctx && quest . node . ctx . templates && quest . node . ctx . templates . combat ) ; const selfStatus = ( multiplayer && multiplayer . clientStatus && multiplayer . clientStatus [ toClientKey ( multiplayer . client , multiplayer . instance ) ] ) ; const storeClient = multiplayer && multiplayer . client ; const storeInstance = multiplayer && multiplayer . instance ; client = client || storeClient ; instance = instance || storeInstance ; if ( ! client || ! instance ) { return Promise . resolve ( ) ; } if ( client === storeClient && instance === storeInstance ) { const event = { connected : true , lastEventID : commitID , line : ( elem && parseInt ( elem . attr ( 'data-line' ) , 10 ) ) || undefined , numLocalPlayers : ( settings && settings . numLocalPlayers ) || 1 , aliveAdventurers : ( combat && combat . numAliveAdventurers ) , type : 'STATUS' , waitingOn : ( selfStatus && selfStatus . waitingOn ) , name : user && user . email , contentSets : settings && Object . keys ( settings . contentSets || { } ) . filter ( ( k ) => settings . contentSets [ k ] ) , ... ( partialStatus || { } ) , } ; c . sendEvent ( event , commitID ) ; dispatch ( { type : 'MULTIPLAYER_CLIENT_STATUS' , client , instance , status : event } ) ; c . publish ( { id : null , client , instance , event } ) ; } else if ( partialStatus !== undefined ) { dispatch ( { type : 'MULTIPLAYER_CLIENT_STATUS' , client , instance , status : partialStatus } ) ; c . publish ( { id : null , client , instance , event : partialStatus } ) ; } return Promise . resolve ( ) ; } ; } export function sendEvent ( event , commitID ? , c = getMultiplayerConnection ( ) ) { return ( dispatch , getState ) : any => { commitID = commitID || getState ( ) . commitID ; c . sendEvent ( event , commitID ) ; } ; } export function subscribeToEvents ( handler , c = getMultiplayerConnection ( ) ) { c . subscribe ( handler ) ; } export function unsubscribeFromEvents ( handler , c = getMultiplayerConnection ( ) ) { c . unsubscribe ( handler ) ; } export function registerHandler ( handler , c = getMultiplayerConnection ( ) ) { c . registerHandler ( handler ) ; } function commit ( id ) { console . log ( 'MULTIPLAYER_COMMIT #' + id ) ; return { type : 'MULTIPLAYER_COMMIT' , id } ; } export function rejectEvent ( n , error ) { console . log ( 'MULTIPLAYER_REJECT #' + n + ': ' + error ) ; return { type : 'MULTIPLAYER_REJECT' , id : n , error , } ; } export function handleEvent ( e , buffered , commitID , multiplayer , c = getMultiplayerConnection ( ) ) { return ( dispatch ) : Promise < void > => { if ( e . id && e . id !== ( commitID + 1 ) ) { console . log ( 'Ignoring #' + e . id + ' ' + e . event . type + ' (counter at #' + commitID + ')' ) ; dispatch ( sendStatus ( undefined , undefined , undefined , c ) ) ; return Promise . resolve ( ) ; } const body = e . event ; switch ( body . type ) { case 'STATUS' : if ( e . client !== multiplayer . client || e . instance !== multiplayer . instance ) { return dispatch ( sendStatus ( e . client , e . instance , body , c ) ) ; } break ; case 'INTERACTION' : break ; case 'ACTION' : if ( e . id === null ) { return Promise . resolve ( ) ; } if ( buffered ) { if ( e . client === multiplayer . client && e . instance === multiplayer . instance ) { dispatch ( commit ( e . id ) ) ; } else { dispatch ( rejectEvent ( e . id , 'Multiplayer ACTION matching buffered' ) ) ; } return Promise . resolve ( ) ; } const a = getMultiplayerAction ( body . name ) ; if ( ! a ) { console . error ( 'Received unknown remote action ' + body . name ) ; return Promise . resolve ( ) ; } console . log ( 'WS: Inbound #' + e . id + ': ' + body . name + '(' + body . args + ')' ) ; const action = a ( JSON . parse ( body . args ) ) ; ( action as any ) . _inflight = 'remote' ; let result ; try { result = dispatch ( action ) ; } finally { if ( e . id !== null ) { dispatch ( commit ( e . id ) ) ; } } return result ; case 'MULTI_EVENT' : if ( multiplayer . multiEvent ) { console . log ( 'Ignoring MULTI_EVENT, already parsing' ) ; return Promise . resolve ( ) ; } let chain = Promise . resolve ( ) . then ( ( ) => { dispatch ( { type : 'MULTIPLAYER_MULTI_EVENT_START' , syncID : body . lastId } as MultiplayerMultiEventStartAction ) ; } ) ; for ( let i = 0 ; i < body . events . length ; i ++ ) { const event = body . events [ i ] ; let parsed ; try { parsed = JSON . parse ( event ) ; if ( ! parsed . id ) { throw new Error ( 'MULTI_EVENT without ID: ' + parsed ) ; } } catch ( e ) { console . error ( e ) ; continue ; } chain = chain . then ( ( _ ) => { return new Promise < void > ( ( fulfill , reject ) => { setTimeout ( ( ) => { const route = dispatch ( handleEvent ( parsed , false , commitID + i , multiplayer ) ) ; if ( route && typeof ( route ) === 'object' && route . then ) { fulfill ( route ) ; } fulfill ( ) ; } , 0 ) ; } ) ; } ) ; } chain = chain . then ( ( _ ) => { dispatch ( { type : 'MULTIPLAYER_MULTI_EVENT' } ) ; } ) ; c . publish ( e ) ; return chain ; case 'ERROR' : console . error ( JSON . stringify ( body ) ) ; break ; default : console . error ( 'UNKNOWN EVENT ' + ( body as any ) . type ) ; if ( e . id ) { dispatch ( commit ( e . id ) ) ; } } c . publish ( e ) ; return Promise . resolve ( ) ; } ; }