summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/command.cpp32
-rw-r--r--src/command_func.h7
-rw-r--r--src/command_type.h8
-rw-r--r--src/core/span_type.hpp2
-rw-r--r--src/misc/CMakeLists.txt1
-rw-r--r--src/misc/endian_buffer.hpp206
-rw-r--r--src/network/core/packet.cpp28
-rw-r--r--src/network/core/packet.h2
-rw-r--r--src/network/network.cpp28
-rw-r--r--src/network/network_admin.cpp5
-rw-r--r--src/network/network_command.cpp129
-rw-r--r--src/network/network_internal.h13
-rw-r--r--src/network/network_server.cpp8
-rw-r--r--src/string.cpp18
-rw-r--r--src/string_func.h3
15 files changed, 455 insertions, 35 deletions
diff --git a/src/command.cpp b/src/command.cpp
index 001f2ee7d..788c71989 100644
--- a/src/command.cpp
+++ b/src/command.cpp
@@ -55,6 +55,8 @@
#include "viewport_cmd.h"
#include "water_cmd.h"
#include "waypoint_cmd.h"
+#include "misc/endian_buffer.hpp"
+#include "string_func.h"
#include <array>
@@ -400,11 +402,37 @@ bool DoCommandP(Commands cmd, StringID err_message, CommandCallback *callback, T
}
/**
+ * Toplevel network safe docommand function for the current company. Must not be called recursively.
+ * The callback is called when the command succeeded or failed. The parameters
+ * \a tile, \a p1, and \a p2 are from the #CommandProc function. The parameter \a cmd is the command to execute.
+ *
+ * @param cmd The command to execute (a CMD_* value)
+ * @param err_message Message prefix to show on error
+ * @param callback A callback function to call after the command is finished
+ * @param my_cmd indicator if the command is from a company or server (to display error messages for a user)
+ * @param tile The tile to perform a command on (see #CommandProc)
+ * @param p1 Additional data for the command (see #CommandProc)
+ * @param p2 Additional data for the command (see #CommandProc)
+ * @param text The text to pass
+ * @return \c true if the command succeeded, else \c false.
+ */
+bool InjectNetworkCommand(Commands cmd, StringID err_message, CommandCallback *callback, bool my_cmd, TileIndex tile, uint32 p1, uint32 p2, const std::string &text)
+{
+ return DoCommandP(cmd, err_message, callback, my_cmd, true, tile, p1, p2, text);
+}
+
+/**
* Helper to deduplicate the code for returning.
* @param cmd the command cost to return.
*/
#define return_dcpi(cmd) { _docommand_recursive = 0; return cmd; }
+/** Helper to format command parameters into a hex string. */
+static std::string CommandParametersToHexString(TileIndex tile, uint32 p1, uint32 p2, const std::string &text)
+{
+ return FormatArrayAsHex(EndianBufferWriter<>::FromValue(std::make_tuple(tile, p1, p2, text)));
+}
+
/*!
* Helper function for the toplevel network safe docommand function for the current company.
*
@@ -482,7 +510,7 @@ CommandCost DoCommandPInternal(Commands cmd, StringID err_message, CommandCallba
if (!_networking || _generating_world || network_command) {
/* Log the failed command as well. Just to be able to be find
* causes of desyncs due to bad command test implementations. */
- Debug(desync, 1, "cmdf: {:08x}; {:02x}; {:02x}; {:06x}; {:08x}; {:08x}; {:08x}; {:08x}; \"{}\" ({})", _date, _date_fract, (int)_current_company, tile, p1, p2, cmd, err_message, text, GetCommandName(cmd));
+ Debug(desync, 1, "cmdf: {:08x}; {:02x}; {:02x}; {:08x}; {:08x}; {:06x}; {} ({})", _date, _date_fract, (int)_current_company, cmd, err_message, tile, CommandParametersToHexString(tile, p1, p2, text), GetCommandName(cmd));
}
cur_company.Restore();
return_dcpi(res);
@@ -502,7 +530,7 @@ CommandCost DoCommandPInternal(Commands cmd, StringID err_message, CommandCallba
* reset the storages as we've not executed the command. */
return_dcpi(CommandCost());
}
- Debug(desync, 1, "cmd: {:08x}; {:02x}; {:02x}; {:06x}; {:08x}; {:08x}; {:08x}; {:08x}; \"{}\" ({})", _date, _date_fract, (int)_current_company, tile, p1, p2, cmd, err_message, text, GetCommandName(cmd));
+ Debug(desync, 1, "cmd: {:08x}; {:02x}; {:02x}; {:08x}; {:08x}; {:06x}; {} ({})", _date, _date_fract, (int)_current_company, cmd, err_message, tile, CommandParametersToHexString(tile, p1, p2, text), GetCommandName(cmd));
/* Actually try and execute the command. If no cost-type is given
* use the construction one */
diff --git a/src/command_func.h b/src/command_func.h
index 03bfc73f1..0d000755c 100644
--- a/src/command_func.h
+++ b/src/command_func.h
@@ -12,6 +12,7 @@
#include "command_type.h"
#include "company_type.h"
+#include <vector>
/**
* Define a default return value for a failed command.
@@ -32,6 +33,9 @@ static const CommandCost CMD_ERROR = CommandCost(INVALID_STRING_ID);
*/
#define return_cmd_error(errcode) return CommandCost(errcode);
+/** Storage buffer for serialized command data. */
+typedef std::vector<byte> CommandDataBuffer;
+
CommandCost DoCommand(DoCommandFlag flags, Commands cmd, TileIndex tile, uint32 p1, uint32 p2, const std::string &text = {});
CommandCost DoCommand(const CommandContainer *container, DoCommandFlag flags);
@@ -41,9 +45,12 @@ bool DoCommandP(Commands cmd, CommandCallback *callback, TileIndex tile, uint32
bool DoCommandP(Commands cmd, TileIndex tile, uint32 p1, uint32 p2, const std::string &text = {});
bool DoCommandP(const CommandContainer *container, bool my_cmd = true, bool network_command = false);
+bool InjectNetworkCommand(Commands cmd, StringID err_message, CommandCallback *callback, bool my_cmd, TileIndex tile, uint32 p1, uint32 p2, const std::string &text);
+
CommandCost DoCommandPInternal(Commands cmd, StringID err_message, CommandCallback *callback, bool my_cmd, bool estimate_only, bool network_command, TileIndex tile, uint32 p1, uint32 p2, const std::string &text);
void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex tile, uint32 p1, uint32 p2, const std::string &text);
+void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex location, const CommandDataBuffer &cmd_data);
extern Money _additional_cash_required;
diff --git a/src/command_type.h b/src/command_type.h
index fa381ce13..3a15087b9 100644
--- a/src/command_type.h
+++ b/src/command_type.h
@@ -424,11 +424,19 @@ enum CommandPauseLevel {
typedef CommandCost CommandProc(DoCommandFlag flags, TileIndex tile, uint32 p1, uint32 p2, const std::string &text);
+template <typename T> struct CommandFunctionTraitHelper;
+template <typename... Targs>
+struct CommandFunctionTraitHelper<CommandCost(*)(DoCommandFlag, Targs...)> {
+ using Args = std::tuple<std::decay_t<Targs>...>;
+};
+
/** Defines the traits of a command. */
template <Commands Tcmd> struct CommandTraits;
#define DEF_CMD_TRAIT(cmd_, proc_, flags_, type_) \
template<> struct CommandTraits<cmd_> { \
+ using Args = typename CommandFunctionTraitHelper<decltype(&proc_)>::Args; \
+ static constexpr Commands cmd = cmd_; \
static constexpr auto &proc = proc_; \
static constexpr CommandFlags flags = (CommandFlags)(flags_); \
static constexpr CommandType type = type_; \
diff --git a/src/core/span_type.hpp b/src/core/span_type.hpp
index 614be8456..0df528816 100644
--- a/src/core/span_type.hpp
+++ b/src/core/span_type.hpp
@@ -92,6 +92,8 @@ public:
constexpr const_iterator cbegin() const noexcept { return const_iterator(first); }
constexpr const_iterator cend() const noexcept { return const_iterator(last); }
+ constexpr reference operator[](size_type idx) const { return first[idx]; }
+
private:
pointer first;
pointer last;
diff --git a/src/misc/CMakeLists.txt b/src/misc/CMakeLists.txt
index ee2ca6a41..24cde73e4 100644
--- a/src/misc/CMakeLists.txt
+++ b/src/misc/CMakeLists.txt
@@ -5,6 +5,7 @@ add_files(
countedptr.hpp
dbg_helpers.cpp
dbg_helpers.h
+ endian_buffer.hpp
fixedsizearray.hpp
getoptdata.cpp
getoptdata.h
diff --git a/src/misc/endian_buffer.hpp b/src/misc/endian_buffer.hpp
new file mode 100644
index 000000000..c20d9a8b9
--- /dev/null
+++ b/src/misc/endian_buffer.hpp
@@ -0,0 +1,206 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/** @file endian_buffer.hpp Endian-aware buffer. */
+
+#ifndef ENDIAN_BUFFER_HPP
+#define ENDIAN_BUFFER_HPP
+
+#include <iterator>
+#include <string_view>
+#include "../core/span_type.hpp"
+#include "../core/bitmath_func.hpp"
+
+struct StrongTypedefBase;
+
+/**
+ * Endian-aware buffer adapter that always writes values in little endian order.
+ * @note This class uses operator overloading (<<, just like streams) for writing
+ * as this allows providing custom operator overloads for more complex types
+ * like e.g. structs without needing to modify this class.
+ */
+template <typename Tcont = typename std::vector<byte>, typename Titer = typename std::back_insert_iterator<Tcont>>
+class EndianBufferWriter {
+ /** Output iterator for the destination buffer. */
+ Titer buffer;
+
+public:
+ EndianBufferWriter(Titer buffer) : buffer(buffer) {}
+ EndianBufferWriter(typename Titer::container_type &container) : buffer(std::back_inserter(container)) {}
+
+ EndianBufferWriter &operator <<(const std::string &data) { return *this << std::string_view{ data }; }
+ EndianBufferWriter &operator <<(const char *data) { return *this << std::string_view{ data }; }
+ EndianBufferWriter &operator <<(std::string_view data) { this->Write(data); return *this; }
+ EndianBufferWriter &operator <<(bool data) { return *this << static_cast<byte>(data ? 1 : 0); }
+
+ template <typename... Targs>
+ EndianBufferWriter &operator <<(const std::tuple<Targs...> &data)
+ {
+ this->WriteTuple(data, std::index_sequence_for<Targs...>{});
+ return *this;
+ }
+
+ template <class T, std::enable_if_t<std::disjunction_v<std::negation<std::is_class<T>>, std::is_base_of<StrongTypedefBase, T>>, int> = 0>
+ EndianBufferWriter &operator <<(const T data)
+ {
+ if constexpr (std::is_enum_v<T>) {
+ this->Write(static_cast<std::underlying_type_t<const T>>(data));
+ } else if constexpr (std::is_base_of_v<StrongTypedefBase, T>) {
+ this->Write(data.value);
+ } else {
+ this->Write(data);
+ }
+ return *this;
+ }
+
+ template <typename Tvalue, typename Tbuf = std::vector<byte>>
+ static Tbuf FromValue(const Tvalue &data)
+ {
+ Tbuf buffer;
+ EndianBufferWriter writer{ buffer };
+ writer << data;
+ return buffer;
+ }
+
+private:
+ /** Helper function to write a tuple to the buffer. */
+ template<class Ttuple, size_t... Tindices>
+ void WriteTuple(const Ttuple &values, std::index_sequence<Tindices...>) {
+ ((*this << std::get<Tindices>(values)), ...);
+ }
+
+ /** Write overload for string values. */
+ void Write(std::string_view value)
+ {
+ for (auto c : value) {
+ this->buffer++ = c;
+ }
+ this->buffer++ = '\0';
+ }
+
+ /** Fundamental write function. */
+ template <class T>
+ void Write(T value)
+ {
+ static_assert(sizeof(T) <= 8, "Value can't be larger than 8 bytes");
+
+ if constexpr (sizeof(T) > 1) {
+ this->buffer++ = GB(value, 0, 8);
+ this->buffer++ = GB(value, 8, 8);
+ if constexpr (sizeof(T) > 2) {
+ this->buffer++ = GB(value, 16, 8);
+ this->buffer++ = GB(value, 24, 8);
+ }
+ if constexpr (sizeof(T) > 4) {
+ this->buffer++ = GB(value, 32, 8);
+ this->buffer++ = GB(value, 40, 8);
+ this->buffer++ = GB(value, 48, 8);
+ this->buffer++ = GB(value, 56, 8);
+ }
+ } else {
+ this->buffer++ = value;
+ }
+ }
+};
+
+/**
+ * Endian-aware buffer adapter that always reads values in little endian order.
+ * @note This class uses operator overloading (>>, just like streams) for reading
+ * as this allows providing custom operator overloads for more complex types
+ * like e.g. structs without needing to modify this class.
+ */
+class EndianBufferReader {
+ /** Reference to storage buffer. */
+ span<const byte> buffer;
+ /** Current read position. */
+ size_t read_pos = 0;
+
+public:
+ EndianBufferReader(span<const byte> buffer) : buffer(buffer) {}
+
+ void rewind() { this->read_pos = 0; }
+
+ EndianBufferReader &operator >>(std::string &data) { data = this->ReadStr(); return *this; }
+ EndianBufferReader &operator >>(bool &data) { data = this->Read<byte>() != 0; return *this; }
+
+ template <typename... Targs>
+ EndianBufferReader &operator >>(std::tuple<Targs...> &data)
+ {
+ this->ReadTuple(data, std::index_sequence_for<Targs...>{});
+ return *this;
+ }
+
+ template <class T, std::enable_if_t<std::disjunction_v<std::negation<std::is_class<T>>, std::is_base_of<StrongTypedefBase, T>>, int> = 0>
+ EndianBufferReader &operator >>(T &data)
+ {
+ if constexpr (std::is_enum_v<T>) {
+ data = static_cast<T>(this->Read<std::underlying_type_t<T>>());
+ } else if constexpr (std::is_base_of_v<StrongTypedefBase, T>) {
+ data.value = this->Read<decltype(data.value)>();
+ } else {
+ data = this->Read<T>();
+ }
+ return *this;
+ }
+
+ template <typename Tvalue>
+ static Tvalue ToValue(span<const byte> buffer)
+ {
+ Tvalue result{};
+ EndianBufferReader reader{ buffer };
+ reader >> result;
+ return result;
+ }
+
+private:
+ /** Helper function to read a tuple from the buffer. */
+ template<class Ttuple, size_t... Tindices>
+ void ReadTuple(Ttuple &values, std::index_sequence<Tindices...>) {
+ ((*this >> std::get<Tindices>(values)), ...);
+ }
+
+ /** Read overload for string data. */
+ std::string ReadStr()
+ {
+ std::string str;
+ while (this->read_pos < this->buffer.size()) {
+ char ch = this->Read<char>();
+ if (ch == '\0') break;
+ str.push_back(ch);
+ }
+ return str;
+ }
+
+ /** Fundamental read function. */
+ template <class T>
+ T Read()
+ {
+ static_assert(!std::is_const_v<T>, "Can't read into const variables");
+ static_assert(sizeof(T) <= 8, "Value can't be larger than 8 bytes");
+
+ if (read_pos + sizeof(T) > this->buffer.size()) return {};
+
+ T value = static_cast<T>(this->buffer[this->read_pos++]);
+ if constexpr (sizeof(T) > 1) {
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 8;
+ }
+ if constexpr (sizeof(T) > 2) {
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 16;
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 24;
+ }
+ if constexpr (sizeof(T) > 4) {
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 32;
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 40;
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 48;
+ value += static_cast<T>(this->buffer[this->read_pos++]) << 56;
+ }
+
+ return value;
+ }
+};
+
+#endif /* ENDIAN_BUFFER_HPP */
diff --git a/src/network/core/packet.cpp b/src/network/core/packet.cpp
index e106d5787..ec0919757 100644
--- a/src/network/core/packet.cpp
+++ b/src/network/core/packet.cpp
@@ -186,6 +186,17 @@ void Packet::Send_string(const std::string_view data)
}
/**
+ * Copy a sized byte buffer into the packet.
+ * @param data The data to send.
+ */
+void Packet::Send_buffer(const std::vector<byte> &data)
+{
+ assert(this->CanWriteToPacket(sizeof(uint16) + data.size()));
+ this->Send_uint16((uint16)data.size());
+ this->buffer.insert(this->buffer.end(), data.begin(), data.end());
+}
+
+/**
* Send as many of the bytes as possible in the packet. This can mean
* that it is possible that not all bytes are sent. To cope with this
* the function returns the amount of bytes that were actually sent.
@@ -367,6 +378,23 @@ uint64 Packet::Recv_uint64()
}
/**
+ * Extract a sized byte buffer from the packet.
+ * @return The extracted buffer.
+ */
+std::vector<byte> Packet::Recv_buffer()
+{
+ uint16 size = this->Recv_uint16();
+ if (size == 0 || !this->CanReadFromPacket(size, true)) return {};
+
+ std::vector<byte> data;
+ while (size-- > 0) {
+ data.push_back(this->buffer[this->pos++]);
+ }
+
+ return data;
+}
+
+/**
* Reads characters (bytes) from the packet until it finds a '\0', or reaches a
* maximum of \c length characters.
* When the '\0' has not been reached in the first \c length read characters,
diff --git a/src/network/core/packet.h b/src/network/core/packet.h
index 277ff8bba..04a232e1c 100644
--- a/src/network/core/packet.h
+++ b/src/network/core/packet.h
@@ -72,6 +72,7 @@ public:
void Send_uint32(uint32 data);
void Send_uint64(uint64 data);
void Send_string(const std::string_view data);
+ void Send_buffer(const std::vector<byte> &data);
size_t Send_bytes (const byte *begin, const byte *end);
/* Reading/receiving of packets */
@@ -87,6 +88,7 @@ public:
uint16 Recv_uint16();
uint32 Recv_uint32();
uint64 Recv_uint64();
+ std::vector<byte> Recv_buffer();
std::string Recv_string(size_t length, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);
size_t RemainingBytesToTransfer() const;
diff --git a/src/network/network.cpp b/src/network/network.cpp
index 13f2fe52a..8194f34d0 100644
--- a/src/network/network.cpp
+++ b/src/network/network.cpp
@@ -35,6 +35,7 @@
#include "../core/pool_func.hpp"
#include "../gfx_func.h"
#include "../error.h"
+#include "../misc_cmd.h"
#include <charconv>
#include <sstream>
#include <iomanip>
@@ -1064,8 +1065,8 @@ void NetworkGameLoop()
while (f != nullptr && !feof(f)) {
if (_date == next_date && _date_fract == next_date_fract) {
if (cp != nullptr) {
- NetworkSendCommand(cp->cmd, cp->err_msg, nullptr, cp->company, cp->tile, cp->p1, cp->p2, cp->text);
- Debug(desync, 0, "Injecting: {:08x}; {:02x}; {:02x}; {:06x}; {:08x}; {:08x}; {:08x}; \"{}\" ({})", _date, _date_fract, (int)_current_company, cp->tile, cp->p1, cp->p2, cp->cmd, cp->text, GetCommandName(cp->cmd));
+ NetworkSendCommand(cp->cmd, cp->err_msg, nullptr, cp->company, cp->data);
+ Debug(desync, 0, "Injecting: {:08x}; {:02x}; {:02x}; {:08x}; {:06x}; {} ({})", _date, _date_fract, (int)_current_company, cp->cmd, cp->tile, FormatArrayAsHex(cp->data), GetCommandName(cp->cmd));
delete cp;
cp = nullptr;
}
@@ -1104,15 +1105,21 @@ void NetworkGameLoop()
cp = new CommandPacket();
int company;
uint cmd;
- char buffer[128];
- int ret = sscanf(p, "%x; %x; %x; %x; %x; %x; %x; %x; \"%127[^\"]\"", &next_date, &next_date_fract, &company, &cp->tile, &cp->p1, &cp->p2, &cmd, &cp->err_msg, buffer);
- cp->text = buffer;
- /* There are 8 pieces of data to read, however the last is a
- * string that might or might not exist. Ignore it if that
- * string misses because in 99% of the time it's not used. */
- assert(ret == 9 || ret == 8);
+ char buffer[256];
+ int ret = sscanf(p, "%x; %x; %x; %x; %x; %x; %255s", &next_date, &next_date_fract, &company, &cmd, &cp->err_msg, &cp->tile, buffer);
+ assert(ret == 6);
cp->company = (CompanyID)company;
cp->cmd = (Commands)cmd;
+
+ /* Parse command data. */
+ std::vector<byte> args;
+ size_t arg_len = strlen(buffer);
+ for (size_t i = 0; i + 1 < arg_len; i += 2) {
+ byte e = 0;
+ std::from_chars(buffer + i, buffer + i + 1, e, 16);
+ args.emplace_back(e);
+ }
+ cp->data = args;
} else if (strncmp(p, "join: ", 6) == 0) {
/* Manually insert a pause when joining; this way the client can join at the exact right time. */
int ret = sscanf(p + 6, "%x; %x", &next_date, &next_date_fract);
@@ -1121,8 +1128,7 @@ void NetworkGameLoop()
cp = new CommandPacket();
cp->company = COMPANY_SPECTATOR;
cp->cmd = CMD_PAUSE;
- cp->p1 = PM_PAUSED_NORMAL;
- cp->p2 = 1;
+ cp->data = EndianBufferWriter<>::FromValue(CommandTraits<CMD_PAUSE>::Args{ 0, PM_PAUSED_NORMAL, 1, "" });
_ddc_fastforward = false;
} else if (strncmp(p, "sync: ", 6) == 0) {
int ret = sscanf(p + 6, "%x; %x; %x; %x", &next_date, &next_date_fract, &sync_state[0], &sync_state[1]);
diff --git a/src/network/network_admin.cpp b/src/network/network_admin.cpp
index 99f803e24..4711cdf04 100644
--- a/src/network/network_admin.cpp
+++ b/src/network/network_admin.cpp
@@ -630,10 +630,7 @@ NetworkRecvStatus ServerNetworkAdminSocketHandler::SendCmdLogging(ClientID clien
p->Send_uint32(client_id);
p->Send_uint8 (cp->company);
p->Send_uint16(cp->cmd);
- p->Send_uint32(cp->p1);
- p->Send_uint32(cp->p2);
- p->Send_uint32(cp->tile);
- p->Send_string(cp->text);
+ p->Send_buffer(cp->data);
p->Send_uint32(cp->frame);
this->SendPacket(p);
diff --git a/src/network/network_command.cpp b/src/network/network_command.cpp
index 0fae6bcbf..472d5e60e 100644
--- a/src/network/network_command.cpp
+++ b/src/network/network_command.cpp
@@ -15,18 +15,41 @@
#include "../company_func.h"
#include "../settings_type.h"
#include "../airport_cmd.h"
+#include "../aircraft_cmd.h"
+#include "../autoreplace_cmd.h"
+#include "../company_cmd.h"
#include "../depot_cmd.h"
#include "../dock_cmd.h"
+#include "../economy_cmd.h"
+#include "../engine_cmd.h"
+#include "../goal_cmd.h"
#include "../group_cmd.h"
#include "../industry_cmd.h"
+#include "../landscape_cmd.h"
+#include "../misc_cmd.h"
+#include "../news_cmd.h"
+#include "../object_cmd.h"
+#include "../order_cmd.h"
#include "../rail_cmd.h"
#include "../road_cmd.h"
+#include "../roadveh_cmd.h"
+#include "../settings_cmd.h"
+#include "../signs_cmd.h"
+#include "../station_cmd.h"
+#include "../story_cmd.h"
+#include "../subsidy_cmd.h"
#include "../terraform_cmd.h"
+#include "../timetable_cmd.h"
#include "../town_cmd.h"
#include "../train_cmd.h"
+#include "../tree_cmd.h"
#include "../tunnelbridge_cmd.h"
#include "../vehicle_cmd.h"
+#include "../viewport_cmd.h"
+#include "../water_cmd.h"
+#include "../waypoint_cmd.h"
#include "../script/script_cmd.h"
+#include <array>
#include "../safeguards.h"
@@ -62,6 +85,23 @@ static CommandCallback * const _callback_table[] = {
/* 0x1B */ CcAddVehicleNewGroup,
};
+/* Helpers to generate the command dispatch table from the command traits. */
+
+template <Commands Tcmd> static CommandDataBuffer SanitizeCmdStrings(const CommandDataBuffer &data);
+template <Commands Tcmd> static void UnpackNetworkCommand(const CommandPacket *cp);
+struct CommandDispatch {
+ CommandDataBuffer(*Sanitize)(const CommandDataBuffer &);
+ void (*Unpack)(const CommandPacket *);
+};
+
+template<typename T, T... i>
+inline constexpr auto MakeDispatchTable(std::integer_sequence<T, i...>) noexcept
+{
+ return std::array<CommandDispatch, sizeof...(i)>{{ { &SanitizeCmdStrings<static_cast<Commands>(i)>, &UnpackNetworkCommand<static_cast<Commands>(i)> }... }};
+}
+static constexpr auto _cmd_dispatch = MakeDispatchTable(std::make_integer_sequence<std::underlying_type_t<Commands>, CMD_END>{});
+
+
/**
* Append a CommandPacket at the end of the queue.
* @param p The packet to append to the queue.
@@ -149,15 +189,28 @@ static CommandQueue _local_execution_queue;
*/
void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex tile, uint32 p1, uint32 p2, const std::string &text)
{
+ auto data = EndianBufferWriter<CommandDataBuffer>::FromValue(std::make_tuple(tile, p1, p2, text));
+ NetworkSendCommand(cmd, err_message, callback, company, tile, data);
+}
+
+/**
+ * Prepare a DoCommand to be send over the network
+ * @param cmd The command to execute (a CMD_* value)
+ * @param err_message Message prefix to show on error
+ * @param callback A callback function to call after the command is finished
+ * @param company The company that wants to send the command
+ * @param location Location of the command (e.g. for error message position)
+ * @param cmd_data The command proc arguments.
+ */
+void NetworkSendCommand(Commands cmd, StringID err_message, CommandCallback *callback, CompanyID company, TileIndex location, const CommandDataBuffer &cmd_data)
+{
CommandPacket c;
c.company = company;
- c.tile = tile;
- c.p1 = p1;
- c.p2 = p2;
c.cmd = cmd;
c.err_msg = err_message;
c.callback = callback;
- c.text = text;
+ c.tile = location;
+ c.data = cmd_data;
if (_network_server) {
/* If we are the server, we queue the command in our 'special' queue.
@@ -220,7 +273,7 @@ void NetworkExecuteLocalCommandQueue()
/* We can execute this command */
_current_company = cp->company;
- DoCommandP(cp, cp->my_cmd, true);
+ _cmd_dispatch[cp->cmd].Unpack(cp);
queue.Pop();
delete cp;
@@ -311,11 +364,8 @@ const char *NetworkGameSocketHandler::ReceiveCommand(Packet *p, CommandPacket *c
if (!IsValidCommand(cp->cmd)) return "invalid command";
if (GetCommandFlags(cp->cmd) & CMD_OFFLINE) return "single-player only command";
cp->err_msg = p->Recv_uint16();
-
- cp->p1 = p->Recv_uint32();
- cp->p2 = p->Recv_uint32();
cp->tile = p->Recv_uint32();
- cp->text = p->Recv_string(NETWORK_COMPANY_NAME_LENGTH, (!_network_server && GetCommandFlags(cp->cmd) & CMD_STR_CTRL) != 0 ? SVS_ALLOW_CONTROL_CODE | SVS_REPLACE_WITH_QUESTION_MARK : SVS_REPLACE_WITH_QUESTION_MARK);
+ cp->data = _cmd_dispatch[cp->cmd].Sanitize(p->Recv_buffer());
byte callback = p->Recv_uint8();
if (callback >= lengthof(_callback_table)) return "invalid callback";
@@ -331,13 +381,11 @@ const char *NetworkGameSocketHandler::ReceiveCommand(Packet *p, CommandPacket *c
*/
void NetworkGameSocketHandler::SendCommand(Packet *p, const CommandPacket *cp)
{
- p->Send_uint8 (cp->company);
+ p->Send_uint8(cp->company);
p->Send_uint16(cp->cmd);
p->Send_uint16(cp->err_msg);
- p->Send_uint32(cp->p1);
- p->Send_uint32(cp->p2);
p->Send_uint32(cp->tile);
- p->Send_string(cp->text);
+ p->Send_buffer(cp->data);
byte callback = 0;
while (callback < lengthof(_callback_table) && _callback_table[callback] != cp->callback) {
@@ -350,3 +398,58 @@ void NetworkGameSocketHandler::SendCommand(Packet *p, const CommandPacket *cp)
}
p->Send_uint8 (callback);
}
+
+/**
+ * Insert a client ID into the command data in a command packet.
+ * @param cp Command packet to modify.
+ * @param client_id Client id to insert.
+ */
+void NetworkReplaceCommandClientId(CommandPacket &cp, ClientID client_id)
+{
+ /* Unpack command parameters. */
+ auto params = EndianBufferReader::ToValue<std::tuple<TileIndex, uint32, uint32, std::string>>(cp.data);
+
+ /* Insert client id. */
+ std::get<2>(params) = client_id;
+
+ /* Repack command parameters. */
+ cp.data = EndianBufferWriter<CommandDataBuffer>::FromValue(params);
+}
+
+
+/** Validate a single string argument coming from network. */
+template <class T>
+static inline void SanitizeSingleStringHelper([[maybe_unused]] CommandFlags cmd_flags, T &data)
+{
+ if constexpr (std::is_same_v<std::string, T>) {
+ data = StrMakeValid(data.substr(0, NETWORK_COMPANY_NAME_LENGTH), (!_network_server && cmd_flags & CMD_STR_CTRL) != 0 ? SVS_ALLOW_CONTROL_CODE | SVS_REPLACE_WITH_QUESTION_MARK : SVS_REPLACE_WITH_QUESTION_MARK);
+ }
+}
+
+/** Helper function to perform validation on command data strings. */
+template<class Ttuple, size_t... Tindices>
+static inline void SanitizeStringsHelper(CommandFlags cmd_flags, Ttuple &values, std::index_sequence<Tindices...>)
+{
+ ((SanitizeSingleStringHelper(cmd_flags, std::get<Tindices>(values))), ...);
+}
+
+/**
+ * Validate and sanitize strings in command data.
+ * @tparam Tcmd Command this data belongs to.
+ * @param data Command data.
+ * @return Sanitized command data.
+ */
+template <Commands Tcmd>
+CommandDataBuffer SanitizeCmdStrings(const CommandDataBuffer &data)
+{
+ auto args = EndianBufferReader::ToValue<typename CommandTraits<Tcmd>::Args>(data);
+ SanitizeStringsHelper(CommandTraits<Tcmd>::flags, args, std::make_index_sequence<std::tuple_size_v<typename CommandTraits<Tcmd>::Args>>{});
+ return EndianBufferWriter<CommandDataBuffer>::FromValue(args);
+}
+
+template <Commands Tcmd>
+void UnpackNetworkCommand(const CommandPacket *cp)
+{
+ auto args = EndianBufferReader::ToValue<typename CommandTraits<Tcmd>::Args>(cp->data);
+ std::apply(&InjectNetworkCommand, std::tuple_cat(std::make_tuple(Tcmd, cp->err_msg, cp->callback, cp->my_cmd), args));
+}
diff --git a/src/network/network_internal.h b/src/network/network_internal.h
index 25240da5d..58c99867c 100644
--- a/src/network/network_internal.h
+++ b/src/network/network_internal.h
@@ -15,6 +15,8 @@
#include "core/tcp_game.h"
#include "../command_type.h"
+#include "../command_func.h"
+#include "../misc/endian_buffer.hpp"
#ifdef RANDOM_DEBUG
/**
@@ -104,19 +106,26 @@ void UpdateNetworkGameWindow();
/**
* Everything we need to know about a command to be able to execute it.
*/
-struct CommandPacket : CommandContainer {
+struct CommandPacket {
/** Make sure the pointer is nullptr. */
- CommandPacket() : next(nullptr), company(INVALID_COMPANY), frame(0), my_cmd(false) {}
+ CommandPacket() : next(nullptr), company(INVALID_COMPANY), frame(0), my_cmd(false), tile(0) {}
CommandPacket *next; ///< the next command packet (if in queue)
CompanyID company; ///< company that is executing the command
uint32 frame; ///< the frame in which this packet is executed
bool my_cmd; ///< did the command originate from "me"
+
+ Commands cmd; ///< command being executed.
+ StringID err_msg; ///< string ID of error message to use.
+ CommandCallback *callback; ///< any callback function executed upon successful completion of the command.
+ TileIndex tile; ///< location of the command (for e.g. error message or effect display).
+ CommandDataBuffer data; ///< command parameters.
};
void NetworkDistributeCommands();
void NetworkExecuteLocalCommandQueue();
void NetworkFreeLocalCommandQueue();
void NetworkSyncCommandQueue(NetworkClientSocket *cs);
+void NetworkReplaceCommandClientId(CommandPacket &cp, ClientID client_id);
void ShowNetworkError(StringID error_string);
void NetworkTextMessage(NetworkAction action, TextColour colour, bool self_send, const std::string &name, const std::string &str = "", int64 data = 0, const std::string &data_str = "");
diff --git a/src/network/network_server.cpp b/src/network/network_server.cpp
index 50b46dfe1..967ad40a8 100644
--- a/src/network/network_server.cpp
+++ b/src/network/network_server.cpp
@@ -24,6 +24,7 @@
#include "../genworld.h"
#include "../company_func.h"
#include "../company_gui.h"
+#include "../company_cmd.h"
#include "../roadveh.h"
#include "../order_backup.h"
#include "../core/pool_func.hpp"
@@ -1048,14 +1049,15 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMMAND(Packet
* to match the company in the packet. If it doesn't, the client has done
* something pretty naughty (or a bug), and will be kicked
*/
- if (!(cp.cmd == CMD_COMPANY_CTRL && cp.p1 == 0 && ci->client_playas == COMPANY_NEW_COMPANY) && ci->client_playas != cp.company) {
+ uint32 company_p1 = cp.cmd == CMD_COMPANY_CTRL ? std::get<1>(EndianBufferReader::ToValue<CommandTraits<CMD_COMPANY_CTRL>::Args>(cp.data)) : 0;
+ if (!(cp.cmd == CMD_COMPANY_CTRL && company_p1 == 0 && ci->client_playas == COMPANY_NEW_COMPANY) && ci->client_playas != cp.company) {
IConsolePrint(CC_WARNING, "Kicking client #{} (IP: {}) due to calling a command as another company {}.",
ci->client_playas + 1, this->GetClientIP(), cp.company + 1);
return this->SendError(NETWORK_ERROR_COMPANY_MISMATCH);
}
if (cp.cmd == CMD_COMPANY_CTRL) {
- if (cp.p1 != 0 || cp.company != COMPANY_SPECTATOR) {
+ if (company_p1 != 0 || cp.company != COMPANY_SPECTATOR) {
return this->SendError(NETWORK_ERROR_CHEATER);
}
@@ -1066,7 +1068,7 @@ NetworkRecvStatus ServerNetworkGameSocketHandler::Receive_CLIENT_COMMAND(Packet
}
}
- if (GetCommandFlags(cp.cmd) & CMD_CLIENT_ID) cp.p2 = this->client_id;
+ if (GetCommandFlags(cp.cmd) & CMD_CLIENT_ID) NetworkReplaceCommandClientId(cp, this->client_id);
this->incoming_queue.Append(&cp);
return NETWORK_RECV_STATUS_OKAY;
diff --git a/src/string.cpp b/src/string.cpp
index d027cb7bf..aeac4fe84 100644
--- a/src/string.cpp
+++ b/src/string.cpp
@@ -19,6 +19,7 @@
#include <stdarg.h>
#include <ctype.h> /* required for tolower() */
#include <sstream>
+#include <iomanip>
#ifdef _MSC_VER
#include <errno.h> // required by vsnprintf implementation for MSVC
@@ -161,6 +162,23 @@ char *CDECL str_fmt(const char *str, ...)
}
/**
+ * Format a byte array into a continuous hex string.
+ * @param data Array to format
+ * @return Converted string.
+ */
+std::string FormatArrayAsHex(span<const byte> data)
+{
+ std::ostringstream ss;
+ ss << std::uppercase << std::setfill('0') << std::setw(2) << std::hex;
+
+ for (auto b : data) {
+ ss << b;
+ }
+
+ return ss.str();
+}
+
+/**
* Scan the string for old values of SCC_ENCODED and fix it to
* it's new, static value.
* @param str the string to scan
diff --git a/src/string_func.h b/src/string_func.h
index 0cbf26d6b..a5d3499c7 100644
--- a/src/string_func.h
+++ b/src/string_func.h
@@ -28,6 +28,7 @@
#include <iosfwd>
#include "core/bitmath_func.hpp"
+#include "core/span_type.hpp"
#include "string_type.h"
char *strecat(char *dst, const char *src, const char *last) NOACCESS(3);
@@ -39,6 +40,8 @@ int CDECL vseprintf(char *str, const char *last, const char *format, va_list ap)
char *CDECL str_fmt(const char *str, ...) WARN_FORMAT(1, 2);
+std::string FormatArrayAsHex(span<const byte> data);
+
void StrMakeValidInPlace(char *str, const char *last, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK) NOACCESS(2);
[[nodiscard]] std::string StrMakeValid(const std::string &str, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);
void StrMakeValidInPlace(char *str, StringValidationSettings settings = SVS_REPLACE_WITH_QUESTION_MARK);