Initial Future Commit (typescript)

This commit is contained in:
Holly Stubbs 2022-11-16 11:59:23 +00:00
parent 61e7476600
commit 4ebf9ee0e6
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
58 changed files with 1879 additions and 3918 deletions

View file

@ -1,79 +0,0 @@
console.clear();
// Globals
global.protocolVersion = 19;
const app = require("express")(),
consoleHelper = require("./consoleHelper.js"),
prometheusApp = require("express")(),
fs = require("fs"),
serverHandler = require("./server/serverHandler.js"),
config = require("./config.json");
if (config.prometheus.enabled) {
// We only need to require this if prom metrics are on.
const prom = require("prom-client");
const register = new prom.Registry();
register.setDefaultLabels({ app: "nodejs_binato" });
prom.collectDefaultMetrics({ register });
prometheusApp.get("*", async (req, res) => {
if (req.url.split("?")[0] != "/metrics") return res.status(404).end("");
res.end(await register.metrics());
});
prometheusApp.listen(config.prometheus.port, () => consoleHelper.printBancho(`Prometheus metrics listening at port ${config.prometheus.port}`));
} else consoleHelper.printWarn("Prometheus is disabled!");
if (config.express.compression) {
app.use(require("compression")());
consoleHelper.printBancho("Compression is enabled.");
} else consoleHelper.printWarn("Compression is disabled!");
app.use((req, res) => {
req.packet = Buffer.alloc(0);
req.on("data", (chunk) => req.packet = Buffer.concat([req.packet, chunk], req.packet.length + chunk.length));
req.on("end", () => {
switch (req.method) {
case "GET":
if (req.url == "/" || req.url == "/index.html" || req.url == "/index") {
res.sendFile(`${__dirname}/web/serverPage.html`);
} else if (req.url == "/chat") {
fs.readFile("./web/chatPageTemplate.html", (err, data) => {
if (err) throw err;
let lines = "", flip = false;
const limit = global.chatHistory.length < 10 ? 10 : global.chatHistory.length;
for (let i = global.chatHistory.length - 10; i < limit; i++) {
if (i < 0) i = 0;
lines += `<div class="line line${flip ? 1 : 0}">${global.chatHistory[i] == null ? "<hidden>blank</hidden>" : global.chatHistory[i]}</div>`
flip = !flip;
}
res.send(data.toString().replace("|content|", lines));
});
}
break;
case "POST":
// Make sure this address should respond to bancho requests
// Bancho addresses: c, c1, c2, c3, c4, c5, c6, ce
// Just looking for the first character being "c" *should* be enough
if (req.headers["host"].split(".")[0][0] == "c")
serverHandler(req, res);
else
res.status(400).send("400 | Bad Request!<br>Binato only accepts POST requests on Bancho subdomains.<hr>Binato");
break;
default:
res.status(405).send("405 | Method not allowed!<hr>Binato");
break;
}
});
});
app.listen(config.express.port, () => consoleHelper.printBancho(`Binato is up! Listening at port ${config.express.port}`));

74
Binato.ts Normal file
View file

