From 9489dfe2ea59ff51fdf4f12eca90924d755c8849 Mon Sep 17 00:00:00 2001 From: tgpethan Date: Tue, 26 Jan 2021 12:24:13 +0000 Subject: [PATCH] Multiplayer Updates --- server/BotCommandHandler.js | 68 +- server/Multiplayer.js | 756 -------------------- server/MultiplayerExtras/OsuBattleRoyale.js | 126 ++++ server/MultiplayerManager.js | 215 ++++++ server/MultiplayerMatch.js | 603 ++++++++++++++++ server/Packets/MultiplayerInvite.js | 2 +- server/Streams.js | 50 +- 7 files changed, 1032 insertions(+), 788 deletions(-) delete mode 100644 server/Multiplayer.js create mode 100644 server/MultiplayerExtras/OsuBattleRoyale.js create mode 100644 server/MultiplayerManager.js create mode 100644 server/MultiplayerMatch.js diff --git a/server/BotCommandHandler.js b/server/BotCommandHandler.js index 8216db5..c09ad1e 100644 --- a/server/BotCommandHandler.js +++ b/server/BotCommandHandler.js @@ -1,6 +1,7 @@ const osu = require("osu-packet"), maths = require("./util/Maths.js"), - Multiplayer = require("./Multiplayer.js"); + OsuBattleRoyale = require("./MultiplayerExtras/OsuBattleRoyale.js"); + //Multiplayer = require("./Multiplayer.js"); module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false) { if (Message[0] != "!") return; @@ -9,7 +10,7 @@ module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false let responseMessage = ""; - let gay = null; + let commandBanchoPacketWriter = null; switch (command) { case "!help": @@ -26,7 +27,8 @@ module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false case "mp": responseMessage = "Multiplayer Commands:" + "\n!mp start - Starts a multiplayer match with a delay" + - "\n!mp abort - Aborts the currently running multiplayer match"; + "\n!mp abort - Aborts the currently running multiplayer match" + + "\n!mp obr - Enables Battle Royale mode"; break; case "admin": @@ -69,29 +71,30 @@ module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false break; case "!msg": - gay = new osu.Bancho.Writer; + commandBanchoPacketWriter = new osu.Bancho.Writer; - gay.RTX(args[1]); + commandBanchoPacketWriter.RTX(args[1]); - //User.addActionToQueue(gay.toBuffer()); - global.StreamsHandler.sendToStream(Stream, gay.toBuffer, null); + global.StreamsHandler.sendToStream(Stream, commandBanchoPacketWriter.toBuffer, null); break; case "!fuckoff": - gay = new osu.Bancho.Writer; + commandBanchoPacketWriter = new osu.Bancho.Writer; - gay.Ping(0); + commandBanchoPacketWriter.Ping(0); - User.addActionToQueue(gay.toBuffer); + User.addActionToQueue(commandBanchoPacketWriter.toBuffer); break; case "!mp": if (!IsCalledFromMultiplayer) return; + if (User.currentMatch.matchStartCountdownActive) return; if (args.length == 1) return; switch (args[1]) { case "start": if (args.length > 3) return; if (`${parseInt(args[2])}` != "NaN") { + User.currentMatch.matchStartCountdownActive = true; let countdown = parseInt(args[2]); let intervalRef = setInterval(() => { let local_osuPacketWriter = new osu.Bancho.Writer; @@ -112,19 +115,38 @@ module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false senderId: global.users[0].id }); global.StreamsHandler.sendToStream(Stream, local_osuPacketWriter.toBuffer, null); - setTimeout(() => Multiplayer.startMatch(User), 1000); + User.currentMatch.matchStartCountdownActive = false; + setTimeout(() => User.currentMatch.startMatch(), 1000); clearInterval(intervalRef); } }, 1000); } else { responseMessage = "Good luck, have fun!"; - setTimeout(() => Multiplayer.startMatch(User), 1000); + setTimeout(() => User.currentMatch.startMatch(), 1000); } break; case "abort": - if (args.length > 2) return; - Multiplayer.finishMatch(User); + //if (args.length > 2) return; + User.currentMatch.finishMatch(); + break; + + case "obr": + if (User.currentMatch.multiplayerExtras != null) { + if (User.currentMatch.multiplayerExtras.name == "osu! Battle Royale") { + commandBanchoPacketWriter = new osu.Bancho.Writer; + commandBanchoPacketWriter.SendMessage({ + sendingClient: global.users[0].username, + message: "osu! Battle Royale has been disabled!", + target: "#multiplayer", + senderId: global.users[0].id + }); + User.currentMatch.multiplayerExtras = null; + global.StreamsHandler.sendToStream(Stream, commandBanchoPacketWriter.toBuffer, null); + } + else enableOBR(User, Stream, commandBanchoPacketWriter); + } + else enableOBR(User, Stream, commandBanchoPacketWriter); break; default: @@ -152,4 +174,22 @@ module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false } } global.StreamsHandler.sendToStream(Stream, osuPacketWriter.toBuffer, null); +} + +function enableOBR(User, Stream, commandBanchoPacketWriter) { + User.currentMatch.multiplayerExtras = new OsuBattleRoyale(User.currentMatch); + commandBanchoPacketWriter = new osu.Bancho.Writer; + commandBanchoPacketWriter.SendMessage({ + sendingClient: global.users[0].username, + message: "osu! Battle Royale has been enabled!", + target: "#multiplayer", + senderId: global.users[0].id + }); + commandBanchoPacketWriter.SendMessage({ + sendingClient: global.users[0].username, + message: "New Multiplayer Rules Added:\n - Players that are in a failed state by the end of the map get eliminated\n - The player(s) with the lowest score get eliminated", + target: "#multiplayer", + senderId: global.users[0].id + }); + global.StreamsHandler.sendToStream(Stream, commandBanchoPacketWriter.toBuffer, null); } \ No newline at end of file diff --git a/server/Multiplayer.js b/server/Multiplayer.js deleted file mode 100644 index 2b9ab05..0000000 --- a/server/Multiplayer.js +++ /dev/null @@ -1,756 +0,0 @@ -const osu = require("osu-packet"), - getUserById = require("./util/getUserById.js"), - StatusUpdate = require("./Packets/StatusUpdate.js"); - -module.exports = { - userEnterLobby:function(currentUser) { - // If the user is currently already in a match force them to leave - if (currentUser.currentMatch != null) { - this.leaveMatch(currentUser); - currentUser.currentMatch = null; - } - - // Add user to the stream for the lobby - global.StreamsHandler.addUserToStream("multiplayer_lobby", currentUser.id); - - const osuPacketWriter1 = new osu.Bancho.Writer; - let userIds = []; - - // Add the ID of every user connected to the server to an array - for (let i = 0; i < global.users.length; i++) { - userIds.push(global.users[i].id); - } - - // Send all user ids back to the client - osuPacketWriter1.UserPresenceBundle(userIds); - - // Send user ids to all users in the lobby - global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter1.toBuffer, null); - - // Loop through all matches - for (let i = 0; i < global.matches.length; i++) { - // Loop through all the users in this match - for (let i1 = 0; i1 < global.matches[i][1].slots.length; i1++) { - const slot = global.matches[i][1].slots[i1]; - // Make sure there is a player / the slot is not locked - if (slot.playerId == -1 || slot.status == 2) continue; - const osuPacketWriter = new osu.Bancho.Writer; - - // Get user in this slot - const User = getUserById(slot.playerId); - - // Get user score info from the database - const userScoreDB = global.DatabaseHelper.getFromDB(`SELECT * FROM users_modes_info WHERE user_id = ${User.id} AND mode_id = ${User.playMode} LIMIT 1`); - - let UserStatusObject = { - userId: User.id, - status: User.actionID, - statusText: User.actionText, - beatmapChecksum: User.beatmapChecksum, - currentMods: User.currentMods, - playMode: User.playMode, - beatmapId: User.beatmapID, - rankedScore: userScoreDB.ranked_score, - accuracy: userScoreDB.avg_accuracy / 100, // Scale of 0 to 1 - playCount: userScoreDB.playcount, - totalScore: userScoreDB.total_score, - rank: User.rank, - performance: userScoreDB.pp_raw - }; - - // Send user status back for client display - osuPacketWriter.HandleOsuUpdate(UserStatusObject); - - // Send this data back to every user in the lobby - global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); - } - const osuPacketWriter = new osu.Bancho.Writer; - - // List the match on the client - osuPacketWriter.MatchNew(global.matches[i][1]); - - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - } - const osuPacketWriter = new osu.Bancho.Writer; - - // Add the user to the #lobby channel - osuPacketWriter.ChannelJoinSuccess("#lobby"); - if (!global.StreamsHandler.isUserInStream("#lobby", currentUser.id)) - global.StreamsHandler.addUserToStream("#lobby", currentUser.id); - - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - }, - - userLeaveLobby:function(currentUser) { - // Remove user from the stream for the multiplayer lobby if they are a part of it - if (global.StreamsHandler.isUserInStream("multiplayer_lobby", currentUser.id)) - global.StreamsHandler.removeUserFromStream("multiplayer_lobby", currentUser.id); - }, - - updateMatchListing:function() { - const osuPacketWriter1 = new osu.Bancho.Writer; - let userIds = []; - - // Add the ID of every user connected to the server to an array - for (let i = 0; i < global.users.length; i++) { - userIds.push(global.users[i].id); - } - - // Send all user ids back to the client - osuPacketWriter1.UserPresenceBundle(userIds); - - // Send user ids to all users in the lobby - global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter1.toBuffer, null); - // List through all matches - for (let i = 0; i < global.matches.length; i++) { - // List through all users in the match - for (let i1 = 0; i1 < global.matches[i][1].slots.length; i1++) { - const slot = global.matches[i][1].slots[i1]; - // Make sure the slot has a user in it / isn't locked - if (slot.playerId == -1 || slot.status == 2) continue; - const osuPacketWriter = new osu.Bancho.Writer; - - // Get the user in this slot - const User = getUserById(slot.playerId); - - // Get user score info from the database - const userScoreDB = global.DatabaseHelper.getFromDB(`SELECT * FROM users_modes_info WHERE user_id = ${User.id} AND mode_id = ${User.playMode} LIMIT 1`); - - let UserStatusObject = { - userId: User.id, - status: User.actionID, - statusText: User.actionText, - beatmapChecksum: User.beatmapChecksum, - currentMods: User.currentMods, - playMode: User.playMode, - beatmapId: User.beatmapID, - rankedScore: userScoreDB.ranked_score, - accuracy: userScoreDB.avg_accuracy / 100, // Scale of 0 to 1 - playCount: userScoreDB.playcount, - totalScore: userScoreDB.total_score, - rank: User.rank, - performance: userScoreDB.pp_raw - }; - - // Send user status back for client display - osuPacketWriter.HandleOsuUpdate(UserStatusObject); - - // Send this data back to every user in the lobby - global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); - } - const osuPacketWriter = new osu.Bancho.Writer; - - // List the match on the client - osuPacketWriter.MatchNew(global.matches[i][1]); - - // Send this data back to every user in the lobby - global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); - } - }, - - createMultiplayerMatch:function(currentUser, data) { - const osuPacketWriter = new osu.Bancho.Writer; - - // If there is no password instead set the password param to null - if (data.gamePassword == '') data.gamePassword == null; - - // Create a match with the data given by the creating client - let NewMatchObject = { - matchId: global.matches.length, - inProgress: false, - matchType: 0, - activeMods: 0, - gameName: data.gameName, - gamePassword: data.gamePassword, - beatmapName: data.beatmapName, - beatmapId: data.beatmapId, - beatmapChecksum: data.beatmapChecksum, - slots: data.slots, - host: currentUser.id, - playMode: 0, - matchScoringType: 0, - matchTeamType: 0, - specialModes: 0, - hidden: false, - seed: data.seed - } - - for (let i = 0; i < NewMatchObject.slots.length; i++) { - let s = NewMatchObject.slots[i]; - s.mods = 0; - } - - // Update the status of the current user - StatusUpdate(currentUser, currentUser.id); - osuPacketWriter.MatchNew(NewMatchObject); - - // Queue match creation for user - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - - global.StreamsHandler.addStream(`mp_${data.gameName.split(" ").join("-")}`, true, NewMatchObject.matchId); - - global.matches.push([`mp_${data.gameName.split(" ").join("-")}`, NewMatchObject]); - - this.updateMatchListing(); - - // Join the user to the newly created match - this.joinMultiplayerMatch(currentUser, { - matchId: NewMatchObject.matchId, - gamePassword: NewMatchObject.gamePassword - }); - }, - - joinMultiplayerMatch:function(currentUser, data) { - try { - let osuPacketWriter = new osu.Bancho.Writer; - const osuPacketWriter1 = new osu.Bancho.Writer; - - const streamName = global.matches[data.matchId][0]; - const mpLobby = global.matches[data.matchId][1]; - - let full = true; - // Loop through all slots to find an empty one - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the slot doesn't have a player in it / the slot is locked - if (slot.playerId !== -1 || slot.status === 2) continue; - - // Slot is empty and not locked, we can join the match! - full = false; - slot.playerId = currentUser.id; - currentUser.matchSlotId = i; - slot.status = 4; - break; - } - - osuPacketWriter1.MatchUpdate(mpLobby); - osuPacketWriter.MatchJoinSuccess(mpLobby); - - if (full) { - // Inform the client that they can't join the match - osuPacketWriter = new osu.Bancho.Writer; - osuPacketWriter.MatchJoinFail(); - } - - // Set the user's current match to this match - currentUser.currentMatch = data.matchId; - - // Add user to the stream for the match - global.StreamsHandler.addUserToStream(streamName, currentUser.id); - - // Inform all users in the match that a new user has joined - global.StreamsHandler.sendToStream(streamName, osuPacketWriter1.toBuffer, null); - - osuPacketWriter.ChannelJoinSuccess("#multiplayer"); - - // Inform joining client they they have joined the match - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - - // Update the match listing for all users in the lobby since - // A user has joined a match - this.updateMatchListing(); - } catch (e) { - const osuPacketWriter = new osu.Bancho.Writer; - - osuPacketWriter.MatchJoinFail(); - - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - - this.updateMatchListing(); - } - }, - - setReadyState:function(currentUser, state) { - // Get the match the user is in - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - // Loop though all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Check if the player in this slot is this user - if (slot.playerId == currentUser.id) { - // Turn on or off the user's ready state - if (state) slot.status = 8; - else slot.status = 4; - break; - } - } - - osuPacketWriter.MatchUpdate(mpLobby); - - // Send this update to all users in the stream - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - }, - - sendMatchUpdate:function(currentUser) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - osuPacketWriter.MatchUpdate(mpLobby); - - // Update all users in the match with new match information - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - }, - - updateMatch:function(currentUser, data) { - // Update match with new data - global.matches[currentUser.currentMatch][1] = data; - const osuPacketWriter = new osu.Bancho.Writer; - - osuPacketWriter.MatchUpdate(global.matches[currentUser.currentMatch][1]); - - // Send this new match data to all users in the match - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Update the match listing in the lobby to reflect these changes - this.updateMatchListing(); - }, - - moveToSlot:function(currentUser, data) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - let currentUserData, slotIndex; - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the user in this slot is the user we want - if (slot.playerId != currentUser.id) continue; - - currentUserData = slot; - slotIndex = i; - break; - } - - // Set the new slot's data to the user's old slot data - mpLobby.slots[data].playerId = currentUserData.playerId; - currentUser.matchSlotId = data; - mpLobby.slots[data].status = currentUserData.status; - - // Set the old slot's data to open - mpLobby.slots[slotIndex].playerId = -1; - mpLobby.slots[slotIndex].status = 1; - - osuPacketWriter.MatchUpdate(mpLobby); - - // Send this change to all users in the match - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Update the match listing in the lobby to reflect this change - this.updateMatchListing(); - }, - - kickPlayer:function(currentUser, data) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - // Make sure the user attempting to kick / lock is the host of the match - if (mpLobby.host != currentUser.id) return; - - // Get the data of the slot at the index sent by the client - const slot = mpLobby.slots[data]; - let cachedPlayerId = slot.playerId; - - // If the slot is empty lock instead of kicking - if (slot.playerId === -1) { // Slot is empty, lock it - if (slot.status === 1) slot.status = 2; - else slot.status = 1; - } - // The slot isn't empty, prepare to kick the player - else { - const kickedPlayer = getUserById(slot.playerId); - kickedPlayer.matchSlotId = -1; - slot.playerId = -1; - slot.status = 1; - } - - osuPacketWriter.MatchUpdate(mpLobby); - - // Inform all users in the match of the change - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Update the match listing in the lobby listing to reflect this change - this.updateMatchListing(); - - if (cachedPlayerId !== null || cachedPlayerId !== -1) { - // Remove the kicked user from the match stream - global.StreamsHandler.removeUserFromStream(global.matches[currentUser.currentMatch][0], cachedPlayerId); - } - }, - - matchSkip:function(currentUser) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - - if (global.matches[currentUser.currentMatch][2] == null) { - global.matches[currentUser.currentMatch][2] = []; - - const skippedSlots = global.matches[currentUser.currentMatch][2]; - - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // 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}); - } - - - } - - const skippedSlots = global.matches[currentUser.currentMatch][2]; - - for (let i = 0; i < skippedSlots.length; i++) { - // If loadslot belongs to this user then set loaded to true - if (skippedSlots[i].playerId == currentUser.id) { - skippedSlots[i].skipped = true; - } - } - - let allSkipped = true; - for (let i = 0; i < skippedSlots.length; i++) { - if (skippedSlots[i].skipped) continue; - - // A user hasn't finished playing - allSkipped = false; - } - - // All players have finished playing, finish the match - if (allSkipped) { - const osuPacketWriter = new osu.Bancho.Writer; - osuPacketWriter.MatchPlayerSkipped(currentUser.id); - osuPacketWriter.MatchSkip(); - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - global.matches[currentUser.currentMatch][2] = null; - } else { - const osuPacketWriter = new osu.Bancho.Writer; - osuPacketWriter.MatchPlayerSkipped(currentUser.id); - - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - } - }, - - missingBeatmap:function(currentUser, state) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the user in the slot is the user we want to update - if (slot.playerId != currentUser.id) continue; - - // If the user is missing the beatmap set the status to reflect it - if (state) slot.status = 16; - // The user is not missing the beatmap, set the status to normal - else slot.status = 4; - break; - } - - osuPacketWriter.MatchUpdate(mpLobby); - - // Inform all users in the match of this change - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - }, - - transferHost:function(currentUser, data) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - - // Get the information of the user that the host is being transfered to - const newUser = getUserById(mpLobby.slots[data].playerId); - - // Set the lobby's host to the new user - mpLobby.host = newUser.id; - - osuPacketWriter.MatchUpdate(mpLobby); - - // Inform all clients in the match of the change - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - }, - - // TODO: Allow freemod to work - updateMods(currentUser, data) { - // Make sure the person updating mods is the host of the match - // TODO: Add a check here for is freemod is enabled - console.log(global.matches[currentUser.currentMatch][1]); - console.log(data); - if (Object.keys(global.matches[currentUser.currentMatch][1].slots[0]).includes("mods")) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - const osuPacketWriter = new osu.Bancho.Writer; - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - if (slot.playerId === currentUser.id) { - slot.mods = data; - break; - } - } - - osuPacketWriter.MatchUpdate(global.matches[currentUser.currentMatch][1]); - - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - } else { - if (global.matches[currentUser.currentMatch][1].host !== currentUser.id) return; - const osuPacketWriter = new osu.Bancho.Writer; - - // Change the matches mods to these new mods - // TODO: Do this per user if freemod is enabled - global.matches[currentUser.currentMatch][1].activeMods = data; - - osuPacketWriter.MatchUpdate(global.matches[currentUser.currentMatch][1]); - - // Inform all users in the match of the change - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - } - - // Update match listing in the lobby to reflect this change - this.updateMatchListing(); - }, - - startMatch(currentUser) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - // Make sure the match is not already in progress - // The client sometimes double fires the start packet - if (mpLobby.inProgress) return; - mpLobby.inProgress = true; - // Create array for monitoring users until they are ready to play - global.matches[currentUser.currentMatch][2] = []; - const loadedSlots = global.matches[currentUser.currentMatch][2]; - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // 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 - loadedSlots.push({playerId: slot.playerId, loaded: false}); - } - const osuPacketWriter = new osu.Bancho.Writer; - - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the slot has a user in it - if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; - - // Set the user's status to playing - slot.status = 32; - } - - osuPacketWriter.MatchStart(mpLobby); - - // Inform all users in the match that it has started - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Update all users in the match with new info - this.sendMatchUpdate(currentUser); - - // Update match listing in lobby to show the game is in progress - this.updateMatchListing(); - }, - - setPlayerLoaded:function(currentUser) { - const loadedSlots = global.matches[currentUser.currentMatch][2]; - - // Loop through all user load check items - for (let i = 0; i < loadedSlots.length; i++) { - // If loadslot belongs to this user then set loaded to true - if (loadedSlots[i].playerId == currentUser.id) { - loadedSlots[i].loaded = true; - } - } - - // Loop through all loaded slots and check if all users are loaded - let allLoaded = true; - for (let i = 0; i < loadedSlots.length; i++) { - if (loadedSlots[i].loaded) continue; - - // A user wasn't loaded, keep waiting. - allLoaded = false; - break; - } - - // All players have loaded the beatmap, start playing. - if (allLoaded) { - let osuPacketWriter = new osu.Bancho.Writer; - osuPacketWriter.MatchAllPlayersLoaded(); - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Blank out user loading array - global.matches[currentUser.currentMatch][2] = null; - } - }, - - onPlayerFinishMatch:function(currentUser) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - // If user loading slots do not exist - if (global.matches[currentUser.currentMatch][2] == null) { - global.matches[currentUser.currentMatch][2] = []; - // Repopulate user loading slots again - const loadedSlots = global.matches[currentUser.currentMatch][2]; - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // 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 - loadedSlots.push({playerId: slot.playerId, loaded: false}); - } - } - - const loadedSlots = global.matches[currentUser.currentMatch][2]; - - // Loop through all loaded slots to make sure all users have finished playing - for (let i = 0; i < loadedSlots.length; i++) { - if (loadedSlots[i].playerId == currentUser.id) { - loadedSlots[i].loaded = true; - } - } - - let allLoaded = true; - for (let i = 0; i < loadedSlots.length; i++) { - if (loadedSlots[i].loaded) continue; - - // A user hasn't finished playing - allLoaded = false; - } - - // All players have finished playing, finish the match - if (allLoaded) this.finishMatch(currentUser); - }, - - finishMatch:function(currentUser) { - const mpLobby = global.matches[currentUser.currentMatch][1]; - if (!mpLobby.inProgress) return; - global.matches[currentUser.currentMatch][2] = []; - mpLobby.inProgress = false; - let osuPacketWriter = new osu.Bancho.Writer; - - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the slot has a user - if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; - - // Set the user's status back to normal from playing - slot.status = 4; - } - - osuPacketWriter.MatchComplete(); - - // Inform all users in the match that it is complete - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - // Update all users in the match with new info - this.sendMatchUpdate(currentUser); - - // Update match info in the lobby to reflect that the match has finished - this.updateMatchListing(); - }, - - updatePlayerScore:function(currentUser, data) { - const osuPacketWriter = new osu.Bancho.Writer; - - // Make sure the user's slot ID is not invalid - if (currentUser.matchSlotId == -1) return; - - // Get the user's current slotID and append it to the givien data, just incase. - data.id = currentUser.matchSlotId; - - osuPacketWriter.MatchScoreUpdate(data); - - // Send the newly updated score to all users in the match - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - }, - - leaveMatch:function(currentUser) { - try { - const mpLobby = global.matches[currentUser.currentMatch][1]; - - let userInMatch = false; - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Check if the user is in this slot - if (slot.playerId == currentUser.id) { - userInMatch = true; - break; - } - } - - // Make sure we don't run more than once - // Again, client double firing packets. - if (!userInMatch) return; - - let osuPacketWriter = new osu.Bancho.Writer; - - // Loop through all slots in the match - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Make sure the user is in this slot - if (slot.playerId != currentUser.id) continue; - - // Set the slot's status to avaliable - slot.playerId = -1; - slot.status = 1; - - break; - } - - osuPacketWriter.MatchUpdate(mpLobby); - - // Remove the leaving user from the match's stream - global.StreamsHandler.removeUserFromStream(global.matches[currentUser.currentMatch][0], currentUser.id); - - // Inform all users in the match that the leaving user has left - global.StreamsHandler.sendToStream(global.matches[currentUser.currentMatch][0], osuPacketWriter.toBuffer, null); - - osuPacketWriter = new osu.Bancho.Writer; - - // Remove user from the multiplayer channel for the match - osuPacketWriter.ChannelRevoked("#multiplayer"); - - currentUser.addActionToQueue(osuPacketWriter.toBuffer); - - let empty = true; - // Check if the match is empty - for (let i = 0; i < mpLobby.slots.length; i++) { - const slot = mpLobby.slots[i]; - // Check if the slot is avaliable - if (slot.playerId === -1) continue; - - // There is a user in the match - empty = false; - break; - } - - // The match is empty, proceed to remove it. - if (empty) { - let matchIndex; - // Loop through all matches - for (let i = 0; i < global.matches.length; i++) { - // If the match matches the match the user has left - if (global.matches[i][0] == global.matches[currentUser.currentMatch][0]) { - matchIndex = i; - break; - } - } - - // Make sure we got a match index - if (matchIndex == null) return; - - // Remove this match from the list of active matches - global.matches.splice(matchIndex, 1); - } - } catch (e) { } - // Update the match listing to reflect this change (either removal or user leaving) - this.updateMatchListing(); - - // Delay a 2nd match listing update - setTimeout(() => { - this.updateMatchListing(); - }, 1000); - } -} \ No newline at end of file diff --git a/server/MultiplayerExtras/OsuBattleRoyale.js b/server/MultiplayerExtras/OsuBattleRoyale.js new file mode 100644 index 0000000..afa5bf6 --- /dev/null +++ b/server/MultiplayerExtras/OsuBattleRoyale.js @@ -0,0 +1,126 @@ +const osu = require("osu-packet"), + MultiplayerMatch = require("../MultiplayerMatch.js"), + getUserById = require("../util/getUserById.js"); + +module.exports = class { + constructor(MultiplayerMatchClass = new MultiplayerMatch()) { + this.name = "osu! Battle Royale"; + this.MultiplayerMatch = MultiplayerMatchClass; + } + + onMatchFinished(playerScores = [{playerId:0,slotId:0,score:0,isCurrentlyFailed:false}]) { + let lowestScore = 9999999999999999; + for (let i = 0; i < playerScores.length; i++) { + const playerScore = playerScores[i]; + if (playerScore.score < lowestScore) lowestScore = playerScore.score; + } + + let everyoneHasTheSameScore = true; + for (let i = 0; i < playerScores.length; i++) { + if (playerScores[i].score != lowestScore) everyoneHasTheSameScore = false; + } + + // Everyone has the same score, we don't need to kick anyone + if (everyoneHasTheSameScore) return; + + // Kick everyone with the lowest score + for (let i = 0; i < playerScores.length; i++) { + // Kick players if they have the lowest score or they are in a failed state + if (playerScores[i].score == lowestScore || playerScores[i].isCurrentlyFailed) { + let osuPacketWriter = new osu.Bancho.Writer; + // Get the slot this player is in + const slot = this.MultiplayerMatch.slots[playerScores[i].slotId]; + // Get the kicked player's user class + const kickedPlayer = getUserById(slot.playerId); + // Remove the kicked player's referance to the slot they were in + kickedPlayer.matchSlotId = -1; + // Lock the slot the kicked player was in + slot.playerId = -1; + slot.status = 2; + // Remove the kicked player from the match's stream + global.StreamsHandler.removeUserFromStream(this.MultiplayerMatch.matchStreamName, kickedPlayer.id); + // Remove the kicked player's referance this this match + kickedPlayer.currentMatch = null; + + // Inform the kicked user's client that they were kicked + osuPacketWriter.MatchUpdate(this.MultiplayerMatch.createOsuMatchJSON()); + osuPacketWriter.SendMessage({ + sendingClient: global.users[0].username, + message: "You were eliminated from the match!", + target: global.users[0].username, + senderId: global.users[0].id + }); + + kickedPlayer.addActionToQueue(osuPacketWriter.toBuffer); + + osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.SendMessage({ + sendingClient: global.users[0].username, + message: `${kickedPlayer.username} was eliminated from the match!`, + target: "#multiplayer", + senderId: global.users[0].id + }); + + global.StreamsHandler.sendToStream(this.MultiplayerMatch.matchStreamName, osuPacketWriter.toBuffer, null); + } + } + + let numberOfPlayersRemaining = 0; + for (let i = 0; i < playerScores.length; i++) { + const slot = this.MultiplayerMatch.slots[playerScores[i].slotId]; + + if (slot.playerId !== -1 && slot.status !== 2) { + numberOfPlayersRemaining++; + } + } + + let playerClassContainer = null; + let remainingWriterContainer = null; + let i = 0; + + if (numberOfPlayersRemaining == 1) { + for (let i1 = 0; i1 < playerScores.length; i++) { + const slot = this.MultiplayerMatch.slots[playerScores[i].slotId]; + if (slot.playerId !== -1 && slot.status !== 2) { + playerClassContainer = getUserById(slot.playerId); + break; + } + } + } + + switch (numberOfPlayersRemaining) { + case 0: + remainingWriterContainer = new osu.Bancho.Writer; + remainingWriterContainer.SendMessage({ + sendingClient: global.users[0].username, + message: "Everyone was eliminated from the match! Nobody wins.", + target: global.users[0].username, + senderId: global.users[0].id + }); + for (i = 0; i < playerScores.length; i++) { + playerClassContainer = getUserById(playerScores[i].playerId); + playerClassContainer.addActionToQueue(remainingWriterContainer.toBuffer); + } + break; + + case 1: + remainingWriterContainer = new osu.Bancho.Writer; + remainingWriterContainer.SendMessage({ + sendingClient: global.users[0].username, + message: "You are the last one remaining, you win!", + target: global.users[0].username, + senderId: global.users[0].id + }); + playerClassContainer.addActionToQueue(remainingWriterContainer.toBuffer); + break; + + default: + break; + } + + this.MultiplayerMatch.sendMatchUpdate(); + // Update the match listing for users in the multiplayer lobby + global.MultiplayerManager.updateMatchListing(); + } +} \ No newline at end of file diff --git a/server/MultiplayerManager.js b/server/MultiplayerManager.js new file mode 100644 index 0000000..b6c4a03 --- /dev/null +++ b/server/MultiplayerManager.js @@ -0,0 +1,215 @@ +const osu = require("osu-packet"), + getUserById = require("./util/getUserById.js"), + UserPresenceBundle = require("./Packets/UserPresenceBundle.js"), + UserPresence = require("./Packets/UserPresence.js"), + StatusUpdate = require("./Packets/StatusUpdate.js"), + MultiplayerMatch = require("./MultiplayerMatch.js"); + +module.exports = class { + constructor() { + this.matches = []; + } + + userEnterLobby(currentUser) { + // If the user is currently already in a match force them to leave + if (currentUser.currentMatch != null) + currentUser.currentMatch.leaveMatch(currentUser); + + // Add user to the stream for the lobby + global.StreamsHandler.addUserToStream("multiplayer_lobby", currentUser.id); + + // Send user ids of all online users to all users in the lobby + global.StreamsHandler.sendToStream("multiplayer_lobby", UserPresenceBundle(currentUser, false), null); + + // Loop through all matches + for (let i = 0; i < this.matches.length; i++) { + // Loop through all the users in this match + for (let i1 = 0; i1 < this.matches[i].slots.length; i1++) { + const slot = this.matches[i].slots[i1]; + // Make sure there is a player / the slot is not locked + if (slot.playerId == -1 || slot.status == 2) continue; + + // Send information for this user to all users in the lobby + global.StreamsHandler.sendToStream("multiplayer_lobby", UserPresence(currentUser, slot.playerId, false), null); + global.StreamsHandler.sendToStream("multiplayer_lobby", StatusUpdate(currentUser, slot.playerId, false), null); + } + const osuPacketWriter = new osu.Bancho.Writer; + + // List the match on the client + osuPacketWriter.MatchNew(this.matches[i].createOsuMatchJSON()); + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + } + const osuPacketWriter = new osu.Bancho.Writer; + + // Add the user to the #lobby channel + if (!global.StreamsHandler.isUserInStream("#lobby", currentUser.id)) { + global.StreamsHandler.addUserToStream("#lobby", currentUser.id); + osuPacketWriter.ChannelJoinSuccess("#lobby"); + } + + currentUser.addActionToQueue(osuPacketWriter.toBuffer); + } + + userLeaveLobby(currentUser) { + // Remove user from the stream for the multiplayer lobby if they are a part of it + if (global.StreamsHandler.isUserInStream("multiplayer_lobby", currentUser.id)) + global.StreamsHandler.removeUserFromStream("multiplayer_lobby", currentUser.id); + } + + updateMatchListing() { + // Send user ids of all online users to all users in the lobby + global.StreamsHandler.sendToStream("multiplayer_lobby", UserPresenceBundle(null, false), null); + + // List through all matches + for (let i = 0; i < this.matches.length; i++) { + // List through all users in the match + for (let i1 = 0; i1 < this.matches[i].slots.length; i1++) { + const slot = this.matches[i].slots[i1]; + // Make sure the slot has a user in it / isn't locked + if (slot.playerId == -1 || slot.status == 2) continue; + + // Send information for this user to all users in the lobby + global.StreamsHandler.sendToStream("multiplayer_lobby", UserPresence(null, slot.playerId, false), null); + global.StreamsHandler.sendToStream("multiplayer_lobby", StatusUpdate(null, slot.playerId, false), null); + } + const osuPacketWriter = new osu.Bancho.Writer; + + // List the match on the client + osuPacketWriter.MatchNew(this.matches[i].createOsuMatchJSON()); + + // Send this data back to every user in the lobby + global.StreamsHandler.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null); + } + } + + createMultiplayerMatch(MatchHost, MatchData) { + let matchClass = null; + this.matches.push(matchClass = new MultiplayerMatch(MatchHost, MatchData)); + + // Join the user to the newly created match + this.joinMultiplayerMatch(MatchHost, { + matchId: matchClass.matchId, + gamePassword: matchClass.gamePassword + }); + } + + joinMultiplayerMatch(JoiningUser, JoinInfo) { + try { + let osuPacketWriter = new osu.Bancho.Writer; + const osuPacketWriter1 = new osu.Bancho.Writer; + + let matchIndex = 0; + for (let i = 0; i < this.matches.length; i++) { + if (this.matches[i].matchId == JoinInfo.matchId) { + matchIndex = i; + break; + } + } + + const streamName = this.matches[matchIndex].matchStreamName; + const match = this.matches[matchIndex]; + + let full = true; + // Loop through all slots to find an empty one + for (let i = 0; i < match.slots.length; i++) { + const slot = match.slots[i]; + // Make sure the slot doesn't have a player in it / the slot is locked + if (slot.playerId !== -1 || slot.status === 2) continue; + + // Slot is empty and not locked, we can join the match! + full = false; + slot.playerId = JoiningUser.id; + JoiningUser.matchSlotId = i; + slot.status = 4; + break; + } + + const matchJSON = match.createOsuMatchJSON(); + osuPacketWriter1.MatchUpdate(matchJSON); + osuPacketWriter.MatchJoinSuccess(matchJSON); + + if (full) { + // Inform the client that they can't join the match + osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchJoinFail(); + + return JoiningUser.addActionToQueue(osuPacketWriter.toBuffer); + } + + // Set the user's current match to this match + JoiningUser.currentMatch = match; + + // Add user to the stream for the match + global.StreamsHandler.addUserToStream(streamName, JoiningUser.id); + + // Inform all users in the match that a new user has joined + global.StreamsHandler.sendToStream(streamName, osuPacketWriter1.toBuffer, null); + + osuPacketWriter.ChannelJoinSuccess("#multiplayer"); + + // Inform joining client they they have joined the match + JoiningUser.addActionToQueue(osuPacketWriter.toBuffer); + + // Update the match listing for all users in the lobby since + // A user has joined a match + this.updateMatchListing(); + } catch (e) { + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchJoinFail(); + + JoiningUser.addActionToQueue(osuPacketWriter.toBuffer); + + this.updateMatchListing(); + } + } + + leaveMultiplayerMatch(MatchUser) { + // Make sure the user is in a match + if (MatchUser.currentMatch == null) return; + + const mpLobby = MatchUser.currentMatch.leaveMatch(MatchUser); + + let empty = true; + // Check if the match is empty + for (let i = 0; i < mpLobby.slots.length; i++) { + const slot = mpLobby.slots[i]; + // Check if the slot is avaliable + if (slot.playerId === -1) continue; + + // There is a user in the match + empty = false; + break; + } + + // The match is empty, proceed to remove it. + if (empty) { + let matchIndex; + // Loop through all matches + for (let i = 0; i < this.matches.length; i++) { + // If the match matches the match the user has left + if (this.matches[i].matchStreamName == MatchUser.currentMatch.matchStreamName) { + matchIndex = i; + break; + } + } + + // Make sure we got a match index + if (matchIndex == null) return; + + // Remove this match from the list of active matches + this.matches.splice(matchIndex, 1); + } + + MatchUser.currentMatch = null; + + // Update the match listing to reflect this change (either removal or user leaving) + this.updateMatchListing(); + + // Delay a 2nd match listing update + setTimeout(() => { + this.updateMatchListing(); + }, 1000); + } +} \ No newline at end of file diff --git a/server/MultiplayerMatch.js b/server/MultiplayerMatch.js new file mode 100644 index 0000000..3f93bdd --- /dev/null +++ b/server/MultiplayerMatch.js @@ -0,0 +1,603 @@ +const osu = require("osu-packet"), + getUserById = require("./util/getUserById.js"), + StatusUpdate = require("./Packets/StatusUpdate.js"); + +// TODO: Cache the player's slot position in their user class for a small optimisation + +module.exports = class { + constructor(MatchHost, MatchData = {matchId: -1,inProgress: false,matchType: 0,activeMods: 0,gameName: "",gamePassword: '',beatmapName: '',beatmapId: 1250198,beatmapChecksum: '',slots: [],host: 0,playMode: 0,matchScoringType: 0,matchTeamType: 0,specialModes: 0,seed: 0}) { + this.matchId = global.getAndAddToHistoricalMultiplayerMatches(); + + this.inProgress = MatchData.inProgress; + this.matchStartCountdownActive = false; + + this.matchType = MatchData.matchType; + + this.activeMods = MatchData.activeMods; + + 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.slots = MatchData.slots; + for (let i = 0; i < this.slots.length; i++) { + this.slots[i].mods = 0; + } + + this.host = MatchData.host; + + this.playMode = MatchData.playMode; + + this.matchScoringType = MatchData.matchScoringType; + this.matchTeamType = MatchData.matchTeamType; + this.specialModes = MatchData.specialModes; + + this.seed = MatchData.seed; + + this.matchStreamName = `mp_${this.matchId}`; + + this.matchLoadSlots = null; + this.matchSkippedSlots = null; + + this.playerScores = null; + + this.multiplayerExtras = null; + + const osuPacketWriter = new osu.Bancho.Writer; + + // Update the status of the current user + StatusUpdate(MatchHost, MatchHost.id); + osuPacketWriter.MatchNew(this.createOsuMatchJSON()); + + // Queue match creation for user + MatchHost.addActionToQueue(osuPacketWriter.toBuffer); + + global.StreamsHandler.addStream(this.matchStreamName, true, this.matchId); + + // Update the match listing for users in the multiplayer lobby + global.MultiplayerManager.updateMatchListing(); + } + + 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) { + try { + let userInMatch = false; + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Check if the user is in this slot + if (slot.playerId == MatchUser.id) { + userInMatch = true; + break; + } + } + + // Make sure we don't run more than once + // Again, client double firing packets. + if (!userInMatch) return; + + let osuPacketWriter = new osu.Bancho.Writer; + + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the user is in this slot + if (slot.playerId != MatchUser.id) continue; + + // Set the slot's status to avaliable + slot.playerId = -1; + slot.status = 1; + + break; + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Remove the leaving user from the match's stream + global.StreamsHandler.removeUserFromStream(this.matchStreamName, MatchUser.id); + + // Inform all users in the match that the leaving user has left + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + + osuPacketWriter = new osu.Bancho.Writer; + + // Remove user from the multiplayer channel for the match + osuPacketWriter.ChannelRevoked("#multiplayer"); + + MatchUser.addActionToQueue(osuPacketWriter.toBuffer); + + return this; + } catch (e) { } + } + + updateMatch(MatchData) { + // Update match with new data + this.inProgress = MatchData.inProgress; + + this.matchType = MatchData.matchType; + + this.activeMods = MatchData.activeMods; + + 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; + + this.seed = MatchData.seed; + const osuPacketWriter = new osu.Bancho.Writer; + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Send this new match data to all users in the match + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + + // 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 + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + moveToSlot(MatchUser, SlotToMoveTo) { + const osuPacketWriter = new osu.Bancho.Writer; + + let currentUserData, slotIndex; + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the user in this slot is the user we want + if (slot.playerId != MatchUser.id) continue; + + currentUserData = slot; + slotIndex = i; + break; + } + + // Set the new slot's data to the user's old slot data + this.slots[SlotToMoveTo].playerId = currentUserData.playerId; + MatchUser.matchSlotId = SlotToMoveTo; + this.slots[SlotToMoveTo].status = currentUserData.status; + + // Set the old slot's data to open + this.slots[slotIndex].playerId = -1; + this.slots[slotIndex].status = 1; + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Send this change to all users in the match + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + + // Update the match listing in the lobby to reflect this change + global.MultiplayerManager.updateMatchListing(); + } + + setReadyState(MatchUser, ReadyState) { + // Get the match the user is in + const osuPacketWriter = new osu.Bancho.Writer; + + // Loop though all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Check if the player in this slot is this user + if (slot.playerId == MatchUser.id) { + // Turn on or off the user's ready state + if (ReadyState) slot.status = 8; // Ready + else slot.status = 4; // Not Ready + break; + } + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Send this update to all users in the stream + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + lockMatchSlot(MatchUser, MatchUserToKick) { + const osuPacketWriter = new osu.Bancho.Writer; + + // 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 cachedPlayerId = slot.playerId; + + // If the slot is empty lock instead of kicking + if (slot.playerId === -1) { // Slot is empty, lock it + if (slot.status === 1) slot.status = 2; + else slot.status = 1; + } + // The slot isn't empty, prepare to kick the player + else { + const kickedPlayer = getUserById(slot.playerId); + kickedPlayer.matchSlotId = -1; + slot.playerId = -1; + slot.status = 1; + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Inform all users in the match of the change + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + + // Update the match listing in the lobby listing to reflect this change + global.MultiplayerManager.updateMatchListing(); + + if (cachedPlayerId !== null && cachedPlayerId !== -1) { + // Remove the kicked user from the match stream + global.StreamsHandler.removeUserFromStream(this.matchStreamName, cachedPlayerId); + } + } + + missingBeatmap(MatchUser) { + const osuPacketWriter = new osu.Bancho.Writer; + + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the user in the slot is the user we want to update + if (slot.playerId != MatchUser.id) continue; + + // User is missing the beatmap set the status to reflect it + slot.status = 16; + break; + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Inform all users in the match of this change + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + notMissingBeatmap(MatchUser) { + const osuPacketWriter = new osu.Bancho.Writer; + + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the user in the slot is the user we want to update + if (slot.playerId != MatchUser.id) continue; + + // The user is not missing the beatmap, set the status to normal + else slot.status = 4; + break; + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Inform all users in the match of this change + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + matchSkip(MatchUser) { + if (this.matchSkippedSlots == null) { + this.matchSkippedSlots = []; + + const skippedSlots = this.matchSkippedSlots; + + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // 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}); + } + } + + const skippedSlots = this.matchSkippedSlots; + + for (let i = 0; i < skippedSlots.length; i++) { + // If loadslot belongs to this user then set loaded to true + if (skippedSlots[i].playerId == MatchUser.id) { + skippedSlots[i].skipped = true; + } + } + + let allSkipped = true; + for (let i = 0; i < skippedSlots.length; i++) { + if (skippedSlots[i].skipped) continue; + + // A user hasn't finished playing + allSkipped = false; + } + + // All players have finished playing, finish the match + if (allSkipped) { + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchPlayerSkipped(MatchUser.id); + osuPacketWriter.MatchSkip(); + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + + this.matchSkippedSlots = null; + } else { + const osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchPlayerSkipped(MatchUser.id); + + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + } + + transferHost(MatchUserToTransferHostTo) { + const osuPacketWriter = new osu.Bancho.Writer; + + // Set the lobby's host to the new user + this.host = this.slots[MatchUserToTransferHostTo].playerId; + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Inform all clients in the match of the change + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + // TODO: Fix not being able to add DT when freemod is active + updateMods(MatchUser, MatchMods) { + // Check if freemod is enabled + if (this.specialModes === 1) { + const osuPacketWriter = new osu.Bancho.Writer; + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + if (slot.playerId === MatchUser.id) { + slot.mods = MatchMods; + break; + } + } + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } else { + // Make sure the person updating mods is the host of the match + if (this.host !== MatchUser.id) return; + const osuPacketWriter = new osu.Bancho.Writer; + + // Change the matches mods to these new mods + // TODO: Do this per user if freemod is enabled + this.activeMods = MatchMods; + + osuPacketWriter.MatchUpdate(this.createOsuMatchJSON()); + + // Inform all users in the match of the change + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + // 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 = []; + const loadedSlots = this.matchLoadSlots; + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // 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 + loadedSlots.push({ + playerId: slot.playerId, + loaded: false + }); + } + const osuPacketWriter = new osu.Bancho.Writer; + + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the slot has a user in it + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + // Set the user's status to playing + slot.status = 32; + } + + osuPacketWriter.MatchStart(this.createOsuMatchJSON()); + + // Inform all users in the match that it has started + global.StreamsHandler.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) { + const loadedSlots = this.matchLoadSlots; + + // Loop through all user load check items + for (let i = 0; i < loadedSlots.length; i++) { + // If loadslot belongs to this user then set loaded to true + if (loadedSlots[i].playerId == MatchUser.id) { + loadedSlots[i].loaded = true; + } + } + + // Loop through all loaded slots and check if all users are loaded + let allLoaded = true; + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].loaded) continue; + + // A user wasn't loaded, keep waiting. + allLoaded = false; + break; + } + + // All players have loaded the beatmap, start playing. + if (allLoaded) { + let osuPacketWriter = new osu.Bancho.Writer; + osuPacketWriter.MatchAllPlayersLoaded(); + global.StreamsHandler.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}); + } + } + } + + onPlayerFinishMatch(MatchUser) { + // If user loading slots do not exist + if (this.matchLoadSlots == null) { + this.matchLoadSlots = []; + // Repopulate user loading slots again + const loadedSlots = this.matchLoadSlots; + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // 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 + loadedSlots.push({ + playerId: slot.playerId, + loaded: false + }); + } + } + + const loadedSlots = this.matchLoadSlots; + + // Loop through all loaded slots to make sure all users have finished playing + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].playerId == MatchUser.id) { + loadedSlots[i].loaded = true; + } + } + + let allLoaded = true; + for (let i = 0; i < loadedSlots.length; i++) { + if (loadedSlots[i].loaded) continue; + + // A user hasn't finished playing + allLoaded = false; + } + + // All players have finished playing, finish the match + if (allLoaded) this.finishMatch(); + } + + finishMatch() { + if (!this.inProgress) return; + this.matchLoadSlots = null; + this.inProgress = false; + let osuPacketWriter = new osu.Bancho.Writer; + + // Loop through all slots in the match + for (let i = 0; i < this.slots.length; i++) { + const slot = this.slots[i]; + // Make sure the slot has a user + if (slot.playerId === -1 || slot.status === 1 || slot.status === 2) continue; + + // Set the user's status back to normal from playing + slot.status = 4; + } + + osuPacketWriter.MatchComplete(); + + // Inform all users in the match that it is complete + global.StreamsHandler.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))); + + this.playerScores = null; + } + + updatePlayerScore(MatchPlayer, 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 i = 0; i < this.playerScores.length; i++) { + if (this.playerScores[i].playerId == MatchPlayer.id) { + this.playerScores[i].score = MatchScoreData.totalScore; + this.playerScores[i].isCurrentlyFailed = MatchScoreData.currentHp == 254; + break; + } + } + + osuPacketWriter.MatchScoreUpdate(MatchScoreData); + + // Send the newly updated score to all users in the match + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } + + matchFailed(MatchUser) { + 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); + + global.StreamsHandler.sendToStream(this.matchStreamName, osuPacketWriter.toBuffer, null); + } +} \ No newline at end of file diff --git a/server/Packets/MultiplayerInvite.js b/server/Packets/MultiplayerInvite.js index e1daaa6..14dda34 100644 --- a/server/Packets/MultiplayerInvite.js +++ b/server/Packets/MultiplayerInvite.js @@ -8,7 +8,7 @@ module.exports = function(CurrentUser, InvitedUser) { osuPacketWriter.SendMessage({ sendingClient: CurrentUser.username, - message: `Come join my multiplayer match: [osump://${CurrentUser.currentMatch}/ ${global.matches[CurrentUser.currentMatch][1].gameName}]`, + message: `Come join my multiplayer match: [osump://${CurrentUser.currentMatch.matchId}/ ${CurrentUser.currentMatch.gameName}]`, target: CurrentUser.username, senderId: CurrentUser.id }); diff --git a/server/Streams.js b/server/Streams.js index e9d68af..0e89368 100644 --- a/server/Streams.js +++ b/server/Streams.js @@ -3,22 +3,25 @@ const getUserById = require("./util/getUserById.js"); module.exports = class { constructor() { this.avaliableStreams = {}; + this.avaliableStreamKeys = []; } - addStream(streamName, removeIfEmpty, spectatorHostId = null) { - const streamNames = Object.keys(this.avaliableStreams); - if (streamNames.includes(streamName)) return global.consoleHelper.printBancho(`Did not add stream [${streamName}] A stream with the same name already exists`); + addStream(streamName = "", removeIfEmpty = false, spectatorHostId = null) { + // Make sure a stream with the same name doesn't exist already + if (this.avaliableStreamKeys.includes(streamName)) + return global.consoleHelper.printBancho(`Did not add stream [${streamName}] A stream with the same name already exists`); // Add new stream to the list of streams this.avaliableStreams[streamName] = { streamUsers: [], // An array containing a list of user IDs of the users in a given stream streamSpectatorHost: spectatorHostId, // null unless stream is for spectating removeIfEmpty: removeIfEmpty } + this.avaliableStreamKeys = Object.keys(this.avaliableStreams); global.consoleHelper.printBancho(`Added stream [${streamName}]`); } // Checks if a stream has no users in it - streamChecker(interval) { + streamChecker(interval = 5000) { setInterval(() => { // Get the names of all currently avaliable streams const streams = global.StreamsHandler.getStreams(); @@ -62,14 +65,35 @@ module.exports = class { } addUserToStream(streamName, userId) { + // Make sure the stream we are attempting to add this user to even exists + if (!this.doesStreamExist(streamName)) + return global.consoleHelper.printBancho(`Did not add user to stream [${streamName}] because it does not exist!`); + + // Make sure the user isn't already in the stream + if (this.avaliableStreams[streamName].streamUsers.includes(userId)) + return global.consoleHelper.printBancho(`Did not add user to stream [${streamName}] because they are already in it!`); + + // Make sure this isn't an invalid user (userId can't be lower than 1) + if (userId <= 0 || userId == null) + return global.consoleHelper.printBancho(`Did not add user to stream [${streamName}] because their userId is invalid!`); + // Add user's id to the stream's user list this.avaliableStreams[streamName].streamUsers.push(userId); global.consoleHelper.printBancho(`Added user [${userId}] to stream ${streamName}`); } removeUserFromStream(streamName, userId) { - // Make sure this isn't an invalid user - if (userId == -1 || userId == null) return; + // Make sure the stream we are attempting to add this user to even exists + if (!this.doesStreamExist(streamName)) + return global.consoleHelper.printBancho(`Did not remove user from stream [${streamName}] because it does not exist!`); + + // Make sure the user isn't already in the stream + if (!this.avaliableStreams[streamName].streamUsers.includes(userId)) + return global.consoleHelper.printBancho(`Did not remove user from stream [${streamName}] because they are not in it!`); + + // Make sure this isn't an invalid user (userId can't be lower than 1) + if (userId <= 0 || userId == null) + return global.consoleHelper.printBancho(`Did not remove user from stream [${streamName}] because their userId is invalid!`); try { // Find index of user to remove let userCurrentIndex; @@ -87,21 +111,12 @@ module.exports = class { } doesStreamExist(streamName) { - let exist = false; - const streamList = Object.keys(this.avaliableStreams); - for (let i = 0; i < streamList.length; i++) { - if (streamList[i] == streamName) { - exist = true; - break; - } - } - - return exist; + return this.avaliableStreamKeys.includes(streamName); } getStreams() { // Return the names of all avaliable streams - return Object.keys(this.avaliableStreams); + return this.avaliableStreamKeys; } isUserInStream(streamName, userId) { @@ -112,6 +127,7 @@ module.exports = class { removeStream(streamName) { try { delete this.avaliableStreams[streamName]; + this.avaliableStreamKeys = Object.keys(this.avaliableStreams); } catch (e) { global.consoleHelper.printError(`Was not able to remove stream [${streamName}]`) } } } \ No newline at end of file