Skip to content

Commit

Permalink
Team registration for tournaments (#205)
Browse files Browse the repository at this point in the history
* initial team model and stuff

* a few team endpoints, teams discordcmd, refresh token functionality

* update migration

* ok i made all the team endpoints

* npm run audit

* update migration

* mode ID filters for bws

* fix build

* discord commands and restructure db function locations

* rename save to upload

* test fixes

* more fixes and add public folder

* no support for gifs (yet)

* more fixes and also better image cropping

* more fixes, more discord cmds, create getTeamInvites

* change team ava url to explicit api url due to proxy

* ok 10 mb kinda ridiculous

* extra security (dont use discord ID lookup), more test fixes

* tournament registration cron

* cron fixes

* ease on cron queries

* fix missing inv accept check + rewrite for loop

* optimize discord invite q (and support actionRow limit)

* use r2 for team avatars

* feat: add base for osu irc bot (#208)

* feat: add base for osu irc bot

* fix: connection log typo

* style: simplify self-message check

* feat: add botAccount boolean

* style: whitespace

* feat: multiplayer commands, listen to channel message event

* style: remove unnecessary parentheses

* fix: placement of parentheses

* feat: reduce command handler duplication

* fix: whitespace

* fix: more whitespace

* Match Infrastructure (#207)

* initial match model creation

* initial match migration and models and routing

* pickban order(maporder) entity

* additional properties for match specs + update migration + change match name (sql reserved keyword)

* remove discriminators in discord + errors

* rename match to matchup everywhere

* matchup generator endpoint (remove stages)

* fix migration also add a first property

* npm run lintfix

* update readme for ircbot

* intiial cron setup

* add matchupGroup to models for qualis

* feat: require a qualifier date when a team registers for a tournament with a qualifier stage (#211)

* feat: require a qualifier date on team register if tourney has qualifiers, create matchup

* fix: wtf

* Auto-running Qualifiers (#213)

* renaming irc to bancho

* more bancho renaming

* some pool check fixes for create/publish

* Create match creation logic stuff

* model + migration fixes

* missing checks on slot map count

* finish initial setup for auto-running qualifier

* 🚑 ⬆️ Upgrade nodesu and bancho.js, fixing typings compilation (#214)

* import osu bot

* fixes to miscellaneous stuff + build

* finish qualis bot

* add missing leftjoin in register endpoint

* more refbot fixes

* change cron delay to 10 seconds

* more fixes and also actually make the timers matter

* check all players in lobby before running autostart

* final fixes before i lose my mind refactoring

* REFACTOR!!!!!

* poon fixes

* nicer log storage Yey

* fix matchup running in cron-runner instead of bancho (+ dupe bancho client instances)

* mod function fix

* wrote wrong publicurl name in docker-compose.json

* actually end match

* remove unnecessary saves keep matchup save for match end

* poon suggestion

* Bancho Message Storage (#215)

* matchupmessages model and migration

* add matchupmessage logic to ref bot

* testing

* batch save and clear timer on end

* date save

---------

Co-authored-by: Hugo Denizart <[email protected]>

* update koa-session to circumvent koa-passport 6 issues rkusa/koa-passport#181

* fixes and refactors to osu apiv2

* add typing to badgefilter

* annoying build fixes and mode type

---------

Co-authored-by: James <[email protected]>
Co-authored-by: Hugo Denizart <[email protected]>
  • Loading branch information
3 people authored Jul 2, 2023
1 parent a43d716 commit 1c0b83e
Show file tree
Hide file tree
Showing 152 changed files with 6,596 additions and 1,731 deletions.
1 change: 1 addition & 0 deletions BanchoBot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Placeholder
15 changes: 15 additions & 0 deletions BanchoBot/commands/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BanchoMessage } from "bancho.js";
import { Command } from "./index";

async function run (message: BanchoMessage, ...args: string[]) {
await message.user.sendMessage("Invoked example command!");
}

const example: Command = {
name: "example",
aliases: ["test"],
multiplayerCommand: false,
run,
};

export default example;
65 changes: 65 additions & 0 deletions BanchoBot/commands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { BanchoMessage } from "bancho.js";
import example from "./example";
import exampleMulti from "./multiplayer/example";
import { Multi } from "nodesu";

interface GlobalCommand {
name: string;
aliases?: string[];
multiplayerCommand: false;
run: (message: BanchoMessage, ...args: string[]) => Promise<void>;
}

interface MultiplayerCommand {
name: string;
aliases?: string[];
multiplayerCommand: true;
run: (message: BanchoMessage, multi: Multi, ...args: string[]) => Promise<void>;
}

type Command = GlobalCommand | MultiplayerCommand;

const commands: Command[] = [];

// all commands
commands.push(example);
commands.push(exampleMulti);

function handleCommand(
commandName: string,
message: BanchoMessage,
...args: string[]
): Promise<void>;

function handleCommand(
commandName: string,
message: BanchoMessage,
multi: Multi,
...args: string[]
): Promise<void>;

async function handleCommand (commandName: string, message: BanchoMessage, multiOrArg?: Multi | string, ...args: string[]) {
const command = commands.find(
(cmd) => cmd.name == commandName.toLowerCase()
|| cmd.aliases?.includes(commandName.toLowerCase())
);

if (!command)
return;

if (command.multiplayerCommand) {
if (!(multiOrArg instanceof Multi)) {
console.error("Invoked multiplayer command without a multi match");
return;
}

await command.run(message, multiOrArg, ...args);
} else {
const allArgs = multiOrArg ? [multiOrArg as string, ...args] : args;
await command.run(message, ...allArgs);
}

console.log(`${message.user.ircUsername} executed command ${command.name}`);
}

export { Command, handleCommand };
16 changes: 16 additions & 0 deletions BanchoBot/commands/multiplayer/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BanchoMessage } from "bancho.js";
import { Command } from "../index";
import { Multi } from "nodesu";

async function run (message: BanchoMessage, multi: Multi, ...args: string[]) {
await message.user.sendMessage(`Invoked example multi command in multi ID ${multi.match.id}!`);
}

const exampleMulti: Command = {
name: "example_multi",
aliases: ["test_multi"],
multiplayerCommand: true,
run,
};

export default exampleMulti;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Matchup } from "../../../../Models/tournaments/matchup";
import { StageType } from "../../../../Models/tournaments/stage";
import getUser from "../../../../Server/functions/get/getUser";

export default async function allAllowedUsersForMatchup (matchup: Matchup) {
if (!matchup.stage)
throw new Error("Matchup has no stage");

const botUsers = await Promise.all([
{ osu: { username: "Corsace", userID: "29191632" } },
{ osu: { username: "BanchoBot", userID: "3" } },
].map((user) => getUser(user.osu.userID, "osu", true)));

if (matchup.stage.stageType === StageType.Qualifiers)
return matchup.teams!
.flatMap(team => team.members.concat(team.manager))
.concat(botUsers)
.filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i);

return matchup.team1!.members
.concat(
matchup.team1!.manager,
matchup.team2!.members,
matchup.team2!.manager,
botUsers
)
.filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i);
}
16 changes: 16 additions & 0 deletions BanchoBot/functions/tournaments/matchup/allPlayersInMatchup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { BanchoLobbyPlayer } from "bancho.js";
import { Matchup } from "../../../../Models/tournaments/matchup";
import { StageType } from "../../../../Models/tournaments/stage";
import { Team } from "../../../../Models/tournaments/team";

export default function allPlayersInMatchup (matchup: Matchup, playersInLobby: BanchoLobbyPlayer[]) {
const numMembersInLobby = (team: Team) => team.members.filter(m =>
playersInLobby.some(p => p.user.id.toString() === m.osu.userID)
).length;

if (matchup.stage!.stageType === StageType.Qualifiers)
return matchup.teams!.map(numMembersInLobby).every(n => n === matchup.stage!.tournament.matchupSize);

return numMembersInLobby(matchup.team1!) === matchup.stage!.tournament.matchupSize &&
numMembersInLobby(matchup.team2!) === matchup.stage!.tournament.matchupSize;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { BanchoLobby, BanchoLobbyPlayer } from "bancho.js";

export default function areAllPlayersInAssignedSlots (mpLobby: BanchoLobby, playersPlaying: BanchoLobbyPlayer[] | undefined) {
return !playersPlaying || playersPlaying.every(p => mpLobby.slots.some(s => s !== null && s.user.id === p.user.id));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BanchoLobby } from "bancho.js";
import { MappoolSlot } from "../../../../Models/tournaments/mappools/mappoolSlot";
import getMappoolSlotMods from "./getMappoolSlotMods";

export default function doAllPlayersHaveCorrectMods (mpLobby: BanchoLobby, slotMod: MappoolSlot) {
if (typeof slotMod.userModCount !== "number" && typeof slotMod.uniqueModCount !== "number")
return true;

const allowedMods = getMappoolSlotMods(slotMod.allowedMods);
if (
mpLobby.slots.some(slot =>
slot.mods.some(mod =>
!allowedMods.some(allowedMod => allowedMod.enumValue === mod.enumValue)
)
)
)
return false;
}
7 changes: 7 additions & 0 deletions BanchoBot/functions/tournaments/matchup/getMappoolSlotMods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { BanchoMod, BanchoMods } from "bancho.js";

export default function getMappoolSlotMods (allowedModsEnum: number | null | undefined): BanchoMod[] {
const modParse = BanchoMods.parseBitFlags(allowedModsEnum || 0, true).concat(BanchoMods.NoFail); // Default NoFail requirements

return modParse.filter((v, i, a) => a.findIndex(m => m.enumValue === v.enumValue) === i); // If a mappool slot's allowedMods has NoFail already, then it will filter it out.
}
20 changes: 20 additions & 0 deletions BanchoBot/functions/tournaments/matchup/getUserInMatchup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { BanchoMessage } from "bancho.js";
import { User } from "../../../../Models/user";

const bots = {
"Corsace": "29191632",
"BanchoBot": "3",
};

export default async function getUserInMatchup (users: User[], message: BanchoMessage): Promise<User> {
if (!message.user || !message.user.id) {
const id = bots[message.user?.ircUsername || "BanchoBot"];
return users.find(user => user.osu.userID === id)!;
}

const user = users.find(user => user.osu.userID === message.user!.id.toString());
if (user)
return user;

throw new Error("User not found");
}
13 changes: 13 additions & 0 deletions BanchoBot/functions/tournaments/matchup/invitePlayersToLobby.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BanchoLobby } from "bancho.js";
import { Matchup } from "../../../../Models/tournaments/matchup";
import { StageType } from "../../../../Models/tournaments/stage";

export default async function invitePlayersToLobby (matchup: Matchup, mpLobby: BanchoLobby) {
if (matchup.stage!.stageType === StageType.Qualifiers) {
const users = matchup.teams!.flatMap(team => team.members.concat(team.manager).filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i));
await Promise.all(users.map(user => mpLobby.invitePlayer(`#${user.osu.userID}`)));
} else {
const users = matchup.team1!.members.concat(matchup.team1!.manager).concat(matchup.team2!.members).concat(matchup.team2!.manager).filter((v, i, a) => a.findIndex(t => t.ID === v.ID) === i);
await Promise.all(users.map(m => mpLobby.invitePlayer(`#${m.osu.userID}`)));
}
}
14 changes: 14 additions & 0 deletions BanchoBot/functions/tournaments/matchup/isPlayerInMatchup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Matchup } from "../../../../Models/tournaments/matchup";
import { StageType } from "../../../../Models/tournaments/stage";
import { Team } from "../../../../Models/tournaments/team";

