Skip to content

Commit 7d0ce5f

Browse files
committed
feat(video): custom cameras
ref #1079
1 parent 59686c5 commit 7d0ce5f

File tree

175 files changed

+4786
-1059
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+4786
-1059
lines changed

cs2-server-plugin/cs2-server-plugin/main.cpp

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,15 @@ ISource2EngineToClient* GetEngine()
170170
return engineToClient;
171171
}
172172

173+
void SendMsg(json msg) {
174+
ws->send(msg.dump());
175+
}
176+
173177
void SendStatusOk() {
174178
json msg;
175179
msg["name"] = "status";
176180
msg["payload"] = "ok";
177-
ws->send(msg.dump());
181+
SendMsg(msg);
178182
}
179183

180184
void RestoreGameinfoFile() {
@@ -255,20 +259,22 @@ void PlaybackLoop() {
255259
}
256260

257261
if (!initialized) {
262+
// Required to make the spec_lock_to_accountid command working since the 25/04/2024 update - it looks like the command has been hidden.
263+
// Also required to use the startmovie command.
264+
UnhideCommandsAndCvars();
265+
258266
// Since the 23/05/2024 CS2 update, the demo playback UI is displayed by default.
259267
// We have to set the demo_ui_mode convar to 0 before starting the playback prevent the UI from being displayed.
260268
engine->ExecuteClientCmd(0, "demo_ui_mode 0", true);
269+
engine->ExecuteClientCmd(0, "sv_cheats 1", true); // required to unlock commands such as getposcopy
270+
261271
initialized = true;
262272
}
263273

264274
bool newIsPlayingDemo = engine->IsPlayingDemo();
265275
if (newIsPlayingDemo && !isPlayingDemo) {
266276
Log("[%d] Demo playback started, sequences %d", currentTick, sequences.size());
267277
currentTick = -1;
268-
269-
// Required to make the spec_lock_to_accountid command working since the 25/04/2024 update - it looks like the command has been hidden.
270-
// Also required to use the startmovie command.
271-
UnhideCommandsAndCvars();
272278
}
273279
else if (!newIsPlayingDemo && isPlayingDemo) {
274280
Log("[%d] Demo playback stopped, sequences %d", currentTick, sequences.size());
@@ -350,6 +356,19 @@ void HandleWebSocketMessage(const std::string& message)
350356
auto engine = GetEngine();
351357
engine->ExecuteClientCmd(0, cmd.c_str(), true);
352358
}
359+
else if (msg["name"] == "capture-player-view") {
360+
auto engine = GetEngine();
361+
engine->ExecuteClientCmd(0, "getposcopy", true);
362+
// The "screenshot" command works only on Windows when the -tools launch option is set.
363+
// As a workaround, we use the startmovie command to take a screenshot.
364+
engine->ExecuteClientCmd(0, "hideconsole", true);
365+
engine->ExecuteClientCmd(0, "startmovie csdmcamera jpg", true);
366+
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
367+
engine->ExecuteClientCmd(0, "endmovie", true);
368+
json msg;
369+
msg["name"] = "capture-player-view-result";
370+
SendMsg(msg);
371+
}
353372
}
354373

355374
void ConnectToWebsocketServer() {

csgo-server-plugin/csgo-server-plugin/cdll_int.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ class IVEngineClient14
3939
virtual void _UNUSED_GetSentence(void) = 0; // 15
4040
virtual void _UNUSED_GetSentenceLength(void) = 0; // 16
4141
virtual void _UNUSED_IsStreaming(void) = 0; // 17
42-
virtual void _UNUSED_GetViewAngles(void) = 0; // 18
42+
virtual void GetViewAngles(QAngle& angles) = 0; // 18
4343
virtual void _UNUSED_SetViewAngles(void) = 0; // 19
44-
virtual void _UNUSED_GetMaxClients(void) = 0; // 20
44+
virtual int GetMaxClients(void) = 0; // 20
4545
virtual void _UNUSED_Key_LookupBinding(void) = 0; // 21
4646
virtual void _UNUSED_Key_BindingForKey(void) = 0; // 22
4747
virtual void _UNUSED_Key_SetBinding(void) = 0; // 23

csgo-server-plugin/csgo-server-plugin/main.cpp

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
#include "plugin.h"
2121
#include "cdll_int.h"
2222
#include "game_ui.h"
23+
#include "game/server/iplayerinfo.h"
24+
25+
#define ENTINDEX(index) static_cast<edict_t *>(globalVars->pEdicts + (index))
26+
#define PLAYER_INFO_FROM_INDEX(index) static_cast<IPlayerInfo *>(playerInfoManager->GetPlayerInfo(ENTINDEX(index)))
2327

2428
using easywsclient::WebSocket;
2529
using nlohmann::json;
@@ -49,6 +53,8 @@ EXPOSE_SINGLE_INTERFACE_GLOBALVAR(CServerPlugin, IServerPluginCallbacks, INTERFA
4953
void* client;
5054
IVEngineClient14* engine = NULL;
5155
CGameUI* gameUi = NULL;
56+
IPlayerInfoManager* playerInfoManager = NULL;
57+
CGlobalVars* globalVars = NULL;
5258
FrameStageNotifyFn originalFrameStageNotify = NULL;
5359
thread* wsConnectionThread = NULL;
5460
WebSocket::pointer ws;
@@ -58,6 +64,7 @@ bool isPlayingDemo = false;
5864
int mainMenuFrameCount = 0;
5965
int currentTick = -1;
6066
bool isQuitting = false;
67+
bool forceSpectatorMode = false;
6168
std::queue<Sequence> sequences;
6269
// Unlike CS2, executing client commands from a different thread than the main game thread may crash the game.
6370
// As the WebSocket connection runs in a separate thread, we defer the possible command execution when we receive a
@@ -84,11 +91,15 @@ void ExecutePendingCommand()
8491
}
8592
}
8693