@ -0,0 +1,74 @@
import { Application } from "express";
import compression from "compression";
import { ConsoleHelper } from "./ConsoleHelper";
import express from "express";
import { readFile } from "fs";
import { Registry, collectDefaultMetrics } from "prom-client";
const binatoApp:Application = express();
const config = require("./config.json");
if (config["prometheus"]["enabled"]) {
const register:Registry = new Registry();
register.setDefaultLabels({ app: "nodejs_binato" });
collectDefaultMetrics({ register });
const prometheusApp:Application = express();
prometheusApp.get("/metrics", async (req, res) => {
res.end(await register.metrics());
});
prometheusApp.listen(config["prometheus"]["port"], () => ConsoleHelper.printBancho(`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");
} else {
ConsoleHelper.printWarn("Compression is disabled");
}
binatoApp.use((req, res) => {
let packet:Buffer = Buffer.alloc(0);
req.on("data", (chunk:Buffer) => packet = Buffer.concat([packet, chunk], packet.length + chunk.length));
req.on("end", () => {
switch (req.method) {
case "GET":
if (req.url == "/" || req.url == "/index.html" || req.url == "/index") {
res.sendFile(`${__dirname}/web/serverPage.html`);
} else if (req.url == "/chat") {
readFile("./web/chatPageTemplate.html", (err, data) => {
if (err) throw err;
let lines = "", flip = false;
const limit = global.chatHistory.length < 10 ? 10 : global.chatHistory.length;
for (let i = global.chatHistory.length - 10; i < limit; i++) {
if (i < 0) i = 0;
lines += `<div class="line line${flip ? 1 : 0}">${global.chatHistory[i] == null ? "<hidden>blank</hidden>" : global.chatHistory[i]}</div>`
flip = !flip;
}
res.send(data.toString().replace("|content|", lines));
});
}
break;
case "POST":
// Make sure this address should respond to bancho requests
// Bancho addresses: c, c1, c2, c3, c4, c5, c6, ce
// Just looking for the first character being "c" *should* be enough
if (req.headers["host"].split(".")[0][0] == "c")
serverHandler(req, res);
else
res.status(400).send("400 | Bad Request!<br>Binato only accepts POST requests on Bancho subdomains.<hr>Binato");
break;
default:
res.status(405).send("405 | Method not allowed!<hr>Binato");
break;
}
});
});

63
ConsoleHelper.ts Normal file
View file

@ -0,0 +1,63 @@
import chalk from "chalk";
enum LogType {
INFO,
WARN,
ERROR
};
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 "))
} as const;
function correctValue(i:number) : string {
if (i <= 9) return `0${i}`;
else return i.toString();
}
function getTime() : string {
const time = new Date();
return chalk.green(`[${correctValue(time.getHours())}:${correctValue(time.getMinutes())}:${correctValue(time.getSeconds())}]`);
}
function log(tag:string, log:string, logType:LogType = LogType.INFO) : void {
switch (logType) {
case LogType.INFO:
return console.log(`${getTime()} ${tag} ${log}`);
case LogType.WARN:
return console.warn(`${getTime()} ${tag} ${log}`);
case LogType.ERROR:
return console.error(`${getTime()} ${tag} ${log}`);
}
}
export class ConsoleHelper {
public static printWebReq(s:string) : void {
log(LogTags.WEBREQ, s);
}
public static printBancho(s:string) : void {
log(LogTags.BANCHO, s);
}
public static printRedis(s:string) : void {
log(LogTags.REDIS, s);
}
public static printChat(s:string) : void {
log(LogTags.CHAT, s);
}
public static printWarn(s:string) : void {
log(LogTags.WARN, s);
}
public static printError(s:string) : void {
log(LogTags.ERROR, s);
}
}

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Holly Stubbs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,82 +0,0 @@
# Binato [![CodeFactor](https://www.codefactor.io/repository/github/tgpholly/binato/badge)](https://www.codefactor.io/repository/github/tgpholly/binato)
An implementation of osu!bancho in Javascript
i'm sorry peppy
<hr>
### Features:
- Multiplayer + Invites
- Spectator
- Tourney Client
- User Panel
- Friends List
- Chat & Channels
- Private Messages
- Minimum Viable Product of a bot
- For a command list check [BotCommandHandler](https://github.com/tgpholly/Binato/blob/master/server/BotCommandHandler.js) or use !help on a live server
### [Planned additions](https://github.com/tgpholly/Binato/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) | [List of currently known bugs](https://github.com/tgpethan/Binato/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
<hr>
## Setup:
While I don't support setting this up yourself it is fairly easy to do, all that should be required is:
- **NodeJS >= 10**
- **MariaDB or MySQL** (MariaDB is prefered as that is what this is tested and ran against in prod)
- Optional (Disabled via config):
- **Redis**
- **Prometheus**
Clone the repo and run `npm i` to install required packages, then copy `config.example.json` to `config.json` and edit to your liking (this is where http compression, prometheus and redis can be enabled/disabled)
After doing this running `node .` should start the server
## Reporting bugs:
To report a bug [create a new issue](https://github.com/tgpholly/Binato/issues/new) and include information such as your OS / Distro, Node version, disabled Binato features (e.g. Prometheus, Redis, compression) and console output at the time of the bug if applicable.
<hr>
## How to connect:
### 2013 - Stable Fallback (2015 / 2016 ?):
Stable fallback uses HTTP so for that you just need to direct it to the server<br>
You can do this using the hosts file
Location on Linux: /etc/hosts<br>
Location on Mac: /private/etc/hosts<br>
Location on Windows: C:/Windows/system32/drivers/etc/hosts
Add an entry in the hosts file that looks like the following:
```
<server_ip> osu.ppy.sh c.ppy.sh c1.ppy.sh
```
Where <server_ip> is the IP Address of the server hosting the bancho server
### 2016 - Early 2021:
Versions of osu! past Stable Fallback use HTTPS and as such you'll have to create a self signed certificate and make the server identify as ppy.sh<br>
In 2018 there were also new subdomains added which are:
- c2.ppy.sh
- c3.ppy.sh
- c4.ppy.sh
- c5.ppy.sh
- c6.ppy.sh
- ce.ppy.sh
### Now (2022):
There is a `-devserver` launch flag in the game which can be passed to the client to connect to a specific server. Example usage:
```
osu!.exe -devserver eusv.ml
```
You need to have your subdomains structured like osu!'s with the exception of `c*.ppy.sh` domains. There is only one that is polled for `-devserver` usage.
An example setup would be:
- osu.example.com (Score submit & web stuff)
- c.example.com (Bancho)
- a.example.com (Profile pictures)
<hr>
## Other Binato components:
### Website:
Binato's website is handled by [Binato-Website](https://github.com/tgpholly/Binato-Website)
### Profile Pictures:
Profile pictures can be handled by any standard HTTP server, there is also one I made for the task here: [Binato-ProfilePicture](https://github.com/tgpholly/Binato-ProfilePicture)

View file

@ -1,29 +0,0 @@
{
"express": {
"port": 5001,
"compression": true
},
"prometheus": {
"enabled": false,
"port": 9100
},
"redis": {
"enabled": false,
"address": "127.0.0.1",
"port": 6379,
"password": "",
"database": 0
},
"database": {
"address": "127.0.0.1",
"port": 3306,
"username": "username",
"password": "password",
"name": "osu!",
"pbkdf2": {
"itterations": 1337,
"keylength": 1337
},
"key": "examplekey"
}
}

View file

@ -1,63 +0,0 @@
const chalk = require("chalk");
const LogType = {
INFO: 0,
WARN: 1,
ERROR: 2
}
const LogTags = {
BANCHO: chalk.bgMagenta(chalk.black(" BANCHO ")),
WEBREQ: chalk.bgGreen(chalk.black(" WEBREQ ")),
CHAT: chalk.bgCyan(chalk.black(" CHATTO ")),
WARN: chalk.bgYellow(chalk.black(" WARNIN ")),
ERROR: chalk.bgRed(" ERROR! "),
REDIS: chalk.bgRed(chalk.white(" bREDIS "))
}
function correctValue(i) {
if (i <= 9) return "0"+i;
else return i;
}
function getTime() {
const time = new Date();
return chalk.green(`[${correctValue(time.getHours())}:${correctValue(time.getMinutes())}:${correctValue(time.getSeconds())}]`);
}
function log(tag = "", log = "", logType = LogType.INFO) {
switch (logType) {
case LogType.INFO:
return console.log(`${getTime()} ${tag} ${log}`);
case LogType.WARN:
return console.warn(`${getTime()} ${tag} ${log}`);
case LogType.ERROR:
return console.error(`${getTime()} ${tag} ${log}`);
}
}
module.exports = {
printWebReq:function(s) {
log(LogTags.WEBREQ, s);
},
printBancho:function(s) {
log(LogTags.BANCHO, s);
},
printRedis:function(s) {
log(LogTags.REDIS, s);
},
printChat:function(s) {
log(LogTags.CHAT, s);
},
printWarn:function(s) {
log(LogTags.WARN, chalk.yellow(s), LogType.WARN);
},
printError:function(s) {
log(LogTags.ERROR, chalk.red(s), LogType.ERROR);
}
}

176
osu!.sql
View file

@ -1,176 +0,0 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET time_zone = "+00:00";
CREATE DATABASE IF NOT EXISTS `osu!` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `osu!`;
CREATE TABLE `friends` (
`user` int(11) NOT NULL,
`friendsWith` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `scores` (
`id` int(11) NOT NULL,
`userid` int(11) NOT NULL,
`beatmap_md5` varchar(32) NOT NULL DEFAULT '',
`username` varchar(30) NOT NULL DEFAULT '',
`score` bigint(20) NOT NULL,
`max_combo` int(11) NOT NULL DEFAULT '0',
`full_combo` tinyint(1) NOT NULL DEFAULT '0',
`mods` int(11) NOT NULL DEFAULT '0',
`300_count` int(11) NOT NULL DEFAULT '0',
`100_count` int(11) NOT NULL DEFAULT '0',
`50_count` int(11) NOT NULL DEFAULT '0',
`katus_count` int(11) NOT NULL DEFAULT '0',
`gekis_count` int(11) NOT NULL DEFAULT '0',
`misses_count` int(11) NOT NULL DEFAULT '0',
`time` varchar(18) NOT NULL DEFAULT '',
`play_mode` tinyint(4) NOT NULL DEFAULT '0',
`completed` tinyint(11) NOT NULL DEFAULT '0',
`accuracy` float(15,12) DEFAULT NULL,
`pp` float NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `mp_matches` (
`id` int(10) UNSIGNED NOT NULL,
`name` varchar(127) NOT NULL,
`open_time` varchar(18) NOT NULL,
`close_time` varchar(18) DEFAULT NULL,
`seed` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `mp_match_rounds` (
`id` int(11) NOT NULL,
`match_id` int(11) NOT NULL,
`round_id` int(11) NOT NULL,
`round_mode` tinyint(4) NOT NULL,
`match_type` tinyint(4) NOT NULL,
`round_scoring_type` tinyint(4) NOT NULL,
`round_team_type` tinyint(4) NOT NULL,
`round_mods` int(11) NOT NULL,
`beatmap_md5` varchar(127) NOT NULL,
`freemod` tinyint(1) NOT NULL DEFAULT 0,
`player0` tinytext DEFAULT NULL,
`player1` tinytext DEFAULT NULL,
`player2` tinytext DEFAULT NULL,
`player3` tinytext DEFAULT NULL,
`player4` tinytext DEFAULT NULL,
`player5` tinytext DEFAULT NULL,
`player6` tinytext DEFAULT NULL,
`player7` tinytext DEFAULT NULL,
`player8` tinytext DEFAULT NULL,
`player9` tinytext DEFAULT NULL,
`player10` tinytext DEFAULT NULL,
`player11` tinytext DEFAULT NULL,
`player12` tinytext DEFAULT NULL,
`player13` tinytext DEFAULT NULL,
`player14` tinytext DEFAULT NULL,
`player15` tinytext DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `users_info` (
`id` int(11) NOT NULL,
`username` varchar(15) NOT NULL,
`username_safe` varchar(15) NOT NULL,
`password_hash` text NOT NULL,
`password_salt` text NOT NULL,
`email` text NOT NULL,
`country` varchar(2) NOT NULL,
`reg_date` datetime NOT NULL,
`last_login_date` datetime NOT NULL,
`last_played_mode` tinyint(4) NOT NULL,
`online_now` tinyint(1) NOT NULL,
`tags` int(11) NOT NULL,
`supporter` tinyint(1) NOT NULL,
`web_session` varchar(64) NOT NULL,
`verification_needed` tinyint(1) NOT NULL DEFAULT '0',
`password_change_required` tinyint(1) NOT NULL,
`has_old_password` int(11) NOT NULL DEFAULT 0,
`password_reset_key` text DEFAULT NULL,
`away_message` varchar(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `users_modes_info` (
`n` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`mode_id` tinyint(4) NOT NULL,
`count300` int(10) UNSIGNED NOT NULL,
`count100` int(10) UNSIGNED NOT NULL,
`count50` int(10) UNSIGNED NOT NULL,
`countmiss` int(10) UNSIGNED NOT NULL,
`playcount` int(10) UNSIGNED NOT NULL,
`total_score` int(10) UNSIGNED NOT NULL,
`ranked_score` int(11) UNSIGNED NOT NULL,
`pp_rank` int(11) NOT NULL,
`pp_raw` int(11) NOT NULL DEFAULT '1',
`count_rank_ss` int(10) UNSIGNED NOT NULL,
`count_rank_s` int(10) UNSIGNED NOT NULL,
`count_rank_a` int(10) UNSIGNED NOT NULL,
`pp_country_rank` int(11) NOT NULL,
`playtime` bigint(255) NOT NULL DEFAULT '0',
`avg_accuracy` float NOT NULL DEFAULT '0',
`level` int(255) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `web_info` (
`i` int(11) NOT NULL,
`HomepageText` varchar(255) NOT NULL DEFAULT 'A default Binato instance!'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `web_prefs` (
`id` int(11) NOT NULL,
`keyboard` tinyint(1) NOT NULL DEFAULT '0',
`mouse` tinyint(1) NOT NULL DEFAULT '0',
`tablet` tinyint(1) NOT NULL DEFAULT '0',
`touch` tinyint(1) NOT NULL DEFAULT '0',
`location` varchar(32) NOT NULL,
`interests` varchar(64) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `web_titles` (
`id` int(11) NOT NULL,
`title` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
ALTER TABLE `scores`
ADD PRIMARY KEY (`id`);
ALTER TABLE `users_info`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `id` (`id`);
ALTER TABLE `users_modes_info`
ADD PRIMARY KEY (`n`);
ALTER TABLE `web_info`
ADD PRIMARY KEY (`i`);
ALTER TABLE `web_prefs`
ADD PRIMARY KEY (`id`);
ALTER TABLE `web_titles`
ADD PRIMARY KEY (`id`);
ALTER TABLE `mp_matches`
ADD PRIMARY KEY (`id`);
ALTER TABLE `mp_match_rounds`
ADD PRIMARY KEY (`id`);
ALTER TABLE `mp_matches`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
ALTER TABLE `mp_match_rounds`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
ALTER TABLE `scores`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=0;
ALTER TABLE `users_info`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=100;
ALTER TABLE `users_modes_info`
MODIFY `n` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=0;
INSERT INTO `web_info` (`i`, `HomepageText`) VALUES ('0', 'A default Binato instance!');

2119
package-lock.json generated

File diff suppressed because it is too large Load diff

20
package.json Executable file → Normal file
View file

@ -2,21 +2,25 @@
"name": "binato",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"main": "Binato.ts",
"scripts": {
"dev:run": "nodemon --watch './**/*.ts' Binato.ts"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"aes256": "^1.1.0",
"@types/express": "^4.17.14",
"@types/node": "^18.11.9",
"chalk": "^4.1.0",
"compression": "^1.7.4",
"express": "^4.17.1",
"express": "^4.18.2",
"mysql2": "^2.3.3",
"node-fetch": "^2.6.7",
"osu-packet": "^4.1.2",
"prom-client": "^13.2.0",
"redis": "^4.0.6",
"uuid": "^8.3.2"
"prom-client": "^14.1.0"
},
"devDependencies": {
"nodemon": "^2.0.20",
"ts-node": "^10.9.1"
}
}

105
server/serverHandler.js → server/BanchoServer.ts Executable file → Normal file
View file

@ -1,7 +1,8 @@
const osu = require("osu-packet"),
fs = require("fs"),
consoleHelper = require("../consoleHelper.js"),
packetIDs = require("./packetIDs.js"),
import * as osu from "osu-packet";
import { ConsoleHelper } from "../ConsoleHelper";
import { Packets } from "./enums/Packets";
const
loginHandler = require("./loginHandler.js"),
parseUserData = require("./util/parseUserData.js"),
User = require("./User.js"),
@ -34,7 +35,7 @@ async function subscribeToChannel(channelName = "", callback = function(message
await subscriptionClient.connect();
// Subscribe to channel
await subscriptionClient.subscribe(channelName, callback);
consoleHelper.printRedis(`Subscribed to ${channelName} channel`);
ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`);
}
// Do redis if it's enabled
@ -128,25 +129,25 @@ const ChangeAction = require("./Packets/ChangeAction.js"),
// A class for managing everything multiplayer
global.MultiplayerManager = new MultiplayerManager();
module.exports = async function(req, res) {
module.exports = async function(req, res, packet:Buffer) {
// Get the client's token string and request data
const requestTokenString = req.header("osu-token"),
requestData = req.packet;
const requestTokenString:string = req.header("osu-token"),
requestData:Buffer = packet;
// Server's response
let responseData;
let responseData:Buffer;
// Check if the user is logged in
if (requestTokenString == null) {
// Client doesn't have a token yet, let's auth them!
const userData = parseUserData(requestData);
consoleHelper.printBancho(`New client connection. [User: ${userData.username}]`);
ConsoleHelper.printBancho(`New client connection. [User: ${userData.username}]`);
await loginHandler(req, res, userData);
} else {
// Client has a token, let's see what they want.
try {
// Get the current user
const PacketUser = getUserFromToken(requestTokenString);
const PacketUser:User = getUserFromToken(requestTokenString);
// Make sure the client's token isn't invalid
if (PacketUser != null) {
@ -161,164 +162,164 @@ module.exports = async function(req, res) {
// Go through each packet sent by the client
for (CurrentPacket of PacketData) {
switch (CurrentPacket.id) {
case packetIDs.client_changeAction:
case Packets.Client_ChangeAction:
ChangeAction(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_sendPublicMessage:
case Packets.Client_SendPublicMessage:
SendPublicMessage(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_logout:
case Packets.Client_Logout:
await Logout(PacketUser);
break;
case packetIDs.client_requestStatusUpdate:
case Packets.Client_RequestStatusUpdate:
UserPresenceBundle(PacketUser);
break;
case packetIDs.client_startSpectating:
case Packets.Client_StartSpectating:
Spectator.startSpectatingUser(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_spectateFrames:
case Packets.Client_SpectateFrames:
Spectator.sendSpectatorFrames(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_stopSpectating:
case Packets.Client_StopSpectating:
Spectator.stopSpectatingUser(PacketUser);
break;
case packetIDs.client_sendPrivateMessage:
case Packets.client_sendPrivateMessage:
SendPrivateMessage(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_joinLobby:
case Packets.client_joinLobby:
global.MultiplayerManager.userEnterLobby(PacketUser);
break;
case packetIDs.client_partLobby:
case Packets.client_partLobby:
global.MultiplayerManager.userLeaveLobby(PacketUser);
break;
case packetIDs.client_createMatch:
case Packets.client_createMatch:
await global.MultiplayerManager.createMultiplayerMatch(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_joinMatch:
case Packets.client_joinMatch:
global.MultiplayerManager.joinMultiplayerMatch(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchChangeSlot:
case Packets.client_matchChangeSlot:
PacketUser.currentMatch.moveToSlot(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchReady:
case Packets.client_matchReady:
PacketUser.currentMatch.setStateReady(PacketUser);
break;
case packetIDs.client_matchChangeSettings:
case Packets.client_matchChangeSettings:
await PacketUser.currentMatch.updateMatch(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchNotReady:
case Packets.client_matchNotReady:
PacketUser.currentMatch.setStateNotReady(PacketUser);
break;
case packetIDs.client_partMatch:
case Packets.client_partMatch:
await global.MultiplayerManager.leaveMultiplayerMatch(PacketUser);
break;
// Also handles user kick if the slot has a user
case packetIDs.client_matchLock:
case Packets.client_matchLock:
PacketUser.currentMatch.lockMatchSlot(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchNoBeatmap:
case Packets.client_matchNoBeatmap:
PacketUser.currentMatch.missingBeatmap(PacketUser);
break;
case packetIDs.client_matchSkipRequest:
case Packets.client_matchSkipRequest:
PacketUser.currentMatch.matchSkip(PacketUser);
break;
case packetIDs.client_matchHasBeatmap:
case Packets.client_matchHasBeatmap:
PacketUser.currentMatch.notMissingBeatmap(PacketUser);
break;
case packetIDs.client_matchTransferHost:
case Packets.client_matchTransferHost:
PacketUser.currentMatch.transferHost(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchChangeMods:
case Packets.client_matchChangeMods:
PacketUser.currentMatch.updateMods(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchStart:
case Packets.client_matchStart:
PacketUser.currentMatch.startMatch();
break;
case packetIDs.client_matchLoadComplete:
case Packets.client_matchLoadComplete:
PacketUser.currentMatch.matchPlayerLoaded(PacketUser);
break;
case packetIDs.client_matchComplete:
case Packets.client_matchComplete:
await PacketUser.currentMatch.onPlayerFinishMatch(PacketUser);
break;
case packetIDs.client_matchScoreUpdate:
case Packets.client_matchScoreUpdate:
PacketUser.currentMatch.updatePlayerScore(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_matchFailed:
case Packets.client_matchFailed:
PacketUser.currentMatch.matchFailed(PacketUser);
break;
case packetIDs.client_matchChangeTeam:
case Packets.client_matchChangeTeam:
PacketUser.currentMatch.changeTeam(PacketUser);
break;
case packetIDs.client_channelJoin:
case Packets.client_channelJoin:
ChannelJoin(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_channelPart:
case Packets.client_channelPart:
ChannelPart(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_setAwayMessage:
case Packets.client_setAwayMessage:
SetAwayMessage(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_friendAdd:
case Packets.client_friendAdd:
AddFriend(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_friendRemove:
case Packets.client_friendRemove:
RemoveFriend(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_userStatsRequest:
case Packets.client_userStatsRequest:
UserStatsRequest(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_specialMatchInfoRequest:
case Packets.client_specialMatchInfoRequest:
TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_specialJoinMatchChannel:
case Packets.client_specialJoinMatchChannel:
TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_specialLeaveMatchChannel:
case Packets.client_specialLeaveMatchChannel:
TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_invite:
case Packets.client_invite:
MultiplayerInvite(PacketUser, CurrentPacket.data);
break;
case packetIDs.client_userPresenceRequest:
case Packets.client_userPresenceRequest:
UserPresence(PacketUser, PacketUser.id); // Can't really think of a way to generalize this?
break;
@ -352,4 +353,4 @@ module.exports = async function(req, res) {
res.end(responseData);
}
}
};
};

View file

@ -1,213 +0,0 @@
const osu = require("osu-packet"),
maths = require("./util/Maths.js"),
Streams = require("./Streams.js"),
OsuBattleRoyale = require("./MultiplayerExtras/OsuBattleRoyale.js");
module.exports = function(User, Message, Stream, IsCalledFromMultiplayer = false) {
if (Message[0] != "!") return;
const command = Message.split(" ")[0];
const args = Message.split(" ");
let responseMessage = "";
let commandBanchoPacketWriter = null;
switch (command) {
case "!help":
// This is terrible
if (args.length == 1) {
responseMessage = "Commands with an * next to them have a sub help section" +
"\n!help - Shows this message" +
"\n!roll - Rolls a random number or a number between 0 and a given number" +
"\n!ranking* - Sets your perfered ranking type" +
"\n!mp* - Shows information about all multiplayer commands" +
"\n!admin* - Shows information about all admin commands";
} else {
switch (args[1]) {
case "ranking":
responseMessage = "Ranking Commands:" +
"\n!ranking pp - Sets your ranking type to pp" +
"\n!ranking score - Sets your ranking type to score" +
"\n!ranking acc - Sets your ranking type to accuracy";
break;
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 obr - Enables Battle Royale mode";
break;
case "admin":
responseMessage = "Admin Commands:" +
"\n!lock - Locks/Unlocks a channel and limits conversation to mods and above only";
break;
default:
break;
}
}
break;
case "!ranking":
if (args.length == 1) {
responseMessage = "You need to select a ranking mode! use \"!help ranking\" to see the options.";
} else {
switch (args[1]) {
case "pp":
responseMessage = "Set ranking mode to pp";
User.rankingMode = 0;
User.updateUserInfo(true);
break;
case "score":
responseMessage = "Set ranking mode to score";
User.rankingMode = 1;
User.updateUserInfo(true);
break;
case "acc":
responseMessage = "Set ranking mode to accuracy";
User.rankingMode = 2;
User.updateUserInfo(true);
break;
}
}
break;
case "!roll":
if (args.length == 1) {
responseMessage = User.username + " rolled " + maths.randInt(0, 65535);
} else {
if (`${parseInt(args[1])}` == "NaN") responseMessage = User.username + " rolled " + maths.randInt(0, 65535);
else responseMessage = User.username + " rolled " + maths.randInt(0, parseInt(args[1]));
}
break;
case "!lock":
if (!Stream.includes("#")) responseMessage = "Multiplayer channels and private channels cannot be locked!";
else {
for (let i = 0; i < global.channels.length; i++) {
// Find the channel that pertains to this stream
if (global.channels[i].channelName == Stream) {
if (global.channels[i].locked) {
global.channels[i].locked = false;
responseMessage = "Channel is now unlocked.";
} else {
global.channels[i].locked = true;
responseMessage = "Channel is now locked, chat restricted to mods and above.";
}
break;
}
}
}
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 (!isNaN(args[2])) {
User.currentMatch.matchStartCountdownActive = true;
let countdown = parseInt(args[2]);
let intervalRef = setInterval(() => {
let local_osuPacketWriter = new osu.Bancho.Writer;
if (countdown != 0 && countdown > 0) countdown--;
if (countdown <= 10 && countdown > 0) {
local_osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: "Starting in " + countdown,
target: "#multiplayer",
senderId: global.botUser.id
});
Streams.sendToStream(Stream, local_osuPacketWriter.toBuffer, null);
} else if (countdown == 0) {
local_osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: "Good luck, have fun!",
target: "#multiplayer",
senderId: global.botUser.id
});
Streams.sendToStream(Stream, local_osuPacketWriter.toBuffer, null);
User.currentMatch.matchStartCountdownActive = false;
setTimeout(() => User.currentMatch.startMatch(), 1000);
clearInterval(intervalRef);
}
}, 1000);
} else {
responseMessage = "Good luck, have fun!";
setTimeout(() => User.currentMatch.startMatch(), 1000);
}
break;
case "abort":
//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.botUser.username,
message: "osu! Battle Royale has been disabled!",
target: "#multiplayer",
senderId: global.botUser.id
});
User.currentMatch.multiplayerExtras = null;
Streams.sendToStream(Stream, commandBanchoPacketWriter.toBuffer, null);
}
else enableOBR(User, Stream, commandBanchoPacketWriter);
}
else enableOBR(User, Stream, commandBanchoPacketWriter);
break;
default:
break;
}
break;
}
const osuPacketWriter = new osu.Bancho.Writer;
if (responseMessage != "") {
if (Stream.includes("#")) {
osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: responseMessage,
target: Stream,
senderId: global.botUser.id
});
} else {
osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: responseMessage,
target: "#multiplayer",
senderId: global.botUser.id
});
}
}
Streams.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.botUser.username,
message: "osu! Battle Royale has been enabled!",
target: "#multiplayer",
senderId: global.botUser.id
});
commandBanchoPacketWriter.SendMessage({
sendingClient: global.botUser.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.botUser.id
});
Streams.sendToStream(Stream, commandBanchoPacketWriter.toBuffer, null);
}

0
server/Channels.ts Normal file
View file

View file

@ -1,79 +0,0 @@
const mysql = require("mysql2");
const consoleHelper = require("../consoleHelper.js");
module.exports = class {
constructor(databaseAddress, databasePort = 3306, databaseUsername, databasePassword, databaseName, connectedCallback) {
this.connectionPool = mysql.createPool({
connectionLimit: 128,
host: databaseAddress,
port: databasePort,
user: databaseUsername,
password: databasePassword,
database: databaseName
});
const classCreationTime = Date.now();
this.dbActive = false;
if (connectedCallback == null) {
this.dbActive = true;
} else {
const connectionCheckInterval = setInterval(() => {
this.query("SELECT name FROM osu_info LIMIT 1")
.then(data => {
consoleHelper.printBancho(`Connected to database. Took ${Date.now() - classCreationTime}ms`);
this.dbActive = true;
clearInterval(connectionCheckInterval);
connectedCallback();
})
.catch(err => {});
}, 167); // Roughly 6 times per sec
}
}
query(query = "", data) {
const limited = query.includes("LIMIT 1");
return new Promise((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
reject(err);
try { connection.release();}
catch (e) {
console.error("Failed to release mysql connection", err);
}
} else {
// Use old query
if (data == null) {
connection.query(query, (err, data) => {
if (err) {
reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
}
});
}
// Use new prepared statements w/ placeholders
else {
connection.execute(query, data, (err, data) => {
if (err) {
reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
}
});
}
}
});
});
}
}
function dataReceived(resolveCallback, data, limited = false) {
if (limited) resolveCallback(data[0]);
else resolveCallback(data);
}

View file

@ -1,143 +0,0 @@
const osu = require("osu-packet"),
MultiplayerMatch = require("../MultiplayerMatch.js"),
getUserById = require("../util/getUserById.js"),
Streams = require("../Streams.js");
function sameScoreCheck(playerScores = [{playerId:0,slotId:0,score:0,isCurrentlyFailed:false}], lowestScore = 0) {
for (let playerScore of playerScores) {
// All players don't have the same score
if (playerScore.score != lowestScore || playerScore.isCurrentlyFailed)
return false;
}
return true;
}
function kickLowScorers(playerScores = [{playerId:0,slotId:0,score:0,isCurrentlyFailed:false}], MultiplayerMatch) {
for (let playerScore of playerScores) {
// Kick players if they have the lowest score or they are in a failed state
if (playerScore.score == lowestScore || playerScore.isCurrentlyFailed) {
let osuPacketWriter = new osu.Bancho.Writer;
// Get the slot this player is in
const slot = MultiplayerMatch.slots[playerScore.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
Streams.removeUserFromStream(MultiplayerMatch.matchStreamName, kickedPlayer.uuid);
Streams.removeUserFromStream(MultiplayerMatch.matchChatStreamName, kickedPlayer.uuid);
// Remove the kicked player's referance this this match
kickedPlayer.currentMatch = null;
// Inform the kicked user's client that they were kicked
osuPacketWriter.MatchUpdate(MultiplayerMatch.createOsuMatchJSON());
osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: "You were eliminated from the match!",
target: global.botUser.username,
senderId: global.botUser.id
});
kickedPlayer.addActionToQueue(osuPacketWriter.toBuffer);
osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: `${kickedPlayer.username} was eliminated from the match!`,
target: "#multiplayer",
senderId: global.botUser.id
});
Streams.sendToStream(MultiplayerMatch.matchChatStreamName, osuPacketWriter.toBuffer, null);
}
}
}
function getRemainingPlayerCount(playerScores = [{playerId:0,slotId:0,score:0,isCurrentlyFailed:false}], MultiplayerMatch) {
let numberOfPlayersRemaining = 0;
for (let playerScore of playerScores) {
const slot = MultiplayerMatch.slots[playerScore.slotId];
if (slot.playerId !== -1 && slot.status !== 2) {
numberOfPlayersRemaining++;
}
}
return numberOfPlayersRemaining;
}
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 = 8589934588;
// Find the lowest score
for (let i = 0; i < playerScores.length; i++) {
const playerScore = playerScores[i];
if (playerScore.score < lowestScore) lowestScore = playerScore.score;
}
// Check if everyone has the same score, we don't need to kick anyone if they do.
if (sameScoreCheck(playerScores)) return;
// Kick everyone with the lowest score
kickLowScorers(playerScores, this.MultiplayerMatch);
// Get number of players remaining
let numberOfPlayersRemaining = getRemainingPlayerCount(playerScores, this.MultiplayerMatch);
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.botUser.username,
message: "Everyone was eliminated from the match! Nobody wins.",
target: global.botUser.username,
senderId: global.botUser.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.botUser.username,
message: "You are the last one remaining, you win!",
target: global.botUser.username,
senderId: global.botUser.id
});
playerClassContainer.addActionToQueue(remainingWriterContainer.toBuffer);
break;
}
// Update match for players in the match
this.MultiplayerMatch.sendMatchUpdate();
// Update the match listing for users in the multiplayer lobby
global.MultiplayerManager.updateMatchListing();
}
}

