diff --git a/skymp5-server/cpp/CMakeLists.txt b/skymp5-server/cpp/CMakeLists.txt index 038111fa32..db09361f7e 100644 --- a/skymp5-server/cpp/CMakeLists.txt +++ b/skymp5-server/cpp/CMakeLists.txt @@ -21,6 +21,19 @@ if(WIN32) add_dependencies(skyrim-platform MpClientPlugin) endif() +# +# messages +# + +file(GLOB_RECURSE src "${CMAKE_CURRENT_SOURCE_DIR}/messages/*") +list(APPEND src "${CMAKE_SOURCE_DIR}/.clang-format") +add_library(messages STATIC ${src}) +target_include_directories(messages PUBLIC "${CMAKE_CURRENT_LIST_DIR}/messages") +target_link_libraries(messages PUBLIC viet) +target_link_libraries(messages PUBLIC papyrus-vm-lib) # Utils.h: stricmp +apply_default_settings(TARGETS messages) +list(APPEND VCPKG_DEPENDENT messages) + # # mp_common # @@ -30,8 +43,7 @@ list(APPEND src "${CMAKE_SOURCE_DIR}/.clang-format") add_library(mp_common STATIC ${src}) target_compile_definitions(mp_common PUBLIC MAX_PLAYERS=1000) target_include_directories(mp_common PUBLIC "${CMAKE_CURRENT_LIST_DIR}/mp_common") -target_include_directories(mp_common PUBLIC "${CMAKE_CURRENT_LIST_DIR}/third_party") -target_link_libraries(mp_common PUBLIC viet) +target_link_libraries(mp_common PUBLIC messages viet) apply_default_settings(TARGETS mp_common) list(APPEND VCPKG_DEPENDENT mp_common) if(WIN32) diff --git a/skymp5-server/cpp/client/main.cpp b/skymp5-server/cpp/client/main.cpp index 2d6a20cd86..e5cbbfb02f 100644 --- a/skymp5-server/cpp/client/main.cpp +++ b/skymp5-server/cpp/client/main.cpp @@ -1,11 +1,43 @@ +#include "MessageSerializerFactory.h" #include "MpClientPlugin.h" #include +#include namespace { MpClientPlugin::State& GetState() { - static MpClientPlugin::State state; - return state; + static MpClientPlugin::State g_state; + return g_state; +} + +MessageSerializer& GetMessageSerializer() +{ + static std::shared_ptr g_serializer = + MessageSerializerFactory::CreateMessageSerializer(); + return *g_serializer; +} + +void MySerializeMessage(const char* jsonContent, + SLNet::BitStream& outputStream) +{ + GetMessageSerializer().Serialize(jsonContent, outputStream); +} + +bool MyDeserializeMessage(const uint8_t* data, size_t length, + std::string& outJsonContent) +{ + std::optional result = + GetMessageSerializer().Deserialize(data, length); + if (!result) { + return false; + } + + // TODO(perf): there should be a faster way to get JS object from binary + // (without extra json building) + nlohmann::json outJson; + result->message->WriteJson(outJson); + outJsonContent = outJson.dump(); + return true; } } @@ -34,28 +66,13 @@ __declspec(dllexport) bool IsConnected() __declspec(dllexport) void Tick(MpClientPlugin::OnPacket onPacket, void* state) { - return MpClientPlugin::Tick(GetState(), onPacket, state); + return MpClientPlugin::Tick(GetState(), onPacket, MyDeserializeMessage, + state); } __declspec(dllexport) void Send(const char* jsonContent, bool reliable) { - return MpClientPlugin::Send(GetState(), jsonContent, reliable); -} - -__declspec(dllexport) bool SKSEPlugin_Query(void* skse, void* info) -{ - struct PluginInfo - { - uint32_t infoVersion = 1; - const char* name = "MpClientPlugin"; - uint32_t version = 1; - }; - new (info) PluginInfo; - return true; -} - -__declspec(dllexport) bool SKSEPlugin_Load(void* skse) -{ - return true; + return MpClientPlugin::Send(GetState(), jsonContent, reliable, + MySerializeMessage); } } diff --git a/skymp5-server/cpp/messages/MessageBase.cpp b/skymp5-server/cpp/messages/MessageBase.cpp new file mode 100644 index 0000000000..9fc7df7534 --- /dev/null +++ b/skymp5-server/cpp/messages/MessageBase.cpp @@ -0,0 +1,3 @@ +#include "MessageBase.h" + +MessageBase::~MessageBase() = default; diff --git a/skymp5-server/cpp/messages/MessageBase.h b/skymp5-server/cpp/messages/MessageBase.h new file mode 100644 index 0000000000..15ca836846 --- /dev/null +++ b/skymp5-server/cpp/messages/MessageBase.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +struct MessageBase +{ + virtual ~MessageBase(); + + virtual void WriteBinary(SLNet::BitStream& stream) const = 0; + virtual void ReadBinary(SLNet::BitStream& stream) = 0; + + virtual void WriteJson(nlohmann::json& json) const = 0; + virtual void ReadJson(const nlohmann::json& json) = 0; +}; diff --git a/skymp5-server/cpp/messages/MessageSerializerFactory.cpp b/skymp5-server/cpp/messages/MessageSerializerFactory.cpp new file mode 100644 index 0000000000..057f22bba9 --- /dev/null +++ b/skymp5-server/cpp/messages/MessageSerializerFactory.cpp @@ -0,0 +1,186 @@ +#include "MessageSerializerFactory.h" +#include "Messages.h" +#include "MinPacketId.h" +#include "MsgType.h" +#include +#include +#include + +namespace { +template +void Serialize(const nlohmann::json& inputJson, SLNet::BitStream& outputStream) +{ + Message message; + message.ReadJson( + inputJson); // may throw. we shouldn't pollute outputStream in this case + + outputStream.Write(static_cast(Networking::MinPacketId)); + outputStream.Write(static_cast(Message::kHeaderByte)); + message.WriteBinary(outputStream); +} + +template +std::optional Deserialize( + const uint8_t* rawMessageJsonOrBinary, size_t length) +{ + if (length >= 2 && rawMessageJsonOrBinary[1] == Message::kHeaderByte) { + // BitStream requires non-const ref even though it doesn't modify it + SLNet::BitStream stream( + const_cast(rawMessageJsonOrBinary) + 2, length - 2, + /*copyData*/ false); + + Message message; + message.ReadBinary(stream); + + DeserializeResult result; + result.msgType = static_cast(Message::kMsgType); + result.message = std::make_unique(std::move(message)); + result.format = DeserializeInputFormat::Binary; + return result; + } + + std::string str(reinterpret_cast(rawMessageJsonOrBinary + 1), + length - 1); + nlohmann::json json = nlohmann::json::parse(str); + + auto msgTypeIt = json.find("t"); + if (msgTypeIt == json.end()) { + // Messages produced by the server use string "type" instead of integer "t" + // We will refactor them out at some point + return std::nullopt; + } + int msgType = msgTypeIt->get(); + + if (msgType != Message::kMsgType) { + // In case of JSON we keep searching in deserializers array + return std::nullopt; + } + + Message message; + message.ReadJson(json); + + DeserializeResult result; + result.msgType = static_cast(msgType); + result.message = std::make_unique(std::move(message)); + result.format = DeserializeInputFormat::Json; + return result; +} +} // namespace + +#define REGISTER_MESSAGE(Message) \ + serializeFns[static_cast(Message::kMsgType)] = Serialize; \ + deserializeFns[static_cast(Message::kHeaderByte)] = \ + Deserialize; + +std::shared_ptr +MessageSerializerFactory::CreateMessageSerializer() +{ + constexpr auto kSerializeFnMax = static_cast(MsgType::Max); + + // A hack to support MovementMessage, the first message that was ported to + // binary back in 2021 MovementMessage uses 'M' char instead of MsgType to + // identify itself in binary encoded packets + constexpr auto kDeserializeFnMax = std::max( + static_cast(MovementMessage::kHeaderByte) + 1, kSerializeFnMax); + + std::vector serializeFns(kSerializeFnMax); + std::vector deserializeFns( + kDeserializeFnMax); + + REGISTER_MESSAGE(MovementMessage) + REGISTER_MESSAGE(UpdateAnimationMessage) + + // make_shared isn't working for private constructors + return std::shared_ptr( + new MessageSerializer(serializeFns, deserializeFns)); +} + +MessageSerializer::MessageSerializer( + std::vector serializerFns_, + std::vector deserializerFns_) + : serializerFns(serializerFns_) + , deserializerFns(deserializerFns_) +{ +} + +void MessageSerializer::Serialize(const char* jsonContent, + SLNet::BitStream& outputStream) +{ + // TODO(perf): consider using simdjson for parsing OR even use JsValue + // directly + + // TODO: logging and write raw instead of throwing exception + const auto parsedJson = nlohmann::json::parse(jsonContent); + + // TODO: logging and write raw instead of throwing exception + const auto msgType = static_cast(parsedJson.at("t").get()); + + auto index = static_cast(msgType); + if (index >= serializerFns.size()) { + // TODO: logging + outputStream.Write(static_cast(Networking::MinPacketId)); + outputStream.Write(jsonContent, strlen(jsonContent)); + return; + } + + auto serializerFn = serializerFns[index]; + if (!serializerFn) { + // TODO: logging + outputStream.Write(static_cast(Networking::MinPacketId)); + outputStream.Write(jsonContent, strlen(jsonContent)); + return; + } + + serializerFn(parsedJson, outputStream); +} + +std::optional MessageSerializer::Deserialize( + const uint8_t* rawMessageJsonOrBinary, size_t length) +{ + if (length < 2) { + spdlog::trace("MessageSerializer::Deserialize - Length < 2"); + return std::nullopt; + } + + auto headerByte = rawMessageJsonOrBinary[1]; + if (headerByte == '{') { + spdlog::trace("MessageSerializer::Deserialize - Encountered JSON message"); + for (auto fn : deserializerFns) { + if (fn) { + auto result = fn(rawMessageJsonOrBinary, length); + if (result) { + spdlog::trace("MessageSerializer::Deserialize - Deserialized"); + return result; + } + } + } + spdlog::trace("MessageSerializer::Deserialize - Failed to deserialize, " + "falling back to PacketParser.cpp"); + return std::nullopt; + } + + if (headerByte >= deserializerFns.size()) { + spdlog::trace( + "MessageSerializer::Deserialize - {} >= deserializerFns.size() ", + static_cast(headerByte)); + return std::nullopt; + } + + auto deserializerFn = deserializerFns[headerByte]; + if (!deserializerFn) { + spdlog::trace("MessageSerializer::Deserialize - deserializerFn not found " + "for headerByte {}", + static_cast(headerByte)); + return std::nullopt; + } + + auto result = deserializerFn(rawMessageJsonOrBinary, length); + if (result == std::nullopt) { + spdlog::trace("MessageSerializer::Deserialize - deserializerFn returned " + "nullopt for headerByte {}", + static_cast(headerByte)); + return std::nullopt; + } + + return result; +} diff --git a/skymp5-server/cpp/messages/MessageSerializerFactory.h b/skymp5-server/cpp/messages/MessageSerializerFactory.h new file mode 100644 index 0000000000..44313b24d8 --- /dev/null +++ b/skymp5-server/cpp/messages/MessageSerializerFactory.h @@ -0,0 +1,55 @@ +#pragma once + +#include "MessageBase.h" +#include "MsgType.h" +#include +#include +#include +#include +#include +#include +#include + +class MessageSerializer; + +class MessageSerializerFactory +{ +public: + static std::shared_ptr CreateMessageSerializer(); +}; + +enum class DeserializeInputFormat +{ + Json, + Binary +}; + +struct DeserializeResult +{ + MsgType msgType = MsgType::Invalid; + std::unique_ptr message; + DeserializeInputFormat format = DeserializeInputFormat::Json; +}; + +class MessageSerializer +{ + friend class MessageSerializerFactory; + +public: + void Serialize(const char* jsonContent, SLNet::BitStream& outputStream); + + std::optional Deserialize( + const uint8_t* rawMessageJsonOrBinary, size_t length); + +private: + typedef void (*SerializeFn)(const nlohmann::json& inputJson, + SLNet::BitStream& outputStream); + typedef std::optional (*DeserializeFn)( + const uint8_t* rawMessageJsonOrBinary, size_t length); + + MessageSerializer(std::vector serializerFns, + std::vector deserializerFns); + + const std::vector serializerFns; + const std::vector deserializerFns; +}; diff --git a/skymp5-server/cpp/messages/Messages.h b/skymp5-server/cpp/messages/Messages.h new file mode 100644 index 0000000000..691b590092 --- /dev/null +++ b/skymp5-server/cpp/messages/Messages.h @@ -0,0 +1,3 @@ +#pragma once +#include "MovementMessage.h" +#include "UpdateAnimationMessage.h" diff --git a/skymp5-server/cpp/messages/MinPacketId.h b/skymp5-server/cpp/messages/MinPacketId.h new file mode 100644 index 0000000000..f808a154ee --- /dev/null +++ b/skymp5-server/cpp/messages/MinPacketId.h @@ -0,0 +1,3 @@ +namespace Networking { +constexpr unsigned char MinPacketId = 134; +} diff --git a/skymp5-server/cpp/messages/MovementMessage.cpp b/skymp5-server/cpp/messages/MovementMessage.cpp new file mode 100644 index 0000000000..04b62640dc --- /dev/null +++ b/skymp5-server/cpp/messages/MovementMessage.cpp @@ -0,0 +1,151 @@ +#include "MovementMessage.h" +#include "SerializationUtil/BitStreamUtil.h" +#include +#include + +namespace { +const inline std::string kStanding = "Standing"; +const inline std::string kWalking = "Walking"; +const inline std::string kRunning = "Running"; +const inline std::string kSprinting = "Sprinting"; +} + +void MovementMessage::WriteBinary(SLNet::BitStream& stream) const +{ + using SerializationUtil::WriteToBitStream; + + WriteToBitStream(stream, idx); + WriteToBitStream(stream, worldOrCell); + WriteToBitStream(stream, pos); + WriteToBitStream(stream, rot); + WriteToBitStream(stream, direction); + WriteToBitStream(stream, healthPercentage); + WriteToBitStream(stream, speed); + + WriteToBitStream(stream, + static_cast(static_cast(runMode) & 2)); + WriteToBitStream(stream, + static_cast(static_cast(runMode) & 1)); + + WriteToBitStream(stream, isInJumpState); + WriteToBitStream(stream, isSneaking); + WriteToBitStream(stream, isBlocking); + WriteToBitStream(stream, isWeapDrawn); + WriteToBitStream(stream, isDead); + WriteToBitStream(stream, lookAt); +} + +void MovementMessage::ReadBinary(SLNet::BitStream& stream) +{ + using SerializationUtil::ReadFromBitStream; + + ReadFromBitStream(stream, idx); + ReadFromBitStream(stream, worldOrCell); + ReadFromBitStream(stream, pos); + ReadFromBitStream(stream, rot); + ReadFromBitStream(stream, direction); + ReadFromBitStream(stream, healthPercentage); + ReadFromBitStream(stream, speed); + + uint8_t tmp = 0; + tmp |= static_cast(ReadFromBitStream(stream)); + tmp <<= 1; + tmp |= static_cast(ReadFromBitStream(stream)); + runMode = static_cast(tmp); + + ReadFromBitStream(stream, isInJumpState); + ReadFromBitStream(stream, isSneaking); + ReadFromBitStream(stream, isBlocking); + ReadFromBitStream(stream, isWeapDrawn); + ReadFromBitStream(stream, isDead); + ReadFromBitStream(stream, lookAt); +} + +void MovementMessage::WriteJson(nlohmann::json& json) const +{ + auto result = nlohmann::json{ + { "t", MsgType::UpdateMovement }, + { "idx", idx }, + { + "data", + { + { "worldOrCell", worldOrCell }, + { "pos", pos }, + { "rot", rot }, + { "runMode", ToString(runMode) }, + { "direction", direction }, + { "healthPercentage", healthPercentage }, + { "speed", speed }, + { "isInJumpState", isInJumpState }, + { "isSneaking", isSneaking }, + { "isBlocking", isBlocking }, + { "isWeapDrawn", isWeapDrawn }, + { "isDead", isDead }, + }, + }, + }; + if (lookAt) { + result["data"]["lookAt"] = *lookAt; + } + json = std::move(result); +} + +void MovementMessage::ReadJson(const nlohmann::json& json) +{ + MovementMessage result; + + result.idx = json.at("idx").get(); + + const auto& data = json.at("data"); + result.worldOrCell = data.at("worldOrCell").get(); + result.pos = data.at("pos").get>(); + result.rot = data.at("rot").get>(); + result.direction = data.at("direction").get(); + result.healthPercentage = data.at("healthPercentage").get(); + result.speed = data.at("speed").get(); + result.runMode = + RunModeFromString(data.at("runMode").get()); + result.isInJumpState = data.at("isInJumpState").get(); + result.isSneaking = data.at("isSneaking").get(); + result.isBlocking = data.at("isBlocking").get(); + result.isWeapDrawn = data.at("isWeapDrawn").get(); + result.isDead = data.at("isDead").get(); + const auto lookAtIt = data.find("lookAt"); + if (lookAtIt != data.end()) { + result.lookAt = lookAtIt->get>(); + } + + *this = std::move(result); +} + +const std::string& ToString(RunMode runMode) +{ + switch (runMode) { + case RunMode::Standing: + return kStanding; + case RunMode::Walking: + return kWalking; + case RunMode::Running: + return kRunning; + case RunMode::Sprinting: + return kSprinting; + default: + throw std::runtime_error("unhandled case for RunMode"); + } +} + +RunMode RunModeFromString(std::string_view str) +{ + if (str == kStanding) { + return RunMode::Standing; + } else if (str == kWalking) { + return RunMode::Walking; + } else if (str == kRunning) { + return RunMode::Running; + } else if (str == kSprinting) { + return RunMode::Sprinting; + } else { + throw std::runtime_error("cannot parse RunMode from " + + std::string{ str }); + } +} diff --git a/skymp5-server/cpp/mp_common/MovementMessage.h b/skymp5-server/cpp/messages/MovementMessage.h similarity index 67% rename from skymp5-server/cpp/mp_common/MovementMessage.h rename to skymp5-server/cpp/messages/MovementMessage.h index da8df50c70..751f438b74 100644 --- a/skymp5-server/cpp/mp_common/MovementMessage.h +++ b/skymp5-server/cpp/messages/MovementMessage.h @@ -1,5 +1,7 @@ #pragma once +#include "MessageBase.h" +#include "MsgType.h" #include #include #include @@ -20,10 +22,16 @@ const std::string& ToString(RunMode runMode); RunMode RunModeFromString(std::string_view str); -struct MovementMessage +struct MovementMessage : public MessageBase { + const static char kMsgType = static_cast(MsgType::UpdateMovement); const static char kHeaderByte = 'M'; + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + uint32_t idx = 0; uint32_t worldOrCell = 0; std::array pos{ 0, 0, 0 }; @@ -40,16 +48,4 @@ struct MovementMessage bool isWeapDrawn = false; bool isDead = false; std::optional> lookAt = std::nullopt; - - auto Tie() const - { - return std::tie(idx, worldOrCell, pos, rot, direction, healthPercentage, - speed, runMode, isInJumpState, isSneaking, isBlocking, - isWeapDrawn, isDead, lookAt); - } - - bool operator==(const MovementMessage& rhs) const - { - return Tie() == rhs.Tie(); - } }; diff --git a/skymp5-server/cpp/messages/MsgType.h b/skymp5-server/cpp/messages/MsgType.h new file mode 100644 index 0000000000..513c4251e0 --- /dev/null +++ b/skymp5-server/cpp/messages/MsgType.h @@ -0,0 +1,28 @@ +#pragma once +#include + +enum class MsgType : uint8_t +{ + Invalid = 0, + CustomPacket = 1, + UpdateMovement = 2, + UpdateAnimation = 3, + UpdateAppearance = 4, + UpdateEquipment = 5, + Activate = 6, + UpdateProperty = 7, + PutItem = 8, + TakeItem = 9, + FinishSpSnippet = 10, + OnEquip = 11, + ConsoleCommand = 12, + CraftItem = 13, + Host = 14, + CustomEvent = 15, + ChangeValues = 16, + OnHit = 17, + DeathStateContainer = 18, + DropItem = 19, + + Max +}; diff --git a/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp new file mode 100644 index 0000000000..c9cb4347a4 --- /dev/null +++ b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.cpp @@ -0,0 +1,22 @@ +#include "BitStreamUtil.h" + +void SerializationUtil::WriteToBitStream(SLNet::BitStream& stream, + const std::string& str) +{ + WriteToBitStream(stream, static_cast(str.size())); + stream.Write(str.data(), static_cast(str.size())); +} + +void SerializationUtil::ReadFromBitStream(SLNet::BitStream& stream, + std::string& str) +{ + uint32_t size = 0; + if (stream.Read(size)) { + std::string tmp; + tmp.resize(size); + stream.Read(tmp.data(), tmp.size()); + str = std::move(tmp); + } else { + str.clear(); + } +} diff --git a/skymp5-server/cpp/mp_common/SerializationUtil/BitStreamUtil.h b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h similarity index 90% rename from skymp5-server/cpp/mp_common/SerializationUtil/BitStreamUtil.h rename to skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h index 94572a6043..bb5e7e066d 100644 --- a/skymp5-server/cpp/mp_common/SerializationUtil/BitStreamUtil.h +++ b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.h @@ -1,7 +1,8 @@ +#pragma once #include #include - #include +#include namespace SerializationUtil { @@ -40,6 +41,10 @@ void WriteToBitStream(SLNet::BitStream& stream, const std::array& arr); template void ReadFromBitStream(SLNet::BitStream& stream, std::array& arr); +void WriteToBitStream(SLNet::BitStream& stream, const std::string& str); + +void ReadFromBitStream(SLNet::BitStream& stream, std::string& str); + template T ReadFromBitStream(SLNet::BitStream& stream); diff --git a/skymp5-server/cpp/mp_common/SerializationUtil/BitStreamUtil.ipp b/skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.ipp similarity index 100% rename from skymp5-server/cpp/mp_common/SerializationUtil/BitStreamUtil.ipp rename to skymp5-server/cpp/messages/SerializationUtil/BitStreamUtil.ipp diff --git a/skymp5-server/cpp/messages/UpdateAnimationMessage.cpp b/skymp5-server/cpp/messages/UpdateAnimationMessage.cpp new file mode 100644 index 0000000000..cfc47d8917 --- /dev/null +++ b/skymp5-server/cpp/messages/UpdateAnimationMessage.cpp @@ -0,0 +1,103 @@ +#include "UpdateAnimationMessage.h" +#include "SerializationUtil/BitStreamUtil.h" +#include "papyrus-vm/Utils.h" // stricmp +#include + +static const char* const kAnimEventNameDictionary[] = { + "jumpStandingStart", + "jumpDirectionalStart", + "JumpFall", + "JumpLandDirectional", + "JumpLand", + "attackStart", + "attackStartLeftHand", + "AttackStartH2HRight", + "AttackStartH2HLeft", + "bowAttackStart", + "attackRelease", + "crossbowAttackStart", + "SneakSprintStartRoll", + "attackStartDualWield", + "attackPowerStartInPlace", + "attackPowerStartBackward", + "attackPowerStartLeft", + "attackPowerStartRight", + "attackPowerStartDualWield", + "attackPowerStartForward", + "attackPowerStart_2HWSprint", + "attackStartSprint", + "attackPowerStart_2HMSprint", + "sprintStart", + "sprintStop", + "blockStart", + "blockStop", + "sneakStart", + "sneakStop" +}; + +void UpdateAnimationMessage::WriteBinary(SLNet::BitStream& stream) const +{ + SerializationUtil::WriteToBitStream(stream, idx); + SerializationUtil::WriteToBitStream(stream, numChanges); + + auto it = + std::find_if(std::begin(kAnimEventNameDictionary), + std::end(kAnimEventNameDictionary), [&](const char* name) { + return Utils::stricmp(name, animEventName.c_str()) == 0; + }); + + if (it == std::end(kAnimEventNameDictionary)) { + SerializationUtil::WriteToBitStream(stream, animEventName); + } else { + SerializationUtil::WriteToBitStream( + stream, static_cast(it - std::begin(kAnimEventNameDictionary))); + } +} + +void UpdateAnimationMessage::ReadBinary(SLNet::BitStream& stream) +{ + SerializationUtil::ReadFromBitStream(stream, idx); + SerializationUtil::ReadFromBitStream(stream, numChanges); + + if (stream.GetNumberOfUnreadBits() == 8) { + uint8_t animEventNameIdx; + SerializationUtil::ReadFromBitStream(stream, animEventNameIdx); + constexpr auto size = + sizeof(kAnimEventNameDictionary) / sizeof(kAnimEventNameDictionary[0]); + if (animEventNameIdx >= size) { + animEventName = ""; + } else { + animEventName = kAnimEventNameDictionary[animEventNameIdx]; + } + } else { + SerializationUtil::ReadFromBitStream(stream, animEventName); + } +} + +void UpdateAnimationMessage::WriteJson(nlohmann::json& json) const +{ + auto result = nlohmann::json{ + { "t", MsgType::UpdateAnimation }, + { "idx", idx }, + { + "data", + { + { "animEventName", animEventName }, + { "numChanges", numChanges }, + }, + }, + }; + json = std::move(result); +} + +void UpdateAnimationMessage::ReadJson(const nlohmann::json& json) +{ + UpdateAnimationMessage result; + result.idx = json.at("idx").get(); + + const auto& data = json.at("data"); + result.numChanges = data.at("numChanges"); + result.animEventName = data.at("animEventName"); + + *this = result; +} diff --git a/skymp5-server/cpp/messages/UpdateAnimationMessage.h b/skymp5-server/cpp/messages/UpdateAnimationMessage.h new file mode 100644 index 0000000000..1b582d087f --- /dev/null +++ b/skymp5-server/cpp/messages/UpdateAnimationMessage.h @@ -0,0 +1,21 @@ +#pragma once + +#include "MessageBase.h" +#include "MsgType.h" +#include +#include + +struct UpdateAnimationMessage : public MessageBase +{ + const static char kMsgType = static_cast(MsgType::UpdateAnimation); + const static char kHeaderByte = static_cast(MsgType::UpdateAnimation); + + void WriteBinary(SLNet::BitStream& stream) const override; + void ReadBinary(SLNet::BitStream& stream) override; + void WriteJson(nlohmann::json& json) const override; + void ReadJson(const nlohmann::json& json) override; + + uint32_t idx = 0; + uint32_t numChanges = 0; + std::string animEventName; +}; diff --git a/skymp5-server/cpp/mp_common/IMessage.h b/skymp5-server/cpp/mp_common/IMessage.h deleted file mode 100644 index 8142f1b2ac..0000000000 --- a/skymp5-server/cpp/mp_common/IMessage.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "Networking.h" -#include "ServerState.h" -#include - -class IMessage -{ -public: - virtual ~IMessage() = default; - virtual void Set(const simdjson::dom::element& msg, - Networking::UserId userId_) = 0; - virtual CachingRequest OnReceive(ServerState& st) = 0; - virtual void OnCacheReady(ServerState& st){}; -}; diff --git a/skymp5-server/cpp/mp_common/MovementMessage.cpp b/skymp5-server/cpp/mp_common/MovementMessage.cpp deleted file mode 100644 index 4fc189011a..0000000000 --- a/skymp5-server/cpp/mp_common/MovementMessage.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include "MovementMessage.h" - -namespace { -const inline std::string kStanding = "Standing"; -const inline std::string kWalking = "Walking"; -const inline std::string kRunning = "Running"; -const inline std::string kSprinting = "Sprinting"; -} - -const std::string& ToString(RunMode runMode) -{ - switch (runMode) { - case RunMode::Standing: - return kStanding; - case RunMode::Walking: - return kWalking; - case RunMode::Running: - return kRunning; - case RunMode::Sprinting: - return kSprinting; - default: - throw std::runtime_error("unhandled case for RunMode"); - } -} - -RunMode RunModeFromString(std::string_view str) -{ - if (str == kStanding) { - return RunMode::Standing; - } else if (str == kWalking) { - return RunMode::Walking; - } else if (str == kRunning) { - return RunMode::Running; - } else if (str == kSprinting) { - return RunMode::Sprinting; - } else { - throw std::runtime_error("cannot parse RunMode from " + - std::string{ str }); - } -} diff --git a/skymp5-server/cpp/mp_common/MovementMessageSerialization.cpp b/skymp5-server/cpp/mp_common/MovementMessageSerialization.cpp deleted file mode 100644 index 25ac671254..0000000000 --- a/skymp5-server/cpp/mp_common/MovementMessageSerialization.cpp +++ /dev/null @@ -1,117 +0,0 @@ -#include "MovementMessageSerialization.h" - -#include -#include - -#include "MsgType.h" -#include "SerializationUtil/BitStreamUtil.h" - -namespace serialization { - -void WriteToBitStream(SLNet::BitStream& stream, const MovementMessage& movData) -{ - using SerializationUtil::WriteToBitStream; - - WriteToBitStream(stream, movData.idx); - WriteToBitStream(stream, movData.worldOrCell); - WriteToBitStream(stream, movData.pos); - WriteToBitStream(stream, movData.rot); - WriteToBitStream(stream, movData.direction); - WriteToBitStream(stream, movData.healthPercentage); - WriteToBitStream(stream, movData.speed); - - WriteToBitStream( - stream, static_cast(static_cast(movData.runMode) & 2)); - WriteToBitStream( - stream, static_cast(static_cast(movData.runMode) & 1)); - - WriteToBitStream(stream, movData.isInJumpState); - WriteToBitStream(stream, movData.isSneaking); - WriteToBitStream(stream, movData.isBlocking); - WriteToBitStream(stream, movData.isWeapDrawn); - WriteToBitStream(stream, movData.isDead); - WriteToBitStream(stream, movData.lookAt); -} - -void ReadFromBitStream(SLNet::BitStream& stream, MovementMessage& movData) -{ - using SerializationUtil::ReadFromBitStream; - - ReadFromBitStream(stream, movData.idx); - ReadFromBitStream(stream, movData.worldOrCell); - ReadFromBitStream(stream, movData.pos); - ReadFromBitStream(stream, movData.rot); - ReadFromBitStream(stream, movData.direction); - ReadFromBitStream(stream, movData.healthPercentage); - ReadFromBitStream(stream, movData.speed); - - uint8_t runMode = 0; - runMode |= static_cast(ReadFromBitStream(stream)); - runMode <<= 1; - runMode |= static_cast(ReadFromBitStream(stream)); - movData.runMode = static_cast(runMode); - - ReadFromBitStream(stream, movData.isInJumpState); - ReadFromBitStream(stream, movData.isSneaking); - ReadFromBitStream(stream, movData.isBlocking); - ReadFromBitStream(stream, movData.isWeapDrawn); - ReadFromBitStream(stream, movData.isDead); - ReadFromBitStream(stream, movData.lookAt); -} - -MovementMessage MovementMessageFromJson(const nlohmann::json& json) -{ - MovementMessage result; - result.idx = json.at("idx").get(); - - const auto& data = json.at("data"); - result.worldOrCell = data.at("worldOrCell").get(); - result.pos = data.at("pos").get>(); - result.rot = data.at("rot").get>(); - result.direction = data.at("direction").get(); - result.healthPercentage = data.at("healthPercentage").get(); - result.speed = data.at("speed").get(); - result.runMode = - RunModeFromString(data.at("runMode").get()); - result.isInJumpState = data.at("isInJumpState").get(); - result.isSneaking = data.at("isSneaking").get(); - result.isBlocking = data.at("isBlocking").get(); - result.isWeapDrawn = data.at("isWeapDrawn").get(); - result.isDead = data.at("isDead").get(); - const auto lookAtIt = data.find("lookAt"); - if (lookAtIt != data.end()) { - result.lookAt = lookAtIt->get>(); - } - return result; -} - -nlohmann::json MovementMessageToJson(const MovementMessage& movData) -{ - auto result = nlohmann::json{ - { "t", MsgType::UpdateMovement }, - { "idx", movData.idx }, - { - "data", - { - { "worldOrCell", movData.worldOrCell }, - { "pos", movData.pos }, - { "rot", movData.rot }, - { "runMode", ToString(movData.runMode) }, - { "direction", movData.direction }, - { "healthPercentage", movData.healthPercentage }, - { "speed", movData.speed }, - { "isInJumpState", movData.isInJumpState }, - { "isSneaking", movData.isSneaking }, - { "isBlocking", movData.isBlocking }, - { "isWeapDrawn", movData.isWeapDrawn }, - { "isDead", movData.isDead }, - }, - }, - }; - if (movData.lookAt) { - result["data"]["lookAt"] = *movData.lookAt; - } - return result; -} - -} diff --git a/skymp5-server/cpp/mp_common/MovementMessageSerialization.h b/skymp5-server/cpp/mp_common/MovementMessageSerialization.h deleted file mode 100644 index 3cd0cf8a65..0000000000 --- a/skymp5-server/cpp/mp_common/MovementMessageSerialization.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -#include "MovementMessage.h" - -namespace serialization { - -void WriteToBitStream(SLNet::BitStream& stream, - const MovementMessage& movData); -void ReadFromBitStream(SLNet::BitStream& stream, MovementMessage& movData); - -MovementMessage MovementMessageFromJson(const nlohmann::json& json); -nlohmann::json MovementMessageToJson(const MovementMessage& movData); - -} diff --git a/skymp5-server/cpp/mp_common/MpClientPlugin.cpp b/skymp5-server/cpp/mp_common/MpClientPlugin.cpp index d174c17422..d470966bac 100644 --- a/skymp5-server/cpp/mp_common/MpClientPlugin.cpp +++ b/skymp5-server/cpp/mp_common/MpClientPlugin.cpp @@ -1,11 +1,13 @@ #include "MpClientPlugin.h" #include "FileUtils.h" +#include "MessageSerializerFactory.h" #include "MovementMessage.h" -#include "MovementMessageSerialization.h" #include "MsgType.h" #include +#include #include +#include #include void MpClientPlugin::CreateClient(State& state, const char* targetHostname, @@ -36,70 +38,50 @@ bool MpClientPlugin::IsConnected(State& state) return state.cl && state.cl->IsConnected(); } -void MpClientPlugin::Tick(State& state, OnPacket onPacket, void* state_) +void MpClientPlugin::Tick(State& state, OnPacket onPacket, + DeserializeMessage deserializeMessageFn, + void* state_) { if (!state.cl) return; - std::pair packetAndState(onPacket, state_); + std::tuple locals( + onPacket, deserializeMessageFn, state_); state.cl->Tick( [](void* rawState, Networking::PacketType packetType, Networking::PacketData data, size_t length, const char* error) { - const auto& [onPacket, state] = - *reinterpret_cast*>(rawState); + const auto& [onPacket, deserializeMessageFn, state] = + *reinterpret_cast*>( + rawState); - std::string jsonContent; + if (packetType != Networking::PacketType::Message) { + return onPacket(static_cast(packetType), "", error, state); + } - if (packetType == Networking::PacketType::Message && length > 1) { - if (data[1] == MovementMessage::kHeaderByte) { - MovementMessage movData; - // BitStream requires non-const ref even though it doesn't modify it - SLNet::BitStream stream(const_cast(data) + 2, - length - 2, /*copyData*/ false); - serialization::ReadFromBitStream(stream, movData); - jsonContent = serialization::MovementMessageToJson(movData).dump(); - } else { - jsonContent = - std::string(reinterpret_cast(data) + 1, length - 1); - } + std::string deserializedJsonContent; + if (deserializeMessageFn(data, length, deserializedJsonContent)) { + return onPacket(static_cast(packetType), + deserializedJsonContent.data(), error, state); } + std::string jsonContent = + std::string(reinterpret_cast(data) + 1, length - 1); onPacket(static_cast(packetType), jsonContent.data(), error, state); }, - &packetAndState); + &locals); } -void MpClientPlugin::Send(State& state, const char* jsonContent, bool reliable) +void MpClientPlugin::Send(State& state, const char* jsonContent, bool reliable, + SerializeMessage serializeMessageFn) { if (!state.cl) { // TODO(#263): we probably should log something here return; } - const auto parsedJson = nlohmann::json::parse(jsonContent); - if (static_cast(parsedJson.at("t").get()) == - MsgType::UpdateMovement) { - const auto movData = serialization::MovementMessageFromJson(parsedJson); - SLNet::BitStream stream; - serialization::WriteToBitStream(stream, movData); - - std::vector buf(stream.GetNumberOfBytesUsed() + 2); - buf[0] = Networking::MinPacketId; - buf[1] = MovementMessage::kHeaderByte; - std::copy(stream.GetData(), - stream.GetData() + stream.GetNumberOfBytesUsed(), - buf.begin() + 2); - state.cl->Send(buf.data(), buf.size(), reliable); - - return; - } - - auto n = strlen(jsonContent); - std::vector buf(n + 1); - buf[0] = Networking::MinPacketId; - memcpy(buf.data() + 1, jsonContent, n); - - state.cl->Send(buf.data(), buf.size(), reliable); + SLNet::BitStream stream; + serializeMessageFn(jsonContent, stream); + state.cl->Send(stream.GetData(), stream.GetNumberOfBytesUsed(), reliable); } diff --git a/skymp5-server/cpp/mp_common/MpClientPlugin.h b/skymp5-server/cpp/mp_common/MpClientPlugin.h index 3b36fbb861..e1818f20da 100644 --- a/skymp5-server/cpp/mp_common/MpClientPlugin.h +++ b/skymp5-server/cpp/mp_common/MpClientPlugin.h @@ -1,6 +1,7 @@ #pragma once #include "Networking.h" #include +#include namespace MpClientPlugin { typedef void (*OnPacket)(int32_t type, const char* jsonContent, @@ -11,9 +12,16 @@ struct State std::shared_ptr cl; }; +typedef void (*SerializeMessage)(const char* jsonContent, + SLNet::BitStream& outputBuffer); +typedef bool (*DeserializeMessage)(const uint8_t* data, size_t length, + std::string& outJsonContent); + void CreateClient(State& st, const char* targetHostname, uint16_t targetPort); void DestroyClient(State& st); bool IsConnected(State& st); -void Tick(State& st, OnPacket onPacket, void* state_); -void Send(State& st, const char* jsonContent, bool reliable); +void Tick(State& st, OnPacket onPacket, + DeserializeMessage deserializeMessageFn, void* state_); +void Send(State& st, const char* jsonContent, bool reliable, + SerializeMessage serializeMessageFn); }; diff --git a/skymp5-server/cpp/mp_common/MsgType.h b/skymp5-server/cpp/mp_common/MsgType.h index 91c5111d80..d9f162f5ce 100644 --- a/skymp5-server/cpp/mp_common/MsgType.h +++ b/skymp5-server/cpp/mp_common/MsgType.h @@ -1,26 +1,2 @@ #pragma once -#include - -enum class MsgType : int64_t -{ - Invalid = 0, - CustomPacket = 1, - UpdateMovement = 2, - UpdateAnimation = 3, - UpdateAppearance = 4, - UpdateEquipment = 5, - Activate = 6, - UpdateProperty = 7, - PutItem = 8, - TakeItem = 9, - FinishSpSnippet = 10, - OnEquip = 11, - ConsoleCommand = 12, - CraftItem = 13, - Host = 14, - CustomEvent = 15, - ChangeValues = 16, - OnHit = 17, - DeathStateContainer = 18, - DropItem = 19 -}; +#include "../messages/MsgType.h" diff --git a/skymp5-server/cpp/mp_common/NetworkingInterface.h b/skymp5-server/cpp/mp_common/NetworkingInterface.h index 569ca5e75d..bc2dd4488e 100644 --- a/skymp5-server/cpp/mp_common/NetworkingInterface.h +++ b/skymp5-server/cpp/mp_common/NetworkingInterface.h @@ -1,20 +1,16 @@ #pragma once +#include "MinPacketId.h" #include #include #include #include namespace Networking { + using UserId = unsigned short; -enum : UserId -{ - InvalidUserId = (UserId)~0 -}; -enum : unsigned char -{ - MinPacketId = 134 -}; +constexpr UserId InvalidUserId = static_cast(~0); + // First byte must represent id of the packet (>= MinPacketId) using PacketData = const unsigned char*; diff --git a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp index e95ff7a6d6..ab6909fb4b 100644 --- a/skymp5-server/cpp/server_guest_lib/ActionListener.cpp +++ b/skymp5-server/cpp/server_guest_lib/ActionListener.cpp @@ -42,6 +42,10 @@ MpActor* ActionListener::SendToNeighbours( auto it = partOne.worldState.hosters.find(actor->GetFormId()); if (it == partOne.worldState.hosters.end() || it->second != myActor->GetFormId()) { + if (idx == 0) { + spdlog::warn("SendToNeighbours - idx=0, ::ReadJson or " + "similar is probably incorrect"); + } spdlog::error("SendToNeighbours - No permission to update actor {:x}", actor->GetFormId()); return nullptr; @@ -834,8 +838,8 @@ void ActionListener::OnHit(const RawMessageData& rawMsgData_, healthPercentage); } -void ActionListener::OnUnknown(const RawMessageData& rawMsgData, - simdjson::dom::element data) +void ActionListener::OnUnknown(const RawMessageData& rawMsgData) { - spdlog::debug("Got unhandled message: {}", simdjson::minify(data)); + spdlog::error("Got unhandled message: {}", + simdjson::minify(rawMsgData.parsed)); } diff --git a/skymp5-server/cpp/server_guest_lib/ActionListener.h b/skymp5-server/cpp/server_guest_lib/ActionListener.h index 352123b032..83db32cc84 100644 --- a/skymp5-server/cpp/server_guest_lib/ActionListener.h +++ b/skymp5-server/cpp/server_guest_lib/ActionListener.h @@ -85,8 +85,7 @@ class ActionListener virtual void OnHit(const RawMessageData& rawMsgData, const HitData& hitData); - virtual void OnUnknown(const RawMessageData& rawMsgData, - simdjson::dom::element data); + virtual void OnUnknown(const RawMessageData& rawMsgData); private: // Returns user's actor if there is attached one diff --git a/skymp5-server/cpp/server_guest_lib/PacketParser.cpp b/skymp5-server/cpp/server_guest_lib/PacketParser.cpp index 5adb62debe..a74ffdeea3 100644 --- a/skymp5-server/cpp/server_guest_lib/PacketParser.cpp +++ b/skymp5-server/cpp/server_guest_lib/PacketParser.cpp @@ -3,10 +3,10 @@ #include "Exceptions.h" #include "HitData.h" #include "JsonUtils.h" -#include "MovementMessage.h" -#include "MovementMessageSerialization.h" +#include "MessageSerializerFactory.h" +#include "Messages.h" #include "MpActor.h" -#include +#include "MsgType.h" #include #include @@ -34,11 +34,14 @@ static const JsonPointer t("t"), idx("idx"), content("content"), data("data"), struct PacketParser::Impl { simdjson::dom::parser simdjsonParser; + std::shared_ptr serializer; + std::once_flag jsonWarning; }; PacketParser::PacketParser() { pImpl.reset(new Impl); + pImpl->serializer = MessageSerializerFactory::CreateMessageSerializer(); } void PacketParser::TransformPacketIntoAction(Networking::UserId userId, @@ -57,19 +60,43 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, userId, }; - if (length > 1 && data[1] == MovementMessage::kHeaderByte) { - MovementMessage movData; - // BitStream requires non-const ref even though it doesn't modify it - SLNet::BitStream stream(const_cast(data) + 2, length - 2, - /*copyData*/ false); - serialization::ReadFromBitStream(stream, movData); - - actionListener.OnUpdateMovement( - rawMsgData, movData.idx, - { movData.pos[0], movData.pos[1], movData.pos[2] }, - { movData.rot[0], movData.rot[1], movData.rot[2] }, - movData.isInJumpState, movData.isWeapDrawn, movData.isBlocking, - movData.worldOrCell); + auto result = pImpl->serializer->Deserialize(data, length); + if (result != std::nullopt) { + if (result->format == DeserializeInputFormat::Json) { + std::call_once(pImpl->jsonWarning, [&] { + spdlog::warn("PacketParser::TransformPacketIntoAction - 1-st time " + "encountered a JSON packet, userId={}, msgType={}", + userId, static_cast(result->msgType)); + }); + } + switch (result->msgType) { + case MsgType::UpdateMovement: { + auto message = + reinterpret_cast(result->message.get()); + actionListener.OnUpdateMovement( + rawMsgData, message->idx, + { message->pos[0], message->pos[1], message->pos[2] }, + { message->rot[0], message->rot[1], message->rot[2] }, + message->isInJumpState, message->isWeapDrawn, message->isBlocking, + message->worldOrCell); + break; + } + case MsgType::UpdateAnimation: { + auto message = + reinterpret_cast(result->message.get()); + AnimationData animationData; + animationData.animEventName = message->animEventName.data(); + animationData.numChanges = message->numChanges; + actionListener.OnUpdateAnimation(rawMsgData, message->idx, + animationData); + break; + } + default: { + spdlog::error("Unhandled MsgType {} after Deserialize", + static_cast(result->msgType)); + break; + } + } return; } @@ -78,11 +105,10 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, const auto& jMessage = rawMsgData.parsed; - using TypeInt = std::underlying_type::type; - auto type = MsgType::Invalid; - Read(jMessage, JsonPointers::t, reinterpret_cast(&type)); + int64_t type = static_cast(MsgType::Invalid); + Read(jMessage, JsonPointers::t, &type); - switch (type) { + switch (static_cast(type)) { case MsgType::Invalid: break; case MsgType::CustomPacket: { @@ -90,51 +116,6 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, Read(jMessage, JsonPointers::content, &content); actionListener.OnCustomPacket(rawMsgData, content); } break; - case MsgType::UpdateMovement: { - uint32_t idx; - ReadEx(jMessage, JsonPointers::idx, &idx); - - simdjson::dom::element data_; - Read(jMessage, JsonPointers::data, &data_); - - simdjson::dom::element jPos; - Read(data_, JsonPointers::pos, &jPos); - float pos[3]; - for (int i = 0; i < 3; ++i) - ReadEx(jPos, i, &pos[i]); - - simdjson::dom::element jRot; - Read(data_, JsonPointers::rot, &jRot); - float rot[3]; - for (int i = 0; i < 3; ++i) - ReadEx(jRot, i, &rot[i]); - - bool isInJumpState = false; - Read(data_, JsonPointers::isInJumpState, &isInJumpState); - - bool isWeapDrawn = false; - Read(data_, JsonPointers::isWeapDrawn, &isWeapDrawn); - - bool isBlocking = false; - Read(data_, JsonPointers::isBlocking, &isBlocking); - - uint32_t worldOrCell = 0; - ReadEx(data_, JsonPointers::worldOrCell, &worldOrCell); - - actionListener.OnUpdateMovement( - rawMsgData, idx, { pos[0], pos[1], pos[2] }, - { rot[0], rot[1], rot[2] }, isInJumpState, isWeapDrawn, isBlocking, - worldOrCell); - - } break; - case MsgType::UpdateAnimation: { - uint32_t idx; - ReadEx(jMessage, JsonPointers::idx, &idx); - simdjson::dom::element jData; - ReadEx(jMessage, JsonPointers::data, &jData); - actionListener.OnUpdateAnimation(rawMsgData, idx, - AnimationData::FromJson(jData)); - } break; case MsgType::UpdateAppearance: { uint32_t idx; ReadEx(jMessage, JsonPointers::idx, &idx); @@ -200,7 +181,7 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, uint32_t target; ReadEx(jMessage, JsonPointers::target, &target); auto e = Inventory::Entry::FromJson(jMessage); - if (type == MsgType::PutItem) { + if (static_cast(type) == MsgType::PutItem) { e.extra.worn = Inventory::Worn::None; actionListener.OnPutItem(rawMsgData, target, e); } else { @@ -308,9 +289,7 @@ void PacketParser::TransformPacketIntoAction(Networking::UserId userId, break; } default: - simdjson::dom::element data_; - ReadEx(jMessage, JsonPointers::data, &data_); - actionListener.OnUnknown(rawMsgData, data_); + actionListener.OnUnknown(rawMsgData); break; } } diff --git a/skymp5-server/ts/systems/masterClient.ts b/skymp5-server/ts/systems/masterClient.ts index f762dff34f..af8784d7c4 100644 --- a/skymp5-server/ts/systems/masterClient.ts +++ b/skymp5-server/ts/systems/masterClient.ts @@ -58,20 +58,7 @@ export class MasterClient implements System { // connect/disconnect events are not reliable so we do full recalculate private getCurrentOnline(svr: ScampServer): number { - let online = 0; - for (let i = 0; i < this.maxPlayers; ++i) { - try { - if (svr.getUserActor(i) != 0) { - ++online; - } - } catch (e) { - const error: Error = e; - if (!error.message.includes(`User with id ${i} doesn't exist`)) { - throw e; - } - } - } - return online; + return (svr as any).get(0, "onlinePlayers").length; } customPacket(): void { diff --git a/unit/MovementSerializationTest.cpp b/unit/MovementSerializationTest.cpp index 3889d0bba6..caeb349d30 100644 --- a/unit/MovementSerializationTest.cpp +++ b/unit/MovementSerializationTest.cpp @@ -4,28 +4,27 @@ #include #include "MovementMessage.h" -#include "MovementMessageSerialization.h" namespace { MovementMessage MakeTestMovementMessage(RunMode runMode, bool hasLookAt) { - MovementMessage result{ - 1337, // idx - 0x2077, // worldOrCell - { 0.25, -100, 0 }, // pos - { 123, 0, 45 }, // rot - 270, // direction - 0.5, // healthPercentage - 400, // speed - RunMode::Running, - true, // isInJumpState - true, // isSneaking - false, // isBlocking - true, // isWeapDrawn - false, // isDead - { { 1, 2, 3 } }, // lookAt - }; + MovementMessage result; + result.idx = 1337; + result.worldOrCell = 0x2077; + result.pos = { 0.25, -100, 0 }; + result.rot = { 123, 0, 45 }; + result.direction = 270; + result.healthPercentage = 0.5; + result.speed = 400; + result.runMode = RunMode::Running; + result.isInJumpState = true; + result.isSneaking = true; + result.isBlocking = false; + result.isWeapDrawn = true; + result.isDead = false; + result.lookAt = { { 1, 2, 3 } }; + result.runMode = runMode; if (!hasLookAt) { result.lookAt = std::nullopt; @@ -54,10 +53,21 @@ TEST_CASE("MovementMessage correctly encoded and decoded to JSON", for (const auto& [name, movData] : MakeTestMovementMessageCases()) { SECTION(name) { - const auto json = serialization::MovementMessageToJson(movData); - const auto movData2 = serialization::MovementMessageFromJson(json); + nlohmann::json json; + movData.WriteJson(json); + + MovementMessage movData2; + movData2.ReadJson(json); + + nlohmann::json json2; + movData2.WriteJson(json2); + INFO(json.dump()); - REQUIRE(movData == movData2); + REQUIRE(json == json2); + REQUIRE(json["t"].get() == + static_cast(MovementMessage::kMsgType)); + REQUIRE(json2["t"].get() == + static_cast(MovementMessage::kMsgType)); } } } @@ -69,12 +79,17 @@ TEST_CASE("MovementMessage correctly encoded and decoded to BitStream", SECTION(name) { SLNet::BitStream stream; - serialization::WriteToBitStream(stream, movData); + movData.WriteBinary(stream); MovementMessage movData2; - serialization::ReadFromBitStream(stream, movData2); + movData2.ReadBinary(stream); + + SLNet::BitStream stream2; + movData2.WriteBinary(stream2); - REQUIRE(movData == movData2); + REQUIRE(stream.GetNumberOfBytesUsed() == stream2.GetNumberOfBytesUsed()); + REQUIRE(memcmp(stream.GetData(), stream2.GetData(), + stream.GetNumberOfBytesUsed()) == 0); } } } diff --git a/unit/TestUtils.hpp b/unit/TestUtils.hpp index ed7d56416f..77d65faf48 100644 --- a/unit/TestUtils.hpp +++ b/unit/TestUtils.hpp @@ -44,9 +44,12 @@ static const auto jMovement = { "rot", { 0, 0, 179 } }, { "runMode", "Standing" }, { "direction", 0 }, + { "healthPercentage", 1 }, + { "speed", 0 }, { "isInJumpState", false }, { "isSneaking", false }, { "isBlocking", false }, + { "isDead", false }, { "isWeapDrawn", false } } } }; static const auto jAppearance = nlohmann::json{