diff --git a/Binato.ts b/Binato.ts index f434d75..a52d263 100644 --- a/Binato.ts +++ b/Binato.ts @@ -1,14 +1,24 @@ +console.clear(); + +import { ConsoleHelper } from "./ConsoleHelper"; +import { readFileSync, existsSync } from "fs"; +if (!existsSync("./config.json")) { + ConsoleHelper.printError("You must have a config file in the root of Binato's folder structure."); + ConsoleHelper.printError("Check the GitHub for an example file"); + process.exit(1); +} + import { ChatHistory } from "./server/ChatHistory"; import compression from "compression"; -import { ConsoleHelper } from "./ConsoleHelper"; import express from "express"; import { HandleRequest } from "./server/BanchoServer"; -import { readFileSync } from "fs"; import { Registry, collectDefaultMetrics } from "prom-client"; const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString()); const binatoApp:express.Application = express(); +ConsoleHelper.printInfo("Starting Binato..."); + if (config["prometheus"]["enabled"]) { const register:Registry = new Registry(); register.setDefaultLabels({ app: "nodejs_binato" }); @@ -20,14 +30,14 @@ if (config["prometheus"]["enabled"]) { res.end(await register.metrics()); }); - prometheusApp.listen(config["prometheus"]["port"], () => ConsoleHelper.printBancho(`Prometheus metrics listening at port ${config["prometheus"]["port"]}`)); + prometheusApp.listen(config["prometheus"]["port"], () => ConsoleHelper.printInfo(`Prometheus metrics listening at port ${config["prometheus"]["port"]}`)); } else { ConsoleHelper.printWarn("Prometheus is disabled!"); } if (config["express"]["compression"]) { binatoApp.use(compression()); - ConsoleHelper.printBancho("Compression is enabled"); + ConsoleHelper.printInfo("Compression is enabled"); } else { ConsoleHelper.printWarn("Compression is disabled"); } @@ -58,4 +68,4 @@ binatoApp.use((req, res) => { }); }); -binatoApp.listen(config.express.port, () => ConsoleHelper.printBancho(`Binato is up! Listening at port ${config.express.port}`)); \ No newline at end of file +binatoApp.listen(config.express.port, () => ConsoleHelper.printInfo(`Binato is up! Listening at port ${config.express.port}`)); \ No newline at end of file diff --git a/ConsoleHelper.ts b/ConsoleHelper.ts index f656bc3..7c955cc 100644 --- a/ConsoleHelper.ts +++ b/ConsoleHelper.ts @@ -7,12 +7,14 @@ enum LogType { }; const LogTags = { - BANCHO: chalk.bgMagenta(chalk.black(" BCHO ")), - WEBREQ: chalk.bgGreen(chalk.black(" WEBR ")), - CHAT: chalk.bgCyan(chalk.black(" CHAT ")), - WARN: chalk.bgYellow(chalk.black(" WARN ")), - ERROR: chalk.bgRed(" ERRR "), - REDIS: chalk.bgRed(chalk.white(" RDIS ")) + INFO: chalk.bgGreen(chalk.black(" INFO ")), + BANCHO: chalk.bgMagenta(chalk.black(" BANCHO ")), + WEBREQ: chalk.bgGreen(chalk.black(" WEBREQ ")), + CHAT: chalk.bgCyan(chalk.black(" CHAT ")), + WARN: chalk.bgYellow(chalk.black(" WARN ")), + ERROR: chalk.bgRed(" ERRR "), + REDIS: chalk.bgRed(chalk.white(" bREDIS ")), + STREAM: chalk.bgBlue(chalk.black(" STREAM ")) } as const; function correctValue(i:number) : string { @@ -41,6 +43,14 @@ export class ConsoleHelper { log(LogTags.WEBREQ, s); } + public static printStream(s:string) : void { + log(LogTags.STREAM, s); + } + + public static printInfo(s:string) : void { + log(LogTags.INFO, s); + } + public static printBancho(s:string) : void { log(LogTags.BANCHO, s); } diff --git a/Constants.ts b/Constants.ts new file mode 100644 index 0000000..9618fad --- /dev/null +++ b/Constants.ts @@ -0,0 +1,3 @@ +export abstract class Constants { + public static readonly DEBUG = true; +} \ No newline at end of file diff --git a/server/BanchoServer.ts b/server/BanchoServer.ts index f9b6796..427ffd3 100644 --- a/server/BanchoServer.ts +++ b/server/BanchoServer.ts @@ -1,4 +1,5 @@ import { ConsoleHelper } from "../ConsoleHelper"; +import { ChatManager } from "./ChatManager"; import { Database } from "./objects/Database"; import { LatLng } from "./objects/LatLng"; import { LoginProcess } from "./LoginProcess"; @@ -9,17 +10,13 @@ import { RedisClientType, createClient } from "redis"; import { Request, Response } from "express"; import { UserArray } from "./objects/UserArray"; import { User } from "./objects/User"; -const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString()); +import { DataStreamArray } from "./objects/DataStreamArray"; +import { MultiplayerManager } from "./MultiplayerManager"; +const config:any = JSON.parse(readFileSync("./config.json").toString()); // TODO: Port osu-packet to TypeScript const osu = require("osu-packet"); -/*const - loginHandler = require("./loginHandler.js"), - parseUserData = require("./util/parseUserData.js"), - getUserFromToken = require("./util/getUserByToken.js"), - getUserById = require("./util/getUserById.js"), - bakedResponses = require("./bakedResponses.js"), - Streams = require("./Streams.js");*/ +/*Streams = require("./Streams.js");*/ const DB:Database = new Database(config.database.address, config.database.port, config.database.username, config.database.password, config.database.name, async () => { // Close any unclosed db matches on startup @@ -27,11 +24,23 @@ const DB:Database = new Database(config.database.address, config.database.port, DB.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'"); }); -// Users funkyArray for session storage -const users = new UserArray(); +// User session storage +const users:UserArray = new UserArray(); + +// DataStream storage +const streams:DataStreamArray = new DataStreamArray(); + +// ChatManager +const chatManager:ChatManager = new ChatManager(streams); +chatManager.AddChatChannel("osu", "The main channel"); +chatManager.AddChatChannel("lobby", "Talk about multiplayer stuff"); +chatManager.AddChatChannel("english", "Talk in exclusively English"); +chatManager.AddChatChannel("japanese", "Talk in exclusively Japanese"); + +const multiplayerManager:MultiplayerManager = new MultiplayerManager(streams); // Add the bot user -const botUser:User = users.add("bot", new User(3, "SillyBot", "bot", DB)); +const botUser:User = users.add("bot", new User(3, "SillyBot", "bot", DB, users, streams, chatManager)); // Set the bot's position on the map botUser.location = new LatLng(50, -32); @@ -84,26 +93,6 @@ setInterval(() => { } }, 10000); -// Init stream class -//Streams.init(); - -// An array containing all chat channels -/*global.channels = [ - { 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 }, -];*/ - -// Create a stream for each chat channel -/*for (let i = 0; i < global.channels.length; i++) { - Streams.addStream(global.channels[i].channelName, false); -}*/ - -// Add a stream for the multiplayer lobby -//Streams.addStream("multiplayer_lobby", false); - // Include packets /*const ChangeAction = require("./Packets/ChangeAction.js"), SendPublicMessage = require("./Packets/SendPublicMessage.js"), @@ -123,9 +112,11 @@ setInterval(() => { TourneyMatchSpecialInfo = require("./Packets/TourneyMatchSpecialInfo.js"), TourneyMatchJoinChannel = require("./Packets/TourneyMatchSpecialInfo.js"), TourneyMatchLeaveChannel = require("./Packets/TourneyLeaveMatchChannel.js");*/ - -// A class for managing everything multiplayer -//const multiplayerManager:MultiplayerManager = new MultiplayerManager(); +import { ChangeAction } from "./packets/ChangeAction"; +import { Logout } from "./packets/Logout"; +import { UserPresence } from "./packets/UserPresence"; +import { UserStatsRequest } from "./packets/UserStatsRequest"; +import { UserPresenceBundle } from "./packets/UserPresenceBundle"; const EMPTY_BUFFER = Buffer.alloc(0); @@ -143,7 +134,7 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { if (DB.connected) { // Client doesn't have a token yet, let's auth them! - await LoginProcess(req, res, packet, DB, users); + await LoginProcess(req, res, packet, DB, users, streams, chatManager); DB.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [users.getLength() - 1]); } } else { @@ -166,13 +157,13 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { // Go through each packet sent by the client for (let CurrentPacket of PacketData) { - /*switch (CurrentPacket.id) { + switch (CurrentPacket.id) { case Packets.Client_ChangeAction: ChangeAction(PacketUser, CurrentPacket.data); break; case Packets.Client_SendPublicMessage: - SendPublicMessage(PacketUser, CurrentPacket.data); + //SendPublicMessage(PacketUser, CurrentPacket.data); break; case Packets.Client_Logout: @@ -184,124 +175,124 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { break; case Packets.Client_StartSpectating: - Spectator.startSpectatingUser(PacketUser, CurrentPacket.data); + //Spectator.startSpectatingUser(PacketUser, CurrentPacket.data); break; case Packets.Client_SpectateFrames: - Spectator.sendSpectatorFrames(PacketUser, CurrentPacket.data); + //Spectator.sendSpectatorFrames(PacketUser, CurrentPacket.data); break; case Packets.Client_StopSpectating: - Spectator.stopSpectatingUser(PacketUser); + //Spectator.stopSpectatingUser(PacketUser); break; case Packets.Client_SendPrivateMessage: - SendPrivateMessage(PacketUser, CurrentPacket.data); + //SendPrivateMessage(PacketUser, CurrentPacket.data); break; case Packets.Client_JoinLobby: - multiplayerManager.userEnterLobby(PacketUser); + //multiplayerManager.userEnterLobby(PacketUser); break; case Packets.Client_PartLobby: - multiplayerManager.userLeaveLobby(PacketUser); + //multiplayerManager.userLeaveLobby(PacketUser); break; case Packets.Client_CreateMatch: - await multiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data); + //await multiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data); break; case Packets.Client_JoinMatch: - multiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data); + //multiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchChangeSlot: - PacketUser.currentMatch.moveToSlot(PacketUser, CurrentPacket.data); + //PacketUser.currentMatch.moveToSlot(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchReady: - PacketUser.currentMatch.setStateReady(PacketUser); + //PacketUser.currentMatch.setStateReady(PacketUser); break; case Packets.Client_MatchChangeSettings: - await PacketUser.currentMatch.updateMatch(PacketUser, CurrentPacket.data); + //await PacketUser.currentMatch.updateMatch(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchNotReady: - PacketUser.currentMatch.setStateNotReady(PacketUser); + //PacketUser.currentMatch.setStateNotReady(PacketUser); break; case Packets.Client_PartMatch: - await multiplayerManager.leaveMultiplayerMatch(PacketUser); + //await multiplayerManager.leaveMultiplayerMatch(PacketUser); break; // Also handles user kick if the slot has a user case Packets.Client_MatchLock: - PacketUser.currentMatch.lockMatchSlot(PacketUser, CurrentPacket.data); + //PacketUser.currentMatch.lockMatchSlot(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchNoBeatmap: - PacketUser.currentMatch.missingBeatmap(PacketUser); + //PacketUser.currentMatch.missingBeatmap(PacketUser); break; case Packets.Client_MatchSkipRequest: - PacketUser.currentMatch.matchSkip(PacketUser); + //PacketUser.currentMatch.matchSkip(PacketUser); break; case Packets.Client_MatchHasBeatmap: - PacketUser.currentMatch.notMissingBeatmap(PacketUser); + //PacketUser.currentMatch.notMissingBeatmap(PacketUser); break; case Packets.Client_MatchTransferHost: - PacketUser.currentMatch.transferHost(PacketUser, CurrentPacket.data); + //PacketUser.currentMatch.transferHost(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchChangeMods: - PacketUser.currentMatch.updateMods(PacketUser, CurrentPacket.data); + //PacketUser.currentMatch.updateMods(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchStart: - PacketUser.currentMatch.startMatch(); + //PacketUser.currentMatch.startMatch(); break; case Packets.Client_MatchLoadComplete: - PacketUser.currentMatch.matchPlayerLoaded(PacketUser); + //PacketUser.currentMatch.matchPlayerLoaded(PacketUser); break; case Packets.Client_MatchComplete: - await PacketUser.currentMatch.onPlayerFinishMatch(PacketUser); + //await PacketUser.currentMatch.onPlayerFinishMatch(PacketUser); break; case Packets.Client_MatchScoreUpdate: - PacketUser.currentMatch.updatePlayerScore(PacketUser, CurrentPacket.data); + //PacketUser.currentMatch.updatePlayerScore(PacketUser, CurrentPacket.data); break; case Packets.Client_MatchFailed: - PacketUser.currentMatch.matchFailed(PacketUser); + //PacketUser.currentMatch.matchFailed(PacketUser); break; case Packets.Client_MatchChangeTeam: - PacketUser.currentMatch.changeTeam(PacketUser); + //PacketUser.currentMatch.changeTeam(PacketUser); break; case Packets.Client_ChannelJoin: - ChannelJoin(PacketUser, CurrentPacket.data); + //ChannelJoin(PacketUser, CurrentPacket.data); break; case Packets.Client_ChannelPart: - ChannelPart(PacketUser, CurrentPacket.data); + //ChannelPart(PacketUser, CurrentPacket.data); break; case Packets.Client_SetAwayMessage: - SetAwayMessage(PacketUser, CurrentPacket.data); + //SetAwayMessage(PacketUser, CurrentPacket.data); break; case Packets.Client_FriendAdd: - AddFriend(PacketUser, CurrentPacket.data); + //AddFriend(PacketUser, CurrentPacket.data); break; case Packets.Client_FriendRemove: - RemoveFriend(PacketUser, CurrentPacket.data); + //RemoveFriend(PacketUser, CurrentPacket.data); break; case Packets.Client_UserStatsRequest: @@ -309,19 +300,19 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { break; case Packets.Client_SpecialMatchInfoRequest: - TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data); + //TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data); break; case Packets.Client_SpecialJoinMatchChannel: - TourneyMatchJoinChannel(PacketUser, CurrentPacket.data); + //TourneyMatchJoinChannel(PacketUser, CurrentPacket.data); break; case Packets.Client_SpecialLeaveMatchChannel: - TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data); + //TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data); break; case Packets.Client_Invite: - MultiplayerInvite(PacketUser, CurrentPacket.data); + //MultiplayerInvite(PacketUser, CurrentPacket.data); break; case Packets.Client_UserPresenceRequest: @@ -334,7 +325,7 @@ export async function HandleRequest(req:Request, res:Response, packet:Buffer) { // Print out unimplemented packet console.dir(CurrentPacket); break; - }*/ + } } responseData = PacketUser.queue; diff --git a/server/Channels.ts b/server/Channels.ts deleted file mode 100644 index e69de29..0000000 diff --git a/server/ChatManager.ts b/server/ChatManager.ts new file mode 100644 index 0000000..ac01449 --- /dev/null +++ b/server/ChatManager.ts @@ -0,0 +1,19 @@ +import { Channel } from "./objects/Channel"; +import { ConsoleHelper } from "../ConsoleHelper"; +import { FunkyArray } from "./objects/FunkyArray"; +import { DataStreamArray } from "./objects/DataStreamArray"; + +export class ChatManager { + public chatChannels:FunkyArray = new FunkyArray(); + public streams:DataStreamArray; + + public constructor(streams:DataStreamArray) { + this.streams = streams; + } + + public AddChatChannel(name:string, description:string, forceJoin:boolean = false) { + const stream = this.streams.CreateStream(`chat_channel:${name}`, false); + this.chatChannels.add(name, new Channel(name, description, stream, forceJoin)); + ConsoleHelper.printChat(`Created chat channel [${name}]`); + } +} \ No newline at end of file diff --git a/server/LoginProcess.ts b/server/LoginProcess.ts index 48c8fe6..72e99e5 100644 --- a/server/LoginProcess.ts +++ b/server/LoginProcess.ts @@ -11,7 +11,12 @@ import { readFileSync } from "fs"; import { Request, Response } from "express"; import { UserArray } from "./objects/UserArray"; import { User } from "./objects/User"; -const config:any = JSON.parse(readFileSync(__dirname + "/config.json").toString()); +import { DataStreamArray } from "./objects/DataStreamArray"; +import { ChatManager } from "./ChatManager"; +import { UserPresenceBundle } from "./packets/UserPresenceBundle"; +import { UserPresence } from "./packets/UserPresence"; +import { StatusUpdate } from "./packets/StatusUpdate"; +const config:any = JSON.parse(readFileSync("./config.json").toString()); const { decrypt: aesDecrypt } = require("aes256"); const osu = require("osu-packet"); @@ -88,7 +93,7 @@ function TestLogin(loginInfo:LoginInfo | undefined, database:Database) { }); } -export async function LoginProcess(req:Request, res:Response, packet:Buffer, database:Database, users:UserArray) { +export async function LoginProcess(req:Request, res:Response, packet:Buffer, database:Database, users:UserArray, streams:DataStreamArray, chatManager:ChatManager) { const loginInfo = LoginInfo.From(packet); const loginStartTime = Date.now(); @@ -145,11 +150,11 @@ export async function LoginProcess(req:Request, res:Response, packet:Buffer, dat // Make sure user is not already connected, kick off if so. const connectedUser = users.getByUsername(loginInfo.username); if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) { - Logout(connectedUser, database); + Logout(connectedUser); } // Retreive the newly created user - const newUser:User = users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, database)); + const newUser:User = users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, database, users, streams, chatManager)); // Set tourney client flag newUser.isTourneyUser = isTourneyClient; newUser.location = userLocation; @@ -173,14 +178,14 @@ export async function LoginProcess(req:Request, res:Response, packet:Buffer, dat osuPacketWriter.LoginPermissions(4); // After sending the user their friends list send them the online users - //UserPresenceBundle(newUser); + UserPresenceBundle(newUser); // Set title screen image //osuPacketWriter.TitleUpdate("http://puu.sh/jh7t7/20c04029ad.png|https://osu.ppy.sh/news/123912240253"); // Add user panel data packets - //UserPresence(newUser, newUser.id); - //StatusUpdate(newUser, newUser.id); + UserPresence(newUser, newUser.id); + StatusUpdate(newUser, newUser.id); // peppy pls, why osuPacketWriter.ChannelListingComplete(); @@ -201,7 +206,7 @@ export async function LoginProcess(req:Request, res:Response, packet:Buffer, dat // Construct user's friends list const userFriends = await database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]); - let friendsArray = []; + let friendsArray = new Array; for (let i = 0; i < userFriends.length; i++) { friendsArray.push(userFriends[i].friendsWith); } diff --git a/server/MultiplayerManager.ts b/server/MultiplayerManager.ts index 799c597..2c7d23c 100644 --- a/server/MultiplayerManager.ts +++ b/server/MultiplayerManager.ts @@ -1,3 +1,20 @@ -export abstract class MultiplayerManager { - +import { DataStream } from "./objects/DataStream"; +import { DataStreamArray } from "./objects/DataStreamArray"; +import { FunkyArray } from "./objects/FunkyArray"; +import { Match } from "./objects/Match"; +import { User } from "./objects/User"; + +export class MultiplayerManager { + private matches:FunkyArray = new FunkyArray(); + private readonly lobbyStream:DataStream; + + public constructor(streams:DataStreamArray) { + this.lobbyStream = streams.CreateStream("multiplayer:lobby", false); + } + + public JoinLobby(user:User) { + if (user.currentMatch != null) { + + } + } } \ No newline at end of file diff --git a/server/objects/Channel.ts b/server/objects/Channel.ts index e69de29..4bffea4 100644 --- a/server/objects/Channel.ts +++ b/server/objects/Channel.ts @@ -0,0 +1,20 @@ +import { DataStream } from "./DataStream"; + +export class Channel { + public name:string; + public description:string; + public userCount:number = 0; + private stream:DataStream; + private forceJoin:boolean; + + public constructor(name:string, description:string, stream:DataStream, forceJoin:boolean = false) { + this.name = name; + this.description = description; + this.stream = stream; + this.forceJoin = forceJoin; + } + + public SendMessage(message:string) { + + } +} \ No newline at end of file diff --git a/server/objects/DataStream.ts b/server/objects/DataStream.ts new file mode 100644 index 0000000..04faeff --- /dev/null +++ b/server/objects/DataStream.ts @@ -0,0 +1,45 @@ +import { ConsoleHelper } from "../../ConsoleHelper"; +import { Constants } from "../../Constants"; +import { DataStreamArray } from "./DataStreamArray"; +import { User } from "./User"; +import { UserArray } from "./UserArray"; + +export class DataStream { + private users:UserArray = new UserArray(); + private readonly name:string; + private readonly parent:DataStreamArray; + private readonly removeWhenEmpty:boolean; + + public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) { + this.name = name; + this.parent = parent; + this.removeWhenEmpty = removeWhenEmpty; + } + + public AddUser(user:User) : void { + if (!(user.uuid in this.users.getItems())) { + this.users.add(user.uuid, user); + ConsoleHelper.printStream(`Added user [${user.username}|${user.uuid}] to stream [${this.name}]`); + } + } + + public RemoveUser(user:User) : void { + if (user.uuid in this.users.getItems()) { + this.users.remove(user.uuid); + ConsoleHelper.printStream(`Removed user [${user.username}|${user.uuid}] from stream [${this.name}]`); + } + if (this.removeWhenEmpty && this.users.getLength() === 0) { + this.parent.remove(this.name); + } + } + + public Send(data:Buffer) { + for (let user of this.users.getIterableItems()) { + user.addActionToQueue(data); + } + + if (Constants.DEBUG) { + ConsoleHelper.printStream(`Sent [${data.toString()}] to all users in stream [${this.name}]`); + } + } +} \ No newline at end of file diff --git a/server/objects/DataStreamArray.ts b/server/objects/DataStreamArray.ts new file mode 100644 index 0000000..ca6aeba --- /dev/null +++ b/server/objects/DataStreamArray.ts @@ -0,0 +1,18 @@ +import { ConsoleHelper } from "../../ConsoleHelper"; +import { DataStream } from "./DataStream"; +import { FunkyArray } from "./FunkyArray"; +import { User } from "./User"; + +export class DataStreamArray extends FunkyArray { + public CreateStream(name:string, removeWhenEmpty:boolean = true) : DataStream { + const dataStream:DataStream = this.add(name, new DataStream(name, this, removeWhenEmpty)); + ConsoleHelper.printStream(`Created stream [${name}]`); + return dataStream; + } + + public RemoveUserFromAllStreams(user:User) { + for (let stream of this.getIterableItems()) { + stream.RemoveUser(user); + } + } +} \ No newline at end of file diff --git a/server/objects/Database.ts b/server/objects/Database.ts index 645eaf7..0e488e2 100644 --- a/server/objects/Database.ts +++ b/server/objects/Database.ts @@ -26,7 +26,7 @@ export class Database { .then(data => { if (!this.connected) { this.connected = true; - ConsoleHelper.printBancho(`Connected to database. Took ${Date.now() - classCreationTime}ms`); + ConsoleHelper.printInfo(`Connected to database. Took ${Date.now() - classCreationTime}ms`); clearInterval(connectionCheckInterval); lastQueryFinished = true; diff --git a/server/objects/FunkyArray.ts b/server/objects/FunkyArray.ts index 6806802..8e29b32 100644 --- a/server/objects/FunkyArray.ts +++ b/server/objects/FunkyArray.ts @@ -3,19 +3,19 @@ export class FunkyArray { private itemKeys:Array = Object.keys(this.items); private iterableArray:Array = new Array(); - public add(uuid:string, item:T, regenerate:boolean = true) : T { - this.items[uuid] = item; + public add(key:string, item:T, regenerate:boolean = true) : T { + this.items[key] = item; if (regenerate) { this.itemKeys = Object.keys(this.items); this.regenerateIterableArray(); } - return this.items[uuid]; + return this.items[key]; } - public remove(uuid:string, regenerate:boolean = true) { - delete this.items[uuid]; + public remove(key:string, regenerate:boolean = true) { + delete this.items[key]; if (regenerate) { this.itemKeys = Object.keys(this.items); this.regenerateIterableArray(); diff --git a/server/objects/Match.ts b/server/objects/Match.ts new file mode 100644 index 0000000..2a90ca4 --- /dev/null +++ b/server/objects/Match.ts @@ -0,0 +1,573 @@ +const osu = require("osu-packet"); + +export class Match { + /*public matchId:number = -1; + public inProgress:boolean = false; + public matchType: 0; + public activeMods: 0; + public gameName: ""; + public gamePassword: ''; + public beatmapName: ''; + public beatmapId: 0; + public beatmapChecksum: ''; + public slots: []; + public host:number = 0; + public playMode:number = 0; + public matchScoringType:number = 0; + public matchTeamType:number = 0; + public specialModes:number = 0; + public seed:number = 0; + + constructor(MatchData = {}) { + this.matchId = MatchData.matchId; + + this.roundId = 0; + + 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.matchChatStreamName = `mp_chat_${this.matchId}`; + + this.matchLoadSlots = null; + this.matchSkippedSlots = null; + + this.playerScores = null; + + this.multiplayerExtras = null; + + this.isTourneyMatch = false; + this.tourneyClientUsers = []; + } + + static createMatch(MatchHost = new User, MatchData = {matchId: -1,inProgress: false,matchType: 0,activeMods: 0,gameName: "",gamePassword: '',beatmapName: '',beatmapId: 0,beatmapChecksum: '',slots: [],host: 0,playMode: 0,matchScoringType: 0,matchTeamType: 0,specialModes: 0,seed: 0}) { + return new Promise(async (resolve, reject) => { + MatchData.matchId = (await global.DatabaseHelper.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 MultiplayerMatch(MatchData); + + 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); + + Streams.addStream(matchInstance.matchStreamName, true, matchInstance.matchId); + Streams.addStream(matchInstance.matchChatStreamName, true, matchInstance.matchId); + + // Update the match listing for users in the multiplayer lobby + global.MultiplayerManager.updateMatchListing(); + + resolve(matchInstance); + }); + } + + getSlotIdByPlayerId(playerId = 0) { + 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); + }*/ +} \ No newline at end of file diff --git a/server/objects/Slot.ts b/server/objects/Slot.ts new file mode 100644 index 0000000..6af715b --- /dev/null +++ b/server/objects/Slot.ts @@ -0,0 +1,3 @@ +export class Slot { + +} \ No newline at end of file diff --git a/server/objects/User.ts b/server/objects/User.ts index ec666b2..38663b8 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -1,6 +1,11 @@ import { Database } from "./Database"; import { LatLng } from "./LatLng"; import { RankingModes } from "../enums/RankingModes"; +import { Match } from "./Match"; +import { DataStream } from "./DataStream"; +import { UserArray } from "./UserArray"; +import { DataStreamArray } from "./DataStreamArray"; +import { ChatManager } from "../ChatManager"; //const StatusUpdate = require("./Packets/StatusUpdate.js"); const rankingModes = [ @@ -12,6 +17,10 @@ const rankingModes = [ export class User { private static readonly EMPTY_BUFFER = Buffer.alloc(0); + public users:UserArray; + public streams:DataStreamArray; + public chatManager:ChatManager; + public id:number; public username:string; public uuid:string; @@ -21,6 +30,7 @@ export class User { // Binato data public rankingMode:RankingModes = RankingModes.PP; + public spectatorStream:DataStream | null = null; // osu! data public playMode:number = 0; @@ -47,7 +57,7 @@ export class User { public pp:number = 0; // Multiplayer data - public currentMatch = null; + public currentMatch:Match | null = null; public matchSlotId:number = -1; public inMatch:boolean = false; @@ -56,12 +66,16 @@ export class User { public dbConnection:Database; - public constructor(id:number, username:string, uuid:string, dbConnection:Database) { + public constructor(id:number, username:string, uuid:string, dbConnection:Database, users:UserArray, streams:DataStreamArray, chatManager:ChatManager) { this.id = id; this.username = username; this.uuid = uuid; this.dbConnection = dbConnection; + + this.users = users; + this.streams = streams; + this.chatManager = chatManager; } // Concats new actions to the user's queue diff --git a/server/packets/ChangeAction.ts b/server/packets/ChangeAction.ts new file mode 100644 index 0000000..137f06f --- /dev/null +++ b/server/packets/ChangeAction.ts @@ -0,0 +1,11 @@ +import { User } from "../objects/User"; +import { StatusUpdate } from "./StatusUpdate"; + +export function ChangeAction(user:User, data:any) { + user.updatePresence(data); + + if (user.spectatorStream != null) { + const statusUpdate = StatusUpdate(user, user.id, false); + user.spectatorStream.Send(statusUpdate); + } +} \ No newline at end of file diff --git a/server/packets/Logout.ts b/server/packets/Logout.ts index 7b922a5..656ea35 100644 --- a/server/packets/Logout.ts +++ b/server/packets/Logout.ts @@ -1,7 +1,19 @@ import { ConsoleHelper } from "../../ConsoleHelper"; import { Database } from "../objects/Database"; +import { DataStreamArray } from "../objects/DataStreamArray"; import { User } from "../objects/User"; -export async function Logout(user:User, database:Database) { - +export async function Logout(user:User) { + if (user.uuid === "bot") throw "Tried to log bot out, WTF???"; + + const logoutStartTime = Date.now(); + + user.streams.RemoveUserFromAllStreams(user); + + // Remove user from user list + user.users.remove(user.uuid); + + await user.dbConnection.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.users.getLength() - 1]); + + ConsoleHelper.printBancho(`User logged out, took ${Date.now() - logoutStartTime}ms. [User: ${user.username}]`); } \ No newline at end of file diff --git a/server/packets/StatusUpdate.ts b/server/packets/StatusUpdate.ts new file mode 100644 index 0000000..e82878b --- /dev/null +++ b/server/packets/StatusUpdate.ts @@ -0,0 +1,37 @@ +import { RankingModes } from "../enums/RankingModes"; +import { User } from "../objects/User"; +const osu = require("osu-packet"); + +export function StatusUpdate(user:User, id:number, sendImmidiate:boolean = false) { + if (id == 3) return; // Ignore Bot + + // Create new osu packet writer + const osuPacketWriter = new osu.Bancho.Writer; + + // Get user's class + const userData = user.users.getById(id); + + if (userData == null) return; + + let UserStatusObject = { + userId: userData.id, + status: userData.actionID, + statusText: userData.actionText, + beatmapChecksum: userData.beatmapChecksum, + currentMods: userData.currentMods, + playMode: userData.playMode, + beatmapId: userData.beatmapID, + rankedScore: userData.rankedScore, + accuracy: userData.accuracy * 0.01, // Scale from 0:100 to 0:1 + playCount: userData.playCount, + totalScore: userData.totalScore, + rank: userData.rank, + performance: (userData.rankingMode == RankingModes.PP ? userData.pp : 0) + }; + + osuPacketWriter.HandleOsuUpdate(UserStatusObject); + + // Send data to user's queue + if (sendImmidiate) user.addActionToQueue(osuPacketWriter.toBuffer); + else return osuPacketWriter.toBuffer; +} \ No newline at end of file diff --git a/server/packets/UserPresence.ts b/server/packets/UserPresence.ts new file mode 100644 index 0000000..d0b747a --- /dev/null +++ b/server/packets/UserPresence.ts @@ -0,0 +1,24 @@ +import { User } from "../objects/User"; +const osu = require("osu-packet"); + +export function UserPresence(user:User, id:number, sendImmidiate:boolean = true) { + const osuPacketWriter = new osu.Bancho.Writer; + + const userData = user.users.getById(id); + + if (userData == null) return; + + osuPacketWriter.UserPresence({ + userId: id, + username: userData.username, + timezone: 0, + countryId: userData.countryID, + permissions: 4, + longitude: userData.location.longitude, + latitude: userData.location.latitude, + rank: userData.rank + }); + + if (sendImmidiate) userData.addActionToQueue(osuPacketWriter.toBuffer); + else return osuPacketWriter.toBuffer; +} \ No newline at end of file diff --git a/server/packets/UserPresenceBundle.ts b/server/packets/UserPresenceBundle.ts new file mode 100644 index 0000000..33069dc --- /dev/null +++ b/server/packets/UserPresenceBundle.ts @@ -0,0 +1,17 @@ +import { User } from "../objects/User"; +const osu = require("osu-packet"); + +export function UserPresenceBundle(user:User, sendImmidiate:boolean = true) { + const osuPacketWriter = new osu.Bancho.Writer; + + let userIds:Array = new Array(); + + for (let userData of user.users.getIterableItems()) { + userIds.push(userData.id); + } + + osuPacketWriter.UserPresenceBundle(userIds); + + if (sendImmidiate) user.addActionToQueue(osuPacketWriter.toBuffer); + else return osuPacketWriter.toBuffer; +} \ No newline at end of file diff --git a/server/packets/UserStatsRequest.ts b/server/packets/UserStatsRequest.ts new file mode 100644 index 0000000..29719e4 --- /dev/null +++ b/server/packets/UserStatsRequest.ts @@ -0,0 +1,13 @@ +import { User } from "../objects/User"; +import { StatusUpdate } from "./StatusUpdate"; +import { UserPresence } from "./UserPresence"; +import { UserPresenceBundle } from "./UserPresenceBundle"; + +export function UserStatsRequest(user:User, data:Array) { + UserPresenceBundle(user); + + for (let id of data) { + UserPresence(user, id); + StatusUpdate(user, id); + } +} \ No newline at end of file