export default function isPlayerInMatchup (matchup: Matchup, playerID: string, checkManager: boolean): boolean {
const isPlayerInTeam = (team: Team) =>
(checkManager && team.manager.osu.userID === playerID) ||
team.members.some(player => player.osu.userID === playerID);

if (matchup.stage!.stageType === StageType.Qualifiers)
return matchup.teams!.some(isPlayerInTeam);
else
return isPlayerInTeam(matchup.team1!) || isPlayerInTeam(matchup.team2!);
}
128 changes: 128 additions & 0 deletions BanchoBot/functions/tournaments/matchup/loadNextBeatmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { BanchoChannel, BanchoLobby, BanchoMessage } from "bancho.js";
import { Beatmap } from "../../../../Models/beatmap";
import { Mappool } from "../../../../Models/tournaments/mappools/mappool";
import { MappoolMap } from "../../../../Models/tournaments/mappools/mappoolMap";
import { MappoolSlot } from "../../../../Models/tournaments/mappools/mappoolSlot";
import { Matchup } from "../../../../Models/tournaments/matchup";
import { leniencyTime, StageType } from "../../../../Models/tournaments/stage";
import getMappoolSlotMods from "./getMappoolSlotMods";

async function getNextBeatmap (matchup: Matchup, mpLobby: BanchoLobby, mpChannel: BanchoChannel, pools: Mappool[]): Promise<[Beatmap, number | null | undefined, boolean] | null> {
return new Promise((resolve, reject) => {
if (matchup.stage!.stageType === StageType.Qualifiers) {
const pool = pools[0];
const beatmaps = pool.slots
.flatMap(slot => slot.maps
.sort((a, b) => a.order - b.order)
)
.filter(map => !matchup.maps!
.some(matchMap => matchMap.map.beatmap?.ID === map.beatmap?.ID)
);

if (!matchup.stage!.qualifierTeamChooseOrder || beatmaps.length === 1) {
if (beatmaps.length === 0)
return resolve(null);

if (!beatmaps[0].beatmap)
return reject(new Error("Beatmap doesn't exist CONTACT CORSACE IMMEDIATELY"));

const slotMod = pool.slots.find(slot => slot.maps.some(map => map.beatmap?.ID === beatmaps[0].beatmap?.ID))!;
return resolve([beatmaps[0].beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]);
}
let gotBeatmap = false;
const messageHandler = async (message: BanchoMessage) => {
if (message.user.ircUsername === "BanchoBot" && message.content === "Countdown finished") {
setTimeout(() => {
if (gotBeatmap)
return;
if (beatmaps.length === 0)
return resolve(null);
if (!beatmaps[0].beatmap)
return reject(new Error("Beatmap doesn't exist CONTACT CORSACE IMMEDIATELY"));

mpChannel.sendMessage("OK U GUYS ARE TAKING TOO LON g im picking a random map for all of u to play now GL");
const slotMod = pool.slots.find(slot => slot.maps.some(map => map.beatmap?.ID === beatmaps[0].beatmap?.ID))!;
mpChannel.removeListener("message", messageHandler);
return resolve([beatmaps[0].beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]);
}, leniencyTime);
}

const isManagerMessage = !message.self && message.user.id && (
(
matchup.stage?.stageType === StageType.Qualifiers &&
matchup.teams?.some(team => team.manager.osu.userID === message.user.id.toString())
) ||
[matchup.team1, matchup.team2].some(team => team?.manager.osu.userID === message.user.id.toString()));

if (!isManagerMessage)
return;

const contentParts = message.content.split(" ");
const command = contentParts[0];
const param = ["!map", "!pick"].includes(command) ? contentParts[1] : command;

if (param.length > 4)
return;

const mapById = (id: number) => beatmaps.find(map => map.beatmap!.ID === id);
const mapBySlotOrder = (slot: MappoolSlot, order: number) => slot.maps.find(map => map.order === order);

const id = parseInt(param);
let map: MappoolMap | undefined;

if (isNaN(id)) {
const nums = param.match(/\d+/g) || [];
const order = parseInt(nums[nums.length - 1]);
const slot = pool.slots.find(slot => param.toLowerCase().includes(slot.acronym.toLowerCase()));

if (!slot)
return;
if (isNaN(order) && slot.maps.length > 1)
return await mpChannel.sendMessage(`Slot ${slot.acronym} has more than 1 map, specify a map #`);

map = mapBySlotOrder(slot, isNaN(order) ? 1 : order);
} else
map = mapById(id);

if (!map)
return await mpChannel.sendMessage("The map ID or slot provided is INVALID .");
if (!map.beatmap)
return reject(new Error("Map is missing beatmap CONTACT CORSACE IMMEDIATELY"));

const slotMod = pool.slots.find(slot => slot.maps.some(slotMap => slotMap.beatmap!.ID === map!.beatmap!.ID))!;

if (!beatmaps.some(beatmap => beatmap.beatmap!.ID === map!.beatmap!.ID))
return await mpChannel.sendMessage(`${slotMod.acronym}${map.order} is ALREADY PLAYED .`);

gotBeatmap = true;
mpChannel.removeListener("message", messageHandler);
return resolve([map.beatmap, slotMod.allowedMods, typeof slotMod.allowedMods !== "number" || typeof slotMod.uniqueModCount === "number" || typeof slotMod.userModCount === "number"]);
};

mpChannel.sendMessage("It's time to pick a map!!11!1");
mpLobby.startTimer(matchup.stage!.tournament.mapTimer || 90);
mpChannel.on("message", messageHandler);
} else {
// TODO: implement this
reject(new Error("Not implemented"));
}
});
}

export default async function loadNextBeatmap (matchup: Matchup, mpLobby: BanchoLobby, mpChannel: BanchoChannel, pools: Mappool[], possibleEnd: boolean): Promise<boolean> {
const nextBeatmapInfo = await getNextBeatmap(matchup, mpLobby, mpChannel, pools);
if (!nextBeatmapInfo) {
if (!possibleEnd)
throw new Error("No maps found? This is probably a mistake CONTACT CORSACE IMMEDIATELY");

return true;
}

const mods = getMappoolSlotMods(nextBeatmapInfo[1]);
await Promise.all([
mpLobby.setMap(nextBeatmapInfo[0].ID),
mpLobby.setMods(mods, nextBeatmapInfo[2]),
]);
await mpLobby.startTimer(matchup.stage!.tournament.readyTimer || 90);
return false;
}
Loading

0 comments on commit 1c0b83e

Please sign in to comment.