diff --git a/csgo_gc/CMakeLists.txt b/csgo_gc/CMakeLists.txt index b8cbb8a..294be6f 100644 --- a/csgo_gc/CMakeLists.txt +++ b/csgo_gc/CMakeLists.txt @@ -31,7 +31,7 @@ target_precompile_headers(csgo_gc PRIVATE stdafx.h) set_target_properties(csgo_gc PROPERTIES PREFIX "") if (MSVC) - target_compile_options(csgo_gc PRIVATE /W4 /wd4244) + target_compile_options(csgo_gc PRIVATE /W4 /wd4244 /wd4819) target_compile_definitions(csgo_gc PRIVATE _CRT_SECURE_NO_WARNINGS) else() target_compile_options(csgo_gc PRIVATE -Wall -Wextra) diff --git a/csgo_gc/gc_client.cpp b/csgo_gc/gc_client.cpp index 8d8dbfa..cc963ab 100644 --- a/csgo_gc/gc_client.cpp +++ b/csgo_gc/gc_client.cpp @@ -23,6 +23,10 @@ struct RconCommand std::promise response; }; +using RconCommandHolder = std::shared_ptr; + +constexpr size_t SourceRconResponseBodyLimit = 4096 - 10; + template bool TryParseNumber(std::string_view text, T &value) { @@ -241,6 +245,41 @@ std::vector SortedInventoryItems(const Inventory &inventory return items; } +template +std::string BuildLimitedRconLines(size_t lineCount, HeaderFor headerFor, LineFor lineFor) +{ + std::vector lines; + lines.reserve(lineCount); + + size_t lineBytes = 0; + bool truncated = false; + + for (size_t i = 0; i < lineCount; i++) + { + std::string line = lineFor(i); + std::string header = headerFor(lines.size() + 1, false); + size_t nextLineBytes = lineBytes + 1 + line.size(); + if (header.size() + nextLineBytes > SourceRconResponseBodyLimit) + { + truncated = true; + break; + } + + lineBytes = nextLineBytes; + lines.push_back(std::move(line)); + } + + std::string response = headerFor(lines.size(), truncated); + response.reserve(response.size() + lineBytes); + for (const std::string &line : lines) + { + response.push_back('\n'); + response.append(line); + } + + return response; +} + std::string Usage(std::string_view usage) { std::string response{ "ERR usage: " }; @@ -302,8 +341,10 @@ std::string ClientGC::RunRconCommand(std::string command) auto request = std::make_shared(std::move(command)); std::future response = request->response.get_future(); - auto holder = new std::shared_ptr{ request }; - PostToGC(GCEvent::RconCommand, 0, &holder, sizeof(holder)); + auto holder = std::make_unique(request); + RconCommandHolder *rawHolder = holder.get(); + PostToGC(GCEvent::RconCommand, 0, &rawHolder, sizeof(rawHolder)); + holder.release(); if (response.wait_for(std::chrono::seconds{ 5 }) != std::future_status::ready) { @@ -402,13 +443,13 @@ void ClientGC::HandleEvent(GCEvent type, uint64_t id, const std::vector break; case GCEvent::RconCommand: - if (buffer.size() == sizeof(std::shared_ptr *)) + if (buffer.size() == sizeof(RconCommandHolder *)) { - std::shared_ptr *holder; - memcpy(&holder, buffer.data(), sizeof(holder)); + RconCommandHolder *rawHolder; + memcpy(&rawHolder, buffer.data(), sizeof(rawHolder)); - std::shared_ptr request = std::move(*holder); - delete holder; + std::unique_ptr holder{ rawHolder }; + RconCommandHolder request = std::move(*holder); request->response.set_value(ExecuteRconCommand(request->command)); } @@ -444,6 +485,20 @@ const ClientGC::RconCommandDef *ClientGC::RconCommands(size_t &count) return Commands; } +std::string ClientGC::RconCommandUsageList() +{ + std::ostringstream response; + + size_t commandCount = 0; + const RconCommandDef *commands = RconCommands(commandCount); + for (size_t i = 0; i < commandCount; i++) + { + response << (i ? ", " : "") << commands[i].usage; + } + + return response.str(); +} + std::string ClientGC::ExecuteRconCommand(std::string_view command) { RconRequest request; @@ -486,17 +541,7 @@ std::string ClientGC::RconHelp(const RconRequest &request) return Usage("help"); } - std::ostringstream response; - response << "OK commands:"; - - size_t commandCount = 0; - const RconCommandDef *commands = RconCommands(commandCount); - for (size_t i = 0; i < commandCount; i++) - { - response << (i ? ", " : " ") << commands[i].usage; - } - - return response.str(); + return "OK commands: " + RconCommandUsageList(); } std::string ClientGC::RconPing(const RconRequest &request) @@ -550,20 +595,18 @@ std::string ClientGC::RconListItems(const RconRequest &request) std::vector items = SortedInventoryItems(m_inventory); const ItemSchema &schema = m_inventory.GetItemSchema(); - std::ostringstream response; - response << "OK count=" << items.size() << " shown=" << std::min(limit, items.size()); - - size_t shown = 0; - for (const CSOEconItem *item : items) - { - if (shown++ >= limit) - { - break; - } - response << "\n" << ItemSummary(schema, *item); - } - - return response.str(); + size_t maxShown = std::min(limit, items.size()); + return BuildLimitedRconLines(maxShown, + [&](size_t shown, bool truncated) { + std::ostringstream response; + response << "OK total=" << items.size() + << " shown=" << shown + << " truncated=" << (truncated ? 1 : 0); + return response.str(); + }, + [&](size_t index) { + return ItemSummary(schema, *items[index]); + }); } std::string ClientGC::RconFindItem(const RconRequest &request) @@ -594,20 +637,18 @@ std::string ClientGC::RconFindItem(const RconRequest &request) } constexpr size_t MaxShown = 50; - std::ostringstream response; - response << "OK matches=" << matches.size() << " shown=" << std::min(MaxShown, matches.size()); - - size_t shown = 0; - for (const CSOEconItem *item : matches) - { - if (shown++ >= MaxShown) - { - break; - } - response << "\n" << ItemSummary(schema, *item); - } - - return response.str(); + size_t maxShown = std::min(MaxShown, matches.size()); + return BuildLimitedRconLines(maxShown, + [&](size_t shown, bool truncated) { + std::ostringstream response; + response << "OK total=" << matches.size() + << " shown=" << shown + << " truncated=" << (truncated ? 1 : 0); + return response.str(); + }, + [&](size_t index) { + return ItemSummary(schema, *matches[index]); + }); } std::string ClientGC::RconItemInfo(const RconRequest &request) @@ -630,22 +671,29 @@ std::string ClientGC::RconItemInfo(const RconRequest &request) } const ItemSchema &schema = m_inventory.GetItemSchema(); - std::ostringstream response; - response << "OK " << ItemSummary(schema, *item) - << " inventory=" << item->inventory() - << " origin=" << item->origin() - << " flags=" << item->flags() - << " in_use=" << item->in_use() - << " equipped=" << EquippedSummary(*item) - << " attributes=" << item->attribute_size(); + auto header = [&](size_t shown, bool truncated) { + std::ostringstream response; + response << "OK " << ItemSummary(schema, *item) + << " inventory=" << item->inventory() + << " origin=" << item->origin() + << " flags=" << item->flags() + << " in_use=" << item->in_use() + << " equipped=" << EquippedSummary(*item) + << " attributes=" << item->attribute_size() + << " attr_shown=" << shown + << " truncated=" << (truncated ? 1 : 0); + return response.str(); + }; - for (const CSOEconItemAttribute &attribute : item->attribute()) - { - response << "\nattr defindex=" << attribute.def_index() + return BuildLimitedRconLines(static_cast(item->attribute_size()), + header, + [&](size_t index) { + const CSOEconItemAttribute &attribute = item->attribute(static_cast(index)); + std::ostringstream line; + line << "attr defindex=" << attribute.def_index() << " value=" << QuoteRconValue(schema.AttributeString(&attribute)); - } - - return response.str(); + return line.str(); + }); } std::string ClientGC::RconGiveItem(const RconRequest &request) diff --git a/csgo_gc/gc_client.h b/csgo_gc/gc_client.h index 1c39a14..54e241c 100644 --- a/csgo_gc/gc_client.h +++ b/csgo_gc/gc_client.h @@ -11,6 +11,7 @@ class ClientGC final : public SharedGC ~ClientGC(); uint32_t LocalPlayerMusicKitMVPsForRoundMVPEvent() const; std::string RunRconCommand(std::string command); + static std::string RconCommandUsageList(); private: struct RconRequest diff --git a/csgo_gc/gc_shared.h b/csgo_gc/gc_shared.h index 7bd6578..05b9ecf 100644 --- a/csgo_gc/gc_shared.h +++ b/csgo_gc/gc_shared.h @@ -17,7 +17,7 @@ enum class GCEvent LocalPlayerRoundMVP, // sent to client gc when the local player earns round MVP SyncLocalPlayerMusicKitState, // sent to client gc from the main thread, buffer contains the local userid ClientSOCacheUnsubscribe, // sent to server gc when a client disconnects, id contains the steam id - RconCommand, // sent to client gc, buffer contains a RconCommand shared_ptr holder + RconCommand, // sent to client gc, buffer contains an owned RconCommand holder pointer }; struct EventData diff --git a/csgo_gc/rcon_server.cpp b/csgo_gc/rcon_server.cpp index 5d03fd0..7e42259 100644 --- a/csgo_gc/rcon_server.cpp +++ b/csgo_gc/rcon_server.cpp @@ -536,7 +536,7 @@ std::string RconServer::ExecuteCommand(std::string command) if (name == "help") { - return "OK commands: help, ping, status, clients, list_items [limit], find_item , item_info , give_item [count] [key=value...], remove_item , refresh_inventory, save_inventory"; + return "OK commands: " + ClientGC::RconCommandUsageList(); } return "ERR no client gc"; diff --git a/docs/rcon.md b/docs/rcon.md index c268fba..6530b94 100644 --- a/docs/rcon.md +++ b/docs/rcon.md @@ -73,11 +73,17 @@ OK ERR ``` +Responses are kept within one Source RCON response packet. Commands that can +produce many lines, such as inventory listing or item detail inspection, include +`shown` and `truncated` fields. `truncated=1` means output stopped before all +available lines could fit in the single response packet. + Examples: ```text OK pong OK item_ids=1234567890123 +OK total=500 shown=48 truncated=1 ERR no client gc ERR unknown parameter foo ``` @@ -120,6 +126,9 @@ list_items [limit] The default limit is `50`; the maximum is `500`. +The response includes `total`, `shown`, and `truncated` fields. If +`truncated=1`, reduce the limit or use `find_item` to narrow the output. + ### `find_item` Finds inventory items by exact item id, exact defindex, display name, or custom @@ -131,6 +140,10 @@ Syntax: find_item ``` +The response includes `total`, `shown`, and `truncated` fields. At most the +first 50 matches are considered for display, and fewer may be shown if the +single Source RCON packet budget is reached. + ### `item_info` Shows detailed information for one inventory item, including attributes and @@ -142,6 +155,10 @@ Syntax: item_info ``` +The response includes `attributes`, `attr_shown`, and `truncated` fields. If an +item has unusually many or long attributes, `truncated=1` indicates that some +attribute lines were omitted from the response. + ### `give_item` Creates one or more inventory items and sends live SO create updates to the game.