import { RocketCrab , Party , Player , ServerGame , ClientParty , FinderState , FINDER_ACTIVE_MS , MAX_CHATS_OVERALL , MAX_CHATS_FROM_SINGLE_PLAYER , } from "../types/types" ; import { PartyStatus , GameStatus , SocketEvent , RocketcrabMode , } from "../types/enums" ; import { getServerGameLibrary } from "../config" ; import { v4 as uuidv4 } from "uuid" ; import { CronJob } from "cron" ; import { getUnixTime } from "date-fns" ; import type { Socket } from "socket.io" ; import { isChatMsgValid } from "../utils/utils" ; const SERVER_GAME_LIST = getServerGameLibrary ( ) . gameList ; const PARTY_EXPIRATION_SEC = 60 ; export const initRocketCrab = ( isDevMode ? ) : RocketCrab => { const partyList = [ ] ; const rocketcrab = { partyList , isFinderActive : false , finderSubscribers : [ ] , } ; if ( isDevMode ) newParty ( { rocketcrab , forceGameCode : "ffff" } ) ; return rocketcrab ; } ; export const initCron = ( rocketcrab ) : void => { const setDates = ( ) => { rocketcrab . finderActiveDates = { lastStart : getUnixTime ( activateFinderJob . lastDate ( ) ) * 1000 , nextStart : activateFinderJob . nextDate ( ) . valueOf ( ) , nextWeekOfStarts : activateFinderJob . nextDates ( 24 ) . map ( ( momentDate ) => momentDate . valueOf ( ) ) , } ; } ; const activateFinderJob = new CronJob ( "0 13 * * 6" , ( ) => { setDates ( ) ; rocketcrab . isFinderActive = true ; sendFinderStateToAll ( rocketcrab ) ; setTimeout ( ( ) => { rocketcrab . isFinderActive = false ; rocketcrab . partyList . forEach ( ( party ) => { if ( ! party . isPublic ) return ; party . isPublic = false ; sendStateToAll ( party , rocketcrab , { enableFinderCheck : false , } ) ; } ) ; sendFinderStateToAll ( rocketcrab ) ; } , FINDER_ACTIVE_MS ) ; } , null , true , "America/Chicago" ) ; setDates ( ) ; } ; export const newParty = ( { rocketcrab : { partyList , finderActiveDates } , forceGameCode , forceUuid , isPublic = false , mode = RocketcrabMode . MAIN , } ) : Party => { const newParty = { status : PartyStatus . party , playerList : [ ] , code : forceGameCode || getUniqueGameCode ( partyList ) , uuid : forceUuid || uuidv4 ( ) , selectedGameId : "" , gameState : { status : GameStatus . loading , } , nextPlayerId : 0 , idealHostId : 0 , isPublic , createdAsPublic : isPublic , chat : [ ] , bannedIPs : [ ] , mode , } ; if ( isPublic ) { newParty . publicEndDate = finderActiveDates . lastStart + FINDER_ACTIVE_MS ; } partyList . push ( newParty ) ; setTimeout ( ( ) => deletePartyIfEmpty ( newParty , partyList ) , PARTY_EXPIRATION_SEC * 1000 ) ; return newParty ; } ; export const getPartyByCode = ( newCode , partyList ) : Party => partyList . find ( ( { code } ) => code === newCode ) ; export const getPartyByUuid = ( newUuid , partyList ) : Party => partyList . find ( ( { uuid } ) => uuid === newUuid ) ; export const reconnectToParty = ( lastPartyState , rocketcrab ) : Party => { const { partyList } = rocketcrab ; if ( ! lastPartyState || ! lastPartyState . uuid ) return ; const { code , uuid , status , selectedGameId , gameState , idealHostId , mode } = lastPartyState || { } ; const partyAlreadyExists = getPartyByUuid ( uuid , partyList ) ; if ( partyAlreadyExists ) return partyAlreadyExists ; const isValidCode = code ?. length === 4 && code ?. match ( /[a-z]/i ) && ! getPartyByCode ( code , partyList ) ; const party = newParty ( { rocketcrab , forceUuid : uuid , ... ( isValidCode ? { forceGameCode : code } : { } ) , } ) ; if ( Object . values ( PartyStatus ) . includes ( status ) ) { party . status = status ; } if ( [ RocketcrabMode . MAIN , RocketcrabMode . KIDS ] . includes ( mode ) ) { party . mode = mode ; } if ( findGameById ( selectedGameId ) ) { party . selectedGameId = selectedGameId ; } party . gameState = gameState ; party . idealHostId = idealHostId ; return party ; } ; export const addPlayer = ( name , socket , party , previousId ? ) : Player => { const { playerList } = party ; const idNotInUse = ! isIDinUse ( previousId , party . playerList ) ; const usePreviousId = Number . isInteger ( previousId ) && previousId >= 0 && idNotInUse ; const id = usePreviousId ? previousId : party . nextPlayerId ++ ; const isFirstPlayer = playerList . length === 0 ; if ( id >= party . nextPlayerId ) { party . nextPlayerId = id + 1 ; } const player = { id , name : "" , socket , isHost : false , } ; playerList . push ( player ) ; setName ( name , player , playerList ) ; if ( isFirstPlayer ) { party . idealHostId = id ; } setHost ( party . idealHostId , playerList ) ; return player ; } ; export const sendStateToAll = ( party , rocketcrab , { enableFinderCheck , forceFinderUpdate , } = { } ) : void => { party . playerList . forEach ( ( { socket , ... player } ) => { const clientParty = { me : player , ... getJsonParty ( party ) , isFinderActive : rocketcrab . isFinderActive , } ; socket . emit ( SocketEvent . UPDATE , clientParty ) ; } ) ; if ( ( enableFinderCheck && shouldSendFinderStateUpdate ( party , rocketcrab ) ) || forceFinderUpdate ) { sendFinderStateToAll ( rocketcrab ) ; } } ; export const sendFinderStateToAll = ( rocketcrab ) : void => { rocketcrab . finderSubscribers . forEach ( ( socket ) => socket . emit ( SocketEvent . FINDER_UPDATE , getFinderState ( rocketcrab ) ) ) ; } ; export const shouldSendFinderStateUpdate = ( { status , isPublic } , { isFinderActive } ) : boolean => { const thisPartyIsShownOnFinder = status === PartyStatus . party && isPublic ; return isFinderActive && thisPartyIsShownOnFinder ; } ; export const removePlayer = ( player , party ) : void => { const { playerList , idealHostId } = party ; const { socket } = player ; if ( socket && socket . disconnect ) { socket . disconnect ( true ) ; } deleteFromArray ( player , playerList ) ; if ( player . isHost ) { setHost ( idealHostId , playerList ) ; } } ; export const deletePartyIfEmpty = ( party , partyList ) : void => { const { playerList , code } = party ; if ( playerList . length === 0 && code !== "ffff" ) { disconnectAllPlayers ( playerList ) ; deleteFromArray ( party , partyList ) ; } } ; export const setName = ( name , playerToName , playerList ) : void => { const validLength = typeof name === "string" && name . length <= 24 ; if ( ! findPlayerByName ( name , playerList ) && validLength ) { playerToName . name = name ; } else { playerToName . name = "" ; if ( name ) { playerToName . socket . emit ( SocketEvent . INVALID_NAME ) ; } } } ; export const setGame = ( gameId , party ) : void => { if ( findGameById ( gameId ) ) { party . selectedGameId = gameId ; } } ; export const startGame = async ( party , rocketcrab ) : Promise < void > => { const { gameState , selectedGameId , playerList } = party ; const willSendFinderUpdate = shouldSendFinderStateUpdate ( party , rocketcrab ) ; party . status === PartyStatus . party && party . isPublic ; const game = findGameById ( selectedGameId ) ; if ( ! game ) return ; if ( game . minPlayers && playerList . length < game . minPlayers ) return ; if ( game . maxPlayers && playerList . length > game . maxPlayers ) return ; party . status = PartyStatus . ingame ; gameState . status = GameStatus . loading ; sendStateToAll ( party , rocketcrab , { forceFinderUpdate : willSendFinderUpdate , } ) ; try { gameState . connectedGame = await game . connectToGame ( ) ; } catch ( e ) { console . error ( e ) ; gameState . status = GameStatus . error ; gameState . error = "❌ Can't connect to " + game . name ; sendStateToAll ( party , rocketcrab ) ; return ; } gameState . status = GameStatus . waitingforhost ; const host = getHost ( playerList ) ; const onHostGameLoaded = ( ) => { gameState . status = GameStatus . inprogress ; sendStateToAll ( party , rocketcrab ) ; } ; const hostGameLoadedBackup = setTimeout ( onHostGameLoaded , 10 * 1000 ) ; host . socket . once ( SocketEvent . HOST_GAME_LOADED , ( ) => { clearTimeout ( hostGameLoadedBackup ) ; onHostGameLoaded ( ) ; } ) ; sendStateToAll ( party , rocketcrab ) ; } ; export const exitGame = ( party ) : void => { party . status = PartyStatus . party ; const { gameState } = party ; gameState . status = GameStatus . loading ; delete gameState . connectedGame ; delete gameState . error ; } ; export const getFinderState = ( { isFinderActive , partyList , finderActiveDates , finderSubscribers , } ) : FinderState => ( { isActive : isFinderActive , publicPartyList : partyList . filter ( ( { isPublic , status , selectedGameId } ) => isPublic && status === PartyStatus . party && selectedGameId ) . map ( ( party ) => getJsonParty ( party ) ) , finderActiveDates , subscriberCount : finderSubscribers . length - 1 , } ) ; export const addChatMessage = ( message , player , party ) : boolean => { if ( ! isChatMsgValid ( message , player , party . chat ) ) return false ; party . chat . push ( { playerId : player . id , playerName : player . name , message , date : Date . now ( ) . valueOf ( ) , } ) ; purgeOverflowMsgs ( player , party ) ; return true ; } ; export const kickPlayer = ( playerId , isBan , party ) : void => { const playerToKick = party . playerList . find ( ( { id } ) => id === playerId ) ; if ( ! playerToKick ) return ; if ( isBan ) { party . bannedIPs . push ( playerToKick . socket . handshake . address ) ; } removePlayer ( playerToKick , party ) ; } ; const purgeOverflowMsgs = ( player , party ) : void => { const numberOfMsgsFromThisPlayer = party . chat . reduce ( ( prev , cur ) => prev + ( cur . playerId === player . id ? 1 : 0 ) , 0 ) ; let numberOfMsgsToRemove = numberOfMsgsFromThisPlayer - MAX_CHATS_FROM_SINGLE_PLAYER ; if ( numberOfMsgsToRemove > 0 ) { party . chat = party . chat . filter ( ( { playerId } ) => { if ( playerId === player . id && numberOfMsgsToRemove > 0 ) { numberOfMsgsToRemove -- ; return false ; } return true ; } ) ; } if ( party . chat . length > MAX_CHATS_OVERALL ) { party . chat . splice ( 0 , party . chat . length - MAX_CHATS_OVERALL ) ; } } ; const findPlayerByName = ( nameToFind , playerList ) : Player => playerList . find ( ( { name } ) => name === nameToFind ) ; const findGameById = ( gameId ) : ServerGame => SERVER_GAME_LIST . find ( ( { id } ) => id === gameId ) ; const disconnectAllPlayers = ( playerList ) : void => playerList . forEach ( ( { socket } ) => socket . disconnect ( true ) ) ; export const getJsonParty = ( { playerList , ... party } ) : ClientParty => ( { playerList : playerList . map ( ( { id , name , isHost } ) => ( { id , name , isHost , } ) ) , ... party , } ) ; const getUniqueGameCode = ( ll ) : string => { let newCode ; do { newCode = getRandomFourLetters ( ) ; } while ( ll . find ( ( { code } ) => code === newCode ) && newCode !== "ffff" ) ; return newCode ; } ; const getRandomFourLetters = ( ) : string => { const len = 4 ; const possible = "abcdefghijklmnopqrstuvwxyz" ; let code = "" ; for ( let i = 0 ; i < len ; i ++ ) { code += possible . charAt ( Math . floor ( Math . random ( ) * possible . length ) ) ; } return code ; } ; const deleteFromArray = ( item , array ) : void => { const index = array . indexOf ( item ) ; if ( index > - 1 ) { array . splice ( index , 1 ) ; } } ; const isIDinUse = ( previousId , playerList ) : boolean => ! ! playerList . find ( ( { id } ) => id === previousId ) ; const setHost = ( idealHostId , playerList ) : void => { playerList . forEach ( ( player ) => ( player . isHost = false ) ) ; const idealHost = playerList . find ( ( { id } ) => id === idealHostId ) ; if ( idealHost ) { idealHost . isHost = true ; return ; } playerList . reduce ( ( acc , cur ) => ( acc . id < cur . id ? acc : cur ) , { id : Number . MAX_SAFE_INTEGER , isHost : null , } ) . isHost = true ; } ; const getHost = ( playerList ) : Player => playerList . find ( ( { isHost } ) => isHost ) ;