Compare commits

...

54 commits

Author SHA1 Message Date
b40361caf0 Replace fileSmasher with ncc. 2023-10-16 10:52:36 +01:00
640c2cdf92 Send friends list info after login 2023-10-10 16:03:55 +01:00
9d53d82997
DB access cleanup 2023-10-07 13:09:10 +01:00
877592bb94 Move all packet interfaces to a better location 2023-10-06 11:19:08 +01:00
2fbdb9799a Remove last instance of explicit any type 2023-10-06 09:55:15 +01:00
a32ab80f73 General cleanup 2023-10-06 09:55:03 +01:00
4ec4cb1c1f Remove duplicate interface 2023-10-06 09:52:22 +01:00
5e1106e488 Give types to all functions in OsuPacketWriter 2023-10-06 09:48:31 +01:00
39f6669f94 add type 2023-10-05 11:15:13 +01:00
056260ad55 improve tooling 2023-10-05 11:13:19 +01:00
78f4a499fa oops 2023-10-04 16:21:29 +01:00
462d0c879c Consolidate admin commands 2023-10-04 16:13:16 +01:00
108f27eb22 Remove unneeded semicolons 2023-10-04 15:52:18 +01:00
8ab318ef12 Correct some mistakes in the DB class 2023-10-04 15:12:35 +01:00
4b90031294 Kickstart the big db rewrite 2023-10-04 15:10:38 +01:00
686e6001b2 Code quality improvements. 2023-10-04 12:28:47 +01:00
93da399fa5 General code quality/readability 2023-10-03 10:09:44 +01:00
04bd1e42bb Change all for(each) blocks to use const instead of let. 2023-10-03 09:57:35 +01:00
25105537ea
Fix rate adjust mods not working in multi when freemod is active 2023-09-13 08:31:16 +01:00
c3b24d32af
Re-add LICENSE 2023-09-12 15:11:34 +01:00
4a6d698b47
build fixes 2023-09-10 20:37:46 +01:00
3070f6a742
Add example config and clean db dump 2023-09-10 19:07:52 +01:00
9a2bfabee6
Rename RollCommand -> Roll to make it consistent 2023-09-10 18:51:29 +01:00
50900e333e
Bump node requirement 2023-09-10 18:48:48 +01:00
06b632af4a
Update README.md 2023-09-10 18:37:34 +01:00
a00aba5825
Allow users to join chat channels 2023-09-10 18:32:45 +01:00
469cbb9bc9
Multiplayer fixes 2023-09-10 18:32:35 +01:00
aff53f1ab9
Bugfix a lot of multiplayer 2023-09-10 18:32:24 +01:00
f66c867d17
Fix spectator not working 2023-09-10 18:31:38 +01:00
09dc1ffb76
General BanchoServer cleanup 2023-09-10 18:31:29 +01:00
0d68b07e9c
Update README.md 2023-09-10 13:19:11 +01:00
91ebbd289b
Update node.js.yml 2023-09-10 13:17:33 +01:00
ba91cb6cdb
Update node.js.yml 2023-09-10 13:16:43 +01:00
726e490fe2
Update README.md 2023-09-10 13:14:37 +01:00
3a07a892e1
fix everything 2023-09-10 12:59:22 +01:00
148c2c341f
Create README.md 2023-09-08 09:45:47 +01:00
b0f4423633
remove webpack config 2023-08-20 13:03:28 +01:00
734cebb19e
progress 2023-08-20 13:03:01 +01:00
1a871e4c35
Permissions Enum 2023-08-20 13:02:16 +01:00
92d4f70af4
Actually leave the match on request 2022-11-27 23:48:53 +00:00
b6a0d5e4b1
make match finish async again 2022-11-27 23:48:43 +00:00
1907e9910d
(untested) spectator implementation 2022-11-27 17:37:28 +00:00
9f6339ce48
Config interface 2022-11-27 17:36:55 +00:00
bb6d86ebbd
MatchScoreData type 2022-11-27 15:59:43 +00:00
f08e34dc82
MULTIPLAYER WORKS KINDA! 2022-11-23 00:48:28 +00:00
be52b19002
more multiplayer stuff 2022-11-21 23:26:20 +00:00
e297aa1128
multiplayer 2022-11-20 23:37:39 +00:00
f7f2df1287
refactor passage of ex-global class instances 2022-11-19 22:28:46 +00:00
3da964f5d6
Make chat work 2022-11-19 15:06:03 +00:00
a09543b2fb
switch 2022-11-19 14:46:40 +00:00
2beeb5fd09
basic functionality 2022-11-19 01:06:03 +00:00
5ed106b7d4
olgfopgkdg 2022-11-17 00:29:07 +00:00
53a12461ce
kopasdkopsdaokp 2022-11-16 15:25:46 +00:00
4ebf9ee0e6
Initial Future Commit (typescript) 2022-11-16 11:59:23 +00:00
134 changed files with 7516 additions and 4398 deletions

View file

@ -1,13 +1,13 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ master ]
branches: [ "master" ]
pull_request:
branches: [ master ]
branches: [ "master" ]
jobs:
build:
@ -16,14 +16,18 @@ jobs:
strategy:
matrix:
node-version: [10.x]
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
# Don't run updateCheck for now
#- run: npm run dev:updateCheck
- run: npm run build
#- run: npm test

2
.gitignore vendored
View file