View file

@ -1,227 +0,0 @@
const osu = require("osu-packet"),
UserPresenceBundle = require("./Packets/UserPresenceBundle.js"),
UserPresence = require("./Packets/UserPresence.js"),
StatusUpdate = require("./Packets/StatusUpdate.js"),
MultiplayerMatch = require("./MultiplayerMatch.js"),
Streams = require("./Streams.js"),
User = require("./User.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
Streams.addUserToStream("multiplayer_lobby", currentUser.uuid);
// Send user ids of all online users to all users in the lobby
Streams.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
Streams.sendToStream("multiplayer_lobby", UserPresence(currentUser, slot.playerId, false), null);
Streams.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 (!Streams.isUserInStream("#lobby", currentUser.uuid)) {
Streams.addUserToStream("#lobby", currentUser.uuid);
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 (Streams.isUserInStream("multiplayer_lobby", currentUser.uuid))
Streams.removeUserFromStream("multiplayer_lobby", currentUser.uuid);
}
updateMatchListing() {
// Send user ids of all online users to all users in the lobby
Streams.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
Streams.sendToStream("multiplayer_lobby", UserPresence(null, slot.playerId, false), null);
Streams.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
Streams.sendToStream("multiplayer_lobby", osuPacketWriter.toBuffer, null);
}
}
async createMultiplayerMatch(MatchHost, MatchData) {
let matchClass = null;
this.matches.push(matchClass = await MultiplayerMatch.createMatch(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 chatStreamName = this.matches[matchIndex].matchChatStreamName;
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) {
throw "MatchFullException";
}
// Set the user's current match to this match
JoiningUser.currentMatch = match;
JoiningUser.inMatch = true;
// Add user to the stream for the match
Streams.addUserToStream(streamName, JoiningUser.uuid);
Streams.addUserToStream(chatStreamName, JoiningUser.uuid);
// Inform all users in the match that a new user has joined
Streams.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) {
// Inform the client that there was an issue joining the match
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.MatchJoinFail();
JoiningUser.addActionToQueue(osuPacketWriter.toBuffer);
this.updateMatchListing();
}
}
async leaveMultiplayerMatch(MatchUser = new User) {
// Make sure the user is in a match
if (MatchUser.currentMatch == null) return;
const mpMatch = MatchUser.currentMatch;
mpMatch.leaveMatch(MatchUser);
let empty = true;
// Check if the match is empty
for (let i = 0; i < mpMatch.slots.length; i++) {
const slot = mpMatch.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);
// Close the db match
global.DatabaseHelper.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE id = ?", [mpMatch.matchId]);
}
MatchUser.currentMatch = null;
MatchUser.matchSlotId = -1;
MatchUser.inMatch = false;
// Update the match listing to reflect this change (either removal or user leaving)
this.updateMatchListing();
}
getMatch(MatchID) {
for (let match in this.matches) {
if (match.matchId == MatchID) return match;
}
return null;
}
}

