Compare commits

...

20 commits

Author SHA1 Message Date
b40361caf0 Replace fileSmasher with ncc. 2023-10-16 10:52:36 +01:00
640c2cdf92 Send friends list info after login 2023-10-10 16:03:55 +01:00
9d53d82997
DB access cleanup 2023-10-07 13:09:10 +01:00
877592bb94 Move all packet interfaces to a better location 2023-10-06 11:19:08 +01:00
2fbdb9799a Remove last instance of explicit any type 2023-10-06 09:55:15 +01:00
a32ab80f73 General cleanup 2023-10-06 09:55:03 +01:00
4ec4cb1c1f Remove duplicate interface 2023-10-06 09:52:22 +01:00
5e1106e488 Give types to all functions in OsuPacketWriter 2023-10-06 09:48:31 +01:00
39f6669f94 add type 2023-10-05 11:15:13 +01:00
056260ad55 improve tooling 2023-10-05 11:13:19 +01:00
78f4a499fa oops 2023-10-04 16:21:29 +01:00
462d0c879c Consolidate admin commands 2023-10-04 16:13:16 +01:00
108f27eb22 Remove unneeded semicolons 2023-10-04 15:52:18 +01:00
8ab318ef12 Correct some mistakes in the DB class 2023-10-04 15:12:35 +01:00
4b90031294 Kickstart the big db rewrite 2023-10-04 15:10:38 +01:00
686e6001b2 Code quality improvements. 2023-10-04 12:28:47 +01:00
93da399fa5 General code quality/readability 2023-10-03 10:09:44 +01:00
04bd1e42bb Change all for(each) blocks to use const instead of let. 2023-10-03 09:57:35 +01:00
25105537ea
Fix rate adjust mods not working in multi when freemod is active 2023-09-13 08:31:16 +01:00
c3b24d32af
Re-add LICENSE 2023-09-12 15:11:34 +01:00
66 changed files with 946 additions and 579 deletions

View file