@ -1,4 +1,6 @@
node_modules/
build/
combined.ts
tHMM.ds
server-stats.log
config.json

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}`));

59
Binato.ts Normal file
View file

@ -0,0 +1,59 @@
import { ConsoleHelper } from "./ConsoleHelper";
import { readFileSync, existsSync } from "fs";
if (!existsSync("./config.json")) {
ConsoleHelper.printError("Config file missing!");
ConsoleHelper.printError("Check the GitHub for an example or create one with the example you have.");
process.exit(1);
}
import ChatHistory from "./server/ChatHistory";
import Config from "./server/interfaces/Config";
import HandleRequest from "./server/BanchoServer";
import { Registry, collectDefaultMetrics } from "prom-client";
import http from "http";
const config:Config = JSON.parse(readFileSync(__dirname + "/config.json").toString()) as Config;
if (config["prometheus"]["enabled"]) {
const register:Registry = new Registry();
register.setDefaultLabels({ app: "nodejs_binato" });
collectDefaultMetrics({ register });
const prometheusServer = http.createServer(async (req, res) => {
if (req.method === "GET") {
res.end(await register.metrics());
}
});
prometheusServer.listen(config["prometheus"]["port"], () => ConsoleHelper.printInfo(`Prometheus metrics listening at port ${config["prometheus"]["port"]}`));
} else {
ConsoleHelper.printWarn("Prometheus is disabled!");
}
const INDEX_PAGE:string = readFileSync("./web/serverPage.html").toString();
const binatoServer = http.createServer((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.end(INDEX_PAGE);
} else if (req.url == "/chat") {
// I don't think this works??
res.end(ChatHistory.GenerateForWeb());
}
break;
case "POST":
HandleRequest(req, res, packet);
break;
default:
res.writeHead(405);
res.end("Method not allowed");
break;
}
});
});
binatoServer.listen(config.http.port, () => ConsoleHelper.printInfo(`Binato is up! Listening at port ${config.http.port}`));

75
ConsoleHelper.ts Normal file
View file

@ -0,0 +1,75 @@
import * as dyetty from "dyetty";
console.clear();
enum LogType {
INFO,
WARN,
ERROR
}
const LogTags = {
INFO: dyetty.bgGreen(dyetty.black(" INFO ")),
BANCHO: dyetty.bgMagenta(dyetty.black(" BANCHO ")),
WEBREQ: dyetty.bgGreen(dyetty.black(" WEBREQ ")),
CHAT: dyetty.bgCyan(dyetty.black(" CHAT ")),
WARN: dyetty.bgYellow(dyetty.black(" WARN ")),
ERROR: dyetty.bgRed(" ERRR "),
REDIS: dyetty.bgRed(dyetty.white(" bREDIS ")),
STREAM: dyetty.bgBlue(dyetty.black(" STREAM "))
} 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 dyetty.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 printStream(s:string) : void {
log(LogTags.STREAM, s);
}
public static printInfo(s:string) : void {
log(LogTags.INFO, s);
}
public static printBancho(s:string) : void {
log(LogTags.BANCHO, s);
}
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);
}
}

4
Constants.ts Normal file
View file

@ -0,0 +1,4 @@
export default abstract class Constants {
public static readonly DEBUG = false;
public static readonly PROTOCOL_VERSION = 19;
}

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Holly Stubbs
Copyright (c) 2019-2023 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

View file

@ -1,5 +1,5 @@
# 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
# Binato [![CodeFactor](https://www.codefactor.io/repository/github/tgpholly/binato/badge)](https://www.codefactor.io/repository/github/tgpholly/binato) [![Node.js CI](https://github.com/tgpholly/Binato/actions/workflows/node.js.yml/badge.svg?branch=master)](https://github.com/tgpholly/Binato/actions/workflows/node.js.yml)
An implementation of osu!bancho in TypeScript
i'm sorry peppy
<hr>
@ -12,8 +12,7 @@ i'm sorry peppy
- 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
- Chat Bot (see [commands folder](https://github.com/tgpholly/Binato/tree/master/server/commands))
### [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)
@ -21,15 +20,17 @@ i'm sorry peppy
## 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)
- **NodeJS > 18**
- **MariaDB**
- 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
After doing this running `npm run dev:run` should start the server.
If you want to build something standalone you can run the build process using `npm run build`.
## 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.
@ -37,14 +38,15 @@ To report a bug [create a new issue](https://github.com/tgpholly/Binato/issues/n
<hr>
## How to connect:
See <ins>Now (2022 - 2023)</ins> for the prefered way to connect now.
### 2013 - Stable Fallback (2015 / 2016 ?):
Stable fallback uses HTTP so for that you just need to direct it to the server<br>
From 2013 to the Fallback client HTTP can be used, 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
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:
```
@ -62,10 +64,10 @@ In 2018 there were also new subdomains added which are:
- c6.ppy.sh
- ce.ppy.sh
### Now (2022):
### Now (2022 - 2023):
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
osu!.exe -devserver eusv.net
```
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.
@ -73,6 +75,8 @@ An example setup would be:
- osu.example.com (Score submit & web stuff)
- c.example.com (Bancho)
- a.example.com (Profile pictures)
In addition to all of this, your domain **must** have HTTPS. I recommend [Cloudflare](https://www.cloudflare.com/) for this task.
<hr>
## Other Binato components:

View file

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

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);
}
}

782
osu!.sql
View file

@ -1,35 +1,241 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
CREATE DATABASE IF NOT EXISTS `osu!` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
USE `osu!`;
--
-- Database: `osu!`
--
-- --------------------------------------------------------
--
-- Table structure for table `achievements`
--
CREATE TABLE `achievements` (
`id` int(11) NOT NULL,
`name` varchar(32) NOT NULL,
`description` varchar(128) NOT NULL,
`icon` varchar(32) NOT NULL,
`version` int(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
--
-- Dumping data for table `achievements`
--
INSERT INTO `achievements` (`id`, `name`, `description`, `icon`, `version`) VALUES
(1, '500 Combo (osu!std)', '500 big ones! You\'re moving up in the world!', 'osu-combo-500', 1),
(2, '750 Combo (osu!std)', '750 big ones! You\'re moving up in the world!', 'osu-combo-750', 1),
(3, '1000 Combo (osu!std)', '1000 big ones! You\'re moving up in the world!', 'osu-combo-1000', 1),
(4, '2000 Combo (osu!std)', '2000 big ones! You\'re moving up in the world!', 'osu-combo-2000', 1),
(5, '500 Combo (osu!taiko)', '500 big ones! You\'re moving up in the world!', 'osu-combo-500', 1),
(6, '750 Combo (osu!taiko)', '750 big ones! You\'re moving up in the world!', 'osu-combo-750', 1),
(7, '1000 Combo (osu!taiko)', '1000 big ones! You\'re moving up in the world!', 'osu-combo-1000', 1),
(8, '2000 Combo (osu!taiko)', '2000 big ones! You\'re moving up in the world!', 'osu-combo-2000', 1),
(9, '500 Combo (osu!ctb)', '500 big ones! You\'re moving up in the world!', 'osu-combo-500', 1),
(10, '750 Combo (osu!ctb)', '750 big ones! You\'re moving up in the world!', 'osu-combo-750', 1),
(11, '1000 Combo (osu!ctb)', '1000 big ones! You\'re moving up in the world!', 'osu-combo-1000', 1),
(12, '2000 Combo (osu!ctb)', '2000 big ones! You\'re moving up in the world!', 'osu-combo-2000', 1),
(13, '500 Combo (osu!mania)', '500 big ones! You\'re moving up in the world!', 'osu-combo-500', 1),
(14, '750 Combo (osu!mania)', '750 big ones! You\'re moving up in the world!', 'osu-combo-750', 1),
(15, '1000 Combo (osu!mania)', '1000 big ones! You\'re moving up in the world!', 'osu-combo-1000', 1),
(16, '2000 Combo (osu!mania)', '2000 big ones! You\'re moving up in the world!', 'osu-combo-2000', 1),
(17, 'Rising Star', 'Can\'t go forward without the first steps.', 'osu-skill-pass-1', 2),
(18, 'My First Don', 'Can\'t go forward without the first steps.', 'taiko-skill-pass-1', 2),
(19, 'A Slice Of Life', 'Can\'t go forward without the first steps.', 'fruits-skill-pass-1', 2),
(20, 'First Steps', 'Can\'t go forward without the first steps.', 'mania-skill-pass-1', 2),
(21, 'Constellation Prize', 'Definitely not a consolation prize. Now things start getting hard!', 'osu-skill-pass-2', 2),
(22, 'Katsu Katsu Katsu', 'Definitely not a consolation prize. Now things start getting hard!', 'taiko-skill-pass-2', 2),
(23, 'Dashing Ever Forward', 'Definitely not a consolation prize. Now things start getting hard!', 'fruits-skill-pass-2', 2),
(24, 'No Normal Player', 'Definitely not a consolation prize. Now things start getting hard!', 'mania-skill-pass-2', 2),
(25, 'Building Confidence', 'Oh, you\'ve SO got this.', 'osu-skill-pass-3', 2),
(26, 'Not Even Trying', 'Oh, you\'ve SO got this.', 'taiko-skill-pass-3', 2),
(27, 'Zesty Disposition', 'Oh, you\'ve SO got this.', 'fruits-skill-pass-3', 2),
(28, 'Impulse Drive', 'Oh, you\'ve SO got this.', 'mania-skill-pass-3', 2),
(29, 'Insanity Approaches', 'You\'re not twitching, you\'re just ready.', 'osu-skill-pass-4', 2),
(30, 'Face Your Demons', 'You\'re not twitching, you\'re just ready.', 'taiko-skill-pass-4', 2),
(31, 'Hyperdash ON!', 'You\'re not twitching, you\'re just ready.', 'fruits-skill-pass-4', 2),
(32, 'Hyperspeed', 'You\'re not twitching, you\'re just ready.', 'mania-skill-pass-4', 2),
(33, 'These Clarion Skies', 'Everything seems so clear now.', 'osu-skill-pass-5', 2),
(34, 'The Demon Within', 'Everything seems so clear now.', 'taiko-skill-pass-5', 2),
(35, 'It\'s Raining Fruit', 'Everything seems so clear now.', 'fruits-skill-pass-5', 2),
(36, 'Ever Onwards', 'Everything seems so clear now.', 'mania-skill-pass-5', 2),
(37, 'Above and Beyond', 'A cut above the rest.', 'osu-skill-pass-6', 2),
(38, 'Drumbreaker', 'A cut above the rest.', 'taiko-skill-pass-6', 2),
(39, 'Fruit Ninja', 'A cut above the rest.', 'fruits-skill-pass-6', 2),
(40, 'Another Surpassed', 'A cut above the rest.', 'mania-skill-pass-6', 2),
(41, 'Supremacy', 'All marvel before your prowess.', 'osu-skill-pass-7', 2),
(42, 'The Godfather', 'All marvel before your prowess.', 'taiko-skill-pass-7', 2),
(43, 'Dreamcatcher', 'All marvel before your prowess.', 'fruits-skill-pass-7', 2),
(44, 'Extra Credit', 'All marvel before your prowess.', 'mania-skill-pass-7', 2),
(45, 'Absolution', 'My god, you\'re full of stars!', 'osu-skill-pass-8', 2),
(46, 'Rhythm Incarnate', 'My god, you\'re full of stars!', 'taiko-skill-pass-8', 2),
(47, 'Lord of the Catch', 'My god, you\'re full of stars!', 'fruits-skill-pass-8', 2),
(48, 'Maniac', 'My god, you\'re full of stars!', 'mania-skill-pass-8', 2),
(49, 'Totality', 'All the notes. Every single one.', 'osu-skill-fc-1', 3),
(50, 'Keeping Time', 'All the notes. Every single one.', 'taiko-skill-fc-1', 3),
(51, 'Sweet And Sour', 'All the notes. Every single one.', 'fruits-skill-fc-1', 3),
(52, 'Keystruck', 'All the notes. Every single one.', 'mania-skill-fc-1', 3),
(53, 'Business As Usual', 'Two to go, please.', 'osu-skill-fc-2', 3),
(54, 'To Your Own Beat', 'Two to go, please.', 'taiko-skill-fc-2', 3),
(55, 'Reaching The Core', 'Two to go, please.', 'fruits-skill-fc-2', 3),
(56, 'Keying In', 'Two to go, please.', 'mania-skill-fc-2', 3),
(57, 'Building Steam', 'Hey, this isn\'t so bad.', 'osu-skill-fc-3', 3),
(58, 'Big Drums', 'Hey, this isn\'t so bad.', 'taiko-skill-fc-3', 3),
(59, 'Clean Platter', 'Hey, this isn\'t so bad.', 'fruits-skill-fc-3', 3),
(60, 'Hyperflow', 'Hey, this isn\'t so bad.', 'mania-skill-fc-3', 3),
(61, 'Moving Forward', 'Bet you feel good about that.', 'osu-skill-fc-4', 3),
(62, 'Adversity Overcome', 'Bet you feel good about that.', 'taiko-skill-fc-4', 3),
(63, 'Between The Rain', 'Bet you feel good about that.', 'fruits-skill-fc-4', 3),
(64, 'Breakthrough', 'Bet you feel good about that.', 'mania-skill-fc-4', 3),
(65, 'Paradigm Shift', 'Surprisingly difficult.', 'osu-skill-fc-5', 3),
(66, 'Demonslayer', 'Surprisingly difficult.', 'taiko-skill-fc-5', 3),
(67, 'Addicted', 'Surprisingly difficult.', 'fruits-skill-fc-5', 3),
(68, 'Everything Extra', 'Surprisingly difficult.', 'mania-skill-fc-5', 3),
(69, 'Anguish Quelled', 'Don\'t choke.', 'osu-skill-fc-6', 3),
(70, 'Rhythm\'s Call', 'Don\'t choke.', 'taiko-skill-fc-6', 3),
(71, 'Quickening', 'Don\'t choke.', 'fruits-skill-fc-6', 3),
(72, 'Level Breaker', 'Don\'t choke.', 'mania-skill-fc-6', 3),
(73, 'Never Give Up', 'Excellence is its own reward.', 'osu-skill-fc-7', 3),
(74, 'Time Everlasting', 'Excellence is its own reward.', 'taiko-skill-fc-7', 3),
(75, 'Supersonic', 'Excellence is its own reward.', 'fruits-skill-fc-7', 3),
(76, 'Step Up', 'Excellence is its own reward.', 'mania-skill-fc-7', 3),
(77, 'Aberration', 'They said it couldn\'t be done. They were wrong.', 'osu-skill-fc-8', 3),
(78, 'The Drummer\'s Throne', 'They said it couldn\'t be done. They were wrong.', 'taiko-skill-fc-8', 3),
(79, 'Dashing Scarlet', 'They said it couldn\'t be done. They were wrong.', 'fruits-skill-fc-8', 3),
(80, 'Behind The Veil', 'They said it couldn\'t be done. They were wrong.', 'mania-skill-fc-8', 3),
(81, 'Finality', 'High stakes, no regrets.', 'all-intro-suddendeath', 4),
(82, 'Perfectionist', 'Accept nothing but the best.', 'all-intro-perfect', 4),
(83, 'Rock Around The Clock', 'You can\'t stop the rock.', 'all-intro-hardrock', 4),
(84, 'Time And A Half', 'Having a right ol\' time. One and a half of them, almost.', 'all-intro-doubletime', 4),
(85, 'Sweet Rave Party', 'Founded in the fine tradition of changing things that were just fine as they were.', 'all-intro-nightcore', 4),
(86, 'Blindsight', 'I can see just perfectly.', 'all-intro-hidden', 4),
(87, 'Are You Afraid Of The Dark?', 'Harder than it looks, probably because it\'s hard to look.', 'all-intro-flashlight', 4),
(88, 'Dial It Right Back', 'Sometimes you just want to take it easy.', 'all-intro-easy', 4),
(89, 'Risk Averse', 'Safety nets are fun!', 'all-intro-nofail', 4),
(90, 'Slowboat', 'You got there. Eventually.', 'all-intro-halftime', 4),
(91, 'Burned Out', 'One cannot always spin to win.', 'all-intro-spunout', 4),
(92, '5,000 Plays', 'There\'s a lot more where that came from.', 'osu-plays-5000', 5),
(93, '15,000 Plays', 'Must.. click.. circles..', 'osu-plays-15000', 5),
(94, '25,000 Plays', 'There\'s no going back.', 'osu-plays-25000', 5),
(95, '50,000 Plays', 'You\'re here forever.', 'osu-plays-50000', 5);
-- --------------------------------------------------------
--
-- Table structure for table `api`
--
CREATE TABLE `api` (
`id` int(10) UNSIGNED NOT NULL,
`user_id` int(10) NOT NULL,
`api_key` varchar(32) NOT NULL,
`banned` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `beatmaps`
--
CREATE TABLE `beatmaps` (
`id` int(11) NOT NULL,
`beatmap_id` int(11) NOT NULL,
`beatmapset_id` int(11) NOT NULL,
`beatmap_md5` varchar(127) NOT NULL,
`song_name` text NOT NULL,
`ar` varchar(30) NOT NULL,
`od` varchar(30) NOT NULL,
`difficulty_std` varchar(30) NOT NULL,
`difficulty_taiko` varchar(30) NOT NULL,
`difficulty_ctb` varchar(30) NOT NULL,
`difficulty_mania` varchar(30) NOT NULL,
`max_combo` int(11) NOT NULL,
`hit_length` int(11) NOT NULL,
`bpm` int(11) NOT NULL,
`ranked` int(11) NOT NULL,
`latest_update` int(11) NOT NULL,
`ranked_status_freezed` int(11) NOT NULL,
`playcount` bigint(20) NOT NULL DEFAULT 0,
`passcount` bigint(20) NOT NULL DEFAULT 0,
`disable_pp` int(11) NOT NULL DEFAULT 0,
`rating` float NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `beatmaps_info`
--
CREATE TABLE `beatmaps_info` (
`id` int(11) NOT NULL,
`approved` tinyint(4) NOT NULL,
`approved_date` date NOT NULL,
`last_update` date NOT NULL,
`set_id` int(11) NOT NULL,
`artist` text NOT NULL,
`creator` text NOT NULL,
`source` text NOT NULL,
`title` text NOT NULL,
`version` text NOT NULL,
`file_md5` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `beatmaps_names`
--
CREATE TABLE `beatmaps_names` (
`id` int(11) NOT NULL,
`beatmap_md5` varchar(32) NOT NULL DEFAULT '',
`beatmap_name` varchar(256) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `beatmaps_rating`
--
CREATE TABLE `beatmaps_rating` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`beatmap_md5` varchar(127) NOT NULL,
`rating` float NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `clans`
--
CREATE TABLE `clans` (
`id` int(11) NOT NULL,
`tag` varchar(127) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `friends`
--
CREATE TABLE `friends` (
`user` int(11) NOT NULL,
`friendsWith` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
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;
-- --------------------------------------------------------
--
-- Table structure for table `mp_matches`
--
CREATE TABLE `mp_matches` (
`id` int(10) UNSIGNED NOT NULL,
@ -37,7 +243,13 @@ CREATE TABLE `mp_matches` (
`open_time` varchar(18) NOT NULL,
`close_time` varchar(18) DEFAULT NULL,
`seed` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `mp_match_rounds`
--
CREATE TABLE `mp_match_rounds` (
`id` int(11) NOT NULL,
@ -66,7 +278,145 @@ CREATE TABLE `mp_match_rounds` (
`player13` tinytext DEFAULT NULL,
`player14` tinytext DEFAULT NULL,
`player15` tinytext DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `osu_info`
--
CREATE TABLE `osu_info` (
`name` varchar(10) NOT NULL,
`value` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `pp_limits`
--
CREATE TABLE `pp_limits` (
`pp` int(11) NOT NULL DEFAULT 0,
`flashlight_pp` int(11) NOT NULL DEFAULT 0,
`relax_pp` int(11) NOT NULL DEFAULT 0,
`gamemode` int(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
--
-- Dumping data for table `pp_limits`
--
INSERT INTO `pp_limits` (`pp`, `flashlight_pp`, `relax_pp`, `gamemode`) VALUES
(999999999, 99999999, 999999999, 0),
(999999999, 99999999, 999999999, 1),
(999999999, 99999999, 999999999, 2),
(999999999, 99999999, 999999999, 3);
-- --------------------------------------------------------
--
-- Table structure for table `rx_modes_info`
--
CREATE TABLE `rx_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(10) 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(20) NOT NULL DEFAULT 0,
`avg_accuracy` float NOT NULL DEFAULT 0,
`level` int(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `scores`
--
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(4) NOT NULL DEFAULT 0,
`accuracy` float(15,12) DEFAULT NULL,
`pp` float NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `scores_first`
--
CREATE TABLE `scores_first` (
`scoreid` int(11) NOT NULL,
`userid` int(11) NOT NULL,
`beatmap_md5` varchar(128) NOT NULL,
`mode` tinyint(4) NOT NULL,
`rx` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `scores_relax`
--
CREATE TABLE `scores_relax` (
`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(4) NOT NULL DEFAULT 0,
`accuracy` float(15,12) DEFAULT NULL,
`pp` float NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users_info`
--
CREATE TABLE `users_info` (
`id` int(11) NOT NULL,
@ -83,12 +433,20 @@ CREATE TABLE `users_info` (
`tags` int(11) NOT NULL,
`supporter` tinyint(1) NOT NULL,
`web_session` varchar(64) NOT NULL,
`verification_needed` tinyint(1) NOT NULL DEFAULT '0',
`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;
`away_message` varchar(100) NOT NULL,
`last_modified_time` datetime NOT NULL DEFAULT current_timestamp(),
`is_deleted` tinyint(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users_modes_info`
--
CREATE TABLE `users_modes_info` (
`n` int(11) NOT NULL,
@ -100,77 +458,401 @@ CREATE TABLE `users_modes_info` (
`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,
`ranked_score` int(10) UNSIGNED NOT NULL,
`pp_rank` int(11) NOT NULL,
`pp_raw` int(11) NOT NULL DEFAULT '1',
`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;
`playtime` bigint(20) NOT NULL DEFAULT 0,
`avg_accuracy` float NOT NULL DEFAULT 0,
`level` int(11) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users_relationships`
--
CREATE TABLE `users_relationships` (
`id` int(11) NOT NULL,
`user1` int(11) NOT NULL,
`user2` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users_replays`
--
CREATE TABLE `users_replays` (
`user_id` int(11) NOT NULL,
`beatmap_id` int(11) NOT NULL,
`mode_id` int(11) NOT NULL,
`replay` text NOT NULL,
`date` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `users_scores_info`
--
CREATE TABLE `users_scores_info` (
`user_id` int(11) NOT NULL,
`username` text NOT NULL,
`beatmap_id` int(11) NOT NULL,
`score_id` int(11) NOT NULL,
`playMode` 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,
`total_score` int(11) NOT NULL,
`maxcombo` int(10) UNSIGNED NOT NULL,
`countkatu` int(10) UNSIGNED DEFAULT NULL,
`countgeki` int(10) UNSIGNED DEFAULT NULL,
`perfect` tinyint(1) NOT NULL,
`enabled_mods` int(11) NOT NULL,
`date` timestamp NOT NULL DEFAULT current_timestamp(),
`rank` varchar(2) NOT NULL,
`pp` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `user_clans`
--
CREATE TABLE `user_clans` (
`id` int(11) NOT NULL,
`tag` varchar(127) NOT NULL DEFAULT '',
`clan` int(11) NOT NULL,
`user` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `web_info`
--
CREATE TABLE `web_info` (
`i` int(11) NOT NULL,
`HomepageText` varchar(255) NOT NULL DEFAULT 'A default Binato instance!'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
--
-- Dumping data for table `web_info`
--
INSERT INTO `web_info` (`i`, `HomepageText`) VALUES
(0, 'Welcome to the Binato website! A server made for fun.');
-- --------------------------------------------------------
--
-- Table structure for table `web_pfp`
--
CREATE TABLE `web_pfp` (
`id` int(10) UNSIGNED NOT NULL,
`userid` int(10) UNSIGNED NOT NULL,
`storageid` varchar(14) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `web_prefs`
--
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',
`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;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
-- --------------------------------------------------------
--
-- Table structure for table `web_titles`
--
CREATE TABLE `web_titles` (
`id` int(11) NOT NULL,
`title` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci;
--
-- Dumping data for table `web_titles`
--
INSERT INTO `web_titles` (`id`, `title`) VALUES
(0, 'Home'),
(1, 'Leaderboard'),
(50, 'User Page'),
(100, 'Login'),
(101, 'Register'),
(102, 'Verification'),
(105, 'User Settings'),
(106, 'Change Password'),
(107, 'Required Password Change'),
(108, 'Change Profile Picture'),
(900, 'Admin Panel Home'),
(910, 'User Management'),
(911, 'User Editor');
--
-- Indexes for dumped tables
--
--
-- Indexes for table `achievements`
--
ALTER TABLE `achievements`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `api`
--
ALTER TABLE `api`
ADD PRIMARY KEY (`id`),
ADD KEY `user_id FK` (`user_id`);
--
-- Indexes for table `beatmaps`
--
ALTER TABLE `beatmaps`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `beatmaps_names`
--
ALTER TABLE `beatmaps_names`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `beatmaps_rating`
--
ALTER TABLE `beatmaps_rating`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `clans`
--
ALTER TABLE `clans`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `mp_matches`
--
ALTER TABLE `mp_matches`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `mp_match_rounds`
--
ALTER TABLE `mp_match_rounds`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `osu_info`
--
ALTER TABLE `osu_info`
ADD UNIQUE KEY `name` (`name`);
--
-- Indexes for table `rx_modes_info`
--
ALTER TABLE `rx_modes_info`
ADD PRIMARY KEY (`n`);
--
-- Indexes for table `scores`
--
ALTER TABLE `scores`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `scores_relax`
--
ALTER TABLE `scores_relax`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `id` (`id`);
--
-- Indexes for table `users_info`
--
ALTER TABLE `users_info`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `id` (`id`);
--
-- Indexes for table `users_modes_info`
--
ALTER TABLE `users_modes_info`
ADD PRIMARY KEY (`n`);
--
-- Indexes for table `users_relationships`
--
ALTER TABLE `users_relationships`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `users_scores_info`
--
ALTER TABLE `users_scores_info`
ADD PRIMARY KEY (`score_id`),
ADD UNIQUE KEY `score_id` (`score_id`);
--
-- Indexes for table `user_clans`
--
ALTER TABLE `user_clans`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `web_info`
--
ALTER TABLE `web_info`
ADD PRIMARY KEY (`i`);
--
-- Indexes for table `web_pfp`
--
ALTER TABLE `web_pfp`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `web_prefs`
--
ALTER TABLE `web_prefs`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `web_titles`
--
ALTER TABLE `web_titles`
ADD PRIMARY KEY (`id`);
ALTER TABLE `mp_matches`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for dumped tables
--
ALTER TABLE `mp_match_rounds`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT for table `api`
--
ALTER TABLE `api`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `beatmaps`
--
ALTER TABLE `beatmaps`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `beatmaps_names`
--
ALTER TABLE `beatmaps_names`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `beatmaps_rating`
--
ALTER TABLE `beatmaps_rating`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `clans`
--
ALTER TABLE `clans`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `mp_matches`
--
ALTER TABLE `mp_matches`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `mp_match_rounds`
--
ALTER TABLE `mp_match_rounds`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `rx_modes_info`
--
ALTER TABLE `rx_modes_info`
MODIFY `n` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `scores`
--
ALTER TABLE `scores`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=0;
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `scores_relax`
--
ALTER TABLE `scores_relax`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `users_info`
--
ALTER TABLE `users_info`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=100;
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `users_modes_info`
--
ALTER TABLE `users_modes_info`
MODIFY `n` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=0;
MODIFY `n` int(11) NOT NULL AUTO_INCREMENT;
INSERT INTO `web_info` (`i`, `HomepageText`) VALUES ('0', 'A default Binato instance!');
--
-- AUTO_INCREMENT for table `users_relationships`
--
ALTER TABLE `users_relationships`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `users_scores_info`
--
ALTER TABLE `users_scores_info`
MODIFY `score_id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `user_clans`
--
ALTER TABLE `user_clans`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `web_pfp`
--
ALTER TABLE `web_pfp`
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `api`
--
ALTER TABLE `api`
ADD CONSTRAINT `user_id FK` FOREIGN KEY (`user_id`) REFERENCES `users_info` (`id`);
COMMIT;

17
osuTyping.ts Normal file
View file

@ -0,0 +1,17 @@
import OsuPacketWriter from "./server/interfaces/OsuPacketWriter";
const nodeOsu = require("osu-packet");
export default abstract class osu {
static Bancho = {
Writer: function() : OsuPacketWriter {
return new nodeOsu.Bancho.Writer();
}
};
static Client = {
Reader: function(data:Buffer) {
return new nodeOsu.Client.Reader(data);
}
};
}

4014
package-lock.json generated

File diff suppressed because it is too large Load diff

37
package.json Executable file → Normal file
View file

@ -2,21 +2,38 @@
"name": "binato",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"main": "Binato.ts",
"scripts": {
"dev:updateCheck": "check-outdated",
"dev:run": "nodemon --watch './**/*.ts' Binato.ts",
"build": "npm-run-all build:*",
"build:build": "ncc build Binato.ts -o build",
"build:mangle": "ts-node ./tooling/mangle.ts",
"build:cleanup": "ts-node ./tooling/cleanup.ts",
"_clean": "tsc --build --clean"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"aes256": "^1.1.0",
"chalk": "^4.1.0",
"compression": "^1.7.4",
"express": "^4.17.1",
"mysql2": "^2.3.3",
"node-fetch": "^2.6.7",
"dyetty": "^1.0.1",
"mysql2": "^3.6.1",
"node-fetch": "^2.7.0",
"osu-packet": "^4.1.2",
"prom-client": "^13.2.0",
"redis": "^4.0.6",
"uuid": "^8.3.2"
"prom-client": "^14.2.0",
"redis": "^4.6.8"
},
"devDependencies": {
"@types/node": "^20.6.0",
"@types/node-fetch": "^2.6.4",
"@vercel/ncc": "^0.38.0",
"check-outdated": "^2.12.0",
"nodemon": "^3.0.1",
"npm-run-all": "^4.1.5",
"terser": "^5.21.0",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}

313
server/BanchoServer.ts Normal file
View file

@ -0,0 +1,313 @@
import Channel from "./objects/Channel";
import { ConsoleHelper } from "../ConsoleHelper";
import Constants from "../Constants";
import LoginProcess from "./LoginProcess";
import { IncomingMessage, ServerResponse } from "http";
import { Packets } from "./enums/Packets";
import { RedisClientType, createClient } from "redis";
import MessageData from "./interfaces/packetTypes/MessageData";
import PrivateMessage from "./packets/PrivateMessage";
import Shared from "./objects/Shared";
import SpectatorManager from "./SpectatorManager";
import osu from "../osuTyping";
const shared:Shared = new Shared();
shared.database.execute("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL");
shared.database.execute("UPDATE osu_info SET value = 0 WHERE name = 'online_now'");
// Server Setup
const spectatorManager:SpectatorManager = new SpectatorManager(shared);
let redisClient:RedisClientType;
async function subscribeToChannel(channelName:string, callback:(message:string) => void) {
// Dup and connect new client for channel subscription (required)
const subscriptionClient:RedisClientType = redisClient.duplicate();
await subscriptionClient.connect();
// Subscribe to channel
await subscriptionClient.subscribe(channelName, callback);
ConsoleHelper.printRedis(`Subscribed to ${channelName} channel`);
}
if (shared.config.redis.enabled) {
(async () => {
redisClient = createClient({
url: `redis://${shared.config.redis.password.replaceAll(" ", "") == "" ? "" : `${shared.config.redis.password}@`}${shared.config.redis.address}:${shared.config.redis.port}/${shared.config.redis.database}`
});
redisClient.on('error', e => ConsoleHelper.printRedis(e));
const connectionStartTime = Date.now();
await redisClient.connect();
ConsoleHelper.printRedis(`Connected to redis server. Took ${Date.now() - connectionStartTime}ms`);
// Score submit update channel
subscribeToChannel("binato:update_user_stats", (message) => {
const user = shared.users.getById(parseInt(message));
if (user != null) {
// Update user info
user.updateUserInfo(true);
ConsoleHelper.printRedis(`Score submission stats update request received for ${user.username}`);
}
});
})();
} else ConsoleHelper.printWarn("Redis is disabled!");
// Import packets
import ChangeAction from "./packets/ChangeAction";
import Logout from "./packets/Logout";
import UserPresence from "./packets/UserPresence";
import UserStatsRequest from "./packets/UserStatsRequest";
import UserPresenceBundle from "./packets/UserPresenceBundle";
import TourneyMatchSpecialInfo from "./packets/TourneyMatchSpecialInfo";
import TourneyMatchJoinChannel from "./packets/TourneyJoinMatchChannel";
import TourneyMatchLeaveChannel from "./packets/TourneyMatchLeaveChannel";
import AddFriend from "./packets/AddFriend";
import RemoveFriend from "./packets/RemoveFriend";
import PrivateChannel from "./objects/PrivateChannel";
import MultiplayerInvite from "./packets/MultiplayerInvite";
import SendPublicMessage from "./packets/SendPublicMessage";
// User timeout interval
setInterval(() => {
for (const User of shared.users.getIterableItems()) {
if (User.uuid == "bot") continue; // Ignore the bot
// Logout this user, they're clearly gone.
if (Date.now() >= User.timeoutTime) {
Logout(User);
}
}
}, 10000);
export default async function HandleRequest(req:IncomingMessage, res:ServerResponse, packet:Buffer) {
// Get the client's token string and request data
const requestTokenString = typeof(req.headers["osu-token"]) === "string" ? req.headers["osu-token"] : undefined;
// Check if the user is logged in
if (requestTokenString === undefined) {
// Client doesn't have a token yet, let's auth them!
await LoginProcess(req, res, packet, shared);
shared.database.execute("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
} else {
let responseData = Buffer.allocUnsafe(0);
// Client has a token, let's see what they want.
try {
// Get the current user
const user = shared.users.getByToken(requestTokenString);
// Make sure the client's token isn't invalid
if (user != null) {
// Update the session timeout time for each request
user.timeoutTime = Date.now() + 60000;
// Parse bancho packets
const osuPacketReader = osu.Client.Reader(packet);
const packets = osuPacketReader.Parse();
// Go through each packet sent by the client
for (const packet of packets) {
switch (packet.id) {
case Packets.Client_ChangeAction:
ChangeAction(user, packet.data);
break;
case Packets.Client_SendPublicMessage:
SendPublicMessage(user, packet.data);
break;
case Packets.Client_Logout:
await Logout(user);
break;
case Packets.Client_RequestStatusUpdate:
UserPresenceBundle(user);
break;
case Packets.Client_StartSpectating:
spectatorManager.startSpectating(user, packet.data);
break;
case Packets.Client_SpectateFrames:
spectatorManager.spectatorFrames(user, packet.data);
break;
case Packets.Client_StopSpectating:
spectatorManager.stopSpectating(user);
break;
case Packets.Client_SendPrivateMessage:
PrivateMessage(user, packet.data);
break;
case Packets.Client_JoinLobby:
shared.multiplayerManager.JoinLobby(user);
break;
case Packets.Client_PartLobby:
shared.multiplayerManager.LeaveLobby(user);
break;
case Packets.Client_CreateMatch:
await shared.multiplayerManager.CreateMatch(user, packet.data);
break;
case Packets.Client_JoinMatch:
shared.multiplayerManager.JoinMatch(user, packet.data);
break;
case Packets.Client_MatchChangeSlot:
user.match?.moveToSlot(user, packet.data);
break;
case Packets.Client_MatchReady:
user.match?.setStateReady(user);
break;
case Packets.Client_MatchChangeSettings:
await user.match?.updateMatch(user, packet.data);
break;
case Packets.Client_MatchNotReady:
user.match?.setStateNotReady(user);
break;
case Packets.Client_PartMatch:
await shared.multiplayerManager.LeaveMatch(user);
break;
case Packets.Client_MatchLock:
user.match?.lockOrKick(user, packet.data);
break;
case Packets.Client_MatchNoBeatmap:
user.match?.missingBeatmap(user);
break;
case Packets.Client_MatchSkipRequest:
user.match?.matchSkip(user);
break;
case Packets.Client_MatchHasBeatmap:
user.match?.notMissingBeatmap(user);
break;
case Packets.Client_MatchTransferHost:
user.match?.transferHost(user, packet.data);
break;
case Packets.Client_MatchChangeMods:
user.match?.updateMods(user, packet.data);
break;
case Packets.Client_MatchStart:
user.match?.startMatch();
break;
case Packets.Client_MatchLoadComplete:
user.match?.matchPlayerLoaded(user);
break;
case Packets.Client_MatchComplete:
await user.match?.onPlayerFinishMatch(user);
break;
case Packets.Client_MatchScoreUpdate:
user.match?.updatePlayerScore(user, packet.data);
break;
case Packets.Client_MatchFailed:
user.match?.matchFailed(user);
break;
case Packets.Client_MatchChangeTeam:
user.match?.changeTeam(user);
break;
case Packets.Client_ChannelJoin:
user.joinChannel(packet.data);
break;
case Packets.Client_ChannelPart:
user.leaveChannel(packet.data);
break;
case Packets.Client_SetAwayMessage:
//SetAwayMessage(PacketUser, CurrentPacket.data);
break;
case Packets.Client_FriendAdd:
await AddFriend(user, packet.data);
break;
case Packets.Client_FriendRemove:
await RemoveFriend(user, packet.data);
break;
case Packets.Client_UserStatsRequest:
UserStatsRequest(user, packet.data);
break;
case Packets.Client_SpecialMatchInfoRequest:
TourneyMatchSpecialInfo(user, packet.data);
break;
case Packets.Client_SpecialJoinMatchChannel:
TourneyMatchJoinChannel(user, packet.data);
break;
case Packets.Client_SpecialLeaveMatchChannel:
TourneyMatchLeaveChannel(user, packet.data);
break;
case Packets.Client_Invite:
MultiplayerInvite(user, packet.data);
break;
case Packets.Client_UserPresenceRequest:
UserPresence(user, user.id);
break;
// Ignored packets
case Packets.Client_Pong:
case Packets.Client_BeatmapInfoRequest:
case Packets.Client_ReceiveUpdates:
break;
default:
// Print out unimplemented packet
console.dir(packet);
break;
}
}
responseData = user.queue;
user.clearQueue();
} else {
// User's token is invlid, force a reconnect
ConsoleHelper.printBancho(`Forced client re-connect (Token is invalid)`);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.Announce("Reconnecting...");
osuPacketWriter.Restart(0);
responseData = osuPacketWriter.toBuffer;
}
} catch (e) {
if (Constants.DEBUG) {
throw e;
}
ConsoleHelper.printError(`${e}`);
} finally {
res.writeHead(200, {
"Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100",
});
// Send the prepared packet(s) to the client
res.end(responseData);
}
}
}

34
server/Bot.ts Normal file
View file

@ -0,0 +1,34 @@
import ICommand from "./interfaces/ICommand";
import Channel from "./objects/Channel";
import Shared from "./objects/Shared";
import User from "./objects/User";
// Commands
import RankingCommand from "./commands/Ranking";
import AdminCommand from "./commands/Admin";
import MultiplayerCommands from "./commands/Multiplayer";
import HelpCommand from "./commands/Help";
import RollCommand from "./commands/Roll";
export default class Bot {
public user:User;
private commands:{ [id: string]: ICommand } = {};
public constructor(shared:Shared, botUser:User) {
this.user = botUser;
this.commands["help"] = new HelpCommand(shared, this.commands);
this.commands["ranking"] = new RankingCommand(shared);
this.commands["admin"] = new AdminCommand(shared);
this.commands["mp"] = new MultiplayerCommands(shared);
this.commands["roll"] = new RollCommand(shared);
}
public OnMessage(channel:Channel, sender:User, text:string) {
const args = text.split(" ");
const command = this.commands[`${args.shift()?.replace("!", "").toLowerCase()}`];
if (command) {
command.exec(channel, sender, args);
}
}
}

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);
}

34
server/ChatHistory.ts Normal file
View file

@ -0,0 +1,34 @@
import { readFileSync } from "fs";
export default abstract class ChatHistory {
private static _history:Array<string> = new Array<string>();
private static _lastGeneratedPage:string;
private static _hasChanged:boolean = true;
private static readonly HISTORY_LENGTH = 10;
private static readonly PAGE_TEMPLATE = readFileSync("./web/chatPageTemplate.html").toString();
public static AddMessage(message:string) : void {
if (this._history.length === this.HISTORY_LENGTH) {
this._history.splice(0, 1);
}
this._history.push(message);
this._hasChanged = true;
}
public static GenerateForWeb() : string {
if (this._hasChanged) {
let lines = "", flip = false;
for (let i = 0; i < this.HISTORY_LENGTH; i++) {
lines += `<div class="line line${flip ? 1 : 0}">${this._history[i] == null ? "<hidden>blank</hidden>" : this._history[i].replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\n", "<br>")}</div>`
flip = !flip;
}
this._lastGeneratedPage = this.PAGE_TEMPLATE.toString().replace("|content|", lines);
this._hasChanged = false;
}
return this._lastGeneratedPage;
}
}

93
server/ChatManager.ts Normal file
View file

@ -0,0 +1,93 @@
import Channel from "./objects/Channel";
import { ConsoleHelper } from "../ConsoleHelper";
import FunkyArray from "./objects/FunkyArray";
import User from "./objects/User";
import Shared from "./objects/Shared";
import osu from "../osuTyping";
import PrivateChannel from "./objects/PrivateChannel";
export default class ChatManager {
public chatChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
public forceJoinChannels:FunkyArray<Channel> = new FunkyArray<Channel>();
private readonly shared:Shared;
public constructor(shared:Shared) {
this.shared = shared;
}
public AddChatChannel(name:string, description:string, forceJoin:boolean = false) : Channel {
const stream = this.shared.streams.CreateStream(`chat_channel:${name}`, false);
const channel = new Channel(this.shared, `#${name}`, description, stream);
this.chatChannels.add(channel.name, channel);
if (forceJoin) {
this.forceJoinChannels.add(name, channel);
}
ConsoleHelper.printChat(`Created chat channel [${name}]`);
return channel;
}
public AddSpecialChatChannel(name:string, streamName:string, forceJoin:boolean = false) : Channel {
const stream = this.shared.streams.CreateStream(`chat_channel:${streamName}`, false);
const channel = new Channel(this.shared, `#${name}`, "", stream);
this.chatChannels.add(channel.name, channel);
if (forceJoin) {
this.forceJoinChannels.add(name, channel);
}
ConsoleHelper.printChat(`Created chat channel [${name}]`);
return channel;
}
public RemoveChatChannel(channel:Channel | string) {
if (channel instanceof Channel) {
channel.stream.Delete();
this.chatChannels.remove(channel.stream.name);
this.forceJoinChannels.remove(channel.stream.name)
} else {
const chatChannel = this.GetChannelByName(channel);
if (chatChannel instanceof Channel) {
chatChannel.stream.Delete();
this.chatChannels.remove(chatChannel.stream.name);
this.forceJoinChannels.remove(chatChannel.stream.name)
}
}
}
public AddPrivateChatChannel(user0:User, user1:User) {
const stream = this.shared.streams.CreateStream(`private_channel:${user0.username},${user1.username}`, true);
const channel = new PrivateChannel(user0, user1, stream);
this.chatChannels.add(channel.name, channel);
ConsoleHelper.printChat(`Created private chat channel [${channel.name}]`);
return channel;
}
public GetChannelByName(channelName:string) : Channel | undefined {
return this.chatChannels.getByKey(channelName);
}
public GetPrivateChannelByName(channelName:string) : Channel | undefined {
return this.chatChannels.getByKey(channelName);
}
public ForceJoinChannels(user:User) {
for (const channel of this.forceJoinChannels.getIterableItems()) {
channel.Join(user);
}
}
public SendChannelListing(user:User) {
const osuPacketWriter = osu.Bancho.Writer();
for (const channel of this.chatChannels.getIterableItems()) {
if (channel.isSpecial) {
continue;
}
osuPacketWriter.ChannelAvailable({
channelName: channel.name,
channelTopic: channel.description,
channelUserCount: channel.userCount
});
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
}

34
server/countryHelper.js → server/Country.ts Executable file → Normal file
View file

@ -1,6 +1,6 @@
const countryCodes = {
const countryCodes:{ [id: string]: number } = {
"LV": 132,
"AD": 3,
"AD": 3,
"LT": 130,
"KM": 116,
"QA": 182,
@ -12,8 +12,8 @@ const countryCodes = {
"NZ": 166,
"TO": 215,
"KZ": 122,
"GA": 76,
"BW": 35,
"GA": 76,
"AX": 247,
"GE": 79,
"UA": 222,
@ -249,26 +249,14 @@ const countryCodes = {
"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";
// Get id of a country from a 2 char code
export function getCountryID(code:string) : number {
const upperCode = code.toUpperCase();
if (upperCode in countryCodes) {
return countryCodes[upperCode];
}
}
module.exports.countryCodes = countryCodes;
return 0;
}

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);
}

243
server/LoginProcess.ts Normal file
View file

@ -0,0 +1,243 @@
import { ConsoleHelper } from "../ConsoleHelper";
import fetch from "node-fetch";
import { getCountryID } from "./Country";
import { generateSession } from "./Util";
import LatLng from "./objects/LatLng";
import LoginInfo from "./objects/LoginInfo";
import Logout from "./packets/Logout";
import { pbkdf2 } from "crypto";
import User from "./objects/User";
import UserPresenceBundle from "./packets/UserPresenceBundle";
import UserPresence from "./packets/UserPresence";
import StatusUpdate from "./packets/StatusUpdate";
import Shared from "./objects/Shared";
import osu from "../osuTyping";
import IpZxqResponse from "./interfaces/IpZxqResponse";
import { IncomingMessage, ServerResponse } from "http";
import UserInfo from "./objects/database/UserInfo";
const { decrypt: aesDecrypt } = require("aes256");
const incorrectLoginResponse:Buffer = osu.Bancho.Writer().LoginReply(-1).toBuffer;
const requiredPWChangeResponse:Buffer = osu.Bancho.Writer()
.LoginReply(-1)
.Announce("As part of migration to a new password system you are required to change your password. Please logon to the website and change your password.").toBuffer;
enum LoginTypes {
CURRENT,
OLD_MD5,
OLD_AES
}
enum LoginResult {
VALID,
MIGRATION,
INCORRECT,
}
function TestLogin(loginInfo:LoginInfo, shared:Shared) {
return new Promise<LoginResult>(async (resolve, reject) => {
const userDBData = await shared.userInfoRepository.selectByUsername(loginInfo.username);
// Make sure a user was found in the database
if (userDBData == null) return resolve(LoginResult.INCORRECT);
// Make sure the username is the same as the login info
if (userDBData.username !== loginInfo.username) return resolve(LoginResult.INCORRECT);
switch (userDBData.has_old_password) {
case LoginTypes.CURRENT:
pbkdf2(loginInfo.password, userDBData.password_salt, shared.config.database.pbkdf2.itterations, shared.config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
if (err) {
return reject(err);
} else {
if (derivedKey.toString("hex") !== userDBData.password_hash)
return resolve(LoginResult.INCORRECT);
return resolve(LoginResult.VALID); // We good
}
});
break;
case LoginTypes.OLD_AES:
if (aesDecrypt(shared.config.database.key, userDBData.password_hash) !== loginInfo.password) {
return resolve(LoginResult.INCORRECT);
}
return resolve(LoginResult.MIGRATION);
case LoginTypes.OLD_MD5:
if (userDBData.password_hash !== loginInfo.password) {
return resolve(LoginResult.INCORRECT);
}
return resolve(LoginResult.MIGRATION);
}
});
}
export default async function LoginProcess(req:IncomingMessage, res:ServerResponse, packet:Buffer, shared:Shared) {
const loginStartTime = Date.now();
const loginInfo = LoginInfo.From(packet);
// Send back no data if there's no loginInfo
// Somebody is doing something funky
if (loginInfo === undefined) {
return res.end("");
}
const loginResult:LoginResult = await TestLogin(loginInfo, shared);
const osuPacketWriter = osu.Bancho.Writer();
let newUser:User | undefined;
let friendsPresence:Buffer = Buffer.alloc(0);
if (loginResult === LoginResult.VALID && loginInfo !== undefined) {
ConsoleHelper.printBancho(`New client connection. [User: ${loginInfo.username}]`);
// Get users IP for getting location
// Get cloudflare requestee IP first
let requestIP = req.headers["cf-connecting-ip"];
// Get IP of requestee since we are probably behind a reverse proxy
if (requestIP === undefined) {
requestIP = req.headers["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 undefined
if (requestIP === undefined) {
requestIP = "";
}
let userCountryCode:string, userLocation:LatLng;
// Check if it is a local or null IP
if (requestIP.includes("192.168.") || requestIP.includes("127.0.") || requestIP === "") {
// Set location to null island
userCountryCode = "XX";
userLocation = new LatLng(0, 0);
} else {
// Get user's location using zxq
const userLocationRequest = await fetch(`https://ip.zxq.co/${requestIP}`);
const userLocationData:IpZxqResponse = await userLocationRequest.json();
const userLatLng = userLocationData.loc.split(",");
userCountryCode = userLocationData.country;
userLocation = new LatLng(parseFloat(userLatLng[0]), parseFloat(userLatLng[1]));
}
// Get information about the user from the database
const userInfo = await shared.userInfoRepository.selectByUsername(loginInfo.username);
if (userInfo == null) {
return;
}
// Create a token for the client
const newClientToken:string = await generateSession();
const isTourneyClient = loginInfo.version.includes("tourney");
// Make sure user is not already connected, kick off if so.
const connectedUser = shared.users.getByUsername(loginInfo.username);
if (connectedUser != null && !isTourneyClient && !connectedUser.isTourneyUser) {
Logout(connectedUser);
}
// Retreive the newly created user
newUser = shared.users.add(newClientToken, new User(userInfo.id, loginInfo.username, newClientToken, userInfo.tags, shared));
// Set tourney client flag
newUser.isTourneyUser = isTourneyClient;
newUser.location = userLocation;
// Get user's data from the database
newUser.updateUserInfo();
try {
newUser.countryID = getCountryID(userCountryCode);
// We're ready to start putting together a login response
// 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);
osuPacketWriter.ProtocolNegotiation(19);
// Permission level 4 is osu!supporter
osuPacketWriter.LoginPermissions(4);
// 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();
// Setup chat
shared.chatManager.ForceJoinChannels(newUser);
shared.chatManager.SendChannelListing(newUser);
// Construct & send user's friends list
const friends = await shared.database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
const friendsArray:Array<number> = new Array<number>();
for (const friend of friends) {
const friendId:number = friend.friendsWith;
friendsArray.push(friendId);
// Also fetch presence for friend if they are online
if (shared.users.getById(friendId) === undefined) { continue; }
const friendPresence = UserPresence(shared, friendId);
if (friendPresence === undefined) {
continue;
}
friendsPresence = Buffer.concat([
friendsPresence,
friendPresence
], friendsPresence.length + friendPresence.length);
}
// Write this to the user's queue rather than just sending it back so we
// don't get the weird `Loading..., Loading...` etc on friends after login.
const friendsPacketWriter = osu.Bancho.Writer();
friendsPacketWriter.FriendsList(friendsArray);
const friendData = friendsPacketWriter.toBuffer;
newUser.addActionToQueue(Buffer.concat([friendData, friendsPresence], friendData.length + friendsPresence.length));
// After sending the user their friends list send them the online users
UserPresenceBundle(newUser);
osuPacketWriter.Announce(`Welcome back ${loginInfo.username}!`);
} catch (err) {
console.error(err);
}
}
res.removeHeader('X-Powered-By');
res.removeHeader('Date');
// Complete / Fail login
const writerBuffer:Buffer = osuPacketWriter.toBuffer;
if (newUser === undefined) {
res.writeHead(200, {
"cho-token": "no", // NOTE: You have to specify a token even if it's an incorrect login for some reason.
"Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100"
});
switch (loginResult) {
case LoginResult.INCORRECT:
res.end(incorrectLoginResponse, () => {
ConsoleHelper.printBancho(`User login failed (Incorrect Password) took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
break;
case LoginResult.MIGRATION:
res.end(requiredPWChangeResponse, () => {
ConsoleHelper.printBancho(`User login failed (Migration Required) took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
break;
}
} else {
res.writeHead(200, {
"cho-token": newUser.uuid,
"Connection": "keep-alive",
"Keep-Alive": "timeout=5, max=100",
});
res.end(writerBuffer, () => {
ConsoleHelper.printBancho(`User login finished, took ${Date.now() - loginStartTime}ms. [User: ${loginInfo.username}]`);
});
}
}

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

@ -0,0 +1,169 @@
import Channel from "./objects/Channel";
import Shared from "./objects/Shared";
import { SlotStatus } from "./enums/SlotStatus";
import DataStream from "./objects/DataStream";
import Match from "./objects/Match";
import User from "./objects/User";
import StatusUpdate from "./packets/StatusUpdate";
import UserPresence from "./packets/UserPresence";
import UserPresenceBundle from "./packets/UserPresenceBundle";
import MatchArray from "./objects/MatchArray";
import MatchJoinData from "./interfaces/packetTypes/MatchJoinData";
import MatchData from "./interfaces/packetTypes/MatchData";
import osu from "../osuTyping";
import TourneyMatchSpecialInfo from "./packets/TourneyMatchSpecialInfo";
export default class MultiplayerManager {
private readonly shared:Shared;
private matches:MatchArray = new MatchArray();
private readonly lobbyStream:DataStream;
private readonly lobbyChat:Channel;
public constructor(shared:Shared) {
this.shared = shared;
this.lobbyStream = shared.streams.CreateStream("multiplayer:lobby", false);
const channel = this.shared.chatManager.GetChannelByName("#lobby");
if (channel === undefined) {
throw "Something has gone horribly wrong, the lobby channel does not exist!";
}
this.lobbyChat = channel;
}
public JoinLobby(user:User) {
if (user.inMatch) {
user.match?.leaveMatch(user);
}
this.lobbyChat.Join(user);
this.GenerateLobbyListing(user);
this.lobbyStream.AddUser(user);
}
public LeaveLobby(user:User) {
this.lobbyStream.RemoveUser(user);
}
public JoinMatch(user:User, matchData:number | MatchJoinData) {
try {
let match:Match | undefined;
if (typeof(matchData) === "number") {
match = this.matches.getById(matchData);
} else {
match = this.matches.getById(matchData.matchId);
}
if (!(match instanceof Match)) {
throw "MatchIdInvalid";
}
if (match.gamePassword !== undefined && typeof(matchData) !== "number") {
if (match.gamePassword !== matchData.gamePassword) {
throw "IncorrectPassword";
}
}
let matchFull = true;
for (const slot of match.slots) {
if (slot.player instanceof User || slot.status === SlotStatus.Locked) {
continue;
}
slot.status = SlotStatus.NotReady
slot.player = user;
user.match = match;
user.matchSlot = slot;
matchFull = false;
break;
}
if (matchFull) {
throw "MatchFull";
}
// Inform users in the match that somebody has joined
match.sendMatchUpdate();
match.matchStream.AddUser(user);
match.matchChatChannel.Join(user);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinSuccess(match.serialiseMatch());
user.addActionToQueue(osuPacketWriter.toBuffer);
this.UpdateLobbyListing();
} catch (e) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchJoinFail();
user.addActionToQueue(osuPacketWriter.toBuffer);
this.GenerateLobbyListing(user);
}
}
public UpdateLobbyListing() {
this.lobbyStream.Send(this.GenerateLobbyListing());
}
public GenerateLobbyListing(user?:User) : Buffer {
const osuPacketWriter = osu.Bancho.Writer();
let bufferToSend = UserPresenceBundle(this.shared);
for (const match of this.matches.getIterableItems()) {
for (const slot of match.slots) {
if (!(slot.player instanceof User) || slot.status === SlotStatus.Locked) {
continue;
}
const presenceBuffer = UserPresence(this.shared, slot.player.id);
const statusBuffer = StatusUpdate(this.shared, slot.player.id);
if (presenceBuffer === undefined || statusBuffer === undefined) {
continue;
}
bufferToSend = Buffer.concat([bufferToSend, presenceBuffer, statusBuffer], bufferToSend.length + presenceBuffer.length + statusBuffer.length);
}
osuPacketWriter.MatchNew(match.serialiseMatch());
}
const osuBuffer = osuPacketWriter.toBuffer;
bufferToSend = Buffer.concat([bufferToSend, osuBuffer], bufferToSend.length + osuBuffer.length);
if (user instanceof User) {
user.addActionToQueue(bufferToSend);
}
return bufferToSend;
}
public GetMatchById(id:number) : Match | undefined {
return this.matches.getById(id);
}
public async CreateMatch(user:User, matchData:MatchData) {
const match = await Match.createMatch(user, matchData, this.shared);
this.matches.add(match.matchId.toString(), match);
this.JoinMatch(user, match.matchId);
}
public async LeaveMatch(user:User) {
if (user.match instanceof Match) {
user.match.leaveMatch(user);
let usersInMatch = false;
for (const slot of user.match.slots) {
if (slot.player !== undefined) {
usersInMatch = true;
break;
}
}
if (!usersInMatch) {
this.matches.remove(user.match.matchId.toString());
}
this.UpdateLobbyListing();
}
}
}

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

@ -0,0 +1,41 @@
import Channel from "./objects/Channel";
import { ConsoleHelper } from "../ConsoleHelper";
import FunkyArray from "./objects/FunkyArray";
import User from "./objects/User";
import Shared from "./objects/Shared";
import osu from "../osuTyping";
import PrivateChannel from "./objects/PrivateChannel";
export default class PrivateChatManager {
public chatChannels:FunkyArray<PrivateChannel> = new FunkyArray<PrivateChannel>();
private readonly shared:Shared;
public constructor(shared:Shared) {
this.shared = shared;
}
public AddChannel(user0:User, user1:User) : PrivateChannel {
const stream = this.shared.streams.CreateStream(`private_channel:${user0.username},${user1.username}`, true);
const channel = new PrivateChannel(user0, user1, stream);
this.chatChannels.add(channel.name, channel);
ConsoleHelper.printChat(`Created private chat channel [${channel.name}]`);
return channel;
}
public RemoveChannel(channel:PrivateChannel | string) {
if (channel instanceof Channel) {
channel.stream.Delete();
this.chatChannels.remove(channel.stream.name);
} else {
const chatChannel = this.GetChannelByName(channel);
if (chatChannel instanceof Channel) {
chatChannel.stream.Delete();
this.chatChannels.remove(chatChannel.stream.name);
}
}
}
public GetChannelByName(channelName:string) : PrivateChannel | undefined {
return this.chatChannels.getByKey(channelName);
}
}

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

@ -0,0 +1,81 @@
import DataStream from "./objects/DataStream";
import Shared from "./objects/Shared";
import User from "./objects/User";
import osu from "../osuTyping";
import SpectateFramesData from "./interfaces/packetTypes/SpectateFramesData";
export default class SpectatorManager {
private shared:Shared;
public constructor(shared:Shared) {
this.shared = shared;
}
public startSpectating(user:User, userIdToSpectate:number) {
const userToSpectate = this.shared.users.getById(userIdToSpectate);
if (userToSpectate === undefined) {
return;
}
// Use existing or create spectator stream
let spectateStream:DataStream;
if (userToSpectate.spectatorStream === undefined) {
user.spectatorStream = spectateStream = userToSpectate.spectatorStream = this.shared.streams.CreateStream(`spectator:${userToSpectate.username}`);
} else {
user.spectatorStream = spectateStream = userToSpectate.spectatorStream;
}
user.spectatingUser = userToSpectate;
spectateStream.AddUser(user);
let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorJoined(user.id);
userToSpectate.addActionToQueue(osuPacketWriter.toBuffer);
osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorJoined(user.id);
spectateStream.Send(osuPacketWriter.toBuffer);
}
public spectatorFrames(user:User, spectateFramesData:SpectateFramesData) {
if (user.spectatorStream === undefined) {
return;
}
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectateFrames(spectateFramesData);
user.spectatorStream.Send(osuPacketWriter.toBuffer);
}
public stopSpectating(user:User) {
if (user.spectatingUser === undefined || user.spectatorStream === undefined) {
return;
}
const spectatedUser = user.spectatingUser;
let osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectatorLeft(user.id);
spectatedUser.addActionToQueue(osuPacketWriter.toBuffer);
const stream = user.spectatorStream;
stream.RemoveUser(user);
user.spectatorStream = undefined;
user.spectatingUser = undefined;
if (stream.IsActive) {
osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.FellowSpectatorLeft(user.id);
stream.Send(osuPacketWriter.toBuffer);
} else {
spectatedUser.spectatorStream = undefined;
}
}
}

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);
}
}

39
server/Util.ts Normal file
View file

@ -0,0 +1,39 @@
import { randomBytes } from "crypto";
export function generateSession() : Promise<string> {
return new Promise<string>((resolve, reject) => {
randomBytes(12, (err, buf) => {
if (err) {
return reject(err);
}
resolve(buf.toString("hex"));
});
});
}
export function hexlify(data:Buffer) : string {
let out:string = "";
for (let i = 0; i < data.length; i++) {
const hex = data[i].toString(16);
if (hex.length === 1) {
out += `0${hex.toUpperCase()},`;
} else {
out += `${hex.toUpperCase()},`;
}
}
return out.slice(0, out.length - 1);
}
export function isNullOrEmpty(str:string | undefined | null) {
if (typeof(str) === "string") {
return str !== "";
}
return false;
}
export function enumHasFlag(value:number, flag:number) : boolean {
return (value & flag) === flag;
}

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);
}
}

35
server/commands/Admin.ts Normal file
View file

@ -0,0 +1,35 @@
import { enumHasFlag } from "../Util";
import { Permissions } from "../enums/Permissions";
import Channel from "../objects/Channel";
import User from "../objects/User";
import BaseCommand from "./BaseCommand";
export default class AdminCommand extends BaseCommand {
public readonly adminOnly:boolean = true;
public readonly helpDescription:string = "Locks/Unlocks a channel and limits conversation to mods and above.";
public exec(channel:Channel, sender:User, args:Array<string>) {
if (!enumHasFlag(sender.permissions, Permissions.Admin) || !enumHasFlag(sender.permissions, Permissions.Peppy)) {
channel.SendBotMessage("You don't have permission to execute that command.");
return;
}
const subCommand = args[0].toLowerCase();
args.shift();
switch (subCommand) {
case "lock":
return adminLock(channel);
}
}
}
function adminLock(channel:Channel) {
if (channel.isSpecial) {
channel.SendBotMessage("Multiplayer channels cannot be locked");
return;
}
channel.isLocked = !channel.isLocked;
channel.SendBotMessage(`Channel is now ${channel.isLocked ? "locked" : "unlocked"}`);
}

View file

@ -0,0 +1,20 @@
import ICommand from "../interfaces/ICommand";
import Channel from "../objects/Channel";
import Shared from "../objects/Shared";
import User from "../objects/User";
export default class BaseCommand implements ICommand {
public shared:Shared;
public readonly adminOnly:boolean = false;
public readonly helpText:string = "No help page was found for that command";
public readonly helpDescription:string = "Command has no description set";
public readonly helpArguments:Array<string> = new Array<string>();
public constructor(shared:Shared) {
this.shared = shared;
}
public exec(channel:Channel, sender:User, args:Array<string>) {
channel.SendBotMessage(`Sorry ${sender.username}! This command has no functionality yet. Args: ["${args.join('", "')}"]`);
}
}

41
server/commands/Help.ts Normal file
View file

@ -0,0 +1,41 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import BaseCommand from "./BaseCommand";
import Shared from "../objects/Shared";
import ICommand from "../interfaces/ICommand";
export default class HelpCommand extends BaseCommand {
public readonly helpDescription:string = "Shows this message! :)";
private readonly commandList:{ [id:string]: ICommand };
private commandKeys:Array<string> = new Array<string>();
public constructor(shared:Shared, commands:{ [id:string]: ICommand }) {
super(shared);
this.commandList = commands;
}
public exec(channel:Channel, sender:User, args:Array<string>) {
if (this.commandKeys.length === 0) {
this.commandKeys = Object.keys(this.commandList);
}
// All commands
if (args.length === 0) {
let constructedHelp = "Help:\n";
for (const key of this.commandKeys) {
constructedHelp += ` !${key} - ${this.commandList[key].helpDescription}\n`;
}
channel.SendBotMessage(constructedHelp.slice(0, constructedHelp.length - 1));
return;
}
// Command page
const commandName = args[0].toLowerCase();
if (commandName in this.commandList) {
channel.SendBotMessage(this.commandList[commandName].helpText);
} else {
channel.SendBotMessage("No help page was found for that command");
}
}
}

View file

@ -0,0 +1,88 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import Match from "../objects/Match";
import BaseCommand from "./BaseCommand";
export default class MultiplayerCommands extends BaseCommand {
public readonly helpText:string = `Multiplayer Subcommands:
!mp start - Starts a multiplayer match with a delay (optional)
!mp abort - Aborts the currently running round / countdown`;
public readonly helpDescription:string = "Command for use in multiplayer matches.";
public readonly helpArguments:Array<string> = ["subCommand"];
public exec(channel:Channel, sender:User, args:Array<string>) {
// TODO: Determine if this check is correct
if (sender.match == undefined || channel.name != "#multiplayer") {
channel.SendBotMessage("You must be in a multiplayer match to use this command");
return;
}
// Check if sender is match host
if (!User.Equals(sender, sender.match.host)) {
channel.SendBotMessage("You must be the match host to use multiplayer commands");
return;
}
if (args.length === 0) {
channel.SendBotMessage("You must specify a sub command, use \"!help mp\" to see a list of them.");
return;
}
const subCommand = args[0].toLowerCase();
args.shift();
switch (subCommand) {
case "start":
return mpStart(channel, sender.match, args);
case "abort":
return mpAbort(channel, sender.match);
}
}
}
function mpStart(channel:Channel, match:Match, args:Array<string>) {
// If no time is specified start instantly
if (args.length === 0) {
channel.SendBotMessage("Good luck, have fun!");
setTimeout(() => match.startMatch(), 1000);
return;
}
const countdownTime = parseInt(args[0]);
if (isNaN(countdownTime)) {
channel.SendBotMessage("Countdown time must be a valid number");
return;
}
let countdownUpdates = 0;
match.countdownTime = countdownTime;
match.countdownTimer = setInterval(() => {
if (match.countdownTime <= 0) {
clearInterval(match.countdownTimer);
match.countdownTimer = undefined;
channel.SendBotMessage("Good luck, have fun!");
setTimeout(() => match.startMatch(), 1000);
return;
}
if (match.countdownTime <= 5 && match.countdownTime > 0) {
channel.SendBotMessage(`Starting in ${match.countdownTime} seconds`);
} else if (match.countdownTime <= 30 ? countdownUpdates % 10 === 0 : countdownUpdates % 30 === 0) {
channel.SendBotMessage(`Starting in ${match.countdownTime} seconds`);
}
match.countdownTime--;
countdownUpdates++;
}, 1000);
}
function mpAbort(channel:Channel, match:Match) {
if (match.countdownTimer && match.countdownTime > 0) {
clearInterval(match.countdownTimer);
match.countdownTimer = undefined;
channel.SendBotMessage("Aborted countdown");
} else {
// TODO: Determine the correct way to abort a round
match.finishMatch();
channel.SendBotMessage("Aborted current round");
}
}

View file

@ -0,0 +1,35 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import { RankingMode } from "../enums/RankingMode";
import BaseCommand from "./BaseCommand";
export default class RankingCommand extends BaseCommand {
public readonly helpText:string = `Ranking Modes:
!ranking pp - Sets your ranking mode to pp
!ranking score - Sets your ranking mode to score
!ranking acc - Sets your ranking mode to accuracy`;
public readonly helpDescription:string = "Sets your prefered ranking type";
public exec(channel:Channel, sender:User, args:Array<string>) {
if (args.length === 0) {
channel.SendBotMessage("You must specify a ranking mode, use \"!help ranking\" to see the options.");
return;
}
switch (args[0].toLowerCase()) {
case "pp":
sender.rankingMode = RankingMode.PP;
channel.SendBotMessage("Set ranking mode to pp.");
break;
case "score":
sender.rankingMode = RankingMode.RANKED_SCORE;
channel.SendBotMessage("Set ranking mode to score.");
break;
case "acc":
sender.rankingMode = RankingMode.AVG_ACCURACY;
channel.SendBotMessage("Set ranking mode to accuracy.");
break;
}
sender.updateUserInfo(true);
}
}

21
server/commands/Roll.ts Normal file
View file

@ -0,0 +1,21 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import BaseCommand from "./BaseCommand";
export default class RollCommand extends BaseCommand {
public readonly helpDescription:string = "Roll some dice and get a random number between 1 and a number (default 100)";
public readonly helpArguments:Array<string> = ["number"];
public exec(channel:Channel, sender:User, args:Array<string>) {
let limit = 99;
if (args.length === 1) {
const userLimit = parseInt(args[0]);
if (!isNaN(userLimit)) {
limit = userLimit;
}
}
const number = Math.round(Math.random() * limit) + 1;
channel.SendBotMessage(`${sender.username} rolls ${number} point(s)`);
}
}

7
server/enums/Mode.ts Normal file
View file

@ -0,0 +1,7 @@
export enum Mode {
Unknown = -1,
Osu,
Taiko,
Catch,
Mania
}

33
server/enums/Mods.ts Normal file
View file

@ -0,0 +1,33 @@
// TODO: Complete mods enum.
export enum Mods {
None,
NoFail = 1 << 0,
Easy = 1 << 1,
// 2 was used for the "No Video" mod but that's gone now.
Hidden = 1 << 3,
HardRock = 1 << 4,
SuddenDeath = 1 << 5,
DoubleTime = 1 << 6,
Relax = 1 << 7,
HalfTime = 1 << 8,
Nightcore = 1 << 9,
Flashlight = 1 << 10,
Autoplay = 1 << 11,
SpunOut = 1 << 12,
Autopilot = 1 << 13, // I think this is autopilot???
Perfect = 1 << 14,
Mania4K = 1 << 15,
Mania5K = 1 << 16,
Mania6K = 1 << 17,
Mania7K = 1 << 18,
Mania8K = 1 << 19,
FadeIn = 1 << 20,
Random = 1 << 21,
Cinema = 1 << 22,
Target = 1 << 23,
Mania9K = 1 << 24,
ManiaCoop = 1 << 25,
Mania1K = 1 << 26,
Mania3K = 1 << 27,
Mania2K = 1 << 28
}

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 = 58,
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_SpecialLeaveMatchChannel,
}

View file

@ -0,0 +1,11 @@
export enum Permissions {
None = 0,
BAT = 2,
Supporter = 4,
Friend = 8,
Peppy = 16,
Tournament = 32,
Bot = 64,
Moderator = 128,
Admin = 256
}

View file

@ -0,0 +1,5 @@
export enum RankingMode {
PP,
RANKED_SCORE,
AVG_ACCURACY
}

View file

@ -0,0 +1,9 @@
export enum SlotStatus {
Empty = 1,
Locked = 2,
NotReady = 4,
Ready = 8,
MissingBeatmap = 16,
Playing = 32,
Quit = 128
}

4
server/enums/Team.ts Normal file
View file

@ -0,0 +1,4 @@
export enum Team {
Red,
Blue
}

View file

@ -0,0 +1,39 @@
export default interface Config {
http:HttpConfigSection,
prometheus:PrometheusConfigSection,
redis:RedisConfigSection,
database:DatabaseConfigSection
}
interface HttpConfigSection {
port:number,
compression:boolean
}
interface PrometheusConfigSection {
enabled:boolean,
port:number
}
interface RedisConfigSection {
enabled:boolean,
address:string,
port:number,
database:number,
password:string
}
interface DatabaseConfigSection {
address:string,
port:number,
username:string,
password:string,
name:string,
pbkdf2:PBKDF2DatabaseConfigSection,
key:string
}
interface PBKDF2DatabaseConfigSection {
itterations:number,
keylength:number
}

View file

@ -0,0 +1,11 @@
import Channel from "../objects/Channel";
import Shared from "../objects/Shared";
import User from "../objects/User";
export default interface ICommand {
shared:Shared,
adminOnly:boolean,
helpText:string,
helpDescription:string,
exec: (channel:Channel, sender:User, args:Array<string>) => void
}

View file

@ -0,0 +1,4 @@
export default interface IpZxqResponse {
country: string,
loc: string
}

View file

@ -0,0 +1,71 @@
import ChannelData from "./packetTypes/ChannelData"
import MatchData from "./packetTypes/MatchData"
import MessageData from "./packetTypes/MessageData"
import ScoreFrameData from "./packetTypes/ScoreFrameData"
import SpectateFramesData from "./packetTypes/SpectateFramesData"
import StatusUpdateData from "./packetTypes/StatusUpdateData"
import UserPresenceData from "./packetTypes/UserPresenceData"
import UserQuitData from "./packetTypes/UserQuitData"
export default interface OsuPacketWriter {
// Functions
LoginReply(data:number) : OsuPacketWriter,
CommandError() : OsuPacketWriter,
SendMessage(data:MessageData) : OsuPacketWriter,
Ping() : OsuPacketWriter,
HandleIrcChangeUsername(data:string) : OsuPacketWriter,
HandleIrcQuit() : OsuPacketWriter,
HandleOsuUpdate(data:StatusUpdateData) : OsuPacketWriter,
HandleUserQuit(data:UserQuitData) : OsuPacketWriter,
SpectatorJoined(data:number) : OsuPacketWriter,
SpectatorLeft(data:number) : OsuPacketWriter,
SpectateFrames(data:SpectateFramesData) : OsuPacketWriter,
VersionUpdate() : OsuPacketWriter,
SpectatorCantSpectate(data:number) : OsuPacketWriter,
GetAttention() : OsuPacketWriter,
Announce(data:string) : OsuPacketWriter,
MatchUpdate(data:MatchData) : OsuPacketWriter,
MatchNew(data:MatchData) : OsuPacketWriter,
MatchDisband(data:number) : OsuPacketWriter,
MatchJoinSuccess(data:MatchData) : OsuPacketWriter,
MatchJoinFail() : OsuPacketWriter,
FellowSpectatorJoined(data:number) : OsuPacketWriter,
FellowSpectatorLeft(data:number) : OsuPacketWriter,
MatchStart(data:MatchData) : OsuPacketWriter,
MatchScoreUpdate(data:ScoreFrameData) : OsuPacketWriter,
MatchTransferHost() : OsuPacketWriter,
MatchAllPlayersLoaded() : OsuPacketWriter,
MatchPlayerFailed(data:number) : OsuPacketWriter,
MatchComplete() : OsuPacketWriter,
MatchSkip() : OsuPacketWriter,
Unauthorised() : OsuPacketWriter,
ChannelJoinSuccess(data:string) : OsuPacketWriter,
ChannelAvailable(data:ChannelData) : OsuPacketWriter,
ChannelRevoked(data:string) : OsuPacketWriter,
ChannelAvailableAutojoin(data:ChannelData) : OsuPacketWriter,
BeatmapInfoReply() : OsuPacketWriter,
LoginPermissions(data:number) : OsuPacketWriter,
FriendsList(data:Array<number>) : OsuPacketWriter,
ProtocolNegotiation(data:number) : OsuPacketWriter,
TitleUpdate(data:string) : OsuPacketWriter,
Monitor() : OsuPacketWriter,
MatchPlayerSkipped(data:number) : OsuPacketWriter,
UserPresence(data:UserPresenceData) : OsuPacketWriter,
Restart(data:number) : OsuPacketWriter,
Invite(data:MessageData) : OsuPacketWriter,
ChannelListingComplete() : OsuPacketWriter,
MatchChangePassword(data:string) : OsuPacketWriter,
BanInfo(data:number) : OsuPacketWriter,
UserSilenced(data:number) : OsuPacketWriter,
UserPresenceSingle(data:number) : OsuPacketWriter,
UserPresenceBundle(data:Array<number>) : OsuPacketWriter,
UserPMBlocked(data:MessageData) : OsuPacketWriter,
TargetIsSilenced(data:MessageData) : OsuPacketWriter,
VersionUpdateForced() : OsuPacketWriter,
SwitchServer(data:number) : OsuPacketWriter,
AccountRestricted() : OsuPacketWriter,
RTX(data:string) : OsuPacketWriter,
SwitchTourneyServer(data:string) : OsuPacketWriter
toBuffer : Buffer
}

View file

@ -0,0 +1,12 @@
import Slot from "../objects/Slot";
import User from "../objects/User";
import ScoreFrameData from "./packetTypes/ScoreFrameData";
export default interface PlayerScore {
player: User,
slot: Slot,
score: number,
isCurrentlyFailed: boolean,
hasFailed: boolean,
_raw?: ScoreFrameData
}

View file

@ -0,0 +1,5 @@
export default interface ChannelData {
channelName: string,
channelTopic: string,
channelUserCount: number
}

View file

@ -0,0 +1,20 @@
import MatchDataSlot from "./MatchDataSlot";
export default interface MatchData {
matchId:number,
matchType:number,
activeMods:number,
gameName:string,
gamePassword:string,
inProgress:boolean,
beatmapName:string,
beatmapId:number,
beatmapChecksum:string,
slots:Array<MatchDataSlot>,
host:number,
playMode:number,
matchScoringType:number,
matchTeamType:number,
specialModes:number,
seed:number
}

View file

@ -0,0 +1,6 @@
export default interface MatchDataSlot {
status:number,
team:number,
playerId:number,
mods:number | undefined,
}

View file

@ -0,0 +1,4 @@
export default interface MatchJoinData {
matchId: number,
gamePassword: string
}

View file

@ -0,0 +1,4 @@
export default interface MatchStartSkipData {
playerId:number,
flag:boolean
}

View file

@ -0,0 +1,6 @@
export default interface MessageData {
sendingClient: string,
message: string,
target: string,
senderId: number
}

View file

@ -0,0 +1,8 @@
export default interface PresenceData {
status: number,
statusText: string,
beatmapId: number,
beatmapChecksum: string,
currentMods: number,
playMode: number,
}

View file

@ -0,0 +1,7 @@
export default interface ReplayFrameData {
buttonState: number,
bt: number,
mouseX: number,
mouseY: number,
time: number
}

View file

@ -0,0 +1,20 @@
export default interface ScoreFrameData {
time: number,
id: number,
count300: number,
count100: number,
count50: number,
countGeki: number,
countKatu: number,
countMiss: number,
totalScore: number,
maxCombo: number,
currentCombo: number,
perfect: boolean,
currentHp: number,
tagByte: number,
usingScoreV2: boolean,
// Only exists if usingScoreV2 = true
comboPortion?: number,
bonusPortion?: number
}

View file

@ -0,0 +1,9 @@
import ReplayFrameData from "./ReplayFrameData";
import ScoreFrameData from "./ScoreFrameData";
export default interface SpectateFramesData {
extra: number,
replayFrames: Array<ReplayFrameData>,
action: number,
scoreFrame: ScoreFrameData
}

View file

@ -0,0 +1,15 @@
export default interface StatusUpdateData {
userId: number,
status: number,
statusText: string,
beatmapChecksum: string,
currentMods: number,
playMode: number,
beatmapId: number,
rankedScore: number,
accuracy: number,
playCount: number,
totalScore: number,
rank: number,
performance: number
}

View file

@ -0,0 +1,12 @@
import { Permissions } from "../../enums/Permissions";
export default interface UserPresenceData {
userId: number,
username: string,
timezone: number,
countryId: number,
permissions: Permissions,
longitude: number,
latitude: number,
rank: number
}

View file

@ -0,0 +1,4 @@
export default interface UserQuitData {
userId: number,
state: number
}

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',
}
];
}

105
server/objects/Channel.ts Normal file
View file

@ -0,0 +1,105 @@
import osu from "../../osuTyping";
import Bot from "../Bot";
import ChatHistory from "../ChatHistory";
import Shared from "../objects/Shared";
import DataStream from "./DataStream";
import User from "./User";
export default class Channel {
public name:string;
public description:string;
public stream:DataStream;
public isLocked:boolean = false;
private _isSpecial:boolean = false;
private readonly bot:Bot;
public constructor(shared:Shared, name:string, description:string, stream:DataStream, isSpecial:boolean = false) {
this.name = name;
this.description = description;
this.stream = stream;
this._isSpecial = isSpecial;
this.bot = shared.bot;
}
public get isSpecial() : boolean {
return this._isSpecial;
}
public get userCount() : number {
return this.stream.userCount;
}
public SendMessage(sender:User, message:string) {
if (!this.isLocked) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: sender.username,
message: message,
target: this.name,
senderId: sender.id
});
this.stream.SendWithExclusion(osuPacketWriter.toBuffer, sender);
if (this.name === "#osu") {
ChatHistory.AddMessage(`${sender.username}: ${message}`);
}
}
if (message[0] === "!") {
this.bot.OnMessage(this, sender, message);
} else if (this.isLocked) {
return this.SendSystemMessage("This channel is currently locked", sender);
}
}
public SendBotMessage(message:string, sendTo?:User) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: this.bot.user.username,
message: message,
target: this.name,
senderId: this.bot.user.id
});
if (sendTo instanceof User) {
sendTo.addActionToQueue(osuPacketWriter.toBuffer);
} else {
this.stream.Send(osuPacketWriter.toBuffer);
}
if (this.name === "#osu") {
ChatHistory.AddMessage(`${this.bot.user.username}: ${message}`);
}
}
public SendSystemMessage(message:string, sendTo?:User) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SendMessage({
sendingClient: "System",
message: message,
target: this.name,
senderId: 1
});
if (sendTo instanceof User) {
sendTo.addActionToQueue(osuPacketWriter.toBuffer);
} else {
this.stream.Send(osuPacketWriter.toBuffer);
}
}
public Join(user:User) {
this.stream.AddUser(user);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.ChannelJoinSuccess(this.name);
user.addActionToQueue(osuPacketWriter.toBuffer);
}
public Leave(user:User) {
this.stream.RemoveUser(user);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.ChannelRevoked(this.name);
user.addActionToQueue(osuPacketWriter.toBuffer);
}
}

