2020-08-27 13:09:35 +01:00
const osu = require ( "osu-packet" ) ,
2022-01-04 03:39:53 +00:00
fs = require ( "fs" ) ,
consoleHelper = require ( "../consoleHelper.js" ) ,
packetIDs = require ( "./packetIDs.js" ) ,
loginHandler = require ( "./loginHandler.js" ) ,
parseUserData = require ( "./util/parseUserData.js" ) ,
User = require ( "./User.js" ) ,
getUserFromToken = require ( "./util/getUserByToken.js" ) ,
bakedResponses = require ( "./bakedResponses.js" ) ,
Streams = require ( "./Streams.js" ) ,
DatabaseHelperClass = require ( "./DatabaseHelper.js" ) ,
config = require ( "../config.json" ) ;
2020-08-27 13:09:35 +01:00
2021-05-15 00:24:39 +01:00
global . users = { } ;
global . userKeys = Object . keys ( global . users ) ;
global . addUser = function ( uuid , userToAdd ) {
2022-01-04 03:39:53 +00:00
global . users [ uuid ] = userToAdd ;
global . refreshUserKeys ( ) ;
2021-05-15 00:24:39 +01:00
}
global . removeUser = function ( userToRemove ) {
2022-01-04 03:39:53 +00:00
delete userToRemove ;
global . refreshUserKeys ( ) ;
2021-05-15 00:24:39 +01:00
}
global . refreshUserKeys = function ( ) {
2022-01-04 03:39:53 +00:00
global . userKeys = Object . keys ( global . users ) ;
2021-05-15 00:24:39 +01:00
}
2020-08-27 13:09:35 +01:00
2021-05-15 00:24:39 +01:00
// Add the bot user
2022-01-04 03:39:53 +00:00
global . addUser ( "bot" , new User ( 3 , "SillyBot" , "bot" , Date . now ( ) ) ) ;
2020-09-07 19:23:06 +01:00
// Set the bot's position on the map
2021-05-15 00:24:39 +01:00
global . users [ "bot" ] . location [ 0 ] = 50 ;
global . users [ "bot" ] . location [ 1 ] = - 32 ;
2020-09-07 19:23:06 +01:00
2021-02-13 02:42:59 +00:00
global . DatabaseHelper = new DatabaseHelperClass ( config . databaseAddress , config . databasePort , config . databaseUsername , config . databasePassword , config . databaseName ) ;
2020-09-07 19:23:06 +01:00
2020-08-27 13:09:35 +01:00
// Start a loop that gets new data for users from the database for use on the user panel
2020-09-02 10:14:47 +01:00
// TODO: Some way of informing bancho that a user has set a score so details can be pulled down quickly
// Possible solution, TCP socket between the score submit server and bancho? redis? (score submit is on a different server, redis probably wouldn't work)
2020-08-27 13:09:35 +01:00
setInterval ( ( ) => {
2022-01-04 03:39:53 +00:00
for ( let i = 0 ; i < global . userKeys . length ; i ++ ) {
const User = global . users [ global . userKeys [ i ] ] ;
if ( User . id == 3 ) continue ; // Ignore the bot
// Bot: :(
2020-08-27 13:09:35 +01:00
2022-01-04 03:39:53 +00:00
User . getNewUserInformationFromDatabase ( ) ;
}
2020-08-27 13:09:35 +01:00
} , 10000 ) ;
// An array containing the last 15 messages in chat
// TODO: Bother making this
global . chatHistory = [ ] ;
2021-01-26 12:26:46 +00:00
global . addChatMessage = function ( msg ) {
2022-01-04 03:39:53 +00:00
if ( global . chatHistory . length == 15 ) {
global . chatHistory . splice ( 0 , 1 ) ;
global . chatHistory . push ( msg ) ;
} else {
global . chatHistory . push ( msg ) ;
}
2021-01-26 12:26:46 +00:00
}
2020-08-27 13:09:35 +01:00
global . StreamsHandler = new Streams ( ) ;
// An array containing all chat channels
// TODO: Send user chat channels and not have osu! crash
global . channels = [
2022-01-04 03:39:53 +00:00
{ channelName : "#osu" , channelTopic : "The main channel" , channelUserCount : 0 , locked : false } ,
{ channelName : "#userlog" , channelTopic : "Log about stuff doing go on yes very" , channelUserCount : 0 , locked : false } ,
{ channelName : "#lobby" , channelTopic : "Talk about multiplayer stuff" , channelUserCount : 0 , locked : false } ,
{ channelName : "#english" , channelTopic : "Talk in exclusively English" , channelUserCount : 0 , locked : false } ,
{ channelName : "#japanese" , channelTopic : "Talk in exclusively Japanese" , channelUserCount : 0 , locked : false } ,
2020-08-27 13:09:35 +01:00
] ;
// Create a stream for each chat channel
for ( let i = 0 ; i < global . channels . length ; i ++ ) {
2022-01-04 03:39:53 +00:00
global . StreamsHandler . addStream ( global . channels [ i ] . channelName , false ) ;
2020-08-27 13:09:35 +01:00
}
// Add a stream for the multiplayer lobby
global . StreamsHandler . addStream ( "multiplayer_lobby" , false ) ;
// Start stream checking interval
global . StreamsHandler . streamChecker ( 5000 ) ;
2020-09-02 10:14:10 +01:00
// Server stats
global . usersOnline = 0 ;
global . multiplayerMatches = [ 0 , 0 ]
global . httpRequestsPerLogInterval = 0 ;
const logInterval = 10 ; // Secs
setInterval ( ( ) => {
2022-01-04 03:39:53 +00:00
global . usersOnline = ( global . userKeys . length - 1 ) ;
global . multiplayerMatches = [ global . MultiplayerManager . matches . length , 0 ] ; // TODO: Respect private matches
2020-09-02 10:14:10 +01:00
2022-01-04 03:39:53 +00:00
fs . appendFile (
"server-stats.log" ,
` ${ global . usersOnline } | ${ global . multiplayerMatches [ 0 ] } , ${ global . multiplayerMatches [ 1 ] } | ${ global . httpRequestsPerLogInterval } | ${ Date . now ( ) } @ ` ,
( ) => { }
) ;
2020-09-02 10:14:10 +01:00
2022-01-04 03:39:53 +00:00
global . httpRequestsPerLogInterval = 0 ;
2020-09-02 10:14:10 +01:00
} , logInterval * 1000 ) ;
2021-01-26 12:26:46 +00:00
if ( ! fs . existsSync ( "tHMM.ds" ) ) fs . writeFileSync ( "tHMM.ds" , "0" ) ;
global . totalHistoricalMultiplayerMatches = parseInt ( fs . readFileSync ( "tHMM.ds" ) . toString ( ) ) ;
global . getAndAddToHistoricalMultiplayerMatches = function ( ) {
2022-01-04 03:39:53 +00:00
global . totalHistoricalMultiplayerMatches ++ ;
fs . writeFile ( "tHMM.ds" , ` ${ global . totalHistoricalMultiplayerMatches } ` , ( ) => { } ) ;
return global . totalHistoricalMultiplayerMatches ;
2021-01-26 12:26:46 +00:00
}
2020-08-27 13:09:35 +01:00
// Include packets
const ChangeAction = require ( "./Packets/ChangeAction.js" ) ,
2022-01-04 03:39:53 +00:00
SendPublicMessage = require ( "./Packets/SendPublicMessage.js" ) ,
Logout = require ( "./Packets/Logout.js" ) ,
Spectator = require ( "./Spectator.js" ) ,
SendPrivateMessage = require ( "./Packets/SendPrivateMessage.js" ) ,
MultiplayerManager = require ( "./MultiplayerManager.js" ) ,
2022-01-04 04:43:32 +00:00
SetAwayMessage = require ( "./Packets/SetAwayMessage.js" ) ,
2022-01-04 03:39:53 +00:00
ChannelJoin = require ( "./Packets/ChannelJoin.js" ) ,
ChannelPart = require ( "./Packets/ChannelPart.js" ) ,
AddFriend = require ( "./Packets/AddFriend.js" ) ,
RemoveFriend = require ( "./Packets/RemoveFriend.js" ) ,
UserPresenceBundle = require ( "./Packets/UserPresenceBundle.js" ) ,
UserPresence = require ( "./Packets/UserPresence.js" ) ,
UserStatsRequest = require ( "./Packets/UserStatsRequest.js" ) ,
MultiplayerInvite = require ( "./Packets/MultiplayerInvite.js" ) ,
TourneyMatchSpecialInfo = require ( "./Packets/TourneyMatchSpecialInfo.js" ) ,
TourneyMatchJoinChannel = require ( "./Packets/TourneyMatchSpecialInfo.js" ) ,
TourneyMatchLeaveChannel = require ( "./Packets/TourneyLeaveMatchChannel.js" ) ;
2020-08-27 13:09:35 +01:00
2021-09-22 19:17:11 +01:00
const emptyBuffer = Buffer . alloc ( 0 ) ;
2021-09-24 01:39:46 +01:00
2021-01-26 12:26:46 +00:00
// A class for managing everything multiplayer
global . MultiplayerManager = new MultiplayerManager ( ) ;
2021-02-13 02:15:25 +00:00
module . exports = async function ( req , res ) {
2022-01-04 03:39:53 +00:00
// Add to requests for logging
global . httpRequestsPerLogInterval ++ ;
// Get the client's token string and request data
const requestTokenString = req . header ( "osu-token" ) ,
requestData = req . packet ;
// Server's response & new client token
let responseData = emptyBuffer ;
// Check if the user is logged in
if ( requestTokenString == null ) {
// Client doesn't have a token yet, let's auth them!
const userData = parseUserData ( requestData ) ;
consoleHelper . printBancho ( ` New client connection. [User: ${ userData . username } ] ` ) ;
await loginHandler ( req , res , userData ) ;
} else {
// Client has a token, let's see what they want.
try {
// Get the current user
const PacketUser = getUserFromToken ( requestTokenString ) ;
// Make sure the client's token isn't invalid
if ( PacketUser != null ) {
// Create a new osu! packet reader
const osuPacketReader = new osu . Client . Reader ( requestData ) ;
// Parse current bancho packet
const PacketData = osuPacketReader . Parse ( ) ;
2022-01-04 04:42:26 +00:00
// Go through each packet sent by the client
PacketData . forEach ( CurrentPacket => {
2022-01-04 03:39:53 +00:00
switch ( CurrentPacket . id ) {
case packetIDs . client _changeAction :
ChangeAction ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _sendPublicMessage :
SendPublicMessage ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _logout :
Logout ( PacketUser ) ;
break ;
case packetIDs . client _requestStatusUpdate :
UserPresenceBundle ( PacketUser ) ;
break ;
case packetIDs . client _pong : // Pretty sure this is just a client ping
// so we probably don't do anything here
2022-01-04 04:42:26 +00:00
break ; // It's probably just the client wanting to pull data down. (That's exactly what it is)
2022-01-04 03:39:53 +00:00
case packetIDs . client _startSpectating :
Spectator . startSpectatingUser ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _spectateFrames :
Spectator . sendSpectatorFrames ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _stopSpectating :
Spectator . stopSpectatingUser ( PacketUser ) ;
break ;
case packetIDs . client _sendPrivateMessage :
SendPrivateMessage ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _joinLobby :
global . MultiplayerManager . userEnterLobby ( PacketUser ) ;
break ;
case packetIDs . client _partLobby :
global . MultiplayerManager . userLeaveLobby ( PacketUser ) ;
break ;
case packetIDs . client _createMatch :
global . MultiplayerManager . createMultiplayerMatch ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _joinMatch :
global . MultiplayerManager . joinMultiplayerMatch ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchChangeSlot :
PacketUser . currentMatch . moveToSlot ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchReady :
PacketUser . currentMatch . setStateReady ( PacketUser ) ;
break ;
case packetIDs . client _matchChangeSettings :
PacketUser . currentMatch . updateMatch ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchNotReady :
PacketUser . currentMatch . setStateNotReady ( PacketUser ) ;
break ;
case packetIDs . client _partMatch :
global . MultiplayerManager . leaveMultiplayerMatch ( PacketUser ) ;
break ;
// Also handles user kick if the slot has a user
case packetIDs . client _matchLock :
PacketUser . currentMatch . lockMatchSlot ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchNoBeatmap :
PacketUser . currentMatch . missingBeatmap ( PacketUser ) ;
break ;
case packetIDs . client _matchSkipRequest :
PacketUser . currentMatch . matchSkip ( PacketUser ) ;
break ;
case packetIDs . client _matchHasBeatmap :
PacketUser . currentMatch . notMissingBeatmap ( PacketUser ) ;
break ;
case packetIDs . client _matchTransferHost :
PacketUser . currentMatch . transferHost ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchChangeMods :
PacketUser . currentMatch . updateMods ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchStart :
PacketUser . currentMatch . startMatch ( ) ;
break ;
case packetIDs . client _matchLoadComplete :
PacketUser . currentMatch . matchPlayerLoaded ( PacketUser ) ;
break ;
case packetIDs . client _matchComplete :
PacketUser . currentMatch . onPlayerFinishMatch ( PacketUser ) ;
break ;
case packetIDs . client _matchScoreUpdate :
PacketUser . currentMatch . updatePlayerScore ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _matchFailed :
PacketUser . currentMatch . matchFailed ( PacketUser ) ;
break ;
case packetIDs . client _matchChangeTeam :
PacketUser . currentMatch . changeTeam ( PacketUser ) ;
break ;
case packetIDs . client _channelJoin :
ChannelJoin ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _channelPart :
ChannelPart ( PacketUser , CurrentPacket . data ) ;
break ;
2022-01-04 04:43:32 +00:00
case packetIDs . client _setAwayMessage :
SetAwayMessage ( PacketUser , CurrentPacket . data ) ;
break ;
2022-01-04 03:39:53 +00:00
case packetIDs . client _friendAdd :
AddFriend ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _friendRemove :
RemoveFriend ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _userStatsRequest :
UserStatsRequest ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _specialMatchInfoRequest :
TourneyMatchSpecialInfo ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _specialJoinMatchChannel :
TourneyMatchJoinChannel ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _specialLeaveMatchChannel :
TourneyMatchLeaveChannel ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _invite :
MultiplayerInvite ( PacketUser , CurrentPacket . data ) ;
break ;
case packetIDs . client _userPresenceRequest :
UserPresence ( PacketUser , PacketUser . id ) ; // Can't really think of a way to generalize this?
break ;
default :
// Ignore client_beatmapInfoRequest and client_receiveUpdates
if ( CurrentPacket . id == 68 || CurrentPacket . id == 79 ) break ;
// Print out unimplemented packet
console . dir ( CurrentPacket ) ;
break ;
}
// Concat current user queue into response data
2022-02-22 09:00:46 +00:00
// NOTE: NEVER EVER remove this buffer concat. For some reason the client freaks out if it's not there.
2022-01-04 03:39:53 +00:00
responseData = Buffer . concat ( [ responseData , PacketUser . queue ] , responseData . length + PacketUser . queue . length ) ;
PacketUser . clearQueue ( ) ;
2022-01-04 04:42:26 +00:00
} ) ;
2022-01-04 03:39:53 +00:00
} else {
// User's token is invlid, force a reconnect
consoleHelper . printBancho ( ` Forced client re-login (Token is invalid) ` ) ;
responseData = bakedResponses ( "reconnect" ) ;
}
} catch ( e ) {
console . error ( e ) ;
} finally {
2022-02-22 09:01:19 +00:00
// Only send the headers that we absolutely have to
res . removeHeader ( 'X-Powered-By' ) ;
res . removeHeader ( 'Date' ) ;
2022-01-04 03:39:53 +00:00
res . writeHead ( 200 , {
"cho-protocol" : global . protocolVersion ,
2022-02-22 09:01:19 +00:00
// Nice to have
2022-01-04 03:39:53 +00:00
"Connection" : "keep-alive" ,
"Keep-Alive" : "timeout=5, max=100" ,
} ) ;
2022-02-22 09:01:19 +00:00
// Send the prepared packet(s) to the client
2022-01-04 03:39:53 +00:00
res . end ( responseData ) ;
}
}
2021-01-26 12:26:46 +00:00
} ;