diff --git a/docs/cvars.md b/docs/cvars.md index e9549eb9c..5c7ee7e0c 100644 --- a/docs/cvars.md +++ b/docs/cvars.md @@ -547,12 +547,16 @@ |sar_tas_play|cmd|sar_tas_play \ [filename2] - plays a TAS script with given name. If two script names are given, play coop| |sar_tas_play_single|cmd|sar_tas_play_single \ [slot] - plays a single coop TAS script, giving the player control of the other slot.| |sar_tas_playback_rate|1.0|The rate at which to play back TAS scripts.| +|sar_tas_protocol_connect|cmd|sar_tas_protocol_connect \ \ - connect to the TAS protocol server.
ex: '127.0.0.1 5666' - '89.10.20.20 5666'.| +|sar_tas_protocol_reconnect_delay|0|A number of seconds after which reconnection to TAS protocol server should be made.
0 means no reconnect attempts will be made.| +|sar_tas_protocol_send_msg|cmd|sar_tas_protocol_send_msg \ - sends a message over TAS protocol.| +|sar_tas_protocol_server|cmd|sar_tas_protocol_server [port] - starts a TAS protocol server. Port is 6555 by default.| +|sar_tas_protocol_stop|cmd|sar_tas_protocol_stop - stops every TAS protocol related connection.| |sar_tas_real_controller_debug|0|Debugs controller.| |sar_tas_replay|cmd|sar_tas_replay - replays the last played TAS| |sar_tas_restore_fps|1|Restore fps_max and host_framerate after TAS playback.| |sar_tas_resume|cmd|sar_tas_resume - resumes TAS playback| |sar_tas_save_raw|cmd|sar_tas_save_raw - saves a processed version of just processed script| -|sar_tas_server|0|Enable the remote TAS server. Setting this value to something higher than one will bind the server on that port.| |sar_tas_skipto|0|Fast-forwards the TAS playback until given playback tick.| |sar_tas_stop|cmd|sar_tas_stop - stop TAS playing| |sar_tas_tools_enabled|1|Enables tool processing for TAS script making.| diff --git a/docs/tas_proto.txt b/docs/tas_proto.txt index 3409e3ae8..2f970bdee 100644 --- a/docs/tas_proto.txt +++ b/docs/tas_proto.txt @@ -1,13 +1,14 @@ -TAS server protocol +TAS protocol =================== -Connect to a TCP socket on localhost:6555. +The following specification lists packets that can be sent between the player (SAR client capable of playing TAS scripts) and controller (external software capable of controlling the player through the protocol like VSC extension or bruteforcer server) +SAR (the player) can act as both the client (connecting to a controller server) and server (listening for controller client connections). Every packet consists of a one-byte (u8) ID, followed by some extra data. the extra data is specified here under the packet name; if there's nothing specified, there's no extra data. The server should always send a packet with ID 255 first; it'll never send this again. -server (SAR) -> client (VSC extension): +player -> controller: [0] set active len1: u32 filename1: [len1]u8 // relative to "Portal 2/tas/", so the same as the arg you'd give to sar_tas_play @@ -47,7 +48,7 @@ server (SAR) -> client (VSC extension): location: [len]u8 // a string like "/home/mlugg/.steam/steam/steamapps/common/Portal 2", used so that the plugin knows whether it has the right script folder open -client (VSC extension) -> server (SAR): +controller -> player: [0] request playback len1: u32 filename1: [len1]u8 // relative to "Portal 2/tas/", so the same as the arg you'd give to sar_tas_play @@ -85,3 +86,7 @@ client (VSC extension) -> server (SAR): [100] request entity info len: u32 entity_selector: [len]u8 + + [101] set continuous entity info + len: u32 + entity_selector: [len]u8 // send empty string to disable continuous entity info diff --git a/src/Features/Tas/TasPlayer.cpp b/src/Features/Tas/TasPlayer.cpp index 49e5dc013..89037eec6 100644 --- a/src/Features/Tas/TasPlayer.cpp +++ b/src/Features/Tas/TasPlayer.cpp @@ -3,7 +3,7 @@ #include "Features/Session.hpp" #include "Features/Tas/TasParser.hpp" #include "Features/Tas/TasTool.hpp" -#include "Features/Tas/TasServer.hpp" +#include "Features/Tas/TasProtocol.hpp" #include "Features/Tas/TasTools/CheckTool.hpp" #include "Features/Hud/Hud.hpp" #include "Features/RNGManip.hpp" @@ -467,7 +467,7 @@ void TasPlayer::SaveProcessedFramebulks() { } } else { std::string slotScript = slotProcessed ? TasParser::SaveRawScriptToString(script) : ""; - TasServer::SendProcessedScript((uint8_t)slot, slotScript); + TasProtocol::SendProcessedScript((uint8_t)slot, slotScript); } } } @@ -759,21 +759,21 @@ ON_EVENT(FRAME) { } void TasPlayer::UpdateServer() { - TasStatus status; + TasProtocol::Status status; status.active = active; status.tas_path[0] = playbackInfo.slots[0].name; status.tas_path[1] = playbackInfo.IsCoop() ? playbackInfo.slots[1].name : ""; status.playback_state = engine->IsAdvancing() - ? PlaybackState::PAUSED + ? TasProtocol::PlaybackState::PAUSED : this->GetTick() < sar_tas_skipto.GetInt() - ? PlaybackState::SKIPPING - : PlaybackState::PLAYING; + ? TasProtocol::PlaybackState::SKIPPING + : TasProtocol::PlaybackState::PLAYING; status.playback_rate = sar_tas_playback_rate.GetFloat(); status.playback_tick = this->GetTick(); - TasServer::SetStatus(status); + TasProtocol::SetStatus(status); } DECL_COMMAND_FILE_COMPLETION(sar_tas_play, TAS_SCRIPT_EXT, TAS_SCRIPTS_DIR, 2) diff --git a/src/Features/Tas/TasProtocol.cpp b/src/Features/Tas/TasProtocol.cpp new file mode 100644 index 000000000..4f56f64ac --- /dev/null +++ b/src/Features/Tas/TasProtocol.cpp @@ -0,0 +1,733 @@ +// This *has* to come first because doesn't like being +// imported after . I fucking hate this platform +#ifdef _WIN32 +# include +# include +#else +# include +# include +# include +# include +# include +#endif + +#ifndef _WIN32 +# define SOCKET int +# define INVALID_SOCKET -1 +# define SOCKET_ERROR -1 +# define closesocket close +# define WSACleanup() (void)0 +#endif + +#include "TasProtocol.hpp" +#include "TasPlayer.hpp" +#include "Event.hpp" +#include "Scheduler.hpp" +#include "Modules/Console.hpp" +#include "Modules/Engine.hpp" +#include "Features/PlayerTrace.hpp" +#include "Features/EntityList.hpp" + +#include +#include +#include +#include +#include +#include + +#define DEFAULT_TAS_CLIENT_SOCKET 6555 +#define DEFAULT_TAS_SERVER_SOCKET 6555 + +Variable sar_tas_protocol_reconnect_delay("sar_tas_protocol_reconnect_delay", "0", 0, + "A number of seconds after which reconnection to TAS protocol server should be made.\n" + "0 means no reconnect attempts will be made.\n"); + +using namespace TasProtocol; + +static SOCKET g_listen_sock = INVALID_SOCKET; +static std::vector g_connections; +static std::atomic g_should_stop; +static bool g_should_run = false; +static bool g_stopped_manually = true; +static std::atomic g_is_server; +static std::chrono::high_resolution_clock::time_point g_last_connection_attemt_timestamp; + +static std::string g_client_ip; +static int g_client_port; +static int g_server_port; +static std::mutex g_conn_data_mutex; + +static Status g_last_status; +static Status g_current_status; +static int g_last_debug_tick; +static int g_current_debug_tick; +static std::mutex g_status_mutex; + +static bool popByte(std::deque &buf, uint8_t &val) { + if (buf.size() < 1) return false; + val = buf[0]; + buf.pop_front(); + return true; +} + +static void encodeByte(std::deque& buf, uint8_t val) { + buf.push_back(val); +} + +static bool popRaw32(std::deque &buf, uint32_t& val) { + + if (buf.size() < 4) return false; + + val = 0; + for (int i = 0; i < 4; ++i) { + val <<= 8; + val |= buf[0]; + buf.pop_front(); + } + + return true; +} + +static void encodeRaw32(std::vector &buf, uint32_t val) { + buf.push_back((val >> 24) & 0xFF); + buf.push_back((val >> 16) & 0xFF); + buf.push_back((val >> 8) & 0xFF); + buf.push_back((val >> 0) & 0xFF); +} + +static bool popRawFloat(std::deque& buf, float& val) { + return popRaw32(buf, (uint32_t&)val); +} + +static void encodeRawFloat(std::vector &buf, float val) { + encodeRaw32(buf, *(uint32_t *)&val); +} + +static bool popString(std::deque &buf, std::string &val) { + uint32_t len; + if(!popRaw32(buf, len)) return false; + if (buf.size() < len) return false; + + for (size_t i = 0; i < len; ++i) { + val += buf[0]; + buf.pop_front(); + } + return true; +} + +static void encodeString(std::vector& buf, std::string val) { + encodeRaw32(buf, (uint32_t)val.size()); + for (char c : val) { + buf.push_back(c); + } +} + +static void sendAll(const std::vector &buf) { + for (auto &cl : g_connections) { + send(cl.sock, (const char *)buf.data(), buf.size(), 0); + } +} + +static void fullUpdate(TasProtocol::ConnectionData &cl, bool first_packet = false) { + std::vector buf; + + if (first_packet) { + buf.push_back(SEND_GAME_LOCATION); + + std::string dir = std::filesystem::current_path().string(); + std::replace(dir.begin(), dir.end(), '\\', '/'); + encodeString(buf, dir); + } + + buf.push_back(SEND_PLAYBACK_RATE); + + union { float f; int i; } rate = { g_last_status.playback_rate }; + encodeRaw32(buf, rate.i); + + if (g_last_status.active) { + buf.push_back(SEND_ACTIVE); + encodeString(buf, g_last_status.tas_path[0]); + encodeString(buf, g_last_status.tas_path[1]); + + // state + switch (g_last_status.playback_state) { + case PlaybackState::PLAYING: + buf.push_back(SEND_PLAYING); + break; + case PlaybackState::PAUSED: + buf.push_back(SEND_PAUSED); + break; + case PlaybackState::SKIPPING: + buf.push_back(SEND_SKIPPING); + break; + } + + buf.push_back(SEND_CURRENT_TICK); + encodeRaw32(buf, g_last_status.playback_tick); + } else { + buf.push_back(SEND_INACTIVE); + } + + buf.push_back(SEND_DEBUG_TICK); + encodeRaw32(buf, (uint32_t)g_last_debug_tick); + + send(cl.sock, (const char *)buf.data(), buf.size(), 0); +} + +static void update() { + g_status_mutex.lock(); + Status status = g_current_status; + int debug_tick = g_current_debug_tick; + g_status_mutex.unlock(); + + if (status.active != g_last_status.active || status.tas_path[0] != g_last_status.tas_path[0] || status.tas_path[1] != g_last_status.tas_path[1]) { + // big change; we might as well just do a full update + g_last_status = status; + g_last_debug_tick = debug_tick; + for (auto &cl : g_connections) fullUpdate(cl); + return; + } + + if (status.playback_rate != g_last_status.playback_rate) { + union { float f; int i; } rate = { status.playback_rate }; + + std::vector buf{SEND_PLAYBACK_RATE}; + encodeRaw32(buf, rate.i); + sendAll(buf); + + g_last_status.playback_rate = status.playback_rate; + } + + if (status.active && status.playback_state != g_last_status.playback_state) { + switch (status.playback_state) { + case PlaybackState::PLAYING: + sendAll({ SEND_PLAYING }); + break; + case PlaybackState::PAUSED: + sendAll({ SEND_PAUSED }); + break; + case PlaybackState::SKIPPING: + sendAll({ SEND_SKIPPING }); + break; + } + + g_last_status.playback_state = status.playback_state; + } + + if (status.active && status.playback_tick != g_last_status.playback_tick) { + std::vector buf{ SEND_CURRENT_TICK}; + encodeRaw32(buf, status.playback_tick); + sendAll(buf); + + g_last_status.playback_tick = status.playback_tick; + } + + if (debug_tick != g_last_debug_tick) { + std::vector buf{ SEND_DEBUG_TICK}; + encodeRaw32(buf, (uint32_t)debug_tick); + sendAll(buf); + + g_last_debug_tick = debug_tick; + } +} + +// reads a single tas protocol command +// returns 0 if command has been handled properly +// returns 1 if there's no data to fully process a command +// returns 2 if bad command is received +static int processCommand(ConnectionData &cl) { + std::deque copy = cl.cmdbuf; + + uint8_t packetId; + if (popByte(cl.cmdbuf, packetId)) switch (packetId) { + + case RECV_PLAY_SCRIPT: { + std::string filename1; + std::string filename2; + + if (!popString(cl.cmdbuf, filename1)) break; + if (!popString(cl.cmdbuf, filename2)) break; + + Scheduler::OnMainThread([=](){ + tasPlayer->PlayFile(filename1, filename2); + }); + + return 0; + } + case RECV_STOP: { + Scheduler::OnMainThread([=](){ + tasPlayer->Stop(true); + }); + + return 0; + } + case RECV_PLAYBACK_RATE: { + float rate; + + popRawFloat(cl.cmdbuf, rate); + + Scheduler::OnMainThread([=](){ + sar_tas_playback_rate.SetValue(rate); + }); + + return 0; + } + case RECV_RESUME: { + Scheduler::OnMainThread([=]() { + tasPlayer->Resume(); + }); + + return 0; + } + case RECV_PAUSE: { + Scheduler::OnMainThread([=]() { + tasPlayer->Pause(); + }); + + return 0; + } + case RECV_FAST_FORWARD: { + int tick; + bool pause_after; + + if (!popRaw32(cl.cmdbuf, (uint32_t &)tick)) break; + if (!popByte(cl.cmdbuf, (uint8_t &)pause_after)) break; + + Scheduler::OnMainThread([=]() { + sar_tas_skipto.SetValue(tick); + if (pause_after) sar_tas_pauseat.SetValue(tick); + }); + return 0; + } + case RECV_SET_PAUSE_TICK: { + int tick; + + if (!popRaw32(cl.cmdbuf, (uint32_t &)tick)) break; + + Scheduler::OnMainThread([=]() { + sar_tas_pauseat.SetValue(tick); + }); + return 0; + } + case RECV_ADVANCE_TICK: { + Scheduler::OnMainThread([]() { + tasPlayer->AdvanceFrame(); + }); + return 0; + } + case RECV_MESSAGE: { + std::string message; + + if (!popString(cl.cmdbuf, message)) break; + + THREAD_PRINT("[TAS Protocol] %s\n", message.c_str()); + + return 0; + } + case RECV_PLAY_SCRIPT_PROTOCOL: { + + std::string slot0Name; + std::string slot0Script; + std::string slot1Name; + std::string slot1Script; + + if (!popString(cl.cmdbuf, slot0Name)) break; + if (!popString(cl.cmdbuf, slot0Script)) break; + if (!popString(cl.cmdbuf, slot1Name)) break; + if (!popString(cl.cmdbuf, slot1Script)) break; + + Scheduler::OnMainThread([=]() { + tasPlayer->PlayScript(slot0Name, slot0Script, slot1Name, slot1Script); + }); + + return 0; + } + case RECV_ENTITY_INFO: + case RECV_SET_CONT_ENTITY_INFO: { + std::string entSelector; + + if (!popString(cl.cmdbuf, entSelector)) break; + + if (packetId == RECV_SET_CONT_ENTITY_INFO) { + cl.contInfoEntSelector = entSelector; + } else { + SendEntityInfo(cl, entSelector); + } + return 0; + } + default: + // Bad command - disconnect + return 2; + } + + // command hasn't been fully read - recover to the copy + cl.cmdbuf = copy; + return 1; +} + +static bool receiveFromConnection(TasProtocol::ConnectionData &cl) { + char buf[1024]; + int len = recv(cl.sock, buf, sizeof buf, 0); + + if (len == 0 || len == SOCKET_ERROR) { // Connection closed or errored + return false; + } + + cl.cmdbuf.insert(cl.cmdbuf.end(), std::begin(buf), std::begin(buf) + len); + + while (true) { + int result = processCommand(cl); + + if (result == 2) { // invalid command - disconnect + closesocket(cl.sock); + return false; + } + if (result == 1) break; // not enough data - wait for more + } + + return true; +} + +static bool attemptToInitializeServer() { + if (!g_is_server.load()) return false; + + g_conn_data_mutex.lock(); + auto server_port = g_server_port; + g_conn_data_mutex.unlock(); + + g_listen_sock = socket(AF_INET6, SOCK_STREAM, 0); + if (g_listen_sock == INVALID_SOCKET) { + THREAD_PRINT("Could not initialize TAS server: socket creation failed\n"); + return false; + } + + // why tf is this enabled by default on Windows + int v6only = 0; + setsockopt(g_listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&v6only, sizeof v6only); + + struct sockaddr_in6 saddr { + AF_INET6, + htons(server_port), + 0, + in6addr_any, + 0, + }; + + if (bind(g_listen_sock, (struct sockaddr *)&saddr, sizeof saddr) == SOCKET_ERROR) { + THREAD_PRINT("Could not initialize TAS server: socket bind failed\n"); + closesocket(g_listen_sock); + return false; + } + + if (listen(g_listen_sock, 4) == SOCKET_ERROR) { + THREAD_PRINT("Could not initialize TAS server: socket listen failed\n"); + closesocket(g_listen_sock); + return false; + } + + THREAD_PRINT("TAS server initialized on port %d.\n", server_port); + + return true; +} + +static bool attemptConnectionToServer() { + if (g_is_server.load()) return false; + + g_conn_data_mutex.lock(); + std::string ip = g_client_ip; + int port = g_client_port; + g_conn_data_mutex.unlock(); + + auto clientSocket = socket(AF_INET, SOCK_STREAM, 0); + if (clientSocket == SOCKET_ERROR) { + THREAD_PRINT("Could not connect to TAS protocol server: socket creation failed\n"); + closesocket(clientSocket); + return false; + } + + sockaddr_in serverAddr; + serverAddr.sin_family = AF_INET; + serverAddr.sin_port = htons(port); + + if (inet_pton(AF_INET, g_client_ip.c_str(), &serverAddr.sin_addr) <= 0) { + THREAD_PRINT("Could not connect to TAS protocol server: invalid address\n"); + closesocket(clientSocket); + return false; + } + + if (connect(clientSocket, reinterpret_cast(&serverAddr), sizeof(serverAddr)) == -1) { + THREAD_PRINT("Could not connect to TAS protocol server: connection failed.\n"); + closesocket(clientSocket); + return false; + } + + g_connections.push_back({clientSocket, {}}); + fullUpdate(g_connections[g_connections.size() - 1], true); + THREAD_PRINT("Successfully connected to TAS server %s:%d.\n", ip.c_str(), port); + + return true; +} + +static void processConnections(bool is_server) { + fd_set set; + FD_ZERO(&set); + + SOCKET max = g_listen_sock; + + if (is_server) { + FD_SET(g_listen_sock, &set); + } + for (auto client : g_connections) { + FD_SET(client.sock, &set); + if (max < client.sock) max = client.sock; + } + + // 0.05s timeout + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 50000; + + int nsock = select(max + 1, &set, nullptr, nullptr, &tv); + if (nsock == SOCKET_ERROR || !nsock) { + return; + } + + if (is_server && FD_ISSET(g_listen_sock, &set)) { + SOCKET cl = accept(g_listen_sock, nullptr, nullptr); + if (cl != INVALID_SOCKET) { + g_connections.push_back({ cl, {} }); + THREAD_PRINT("A controller connected to TAS server. Number of controllers: %d\n", g_connections.size()); + fullUpdate(g_connections[g_connections.size() - 1], true); + } + } + + for (size_t i = 0; i < g_connections.size(); ++i) { + auto &cl = g_connections[i]; + + if (!FD_ISSET(cl.sock, &set)) continue; + + if (!receiveFromConnection(cl)) { + g_connections.erase(g_connections.begin() + i); + --i; + + if (is_server) { + THREAD_PRINT("A controller disconnected from TAS server. Number of controllers: %d\n", g_connections.size()); + } + } + } +} + +static void mainThread() { + THREAD_PRINT("Starting TAS protocol connection\n"); + +#ifdef _WIN32 + WSADATA wsa_data; + int err = WSAStartup(MAKEWORD(2,2), &wsa_data); + if (err){ + THREAD_PRINT("Could not initialize TAS protocol: WSAStartup failed (%d)\n", err); + g_should_stop.store(true); + return; + } +#endif + + bool is_server = g_is_server.load(); + + if (is_server && !attemptToInitializeServer()) { + WSACleanup(); + g_should_stop.store(true); + return; + } + if (!is_server && !attemptConnectionToServer()) { + WSACleanup(); + g_should_stop.store(true); + return; + } + + while (!g_should_stop.load()) { + processConnections(is_server); + update(); + + if (g_connections.size() == 0 && !is_server) { + break; + } + } + + if (is_server) { + THREAD_PRINT("Stopping TAS server\n"); + } else { + THREAD_PRINT("Stopping TAS client\n"); + } + + for (auto &cl : g_connections) { + closesocket(cl.sock); + } + + closesocket(g_listen_sock); + WSACleanup(); + g_should_stop.store(true); +} + +static std::thread g_net_thread; +static bool g_running; + +static void restart() { + g_should_stop.store(true); + g_should_run = true; + g_stopped_manually = true; +} + +ON_EVENT(FRAME) { + auto now = std::chrono::high_resolution_clock::now(); + + if (!g_running && !g_stopped_manually && sar_tas_protocol_reconnect_delay.GetBool() && !g_is_server.load()) { + auto duration = ((std::chrono::duration)(now - g_last_connection_attemt_timestamp)).count(); + if (duration > sar_tas_protocol_reconnect_delay.GetFloat()) { + g_should_run = true; + console->Print("Attempting to reconnect to the TAS protocol server\n"); + } + } + + if (g_running && g_should_stop.load()) { + if (g_net_thread.joinable()) g_net_thread.join(); + g_running = false; + } else if (!g_running && g_should_run) { + g_should_stop.store(false); + g_net_thread = std::thread(mainThread); + g_running = true; + g_should_run = false; + g_stopped_manually = false; + + g_last_connection_attemt_timestamp = now; + } +} + +ON_EVENT_P(SAR_UNLOAD, -100) { + g_should_stop.store(true); + if (g_net_thread.joinable()) g_net_thread.join(); +} + +void TasProtocol::SetStatus(Status s) { + g_status_mutex.lock(); + g_current_status = s; + g_status_mutex.unlock(); + + if (s.active) { + for (auto &cl : g_connections) { + if (cl.contInfoEntSelector.length() == 0) continue; + + std::vector buf{SEND_CURRENT_TICK}; + encodeRaw32(buf, s.playback_tick); + send(cl.sock, (const char *)buf.data(), buf.size(), 0); + SendEntityInfo(cl, cl.contInfoEntSelector); + } + } +} + +void TasProtocol::SendProcessedScript(uint8_t slot, std::string scriptString) { + std::vector buf; + + buf.push_back(SEND_PROCESSED_SCRIPT); + + // slot + buf.push_back(slot); + + // script + encodeString(buf, scriptString); + + sendAll(buf); +} + +void TasProtocol::SendEntityInfo(TasProtocol::ConnectionData &conn, std::string entSelector) { + std::vector buf; + buf.push_back(SEND_ENTITY_INFO); + + CEntInfo *entInfo = entityList->QuerySelector(entSelector.c_str()); + if (entInfo != NULL) { + buf.push_back(1); + ServerEnt *ent = (ServerEnt *)entInfo->m_pEntity; + + Vector position = ent->abs_origin(); + encodeRawFloat(buf, position.x); + encodeRawFloat(buf, position.y); + encodeRawFloat(buf, position.z); + + QAngle angles = ent->abs_angles(); + encodeRawFloat(buf, angles.x); + encodeRawFloat(buf, angles.y); + encodeRawFloat(buf, angles.z); + + Vector velocity = ent->abs_velocity(); + encodeRawFloat(buf, velocity.x); + encodeRawFloat(buf, velocity.y); + encodeRawFloat(buf, velocity.z); + + } else { + buf.push_back(0); + } + + send(conn.sock, (const char *)buf.data(), buf.size(), 0); +} + +void TasProtocol::SendTextMessage(std::string message) { + std::vector buf; + buf.push_back(SEND_MESSAGE); + encodeString(buf, message); + sendAll(buf); +} + +ON_EVENT(FRAME) { + g_status_mutex.lock(); + if (g_current_status.active) { + g_current_debug_tick = g_current_status.playback_tick; + } else { + g_current_debug_tick = playerTrace->GetTasTraceTick(); + } + g_status_mutex.unlock(); +} + +CON_COMMAND(sar_tas_protocol_connect, + "sar_tas_protocol_connect - connect to the TAS protocol server.\n" + "ex: '127.0.0.1 5666' - '89.10.20.20 5666'.\n") { + if (args.ArgC() < 2 || args.ArgC() > 3) { + return console->Print(sar_tas_protocol_connect.ThisPtr()->m_pszHelpString); + } + + g_conn_data_mutex.lock(); + + g_client_ip = args[1]; + g_client_port = args.ArgC() >= 3 ? std::atoi(args[2]) : DEFAULT_TAS_CLIENT_SOCKET; + + g_conn_data_mutex.unlock(); + + g_is_server.store(false); + + restart(); +} + +CON_COMMAND(sar_tas_protocol_server, + "sar_tas_protocol_server [port] - starts a TAS protocol server. Port is 6555 by default.\n") { + if (args.ArgC() < 1 || args.ArgC() > 2) { + return console->Print(sar_tas_protocol_server.ThisPtr()->m_pszHelpString); + } + g_conn_data_mutex.lock(); + g_server_port = args.ArgC() >= 2 ? std::atoi(args[1]) : DEFAULT_TAS_SERVER_SOCKET; + g_conn_data_mutex.unlock(); + + g_is_server.store(true); + + restart(); +} + +CON_COMMAND(sar_tas_protocol_stop, + "sar_tas_protocol_stop - stops every TAS protocol related connection.\n") { + g_should_stop.store(true); + g_stopped_manually = true; +} + +CON_COMMAND(sar_tas_protocol_send_msg, "sar_tas_protocol_send_msg - sends a message over TAS protocol.\n") { + if (args.ArgC() != 2) { + return console->Print(sar_tas_protocol_send_msg.ThisPtr()->m_pszHelpString); + } + + TasProtocol::SendTextMessage(args[1]); +} \ No newline at end of file diff --git a/src/Features/Tas/TasProtocol.hpp b/src/Features/Tas/TasProtocol.hpp new file mode 100644 index 000000000..446bd6b67 --- /dev/null +++ b/src/Features/Tas/TasProtocol.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +// defining socket here manually because +// for unknown reasons winsock cannot be included here +#ifdef _WIN32 +# define SOCKET unsigned int +#else +# define SOCKET int +#endif + + +namespace TasProtocol { + enum class PlaybackState { + PLAYING, + PAUSED, + SKIPPING, + }; + + enum RecvMsg : uint8_t { + RECV_PLAY_SCRIPT = 0, + RECV_STOP = 1, + RECV_PLAYBACK_RATE = 2, + RECV_RESUME = 3, + RECV_PAUSE = 4, + RECV_FAST_FORWARD = 5, + RECV_SET_PAUSE_TICK = 6, + RECV_ADVANCE_TICK = 7, + RECV_MESSAGE = 8, + RECV_PLAY_SCRIPT_PROTOCOL = 10, + RECV_ENTITY_INFO = 100, + RECV_SET_CONT_ENTITY_INFO = 101 + }; + + enum SendMsg : uint8_t { + SEND_ACTIVE = 0, + SEND_INACTIVE = 1, + SEND_PLAYBACK_RATE = 2, + SEND_PLAYING = 3, + SEND_PAUSED = 4, + SEND_SKIPPING = 5, + SEND_CURRENT_TICK = 6, + SEND_DEBUG_TICK = 7, + SEND_MESSAGE = 8, + SEND_PROCESSED_SCRIPT = 10, + SEND_ENTITY_INFO = 100, + SEND_GAME_LOCATION = 255 + }; + + struct Status { + bool active; + std::string tas_path[2]; + PlaybackState playback_state; + float playback_rate; + int playback_tick; + }; + + struct ConnectionData { + SOCKET sock; + std::deque cmdbuf; + std::string contInfoEntSelector; + }; + + void SetStatus(Status s); + void SendProcessedScript(uint8_t slot, std::string scriptString); + void SendEntityInfo(ConnectionData& conn, std::string entSelector); + void SendTextMessage(std::string message); + +} // namespace TasProtocol diff --git a/src/Features/Tas/TasServer.cpp b/src/Features/Tas/TasServer.cpp deleted file mode 100644 index d25804326..000000000 --- a/src/Features/Tas/TasServer.cpp +++ /dev/null @@ -1,566 +0,0 @@ -// This *has* to come first because doesn't like being -// imported after . I fucking hate this platform -#ifdef _WIN32 -# include -# include -#else -# include -# include -# include -# include -#endif - -#include "TasServer.hpp" -#include "TasPlayer.hpp" -#include "Event.hpp" -#include "Scheduler.hpp" -#include "Modules/Console.hpp" -#include "Modules/Engine.hpp" -#include "Features/PlayerTrace.hpp" -#include "Features/EntityList.hpp" - -#include -#include -#include -#include -#include -#include - -#ifndef _WIN32 -# define SOCKET int -# define INVALID_SOCKET -1 -# define SOCKET_ERROR -1 -# define closesocket close -# define WSACleanup() (void)0 -#endif - -#define DEFAULT_TAS_CLIENT_SOCKET 6555 - -Variable sar_tas_server("sar_tas_server", "0", 0, "Enable the remote TAS server. Setting this value to something higher than one will bind the server on that port.\n"); - -struct ClientData { - SOCKET sock; - std::deque cmdbuf; -}; - -static SOCKET g_listen_sock = INVALID_SOCKET; -static std::vector g_clients; -static std::atomic g_should_stop; - -static TasStatus g_last_status; -static TasStatus g_current_status; -static int g_last_debug_tick; -static int g_current_debug_tick; -static std::mutex g_status_mutex; - -static uint32_t popRaw32(std::deque &buf) { - uint32_t val = 0; - for (int i = 0; i < 4; ++i) { - val <<= 8; - val |= buf[0]; - buf.pop_front(); - } - return val; -} - -static void encodeRaw32(std::vector &buf, uint32_t val) { - buf.push_back((val >> 24) & 0xFF); - buf.push_back((val >> 16) & 0xFF); - buf.push_back((val >> 8) & 0xFF); - buf.push_back((val >> 0) & 0xFF); -} - -// I know this is ugly, but I think copy pasting the above code is -// stupider so whatever -static void encodeRawFloat(std::vector &buf, float val) { - encodeRaw32(buf, *(uint32_t *) &val); -} - -static void sendAll(const std::vector &buf) { - for (auto &cl : g_clients) { - send(cl.sock, (const char *)buf.data(), buf.size(), 0); - } -} - -static void fullUpdate(ClientData &cl, bool first_packet = false) { - std::vector buf; - - if (first_packet) { - // game location (255) - std::string dir = std::filesystem::current_path().string(); - std::replace(dir.begin(), dir.end(), '\\', '/'); - buf.push_back(255); - encodeRaw32(buf, (uint32_t)dir.size()); - for (char c : dir) { - buf.push_back(c); - } - } - - // playback rate (2) - union { float f; int i; } rate = { g_last_status.playback_rate }; - buf.push_back(2); - encodeRaw32(buf, rate.i); - - if (g_last_status.active) { - // active (0) - buf.push_back(0); - encodeRaw32(buf, g_last_status.tas_path[0].size()); - for (char c : g_last_status.tas_path[0]) { - buf.push_back(c); - } - encodeRaw32(buf, g_last_status.tas_path[1].size()); - for (char c : g_last_status.tas_path[1]) { - buf.push_back(c); - } - - // state (3/4/5) - switch (g_last_status.playback_state) { - case PlaybackState::PLAYING: - buf.push_back(3); - break; - case PlaybackState::PAUSED: - buf.push_back(4); - break; - case PlaybackState::SKIPPING: - buf.push_back(5); - break; - } - - // current tick (6) - buf.push_back(6); - encodeRaw32(buf, g_last_status.playback_tick); - } else { - // inactive (1) - buf.push_back(1); - } - - // debug tick (7) - buf.push_back(7); - encodeRaw32(buf, (uint32_t)g_last_debug_tick); - - send(cl.sock, (const char *)buf.data(), buf.size(), 0); -} - -static void update() { - g_status_mutex.lock(); - TasStatus status = g_current_status; - int debug_tick = g_current_debug_tick; - g_status_mutex.unlock(); - - if (status.active != g_last_status.active || status.tas_path[0] != g_last_status.tas_path[0] || status.tas_path[1] != g_last_status.tas_path[1]) { - // big change; we might as well just do a full update - g_last_status = status; - g_last_debug_tick = debug_tick; - for (auto &cl : g_clients) fullUpdate(cl); - return; - } - - if (status.playback_rate != g_last_status.playback_rate) { - // playback rate (2) - - union { float f; int i; } rate = { status.playback_rate }; - - std::vector buf{2}; - encodeRaw32(buf, rate.i); - sendAll(buf); - - g_last_status.playback_rate = status.playback_rate; - } - - if (status.active && status.playback_state != g_last_status.playback_state) { - // state (3/4/5) - - switch (status.playback_state) { - case PlaybackState::PLAYING: - sendAll({3}); - break; - case PlaybackState::PAUSED: - sendAll({4}); - break; - case PlaybackState::SKIPPING: - sendAll({5}); - break; - } - - g_last_status.playback_state = status.playback_state; - } - - if (status.active && status.playback_tick != g_last_status.playback_tick) { - // tick (6) - - std::vector buf{6}; - encodeRaw32(buf, status.playback_tick); - sendAll(buf); - - g_last_status.playback_tick = status.playback_tick; - } - - if (debug_tick != g_last_debug_tick) { - // debug tick (7) - - std::vector buf{7}; - encodeRaw32(buf, (uint32_t)debug_tick); - sendAll(buf); - - g_last_debug_tick = debug_tick; - } -} - -static bool processCommands(ClientData &cl) { - while (true) { - if (cl.cmdbuf.size() == 0) return true; - - size_t extra = cl.cmdbuf.size() - 1; - - switch (cl.cmdbuf[0]) { - case 0: // request playback - if (extra < 8) return true; - { - std::deque copy = cl.cmdbuf; - - copy.pop_front(); - - uint32_t len1 = popRaw32(copy); - if (extra < 8 + len1) return true; - - std::string filename1; - for (size_t i = 0; i < len1; ++i) { - filename1 += copy[0]; - copy.pop_front(); - } - - uint32_t len2 = popRaw32(copy); - if (extra < 8 + len1 + len2) return true; - - std::string filename2; - for (size_t i = 0; i < len2; ++i) { - filename2 += copy[0]; - copy.pop_front(); - } - - cl.cmdbuf = copy; // We actually had everything we needed, so switch to the modified buffer - - Scheduler::OnMainThread([=](){ - tasPlayer->PlayFile(filename1, filename2); - }); - } - break; - - case 1: // stop playback - cl.cmdbuf.pop_front(); - Scheduler::OnMainThread([=](){ - tasPlayer->Stop(true); - }); - break; - - case 2: // request playback rate change - if (extra < 4) return true; - cl.cmdbuf.pop_front(); - { - union { uint32_t i; float f; } rate = { popRaw32(cl.cmdbuf) }; - Scheduler::OnMainThread([=](){ - sar_tas_playback_rate.SetValue(rate.f); - }); - } - break; - - case 3: // request state=playing - cl.cmdbuf.pop_front(); - Scheduler::OnMainThread([=](){ - tasPlayer->Resume(); - }); - break; - - case 4: // request state=paused - cl.cmdbuf.pop_front(); - Scheduler::OnMainThread([=](){ - tasPlayer->Pause(); - }); - break; - - case 5: // request state=fast-forward - if (extra < 5) return true; - cl.cmdbuf.pop_front(); - { - int tick = popRaw32(cl.cmdbuf); - bool pause_after = cl.cmdbuf[0]; - cl.cmdbuf.pop_front(); - Scheduler::OnMainThread([=](){ - sar_tas_skipto.SetValue(tick); - if (pause_after) sar_tas_pauseat.SetValue(tick); - }); - } - break; - - case 6: // set next pause tick - if (extra < 4) return true; - cl.cmdbuf.pop_front(); - { - int tick = popRaw32(cl.cmdbuf); - Scheduler::OnMainThread([=](){ - sar_tas_pauseat.SetValue(tick); - }); - } - break; - - case 7: // advance tick - cl.cmdbuf.pop_front(); - Scheduler::OnMainThread([](){ - tasPlayer->AdvanceFrame(); - }); - break; - - case 10: // send script and request playback - if (extra < 16) return true; - { - std::deque copy = cl.cmdbuf; - - copy.pop_front(); - - std::string data[4]; - uint32_t size_total = 0; - for (int i = 0; i < 4; ++i) { - uint32_t len = popRaw32(copy); - size_total += len; - if (extra < 16 + size_total) return true; - - for (size_t j = 0; j < len; ++j) { - data[i] += copy[0]; - copy.pop_front(); - } - } - - cl.cmdbuf = copy; // We actually had everything we needed, so switch to the modified buffer - - Scheduler::OnMainThread([=]() { - tasPlayer->PlayScript(data[0], data[1], data[2], data[3]); - }); - } - break; - case 100: // request entity info - if (extra < 4) return true; - { - std::deque copy = cl.cmdbuf; - copy.pop_front(); - - uint32_t len = popRaw32(copy); - if (extra < 4 + len) return true; - - std::string entSelector; - for (size_t i = 0; i < len; ++i) { - entSelector += copy[0]; - copy.pop_front(); - } - - cl.cmdbuf = copy; - - std::vector buf; - buf.push_back(100); // 100 response - entity info - - CEntInfo *entInfo = entityList->QuerySelector(entSelector.c_str()); - if (entInfo != NULL) { - buf.push_back(1); - ServerEnt* ent = (ServerEnt*)entInfo->m_pEntity; - - Vector position = ent->abs_origin(); - encodeRawFloat(buf, position.x); - encodeRawFloat(buf, position.y); - encodeRawFloat(buf, position.z); - - QAngle angles = ent->abs_angles(); - encodeRawFloat(buf, angles.x); - encodeRawFloat(buf, angles.y); - encodeRawFloat(buf, angles.z); - - Vector velocity = ent->abs_velocity(); - encodeRawFloat(buf, velocity.x); - encodeRawFloat(buf, velocity.y); - encodeRawFloat(buf, velocity.z); - - } else { - buf.push_back(0); - } - - send(cl.sock, (const char *)buf.data(), buf.size(), 0); - } - break; - - default: - return false; // Bad command - disconnect - } - } -} - -static void processConnections() { - fd_set set; - FD_ZERO(&set); - - SOCKET max = g_listen_sock; - - FD_SET(g_listen_sock, &set); - for (auto client : g_clients) { - FD_SET(client.sock, &set); - if (max < client.sock) max = client.sock; - } - - // 0.05s timeout - timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 50000; - - int nsock = select(max + 1, &set, nullptr, nullptr, &tv); - if (nsock == SOCKET_ERROR || !nsock) { - return; - } - - if (FD_ISSET(g_listen_sock, &set)) { - SOCKET cl = accept(g_listen_sock, nullptr, nullptr); - if (cl != INVALID_SOCKET) { - g_clients.push_back({ cl, {} }); - fullUpdate(g_clients[g_clients.size() - 1], true); - } - } - - for (size_t i = 0; i < g_clients.size(); ++i) { - auto &cl = g_clients[i]; - - if (!FD_ISSET(cl.sock, &set)) continue; - - char buf[1024]; - int len = recv(cl.sock, buf, sizeof buf, 0); - - if (len == 0 || len == SOCKET_ERROR) { // Connection closed or errored - g_clients.erase(g_clients.begin() + i); - --i; - continue; - } - - cl.cmdbuf.insert(cl.cmdbuf.end(), std::begin(buf), std::begin(buf) + len); - - if (!processCommands(cl)) { - // Client sent a bad command; terminate connection - closesocket(cl.sock); - g_clients.erase(g_clients.begin() + i); - --i; - continue; - } - } -} - -static void mainThread(int tas_server_port) { - THREAD_PRINT("Starting TAS server\n"); - -#ifdef _WIN32 - WSADATA wsa_data; - int err = WSAStartup(MAKEWORD(2,2), &wsa_data); - if (err){ - THREAD_PRINT("Could not initialize TAS server: WSAStartup failed (%d)\n", err); - return; - } -#endif - - g_listen_sock = socket(AF_INET6, SOCK_STREAM, 0); - if (g_listen_sock == INVALID_SOCKET) { - THREAD_PRINT("Could not initialize TAS server: socket creation failed\n"); - WSACleanup(); - return; - } - - // why tf is this enabled by default on Windows - int v6only = 0; - setsockopt(g_listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&v6only, sizeof v6only); - - struct sockaddr_in6 saddr{ - AF_INET6, - htons(tas_server_port), - 0, - in6addr_any, - 0, - }; - - if (bind(g_listen_sock, (struct sockaddr *)&saddr, sizeof saddr) == SOCKET_ERROR) { - THREAD_PRINT("Could not initialize TAS server: socket bind failed\n"); - closesocket(g_listen_sock); - WSACleanup(); - return; - } - - if (listen(g_listen_sock, 4) == SOCKET_ERROR) { - THREAD_PRINT("Could not initialize TAS server: socket listen failed\n"); - closesocket(g_listen_sock); - WSACleanup(); - return; - } - - while (!g_should_stop.load()) { - processConnections(); - update(); - } - - THREAD_PRINT("Stopping TAS server\n"); - - for (auto &cl : g_clients) { - closesocket(cl.sock); - } - - closesocket(g_listen_sock); - WSACleanup(); -} - -static std::thread g_net_thread; -static bool g_running; - -ON_EVENT(FRAME) { - int tas_server_port = sar_tas_server.GetInt() == 1 ? DEFAULT_TAS_CLIENT_SOCKET : sar_tas_server.GetInt(); - bool should_run = sar_tas_server.GetBool(); - - if (g_running && !should_run) { - g_should_stop.store(true); - if (g_net_thread.joinable()) g_net_thread.join(); - g_running = false; - } else if (!g_running && should_run) { - g_should_stop.store(false); - g_net_thread = std::thread(mainThread, tas_server_port); - g_running = true; - } -} - -ON_EVENT_P(SAR_UNLOAD, -100) { - sar_tas_server.SetValue(false); - g_should_stop.store(true); - if (g_net_thread.joinable()) g_net_thread.join(); -} - -void TasServer::SetStatus(TasStatus s) { - g_status_mutex.lock(); - g_current_status = s; - g_status_mutex.unlock(); -} - -void TasServer::SendProcessedScript(uint8_t slot, std::string scriptString) { - std::vector buf; - - // processed script reply (10) - buf.push_back(10); - - // slot - buf.push_back(slot); - - // script - encodeRaw32(buf, (uint32_t)scriptString.size()); - for (char c : scriptString) { - buf.push_back(c); - } - - sendAll(buf); -} - -ON_EVENT(FRAME) { - g_status_mutex.lock(); - if (g_current_status.active) { - g_current_debug_tick = g_current_status.playback_tick; - } else { - g_current_debug_tick = playerTrace->GetTasTraceTick(); - } - g_status_mutex.unlock(); -} diff --git a/src/Features/Tas/TasServer.hpp b/src/Features/Tas/TasServer.hpp deleted file mode 100644 index 3c543fb57..000000000 --- a/src/Features/Tas/TasServer.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -enum class PlaybackState { - PLAYING, - PAUSED, - SKIPPING, -}; - -struct TasStatus { - bool active; - std::string tas_path[2]; - PlaybackState playback_state; - float playback_rate; - int playback_tick; -}; - -namespace TasServer { - void SetStatus(TasStatus s); - void SendProcessedScript(uint8_t slot, std::string scriptString); -}; diff --git a/src/SourceAutoRecord.vcxproj b/src/SourceAutoRecord.vcxproj index b4d140706..243a1a3bd 100644 --- a/src/SourceAutoRecord.vcxproj +++ b/src/SourceAutoRecord.vcxproj @@ -237,7 +237,7 @@ - + @@ -436,7 +436,7 @@ - + diff --git a/src/SourceAutoRecord.vcxproj.filters b/src/SourceAutoRecord.vcxproj.filters index f2244420d..83f56fc83 100644 --- a/src/SourceAutoRecord.vcxproj.filters +++ b/src/SourceAutoRecord.vcxproj.filters @@ -316,7 +316,7 @@ SourceAutoRecord\Features\Tas - + SourceAutoRecord\Features\Tas @@ -969,7 +969,7 @@ SourceAutoRecord\Features\Tas - + SourceAutoRecord\Features\Tas