From c8039213aacc19c5bdcecf4c1ccd03eb1383689f Mon Sep 17 00:00:00 2001 From: JensForstmann Date: Fri, 11 Oct 2024 21:55:12 +0200 Subject: [PATCH] Add possibility to force players by their steam ids into a specific team. --- backend/src/match.ts | 25 ++++++--- backend/src/player.ts | 35 ++++++++++-- backend/src/routes.ts | 2 + backend/swagger.json | 16 +++++- common/types/player.ts | 6 ++- common/types/team.ts | 4 ++ frontend/src/components/CreateUpdateMatch.tsx | 54 ++++++++++++++++++- frontend/src/utils/copyObject.ts | 2 +- 8 files changed, 130 insertions(+), 14 deletions(-) diff --git a/backend/src/match.ts b/backend/src/match.ts index 0fca173..203271b 100644 --- a/backend/src/match.ts +++ b/backend/src/match.ts @@ -447,7 +447,13 @@ export const onLog = async (match: Match, body: string) => { // logBuffer was empty before -> no other onLogLine is in progress right now while (match.logBuffer.length > 0) { const oldestLine = match.logBuffer[0]!; - await onLogLine(match, oldestLine); + try { + await onLogLine(match, oldestLine); + } catch (err) { + match.log(`Error processing incoming log line from game server: ${oldestLine}`); + match.log(`Failed line: ${oldestLine}`); + match.log(`Message: ${err}`); + } match.logBuffer.splice(0, 1); } } @@ -586,10 +592,10 @@ const onPlayerLogLine = async ( const steamId64 = Player.getSteamID64(steamId); player = match.data.players.find((p) => p.steamId64 === steamId64); if (!player) { - player = Player.create(steamId, name); + player = Player.create(match, steamId, name); match.log(`Player ${player.steamId64} (${name}) created`); match.data.players.push(player); - player = match.data.players[match.data.players.length - 1]!; + player = match.data.players[match.data.players.length - 1]!; // re-assign to work nicely with changeListener (ProxyHandler) MatchService.scheduleSave(match); } if (player.name !== name) { @@ -828,10 +834,14 @@ export const sayWhatTeamToJoin = async (match: Match) => { const onTeamCommand: commands.CommandHandler = async ({ match, player, parameters }) => { const firstParameter = parameters[0]?.toUpperCase(); if (firstParameter === 'A' || firstParameter === 'B') { + if (Player.getForcedTeam(match, player.steamId64)) { + await say(match, `PLAYER ${escapeRconString(player.name)} CANNOT CHANGE THEIR TEAM`); + return; + } player.team = firstParameter === 'A' ? 'TEAM_A' : 'TEAM_B'; MatchService.scheduleSave(match); const team = getTeamByAB(match, player.team); - say( + await say( match, `PLAYER ${escapeRconString(player.name)} JOINED TEAM ${escapeRconString(team.name)}` ); @@ -839,14 +849,14 @@ const onTeamCommand: commands.CommandHandler = async ({ match, player, parameter } else { const playerTeam = player.team; if (playerTeam) { - say( + await say( match, - `YOU ARE IN TEAM ${playerTeam === 'TEAM_A' ? 'A' : 'B'}: ${escapeRconString( + `PLAYER ${escapeRconString(player.name)} IS IN TEAM ${playerTeam === 'TEAM_A' ? 'A' : 'B'}: ${escapeRconString( playerTeam === 'TEAM_A' ? match.data.teamA.name : match.data.teamB.name )}` ); } else { - say(match, `YOU HAVE NO TEAM`); + await say(match, `PLAYER ${escapeRconString(player.name)} HAS NO TEAM`); } } }; @@ -1049,6 +1059,7 @@ export const update = async (match: Match, dto: IMatchUpdateDto) => { } if (dto.teamA || dto.teamB) { + Player.forcePlayerIntoTeams(match); await setTeamNames(match); } diff --git a/backend/src/player.ts b/backend/src/player.ts index df96305..9116123 100644 --- a/backend/src/player.ts +++ b/backend/src/player.ts @@ -1,10 +1,13 @@ import SteamID from 'steamid'; -import { IPlayer, TTeamSides, TTeamString } from '../../common'; +import { IPlayer, TTeamAB, TTeamSides, TTeamString } from '../../common'; +import * as Match from './match'; -export const create = (steamId: string, name: string): IPlayer => { +export const create = (match: Match.Match, steamId: string, name: string): IPlayer => { + const steamId64 = getSteamID64(steamId); return { name: name, - steamId64: getSteamID64(steamId), + steamId64: steamId64, + team: getForcedTeam(match, steamId64), }; }; @@ -12,6 +15,32 @@ export const getSteamID64 = (steamId: string) => { return new SteamID(steamId).getSteamID64(); }; +export const getForcedTeam = (match: Match.Match, steamId64: string): TTeamAB | undefined => { + const isTeamA = match.data.teamA.playerSteamIds64?.includes(steamId64); + const isTeamB = match.data.teamB.playerSteamIds64?.includes(steamId64); + if (isTeamA === isTeamB) { + // either: configured for no teams + // or: configured for both teams + return undefined; + } + return isTeamA ? 'TEAM_A' : 'TEAM_B'; +}; + +export const forcePlayerIntoTeams = (match: Match.Match) => { + match.data.players.forEach((player, index) => { + const prevTeamAB = player.team; + const newTeamAB = getForcedTeam(match, player.steamId64); + if (newTeamAB && prevTeamAB !== newTeamAB) { + const fromTeam = prevTeamAB + ? ` from ${prevTeamAB} (${Match.getTeamByAB(match, prevTeamAB).name})` + : ''; + const toTeam = ` into team ${newTeamAB} (${Match.getTeamByAB(match, newTeamAB).name})`; + match.log(`Force player ${player.name}${fromTeam}${toTeam}`); + match.data.players[index]!.team = newTeamAB; + } + }); +}; + export const getSideFromTeamString = (teamString: TTeamString): TTeamSides | null => { switch (teamString) { case 'CT': diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 9eb4c84..f3d0ff4 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -47,6 +47,7 @@ const models: TsoaRoute.Models = { passthrough: { dataType: 'string' }, name: { dataType: 'string', required: true }, advantage: { dataType: 'double', required: true }, + playerSteamIds64: { dataType: 'array', array: { dataType: 'string' } }, }, additionalProperties: false, }, @@ -545,6 +546,7 @@ const models: TsoaRoute.Models = { name: { dataType: 'string', required: true }, passthrough: { dataType: 'string' }, advantage: { dataType: 'double' }, + playerSteamIds64: { dataType: 'array', array: { dataType: 'string' } }, }, additionalProperties: false, }, diff --git a/backend/swagger.json b/backend/swagger.json index f646e0f..65889e5 100644 --- a/backend/swagger.json +++ b/backend/swagger.json @@ -27,6 +27,13 @@ "type": "number", "format": "double", "description": "Advantage in map wins, useful for double elemination tournament finals." + }, + "playerSteamIds64": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Steam ids of players in \"Steam ID 64\" format. Will be forced into this team." } }, "required": ["name", "advantage"], @@ -524,7 +531,7 @@ }, "team": { "$ref": "#/components/schemas/TTeamAB", - "description": "Current team as they joined with `.team`." + "description": "Current team as they joined with `.team`.\nIf the player's steam id is in the team's `playerSteamIds64`\nthis cannot be changed and is always set to the team." }, "side": { "allOf": [ @@ -753,6 +760,13 @@ "type": "number", "format": "double", "description": "Advantage in map wins, useful for double elemination tournament finals." + }, + "playerSteamIds64": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Steam ids of players in \"Steam ID 64\" format. Will be forced into this team." } }, "required": ["name"], diff --git a/common/types/player.ts b/common/types/player.ts index af99bb1..f26cfa5 100644 --- a/common/types/player.ts +++ b/common/types/player.ts @@ -9,7 +9,11 @@ export interface IPlayer { steamId64: string; /** Name. */ name: string; - /** Current team as they joined with `.team`. */ + /** + * Current team as they joined with `.team`. + * If the player's steam id is in the team's `playerSteamIds64` + * this cannot be changed and is always set to the team. + */ team?: TTeamAB; /** Current ingame side. */ side?: TTeamSides | null; diff --git a/common/types/team.ts b/common/types/team.ts index 495beea..8c51774 100644 --- a/common/types/team.ts +++ b/common/types/team.ts @@ -11,6 +11,8 @@ export interface ITeam { name: string; /** Advantage in map wins, useful for double elemination tournament finals. */ advantage: number; + /** Steam ids of players in "Steam ID 64" format. Will be forced into this team.*/ + playerSteamIds64?: string[]; } /** @@ -25,6 +27,8 @@ export interface ITeamCreateDto { passthrough?: string; /** Advantage in map wins, useful for double elemination tournament finals. */ advantage?: number; + /** Steam ids of players in "Steam ID 64" format. Will be forced into this team.*/ + playerSteamIds64?: string[]; } /** Possible ingame sides of a player. */ diff --git a/frontend/src/components/CreateUpdateMatch.tsx b/frontend/src/components/CreateUpdateMatch.tsx index dd28ef4..0b37394 100644 --- a/frontend/src/components/CreateUpdateMatch.tsx +++ b/frontend/src/components/CreateUpdateMatch.tsx @@ -32,7 +32,7 @@ import { SelectInput, TextArea, TextInput, ToggleInput } from './Inputs'; import { Modal } from './Modal'; const Presets: Component<{ - onSelect: (preset: IPreset) => void; + onSelect: (preset: IMatchCreateDto) => void; matchCreateDto: IMatchCreateDto; }> = (props) => { const fetcher = createFetcher(); @@ -235,6 +235,10 @@ const minifyMapPool = (maps: string[]) => { return maps.map((map) => map.trim()).filter((l) => l.length > 0); }; +const minifyPlayerSteamIds64 = (steamIds: string[]) => { + return steamIds.map((steamId) => steamId.trim()).filter((l) => l.length > 0); +}; + export const CreateUpdateMatch: Component< ( | { @@ -265,6 +269,12 @@ export const CreateUpdateMatch: Component< try { const tempDto = copyObject(dto); tempDto.mapPool = minifyMapPool(tempDto.mapPool); + tempDto.teamA.playerSteamIds64 = minifyPlayerSteamIds64( + tempDto.teamA.playerSteamIds64 ?? [] + ); + tempDto.teamB.playerSteamIds64 = minifyPlayerSteamIds64( + tempDto.teamB.playerSteamIds64 ?? [] + ); setJson(props.getFinalDto?.(tempDto) ?? JSON.stringify(tempDto, undefined, 4)); } catch (err) { setJson('ERROR!\n' + err); @@ -812,6 +822,27 @@ export const CreateUpdateMatch: Component< )} onInput={(e) => setDto('teamA', 'passthrough', e.currentTarget.value)} /> +