1
0
mirror of https://github.com/lexogrine/cs2-react-hud.git synced 2026-05-04 12:13:11 +02:00

Updated setup to vite and moved to hooks instead of class

This commit is contained in:
Hubert Walczak
2023-11-02 12:11:03 +01:00
parent 44f173f23c
commit f88baa5fc9
90 changed files with 7411 additions and 2706 deletions
+152
View File
@@ -0,0 +1,152 @@
import { Instance, SignalData } from 'simple-peer';
import api from '..';
import { socket } from '../socket';
import Peer from 'simple-peer';
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
type OfferData = {
offer: SignalData,
}
type PeerInstance = Instance & { _remoteStreams: MediaStream[] }
type MediaStreamPlayer = {
peerConnection: PeerInstance | null;
steamid: string;
}
type ListenerType = ({ listener: (stream: MediaStream) => void, event: 'create', steamid: string } | ({ listener: () => void, event: 'destroy', steamid: string }));
type MediaStreamManager = {
blocked: string[];
blockedListeners: ((blocked: string[]) => void)[],
players: MediaStreamPlayer[];
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => void;
onStreamDestroy: (listener: () => void, steamid: string) => void;
onBlockedUpdate: (listener: (steamids: string[]) => void) => void;
removeListener: (listener: any) => void;
listeners: ListenerType[];
}
const mediaStreams: MediaStreamManager = {
blocked: [],
blockedListeners: [],
players: [],
listeners: [],
onStreamCreate: (listener: (stream: MediaStream) => void, steamid: string) => {
mediaStreams.listeners.push({ listener, event: "create", steamid });
},
onBlockedUpdate: (listener: (blocked: string[]) => void) => {
mediaStreams.blockedListeners.push(listener);
},
onStreamDestroy: (listener: () => void, steamid: string) => {
mediaStreams.listeners.push({ listener, event: "destroy", steamid });
},
removeListener: (listenerToRemove: any) => {
mediaStreams.listeners = mediaStreams.listeners.filter(listener => listener !== listenerToRemove);
mediaStreams.blockedListeners = mediaStreams.blockedListeners.filter(listener => listener !== listenerToRemove);
}
};
const getConnectionInfo = (steamid: string) => mediaStreams.players.find(player => player.steamid === steamid) || null;
const closeConnection = (steamid: string) => {
const connectionInfo = getConnectionInfo(steamid);
try {
if (connectionInfo) {
if (connectionInfo.peerConnection) {
connectionInfo.peerConnection.removeAllListeners();
connectionInfo.peerConnection.destroy();
}
connectionInfo.peerConnection = null;
}
} catch {
}
for (const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)) {
if (listener.event === "destroy") listener.listener();
}
mediaStreams.players = mediaStreams.players.filter(player => player.steamid !== steamid);
console.log(mediaStreams.players)
}
const initiateConnection = async () => {
const camera = await api.camera.get();
await wait(1000);
socket.emit("registerAsHUD", camera.uuid);
socket.on('playersCameraStatus', (players: { steamid: string, label: string, allow: boolean, active: boolean }[]) => {
const blockedSteamids = players.filter(player => !player.allow).map(player => player.steamid);
mediaStreams.blocked = blockedSteamids;
for (const listener of mediaStreams.blockedListeners) {
listener(blockedSteamids);
}
});
socket.on('offerFromPlayer', async (roomId: string, offerData: OfferData, steamid: string) => {
// It's not from available player, ignore incoming request
/*if(!camera.availablePlayers.find(player => player.steamid === steamid)){
console.log("Wrong player");
return;
}*/
const currentConnection = getConnectionInfo(steamid);
// Connection already made, ignore incoming request
if (currentConnection) {
console.log("Connection has been made already");
return;
}
if (camera.uuid !== roomId) return;
const peerConnection = new Peer({ initiator: false, trickle: false }) as PeerInstance;
const mediaStreamPlayer: MediaStreamPlayer = { peerConnection, steamid };
mediaStreams.players.push(mediaStreamPlayer);
peerConnection.on('signal', answer => {
console.log("SIGNAL COMING IN");
const offer = JSON.parse(JSON.stringify(answer)) as RTCSessionDescriptionInit;
socket.emit("offerFromHUD", roomId, { offer }, steamid);
});
peerConnection.on('error', (err) => {
console.log(err)
closeConnection(steamid);
});
peerConnection.on('stream', () => {
console.log("STREAM COMING IN");
const currentConnection = getConnectionInfo(steamid);
if (!currentConnection) {
console.log("Connection not established");
closeConnection(steamid);
return;
}
if (peerConnection._remoteStreams.length === 0) {
console.log('no stream?');
return;
}
for (const listener of mediaStreams.listeners.filter(listener => listener.steamid === steamid)) {
if (listener.event === "create") listener.listener(peerConnection._remoteStreams[0]);
}
});
peerConnection.on('close', () => {
console.log("CLOSE COMING IN");
const currentConnection = getConnectionInfo(steamid);
if (!currentConnection) return;
closeConnection(steamid);
});
console.log("Sending offer");
peerConnection.signal(offerData.offer);
});
}
export { mediaStreams, initiateConnection };
+88
View File
@@ -0,0 +1,88 @@
import { CSGOGSI, Player, PlayerExtension } from 'csgogsi';
import api, { isDev } from '..';
export const hudIdentity = {
name: '',
isDev
};
export const GSI = new CSGOGSI();
GSI.regulationMR = 12;
GSI.on("data", data => {
loadPlayers(data.players);
});
const requestedSteamIDs: string[] = [];
const loadPlayers = async (players: Player[]) => {
const leftOvers = players.filter(player => !requestedSteamIDs.includes(player.steamid));
const leftOverSteamids = leftOvers.map(player => player.steamid);
if(!leftOvers.length) return;
requestedSteamIDs.push(...leftOverSteamids);
const extensions = await api.players.get(leftOverSteamids);
const playersExtensions: PlayerExtension[] = extensions.map(player => (
{
id: player._id,
name: player.username,
realName: `${player.firstName} ${player.lastName}`,
steamid: player.steamid,
country: player.country,
avatar: player.avatar,
extra: player.extra,
})
)
GSI.players.push(...playersExtensions);
leftOvers.forEach(player => {
loadAvatarURL(player);
});
}
interface AvatarLoader {
loader: Promise<string>,
url: string,
}
const avatars: { [key: string]: AvatarLoader } = {};
const loadAvatarURL = (player: Player) => {
if(!player.steamid) return;
if(avatars[player.steamid]) return avatars[player.steamid].url;
avatars[player.steamid] = {
url: player.avatar || '',
loader: new Promise((resolve) => {
api.players.getAvatarURLs(player.steamid).then(result => {
const avatarUrl = result.custom || result.steam;
const existing = GSI.players.find(playerEx => playerEx.steamid === player.steamid);
const target = existing || {
id: player.steamid,
name: player.name,
realName: player.realName,
steamid: player.steamid,
country: player.country,
avatar: player.avatar,
extra: player.extra
}
if(target) target.avatar = avatarUrl;
if(!existing){
GSI.players.push(target);
}
avatars[player.steamid].url = result.custom || result.steam;
resolve(result.custom || result.custom);
}).catch(() => {
delete avatars[player.steamid];
resolve('');
});
})
}
}
+62
View File
@@ -0,0 +1,62 @@
import { useCallback, useEffect, useState } from "react";
import ActionManager, { ActionHandler, ConfigManager } from "./managers";
import { Events } from "csgogsi";
import { GSI } from "../HUD";
import type { AllActions, GetInputsFromSection, Sections } from "./settings";
export const actions = new ActionManager();
export const configs = new ConfigManager();
type EmptyListener = () => void;
type BaseEvents = keyof Events;
type Callback<K> = K extends BaseEvents ? Events[K] | EmptyListener : EmptyListener;
export function useAction<T extends keyof AllActions>(action: T, callback: ActionHandler<T extends keyof AllActions ? AllActions[T] : never>, deps?: React.DependencyList) {
useEffect(() => {
actions.on(action, callback);
return () => {
actions.off(action, callback);
};
}, deps ? [action, ...deps] : [action, callback]);
return null;
}
export function useOnConfigChange<K extends keyof Sections, T = any>(section: K, callback: ActionHandler<{ [L in keyof (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)]?: (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)[L] } | null>, deps?: React.DependencyList){
useEffect(() => {
const onDataChanged = (data: any) => {
callback(data?.[section] || null);
};
configs.onChange(onDataChanged);
onDataChanged(configs.data);
return () => {
configs.off(onDataChanged);
}
}, deps ? [section, ...deps] : [section, callback])
return null;
}
export function onGSI<T extends BaseEvents>(event: T, callback: Callback<T>, deps?: React.DependencyList){
useEffect(() => {
GSI.on(event, callback);
return () => {
GSI.off(event, callback);
}
}, deps ? [event, ...deps] : [event, callback])
}
export function useConfig<K extends keyof Sections, T extends { [K: string]: any } = {}>(section: K){
const [ data, setData ] = useState<{ [L in keyof (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)]?: (K extends keyof Sections ? GetInputsFromSection<Sections[K]> : T)[L] } | null>(configs.data?.[section] || null);
const onDataChanged = useCallback((sectionData: any) => {
setData(sectionData || null);
}, [section]);
useOnConfigChange(section, onDataChanged);
return data;
}
+14
View File
@@ -0,0 +1,14 @@
export const keybindDefinition = [
{
"bind": "Alt+C",
"action": "toggleCams"
},
{
"bind": "Alt+V",
"action": "radarBigger"
},
{
"bind": "Alt+B",
"action": "radarSmaller"
}
] as const;
+71
View File
@@ -0,0 +1,71 @@
export type ActionHandler<T = any> = (data: T) => void;
export default class ActionManager {
handlers: { [K in string]?: ActionHandler[] };
constructor(){
this.handlers = {}
/*this.on('data', _data => {
});*/
}
execute = <T = any>(eventName: string, argument?: T) => {
const handlers = this.handlers[eventName] || [];
for(const handler of handlers){
handler(argument);
}
}
on = <T = any>(eventName: string, handler: ActionHandler<T>) => {
if(!this.handlers[eventName]) this.handlers[eventName] = [];
this.handlers[eventName]!.push(handler);
}
off = (eventName: string, handler: ActionHandler) => {
if(!this.handlers[eventName]) this.handlers[eventName] = [];
this.handlers[eventName] = this.handlers[eventName]!.filter(h => h !== handler);
}
}
export class ConfigManager {
listeners: ActionHandler[];
data: { [K in string]?: any };
constructor(){
this.listeners = [];
this.data = {};
}
save(data: { [K in string]?: any }){
this.data = data;
this.execute();
/*const listeners = this.listeners.get(eventName);
if(!listeners) return false;
listeners.forEach(callback => {
if(argument) callback(argument);
else callback();
});
return true;*/
}
execute(){
const listeners = this.listeners;
if(!listeners || !listeners.length) return false;
listeners.forEach(listener => {
listener(this.data);
});
return true;
}
onChange = (listener: ActionHandler) => {
const listOfListeners = this.listeners || [];
listOfListeners.push(listener);
this.listeners = listOfListeners;
return true;
}
off = (listener: ActionHandler) => {
this.listeners = this.listeners.filter(l => l !== listener);
}
}
+162
View File
@@ -0,0 +1,162 @@
export const panelDefinition = [
{
"label": "Trivia",
"name": "trivia",
"inputs": [
{
"type": "text",
"name": "title",
"label": "Trivia title"
},
{
"type": "text",
"name": "content",
"label": "Trivia content"
},
{
"type": "action",
"name": "triviaState",
"values": [
{
"name": "show",
"label": "Show trivia"
},
{
"name": "hide",
"label": "Hide trivia"
}
]
}
]
},
{
"label": "Display settings",
"name": "display_settings",
"inputs": [
{
"type": "text",
"name": "left_title",
"label": "Left box's title"
},
{
"type": "text",
"name": "right_title",
"label": "Right box's title"
},
{
"type": "text",
"name": "left_subtitle",
"label": "Left box's subtitle"
},
{
"type": "text",
"name": "right_subtitle",
"label": "Right box's subtitle"
},
{
"type": "image",
"name": "left_image",
"label": "Left box's image logo"
},
{
"type": "image",
"name": "right_image",
"label": "Right box's image logo"
},
{
"type": "select",
"name": "replace_avatars",
"label": "Use team logos as player avatars",
"values": [
{
"label": "Only if player has no avatar",
"name": "if_missing"
},
{
"label": "Always",
"name": "always"
}
]
},
{
"type": "action",
"name": "boxesState",
"values": [
{
"name": "show",
"label": "Show boxes"
},
{
"name": "hide",
"label": "Hide boxes"
}
]
},
{
"type": "action",
"name": "toggleRadarView",
"values": [
{
"name": "toggler",
"label": "Toggle radar view"
}
]
}
]
},
{
"label": "Player & Match overview",
"name": "preview_settings",
"inputs": [
{
"type": "match",
"name": "match_preview",
"label": "Pick an upcoming match"
},
{
"type": "select",
"name": "select_preview",
"label": "Mood indicator",
"values": [
{
"name": "show",
"label": ":)"
},
{
"name": "hide",
"label": ":("
}
]
},
{
"type": "player",
"name": "player_preview",
"label": "Pick a player to preview"
},
{
"type": "checkbox",
"name": "player_preview_toggle",
"label": "Show player preview"
},
{
"type": "checkbox",
"name": "match_preview_toggle",
"label": "Show upcoming match"
},
{
"type": "action",
"name": "showTournament",
"values": [
{
"name": "show",
"label": "Show tournament"
},
{
"name": "hide",
"label": "Hide tournament"
}
]
}
]
}
] as const;
+49
View File
@@ -0,0 +1,49 @@
import { Match, Player, Team } from "../types"
import { keybindDefinition } from "./keybinds";
import { panelDefinition } from "./panel";
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type Settings = typeof panelDefinition;
type Keybinds = typeof keybindDefinition;
type InputMapper = {
text: string;
player: {
type: "player",
id: string,
player: Player | null
};
match: {
type: "match",
id: string,
match: Match | null
};
team: {
type: "team",
id: string,
team: Team | null
};
checkbox: boolean;
action: never;
image: string;
images: string[];
select: string;
}
type NonNeverKeys<T extends { [K: string]: any }> = { [K in keyof T]: T[K] extends never ? never : K }[keyof T];
type ValueMapper<T extends Settings[number]["inputs"]> = { [K in T[number] as K["name"]]: K extends { type: "action" } ? never : (K extends { type: "select" } ? K["values"][number]["name"] | "" : InputMapper[K["type"]]) }
export type GetInputsFromSection<T extends Settings[number]["inputs"]> = { [K in NonNeverKeys<ValueMapper<T>>]: ValueMapper<T>[K]};
export type Sections = { [K in Settings[number] as K["name"]]: K["inputs"]}
type ActionValueMapper<T extends Settings[number]["inputs"]> = { [K in T[number] as K["name"]]: K extends { type: "action" } ? K["values"][number]["name"] : never }
type GetActionsFromSection<T extends Settings[number]["inputs"]> = { [K in NonNeverKeys<ActionValueMapper<T>>]: ActionValueMapper<T>[K]};
export type AllActions = Prettify<GetActionsFromSection<Settings[number]["inputs"]> & { [K in Keybinds[number]["action"]]: never }>;
+69
View File
@@ -0,0 +1,69 @@
import * as I from './types';
import { MapConfig } from '../HUD/Radar/LexoRadar/maps';
const query = new URLSearchParams(window.location.search);
export const port = Number(query.get('port') || 1349);
export const variant = query.get("variant") || "default";
export const isDev = !query.get("isProd");
export const config = {apiAddress:isDev ? `http://localhost:${port}/` : '/'}
export const apiUrl = config.apiAddress;
export async function apiV2(url: string, method = 'GET', body?: any) {
const options: RequestInit = {
method,
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
}
if (body) {
options.body = JSON.stringify(body)
}
let data: any = null;
return fetch(`${apiUrl}api/${url}`, options)
.then(res => {
data = res;
return res.json().catch(_e => data && data.status < 300)
});
}
const api = {
match: {
get: async (): Promise<I.Match[]> => apiV2(`match`),
getCurrent: async (): Promise<I.Match> => apiV2(`match/current`)
},
camera: {
get: (): Promise<{ availablePlayers: ({steamid:string, label: string})[], uuid: string }> => apiV2('camera'),
toggleVmix: (status?: boolean) => new Promise<boolean>(r => {
const controller = new AbortController();
const signal = controller.signal;
// let finished = false;
const timeoutId = setTimeout(() => {
controller.abort();
r(false);
}, 1000)
fetch(`http://localhost:2715/visibility${status !== undefined ? `?status=${status}` : ''}`, { method: "POST", signal }).then(() => {
clearTimeout(timeoutId);
r(true);
//finished = true;
}).catch(() => { r(false); });
})
},
teams: {
getOne: async (id: string): Promise<I.Team> => apiV2(`teams/${id}`),
get: (): Promise<I.Team[]> => apiV2(`teams`),
},
players: {
get: async (steamids?: string[]): Promise<I.Player[]> => apiV2(steamids ? `players?steamids=${steamids.join(';')}` :`players`),
getAvatarURLs: async (steamid: string): Promise<{custom: string, steam: string}> => apiV2(`players/avatar/steamid/${steamid}`)
},
tournaments: {
get: () => apiV2('tournament')
},
maps: {
get: (): Promise<{ [key: string] : MapConfig}> => apiV2('radar/maps')
}
}
export default api;
+69
View File
@@ -0,0 +1,69 @@
import { io } from "socket.io-client";
import { isDev, port } from ".";
import { GSI, hudIdentity } from "./HUD";
import { CSGORaw } from "csgogsi";
import { actions, configs } from "./contexts/actions";
import { initiateConnection } from "./HUD/camera";
export const socket = io(isDev ? `localhost:${port}` : '/');
type RoundPlayerDamage = {
steamid: string;
damage: number;
};
type RoundDamage = {
round: number;
players: RoundPlayerDamage[];
};
socket.on("update", (data: any, damage: any) => {
if (damage) {
GSI.damage = damage;
}
GSI.digest(data);
});
const isInWindow = !!window.parent.ipcApi;
if(isInWindow){
window.parent.ipcApi.receive('raw', (data: CSGORaw, damage?: RoundDamage[]) => {
if(damage){
GSI.damage = damage;
}
GSI.digest(data);
});
}
const href = window.location.href;
socket.emit("started");
if (isDev) {
hudIdentity.name = (Math.random() * 1000 + 1).toString(36).replace(/[^a-z]+/g, '').substr(0, 15);
hudIdentity.isDev = true;
} else {
const segment = href.substr(href.indexOf('/huds/') + 6);
hudIdentity.name = segment.substr(0, segment.lastIndexOf('/'));
}
socket.on("readyToRegister", () => {
socket.emit("register", hudIdentity.name, isDev, "cs2", isInWindow ? "IPC" : "DEFAULT");
initiateConnection();
});
socket.on(`hud_config`, (data: any) => {
configs.save(data);
});
socket.on(`hud_action`, (data: any) => {
actions.execute(data.action, data.data);
});
socket.on('keybindAction', (action: string) => {
actions.execute(action);
});
socket.on("refreshHUD", () => {
window.top?.location.reload();
});
socket.on("update_mirv", (data: any) => {
GSI.digestMIRV(data);
})
+161
View File
@@ -0,0 +1,161 @@
export interface Player {
_id: string;
firstName: string;
lastName: string;
username: string;
avatar: string;
country: string;
steamid: string;
team: string;
extra: Record<string, string>;
}
export interface Team {
_id: string;
name: string;
country: string;
shortName: string;
logo: string;
extra: Record<string, string>;
}
/*
export interface HUD {
name: string,
version: string,
author: string,
legacy: boolean,
dir: string
}
export interface Config {
port: number,
steamApiKey: string,
token: string,
}*/
export interface TournamentMatchup {
_id: string;
loser_to: string | null; // IDs of Matchups, not Matches
winner_to: string | null;
label: string;
matchId: string | null;
parents: TournamentMatchup[];
}
export interface DepthTournamentMatchup extends TournamentMatchup {
depth: number;
parents: DepthTournamentMatchup[];
}
export type TournamentTypes = 'swiss' | 'single' | 'double';
export type TournamentStage = {
type: TournamentTypes;
matchups: TournamentMatchup[];
teams: number;
phases: number;
participants: string[];
};
export interface Tournament {
_id: string;
name: string;
logo: string;
groups: TournamentStage[];
playoffs: TournamentStage;
autoCreate: boolean;
}
export interface RoundData {
round: number,
players: {
[steamid: string]: PlayerRoundData
},
winner: 'CT' | 'T' | null,
win_type: 'bomb' | 'elimination' | 'defuse' | 'time',
}
export interface PlayerRoundData {
kills: number,
killshs: number,
damage: number,
}
export interface Veto {
teamId: string;
mapName: string;
side: "CT" | "T" | "NO";
type: "ban" | "pick" | "decider";
reverseSide?: boolean;
rounds?: (RoundData | null)[],
score?: {
[key: string]: number;
};
winner?: string;
mapEnd: boolean;
}
export interface Match {
id: string;
current: boolean;
left: {
id: string | null;
wins: number;
};
right: {
id: string | null;
wins: number;
};
matchType: "bo1" | "bo2" | "bo3" | "bo5";
vetos: Veto[];
}
export type Weapon =
| "ak47"
| "aug"
| "awp"
| "bizon"
| "famas"
| "g3sg1"
| "galilar"
| "m4a1"
| "m4a1_silencer"
| "m249"
| "mac10"
| "mag7"
| "mp5sd"
| "mp7"
| "mp9"
| "negev"
| "nova"
| "p90"
| "sawedoff"
| "scar20"
| "sg556"
| "ssg08"
| "ump45"
| "xm1014"
| Pistol
| Knife;
export type Pistol = "c75a" | "deagle" | "elite" | "fiveseven" | "glock" | "hkp2000" | "p250" | "revolver" | "taser" | "tec9" | "usp_silencer";
export type Knife =
| "knife"//
| "knife_css"//--
| "knife_butterfly"//
| "knife_falchion"//
| "knife_flip"//
| "knife_outdoor" // Nomad Knife
| "knife_gut"//
| "knife_gypsy_jackknife"//
| "knife_karambit"//
| "knife_bayonet" //
| "knife_cord" //
| "knife_m9_bayonet"//
| "knife_push" // Shadow daggers
| "knife_stiletto"//
| "knife_survival_bowie"//
| "knife_t"//
| "knife_skeleton" //
| "knife_tactical"//
| "knife_ursus"//
| "knife_widowmaker"//
| "knife_canis";//