View file

@ -1,564 +0,0 @@
const osu = require("osu-packet"),
getUserById = require("./util/getUserById.js"),
StatusUpdate = require("./Packets/StatusUpdate.js"),
Streams = require("./Streams.js"),
User = require("./User.js");
// TODO: Cache the player's slot position in their user class for a small optimisation
class MultiplayerMatch {
constructor(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}) {
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);
}
}
module.exports = MultiplayerMatch;

View file

@ -1,3 +0,0 @@
module.exports = function(CurrentUser, FriendToAdd) {
global.DatabaseHelper.query("INSERT INTO friends (user, friendsWith) VALUES (?, ?);", [CurrentUser.id, FriendToAdd]);
}

View file

@ -1,11 +0,0 @@
const StatusUpdate = require("./StatusUpdate.js"),
Streams = require("../Streams.js");
module.exports = function(currentUser, data) {
currentUser.updatePresence(data);
if (Streams.exists(`sp_${currentUser.username}`)) {
const statusUpdate = StatusUpdate(currentUser, currentUser.id, false);
Streams.sendToStream(`sp_${currentUser.username}`, statusUpdate, null);
}
}

View file

@ -1,17 +0,0 @@
const osu = require("osu-packet"),
consoleHelper = require("../../consoleHelper.js"),
Streams = require("../Streams.js");
module.exports = function(CurrentUser, channelName = "") {
// Make sure the user is not already in the channel
if (Streams.isUserInStream(channelName, CurrentUser.uuid))
return consoleHelper.printBancho(`Did not add user to channel ${channelName} because they are already in it`);
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.ChannelJoinSuccess(channelName);
if (!Streams.isUserInStream(channelName, CurrentUser.uuid))
Streams.addUserToStream(channelName, CurrentUser.uuid);
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
}

View file

@ -1,7 +0,0 @@
const Streams = require("../Streams.js");
module.exports = function(CurrentUser, data) {
if (data == "#multiplayer") return; // Ignore requests for multiplayer
Streams.removeUserFromStream(data, CurrentUser.uuid);
}

View file

@ -1,23 +0,0 @@
const consoleHelper = require("../../consoleHelper.js"),
Streams = require("../Streams.js");
module.exports = async function(CurrentUser) {
if (CurrentUser.uuid === "bot") throw "Tried to log bot out, WTF???";
const logoutStartTime = Date.now();
const streamList = Streams.getStreams();
for (let i = 0; i < streamList.length; i++) {
if (Streams.isUserInStream(streamList[i], CurrentUser.uuid)) {
Streams.removeUserFromStream(streamList[i], CurrentUser.uuid);
}
}
// Remove user from user list
global.users.remove(CurrentUser.uuid);
await global.DatabaseHelper.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [global.users.getLength() - 1]);
consoleHelper.printBancho(`User logged out, took ${Date.now() - logoutStartTime}ms. [User: ${CurrentUser.username}]`);
}

View file

@ -1,17 +0,0 @@
const osu = require("osu-packet"),
getUserById = require("../util/getUserById.js");
module.exports = function(CurrentUser, InvitedUser) {
let osuPacketWriter = new osu.Bancho.Writer;
const InvitedUserClass = getUserById(InvitedUser);
osuPacketWriter.SendMessage({
sendingClient: CurrentUser.username,
message: `Come join my multiplayer match: [osump://${CurrentUser.currentMatch.matchId}/ ${CurrentUser.currentMatch.gameName}]`,
target: CurrentUser.username,
senderId: CurrentUser.id
});
InvitedUserClass.addActionToQueue(osuPacketWriter.toBuffer);
}

View file