View file

@ -0,0 +1,97 @@
import { ConsoleHelper } from "../../ConsoleHelper";
import Constants from "../../Constants";
import DataStreamArray from "./DataStreamArray";
import User from "./User";
import UserArray from "./UserArray";
import { hexlify } from "../Util";
type DeleteFunction = (dataStream:DataStream) => void;
export default class DataStream {
private users:UserArray = new UserArray();
public readonly name:string;
private readonly parent:DataStreamArray;
private readonly removeWhenEmpty:boolean;
private inactive:boolean = false;
public onDelete?:DeleteFunction;
public constructor(name:string, parent:DataStreamArray, removeWhenEmpty:boolean) {
this.name = name;
this.parent = parent;
this.removeWhenEmpty = removeWhenEmpty;
}
public get IsActive() : boolean {
return this.inactive;
}
private checkInactive() {
if (this.inactive) {
throw `Stream ${this.name} is inactive (deleted) and cannot be used here.`;
}
}
public get userCount() : number {
return this.users.getLength();
}
public HasUser(user:User) : boolean {
return this.users.getByKey(user.uuid) !== undefined;
}
public AddUser(user:User) : void {
this.checkInactive();
if (!(user.uuid in this.users.getItems())) {
this.users.add(user.uuid, user);
ConsoleHelper.printStream(`Added [${user.username}] to stream [${this.name}]`);
}
}
public RemoveUser(user:User) : void {
this.checkInactive();
if (user.uuid in this.users.getItems()) {
this.users.remove(user.uuid);
ConsoleHelper.printStream(`Removed [${user.username}] from stream [${this.name}]`);
}
if (this.removeWhenEmpty && this.users.getLength() === 0) {
this.Delete();
}
}
public Delete() {
if (typeof(this.onDelete) === "function") {
this.onDelete(this);
}
this.parent.DeleteStream(this);
}
public Deactivate() {
this.inactive = true;
}
public Send(data:Buffer) {
this.checkInactive();
for (const user of this.users.getIterableItems()) {
user.addActionToQueue(data);
}
if (Constants.DEBUG) {
ConsoleHelper.printStream(`Sent Buffer<${hexlify(data)}> to all users in stream [${this.name}]`);
}
}
public SendWithExclusion(data:Buffer, exclude:User) {
this.checkInactive();
for (const user of this.users.getIterableItems()) {
if (user.uuid !== exclude.uuid) {
user.addActionToQueue(data);
}
}
if (Constants.DEBUG) {
ConsoleHelper.printStream(`Sent Buffer<${hexlify(data)}> to all users in stream [${this.name}] excluding user [${exclude.username}]`);
}
}
}