94+
void SendMsg(json msg) {
95+
ws->send(msg.dump());
96+
}
97+
8798
void SendStatusOk() {
8899
json msg;
89100
msg["name"] = "status";
90101
msg["payload"] = "ok";
91-
ws->send(msg.dump());
102+
SendMsg(msg);
92103
}
93104

94105
void LoadSequencesFile(string demoPath) {
@@ -138,6 +149,40 @@ void HandleWebSocketMessage(const string& message)
138149

139150
std::lock_guard<mutex> lock(pendingCmdMutex);
140151
pendingCmd = "playdemo \"" + demoPath + "\"";
152+
} else if (msg["name"] == "capture-player-view") {
153+
float x, y, z, pitch, yaw = 0;
154+
for (int i = 1; i <= engine->GetMaxClients(); i++) {
155+
IPlayerInfo* player = PLAYER_INFO_FROM_INDEX(i);
156+
if (player && player->IsConnected() && player->GetNetworkIDString() != "BOT") {
157+
auto position = player->GetAbsOrigin();
158+
x = position.x;
159+
y = position.y;
160+
z = position.z;
161+
// player->GetAbsAngles() is not reliable, use engine->GetViewAngles() instead.
162+
QAngle viewAngles;
163+
engine->GetViewAngles(viewAngles);
164+
pitch = viewAngles.x;
165+
yaw = viewAngles.y;
166+
break;
167+
}
168+
}
169+
170+
// use the startmovie command to generate a screenshot like CS2 so we don't maintain two separate code paths.
171+
engine->ExecuteClientCmd("hideconsole");
172+
engine->ExecuteClientCmd("startmovie csdmcamera jpg");
173+
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
174+
engine->ExecuteClientCmd("endmovie");
175+
176+
json msg;
177+
msg["name"] = "capture-player-view-result";
178+
msg["payload"] = {
179+
{"x", x},
180+
{"y", y},
181+
{"z", z},
182+
{"pitch", pitch},
183+
{"yaw", yaw}
184+
};
185+
SendMsg(msg);
141186
}
142187
}
143188

@@ -272,11 +317,34 @@ void NewFrameStageNotify(void* thisptr, CClientFrameStage stage)
272317
#endif
273318
}
274319

320+
void CServerPlugin::ClientFullyConnect(edict_t* pEntity) {
321+
if (!forceSpectatorMode) {
322+
return;
323+
}
324+
auto player = playerInfoManager->GetPlayerInfo(pEntity);
325+
if (player) {
326+
player->ChangeTeam(1);
327+
}
328+
}
329+
275330
// Called when the plugin is loaded ONLY if the -insecure launch parameter is set.
276331
bool CServerPlugin::Load(CreateInterfaceFn interfaceFactory, CreateInterfaceFn gameServerFactory)
277332
{
278333
DeleteLogFile();
279334

335+
playerInfoManager = (IPlayerInfoManager*)gameServerFactory(INTERFACEVERSION_PLAYERINFOMANAGER, NULL);
336+
if (!playerInfoManager)
337+
{
338+
Log("Could not find IPlayerInfoManager : %s", GetLastErrorString());
339+
return false;
340+
}
341+
342+
globalVars = playerInfoManager->GetGlobalVars();
343+
if (!globalVars) {
344+
Log("Could not find CGlobalVars : %s", GetLastErrorString());
345+
return false;
346+
}
347+
280348
engine = (IVEngineClient14*)interfaceFactory("VEngineClient014", NULL);
281349
if (engine == NULL)
282350
{
@@ -368,6 +436,10 @@ bool CServerPlugin::Load(CreateInterfaceFn interfaceFactory, CreateInterfaceFn g
368436
LoadSequencesFile(demoPath);
369437
break;
370438
}
439+
440+
if (strcmp(param, "forcespec") == 0) {
441+
forceSpectatorMode = true;
442+
}
371443
}
372444

373445
wsConnectionThread = new thread(ConnectToWebsocketServerLoop);
@@ -402,6 +474,15 @@ const char *CServerPlugin::GetPluginDescription()
402474
return "CS Demo Manager plugin";
403475
}
404476