@ -1,3 +0,0 @@
module.exports = function(CurrentUser, FriendToRemove) {
global.DatabaseHelper.query("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [CurrentUser.id, FriendToRemove]);
}

View file

@ -1,19 +0,0 @@
const osu = require("osu-packet"),
getUserByUsername = require("../util/getUserByUsername.js");
module.exports = function(CurrentUser, CurrentPacket) {
const osuPacketWriter = new osu.Bancho.Writer;
const userSentTo = getUserByUsername(CurrentPacket.target);
if (userSentTo == null) return;
osuPacketWriter.SendMessage({
sendingClient: CurrentUser.username,
message: CurrentPacket.message,
target: CurrentUser.username,
senderId: CurrentUser.id
});
// Write chat message to stream asociated with chat channel
return userSentTo.addActionToQueue(osuPacketWriter.toBuffer);
}

View file

@ -1,58 +0,0 @@
const osu = require("osu-packet"),
botCommandHandler = require("../BotCommandHandler.js"),
consoleHelper = require("../../consoleHelper.js"),
Streams = require("../Streams.js");
module.exports = function(CurrentUser, CurrentPacket) {
let isSendingChannelLocked = false;
for (let i = 0; i < global.channels.length; i++) {
if (!CurrentPacket.target.includes("#")) break;
if (global.channels[i].channelName == CurrentPacket.target) {
isSendingChannelLocked = global.channels[i].locked;
break;
}
}
if (isSendingChannelLocked) {
if (CurrentPacket.message.includes("!")) {
botCommandHandler(CurrentUser, CurrentPacket.message, CurrentPacket.target);
} else {
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.SendMessage({
sendingClient: global.botUser.username,
message: "The channel you are currently trying to send to is locked, please check back later!",
target: CurrentPacket.target,
senderId: global.botUser.id
});
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
}
return;
}
consoleHelper.printChat(`${CurrentUser.username} in ${CurrentPacket.target} sent: ${CurrentPacket.message}`);
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.SendMessage({
sendingClient: CurrentUser.username,
message: CurrentPacket.message,
target: CurrentPacket.target,
senderId: CurrentUser.id
});
if (CurrentPacket.target == "#multiplayer") {
Streams.sendToStream(CurrentUser.currentMatch.matchChatStreamName, osuPacketWriter.toBuffer, CurrentUser.uuid);
botCommandHandler(CurrentUser, CurrentPacket.message, CurrentUser.currentMatch.matchChatStreamName, true);
return;
}
// Check the stream that we're sending to even exists
if (!Streams.exists(CurrentPacket.target)) return;
// Write chat message to stream asociated with chat channel
Streams.sendToStream(CurrentPacket.target, osuPacketWriter.toBuffer, CurrentUser.uuid);
if (CurrentPacket.target == "#osu")
global.addChatMessage(`${CurrentUser.username}: ${CurrentPacket.message.replaceAll("<", "&lt;").replaceAll(">", "&gt;")}`);
botCommandHandler(CurrentUser, CurrentPacket.message, CurrentPacket.target);
return;
}

View file

@ -1,3 +0,0 @@
module.exports = function(CurrentUser, Message) {
global.DatabaseHelper.query("UPDATE users_info SET away_message = ? WHERE id = ?", [Message.message, CurrentUser.id]);
}

View file

@ -1,36 +0,0 @@
const osu = require("osu-packet"),
getUserById = require("../util/getUserById.js");
module.exports = function(currentUser, id = 0, sendImmidiate = true) {
if (id == 3) return; // Ignore Bot
// Create new osu packet writer
const osuPacketWriter = new osu.Bancho.Writer;
// Get user's class
const User = getUserById(id);
if (User == null) return;
let UserStatusObject = {
userId: User.id,
status: User.actionID,
statusText: User.actionText,
beatmapChecksum: User.beatmapChecksum,
currentMods: User.currentMods,
playMode: User.playMode,
beatmapId: User.beatmapID,
rankedScore: User.rankedScore,
accuracy: User.accuracy * 0.01, // Scale from 0:100 to 0:1
playCount: User.playCount,
totalScore: User.totalScore,
rank: User.rank,
performance: (User.rankingMode == 0 ? User.pp : 0)
};
osuPacketWriter.HandleOsuUpdate(UserStatusObject);
// Send data to user's queue
if (sendImmidiate) currentUser.addActionToQueue(osuPacketWriter.toBuffer);
else return osuPacketWriter.toBuffer;
}

View file

@ -1,28 +0,0 @@
const osu = require("osu-packet"),
consoleHelper = require("./consoleHelper.js"),
Streams = require("../Streams.js");
module.exports = function(CurrentUser, MatchID) {
const match = global.MultiplayerManager.getMatch(MatchID);
if (match != null) {
match.isTourneyMatch = true;
for (let user of global.users.getIterableItems()) {
if (user.id == CurrentUser.id) {
match.tourneyClientUsers.push(user);
}
}
if (Streams.isUserInStream(match.matchChatStreamName, CurrentUser.uuid))
return consoleHelper.printBancho(`Did not add user to channel ${match.matchChatStreamName} because they are already in it`);
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.ChannelJoinSuccess("#multiplayer");
if (!Streams.isUserInStream(match.matchChatStreamName, CurrentUser.uuid))
Streams.addUserToStream(match.matchChatStreamName, CurrentUser.uuid);
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
}
}

View file

@ -1,32 +0,0 @@
const osu = require("osu-packet"),
consoleHelper = require("../../consoleHelper.js"),
Streams = require("../Streams.js");
module.exports = function(CurrentUser, MatchID) {
const match = global.MultiplayerManager.getMatch(MatchID);
if (match != null) {
match.isTourneyMatch = false;
match.tourneyClientUsers = [];
if (Streams.isUserInStream(match.matchChatStreamName, CurrentUser.uuid))
return consoleHelper.printBancho(`Did not add user to channel ${match.matchChatStreamName} because they are already in it`);
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.ChannelRevoked("#multiplayer");
if (!Streams.isUserInStream(match.matchChatStreamName, CurrentUser.uuid))
Streams.removeUserFromStream(match.matchChatStreamName, CurrentUser.uuid);
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
} else {
// Still provide feedback just in case
// TODO: Check if this has any effect, if not then remove this.
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.ChannelRevoked("#multiplayer");
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
}
}

View file

@ -1,22 +0,0 @@
const osu = require("osu-packet"),
UserPresence = require("./UserPresence.js"),
StatusUpdate = require("./StatusUpdate.js");
module.exports = function(CurrentUser, MatchID) {
const matchData = global.MultiplayerManager.getMatch(MatchID);
if (matchData != null) {
const osuPacketWriter = new osu.Bancho.Writer();
osuPacketWriter.MatchUpdate(matchData.createOsuMatchJSON());
// Queue info on all the users in the match to the client
for (let slot in matchData.slots) {
CurrentUser.addActionToQueue(UserPresence(CurrentUser, slot.playerId, false));
CurrentUser.addActionToQueue(StatusUpdate(CurrentUser, slot.playerId, false));
}
// Queue data
CurrentUser.addActionToQueue(osuPacketWriter.toBuffer);
}
}

View file

@ -1,24 +0,0 @@
const osu = require("osu-packet"),
getUserById = require("../util/getUserById.js");
module.exports = function(currentUser, id = 0, sendImmidiate = true) {
const osuPacketWriter = new osu.Bancho.Writer;
const User = getUserById(id);
if (User == null) return;
osuPacketWriter.UserPresence({
userId: id,
username: User.username,
timezone: 0,
countryId: User.countryID,
permissions: 4,
longitude: User.location[1],
latitude: User.location[0],
rank: User.rank
});
if (sendImmidiate) currentUser.addActionToQueue(osuPacketWriter.toBuffer);
else return osuPacketWriter.toBuffer;
}

View file

@ -1,16 +0,0 @@
const osu = require("osu-packet");
module.exports = function(currentUser, sendImmidiate = true) {
const osuPacketWriter = new osu.Bancho.Writer;
let userIds = [];
for (let user of global.users.getIterableItems()) {
userIds.push(user.id);
}
osuPacketWriter.UserPresenceBundle(userIds);
if (sendImmidiate) currentUser.addActionToQueue(osuPacketWriter.toBuffer);
else return osuPacketWriter.toBuffer;
}

View file

@ -1,14 +0,0 @@
const UserPresenceBundle = require("./UserPresenceBundle.js"),
UserPresence = require("./UserPresence.js"),
StatusUpdate = require("./StatusUpdate.js");
module.exports = function (currentUser, data = [0]) {
UserPresenceBundle(currentUser);
for (let i1 = 0; i1 < data.length; i1++) {
const CurrentUserID = data[i1];
UserPresence(currentUser, CurrentUserID);
StatusUpdate(currentUser, CurrentUserID);
}
}

View file

@ -1,76 +0,0 @@
const osu = require("osu-packet"),
getUserById = require("./util/getUserById.js"),
Streams = require("./Streams.js");
module.exports = {
startSpectatingUser:function(currentUser, spectatedId) {
// Get the user this user is trying to spectate
const User = getUserById(spectatedId);
if (Streams.exists(`sp_${User.id}`)) {
// Just add user to stream since it already exists
Streams.addUserToStream(`sp_${User.id}`, currentUser.uuid);
} else {
// Stream doesn't exist, create it and add the spectator
Streams.addStream(`sp_${User.id}`, true, spectatedId);
Streams.addUserToStream(`sp_${User.id}`, currentUser.uuid);
}
// We want to do this stuff regardless
// Create a new osu packet writer
let osuPacketWriter = new osu.Bancho.Writer;
// Set the user requesting to be spectating this user
currentUser.spectating = spectatedId;
// Tell the client of the user being spectated that they are being spectated
osuPacketWriter.SpectatorJoined(currentUser.id);
// Send the packet to the spectated user's queue
User.addActionToQueue(osuPacketWriter.toBuffer);
// Make a new clear osu packet writer
osuPacketWriter = new osu.Bancho.Writer;
// Tell everyone spectating this user that another user has started spectating
osuPacketWriter.FellowSpectatorJoined(currentUser.id);
// Send this packet to all the spectators
Streams.sendToStream(`sp_${User.id}`, osuPacketWriter.toBuffer);
},
sendSpectatorFrames(currentUser, data) {
// Create new osu packet writer
const osuPacketWriter = new osu.Bancho.Writer;
// Data containing the user's actions
osuPacketWriter.SpectateFrames(data);
// Send the frames to all the spectators
Streams.sendToStream(`sp_${currentUser.id}`, osuPacketWriter.toBuffer, null);
},
stopSpectatingUser(currentUser) {
// Get the user this user is spectating
const spectatedUser = getUserById(currentUser.spectating);
// Create new osu packet writer
let osuPacketWriter = new osu.Bancho.Writer;
// Inform the client being spectated that this user has stopped spectating
osuPacketWriter.SpectatorLeft(currentUser.id);
// Add this packet to the spectated user's queue
spectatedUser.addActionToQueue(osuPacketWriter.toBuffer);
// Remove this user from the spectator stream
Streams.removeUserFromStream(`sp_${spectatedUser.id}`, currentUser.uuid);
// Make a new clear osu packet writer
osuPacketWriter = new osu.Bancho.Writer;
// Inform other users spectating that this spectator has left
osuPacketWriter.FellowSpectatorLeft(currentUser.id);
// Send this packet to all spectators
Streams.sendToStream(`sp_${spectatedUser.id}`, osuPacketWriter.toBuffer);
}
}

View file

@ -1,129 +0,0 @@
const getUserByToken = require("./util/getUserByToken.js"),
consoleHelper = require("../consoleHelper.js");
module.exports = class {
static init() {
global.avaliableStreams = {};
global.avaliableStreamKeys = [];
}
static addStream(streamName = "", removeIfEmpty = false, spectatorHostId = null) {
// Make sure a stream with the same name doesn't exist already
if (global.avaliableStreamKeys.includes(streamName))
return consoleHelper.printBancho(`Did not add stream [${streamName}] A stream with the same name already exists`);
// Add new stream to the list of streams
global.avaliableStreams[streamName] = {
streamUsers: [], // An array containing a list of user tokens of the users in a given stream
streamSpectatorHost: spectatorHostId, // null unless stream is for spectating
removeIfEmpty: removeIfEmpty
}
global.avaliableStreamKeys = Object.keys(global.avaliableStreams);
consoleHelper.printBancho(`Added stream [${streamName}]`);
}
static removeStream(streamName) {
try {
delete global.avaliableStreams[streamName];
global.avaliableStreamKeys = Object.keys(global.avaliableStreams);
} catch (e) {
consoleHelper.printError(`Was not able to remove stream [${streamName}]`);
console.error(e);
}
}
static addUserToStream(streamName, userToken) {
// Make sure the stream we are attempting to add this user to even exists
if (!this.exists(streamName))
return 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 (global.avaliableStreams[streamName].streamUsers.includes(userToken))
return 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 (userToken == "" || userToken == null)
return consoleHelper.printBancho(`Did not add user to stream [${streamName}] because their token is invalid!`);
// Add user's token to the stream's user list
global.avaliableStreams[streamName].streamUsers.push(userToken);
consoleHelper.printBancho(`Added user [${userToken}] to stream ${streamName}`);
}
static removeUserFromStream(streamName, userToken) {
// Make sure the stream we are attempting to add this user to even exists
if (!this.exists(streamName))
return consoleHelper.printBancho(`Did not remove user from stream [${streamName}] because it does not exist!`);
const stream = global.avaliableStreams[streamName];
// Make sure the user isn't already in the stream
if (!stream.streamUsers.includes(userToken))
return 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 (userToken == "" || userToken == null)
return consoleHelper.printBancho(`Did not remove user from stream [${streamName}] because their userId is invalid!`);
try {
// Find index of user to remove
let userCurrentIndex;
for (let i = 0; i < stream.streamUsers.length; i++) {
if (userToken == stream.streamUsers[i]) {
userCurrentIndex = i;
break;
}
}
// Remove user from stream's user list
stream.streamUsers.splice(userCurrentIndex, 1);
consoleHelper.printBancho(`Removed user [${userToken}] from stream ${streamName}`);
} catch (e) {
consoleHelper.printBancho(`Can't Remove user [${userToken}] from stream ${streamName}`);
console.error(e);
}
if (stream.removeIfEmpty && stream.streamUsers.length == 0) {
this.removeStream(streamName);
consoleHelper.printBancho(`Removed stream [${streamName}] There were no users in stream`);
}
}
static sendToStream(streamName, streamData, initUser = null) {
// Make sure the stream we are attempting to send to even exists
if (!this.exists(streamName))
return consoleHelper.printBancho(`Did not send to stream [${streamName}] because it does not exist!`);
// Get the stream to send the data to
const currentStream = global.avaliableStreams[streamName];
// Loop through the users in this stream
for (let i = 0; i < currentStream.streamUsers.length; i++) {
// Get the user token of the user in the queue
const currentUserToken = currentStream.streamUsers[i];
// Make sure we don't send this data back to the user requesting this data to be sent
if (initUser != null && currentUserToken == initUser && (streamName[0] == "#" || streamName.includes("mp_"))) continue;
if (currentUserToken == 3) continue; // Skip if user is bot
// Get user object
const currentUser = getUserByToken(currentUserToken);
// Skip if user is nonexistant
if (currentUser == null) continue;
// Send stream data to user's own queue
currentUser.addActionToQueue(streamData);
}
}
static exists(streamName) {
return global.avaliableStreamKeys.includes(streamName);
}
static getStreams() {
// Return the names of all avaliable streams
return global.avaliableStreamKeys;
}
static isUserInStream(streamName, userToken) {
if (global.avaliableStreams[streamName].streamUsers.includes(userToken)) return true;
else return false;
}
}

View file

@ -1,125 +0,0 @@
const StatusUpdate = require("./Packets/StatusUpdate.js");
const rankingModes = [
"pp_raw",
"ranked_score",
"avg_accuracy"
];
module.exports = class {
constructor(id, username, uuid) {
this.id = id;
this.username = username;
this.uuid = uuid;
this.connectTime = Date.now();
this.timeoutTime = Date.now() + 30000;
this.queue = Buffer.alloc(0);
// Binato specific
this.rankingMode = 0;
this.playMode = 0;
this.countryID = 0;
this.spectators = [];
this.spectating = 0;
this.location = [0,0];
this.joinedChannels = [];
// Presence data
this.actionID = 0;
this.actionText = "";
this.actionMods = 0;
this.beatmapChecksum = "";
this.beatmapID = 0;
this.currentMods = 0;
// Cached db data
this.rankedScore = 0;
this.accuracy = 0;
this.playCount = 0;
this.totalScore = 0;
this.rank = 0;
this.pp = 0;
// Multiplayer data
this.currentMatch = null;
this.matchSlotId = -1;
this.inMatch = false;
this.isTourneyUser = false;
}
// Adds new actions to the user's queue
addActionToQueue(newData) {
this.queue = Buffer.concat([this.queue, newData], this.queue.length + newData.length);
}
// Updates the user's current action
updatePresence(action) {
this.actionID = action.status;
this.actionText = action.statusText;
this.beatmapChecksum = action.beatmapChecksum;
this.currentMods = action.currentMods;
this.actionMods = action.currentMods;
if (action.playMode != this.playMode) {
this.updateUserInfo(true);
this.playMode = action.playMode;
}
this.beatmapID = action.beatmapId;
}
// Gets the user's score information from the database and caches it
async updateUserInfo(forceUpdate = false) {
const userScoreDB = await global.DatabaseHelper.query("SELECT * FROM users_modes_info WHERE user_id = ? AND mode_id = ? LIMIT 1", [this.id, this.playMode]);
const mappedRankingMode = rankingModes[this.rankingMode];
const userRankDB = await global.DatabaseHelper.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]);
if (userScoreDB == null || userRankDB == null) throw "fuck";
// Handle "if we should update" checks for each rankingMode
let userScoreUpdate = false;
switch (this.rankingMode) {
case 0:
if (this.pp != userScoreDB.pp_raw)
userScoreUpdate = true;
break;
case 1:
if (this.rankedScore != userScoreDB.ranked_score)
userScoreUpdate = true;
break;
case 2:
if (this.accuracy != userScoreDB.avg_accuracy)
userScoreUpdate = true;
break;
}
this.rankedScore = userScoreDB.ranked_score;
this.totalScore = userScoreDB.total_score;
this.accuracy = userScoreDB.avg_accuracy;
this.playCount = userScoreDB.playcount;
// Fetch rank
for (let i = 0; i < userRankDB.length; i++) {
if (userRankDB[i]["user_id"] == this.id) {
this.rank = i + 1;
break;
}
}
// Set PP to none if ranking mode is not PP
if (this.rankingMode == 0) this.pp = userScoreDB.pp_raw;
else this.pp = 0;
if (userScoreUpdate || forceUpdate) {
StatusUpdate(this, this.id);
}
}
// Clears out the user's queue
clearQueue() {
this.queue = Buffer.alloc(0);
}
}