View file

@ -0,0 +1,33 @@
import { ConsoleHelper } from "../../ConsoleHelper";
import DataStream from "./DataStream";
import FunkyArray from "./FunkyArray";
import User from "./User";
export default class DataStreamArray extends FunkyArray<DataStream> {
public CreateStream(name:string, removeWhenEmpty:boolean = true) : DataStream {
const dataStream:DataStream = this.add(name, new DataStream(name, this, removeWhenEmpty));
ConsoleHelper.printStream(`Created stream [${name}]`);
return dataStream;
}
public DeleteStream(stream:DataStream | string) {
const isObject = stream instanceof DataStream;
if (isObject) {
stream.Deactivate();
this.remove(stream.name);
} else {
const dso = this.getByKey(stream);
if (dso != null) {
dso.Deactivate();
}
this.remove(stream);
}
ConsoleHelper.printStream(`Deleted stream [${isObject ? stream.name : stream}]`);
}
public RemoveUserFromAllStreams(user:User) {
for (const stream of this.getIterableItems()) {
stream.RemoveUser(user);
}
}
}

View file

@ -0,0 +1,97 @@
import { ConsoleHelper } from "../../ConsoleHelper";
import { createPool, Pool, RowDataPacket } from "mysql2";
import { DBInDataType } from "../types/DBTypes";
export default class Database {
private connectionPool:Pool;
private static readonly CONNECTION_LIMIT = 128;
public connected:boolean = false;
public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string) {
this.connectionPool = createPool({
connectionLimit: Database.CONNECTION_LIMIT,
host: databaseAddress,
port: databasePort,
user: databaseUsername,
password: databasePassword,
database: databaseName
});
ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
}
public execute(query:string, data?:Array<DBInDataType>) {
return new Promise<boolean>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
return reject(err);
}
if (data == null) {
connection.execute(query, (err, result) => {
if (err) {
connection.release();
return reject(err);
}
resolve(result !== undefined);
});
} else {
connection.execute(query, data, (err, result) => {
if (err) {
connection.release();
return reject(err);
}
resolve(result !== undefined);
});
}
});
});
}
public query(query:string, data?:Array<DBInDataType>) {
return new Promise<RowDataPacket[]>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
return reject(err);
} else {
// Use old query
if (data == null) {
connection.query<RowDataPacket[]>(query, (err, rows) => {
connection.release();
if (err) {
return reject(err);
}
resolve(rows);
connection.release();
});
}
// Use new prepared statements w/ placeholders
else {
connection.execute<RowDataPacket[]>(query, data, (err, rows) => {
connection.release();
if (err) {
return reject(err);
}
resolve(rows);
connection.release();
});
}
}
});
});
}
public async querySingle(query:string, data?:Array<DBInDataType>) {
const dbData = await this.query(query, data);
if (dbData != null && dbData.length > 0) {
return dbData[0];
}
return null;
}
}

