Compare commits
54 commits
legacy-jav
...
master
Author | SHA1 | Date | |
---|---|---|---|
b40361caf0 | |||
640c2cdf92 | |||
9d53d82997 | |||
877592bb94 | |||
2fbdb9799a | |||
a32ab80f73 | |||
4ec4cb1c1f | |||
5e1106e488 | |||
39f6669f94 | |||
056260ad55 | |||
78f4a499fa | |||
462d0c879c | |||
108f27eb22 | |||
8ab318ef12 | |||
4b90031294 | |||
686e6001b2 | |||
93da399fa5 | |||
04bd1e42bb | |||
25105537ea | |||
c3b24d32af | |||
4a6d698b47 | |||
3070f6a742 | |||
9a2bfabee6 | |||
50900e333e | |||
06b632af4a | |||
a00aba5825 | |||
469cbb9bc9 | |||
aff53f1ab9 | |||
f66c867d17 | |||
09dc1ffb76 | |||
0d68b07e9c | |||
91ebbd289b | |||
ba91cb6cdb | |||
726e490fe2 | |||
3a07a892e1 | |||
148c2c341f | |||
b0f4423633 | |||
734cebb19e | |||
1a871e4c35 | |||
92d4f70af4 | |||
b6a0d5e4b1 | |||
1907e9910d | |||
9f6339ce48 | |||
bb6d86ebbd | |||
f08e34dc82 | |||
be52b19002 | |||
e297aa1128 | |||
f7f2df1287 | |||
3da964f5d6 | |||
a09543b2fb | |||
2beeb5fd09 | |||
5ed106b7d4 | |||
53a12461ce | |||
4ebf9ee0e6 |
134 changed files with 7516 additions and 4398 deletions
22
.github/workflows/node.js.yml
vendored
22
.github/workflows/node.js.yml
vendored
|
@ -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
|
# 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://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
name: Node.js CI
|
name: Node.js CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ "master" ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -16,14 +16,18 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [10.x]
|
node-version: [18.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: npm install
|
cache: 'npm'
|
||||||
- run: npm ci
|
- 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
2
.gitignore
vendored
|
@ -1,4 +1,6 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
|
build/
|
||||||
|
combined.ts
|
||||||
tHMM.ds
|
tHMM.ds
|
||||||
server-stats.log
|
server-stats.log
|
||||||
config.json
|
config.json
|
79
Binato.js
79
Binato.js
|
@ -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
59
Binato.ts
Normal 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
75
ConsoleHelper.ts
Normal 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
4
Constants.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default abstract class Constants {
|
||||||
|
public static readonly DEBUG = false;
|
||||||
|
public static readonly PROTOCOL_VERSION = 19;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
30
README.md
30
README.md
|
@ -1,5 +1,5 @@
|
||||||
# Binato [![CodeFactor](https://www.codefactor.io/repository/github/tgpholly/binato/badge)](https://www.codefactor.io/repository/github/tgpholly/binato)
|
# 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 Javascript
|
An implementation of osu!bancho in TypeScript
|
||||||
|
|
||||||
i'm sorry peppy
|
i'm sorry peppy
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -12,8 +12,7 @@ i'm sorry peppy
|
||||||
- Friends List
|
- Friends List
|
||||||
- Chat & Channels
|
- Chat & Channels
|
||||||
- Private Messages
|
- Private Messages
|
||||||
- Minimum Viable Product of a bot
|
- Chat Bot (see [commands folder](https://github.com/tgpholly/Binato/tree/master/server/commands))
|
||||||
- For a command list check [BotCommandHandler](https://github.com/tgpholly/Binato/blob/master/server/BotCommandHandler.js) or use !help on a live server
|
|
||||||
|
|
||||||
### [Planned additions](https://github.com/tgpholly/Binato/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement) | [List of currently known bugs](https://github.com/tgpethan/Binato/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
|
### [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:
|
## Setup:
|
||||||
While I don't support setting this up yourself it is fairly easy to do, all that should be required is:
|
While I don't support setting this up yourself it is fairly easy to do, all that should be required is:
|
||||||
- **NodeJS >= 10**
|
- **NodeJS > 18**
|
||||||
- **MariaDB or MySQL** (MariaDB is prefered as that is what this is tested and ran against in prod)
|
- **MariaDB**
|
||||||
- Optional (Disabled via config):
|
- Optional (Disabled via config):
|
||||||
- **Redis**
|
- **Redis**
|
||||||
- **Prometheus**
|
- **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)
|
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:
|
## 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.
|
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>
|
<hr>
|
||||||
|
|
||||||
## How to connect:
|
## How to connect:
|
||||||
|
See <ins>Now (2022 - 2023)</ins> for the prefered way to connect now.
|
||||||
|
|
||||||
### 2013 - Stable Fallback (2015 / 2016 ?):
|
### 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
|
You can do this using the hosts file
|
||||||
|
|
||||||
Location on Linux: /etc/hosts<br>
|
Location on Linux: `/etc/hosts`<br>
|
||||||
Location on Mac: /private/etc/hosts<br>
|
Location on Mac: `/private/etc/hosts`<br>
|
||||||
Location on Windows: C:/Windows/system32/drivers/etc/hosts
|
Location on Windows: `C:\Windows\system32\drivers\etc\hosts`
|
||||||
|
|
||||||
Add an entry in the hosts file that looks like the following:
|
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
|
- c6.ppy.sh
|
||||||
- ce.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:
|
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.
|
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)
|
- osu.example.com (Score submit & web stuff)
|
||||||
- c.example.com (Bancho)
|
- c.example.com (Bancho)
|
||||||
- a.example.com (Profile pictures)
|
- 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>
|
<hr>
|
||||||
|
|
||||||
## Other Binato components:
|
## Other Binato components:
|
||||||
|
|
|
@ -1,29 +1,28 @@
|
||||||
{
|
{
|
||||||
"express": {
|
"http": {
|
||||||
"port": 5001,
|
"port": 5001,
|
||||||
"compression": true
|
"compression": false
|
||||||
},
|
},
|
||||||
"prometheus": {
|
"prometheus": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"port": 9100
|
"port": 9103
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"address": "127.0.0.1",
|
"address": "127.0.0.1",
|
||||||
"port": 6379,
|
"port": 6379,
|
||||||
"password": "",
|
"database": 0,
|
||||||
"database": 0
|
"password": ""
|
||||||
},
|
},
|
||||||
"database": {
|
"database": {
|
||||||
"address": "127.0.0.1",
|
"address": "127.0.0.1",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"username": "username",
|
"username": "user",
|
||||||
"password": "password",
|
"password": "password",
|
||||||
"name": "osu!",
|
"name": "osu!",
|
||||||
"pbkdf2": {
|
"pbkdf2": {
|
||||||
"itterations": 1337,
|
"itterations": 1000,
|
||||||
"keylength": 1337
|
"keylength": 64
|
||||||
},
|
}
|
||||||
"key": "examplekey"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
782
osu!.sql
|
@ -1,35 +1,241 @@
|
||||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
||||||
|
START TRANSACTION;
|
||||||
SET time_zone = "+00:00";
|
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` (
|
CREATE TABLE `friends` (
|
||||||
`user` int(11) NOT NULL,
|
`user` int(11) NOT NULL,
|
||||||
`friendsWith` 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 '',
|
-- Table structure for table `mp_matches`
|
||||||
`username` varchar(30) NOT NULL DEFAULT '',
|
--
|
||||||
`score` bigint(20) NOT NULL,
|
|
||||||
`max_combo` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`full_combo` tinyint(1) NOT NULL DEFAULT '0',
|
|
||||||
`mods` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`300_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`100_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`50_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`katus_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`gekis_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`misses_count` int(11) NOT NULL DEFAULT '0',
|
|
||||||
`time` varchar(18) NOT NULL DEFAULT '',
|
|
||||||
`play_mode` tinyint(4) NOT NULL DEFAULT '0',
|
|
||||||
`completed` tinyint(11) NOT NULL DEFAULT '0',
|
|
||||||
`accuracy` float(15,12) DEFAULT NULL,
|
|
||||||
`pp` float NOT NULL DEFAULT '0'
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
|
||||||
|
|
||||||
CREATE TABLE `mp_matches` (
|
CREATE TABLE `mp_matches` (
|
||||||
`id` int(10) UNSIGNED NOT NULL,
|
`id` int(10) UNSIGNED NOT NULL,
|
||||||
|
@ -37,7 +243,13 @@ CREATE TABLE `mp_matches` (
|
||||||
`open_time` varchar(18) NOT NULL,
|
`open_time` varchar(18) NOT NULL,
|
||||||
`close_time` varchar(18) DEFAULT NULL,
|
`close_time` varchar(18) DEFAULT NULL,
|
||||||
`seed` int(11) NOT 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` (
|
CREATE TABLE `mp_match_rounds` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
|
@ -66,7 +278,145 @@ CREATE TABLE `mp_match_rounds` (
|
||||||
`player13` tinytext DEFAULT NULL,
|
`player13` tinytext DEFAULT NULL,
|
||||||
`player14` tinytext DEFAULT NULL,
|
`player14` tinytext DEFAULT NULL,
|
||||||
`player15` 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` (
|
CREATE TABLE `users_info` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
|
@ -83,12 +433,20 @@ CREATE TABLE `users_info` (
|
||||||
`tags` int(11) NOT NULL,
|
`tags` int(11) NOT NULL,
|
||||||
`supporter` tinyint(1) NOT NULL,
|
`supporter` tinyint(1) NOT NULL,
|
||||||
`web_session` varchar(64) 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,
|
`password_change_required` tinyint(1) NOT NULL,
|
||||||
`has_old_password` int(11) NOT NULL DEFAULT 0,
|
`has_old_password` int(11) NOT NULL DEFAULT 0,
|
||||||
`password_reset_key` text DEFAULT NULL,
|
`password_reset_key` text DEFAULT NULL,
|
||||||
`away_message` varchar(100) NOT NULL
|
`away_message` varchar(100) NOT NULL,
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
`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` (
|
CREATE TABLE `users_modes_info` (
|
||||||
`n` int(11) NOT NULL,
|
`n` int(11) NOT NULL,
|
||||||
|
@ -100,77 +458,401 @@ CREATE TABLE `users_modes_info` (
|
||||||
`countmiss` int(10) UNSIGNED NOT NULL,
|
`countmiss` int(10) UNSIGNED NOT NULL,
|
||||||
`playcount` int(10) UNSIGNED NOT NULL,
|
`playcount` int(10) UNSIGNED NOT NULL,
|
||||||
`total_score` 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_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_ss` int(10) UNSIGNED NOT NULL,
|
||||||
`count_rank_s` int(10) UNSIGNED NOT NULL,
|
`count_rank_s` int(10) UNSIGNED NOT NULL,
|
||||||
`count_rank_a` int(10) UNSIGNED NOT NULL,
|
`count_rank_a` int(10) UNSIGNED NOT NULL,
|
||||||
`pp_country_rank` int(11) NOT NULL,
|
`pp_country_rank` int(11) NOT NULL,
|
||||||
`playtime` bigint(255) NOT NULL DEFAULT '0',
|
`playtime` bigint(20) NOT NULL DEFAULT 0,
|
||||||
`avg_accuracy` float NOT NULL DEFAULT '0',
|
`avg_accuracy` float NOT NULL DEFAULT 0,
|
||||||
`level` int(255) NOT NULL DEFAULT '0'
|
`level` int(11) NOT NULL DEFAULT 0
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
|
) 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` (
|
CREATE TABLE `web_info` (
|
||||||
`i` int(11) NOT NULL,
|
`i` int(11) NOT NULL,
|
||||||
`HomepageText` varchar(255) NOT NULL DEFAULT 'A default Binato instance!'
|
`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` (
|
CREATE TABLE `web_prefs` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
`keyboard` tinyint(1) NOT NULL DEFAULT '0',
|
`keyboard` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`mouse` tinyint(1) NOT NULL DEFAULT '0',
|
`mouse` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`tablet` tinyint(1) NOT NULL DEFAULT '0',
|
`tablet` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`touch` tinyint(1) NOT NULL DEFAULT '0',
|
`touch` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
`location` varchar(32) NOT NULL,
|
`location` varchar(32) NOT NULL,
|
||||||
`interests` varchar(64) 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` (
|
CREATE TABLE `web_titles` (
|
||||||
`id` int(11) NOT NULL,
|
`id` int(11) NOT NULL,
|
||||||
`title` varchar(32) 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`
|
ALTER TABLE `scores`
|
||||||
ADD PRIMARY KEY (`id`);
|
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`
|
ALTER TABLE `users_info`
|
||||||
ADD PRIMARY KEY (`id`),
|
ADD PRIMARY KEY (`id`),
|
||||||
ADD UNIQUE KEY `id` (`id`);
|
ADD UNIQUE KEY `id` (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `users_modes_info`
|
||||||
|
--
|
||||||
ALTER TABLE `users_modes_info`
|
ALTER TABLE `users_modes_info`
|
||||||
ADD PRIMARY KEY (`n`);
|
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`
|
ALTER TABLE `web_info`
|
||||||
ADD PRIMARY KEY (`i`);
|
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`
|
ALTER TABLE `web_prefs`
|
||||||
ADD PRIMARY KEY (`id`);
|
ADD PRIMARY KEY (`id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Indexes for table `web_titles`
|
||||||
|
--
|
||||||
ALTER TABLE `web_titles`
|
ALTER TABLE `web_titles`
|
||||||
ADD PRIMARY KEY (`id`);
|
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`
|
ALTER TABLE `mp_matches`
|
||||||
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
|
MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- AUTO_INCREMENT for table `mp_match_rounds`
|
||||||
|
--
|
||||||
ALTER TABLE `mp_match_rounds`
|
ALTER TABLE `mp_match_rounds`
|
||||||
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
|
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`
|
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`
|
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`
|
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
17
osuTyping.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
4038
package-lock.json
generated
4038
package-lock.json
generated
File diff suppressed because it is too large
Load diff
37
package.json
Executable file → Normal file
37
package.json
Executable file → Normal file
|
@ -2,21 +2,38 @@
|
||||||
"name": "binato",
|
"name": "binato",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "Binato.ts",
|
||||||
"scripts": {},
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aes256": "^1.1.0",
|
"aes256": "^1.1.0",
|
||||||
"chalk": "^4.1.0",
|
"dyetty": "^1.0.1",
|
||||||
"compression": "^1.7.4",
|
"mysql2": "^3.6.1",
|
||||||
"express": "^4.17.1",
|
"node-fetch": "^2.7.0",
|
||||||
"mysql2": "^2.3.3",
|
|
||||||
"node-fetch": "^2.6.7",
|
|
||||||
"osu-packet": "^4.1.2",
|
"osu-packet": "^4.1.2",
|
||||||
"prom-client": "^13.2.0",
|
"prom-client": "^14.2.0",
|
||||||
"redis": "^4.0.6",
|
"redis": "^4.6.8"
|
||||||
"uuid": "^8.3.2"
|
},
|
||||||
|
"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
313
server/BanchoServer.ts
Normal 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
34
server/Bot.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
34
server/ChatHistory.ts
Normal 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("<", "<").replaceAll(">", ">").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
93
server/ChatManager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
32
server/countryHelper.js → server/Country.ts
Executable file → Normal file
32
server/countryHelper.js → server/Country.ts
Executable file → Normal file
|
@ -1,4 +1,4 @@
|
||||||
const countryCodes = {
|
const countryCodes:{ [id: string]: number } = {
|
||||||
"LV": 132,
|
"LV": 132,
|
||||||
"AD": 3,
|
"AD": 3,
|
||||||
"LT": 130,
|
"LT": 130,
|
||||||
|
@ -12,8 +12,8 @@ const countryCodes = {
|
||||||
"NZ": 166,
|
"NZ": 166,
|
||||||
"TO": 215,
|
"TO": 215,
|
||||||
"KZ": 122,
|
"KZ": 122,
|
||||||
"GA": 76,
|
|
||||||
"BW": 35,
|
"BW": 35,
|
||||||
|
"GA": 76,
|
||||||
"AX": 247,
|
"AX": 247,
|
||||||
"GE": 79,
|
"GE": 79,
|
||||||
"UA": 222,
|
"UA": 222,
|
||||||
|
@ -249,26 +249,14 @@ const countryCodes = {
|
||||||
"UY": 226,
|
"UY": 226,
|
||||||
"SI": 194,
|
"SI": 194,
|
||||||
"AI": 7
|
"AI": 7
|
||||||
}
|
};
|
||||||
|
|
||||||
const countryCodeKeys = Object.keys(countryCodes);
|
// Get id of a country from a 2 char code
|
||||||
|
export function getCountryID(code:string) : number {
|
||||||
module.exports = {
|
const upperCode = code.toUpperCase();
|
||||||
getCountryID:function(code = "") {
|
if (upperCode in countryCodes) {
|
||||||
// Get id of a country from a 2 char code
|
return countryCodes[upperCode];
|
||||||
code = code.toUpperCase();
|
|
||||||
if (countryCodes[code] != null) return countryCodes[code];
|
|
||||||
else return 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
getCountryLetters:function(code) {
|
|
||||||
// Get country char code from id
|
|
||||||
for (var i = 0; i < countryCodes.length; i++) {
|
|
||||||
const countryId = countryCodes[countryCodeKeys[i]];
|
|
||||||
if (countryId === code) return countryId;
|
|
||||||
}
|
}
|
||||||
return "XX";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.countryCodes = countryCodes;
|
return 0;
|
||||||
|
}
|
|
@ -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
243
server/LoginProcess.ts
Normal 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}]`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
169
server/MultiplayerManager.ts
Normal file
169
server/MultiplayerManager.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = function(CurrentUser, FriendToAdd) {
|
|
||||||
global.DatabaseHelper.query("INSERT INTO friends (user, friendsWith) VALUES (?, ?);", [CurrentUser.id, FriendToAdd]);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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}]`);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = function(CurrentUser, FriendToRemove) {
|
|
||||||
global.DatabaseHelper.query("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [CurrentUser.id, FriendToRemove]);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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("<", "<").replaceAll(">", ">")}`);
|
|
||||||
|
|
||||||
botCommandHandler(CurrentUser, CurrentPacket.message, CurrentPacket.target);
|
|
||||||
return;
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = function(CurrentUser, Message) {
|
|
||||||
global.DatabaseHelper.query("UPDATE users_info SET away_message = ? WHERE id = ?", [Message.message, CurrentUser.id]);
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
41
server/PrivateChatManager.ts
Normal file
41
server/PrivateChatManager.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
81
server/SpectatorManager.ts
Normal file
81
server/SpectatorManager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
125
server/User.js
125
server/User.js
|
@ -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
39
server/Util.ts
Normal 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;
|
||||||
|
}
|
|
@ -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
35
server/commands/Admin.ts
Normal 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"}`);
|
||||||
|
}
|
20
server/commands/BaseCommand.ts
Normal file
20
server/commands/BaseCommand.ts
Normal 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
41
server/commands/Help.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
server/commands/Multiplayer.ts
Normal file
88
server/commands/Multiplayer.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
35
server/commands/Ranking.ts
Normal file
35
server/commands/Ranking.ts
Normal 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
21
server/commands/Roll.ts
Normal 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
7
server/enums/Mode.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export enum Mode {
|
||||||
|
Unknown = -1,
|
||||||
|
Osu,
|
||||||
|
Taiko,
|
||||||
|
Catch,
|
||||||
|
Mania
|
||||||
|
}
|
33
server/enums/Mods.ts
Normal file
33
server/enums/Mods.ts
Normal 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
113
server/enums/Packets.ts
Normal 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,
|
||||||
|
}
|
11
server/enums/Permissions.ts
Normal file
11
server/enums/Permissions.ts
Normal 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
|
||||||
|
}
|
5
server/enums/RankingMode.ts
Normal file
5
server/enums/RankingMode.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export enum RankingMode {
|
||||||
|
PP,
|
||||||
|
RANKED_SCORE,
|
||||||
|
AVG_ACCURACY
|
||||||
|
}
|
9
server/enums/SlotStatus.ts
Normal file
9
server/enums/SlotStatus.ts
Normal 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
4
server/enums/Team.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum Team {
|
||||||
|
Red,
|
||||||
|
Blue
|
||||||
|
}
|
39
server/interfaces/Config.ts
Normal file
39
server/interfaces/Config.ts
Normal 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
|
||||||
|
}
|
11
server/interfaces/ICommand.ts
Normal file
11
server/interfaces/ICommand.ts
Normal 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
|
||||||
|
}
|
4
server/interfaces/IpZxqResponse.ts
Normal file
4
server/interfaces/IpZxqResponse.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface IpZxqResponse {
|
||||||
|
country: string,
|
||||||
|
loc: string
|
||||||
|
}
|
71
server/interfaces/OsuPacketWriter.ts
Normal file
71
server/interfaces/OsuPacketWriter.ts
Normal 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
|
||||||
|
}
|
12
server/interfaces/PlayerScore.ts
Normal file
12
server/interfaces/PlayerScore.ts
Normal 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
|
||||||
|
}
|
5
server/interfaces/packetTypes/ChannelData.ts
Normal file
5
server/interfaces/packetTypes/ChannelData.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export default interface ChannelData {
|
||||||
|
channelName: string,
|
||||||
|
channelTopic: string,
|
||||||
|
channelUserCount: number
|
||||||
|
}
|
20
server/interfaces/packetTypes/MatchData.ts
Normal file
20
server/interfaces/packetTypes/MatchData.ts
Normal 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
|
||||||
|
}
|
6
server/interfaces/packetTypes/MatchDataSlot.ts
Normal file
6
server/interfaces/packetTypes/MatchDataSlot.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default interface MatchDataSlot {
|
||||||
|
status:number,
|
||||||
|
team:number,
|
||||||
|
playerId:number,
|
||||||
|
mods:number | undefined,
|
||||||
|
}
|
4
server/interfaces/packetTypes/MatchJoinData.ts
Normal file
4
server/interfaces/packetTypes/MatchJoinData.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface MatchJoinData {
|
||||||
|
matchId: number,
|
||||||
|
gamePassword: string
|
||||||
|
}
|
4
server/interfaces/packetTypes/MatchStartSkipData.ts
Normal file
4
server/interfaces/packetTypes/MatchStartSkipData.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface MatchStartSkipData {
|
||||||
|
playerId:number,
|
||||||
|
flag:boolean
|
||||||
|
}
|
6
server/interfaces/packetTypes/MessageData.ts
Normal file
6
server/interfaces/packetTypes/MessageData.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default interface MessageData {
|
||||||
|
sendingClient: string,
|
||||||
|
message: string,
|
||||||
|
target: string,
|
||||||
|
senderId: number
|
||||||
|
}
|
8
server/interfaces/packetTypes/PresenceData.ts
Normal file
8
server/interfaces/packetTypes/PresenceData.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default interface PresenceData {
|
||||||
|
status: number,
|
||||||
|
statusText: string,
|
||||||
|
beatmapId: number,
|
||||||
|
beatmapChecksum: string,
|
||||||
|
currentMods: number,
|
||||||
|
playMode: number,
|
||||||
|
}
|
7
server/interfaces/packetTypes/ReplayFrameData.ts
Normal file
7
server/interfaces/packetTypes/ReplayFrameData.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default interface ReplayFrameData {
|
||||||
|
buttonState: number,
|
||||||
|
bt: number,
|
||||||
|
mouseX: number,
|
||||||
|
mouseY: number,
|
||||||
|
time: number
|
||||||
|
}
|
20
server/interfaces/packetTypes/ScoreFrameData.ts
Normal file
20
server/interfaces/packetTypes/ScoreFrameData.ts
Normal 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
|
||||||
|
}
|
9
server/interfaces/packetTypes/SpectateFramesData.ts
Normal file
9
server/interfaces/packetTypes/SpectateFramesData.ts
Normal 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
|
||||||
|
}
|
15
server/interfaces/packetTypes/StatusUpdateData.ts
Normal file
15
server/interfaces/packetTypes/StatusUpdateData.ts
Normal 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
|
||||||
|
}
|
12
server/interfaces/packetTypes/UserPresenceData.ts
Normal file
12
server/interfaces/packetTypes/UserPresenceData.ts
Normal 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
|
||||||
|
}
|
4
server/interfaces/packetTypes/UserQuitData.ts
Normal file
4
server/interfaces/packetTypes/UserQuitData.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default interface UserQuitData {
|
||||||
|
userId: number,
|
||||||
|
state: number
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
105
server/objects/Channel.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
97
server/objects/DataStream.ts
Normal file
97
server/objects/DataStream.ts
Normal 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}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
server/objects/DataStreamArray.ts
Normal file
33
server/objects/DataStreamArray.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
server/objects/Database.ts
Normal file
97
server/objects/Database.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
74
server/objects/FunkyArray.ts
Normal file
74
server/objects/FunkyArray.ts
Normal 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
9
server/objects/LatLng.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
33
server/objects/LoginInfo.ts
Normal file
33
server/objects/LoginInfo.ts
Normal 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
693
server/objects/Match.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
14
server/objects/MatchArray.ts
Normal file
14
server/objects/MatchArray.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
61
server/objects/PrivateChannel.ts
Normal file
61
server/objects/PrivateChannel.ts
Normal 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
60
server/objects/Shared.ts
Normal 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
48
server/objects/Slot.ts
Normal 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
151
server/objects/User.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
server/objects/UserArray.ts
Normal file
26
server/objects/UserArray.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
25
server/objects/database/UserInfo.ts
Normal file
25
server/objects/database/UserInfo.ts
Normal 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;
|
||||||
|
}
|
24
server/objects/database/UserModeInfo.ts
Normal file
24
server/objects/database/UserModeInfo.ts
Normal 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;
|
||||||
|
}
|
|
@ -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
Loading…
Reference in a new issue