View file

@ -1,9 +0,0 @@
module.exports = function(s) {
switch (s) {
case "reconnect":
return "\u0005\u0000\u0000\u0004\u0000\u0000\u0000<30><30><EFBFBD><EFBFBD>\u0018\u0000\u0000\u0011\u0000\u0000\u0000\u000b\u000fReconnecting...";
default:
return Buffer.alloc(0);
}
}

View file

@ -1,274 +0,0 @@
const countryCodes = {
"LV": 132,
"AD": 3,
"LT": 130,
"KM": 116,
"QA": 182,
"VA": 0,
"PK": 173,
"KI": 115,
"SS": 0,
"KH": 114,
"NZ": 166,
"TO": 215,
"KZ": 122,
"GA": 76,
"BW": 35,
"AX": 247,
"GE": 79,
"UA": 222,
"CR": 50,
"AE": 0,
"NE": 157,
"ZA": 240,
"SK": 196,
"BV": 34,
"SH": 0,
"PT": 179,
"SC": 189,
"CO": 49,
"GP": 86,
"GY": 93,
"CM": 47,
"TJ": 211,
"AF": 5,
"IE": 101,
"AL": 8,
"BG": 24,
"JO": 110,
"MU": 149,
"PM": 0,
"LA": 0,
"IO": 104,
"KY": 121,
"SA": 187,
"KN": 0,
"OM": 167,
"CY": 54,
"BQ": 0,
"BT": 33,
"WS": 236,
"ES": 67,
"LR": 128,
"RW": 186,
"AQ": 12,
"PW": 180,
"JE": 250,
"TN": 214,
"ZW": 243,
"JP": 111,
"BB": 20,
"VN": 233,
"HN": 96,
"KP": 0,
"WF": 235,
"EC": 62,
"HU": 99,
"GF": 80,
"GQ": 87,
"TW": 220,
"MC": 135,
"BE": 22,
"PN": 176,
"SZ": 205,
"CZ": 55,
"LY": 0,
"IN": 103,
"FM": 0,
"PY": 181,
"PH": 172,
"MN": 142,
"GG": 248,
"CC": 39,
"ME": 242,
"DO": 60,
"KR": 0,
"PL": 174,
"MT": 148,
"MM": 141,
"AW": 17,
"MV": 150,
"BD": 21,
"NR": 164,
"AT": 15,
"GW": 92,
"FR": 74,
"LI": 126,
"CF": 41,
"DZ": 61,
"MA": 134,
"VG": 0,
"NC": 156,
"IQ": 105,
"BN": 0,
"BF": 23,
"BO": 30,
"GB": 77,
"CU": 51,
"LU": 131,
"YT": 238,
"NO": 162,
"SM": 198,
"GL": 83,
"IS": 107,
"AO": 11,
"MH": 138,
"SE": 191,
"ZM": 241,
"FJ": 70,
"SL": 197,
"CH": 43,
"RU": 0,
"CW": 0,
"CX": 53,
"TF": 208,
"NL": 161,
"AU": 16,
"FI": 69,
"MS": 147,
"GH": 81,
"BY": 36,
"IL": 102,
"VC": 0,
"NG": 159,
"HT": 98,
"LS": 129,
"MR": 146,
"YE": 237,
"MP": 144,
"SX": 0,
"RE": 183,
"RO": 184,
"NP": 163,
"CG": 0,
"FO": 73,
"CI": 0,
"TH": 210,
"HK": 94,
"TK": 212,
"XK": 0,
"DM": 59,
"LC": 0,
"ID": 100,
"MG": 137,
"JM": 109,
"IT": 108,
"CA": 38,
"TZ": 221,
"GI": 82,
"KG": 113,
"NU": 165,
"TV": 219,
"LB": 124,
"SY": 0,
"PR": 177,
"NI": 160,
"KE": 112,
"MO": 0,
"SR": 201,
"VI": 0,
"SV": 203,
"HM": 0,
"CD": 0,
"BI": 26,
"BM": 28,
"MW": 151,
"TM": 213,
"GT": 90,
"AG": 0,
"UM": 0,
"US": 225,
"AR": 13,
"DJ": 57,
"KW": 120,
"MY": 153,
"FK": 71,
"EG": 64,
"BA": 0,
"CN": 48,
"GN": 85,
"PS": 178,
"SO": 200,
"IM": 249,
"GS": 0,
"BR": 31,
"GM": 84,
"PF": 170,
"PA": 168,
"PG": 171,
"BH": 25,
"TG": 209,
"GU": 91,
"CK": 45,
"MF": 252,
"VE": 230,
"CL": 46,
"TR": 217,
"UG": 223,
"GD": 78,
"TT": 218,
"TL": 0,
"MD": 0,
"MK": 0,
"ST": 202,
"CV": 52,
"MQ": 145,
"GR": 88,
"HR": 97,
"BZ": 37,
"UZ": 227,
"DK": 58,
"SN": 199,
"ET": 68,
"VU": 234,
"ER": 66,
"BJ": 27,
"LK": 127,
"NA": 155,
"AS": 14,
"SG": 192,
"PE": 169,
"IR": 0,
"MX": 152,
"TD": 207,
"AZ": 18,
"AM": 9,
"BL": 0,
"SJ": 195,
"SB": 188,
"NF": 158,
"RS": 239,
"DE": 56,
"EH": 65,
"EE": 63,
"SD": 190,
"ML": 140,
"TC": 206,
"MZ": 154,
"BS": 32,
"UY": 226,
"SI": 194,
"AI": 7
}
const countryCodeKeys = Object.keys(countryCodes);
module.exports = {
getCountryID:function(code = "") {
// Get id of a country from a 2 char code
code = code.toUpperCase();
if (countryCodes[code] != null) return countryCodes[code];
else return 0;
},
getCountryLetters:function(code) {
// Get country char code from id
for (var i = 0; i < countryCodes.length; i++) {
const countryId = countryCodes[countryCodeKeys[i]];
if (countryId === code) return countryId;
}
return "XX";
}
}
module.exports.countryCodes = countryCodes;

113
server/enums/Packets.ts Normal file
View file