@ -6,7 +6,7 @@ enum LogType {
INFO,
WARN,
ERROR
};
}
const LogTags = {
INFO: dyetty.bgGreen(dyetty.black(" INFO ")),

21
LICENSE.txt Normal file
View file

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

View file

@ -10,7 +10,7 @@ export default abstract class osu {
};
static Client = {
Reader: function(data:any) : any {
Reader: function(data:Buffer) {
return new nodeOsu.Client.Reader(data);
}
};

29
package-lock.json generated
View file

@ -20,9 +20,11 @@
"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"
@ -45,7 +47,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
"integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -69,7 +70,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6.0.0"
}
@ -79,7 +79,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
@ -230,6 +229,15 @@
"form-data": "^3.0.0"
}
},
"node_modules/@vercel/ncc": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.0.tgz",
"integrity": "sha512-B4YKZMm/EqMptKSFyAq4q2SlgJe+VCmEH6Y8gf/E1pTlWbsUJpuH1ymik2Ex3aYO5mCWwV1kaSYHSQOT8+4vHA==",
"dev": true,
"bin": {
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
@ -635,8 +643,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/call-bind": {
"version": "1.0.2",
@ -774,8 +781,7 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/concat-map": {
"version": "0.0.1",
@ -2376,7 +2382,6 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -2386,7 +2391,6 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -2545,11 +2549,10 @@
}
},
"node_modules/terser": {
"version": "5.19.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
"version": "5.21.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz",
"integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==",
"dev": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",

View file

@ -7,8 +7,7 @@
"dev:updateCheck": "check-outdated",
"dev:run": "nodemon --watch './**/*.ts' Binato.ts",
"build": "npm-run-all build:*",
"build:smash": "ts-node ./tooling/fileSmasher.ts",
"build:build": "tsc --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"
@ -28,9 +27,11 @@
"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"

View file

@ -5,15 +5,15 @@ import LoginProcess from "./LoginProcess";
import { IncomingMessage, ServerResponse } from "http";
import { Packets } from "./enums/Packets";
import { RedisClientType, createClient } from "redis";
import MessageData from "./interfaces/MessageData";
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.query("UPDATE mp_matches SET close_time = UNIX_TIMESTAMP() WHERE close_time IS NULL");
shared.database.query("UPDATE osu_info SET value = 0 WHERE name = 'online_now'");
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);
@ -67,10 +67,11 @@ 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 (let User of shared.users.getIterableItems()) {
for (const User of shared.users.getIterableItems()) {
if (User.uuid == "bot") continue; // Ignore the bot
// Logout this user, they're clearly gone.
@ -80,8 +81,6 @@ setInterval(() => {
}
}, 10000);
const EMPTY_BUFFER = Buffer.alloc(0);
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;
@ -91,154 +90,149 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo
// Client doesn't have a token yet, let's auth them!
await LoginProcess(req, res, packet, shared);
shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
shared.database.execute("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [shared.users.getLength() - 1]);
} else {
let responseData = EMPTY_BUFFER;
let responseData = Buffer.allocUnsafe(0);
// Client has a token, let's see what they want.
try {
// Get the current user
const PacketUser = shared.users.getByToken(requestTokenString);
const user = shared.users.getByToken(requestTokenString);
// Make sure the client's token isn't invalid
if (PacketUser != null) {
// Update the session timeout time
PacketUser.timeoutTime = Date.now() + 60000;
if (user != null) {
// Update the session timeout time for each request
user.timeoutTime = Date.now() + 60000;
// Create a new osu! packet reader
// Parse bancho packets
const osuPacketReader = osu.Client.Reader(packet);
// Parse current bancho packet
const PacketData = osuPacketReader.Parse();
const packets = osuPacketReader.Parse();
// Go through each packet sent by the client
for (let CurrentPacket of PacketData) {
switch (CurrentPacket.id) {
for (const packet of packets) {
switch (packet.id) {
case Packets.Client_ChangeAction:
ChangeAction(PacketUser, CurrentPacket.data);
ChangeAction(user, packet.data);
break;
case Packets.Client_SendPublicMessage:
const message:MessageData = CurrentPacket.data;
let channel = shared.chatManager.GetChannelByName(message.target);
if (channel instanceof Channel) {
channel.SendMessage(PacketUser, CurrentPacket.data.message);
}
SendPublicMessage(user, packet.data);
break;
case Packets.Client_Logout:
await Logout(PacketUser);
await Logout(user);
break;
case Packets.Client_RequestStatusUpdate:
UserPresenceBundle(PacketUser);
UserPresenceBundle(user);
break;
case Packets.Client_StartSpectating:
spectatorManager.startSpectating(PacketUser, CurrentPacket.data);
spectatorManager.startSpectating(user, packet.data);
break;
case Packets.Client_SpectateFrames:
spectatorManager.spectatorFrames(PacketUser, CurrentPacket.data);
spectatorManager.spectatorFrames(user, packet.data);
break;
case Packets.Client_StopSpectating:
spectatorManager.stopSpectating(PacketUser);
spectatorManager.stopSpectating(user);
break;
case Packets.Client_SendPrivateMessage:
PrivateMessage(PacketUser, CurrentPacket.data);
PrivateMessage(user, packet.data);
break;
case Packets.Client_JoinLobby:
shared.multiplayerManager.JoinLobby(PacketUser);
shared.multiplayerManager.JoinLobby(user);
break;
case Packets.Client_PartLobby:
shared.multiplayerManager.LeaveLobby(PacketUser);
shared.multiplayerManager.LeaveLobby(user);
break;
case Packets.Client_CreateMatch:
await shared.multiplayerManager.CreateMatch(PacketUser, CurrentPacket.data);
await shared.multiplayerManager.CreateMatch(user, packet.data);
break;
case Packets.Client_JoinMatch:
shared.multiplayerManager.JoinMatch(PacketUser, CurrentPacket.data);
shared.multiplayerManager.JoinMatch(user, packet.data);
break;
case Packets.Client_MatchChangeSlot:
PacketUser.match?.moveToSlot(PacketUser, CurrentPacket.data);
user.match?.moveToSlot(user, packet.data);
break;
case Packets.Client_MatchReady:
PacketUser.match?.setStateReady(PacketUser);
user.match?.setStateReady(user);
break;
case Packets.Client_MatchChangeSettings:
await PacketUser.match?.updateMatch(PacketUser, CurrentPacket.data);
await user.match?.updateMatch(user, packet.data);
break;
case Packets.Client_MatchNotReady:
PacketUser.match?.setStateNotReady(PacketUser);
user.match?.setStateNotReady(user);
break;
case Packets.Client_PartMatch:
await shared.multiplayerManager.LeaveMatch(PacketUser);
await shared.multiplayerManager.LeaveMatch(user);
break;
case Packets.Client_MatchLock:
PacketUser.match?.lockOrKick(PacketUser, CurrentPacket.data);
user.match?.lockOrKick(user, packet.data);
break;
case Packets.Client_MatchNoBeatmap:
PacketUser.match?.missingBeatmap(PacketUser);
user.match?.missingBeatmap(user);
break;
case Packets.Client_MatchSkipRequest:
PacketUser.match?.matchSkip(PacketUser);
user.match?.matchSkip(user);
break;
case Packets.Client_MatchHasBeatmap:
PacketUser.match?.notMissingBeatmap(PacketUser);
user.match?.notMissingBeatmap(user);
break;
case Packets.Client_MatchTransferHost:
PacketUser.match?.transferHost(PacketUser, CurrentPacket.data);
user.match?.transferHost(user, packet.data);
break;
case Packets.Client_MatchChangeMods:
PacketUser.match?.updateMods(PacketUser, CurrentPacket.data);
user.match?.updateMods(user, packet.data);
break;
case Packets.Client_MatchStart:
PacketUser.match?.startMatch();
user.match?.startMatch();
break;
case Packets.Client_MatchLoadComplete:
PacketUser.match?.matchPlayerLoaded(PacketUser);
user.match?.matchPlayerLoaded(user);
break;
case Packets.Client_MatchComplete:
await PacketUser.match?.onPlayerFinishMatch(PacketUser);
await user.match?.onPlayerFinishMatch(user);
break;
case Packets.Client_MatchScoreUpdate:
PacketUser.match?.updatePlayerScore(PacketUser, CurrentPacket.data);
user.match?.updatePlayerScore(user, packet.data);
break;
case Packets.Client_MatchFailed:
PacketUser.match?.matchFailed(PacketUser);
user.match?.matchFailed(user);
break;
case Packets.Client_MatchChangeTeam:
PacketUser.match?.changeTeam(PacketUser);
user.match?.changeTeam(user);
break;
case Packets.Client_ChannelJoin:
PacketUser.joinChannel(CurrentPacket.data);
user.joinChannel(packet.data);
break;
case Packets.Client_ChannelPart:
PacketUser.leaveChannel(CurrentPacket.data);
user.leaveChannel(packet.data);
break;
case Packets.Client_SetAwayMessage:
@ -246,35 +240,35 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo
break;
case Packets.Client_FriendAdd:
AddFriend(PacketUser, CurrentPacket.data);
await AddFriend(user, packet.data);
break;
case Packets.Client_FriendRemove:
RemoveFriend(PacketUser, CurrentPacket.data);
await RemoveFriend(user, packet.data);
break;
case Packets.Client_UserStatsRequest:
UserStatsRequest(PacketUser, CurrentPacket.data);
UserStatsRequest(user, packet.data);
break;
case Packets.Client_SpecialMatchInfoRequest:
TourneyMatchSpecialInfo(PacketUser, CurrentPacket.data);
TourneyMatchSpecialInfo(user, packet.data);
break;
case Packets.Client_SpecialJoinMatchChannel:
TourneyMatchJoinChannel(PacketUser, CurrentPacket.data);
TourneyMatchJoinChannel(user, packet.data);
break;
case Packets.Client_SpecialLeaveMatchChannel:
TourneyMatchLeaveChannel(PacketUser, CurrentPacket.data);
TourneyMatchLeaveChannel(user, packet.data);
break;
case Packets.Client_Invite:
MultiplayerInvite(PacketUser, CurrentPacket.data);
MultiplayerInvite(user, packet.data);
break;
case Packets.Client_UserPresenceRequest:
UserPresence(PacketUser, PacketUser.id);
UserPresence(user, user.id);
break;
// Ignored packets
@ -286,13 +280,13 @@ export default async function HandleRequest(req:IncomingMessage, res:ServerRespo
default:
// Print out unimplemented packet
console.dir(CurrentPacket);
console.dir(packet);
break;
}
}
responseData = PacketUser.queue;
PacketUser.clearQueue();
responseData = user.queue;
user.clearQueue();
} else {
// User's token is invlid, force a reconnect
ConsoleHelper.printBancho(`Forced client re-connect (Token is invalid)`);

View file

@ -5,7 +5,7 @@ import User from "./objects/User";
// Commands
import RankingCommand from "./commands/Ranking";
import LockCommand from "./commands/Lock";
import AdminCommand from "./commands/Admin";
import MultiplayerCommands from "./commands/Multiplayer";
import HelpCommand from "./commands/Help";
import RollCommand from "./commands/Roll";
@ -19,7 +19,7 @@ export default class Bot {
this.commands["help"] = new HelpCommand(shared, this.commands);
this.commands["ranking"] = new RankingCommand(shared);
this.commands["lock"] = new LockCommand(shared);
this.commands["admin"] = new AdminCommand(shared);
this.commands["mp"] = new MultiplayerCommands(shared);
this.commands["roll"] = new RollCommand(shared);
}

View file

@ -69,7 +69,7 @@ export default class ChatManager {
}
public ForceJoinChannels(user:User) {
for (let channel of this.forceJoinChannels.getIterableItems()) {
for (const channel of this.forceJoinChannels.getIterableItems()) {
channel.Join(user);
}
}
@ -77,7 +77,7 @@ export default class ChatManager {
public SendChannelListing(user:User) {
const osuPacketWriter = osu.Bancho.Writer();
for (let channel of this.chatChannels.getIterableItems()) {
for (const channel of this.chatChannels.getIterableItems()) {
if (channel.isSpecial) {
continue;
}

View file

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

View file

@ -1,6 +1,6 @@
import { ConsoleHelper } from "../ConsoleHelper";
import fetch from "node-fetch";
import getCountryID from "./Country";
import { getCountryID } from "./Country";
import { generateSession } from "./Util";
import LatLng from "./objects/LatLng";
import LoginInfo from "./objects/LoginInfo";
@ -14,6 +14,7 @@ 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;
@ -36,7 +37,7 @@ enum LoginResult {
function TestLogin(loginInfo:LoginInfo, shared:Shared) {
return new Promise<LoginResult>(async (resolve, reject) => {
const userDBData:any = await shared.database.query("SELECT * FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
const userDBData = await shared.userInfoRepository.selectByUsername(loginInfo.username);
// Make sure a user was found in the database
if (userDBData == null) return resolve(LoginResult.INCORRECT);
@ -81,7 +82,7 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
}
const loginResult:LoginResult = await TestLogin(loginInfo, shared);
let osuPacketWriter = osu.Bancho.Writer();
const osuPacketWriter = osu.Bancho.Writer();
let newUser:User | undefined;
let friendsPresence:Buffer = Buffer.alloc(0);
@ -122,7 +123,10 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
}
// Get information about the user from the database
const userDB = await shared.database.query("SELECT id FROM users_info WHERE username = ? LIMIT 1", [loginInfo.username]);
const userInfo = await shared.userInfoRepository.selectByUsername(loginInfo.username);
if (userInfo == null) {
return;
}
// Create a token for the client
const newClientToken:string = await generateSession();
@ -135,7 +139,7 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
}
// Retreive the newly created user
newUser = shared.users.add(newClientToken, new User(userDB.id, loginInfo.username, newClientToken, shared));
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;
@ -169,24 +173,31 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
shared.chatManager.SendChannelListing(newUser);
// Construct & send user's friends list
const userFriends = await shared.database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
const friends = await shared.database.query("SELECT friendsWith FROM friends WHERE user = ?", [newUser.id]);
const friendsArray:Array<number> = new Array<number>();
for (let useFriend of userFriends) {
const friendId:number = useFriend.friendsWith;
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; }
if (friendPresence === undefined) {
continue;
}
friendsPresence = Buffer.concat([
friendsPresence,
friendPresence
], friendsPresence.length + friendPresence.length);
}
osuPacketWriter.FriendsList(friendsArray);
// 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);
@ -203,7 +214,7 @@ export default async function LoginProcess(req:IncomingMessage, res:ServerRespon
const writerBuffer:Buffer = osuPacketWriter.toBuffer;
if (newUser === undefined) {
res.writeHead(200, {
"cho-token": "no",
"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"
});

View file

@ -8,8 +8,8 @@ import StatusUpdate from "./packets/StatusUpdate";
import UserPresence from "./packets/UserPresence";
import UserPresenceBundle from "./packets/UserPresenceBundle";
import MatchArray from "./objects/MatchArray";
import MatchJoinData from "./interfaces/MatchJoinData";
import MatchData from "./interfaces/MatchData";
import MatchJoinData from "./interfaces/packetTypes/MatchJoinData";
import MatchData from "./interfaces/packetTypes/MatchData";
import osu from "../osuTyping";
import TourneyMatchSpecialInfo from "./packets/TourneyMatchSpecialInfo";
@ -62,7 +62,7 @@ export default class MultiplayerManager {
}
let matchFull = true;
for (let slot of match.slots) {
for (const slot of match.slots) {
if (slot.player instanceof User || slot.status === SlotStatus.Locked) {
continue;
}
@ -111,8 +111,8 @@ export default class MultiplayerManager {
const osuPacketWriter = osu.Bancho.Writer();
let bufferToSend = UserPresenceBundle(this.shared);
for (let match of this.matches.getIterableItems()) {
for (let slot of match.slots) {
for (const match of this.matches.getIterableItems()) {
for (const slot of match.slots) {
if (!(slot.player instanceof User) || slot.status === SlotStatus.Locked) {
continue;
}

View file

@ -2,6 +2,7 @@ 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;
@ -41,14 +42,13 @@ export default class SpectatorManager {
spectateStream.Send(osuPacketWriter.toBuffer);
}
// TODO: Interface for spectateFrameData
public spectatorFrames(user:User, spectateFrameData:any) {
public spectatorFrames(user:User, spectateFramesData:SpectateFramesData) {
if (user.spectatorStream === undefined) {
return;
}
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.SpectateFrames(spectateFrameData);
osuPacketWriter.SpectateFrames(spectateFramesData);
user.spectatorStream.Send(osuPacketWriter.toBuffer);
}

View file

@ -32,4 +32,8 @@ export function isNullOrEmpty(str:string | undefined | null) {
}
return false;
}
export function enumHasFlag(value:number, flag:number) : boolean {
return (value & flag) === flag;
}

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

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

View file

@ -5,6 +5,7 @@ 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>();
@ -14,6 +15,6 @@ export default class BaseCommand implements ICommand {
}
public exec(channel:Channel, sender:User, args:Array<string>) {
channel.SendBotMessage(`Sorry ${sender.username}! This command has no functionality yet. Args: ["${args.join('", "')}"]`);
}
}

View file

@ -23,7 +23,7 @@ export default class HelpCommand extends BaseCommand {
// All commands
if (args.length === 0) {
let constructedHelp = "Help:\n";
for (let key of this.commandKeys) {
for (const key of this.commandKeys) {
constructedHelp += ` !${key} - ${this.commandList[key].helpDescription}\n`;
}
channel.SendBotMessage(constructedHelp.slice(0, constructedHelp.length - 1));

View file

@ -1,17 +0,0 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import BaseCommand from "./BaseCommand";
export default class LockCommand extends BaseCommand {
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 (channel.isSpecial) {
channel.SendBotMessage("Multiplayer channels cannot be locked");
return;
}
channel.isLocked = !channel.isLocked;
channel.SendBotMessage(`Channel is now ${channel.isLocked ? "locked" : "unlocked"}`);
}
}

View file

@ -6,8 +6,7 @@ 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
!mp obr - Toggles Battle Royale mode`;
!mp abort - Aborts the currently running round / countdown`;
public readonly helpDescription:string = "Command for use in multiplayer matches.";
public readonly helpArguments:Array<string> = ["subCommand"];
@ -36,9 +35,6 @@ export default class MultiplayerCommands extends BaseCommand {
case "abort":
return mpAbort(channel, sender.match);
case "obr":
return mpOBR(channel, sender.match);
}
}
}
@ -89,8 +85,4 @@ function mpAbort(channel:Channel, match:Match) {
match.finishMatch();
channel.SendBotMessage("Aborted current round");
}
}
function mpOBR(channel:Channel, match:Match) {
}

View file

@ -1,6 +1,6 @@
import Channel from "../objects/Channel";
import User from "../objects/User";
import { RankingModes } from "../enums/RankingModes";
import { RankingMode } from "../enums/RankingMode";
import BaseCommand from "./BaseCommand";
export default class RankingCommand extends BaseCommand {
@ -18,15 +18,15 @@ export default class RankingCommand extends BaseCommand {
switch (args[0].toLowerCase()) {
case "pp":
sender.rankingMode = RankingModes.PP;
sender.rankingMode = RankingMode.PP;
channel.SendBotMessage("Set ranking mode to pp.");
break;
case "score":
sender.rankingMode = RankingModes.RANKED_SCORE;
sender.rankingMode = RankingMode.RANKED_SCORE;
channel.SendBotMessage("Set ranking mode to score.");
break;
case "acc":
sender.rankingMode = RankingModes.AVG_ACCURACY;
sender.rankingMode = RankingMode.AVG_ACCURACY;
channel.SendBotMessage("Set ranking mode to accuracy.");
break;
}

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

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

View file

@ -1,4 +1,33 @@
// TODO: Mods enum
// TODO: Complete mods enum.
export enum Mods {
None
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
}

View file

@ -1,4 +1,5 @@
export enum Permissions {
None = 0,
BAT = 2,
Supporter = 4,
Friend = 8,

View file

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

View file

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

View file

@ -1,19 +0,0 @@
export default interface MatchScoreData {
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,
comboPortion:number,
bonusPortion:number
}

View file

@ -1,5 +1,11 @@
import MatchData from "./MatchData"
import MessageData from "./MessageData"
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
@ -7,59 +13,59 @@ export default interface OsuPacketWriter {
CommandError() : OsuPacketWriter,
SendMessage(data:MessageData) : OsuPacketWriter,
Ping() : OsuPacketWriter,
HandleIrcChangeUsername(data:any) : OsuPacketWriter,
HandleIrcChangeUsername(data:string) : OsuPacketWriter,
HandleIrcQuit() : OsuPacketWriter,
HandleOsuUpdate(data:any) : OsuPacketWriter,
HandleUserQuit(data:any) : OsuPacketWriter,
SpectatorJoined(data:any) : OsuPacketWriter,
SpectatorLeft(data:any) : OsuPacketWriter,
SpectateFrames(data:any) : 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:any) : OsuPacketWriter,
SpectatorCantSpectate(data:number) : OsuPacketWriter,
GetAttention() : OsuPacketWriter,
Announce(data:string) : OsuPacketWriter,
MatchUpdate(data:MatchData) : OsuPacketWriter,
MatchNew(data:any) : OsuPacketWriter,
MatchDisband(data:any) : OsuPacketWriter,
MatchJoinSuccess(data:any) : OsuPacketWriter,
MatchNew(data:MatchData) : OsuPacketWriter,
MatchDisband(data:number) : OsuPacketWriter,
MatchJoinSuccess(data:MatchData) : OsuPacketWriter,
MatchJoinFail() : OsuPacketWriter,
FellowSpectatorJoined(data:any) : OsuPacketWriter,
FellowSpectatorLeft(data:any) : OsuPacketWriter,
MatchStart(data:any) : OsuPacketWriter,
MatchScoreUpdate(data:any) : OsuPacketWriter,
MatchTransferHost(data:any) : OsuPacketWriter,
FellowSpectatorJoined(data:number) : OsuPacketWriter,
FellowSpectatorLeft(data:number) : OsuPacketWriter,
MatchStart(data:MatchData) : OsuPacketWriter,
MatchScoreUpdate(data:ScoreFrameData) : OsuPacketWriter,
MatchTransferHost() : OsuPacketWriter,
MatchAllPlayersLoaded() : OsuPacketWriter,
MatchPlayerFailed(data:any) : OsuPacketWriter,
MatchPlayerFailed(data:number) : OsuPacketWriter,
MatchComplete() : OsuPacketWriter,
MatchSkip() : OsuPacketWriter,
Unauthorised() : OsuPacketWriter,
ChannelJoinSuccess(data:any) : OsuPacketWriter,
ChannelAvailable(data:any) : OsuPacketWriter,
ChannelRevoked(data:any) : OsuPacketWriter,
ChannelAvailableAutojoin(data:any) : 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:any) : OsuPacketWriter,
UserPresence(data:any) : OsuPacketWriter,
Restart(data:any) : OsuPacketWriter,
Invite(data:any) : OsuPacketWriter,
MatchPlayerSkipped(data:number) : OsuPacketWriter,
UserPresence(data:UserPresenceData) : OsuPacketWriter,
Restart(data:number) : OsuPacketWriter,
Invite(data:MessageData) : OsuPacketWriter,
ChannelListingComplete() : OsuPacketWriter,
MatchChangePassword(data:any) : OsuPacketWriter,
BanInfo(data:any) : OsuPacketWriter,
UserSilenced(data:any) : OsuPacketWriter,
UserPresenceSingle(data:any) : OsuPacketWriter,
UserPresenceBundle(data:any) : OsuPacketWriter,
UserPMBlocked(data:any) : OsuPacketWriter,
TargetIsSilenced(data:any) : 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:any) : OsuPacketWriter,
SwitchServer(data:number) : OsuPacketWriter,
AccountRestricted() : OsuPacketWriter,
RTX(data:any) : OsuPacketWriter,
SwitchTourneyServer(data:any) : OsuPacketWriter
RTX(data:string) : OsuPacketWriter,
SwitchTourneyServer(data:string) : OsuPacketWriter
toBuffer : Buffer
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,7 +74,7 @@ export default class DataStream {
public Send(data:Buffer) {
this.checkInactive();
for (let user of this.users.getIterableItems()) {
for (const user of this.users.getIterableItems()) {
user.addActionToQueue(data);
}
if (Constants.DEBUG) {
@ -85,7 +85,7 @@ export default class DataStream {
public SendWithExclusion(data:Buffer, exclude:User) {
this.checkInactive();
for (let user of this.users.getIterableItems()) {
for (const user of this.users.getIterableItems()) {
if (user.uuid !== exclude.uuid) {
user.addActionToQueue(data);
}

View file

@ -26,7 +26,7 @@ export default class DataStreamArray extends FunkyArray<DataStream> {
}
public RemoveUserFromAllStreams(user:User) {
for (let stream of this.getIterableItems()) {
for (const stream of this.getIterableItems()) {
stream.RemoveUser(user);
}
}

View file

@ -1,5 +1,6 @@
import { ConsoleHelper } from "../../ConsoleHelper";
import { createPool, Pool } from "mysql2";
import { createPool, Pool, RowDataPacket } from "mysql2";
import { DBInDataType } from "../types/DBTypes";
export default class Database {
private connectionPool:Pool;
@ -20,50 +21,77 @@ export default class Database {
ConsoleHelper.printInfo(`Connected DB connection pool. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
}
public query(query = "", data?:Array<any>) {
const limited = query.includes("LIMIT 1");
return new Promise<any>((resolve, reject) => {
public execute(query:string, data?:Array<DBInDataType>) {
return new Promise<boolean>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
reject(err);
try {
connection.release();
} catch (e) {
ConsoleHelper.printError("Failed to release mysql connection\n" + 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(query, (err, data) => {
connection.query<RowDataPacket[]>(query, (err, rows) => {
connection.release();
if (err) {
reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
return reject(err);
}
resolve(rows);
connection.release();
});
}
// Use new prepared statements w/ placeholders
else {
connection.execute(query, data, (err, data) => {
connection.execute<RowDataPacket[]>(query, data, (err, rows) => {
connection.release();
if (err) {
reject(err);
connection.release();
} else {
dataReceived(resolve, data, limited);
connection.release();
return reject(err);
}
resolve(rows);
connection.release();
});
}
}
});
});
}
}
function dataReceived(resolveCallback:(value:unknown) => void, data:any, limited:boolean = false) : void {
if (limited) resolveCallback(data[0]);
else resolveCallback(data);
public async querySingle(query:string, data?:Array<DBInDataType>) {
const dbData = await this.query(query, data);
if (dbData != null && dbData.length > 0) {
return dbData[0];
}
return null;
}
}

View file

@ -1,5 +1,5 @@
export default class FunkyArray<T> {
private items:any = {};
private items:{ [id: string]: T } = {};
private itemKeys:Array<string> = Object.keys(this.items);
private iterableArray:Array<T> = new Array<T>();
@ -29,8 +29,8 @@ export default class FunkyArray<T> {
}
public regenerateIterableArray() : void {
this.iterableArray = new Array();
for (let itemKey of this.itemKeys) {
this.iterableArray = new Array<T>();
for (const itemKey of this.itemKeys) {
this.iterableArray.push(this.items[itemKey]);
}
this.itemKeys = Object.keys(this.items);
@ -64,7 +64,7 @@ export default class FunkyArray<T> {
return this.itemKeys;
}
public getItems() : any {
public getItems() : { [id: string]: T } {
return this.items;
}

View file

@ -5,13 +5,19 @@ import Slot from "./Slot";
import User from "./User";
import StatusUpdate from "../packets/StatusUpdate";
import { SlotStatus } from "../enums/SlotStatus";
import MatchData from "../interfaces/MatchData";
import MatchData from "../interfaces/packetTypes/MatchData";
import { Team } from "../enums/Team";
import MatchStartSkipData from "../interfaces/MatchStartSkipData";
import MatchStartSkipData from "../interfaces/packetTypes/MatchStartSkipData";
import { Mods } from "../enums/Mods";
import PlayerScore from "../interfaces/PlayerScore";
import MatchScoreData from "../interfaces/MatchScoreData";
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
@ -236,7 +242,7 @@ export default class Match {
}
queryData.push(this.matchId);
await this.shared.database.query(
await this.shared.database.execute(
`UPDATE mp_matches SET ${gameNameChanged ? `name = ?${gameSeedChanged ? ", " : ""}` : ""}${gameSeedChanged ? `seed = ?` : ""} WHERE id = ?`,
queryData
);
@ -391,11 +397,16 @@ export default class Match {
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(user.id);
osuPacketWriter.MatchPlayerSkipped(slotId);
osuPacketWriter.MatchSkip();
this.matchStream.Send(osuPacketWriter.toBuffer);
@ -404,7 +415,7 @@ export default class Match {
} else {
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchPlayerSkipped(user.id);
osuPacketWriter.MatchPlayerSkipped(slotId);
this.matchStream.Send(osuPacketWriter.toBuffer);
}
@ -420,7 +431,6 @@ export default class Match {
}
}
// TODO: Fix not being able to add DT when freemod is active
public updateMods(user:User, mods:Mods) {
const slot = user.matchSlot;
if (!(slot instanceof Slot)) {
@ -431,6 +441,19 @@ export default class Match {
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)) {
@ -503,7 +526,7 @@ export default class Match {
// All players have loaded the beatmap, start playing.
if (allLoaded) {
let osuPacketWriter = osu.Bancho.Writer();
const osuPacketWriter = osu.Bancho.Writer();
osuPacketWriter.MatchAllPlayersLoaded();
this.matchStream.Send(osuPacketWriter.toBuffer);
@ -572,9 +595,9 @@ export default class Match {
this.matchLoadSlots = undefined;
this.inProgress = false;
let osuPacketWriter = osu.Bancho.Writer();
const osuPacketWriter = osu.Bancho.Writer();
let queryData:Array<any> = [
const queryData:Array<string | number | null> = [
this.matchId,
this.roundId++,
this.playMode,
@ -608,7 +631,7 @@ export default class Match {
slot.status = SlotStatus.NotReady;
}
await this.shared.database.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);
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();
@ -625,31 +648,31 @@ export default class Match {
this.playerScores = undefined;
}
updatePlayerScore(user:User, matchScoreData:MatchScoreData) {
updatePlayerScore(user:User, scoreFrameData:ScoreFrameData) {
const osuPacketWriter = osu.Bancho.Writer();
if (user.matchSlot === undefined || user.matchSlot.player === undefined || this.playerScores === undefined) {
return;
}
matchScoreData.id = user.id;
scoreFrameData.id = user.matchSlot.slotId;
// Update playerScores
for (const playerScore of this.playerScores) {
if (playerScore.player?.id === user.id) {
playerScore.score = matchScoreData.totalScore;
const isCurrentlyFailed = matchScoreData.currentHp == 254;
playerScore.score = scoreFrameData.totalScore;
const isCurrentlyFailed = scoreFrameData.currentHp == 254;
playerScore.isCurrentlyFailed = isCurrentlyFailed;
if (!playerScore.hasFailed && isCurrentlyFailed) {
playerScore.hasFailed = true;
}
playerScore._raw = matchScoreData;
playerScore._raw = scoreFrameData;
break;
}
}
osuPacketWriter.MatchScoreUpdate(matchScoreData);
osuPacketWriter.MatchScoreUpdate(scoreFrameData);
// Send the newly updated score to all users in the match
this.matchStream.Send(osuPacketWriter.toBuffer);

View file

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

View file

@ -10,6 +10,9 @@ 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;
@ -21,6 +24,9 @@ export default class Shared {
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!");
@ -33,7 +39,7 @@ export default class Shared {
// Add the bot user
this.users = new UserArray();
const botUser = this.users.add("bot", new User(3, "SillyBot", "bot", this));
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);
@ -46,5 +52,9 @@ export default class Shared {
this.multiplayerManager = new MultiplayerManager(this);
this.privateChatManager = new PrivateChatManager(this);
// DB Repos
this.userInfoRepository = new UserInfoRepository(this);
this.userModesInfoRepository = new UserModesInfoRepository(this);
}
}

View file

@ -1,21 +1,15 @@
import LatLng from "./LatLng";
import { RankingModes } from "../enums/RankingModes";
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";
const rankingModes = [
"pp_raw",
"ranked_score",
"avg_accuracy"
];
import PresenceData from "../interfaces/packetTypes/PresenceData";
import { Permissions } from "../enums/Permissions";
export default class User {
private static readonly EMPTY_BUFFER = Buffer.alloc(0);
public shared:Shared;
public id:number;
@ -23,12 +17,13 @@ export default class User {
public uuid:string;
public readonly connectTime:number = Date.now();
public timeoutTime:number = Date.now() + 30000;
public queue:Buffer = User.EMPTY_BUFFER;
public queue:Buffer = Buffer.allocUnsafe(0);
// Binato data
public rankingMode:RankingModes = RankingModes.PP;
public rankingMode:RankingMode = RankingMode.PP;
public spectatorStream?:DataStream;
public spectatingUser?:User;
public permissions:Permissions;
// osu! data
public playMode:number = 0;
@ -66,10 +61,11 @@ export default class User {
return user0.uuid === user1.uuid;
}
public constructor(id:number, username:string, uuid:string, shared:Shared) {
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;
}
@ -80,11 +76,11 @@ export default class User {
}
clearQueue() {
this.queue = User.EMPTY_BUFFER;
this.queue = Buffer.allocUnsafe(0);
}
// Updates the user's current action
updatePresence(action:any) {
updatePresence(action:PresenceData) {
this.actionID = action.status;
this.actionText = action.statusText;
this.beatmapChecksum = action.beatmapChecksum;
@ -99,26 +95,27 @@ export default class User {
// Gets the user's score information from the database and caches it
async updateUserInfo(forceUpdate:boolean = false) {
const userScoreDB = await this.shared.database.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 this.shared.database.query(`SELECT user_id, ${mappedRankingMode} FROM users_modes_info WHERE mode_id = ? ORDER BY ${mappedRankingMode} DESC`, [this.playMode]);
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 || userRankDB == null) throw "fuck";
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 RankingModes.PP:
case RankingMode.PP:
if (this.pp != userScoreDB.pp_raw)
userScoreUpdate = true;
break;
case RankingModes.RANKED_SCORE:
case RankingMode.RANKED_SCORE:
if (this.rankedScore != userScoreDB.ranked_score)
userScoreUpdate = true;
break;
case RankingModes.AVG_ACCURACY:
case RankingMode.AVG_ACCURACY:
if (this.accuracy != userScoreDB.avg_accuracy)
userScoreUpdate = true;
break;
@ -129,14 +126,6 @@ export default class User {
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;

View file

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

View file

@ -1,3 +1,5 @@
import { Permissions } from "../../enums/Permissions";
export default class UserInfo {
id:number = Number.MIN_VALUE;
username:string = "";
@ -10,7 +12,7 @@ export default class UserInfo {
last_login_date:Date = new Date(0);
last_played_mode:number = Number.MIN_VALUE;
online_now:boolean = false;
tags:number = Number.MIN_VALUE;
tags:Permissions = Permissions.None;
supporter:boolean = false;
web_session:string = "";
verification_needed:boolean = false;

View file

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

View file

@ -1,7 +1,7 @@
import User from "../objects/User";
export default function AddFriend(user:User, friendId:number) {
user.shared.database.query("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [
export default async function AddFriend(user:User, friendId:number) {
await user.shared.database.execute("INSERT INTO friends (user, friendsWith) VALUES (?, ?)", [
user.id, friendId
]);
}

View file

@ -1,7 +1,8 @@
import PresenceData from "../interfaces/packetTypes/PresenceData";
import User from "../objects/User";
import StatusUpdate from "./StatusUpdate";
export default function ChangeAction(user:User, data:any) {
export default function ChangeAction(user:User, data:PresenceData) {
user.updatePresence(data);
if (user.spectatorStream != null) {

View file

@ -13,7 +13,7 @@ export default async function Logout(user:User) {
// Remove user from user list
user.shared.users.remove(user.uuid);
await user.shared.database.query("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.shared.users.getLength() - 1]);
await user.shared.database.execute("UPDATE osu_info SET value = ? WHERE name = 'online_now'", [user.shared.users.getLength() - 1]);
ConsoleHelper.printBancho(`User logged out, took ${Date.now() - logoutStartTime}ms. [User: ${user.username}]`);
}

View file

@ -1,4 +1,4 @@
import MessageData from "../interfaces/MessageData";
import MessageData from "../interfaces/packetTypes/MessageData";
import Shared from "../objects/Shared";
import PrivateChannel from "../objects/PrivateChannel";
import User from "../objects/User";

View file

@ -1,7 +1,7 @@
import User from "../objects/User";
export default function RemoveFriend(user:User, friendId:number) {
user.shared.database.query("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [
export default async function RemoveFriend(user:User, friendId:number) {
await user.shared.database.execute("DELETE FROM friends WHERE user = ? AND friendsWith = ? LIMIT 1", [
user.id, friendId
]);
}

View file

@ -0,0 +1,10 @@
import { Channel } from "diagnostics_channel";
import MessageData from "../interfaces/packetTypes/MessageData";
import User from "../objects/User";
export default function SendPublicMessage(user:User, message:MessageData) {
const channel = user.shared.chatManager.GetChannelByName(message.target);
if (channel instanceof Channel) {
channel.SendMessage(user, message.message);
}
}

View file

@ -1,10 +1,13 @@
import Shared from "../objects/Shared";
import { RankingModes } from "../enums/RankingModes";
import { RankingMode } from "../enums/RankingMode";
import User from "../objects/User";
import osu from "../../osuTyping";
export default function StatusUpdate(arg0:User | Shared, id:number) {
if (id == 3) return; // Ignore Bot
// Ignore Bot
if (id == 3) {
return Buffer.allocUnsafe(0);
}
// Create new osu packet writer
const osuPacketWriter = osu.Bancho.Writer();
@ -18,9 +21,11 @@ export default function StatusUpdate(arg0:User | Shared, id:number) {
// Get user's class
const userData = shared.users.getById(id);
if (userData == null) return;
if (userData == null) {
return Buffer.allocUnsafe(0);
}
let UserStatusObject = {
osuPacketWriter.HandleOsuUpdate({
userId: userData.id,
status: userData.actionID,
statusText: userData.actionText,
@ -33,10 +38,8 @@ export default function StatusUpdate(arg0:User | Shared, id:number) {
playCount: userData.playCount,
totalScore: userData.totalScore,
rank: userData.rank,
performance: (userData.rankingMode == RankingModes.PP ? userData.pp : 0)
};
osuPacketWriter.HandleOsuUpdate(UserStatusObject);
performance: (userData.rankingMode == RankingMode.PP ? userData.pp : 0)
});
// Send data to user's queue
if (arg0 instanceof User) {

View file

@ -11,9 +11,9 @@ export default function UserPresenceBundle(arg0:User | Shared) : Buffer {
shared = arg0;
}
let userIds:Array<number> = new Array<number>();
const userIds:Array<number> = new Array<number>();
for (let userData of shared.users.getIterableItems()) {
for (const userData of shared.users.getIterableItems()) {
userIds.push(userData.id);
}

View file

@ -6,7 +6,7 @@ import UserPresenceBundle from "./UserPresenceBundle";
export default function UserStatsRequest(user:User, data:Array<number>) {
UserPresenceBundle(user);
for (let id of data) {
for (const id of data) {
UserPresence(user, id);
StatusUpdate(user, id);
}

View file

@ -0,0 +1,59 @@
import { RowDataPacket } from "mysql2";
import Database from "../objects/Database";
import Shared from "../objects/Shared";
import UserInfo from "../objects/database/UserInfo";
export default class UserInfoRepository {
private database:Database;
public constructor(shared:Shared) {
this.database = shared.database;
}
public async selectById(id:number) {
const query = await this.database.query("CALL SelectUserInfoById(?)", [id]);
if (query != null) {
const userInfo = new UserInfo();
populateUserInfoFromRowDataPacket(userInfo, query[0][0]);
return userInfo;
}
return null;
}
public async selectByUsername(username:string) {
const query = await this.database.query("CALL SelectUserInfoByUsername(?)", [username]);
if (query != null) {
const userInfo = new UserInfo();
populateUserInfoFromRowDataPacket(userInfo, query[0][0]);
return userInfo;
}
return null;
}
}
function populateUserInfoFromRowDataPacket(userInfo:UserInfo, rowDataPacket:RowDataPacket) {
userInfo.id = rowDataPacket["id"];
userInfo.username = rowDataPacket["username"];
userInfo.username_safe = rowDataPacket["username_safe"];
userInfo.password_hash = rowDataPacket["password_hash"];
userInfo.password_salt = rowDataPacket["password_salt"];
userInfo.email = rowDataPacket["email"];
userInfo.country = rowDataPacket["country"];
userInfo.reg_date = rowDataPacket["reg_date"];
userInfo.last_login_date = rowDataPacket["last_login_date"];
userInfo.last_played_mode = rowDataPacket["last_played_mode"];
userInfo.online_now = rowDataPacket["online_now"];
userInfo.tags = rowDataPacket["tags"];
userInfo.supporter = rowDataPacket["supporter"];
userInfo.web_session = rowDataPacket["web_session"];
userInfo.verification_needed = rowDataPacket["verification_needed"];
userInfo.password_change_required = rowDataPacket["password_change_required"];
userInfo.has_old_password = rowDataPacket["has_old_password"];
userInfo.password_reset_key = rowDataPacket["password_reset_key"];
userInfo.away_message = rowDataPacket["away_message"];
userInfo.last_modified_time = rowDataPacket["last_modified_time"];
userInfo.is_deleted = rowDataPacket["is_deleted"];
}

View file

@ -0,0 +1,72 @@
import { RowDataPacket } from "mysql2";
import Database from "../objects/Database";
import Shared from "../objects/Shared";
import UserModeInfo from "../objects/database/UserModeInfo";
import { Mode } from "fs";
import { RankingMode } from "../enums/RankingMode";
export default class UserModesInfoRepository {
private database:Database;
public constructor(shared:Shared) {
this.database = shared.database;
}
public async selectByUserIdModeId(id:number, mode:Mode) {
const query = await this.database.query("CALL SelectUserModesInfoByUserIdModeId(?,?)", [id, mode]);
if (query != null) {
const userModeInfo = new UserModeInfo();
populateUserModeInfoFromRowDataPacket(userModeInfo, query[0][0]);
return userModeInfo;
}
return null;
}
public async selectRankByIdModeIdRankingMode(id:number, mode:Mode, rankingMode:RankingMode) : Promise<number | null> {
let query:RowDataPacket[] | undefined;
switch (rankingMode) {
case RankingMode.RANKED_SCORE:
query = await this.database.query("CALL SelectUserScoreRankByIdModeId(?,?)", [id, mode]);
break;
case RankingMode.AVG_ACCURACY:
query = await this.database.query("CALL SelectUserAccRankByIdModeId(?,?)", [id, mode]);
break;
case RankingMode.PP:
default:
query = await this.database.query("CALL SelectUserPPRankByIdModeId(?,?)", [id, mode]);
break;
}
if (query != null && query.length != 0) {
return query[0][0].rank;
}
return null;
}
}
function populateUserModeInfoFromRowDataPacket(userModeInfo:UserModeInfo, rowDataPacket:RowDataPacket) {
userModeInfo.n = rowDataPacket["n"];
userModeInfo.user_id = rowDataPacket["user_id"];
userModeInfo.mode_id = rowDataPacket["mode_id"];
userModeInfo.count300 = rowDataPacket["count300"];
userModeInfo.count100 = rowDataPacket["count100"];
userModeInfo.count50 = rowDataPacket["count50"];
userModeInfo.countmiss = rowDataPacket["countmiss"];
userModeInfo.playcount = rowDataPacket["playcount"];
userModeInfo.total_score = rowDataPacket["total_score"];
userModeInfo.ranked_score = rowDataPacket["ranked_score"];
userModeInfo.pp_rank = rowDataPacket["pp_rank"];
userModeInfo.pp_raw = rowDataPacket["pp_raw"];
userModeInfo.count_rank_ss = rowDataPacket["count_rank_ss"];
userModeInfo.count_rank_s = rowDataPacket["count_rank_s"];
userModeInfo.count_rank_a = rowDataPacket["count_rank_a"];
userModeInfo.pp_country_rank = rowDataPacket["pp_country_rank"];
userModeInfo.playtime = rowDataPacket["playtime"];
userModeInfo.avg_accuracy = rowDataPacket["avg_accuracy"];
userModeInfo.level = rowDataPacket["level"];
userModeInfo.is_deleted = rowDataPacket["is_deleted"];
}

1
server/types/DBTypes.ts Normal file
View file

@ -0,0 +1 @@
export type DBInDataType = string | number | null | undefined;

View file

@ -1,12 +1,11 @@
import { readdirSync, rmSync, renameSync } from "fs";
import { readdirSync, rmSync, readFileSync } from "fs";
const libFiles = readdirSync("./build");
const mangled = readFileSync("./build/.MANGLED").toString() === "false";
for (const file of libFiles) {
if (!file.startsWith("Binato.min.js")) {
if (!file.startsWith(mangled ? "Binato.min.js" : "Binato.js")) {
rmSync(`./build/${file}`, { recursive: true });
}
}
//renameSync("./build/combined.js", "./build/index.js");
//renameSync("./build/combined.d.ts", "./build/index.d.ts");
}

View file

@ -5,6 +5,7 @@
import { readdirSync, lstatSync, readFileSync, writeFileSync } from "fs";
let tsFileData:Array<string> = new Array<string>();
const tsHighPriorityData:Array<string> = new Array<string>();
const tsEvenFirsterData:Array<string> = new Array<string>();
const tsVeryFirstData:Array<string> = new Array<string>();
const tsFirstFileData:Array<string> = new Array<string>();
@ -25,9 +26,11 @@ function readDir(nam:string) {
} else if (file.endsWith(".ts")) {
if (file == "Binato.ts") {
tsLastFileData.push(readFileSync((`${nam}/${file}`).replace("//", "/")).toString());
} else if (file.includes("BaseCommand")) {
tsHighPriorityData.push(readFileSync((`${nam}/${file}`).replace("//", "/")).toString());
} else if (nam.includes("commands") || file.includes("ConsoleHelper")) {
tsEvenFirsterData.push(readFileSync((`${nam}/${file}`).replace("//", "/")).toString());
} else if (file.includes("FunkyArray") || file.includes("ChatManager") || file.includes("MultiplayerManager") || file === "Bot.ts") {
} else if (file.includes("FunkyArray") || file.includes("ChatManager") || file.includes("MultiplayerManager") || file === "BaseCommand.ts" || file.includes("Bot.ts")) {
tsVeryFirstData.push(readFileSync((`${nam}/${file}`).replace("//", "/")).toString());
} else if (nam.includes("enum") || nam.includes("packets") || (nam.includes("objects") && !file.includes("FunkyArray") ) || file.includes("SpectatorManager")) {
tsFirstFileData.push(readFileSync((`${nam}/${file}`).replace("//", "/")).toString());
@ -40,7 +43,7 @@ function readDir(nam:string) {
readDir("./");
tsFileData = tsFileData.concat(tsEvenFirsterData).concat(tsVeryFirstData).concat(tsFirstFileData).concat(tsEverythingElse).concat(tsLastFileData);
tsFileData = tsFileData.concat(tsHighPriorityData).concat(tsEvenFirsterData).concat(tsVeryFirstData).concat(tsFirstFileData).concat(tsEverythingElse).concat(tsLastFileData);
const combinedFiles = tsFileData.join("\n");
@ -53,7 +56,7 @@ import { Registry, collectDefaultMetrics } from "prom-client";
import { RedisClientType, createClient } from "redis";
import { readFileSync, existsSync } from "fs";
import { randomBytes, pbkdf2 } from "crypto";
import { createPool, Pool } from "mysql2";
import { createPool, Pool, RowDataPacket } from "mysql2";
import * as dyetty from "dyetty";
import fetch from "node-fetch";
import http from "http";`);

View file

@ -2,13 +2,14 @@ import { readFileSync, writeFileSync } from "fs";
import { minify } from "terser";
const DISABLE = false;
writeFileSync("./build/.MANGLED", `${DISABLE}`);
if (DISABLE) {
writeFileSync("./build/Binato.min.js", readFileSync("./build/combined.js"));
writeFileSync("./build/Binato.js", readFileSync("./build/index.js"));
console.warn("[WARNING] mangle.ts is disabled!");
} else {
(async () => {
const mangled = await minify(readFileSync("./build/combined.js").toString(), {
const mangled = await minify(readFileSync("./build/index.js").toString(), {
mangle: true,
toplevel: true,
});