View file

@ -0,0 +1,74 @@
export default class FunkyArray<T> {
private items:{ [id: string]: T } = {};
private itemKeys:Array<string> = Object.keys(this.items);
private iterableArray:Array<T> = new Array<T>();
public add(key:string, item:T, regenerate:boolean = true) : T {
this.items[key] = item;
if (regenerate) {
this.itemKeys = Object.keys(this.items);
this.regenerateIterableArray();
}
return this.items[key];
}
public remove(key:string, regenerate:boolean = true) {
delete this.items[key];
if (regenerate) {
this.itemKeys = Object.keys(this.items);
this.regenerateIterableArray();
}
}
public removeFirstItem(regenerate:boolean = true) : void {
delete this.items[this.itemKeys[0]];
this.itemKeys = Object.keys(this.items);
if (regenerate) this.regenerateIterableArray();
}
public regenerateIterableArray() : void {
this.iterableArray = new Array<T>();
for (const itemKey of this.itemKeys) {
this.iterableArray.push(this.items[itemKey]);
}
this.itemKeys = Object.keys(this.items);
}
public getFirstItem() : T {
return this.items[this.itemKeys[0]];
}
public getLength() : number {
return this.itemKeys.length;
}
public getKeyById(id:number) : string {
return this.itemKeys[id];
}
public getById(id:number) : T | undefined {
return this.items[this.itemKeys[id]];
}
public getByKey(key:string) : T | undefined {
if (key in this.items) {
return this.items[key];
}
return undefined;
}
public getKeys() : Array<string> {
return this.itemKeys;
}
public getItems() : { [id: string]: T } {
return this.items;
}
public getIterableItems() : Array<T> {
return this.iterableArray;
}
}