@ -0,0 +1,113 @@
export enum Packets {
Client_ChangeAction,
Client_SendPublicMessage,
Client_Logout,
Client_RequestStatusUpdate,
Client_Pong,
Server_LoginReply,
Server_CommandError,
Server_SendMessage,
Server_Ping,
Server_IRCUsernameChange,
Server_IRCQuit,
Server_UserStats,
Server_UserLogout,
Server_SpectatorJoined,
Server_SpectatorLeft,
Server_SpectateFrames,
Client_StartSpectating,
Client_StopSpectating,
Client_SpectateFrames,
Server_VersionUpdate,
Client_ErrorReport,
Client_CantSpectate,
Server_SpectatorCantSpectate,
Server_GetAttention,
Server_Notification,
Client_SendPrivateMessage,
Server_UpdateMatch,
Server_NewMatch,
Server_DisposeMatch,
Client_PartLobby,
Client_JoinLobby,
Client_CreateMatch,
Client_JoinMatch,
Client_PartMatch,
Server_LobbyJoin_OLD,
Server_LobbyPart_OLD,
Server_MatchJoinSuccess,
Server_MatchJoinFail,
Client_MatchChangeSlot,
Client_MatchReady,
Client_MatchLock,
Client_MatchChangeSettings,
Server_FellowSpectatorJoined,
Server_FellowSpectatorLeft,
Client_MatchStart,
AllPlayersLoaded,
Server_MatchStart,
Client_MatchScoreUpdate,
Server_MatchScoreUpdate,
Client_MatchComplete,
Server_MatchTransferHost,
Client_MatchChangeMods,
Client_MatchLoadComplete,
Server_MatchAllPlayersLoaded,
Client_MatchNoBeatmap,
Client_MatchNotReady,
Client_MatchFailed,
Server_MatchComplete,
Client_MatchHasBeatmap,
Client_MatchSkipRequest,
Server_MatchSkip,
Server_Unauthorised,
Client_ChannelJoin,
Server_ChannelJoinSuccess,
Server_ChannelInfo,
Server_ChannelKicked,
Server_ChannelAvailableAutojoin,
Client_BeatmapInfoRequest,
Server_BeatmapInfoReply,
Client_MatchTransferHost,
Server_SupporterGMT,
Server_FriendsList,
Client_FriendAdd,
Client_FriendRemove,
Server_ProtocolNegotiation,
Server_MainMenuIcon,
Client_MatchChangeTeam,
Client_ChannelPart,
Client_ReceiveUpdates,
Server_Monitor,
Server_MatchPlayerSkipped,
Client_SetAwayMessage,
Server_UserPanel,
IRC_only,
Client_UserStatsRequest,
Server_Restart,
Client_Invite,
Server_Invite,
Server_ChannelInfoEnd,
Client_MatchChangePassword,
Server_MatchChangePassword,
Server_SilenceEnd,
Client_SpecialMatchInfoRequest,
Server_UserSilenced,
Server_UserPresenceSingle,
Server_UserPresenceBundle,
Client_UserPresenceRequest,
Client_UserPresenceRequestAll,
Client_UserToggleBlockNonFriendPM,
Server_UserPMBlocked,
Server_TargetIsSilenced,
Server_VersionUpdateForced,
Server_SwitchServer,
Server_AccountRestricted,
Server_RTX,
Client_MatchAbort,
Server_SwitchTourneyServer,
// NOTE: Tournament client only
Client_SpecialJoinMatchChannel,
// NOTE: Tournament client only
Client_SpecialLeaveMatchChanne,
}

View file

@ -1,153 +0,0 @@
const osu = require("osu-packet"),
User = require("./User.js"),
uuid = require("./util/shortUUID.js"),
ahttp = require("./util/AsyncHttpRequest.js"),
consoleHelper = require("../consoleHelper.js"),
// Packets
getUserByUsername = require("./util/getUserByUsername.js"),
getUserByToken = require("./util/getUserByToken.js"),
countryHelper = require("./countryHelper.js"),
loginHelper = require("./loginHelper.js"),
Logout = require("./Packets/Logout.js"),
Streams = require("./Streams.js"),
UserPresenceBundle = require("./Packets/UserPresenceBundle.js"),
UserPresence = require("./Packets/UserPresence.js"),
StatusUpdate = require("./Packets/StatusUpdate.js");
module.exports = async function(req, res, loginInfo) {
// Get time at the start of login
const loginStartTime = Date.now(),
isTourneyClient = loginInfo.osuversion.includes("tourney");
// Check login
const loginCheck = await loginHelper.checkLogin(loginInfo);
if (loginCheck != null) {
res.removeHeader('X-Powered-By');
res.removeHeader('Date');
res.writeHead(200, loginCheck[1]);
return res.end(loginCheck[0]);
}
// Get users IP for getting location
// Get cloudflare requestee IP first
let requestIP = req.get("cf-connecting-ip");
// Get IP of requestee since we are probably behind a reverse proxy
if (requestIP == null)
requestIP = req.get("X-Real-IP");
// Just get the requestee IP (we are not behind a reverse proxy)
if (requestIP == null)
requestIP = req.remote_addr;
// Make sure requestIP is never null
if (requestIP == null)
requestIP = "";
let userLocationData = [], userLocation;
// Check if it is a local or null IP
if (requestIP.includes("192.168.") || requestIP.includes("127.0.") || requestIP == "") {
// Set location to null island
userLocationData.country = "XX";
userLocation = [0, 0];
} else {
// Get user's location using zxq
userLocationData = await ahttp(`http://ip.zxq.co/${requestIP}`, "json");
userLocation = userLocationData.loc.split(",");
}
// Get information about the user from the database
const userDB = await global.DatabaseHelper.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
// Create a token for the client
const newClientToken = uuid();
// Make sure user is not already connected, kick off if so.
const connectedUser = getUserByUsername(loginInfo.username);
if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) {
Logout(connectedUser);
}
// Retreive the newly created user
const NewUser = global.users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken));
// Set tourney client flag
NewUser.isTourneyUser = isTourneyClient;
// Get user's data from the database
NewUser.updateUserInfo();
try {
// Save the user's location to their class for later use
NewUser.location[0] = parseFloat(userLocation[0]);
NewUser.location[1] = parseFloat(userLocation[1]);
// Save the country id for the same reason as above
NewUser.countryID = countryHelper.getCountryID(userLocationData.country);
// We're ready to start putting together a login packet
// Create an osu! Packet writer
let osuPacketWriter = new osu.Bancho.Writer;
// The reply id is the user's id in any other case than an error in which case negative numbers are used
osuPacketWriter.LoginReply(NewUser.id);
// Current bancho protocol version. Defined in Binato.js
osuPacketWriter.ProtocolNegotiation(global.protocolVersion);
// Permission level 4 is osu!supporter
osuPacketWriter.LoginPermissions(4);
// After sending the user their friends list send them the online users
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);
// peppy pls, why
osuPacketWriter.ChannelListingComplete();
// Add user to #osu
osuPacketWriter.ChannelJoinSuccess("#osu");
if (!Streams.isUserInStream("#osu", NewUser.uuid))
Streams.addUserToStream("#osu", NewUser.uuid);
// List all channels out to the client
for (let i = 0; i < global.channels.length; i++) {
osuPacketWriter.ChannelAvailable({
channelName: global.channels[i].channelName,
channelTopic: global.channels[i].channelTopic,
channelUserCount: global.channels[i].channelUserCount
});
}
// Construct user's friends list
const userFriends = await global.DatabaseHelper.query("SELECT friendsWith FROM friends WHERE user = ?", [NewUser.id]);
let friendsArray = [];
for (let i = 0; i < userFriends.length; i++) {
friendsArray.push(userFriends[i].friendsWith);
}
// Send user's friends list
osuPacketWriter.FriendsList(friendsArray);
osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`);
global.DatabaseHelper.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [global.users.getLength() - 1]);
res.removeHeader('X-Powered-By');
res.removeHeader('Date');
// Complete login
res.writeHead(200, {
"cho-token": NewUser.uuid,
"Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100",
});
res.end(osuPacketWriter.toBuffer, () => {
consoleHelper.printBancho(`User login finished, took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
} catch (err) {
console.error(err);
}
}

View file

