2022-11-20 23:37:39 +00:00
import { Channel } from "./Channel" ;
import { SharedContent } from "../BanchoServer" ;
import { DataStream } from "./DataStream" ;
import { Slot } from "./Slot" ;
import { User } from "./User" ;
import { StatusUpdate } from "../packets/StatusUpdate" ;
2022-11-19 01:06:03 +00:00
const osu = require ( "osu-packet" ) ;
2022-11-20 23:37:39 +00:00
export interface MatchData {
matchId :number ,
matchType :number ,
activeMods :number ,
gameName :string ,
gamePassword :string ,
inProgress :boolean ,
beatmapName :string ,
beatmapId :number ,
beatmapChecksum :string ,
slots :Array < any > ,
host :number ,
playMode :number ,
matchScoringType :number ,
matchTeamType :number ,
specialModes :number ,
seed :number
}
2022-11-19 01:06:03 +00:00
export class Match {
2022-11-20 23:37:39 +00:00
// osu! Data
public matchId :number = - 1 ;
2022-11-19 01:06:03 +00:00
public inProgress :boolean = false ;
2022-11-20 23:37:39 +00:00
public matchType :number = 0 ;
public activeMods :number = 0 ;
public gameName :string = "" ;
public gamePassword :string | undefined = '' ;
public beatmapName :string = '' ;
public beatmapId :number = 0 ;
public beatmapChecksum :string = '' ;
public slots :Array < Slot > = new Array < Slot > ( ) ;
2022-11-19 01:06:03 +00:00
public host :number = 0 ;
public playMode :number = 0 ;
public matchScoringType :number = 0 ;
public matchTeamType :number = 0 ;
public specialModes :number = 0 ;
2022-11-20 23:37:39 +00:00
public seed :number = 0 ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
// Binato data
public roundId :number = 0 ;
public matchStartCountdownActive :boolean = false ;
public matchStream :DataStream ;
public matchChatChannel :Channel ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
private constructor ( matchData :MatchData , sharedContent :SharedContent ) {
console . log ( matchData ) ;
this . matchId = matchData . matchId ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . inProgress = matchData . inProgress ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . matchType = matchData . matchType ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . activeMods = matchData . activeMods ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . gameName = matchData . gameName ;
if ( matchData . gamePassword == '' ) matchData . gamePassword == null ;
this . gamePassword = matchData . gamePassword ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . beatmapName = matchData . beatmapName ;
this . beatmapId = matchData . beatmapId ;
this . beatmapChecksum = matchData . beatmapChecksum ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . slots = matchData . slots ;
2022-11-19 01:06:03 +00:00
for ( let i = 0 ; i < this . slots . length ; i ++ ) {
2022-11-20 23:37:39 +00:00
//this.slots[i].mods = 0;
2022-11-19 01:06:03 +00:00
}
2022-11-20 23:37:39 +00:00
this . host = matchData . host ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . playMode = matchData . playMode ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . matchScoringType = matchData . matchScoringType ;
this . matchTeamType = matchData . matchTeamType ;
this . specialModes = matchData . specialModes ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . seed = matchData . seed ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
this . matchStream = sharedContent . streams . CreateStream ( ` multiplayer:match_ ${ this . matchId } ` ) ;
this . matchChatChannel = sharedContent . chatManager . AddSpecialChatChannel ( "multiplayer" , ` mp_ ${ this . matchId } ` ) ;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
//this.matchLoadSlots = null;
//this.matchSkippedSlots = null;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
//this.playerScores = null;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
//this.multiplayerExtras = null;
2022-11-19 01:06:03 +00:00
2022-11-20 23:37:39 +00:00
//this.isTourneyMatch = false;
//this.tourneyClientUsers = [];
2022-11-19 01:06:03 +00:00
}
2022-11-20 23:37:39 +00:00
public static createMatch ( matchHost :User , matchData :MatchData , sharedContent :SharedContent ) : Promise < Match > {
return new Promise < Match > ( async ( resolve , reject ) = > {
try {
matchData . matchId = ( await sharedContent . database . query (
"INSERT INTO mp_matches (id, name, open_time, close_time, seed) VALUES (NULL, ?, UNIX_TIMESTAMP(), NULL, ?) RETURNING id;" ,
[ matchData . gameName , matchData . seed ]
) ) [ 0 ] [ "id" ] ;
const matchInstance = new Match ( matchData , sharedContent ) ;
console . log ( matchInstance . matchId ) ;
// Update the status of the current user
StatusUpdate ( matchHost , matchHost . id ) ;
const osuPacketWriter = new osu . Bancho . Writer ;
//osuPacketWriter.MatchNew(matchInstance.createOsuMatchJSON());
matchHost . addActionToQueue ( osuPacketWriter . toBuffer ) ;
// Update the match listing for users in the multiplayer lobby
//global.MultiplayerManager.updateMatchListing();
resolve ( matchInstance ) ;
} catch ( e ) {
reject ( e ) ;
}
2022-11-19 01:06:03 +00:00
} ) ;
}
2022-11-20 23:37:39 +00:00
/ * g e t S l o t I d B y P l a y e r I d ( p l a y e r I d = 0 ) {
2022-11-19 01:06:03 +00:00
const player = getUserById ( playerId ) ;
if ( player != null ) return player . matchSlotId ;
else return null ;
}
createOsuMatchJSON() {
return {
matchId : this.matchId ,
inProgress : this.inProgress ,
matchType : this.matchType ,
activeMods : this.activeMods ,
gameName : this.gameName ,
gamePassword : this.gamePassword ,
beatmapName : this.beatmapName ,
beatmapId : this.beatmapId ,
beatmapChecksum : this.beatmapChecksum ,
slots : this.slots ,
host : this.host ,
playMode : this.playMode ,
matchScoringType : this.matchScoringType ,
matchTeamType : this.matchTeamType ,
specialModes : this.specialModes ,
seed : this.seed
} ;
}
leaveMatch ( MatchUser = new User ) {
// Make sure this leave call is valid
if ( ! MatchUser . inMatch ) return ;
// Get the user's slot
const slot = this . slots [ MatchUser . matchSlotId ] ;
// Set the slot's status to avaliable
slot . playerId = - 1 ;
slot . status = 1 ;
// Remove the leaving user from the match's stream
Streams . removeUserFromStream ( this . matchStreamName , MatchUser . uuid ) ;
Streams . removeUserFromStream ( this . matchChatStreamName , MatchUser . uuid ) ;
// Send this after removing the user from match streams to avoid a leave notification for self
this . sendMatchUpdate ( ) ;
const osuPacketWriter = new osu . Bancho . Writer ;
// Remove user from the multiplayer channel for the match
osuPacketWriter . ChannelRevoked ( "#multiplayer" ) ;
MatchUser . addActionToQueue ( osuPacketWriter . toBuffer ) ;
}
async updateMatch ( MatchUser = new User , MatchData ) {
// Update match with new data
this . inProgress = MatchData . inProgress ;
this . matchType = MatchData . matchType ;
this . activeMods = MatchData . activeMods ;
const gameNameChanged = this . gameName !== MatchData . gameName ;
this . gameName = MatchData . gameName ;
if ( MatchData . gamePassword == '' ) MatchData . gamePassword == null ;
this . gamePassword = MatchData . gamePassword ;
this . beatmapName = MatchData . beatmapName ;
this . beatmapId = MatchData . beatmapId ;
this . beatmapChecksum = MatchData . beatmapChecksum ;
this . host = MatchData . host ;
this . playMode = MatchData . playMode ;
this . matchScoringType = MatchData . matchScoringType ;
this . matchTeamType = MatchData . matchTeamType ;
this . specialModes = MatchData . specialModes ;
const gameSeedChanged = this . seed !== MatchData . seed ;
this . seed = MatchData . seed ;
if ( gameNameChanged || gameSeedChanged ) {
const queryData = [ ] ;
if ( gameNameChanged ) {
queryData . push ( MatchData . gameName ) ;
}
if ( gameSeedChanged ) {
queryData . push ( MatchData . seed ) ;
}
queryData . push ( this . matchId ) ;
await global . DatabaseHelper . query ( ` UPDATE mp_matches SET ${ gameNameChanged ? ` name = ? ${ gameSeedChanged ? ", " : "" } ` : "" } ${ gameSeedChanged ? ` seed = ? ` : "" } WHERE id = ? ` , queryData ) ;
}
this . sendMatchUpdate ( ) ;
// Update the match listing in the lobby to reflect these changes
global . MultiplayerManager . updateMatchListing ( ) ;
}
sendMatchUpdate() {
const osuPacketWriter = new osu . Bancho . Writer ;
osuPacketWriter . MatchUpdate ( this . createOsuMatchJSON ( ) ) ;
// Update all users in the match with new match information
if ( Streams . exists ( this . matchStreamName ) )
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
}
moveToSlot ( MatchUser = new User , SlotToMoveTo ) {
const oldSlot = this . slots [ MatchUser . matchSlotId ] ;
// Set the new slot's data to the user's old slot data
this . slots [ SlotToMoveTo ] . playerId = MatchUser . id ;
MatchUser . matchSlotId = SlotToMoveTo ;
this . slots [ SlotToMoveTo ] . status = 4 ;
// Set the old slot's data to open
oldSlot . playerId = - 1 ;
oldSlot . status = 1 ;
this . sendMatchUpdate ( ) ;
// Update the match listing in the lobby to reflect this change
global . MultiplayerManager . updateMatchListing ( ) ;
}
changeTeam ( MatchUser = new User ) {
const slot = this . slots [ MatchUser . matchSlotId ] ;
slot . team = slot . team == 0 ? 1 : 0 ;
this . sendMatchUpdate ( ) ;
}
setStateReady ( MatchUser = new User ) {
if ( ! MatchUser . inMatch ) return ;
// Set the user's ready state to ready
this . slots [ MatchUser . matchSlotId ] . status = 8 ;
this . sendMatchUpdate ( ) ;
}
setStateNotReady ( MatchUser = new User ) {
if ( ! MatchUser . inMatch ) return ;
// Set the user's ready state to not ready
this . slots [ MatchUser . matchSlotId ] . status = 4 ;
this . sendMatchUpdate ( ) ;
}
lockMatchSlot ( MatchUser = new User , MatchUserToKick ) {
// Make sure the user attempting to kick / lock is the host of the match
if ( this . host != MatchUser . id ) return ;
// Make sure the user that is attempting to be kicked is not the host
if ( this . slots [ MatchUserToKick ] . playerId === this . host ) return ;
// Get the data of the slot at the index sent by the client
const slot = this . slots [ MatchUserToKick ] ;
let isSlotEmpty = true ;
// If the slot is empty lock/unlock instead of kicking
if ( slot . playerId === - 1 )
slot . status = slot . status === 1 ? 2 : 1 ;
// The slot isn't empty, kick the player
else {
const kickedPlayer = getUserById ( slot . playerId ) ;
kickedPlayer . matchSlotId = - 1 ;
slot . playerId = - 1 ;
slot . status = 1 ;
isSlotEmpty = false ;
}
this . sendMatchUpdate ( ) ;
// Update the match listing in the lobby listing to reflect this change
global . MultiplayerManager . updateMatchListing ( ) ;
if ( ! isSlotEmpty ) {
let cachedPlayerToken = getUserById ( slot . playerId ) . uuid ;
if ( cachedPlayerToken !== null && cachedPlayerToken !== "" ) {
// Remove the kicked user from the match stream
Streams . removeUserFromStream ( this . matchStreamName , cachedPlayerToken ) ;
}
}
}
missingBeatmap ( MatchUser = new User ) {
// User is missing the beatmap set the status to reflect it
this . slots [ MatchUser . matchSlotId ] . status = 16 ;
this . sendMatchUpdate ( ) ;
}
notMissingBeatmap ( MatchUser = new User ) {
// The user is not missing the beatmap, set the status to normal
this . slots [ MatchUser . matchSlotId ] . status = 4 ;
this . sendMatchUpdate ( ) ;
}
matchSkip ( MatchUser = new User ) {
if ( this . matchSkippedSlots == null ) {
this . matchSkippedSlots = [ ] ;
const skippedSlots = this . matchSkippedSlots ;
for ( let slot of this . slots ) {
// Make sure the slot has a user in it
if ( slot . playerId === - 1 || slot . status === 1 || slot . status === 2 ) continue ;
// Add the slot's user to the loaded checking array
skippedSlots . push ( { playerId : slot.playerId , skipped : false } ) ;
}
}
let allSkipped = true ;
for ( let skippedSlot of this . matchSkippedSlots ) {
// If loadslot belongs to this user then set loaded to true
if ( skippedSlot . playerId == MatchUser . id ) {
skippedSlot . skipped = true ;
}
if ( skippedSlot . skipped ) continue ;
// A user hasn't skipped
allSkipped = false ;
}
// All players have finished playing, finish the match
if ( allSkipped ) {
const osuPacketWriter = new osu . Bancho . Writer ;
osuPacketWriter . MatchPlayerSkipped ( MatchUser . id ) ;
osuPacketWriter . MatchSkip ( ) ;
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
this . matchSkippedSlots = null ;
} else {
const osuPacketWriter = new osu . Bancho . Writer ;
osuPacketWriter . MatchPlayerSkipped ( MatchUser . id ) ;
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
}
}
transferHost ( MatchUser = new User , SlotIDToTransferTo ) {
// Set the lobby's host to the new user
this . host = this . slots [ SlotIDToTransferTo ] . playerId ;
this . sendMatchUpdate ( ) ;
}
// TODO: Fix not being able to add DT when freemod is active
updateMods ( MatchUser = new User , MatchMods ) {
// Check if freemod is enabled
if ( this . specialModes === 1 ) {
this . slots [ MatchUser . matchSlotId ] . mods = MatchMods ;
this . sendMatchUpdate ( ) ;
} else {
// Make sure the person updating mods is the host of the match
if ( this . host !== MatchUser . id ) return ;
// Change the matches mods to these new mods
// TODO: Do this per user if freemod is enabled
this . activeMods = MatchMods ;
this . sendMatchUpdate ( ) ;
}
// Update match listing in the lobby to reflect this change
global . MultiplayerManager . updateMatchListing ( ) ;
}
startMatch() {
// Make sure the match is not already in progress
// The client sometimes double fires the start packet
if ( this . inProgress ) return ;
this . inProgress = true ;
// Create array for monitoring users until they are ready to play
this . matchLoadSlots = [ ] ;
// Loop through all slots in the match
for ( let slot of this . slots ) {
// Make sure the slot has a user in it
if ( slot . playerId === - 1 || slot . status === 1 || slot . status === 2 ) continue ;
// Add the slot's user to the loaded checking array
this . matchLoadSlots . push ( {
playerId : slot.playerId ,
loaded : false
} ) ;
// Set the user's status to playing
slot . status = 32 ;
}
const osuPacketWriter = new osu . Bancho . Writer ;
osuPacketWriter . MatchStart ( this . createOsuMatchJSON ( ) ) ;
// Inform all users in the match that it has started
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
// Update all users in the match with new info
this . sendMatchUpdate ( ) ;
// Update match listing in lobby to show the game is in progress
global . MultiplayerManager . updateMatchListing ( ) ;
}
matchPlayerLoaded ( MatchUser = new User ) {
// Loop through all user load check items and check if all users are loaded
let allLoaded = true ;
for ( let loadedSlot of this . matchLoadSlots ) {
// If loadslot belongs to this user then set loaded to true
if ( loadedSlot . playerId == MatchUser . id ) {
loadedSlot . loaded = true ;
}
if ( loadedSlot . loaded ) continue ;
allLoaded = false ;
}
// All players have loaded the beatmap, start playing.
if ( allLoaded ) {
let osuPacketWriter = new osu . Bancho . Writer ;
osuPacketWriter . MatchAllPlayersLoaded ( ) ;
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
// Blank out user loading array
this . matchLoadSlots = null ;
this . playerScores = [ ] ;
for ( let i = 0 ; i < this . slots . length ; i ++ ) {
const slot = this . slots [ i ] ;
if ( slot . playerId === - 1 || slot . status === 1 || slot . status === 2 ) continue ;
this . playerScores . push ( { playerId : slot.playerId , slotId : i , score : 0 , isCurrentlyFailed : false } ) ;
}
}
}
async onPlayerFinishMatch ( MatchUser = new User ) {
if ( this . matchLoadSlots == null ) {
// Repopulate user loading slots again
this . matchLoadSlots = [ ] ;
for ( let slot of this . slots ) {
// Make sure the slot has a user
if ( slot . playerId === - 1 || slot . status === 1 || slot . status === 2 ) continue ;
// Populate user loading slots with this user's id and load status
this . matchLoadSlots . push ( {
playerId : slot.playerId ,
loaded : false
} ) ;
}
}
let allLoaded = true ;
// Loop through all loaded slots to make sure all users have finished playing
for ( let loadedSlot of this . matchLoadSlots ) {
if ( loadedSlot . playerId == MatchUser . id ) {
loadedSlot . loaded = true ;
}
if ( loadedSlot . loaded ) continue ;
// A user hasn't finished playing
allLoaded = false ;
}
// All players have finished playing, finish the match
if ( allLoaded ) await this . finishMatch ( ) ;
}
async finishMatch() {
if ( ! this . inProgress ) return ;
this . matchLoadSlots = null ;
this . inProgress = false ;
let osuPacketWriter = new osu . Bancho . Writer ;
let queryData = [ this . matchId , this . roundId ++ , this . playMode , this . matchType , this . matchScoringType , this . matchTeamType , this . activeMods , this . beatmapChecksum , ( this . specialModes === 1 ) ? 1 : 0 ] ;
// Loop through all slots in the match
for ( let slot of this . slots ) {
// Make sure the slot has a user
if ( slot . playerId === - 1 || slot . status === 1 || slot . status === 2 ) {
queryData . push ( null ) ;
continue ;
}
let score = null ;
for ( let _playerScore of this . playerScores ) {
if ( _playerScore . playerId === slot . playerId ) {
score = _playerScore . _raw ;
break ;
}
}
queryData . push ( ` ${ slot . playerId } | ${ score . totalScore } | ${ score . maxCombo } | ${ score . count300 } | ${ score . count100 } | ${ score . count50 } | ${ score . countGeki } | ${ score . countKatu } | ${ score . countMiss } | ${ ( score . currentHp == 254 ) ? 1 : 0 } ${ ( this . specialModes === 1 ) ? ` | ${ slot . mods } ` : "" } | ${ score . usingScoreV2 ? 1 : 0 } ${ score . usingScoreV2 ? ` | ${ score . comboPortion } | ${ score . bonusPortion } ` : "" } ` ) ;
// Set the user's status back to normal from playing
slot . status = 4 ;
}
console . log ( queryData ) ;
osuPacketWriter . MatchComplete ( ) ;
// Inform all users in the match that it is complete
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
// Update all users in the match with new info
this . sendMatchUpdate ( ) ;
// Update match info in the lobby to reflect that the match has finished
global . MultiplayerManager . updateMatchListing ( ) ;
if ( this . multiplayerExtras != null ) this . multiplayerExtras . onMatchFinished ( JSON . parse ( JSON . stringify ( this . playerScores ) ) ) ;
await global . DatabaseHelper . query ( "INSERT INTO mp_match_rounds (id, match_id, round_id, round_mode, match_type, round_scoring_type, round_team_type, round_mods, beatmap_md5, freemod, player0, player1, player2, player3, player4, player5, player6, player7, player8, player9, player10, player11, player12, player13, player14, player15) VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" , queryData ) ;
this . playerScores = null ;
}
updatePlayerScore ( MatchPlayer = new User , MatchScoreData ) {
const osuPacketWriter = new osu . Bancho . Writer ;
// Make sure the user's slot ID is not invalid
if ( this . matchSlotId == - 1 ) return ;
// Get the user's current slotID and append it to the givien data, just incase.
MatchScoreData . id = MatchPlayer . matchSlotId ;
// Update the playerScores array accordingly
for ( let playerScore of this . playerScores ) {
if ( playerScore . playerId == MatchPlayer . id ) {
playerScore . score = MatchScoreData . totalScore ;
playerScore . isCurrentlyFailed = MatchScoreData . currentHp == 254 ;
playerScore . _raw = MatchScoreData ;
break ;
}
}
osuPacketWriter . MatchScoreUpdate ( MatchScoreData ) ;
// Send the newly updated score to all users in the match
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
}
matchFailed ( MatchUser = new User ) {
const osuPacketWriter = new osu . Bancho . Writer ;
// Make sure the user's slot ID is not invalid
if ( MatchUser . matchSlotId == - 1 ) return ;
osuPacketWriter . MatchPlayerFailed ( MatchUser . id ) ;
Streams . sendToStream ( this . matchStreamName , osuPacketWriter . toBuffer , null ) ;
} * /
}