9
server/objects/LatLng.ts Normal file
View file

@ -0,0 +1,9 @@
export default class LatLng {
public latitude:number;
public longitude:number;
public constructor(latitude:number, longitude:number) {
this.latitude = latitude;
this.longitude = longitude;
}
}

View file

@ -0,0 +1,33 @@
export default class LoginInfo {
public username:string;
public password:string;
public version:string;
public timeOffset:number;
// TODO: Parse client data
public clientData:string;
private constructor(username:string, password:string, version:string, timeOffset:number, clientData:string) {
this.username = username;
this.password = password;
this.version = version;
this.timeOffset = timeOffset;
this.clientData = clientData;
}
public static From(data:Buffer | string) : LoginInfo | undefined {
if (data instanceof Buffer) {
data = data.toString();
}
const loginData:Array<string> = data.split("\n");
const extraData:Array<string> = loginData[2].split("|");
if (loginData.length !== 4 || extraData.length !== 5) {
return undefined;
}
// TODO: Parse client data
return new LoginInfo(loginData[0], loginData[1], extraData[0], parseInt(extraData[1]), extraData[3].split(":")[2]);
}
}

693
server/objects/Match.ts Normal file
View file

@ -0,0 +1,693 @@
import Channel from "./Channel";
import Shared from "../objects/Shared";
import DataStream from "./DataStream";
import Slot from "./Slot";
import User from "./User";
import StatusUpdate from "../packets/StatusUpdate";
import { SlotStatus } from "../enums/SlotStatus";
import MatchData from "../interfaces/packetTypes/MatchData";
import { Team } from "../enums/Team";
import MatchStartSkipData from "../interfaces/packetTypes/MatchStartSkipData";
import { Mods } from "../enums/Mods";
import PlayerScore from "../interfaces/PlayerScore";
import { enumHasFlag } from "../Util";
import osu from "../../osuTyping";
import ScoreFrameData from "../interfaces/packetTypes/ScoreFrameData";
// Mods which need to be applied to the match during freemod.
const matchFreemodGlobalMods:Array<Mods> = [
Mods.DoubleTime, Mods.Nightcore, Mods.HalfTime
]
export default class Match {
// osu! Data
public matchId:number = -1;
public inProgress:boolean = false;
public matchType:number = 0;
public activeMods:number = 0;
public gameName:string = "";
public gamePassword?:string;
public beatmapName:string = '';
public beatmapId:number = 0;
public beatmapChecksum:string = '';
public slots:Array<Slot> = new Array<Slot>();
public host:User;
public playMode:number = 0;
public matchScoringType:number = 0;
public matchTeamType:number = 0;
public specialModes:number = 0;
public seed:number = 0;
// Binato data
public roundId:number = 0;
public matchStartCountdownActive:boolean = false;
public matchStream:DataStream;
public matchChatChannel:Channel;
public matchLoadSlots?:Array<MatchStartSkipData>;
public matchSkippedSlots?:Array<MatchStartSkipData>;
public playerScores?:Array<PlayerScore>;
public countdownTime:number = 0;
public countdownTimer?:NodeJS.Timeout;
private serialisedMatchJSON:MatchData;
private readonly shared:Shared;
private constructor(matchData:MatchData, shared:Shared) {
this.shared = shared;
this.matchId = matchData.matchId;
this.inProgress = matchData.inProgress;
this.matchType = matchData.matchType;
this.activeMods = matchData.activeMods;
this.gameName = matchData.gameName;
if (matchData.gamePassword == '') matchData.gamePassword == null;
this.gamePassword = matchData.gamePassword;
this.beatmapName = matchData.beatmapName;
this.beatmapId = matchData.beatmapId;
this.beatmapChecksum = matchData.beatmapChecksum;
for (let i = 0; i < matchData.slots.length; i++) {
const slot = matchData.slots[i];
if (slot.playerId === -1) {
this.slots.push(new Slot(i, slot.status, slot.team, undefined, slot.mods));
} else {
this.slots.push(new Slot(i, slot.status, slot.team, shared.users.getById(slot.playerId), slot.mods));
}
}
const hostUser = shared.users.getById(matchData.host);
if (hostUser === undefined) {
// NOTE: This should never be possible to hit
// since this user JUST made the match.
throw "Host User of match was undefined";
}
this.host = hostUser;
this.playMode = matchData.playMode;
this.matchScoringType = matchData.matchScoringType;
this.matchTeamType = matchData.matchTeamType;
this.specialModes = matchData.specialModes;
this.seed = matchData.seed;
this.matchStream = shared.streams.CreateStream(`multiplayer:match_${this.matchId}`, false);
this.matchChatChannel = shared.chatManager.AddSpecialChatChannel("multiplayer", `mp_${this.matchId}`);
this.serialisedMatchJSON = matchData;
//this.multiplayerExtras = null;
//this.isTourneyMatch = false;
//this.tourneyClientUsers = [];
}
public static createMatch(matchHost:User, matchData:MatchData, shared:Shared) : Promise<Match> {
return new Promise<Match>(async (resolve, reject) => {
try {
matchData.matchId = (await shared.database.query(
"INSERT INTO mp_matches (id, name, open_time, close_time, seed) VALUES (NULL, ?, UNIX_TIMESTAMP(), NULL, ?) RETURNING id;",
[matchData.gameName, matchData.seed]
))[0]["id"];
const matchInstance = new Match(matchData, shared);
// Update the status of the current user
StatusUpdate(matchHost, matchHost.id);
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchNew(matchInstance.serialiseMatch());
matchHost.addActionToQueue(osuPacketWriter.toBuffer);
shared.multiplayerManager.UpdateLobbyListing();
resolve(matchInstance);
} catch (e) {
reject(e);
}
});
}
// Convert class data back to a format that osu-packet can understand
public serialiseMatch() : MatchData {
const matchDataRef = this.serialisedMatchJSON;
matchDataRef.matchId = this.matchId;
matchDataRef.matchType = this.matchType;
matchDataRef.activeMods = this.activeMods;
matchDataRef.gameName = this.gameName;
matchDataRef.gamePassword = this.gamePassword ?? "";
matchDataRef.inProgress = this.inProgress;
matchDataRef.beatmapName = this.beatmapName;
matchDataRef.beatmapId = this.beatmapId;
matchDataRef.beatmapChecksum = this.beatmapChecksum;
matchDataRef.host = this.host.id;
matchDataRef.playMode = this.playMode;
matchDataRef.matchScoringType = this.matchScoringType;
matchDataRef.matchTeamType = this.matchTeamType;
matchDataRef.specialModes = this.specialModes;
matchDataRef.seed = this.seed;
for (let i = 0; i < this.slots.length; i++) {
const slot = this.slots[i];
const osuSlot = this.serialisedMatchJSON.slots[i];
osuSlot.status = slot.status;
osuSlot.team = slot.team;
osuSlot.mods = slot.mods;
if (slot.player instanceof User) {
osuSlot.playerId = slot.player.id;
} else {
osuSlot.playerId = -1;
}
}
return this.serialisedMatchJSON;
}
public leaveMatch(user:User) {
// Make sure this leave call is valid
if (!user.inMatch || user.matchSlot === undefined) {
return;
}
// Set the slot's status to avaliable
user.matchSlot.status = SlotStatus.Empty;
user.matchSlot.team = 0;
user.matchSlot.player = undefined;
user.matchSlot.mods = 0;
// Remove the leaving user from the match's stream
this.matchStream.RemoveUser(user);
this.matchChatChannel.Leave(user);
// Send this after removing the user from match streams to avoid a leave notification for self?
this.sendMatchUpdate();
}
public async updateMatch(user:User, matchData: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 === "") {
this.gamePassword = undefined;
} else {
this.gamePassword = matchData.gamePassword;
}
this.beatmapName = matchData.beatmapName;
this.beatmapId = matchData.beatmapId;
this.beatmapChecksum = matchData.beatmapChecksum;
if (matchData.host !== this.host.id) {
const hostUser = this.shared.users.getById(matchData.host);
if (hostUser === undefined) {
// NOTE: This should never be possible to hit
throw "Host User of match was undefined";
}
this.host = hostUser;
}
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 this.shared.database.execute(
`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
this.shared.multiplayerManager.UpdateLobbyListing();
}
public sendMatchUpdate() {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchUpdate(this.serialiseMatch());
// Update all users in the match with new match information
this.matchStream.Send(osuPacketWriter.toBuffer);
}
public moveToSlot(user:User, slotToMoveTo:number) {
if (slotToMoveTo < 0 || slotToMoveTo >= this.slots.length) {
return;
}
const newSlot = this.slots[slotToMoveTo];
if (newSlot.status === SlotStatus.Locked || !(user.matchSlot instanceof Slot)) {
return;
}
user.matchSlot = newSlot.transferFrom(user.matchSlot);
this.sendMatchUpdate();
this.shared.multiplayerManager.UpdateLobbyListing();
}
public changeTeam(user:User) {
if (!(user.matchSlot instanceof Slot)) {
return;
}
user.matchSlot.team = user.matchSlot.team === Team.Red ? Team.Blue : Team.Red;
this.sendMatchUpdate();
}
public setStateReady(user:User) {
if (!(user.matchSlot instanceof Slot)) {
return;
}
user.matchSlot.status = SlotStatus.Ready;
this.sendMatchUpdate();
}
public setStateNotReady(user:User) {
if (!(user.matchSlot instanceof Slot)) {
return;
}
user.matchSlot.status = SlotStatus.NotReady;
this.sendMatchUpdate();
}
public lockOrKick(user:User, slotToActionOn:number) {
if (slotToActionOn < 0 || slotToActionOn >= 16) {
return;
}
// Make sure the user attempting to kick / lock is the host of the match
if (!User.Equals(user, this.host)) {
return;
}
const slot = this.slots[slotToActionOn];
if (slot.player instanceof User) { // Kick
const kickedPlayer = slot.player;
// Remove player's refs to the match & slot
kickedPlayer.match = undefined;
kickedPlayer.matchSlot = undefined;
// Nuke all slot properties
slot.reset();
// Kick player
this.shared.multiplayerManager.LeaveMatch(kickedPlayer);
this.sendMatchUpdate();
} else { // Lock / Unlock
slot.status = slot.status === SlotStatus.Empty ? SlotStatus.Locked : SlotStatus.Empty;
this.sendMatchUpdate();
}
this.shared.multiplayerManager.UpdateLobbyListing();
}
public missingBeatmap(user:User) {
const slot = user.matchSlot;
if (!(slot instanceof Slot)) {
return;
}
slot.status = SlotStatus.MissingBeatmap;
this.sendMatchUpdate();
}
public notMissingBeatmap(user:User) {
const slot = user.matchSlot;
if (!(slot instanceof Slot)) {
return;
}
slot.status = SlotStatus.NotReady;
this.sendMatchUpdate();
}
public matchSkip(user:User) {
if (this.matchSkippedSlots === undefined) {
this.matchSkippedSlots = new Array<MatchStartSkipData>();
for (const slot of this.slots) {
// Make sure the slot has a user in it
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
continue;
}
// Add the slot's user to the loaded checking array
this.matchSkippedSlots.push({
playerId: slot.player?.id,
flag: false
});
}
}
let allSkipped = true;
for (const skippedSlot of this.matchSkippedSlots) {
// If loadslot belongs to this user then set loaded to true
if (skippedSlot.playerId === user.id) {
skippedSlot.flag = true;
}
if (skippedSlot.flag) continue;
// A user hasn't skipped
allSkipped = false;
}
const slotId = user.matchSlot?.slotId ?? Number.MIN_VALUE;
if (slotId === Number.MIN_VALUE) {
return;
}
// All players have finished playing, finish the match
if (allSkipped) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchPlayerSkipped(slotId);
osuPacketWriter.MatchSkip();
this.matchStream.Send(osuPacketWriter.toBuffer);
this.matchSkippedSlots = undefined;
} else {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchPlayerSkipped(slotId);
this.matchStream.Send(osuPacketWriter.toBuffer);
}
}
public transferHost(user:User, slotIDToTransferTo:number) {
// Set the lobby's host to the new user
const newHost = this.slots[slotIDToTransferTo].player;
if (newHost instanceof User) {
this.host = newHost;
this.sendMatchUpdate();
}
}
public updateMods(user:User, mods:Mods) {
const slot = user.matchSlot;
if (!(slot instanceof Slot)) {
return;
}
// Check if freemod is enabled or not
if (this.specialModes === 1) {
slot.mods = mods;
// Extra check for host during freemod
if (User.Equals(this.host, user)) {
let generatedMatchModList = 0;
for (const mod of matchFreemodGlobalMods) {
if (enumHasFlag(slot.mods, mod)) {
slot.mods -= mod;
generatedMatchModList += mod;
}
}
this.activeMods = generatedMatchModList;
}
this.sendMatchUpdate();
} else {
if (!User.Equals(this.host, user)) {
return;
}
this.activeMods = mods;
this.sendMatchUpdate();
}
this.shared.multiplayerManager.UpdateLobbyListing();
}
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;
this.matchLoadSlots = new Array<MatchStartSkipData>();
// Loop through all slots in the match
for (const slot of this.slots) {
// Make sure the slot has a user in it
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
continue;
}
// Add the slot's user to the loaded checking array
this.matchLoadSlots.push({
playerId: slot.player?.id,
flag: false
});
// Set the user's status to playing
slot.status = 32;
}
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchStart(this.serialiseMatch());
// Inform all users in the match that it has started
this.matchStream.Send(osuPacketWriter.toBuffer);
// Update all users in the match with new info
this.sendMatchUpdate();
// Update match listing in lobby to show the game is in progress
this.shared.multiplayerManager.UpdateLobbyListing();
}
public matchPlayerLoaded(user:User) {
if (this.matchLoadSlots === undefined) {
return;
}
let allLoaded = true;
for (const loadedSlot of this.matchLoadSlots) {
if (loadedSlot.playerId === user.id) {
loadedSlot.flag = true;
}
if (loadedSlot.flag) continue;
allLoaded = false;
}
// All players have loaded the beatmap, start playing.
if (allLoaded) {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchAllPlayersLoaded();
this.matchStream.Send(osuPacketWriter.toBuffer);
// Blank out user loading array
this.matchLoadSlots = undefined;
this.playerScores = new Array<PlayerScore>();
for (const slot of this.slots) {
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
continue;
}
this.playerScores.push({
player: slot.player,
slot: slot,
score: 0,
isCurrentlyFailed: false,
hasFailed: false,
_raw: undefined,
});
}
}
}
public async onPlayerFinishMatch(user:User) {
if (this.matchLoadSlots === undefined) {
// Repopulate user loading slots again
this.matchLoadSlots = [];
for (const slot of this.slots) {
// Make sure the slot has a user
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
continue;
}
// Populate user loading slots with this user's id and load status
this.matchLoadSlots.push({
playerId: slot.player?.id,
flag: false
});
}
}
let allLoaded = true;
for (const loadedSlot of this.matchLoadSlots) {
if (loadedSlot.playerId == user.id) {
loadedSlot.flag = true;
}
if (loadedSlot.flag) continue;
// A user hasn't finished playing
allLoaded = false;
}
// All players have finished playing, finish the match
if (allLoaded) {
await this.finishMatch();
}
}
public async finishMatch() {
if (!this.inProgress) {
return;
}
this.matchLoadSlots = undefined;
this.inProgress = false;
const osuPacketWriter = osu.Bancho.Writer();
const queryData:Array<string | number | null> = [
this.matchId,
this.roundId++,
this.playMode,
this.matchType,
this.matchScoringType,
this.matchTeamType,
this.activeMods,
this.beatmapChecksum,
(this.specialModes === 1) ? 1 : 0
];
if (this.playerScores === undefined) {
throw "playerScores was null in a place it really shouldn't have been!";
}
for (const slot of this.slots) {
// For every empty / locked slot push a null to the data array
if (slot.player === undefined || slot.status === SlotStatus.Empty || slot.status === SlotStatus.Locked) {
queryData.push(null);
continue;
}
for (const _playerScore of this.playerScores) {
if (_playerScore.player?.id === slot.player?.id && _playerScore._raw !== undefined) {
const score = _playerScore._raw;
queryData.push(`${slot.player?.id}|${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}` : ""}`);
break;
}
}
slot.status = SlotStatus.NotReady;
}
await this.shared.database.execute("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);
osuPacketWriter.MatchComplete();
// Inform all users in the match that it is complete
this.matchStream.Send(osuPacketWriter.toBuffer);
// Update all users in the match with new info
this.sendMatchUpdate();
this.shared.multiplayerManager.UpdateLobbyListing();
// TODO: Re-implement multiplayer extras
//if (this.multiplayerExtras != null) this.multiplayerExtras.onMatchFinished(JSON.parse(JSON.stringify(this.playerScores)));
this.playerScores = undefined;
}
updatePlayerScore(user:User, scoreFrameData:ScoreFrameData) {
const osuPacketWriter = osu.Bancho.Writer();
if (user.matchSlot === undefined || user.matchSlot.player === undefined || this.playerScores === undefined) {
return;
}
scoreFrameData.id = user.matchSlot.slotId;
// Update playerScores
for (const playerScore of this.playerScores) {
if (playerScore.player?.id === user.id) {
playerScore.score = scoreFrameData.totalScore;
const isCurrentlyFailed = scoreFrameData.currentHp == 254;
playerScore.isCurrentlyFailed = isCurrentlyFailed;
if (!playerScore.hasFailed && isCurrentlyFailed) {
playerScore.hasFailed = true;
}
playerScore._raw = scoreFrameData;
break;
}
}
osuPacketWriter.MatchScoreUpdate(scoreFrameData);
// Send the newly updated score to all users in the match
this.matchStream.Send(osuPacketWriter.toBuffer);
}
matchFailed(user:User) {
const osuPacketWriter = osu.Bancho.Writer();
// Make sure the user is in the match in a valid slot
if (user.matchSlot === undefined) {
return;
}
osuPacketWriter.MatchPlayerFailed(user.id);
this.matchStream.Send(osuPacketWriter.toBuffer);
}
}