@ -1,74 +0,0 @@
const osu = require("osu-packet"),
aes256 = require("aes256"),
crypto = require("crypto"),
config = require("../config.json");
module.exports = {
checkLogin: function(loginInfo) {
return new Promise(async (resolve, reject) => {
// Check if there is any login information provided
if (loginInfo == null) return resolve(incorrectLoginResponse());
const userDBData = await global.DatabaseHelper.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
// Make sure a user was found in the database
if (userDBData == null) return resolve(incorrectLoginResponse());
// Make sure the username is the same as the login info
if (userDBData.username !== loginInfo.username) return resolve(incorrectLoginResponse());
/*
1: Old MD5 password
2: Old AES password
*/
if (userDBData.has_old_password === 1) {
if (userDBData.password_hash !== loginInfo.password)
return resolve(incorrectLoginResponse());
return resolve(requiredPWChangeResponse());
} else if (userDBData.has_old_password === 2) {
if (aes256.decrypt(config.database.key, userDBData.password_hash) !== loginInfo.password)
return resolve(resolve(incorrectLoginResponse()));
return resolve(requiredPWChangeResponse());
} else {
crypto.pbkdf2(loginInfo.password, userDBData.password_salt, config.database.pbkdf2.itterations, config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
if (err) {
return reject(err);
} else {
if (derivedKey.toString("hex") !== userDBData.password_hash)
return resolve(incorrectLoginResponse());
return resolve(null); // We good
}
});
}
});
},
incorrectLoginResponse: incorrectLoginResponse
}
function incorrectLoginResponse() {
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': global.protocolVersion,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}
function requiredPWChangeResponse() {
const osuPacketWriter = new osu.Bancho.Writer;
osuPacketWriter.Announce("As part of migration to a new password system you are required to change your password. Please log in on the website and change your password.");
osuPacketWriter.LoginReply(-1);
return [
osuPacketWriter.toBuffer,
{
'cho-protocol': global.protocolVersion,
'Connection': 'keep-alive',
'Keep-Alive': 'timeout=5, max=100',
}
];
}

View file

0
server/objects/User.ts Normal file
View file

View file

@ -1,111 +0,0 @@
module.exports = {
"client_changeAction":0,
"client_sendPublicMessage":1,
"client_logout":2,
"client_requestStatusUpdate":3,
"client_pong":4,
"server_userID":5,
"server_commandError":6,
"server_sendMessage":7,
"server_ping":8,
"server_handleIRCUsernameChange":9,
"server_handleIRCQuit":10,
"server_userStats":11,
"server_userLogout":12,
"server_spectatorJoined":13,
"server_spectatorLeft":14,
"server_spectateFrames":15,
"client_startSpectating":16,
"client_stopSpectating":17,
"client_spectateFrames":18,
"server_versionUpdate":19,
"client_errorReport":20,
"client_cantSpectate":21,
"server_spectatorCantSpectate":22,
"server_getAttention":23,
"server_notification":24,
"client_sendPrivateMessage":25,
"server_updateMatch":26,
"server_newMatch":27,
"server_disposeMatch":28,
"client_partLobby":29,
"client_joinLobby":30,
"client_createMatch":31,
"client_joinMatch":32,
"client_partMatch":33,
"server_lobbyJoin_obsolete":34,
"server_lobbyPart_obsolete":35,
"server_matchJoinSuccess":36,
"server_matchJoinFail":37,
"client_matchChangeSlot":38,
"client_matchReady":39,
"client_matchLock":40,
"client_matchChangeSettings":41,
"server_fellowSpectatorJoined":42,
"server_fellowSpectatorLeft":43,
"client_matchStart":44,
"AllPlayersLoaded":45,
"server_matchStart":46,
"client_matchScoreUpdate":47,
"server_matchScoreUpdate":48,
"client_matchComplete":49,
"server_matchTransferHost":50,
"client_matchChangeMods":51,
"client_matchLoadComplete":52,
"server_matchAllPlayersLoaded":53,
"client_matchNoBeatmap":54,
"client_matchNotReady":55,
"client_matchFailed":56,
"server_matchComplete":58,
"client_matchHasBeatmap":59,
"client_matchSkipRequest":60,
"server_matchSkip":61,
"server_unauthorised":62,
"client_channelJoin":63,
"server_channelJoinSuccess":64,
"server_channelInfo":65,
"server_channelKicked":66,
"server_channelAvailableAutojoin":67,
"client_beatmapInfoRequest":68,
"server_beatmapInfoReply":69,
"client_matchTransferHost":70,
"server_supporterGMT":71,
"server_friendsList":72,
"client_friendAdd":73,
"client_friendRemove":74,
"server_protocolVersion":75,
"server_mainMenuIcon":76,
"client_matchChangeTeam":77,
"client_channelPart":78,
"client_receiveUpdates":79,
"server_topBotnet":80,
"server_matchPlayerSkipped":81,
"client_setAwayMessage":82,
"server_userPanel":83,
"IRC_only":84,
"client_userStatsRequest":85,
"server_restart":86,
"client_invite":87,
"server_invite":88,
"server_channelInfoEnd":89,
"client_matchChangePassword":90,
"server_matchChangePassword":91,
"server_silenceEnd":92,
"client_specialMatchInfoRequest":93,
"server_userSilenced":94,
"server_userPresenceSingle":95,
"server_userPresenceBundle":96,
"client_userPresenceRequest":97,
"client_userPresenceRequestAll":98,
"client_userToggleBlockNonFriendPM":99,
"server_userPMBlocked":100,
"server_targetIsSilenced":101,
"server_versionUpdateForced":102,
"server_switchServer":103,
"server_accountRestricted":104,
"server_jumpscare":105,
"client_matchAbort":106,
"server_switchTourneyServer":107,
"client_specialJoinMatchChannel":108,
"client_specialLeaveMatchChannel":109
}

View file

@ -1,111 +0,0 @@
{
"client_changeAction":0,
"client_sendPublicMessage":1,
"client_logout":2,
"client_requestStatusUpdate":3,
"client_pong":4,
"server_userID":5,
"server_commandError":6,
"server_sendMessage":7,
"server_ping":8,
"server_handleIRCUsernameChange":9,
"server_handleIRCQuit":10,
"server_userStats":11,
"server_userLogout":12,
"server_spectatorJoined":13,
"server_spectatorLeft":14,
"server_spectateFrames":15,
"client_startSpectating":16,
"client_stopSpectating":17,
"client_spectateFrames":18,
"server_versionUpdate":19,
"client_errorReport":20,
"client_cantSpectate":21,
"server_spectatorCantSpectate":22,
"server_getAttention":23,
"server_notification":24,
"client_sendPrivateMessage":25,
"server_updateMatch":26,
"server_newMatch":27,
"server_disposeMatch":28,
"client_partLobby":29,
"client_joinLobby":30,
"client_createMatch":31,
"client_joinMatch":32,
"client_partMatch":33,
"server_lobbyJoin_obsolete":34,
"server_lobbyPart_obsolete":35,
"server_matchJoinSuccess":36,
"server_matchJoinFail":37,
"client_matchChangeSlot":38,
"client_matchReady":39,
"client_matchLock":40,
"client_matchChangeSettings":41,
"server_fellowSpectatorJoined":42,
"server_fellowSpectatorLeft":43,
"client_matchStart":44,
"AllPlayersLoaded":45,
"server_matchStart":46,
"client_matchScoreUpdate":47,
"server_matchScoreUpdate":48,
"client_matchComplete":49,
"server_matchTransferHost":50,
"client_matchChangeMods":51,
"client_matchLoadComplete":52,
"server_matchAllPlayersLoaded":53,
"client_matchNoBeatmap":54,
"client_matchNotReady":55,
"client_matchFailed":56,
"server_matchComplete":58,
"client_matchHasBeatmap":59,
"client_matchSkipRequest":60,
"server_matchSkip":61,
"server_unauthorised":62,
"client_channelJoin":63,
"server_channelJoinSuccess":64,
"server_channelInfo":65,
"server_channelKicked":66,
"server_channelAvailableAutojoin":67,
"client_beatmapInfoRequest":68,
"server_beatmapInfoReply":69,
"client_matchTransferHost":70,
"server_supporterGMT":71,
"server_friendsList":72,
"client_friendAdd":73,
"client_friendRemove":74,
"server_protocolVersion":75,
"server_mainMenuIcon":76,
"client_matchChangeTeam":77,
"client_channelPart":78,
"client_receiveUpdates":79,
"server_topBotnet":80,
"server_matchPlayerSkipped":81,
"client_setAwayMessage":82,
"server_userPanel":83,
"IRC_only":84,
"client_userStatsRequest":85,
"server_restart":86,
"client_invite":87,
"server_invite":88,
"server_channelInfoEnd":89,
"client_matchChangePassword":90,
"server_matchChangePassword":91,
"server_silenceEnd":92,
"client_specialMatchInfoRequest":93,
"server_userSilenced":94,
"server_userPresenceSingle":95,
"server_userPresenceBundle":96,
"client_userPresenceRequest":97,
"client_userPresenceRequestAll":98,
"client_userToggleBlockNonFriendPM":99,
"server_userPMBlocked":100,
"server_targetIsSilenced":101,
"server_versionUpdateForced":102,
"server_switchServer":103,
"server_accountRestricted":104,
"server_jumpscare":105,
"client_matchAbort":106,
"server_switchTourneyServer":107,
"client_specialJoinMatchChannel":108,
"client_specialLeaveMatchChannel":109
}

View file

@ -1,16 +0,0 @@
const fetch = require("node-fetch");
const functionMap = {
"text": async (res) => await res.text(),
"json": async (res) => await res.json()
};
module.exports = async function(url, reqType = "text") {
return new Promise(async (resolve, reject) => {
try {
resolve(functionMap[reqType](await fetch(url)));
} catch (e) {
reject(e);
}
});
}

View file

@ -1,15 +0,0 @@
module.exports = {
map:function(input, inputMin, inputMax, outputMin, outputMax) {
const newv = (input - inputMin) / (inputMax - inputMin) * (outputMax - outputMin) + outputMin;
if (outputMin < outputMax) return this.constrain(newv, outputMin, outputMax);
else return this.constrain(newv, outputMax, outputMin);
},
constrain:function(input, low, high) {
return Math.max(Math.min(input, high), low);
},
randInt:function(from, to) {
return Math.round(this.map(Math.random(), 0, 1, from, to));
}
}

View file

@ -1,5 +0,0 @@
{
"Text": 0,
"JSON": 1,
"XML": 2
}

View file

@ -1,75 +0,0 @@
class FunkyArray {
constructor() {
this.items = {};
this.itemKeys = Object.keys(this.items);
this.iterableArray = [];
}
add(uuid, item, regenerate = true) {
this.items[uuid] = item;
if (regenerate) {
this.itemKeys = Object.keys(this.items);
this.regenerateIterableArray();
}
return this.items[uuid];
}
remove(uuid, regenerate = true) {
delete this.items[uuid];
if (regenerate) {
this.itemKeys = Object.keys(this.items);
this.regenerateIterableArray();
}
}
removeFirstItem(regenerate = true) {
delete this.items[this.itemKeys[0]];
this.itemKeys = Object.keys(this.items);
if (regenerate) this.regenerateIterableArray();
}
regenerateIterableArray() {
this.iterableArray = new Array();
for (let itemKey of this.itemKeys) {
this.iterableArray.push(this.items[itemKey]);
}
this.itemKeys = Object.keys(this.items);
}
getFirstItem() {
return this.items[this.itemKeys[0]];
}
getLength() {
return this.itemKeys.length;
}
getKeyById(id) {
return this.itemKeys[id];
}
getById(id) {
return this.items[this.itemKeys[id]];
}
getByKey(key) {
return this.items[key];
}
getKeys() {
return this.itemKeys;
}
getItems() {
return this.items;
}
getIterableItems() {
return this.iterableArray;
}
}
module.exports = FunkyArray;

View file

@ -1,6 +0,0 @@
module.exports = function(id) {
for (let user of global.users.getIterableItems()) {
if (user.id == id)
return user;
}
}

View file

@ -1,3 +0,0 @@
module.exports = function(token) {
return global.users.getByKey(token);
}

View file

@ -1,6 +0,0 @@
module.exports = function(username) {
for (let user of global.users.getIterableItems()) {
if (user.username === username)
return user;
}
}

View file

@ -1,33 +0,0 @@
module.exports = function(packet) {
try {
const p = packet.toString(); // Convert our buffer to a String
const s = p.split('\n'); // Split our Login Data to Username Password Osuversion|blabla|bla
const n = s[2].split('|'); // Split osuversion|blablabla|blablabla to a object.
const username = s[0]; // Username ofc
const password = s[1]; // Password ofc
const osuversion = n[0]; // OsuVersion ofc.
const TimeOffset = Number(n[1]); // Comeon, i dont realy have to tell you what this is.
const clientData = n[3].split(':')[2]; // Some system information. such as MacAdress or DiskID
// If some data is not set OR is invailed throw errors
if (username == undefined) throw 'UserName';
if (password == undefined) throw 'password';
if (osuversion == undefined) throw 'osuversion';
if (TimeOffset == undefined) throw 'offset';
if (clientData == undefined) throw 'clientData';
// Everything alright? return parsed data.
const obj = {
username: String(username),
password: String(password),
osuversion: String(osuversion),
timeoffset: Number(TimeOffset),
clientdata: String(clientData)
};
// Here is the return.
return obj;
} catch (ex) {
// Else return undefined, that the login request got broke.
return undefined;
}
}

View file

@ -1,5 +0,0 @@
const uuid = require("uuid").v4;
module.exports = function() {
return uuid().split("-").slice(0, 2).join("");
}

10
tsconfig.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"rootDir": "./",
"outDir": "./build",
"strict": true
}
}

View file

@ -1,39 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
padding: 0;
}
.container {
width: 482px;
height: 165px;
}
hidden {
visibility: hidden;
}
.line {
padding: 2px;
font-size: 8pt;
font-family: sans-serif;
}
.line0 {
background-color: #edebfa;
}
.line1 {
background-color: #e3e1fa;
}
</style>
</head>
<body>
<div class="container">
|content|
</div>
</body>
</html>

View file

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Binato</title>
</head>
<body>
<pre style="border-style:double;border-width:4px;width:376px;">
. o ..
o . o o.o
...oo
__[]__ <b>Binato</b>
__|_o_o_o\__ A custom osu!Bancho
\""""""""""/
\. .. . / <a href="https://binato.eusv.ml">Website</a> | <a href="https://github.com/tgpethan/Binato">Github</a>
^^^^^^^^^^^^^^^^^^^^
</pre>
</body>
</html>