477+
void LogPlayers() {
478+
for (int i = 1; i <= engine->GetMaxClients(); i++) {
479+
IPlayerInfo* player = PLAYER_INFO_FROM_INDEX(i);
480+
if (player && player->IsConnected()) {
481+
Log("%s - userID: %d SteamID: %s", player->GetName(), player->GetUserID(), player->GetNetworkIDString());
482+
}
483+
}
484+
}
485+
405486
CON_COMMAND(csdm_info, "Show info"){
406487
if (!demoPath.empty()) {
407488
Log("Demo path: %s", demoPath.c_str());
@@ -417,4 +498,6 @@ CON_COMMAND(csdm_info, "Show info"){
417498
else {
418499
Log("WebSocket not connected");
419500
}
501+
502+
LogPlayers();
420503
}

csgo-server-plugin/csgo-server-plugin/plugin.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class CServerPlugin : public IServerPluginCallbacks
2020
virtual void GameFrame(bool simulating) {};
2121
virtual void LevelShutdown() {};
2222
virtual void ClientActive(edict_t* pEntity) {};
23-
virtual void ClientFullyConnect(edict_t* pEntity) {};
23+
virtual void ClientFullyConnect(edict_t* pEntity);
2424
virtual void ClientDisconnect(edict_t* pEntity) {};
2525
virtual void ClientPutInServer(edict_t* pEntity, const char* playername) {};
2626
virtual void SetCommandClient(int index) {};

eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ export default [
185185
'^GitHub$',
186186
'Inter var$',
187187
'CS:DM',
188+
'CS:GO',
189+
'CS2 Limited Test',
190+
'^setpos_exact',
188191
],
189192
ignoreNames: [
190193
// Ignore matching className (case-insensitive)

src/cli/commands/video-command.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export class VideoCommand extends Command {
147147
showAssists: this.showAssists ?? settings.video.showAssists,
148148
showOnlyDeathNotices: this.showOnlyDeathNotices ?? settings.video.showOnlyDeathNotices,
149149
playersOptions: [],
150+
playerCameras: [],
150151
cameras: [],
151152
recordAudio: this.recordAudio ?? settings.video.recordAudio,
152153
playerVoicesEnabled: this.playerVoices ?? settings.video.playerVoicesEnabled,
@@ -156,7 +157,7 @@ export class VideoCommand extends Command {
156157

157158
if (this.focusPlayerSteamId) {
158159
const player = await fetchPlayer(this.focusPlayerSteamId);
159-
sequence.cameras.push({
160+
sequence.playerCameras.push({
160161
tick: this.startTick,
161162
playerSteamId: this.focusPlayerSteamId,
162163
playerName: player.name,

src/common/error-code.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export const ErrorCode = {
2828
MissingPlayerSlot: 427,
2929
CustomCounterStrikeExecutableNotFound: 428,
3030
CounterStrikeAlreadyRunning: 429,
31+
CounterStrikeNotRunning: 430,
32+
CounterStrikeNotConnected: 431,
33+
CounterStrikeNoResponse: 432,
3134
InvalidDemoPath: 505,
3235
ChecksumsMismatch: 506,
3336
DuplicateTeamName: 507,
@@ -87,12 +90,14 @@ export const ErrorCode = {
8790
InvalidSteamCommunityUrl: 2002,
8891
SteamAccountNameTooLong: 2003,
8992

90-
TagNameAlreadyToken: 3000,
93+
TagNameAlreadyTaken: 3000,
9194
TagNameTooShort: 3001,
9295
TagNameTooLong: 3002,
9396
InvalidTagColor: 3003,
9497
TagNotFound: 3004,
9598

99+
CameraAlreadyExists: 3010,
100+
96101
InvalidDemoName: 3100,
97102
AnalyzeCorruptedDemo: 3200,
98103
InsertMatchDuplicatedChecksum: 3201,

src/common/types/camera.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Game } from 'csdm/common/types/counter-strike';
2+
3+
export type CameraCoordinates = {
4+
x: number;
5+
y: number;
6+
z: number;
7+
yaw: number;
8+
pitch: number;
9+
};
10+
11+
export type Camera = CameraCoordinates & {
12+
id: string;
13+
name: string;
14+
game: Game;
15+
mapName: string;
16+
comment: string;
17+
color: string;
18+
imagePath: string | null;
19+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type CustomCameraFocus = {
2+
tick: number;
3+
id: string;
4+
name: string;
5+
color: string;
6+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type ImageInformation = {
2+
width: number;
3+
height: number;
4+
base64: string;
5+
};

0 commit comments

Comments
 (0)