View file

@ -0,0 +1,14 @@
import FunkyArray from "./FunkyArray";
import Match from "./Match";
export default class MatchArray extends FunkyArray<Match> {
public getById(id:number) : Match | undefined {
for (const match of this.getIterableItems()) {
if (match.matchId === id) {
return match;
}
}
return undefined;
}
}

View file

@ -0,0 +1,61 @@
import osu from "../../osuTyping";
import Shared from "../objects/Shared";
import Channel from "./Channel";
import DataStream from "./DataStream";
import User from "./User";
export default class PrivateChannel extends Channel {
private readonly user0:User;
private readonly user1:User;
public constructor(user0:User, user1:User, stream:DataStream) {
super(user0.shared, `${user0.username}${user1.username}`, "", stream);
this.user0 = user0;
this.user1 = user1;
}
public override SendMessage(sender:User, message:string) {
const osuPacketWriter = osu.Bancho.Writer();
if (!this.stream.HasUser(this.user0)) {
this.Join(this.user0);
}
if (!this.stream.HasUser(this.user1)) {
this.Join(this.user1);
}
let target:string = this.user1.username;
if (sender.uuid === this.user1.uuid) {
target = this.user0.username;
}
osuPacketWriter.SendMessage({
sendingClient: sender.username,
message: message,
target: target,
senderId: sender.id
});
this.stream.SendWithExclusion(osuPacketWriter.toBuffer, sender);
}
public override Join(user:User) {
this.stream.AddUser(user);
const osuPacketWriter = osu.Bancho.Writer();
if (user.uuid === this.user0.uuid) {
osuPacketWriter.ChannelJoinSuccess(this.user1.username);
} else if (user.uuid === this.user1.uuid) {
osuPacketWriter.ChannelJoinSuccess(this.user0.username);
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
public override Leave(user:User) {
this.stream.RemoveUser(user);
const osuPacketWriter = osu.Bancho.Writer();
if (user.id === this.user0.id) {
osuPacketWriter.ChannelRevoked(this.user1.username);
} else if (user.id === this.user1.id) {
osuPacketWriter.ChannelRevoked(this.user0.username);
}
user.addActionToQueue(osuPacketWriter.toBuffer);
}
}

60
server/objects/Shared.ts Normal file
View file

@ -0,0 +1,60 @@
import ChatManager from "../ChatManager";
import Config from "../interfaces/Config";
import Database from "../objects/Database";
import DataStreamArray from "../objects/DataStreamArray";
import MultiplayerManager from "../MultiplayerManager";
import PrivateChatManager from "../PrivateChatManager";
import { existsSync, readFileSync } from "fs";
import UserArray from "../objects/UserArray";
import User from "./User";
import LatLng from "./LatLng";
import Bot from "../Bot";
import { ConsoleHelper } from "../../ConsoleHelper";
import UserInfoRepository from "../repos/UserInfoRepository";
import { Permissions } from "../enums/Permissions";
import UserModesInfoRepository from "../repos/UserModesInfoRepository";
export default class Shared {
public readonly chatManager:ChatManager;
public readonly config:Config;
public readonly database:Database;
public readonly multiplayerManager:MultiplayerManager;
public readonly privateChatManager:PrivateChatManager;
public readonly streams:DataStreamArray;
public readonly users:UserArray;
public readonly bot:Bot;
public readonly userInfoRepository:UserInfoRepository;
public readonly userModesInfoRepository:UserModesInfoRepository;
public constructor() {
if (!existsSync("./config.json")) {
ConsoleHelper.printError("Config file missing!");
ConsoleHelper.printError("Check the GitHub for an example or create one with the example you have.");
process.exit(1);
}
this.config = JSON.parse(readFileSync("./config.json").toString()) as Config;
this.database = new Database(this.config.database.address, this.config.database.port, this.config.database.username, this.config.database.password, this.config.database.name);
this.streams = new DataStreamArray();
// Add the bot user
this.users = new UserArray();
const botUser = this.users.add("bot", new User(3, "SillyBot", "bot", Permissions.None, this));
botUser.location = new LatLng(50, -32);
this.bot = new Bot(this, botUser);
this.chatManager = new ChatManager(this);
// Setup chat channels
this.chatManager.AddChatChannel("osu", "The main channel", true);
this.chatManager.AddChatChannel("lobby", "Talk about multiplayer stuff");
this.chatManager.AddChatChannel("english", "Talk in exclusively English");
this.chatManager.AddChatChannel("japanese", "Talk in exclusively Japanese");
this.multiplayerManager = new MultiplayerManager(this);
this.privateChatManager = new PrivateChatManager(this);
// DB Repos
this.userInfoRepository = new UserInfoRepository(this);
this.userModesInfoRepository = new UserModesInfoRepository(this);
}
}

48
server/objects/Slot.ts Normal file
View file

@ -0,0 +1,48 @@
import { Mods } from "../enums/Mods";
import { SlotStatus } from "../enums/SlotStatus";
import User from "./User";
export default class Slot {
public readonly slotId:number;
public status:SlotStatus;
public team:number;
public player?:User;
public mods:Mods;
public constructor(slotId:number, status:SlotStatus, team:number, player?:User, mods:Mods = Mods.None) {
this.slotId = slotId;
this.status = status;
this.team = team;
this.player = player;
this.mods = mods;
}
public transferFrom(slot:Slot) : Slot {
this.status = slot.status;
this.team = slot.team;
this.player = slot.player;
this.mods = slot.mods;
slot.reset();
return this;
}
public transferTo(slot:Slot) : Slot {
slot.status = this.status;
slot.team = this.team;
slot.player = this.player;
slot.mods = this.mods;
this.reset();
return slot;
}
public reset() : Slot {
this.status = SlotStatus.Empty;
this.team = 0;
this.player = undefined;
this.mods = Mods.None;
return this;
}
}

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

@ -0,0 +1,151 @@
import LatLng from "./LatLng";
import { RankingMode } from "../enums/RankingMode";
import Match from "./Match";
import DataStream from "./DataStream";
import StatusUpdate from "../packets/StatusUpdate";
import Shared from "../objects/Shared";
import Slot from "./Slot";
import Channel from "./Channel";
import PresenceData from "../interfaces/packetTypes/PresenceData";
import { Permissions } from "../enums/Permissions";
export default class User {
public shared:Shared;
public id:number;
public username:string;
public uuid:string;
public readonly connectTime:number = Date.now();
public timeoutTime:number = Date.now() + 30000;
public queue:Buffer = Buffer.allocUnsafe(0);
// Binato data
public rankingMode:RankingMode = RankingMode.PP;
public spectatorStream?:DataStream;
public spectatingUser?:User;
public permissions:Permissions;
// osu! data
public playMode:number = 0;
public countryID:number = 0;
public location:LatLng = new LatLng(0, 0);
public joinedChannels:Array<string> = new Array<string>();
// Presence data
public actionID:number = 0;
public actionText:string = "";
public actionMods:number = 0;
public beatmapChecksum:string = "";
public beatmapID:number = 0;
public currentMods:number = 0;
// Cached db data
public rankedScore:number = 0;
public accuracy:number = 0;
public playCount:number = 0;
public totalScore:number = 0;
public rank:number = 0;
public pp:number = 0;
// Multiplayer data
public match?:Match;
public matchSlot?:Slot;
public get inMatch() {
return this.match instanceof Match;
}
// Tournament client flag
public isTourneyUser:boolean = false;
static Equals(user0:User, user1:User) {
return user0.uuid === user1.uuid;
}
public constructor(id:number, username:string, uuid:string, permissions:Permissions, shared:Shared) {
this.id = id;
this.username = username;
this.uuid = uuid;
this.permissions = permissions;
this.shared = shared;
}
// Concats new actions to the user's queue
public addActionToQueue(newData:Buffer) {
this.queue = Buffer.concat([this.queue, newData], this.queue.length + newData.length);
}
clearQueue() {
this.queue = Buffer.allocUnsafe(0);
}
// Updates the user's current action
updatePresence(action:PresenceData) {
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:boolean = false) {
const userScoreDB = await this.shared.userModesInfoRepository.selectByUserIdModeId(this.id, this.playMode);
const userRank = await this.shared.userModesInfoRepository.selectRankByIdModeIdRankingMode(this.id, this.playMode, this.rankingMode);
if (userScoreDB == null || userRank == null) throw "fuck";
this.rank = userRank;
// Handle "if we should update" checks for each rankingMode
let userScoreUpdate = false;
switch (this.rankingMode) {
case RankingMode.PP:
if (this.pp != userScoreDB.pp_raw)
userScoreUpdate = true;
break;
case RankingMode.RANKED_SCORE:
if (this.rankedScore != userScoreDB.ranked_score)
userScoreUpdate = true;
break;
case RankingMode.AVG_ACCURACY:
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;
// 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);
}
}
joinChannel(channelName:string) {
const channel = this.shared.chatManager.GetChannelByName(channelName);
if (channel instanceof Channel) {
channel.Join(this);
}
}
leaveChannel(channelName:string) {
const channel = this.shared.chatManager.GetChannelByName(channelName);
if (channel instanceof Channel) {
channel.Leave(this);
}
}
}

View file

@ -0,0 +1,26 @@
import FunkyArray from "./FunkyArray";
import User from "./User";
export default class UserArray extends FunkyArray<User> {
public getById(id:number) : User | undefined {
for (const user of this.getIterableItems()) {
if (user.id == id)
return user;
}
return undefined;
}
public getByUsername(username:string) : User | undefined {
for (const user of this.getIterableItems()) {
if (user.username === username)
return user;
}
return undefined;
}
public getByToken(token:string) : User | undefined {
return this.getByKey(token);
}
}

View file

@ -0,0 +1,25 @@
import { Permissions } from "../../enums/Permissions";
export default class UserInfo {
id:number = Number.MIN_VALUE;
username:string = "";
username_safe:string = "";
password_hash:string = "";
password_salt:string = "";
email:string = "";
country:string = "";
reg_date:Date = new Date(0);
last_login_date:Date = new Date(0);
last_played_mode:number = Number.MIN_VALUE;
online_now:boolean = false;
tags:Permissions = Permissions.None;
supporter:boolean = false;
web_session:string = "";
verification_needed:boolean = false;
password_change_required:boolean = false;
has_old_password:number = Number.MIN_VALUE;
password_reset_key:string = "";
away_message:string = "";
last_modified_time:Date = new Date(0);
is_deleted:boolean = false;
}

View file

@ -0,0 +1,24 @@
import { Mode } from "../../enums/Mode";
export default class UserModeInfo {
n:number = Number.MIN_VALUE;
user_id:number = Number.MIN_VALUE;
mode_id:Mode = Mode.Unknown;
count300:number = Number.MIN_VALUE;
count100:number = Number.MIN_VALUE;
count50:number = Number.MIN_VALUE;
countmiss:number = Number.MIN_VALUE;
playcount:number = Number.MIN_VALUE;
total_score:number = Number.MIN_VALUE;
ranked_score:number = Number.MIN_VALUE;
pp_rank:number = Number.MIN_VALUE;
pp_raw:number = Number.MIN_VALUE;
count_rank_ss:number = Number.MIN_VALUE;
count_rank_s:number = Number.MIN_VALUE;
count_rank_a:number = Number.MIN_VALUE;
pp_country_rank:number = Number.MIN_VALUE;
playtime:number = Number.MIN_VALUE;
avg_accuracy:number = Number.MIN_VALUE;
level:number = Number.MIN_VALUE;
is_deleted:boolean = false;
}

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
}

Some files were not shown because too many files have changed in this diff Show more