From 5f5ea132516d8a55aca48c5c40e1198b0486afa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=BCller?= <34514239+appgurueu@users.noreply.github.com> Date: Mon, 22 Sep 2025 18:46:34 +0200 Subject: [PATCH] Network: Batch individual particle packets (#16458) also bumps proto ver --- builtin/game/misc_s.lua | 1 + src/client/client.h | 1 + src/network/clientopcodes.cpp | 1 + src/network/clientpackethandler.cpp | 24 ++++++ src/network/networkprotocol.cpp | 5 +- src/network/networkprotocol.h | 11 ++- src/network/serveropcodes.cpp | 1 + src/server.cpp | 111 ++++++++++++++++------------ src/server.h | 12 ++- 9 files changed, 116 insertions(+), 51 deletions(-) diff --git a/builtin/game/misc_s.lua b/builtin/game/misc_s.lua index 983bd70965..739add9049 100644 --- a/builtin/game/misc_s.lua +++ b/builtin/game/misc_s.lua @@ -131,6 +131,7 @@ core.protocol_versions = { ["5.11.0"] = 47, ["5.12.0"] = 48, ["5.13.0"] = 49, + ["5.14.0"] = 50, } setmetatable(core.protocol_versions, {__newindex = function() diff --git a/src/client/client.h b/src/client/client.h index d08e1e6c89..746db1027d 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -194,6 +194,7 @@ public: void handleCommand_DetachedInventory(NetworkPacket* pkt); void handleCommand_ShowFormSpec(NetworkPacket* pkt); void handleCommand_SpawnParticle(NetworkPacket* pkt); + void handleCommand_SpawnParticleBatch(NetworkPacket *pkt); void handleCommand_AddParticleSpawner(NetworkPacket* pkt); void handleCommand_DeleteParticleSpawner(NetworkPacket* pkt); void handleCommand_HudAdd(NetworkPacket* pkt); diff --git a/src/network/clientopcodes.cpp b/src/network/clientopcodes.cpp index 9a9cb5968c..77f4074cda 100644 --- a/src/network/clientopcodes.cpp +++ b/src/network/clientopcodes.cpp @@ -111,6 +111,7 @@ const ToClientCommandHandler toClientCommandTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_FORMSPEC_PREPEND", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_FormspecPrepend }, // 0x61, { "TOCLIENT_MINIMAP_MODES", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_MinimapModes }, // 0x62, { "TOCLIENT_SET_LIGHTING", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_SetLighting }, // 0x63, + { "TOCLIENT_SPAWN_PARTICLE_BATCH", TOCLIENT_STATE_CONNECTED, &Client::handleCommand_SpawnParticleBatch }, // 0x64, }; const static ServerCommandFactory null_command_factory = { nullptr, 0, false }; diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp index fc6332bd8c..4df8a3ae3b 100644 --- a/src/network/clientpackethandler.cpp +++ b/src/network/clientpackethandler.cpp @@ -34,6 +34,7 @@ #include "skyparams.h" #include "particles.h" #include +#include const char *accessDeniedStrings[SERVER_ACCESSDENIED_MAX] = { N_("Invalid password"), @@ -979,6 +980,29 @@ void Client::handleCommand_SpawnParticle(NetworkPacket* pkt) m_client_event_queue.push(event); } +void Client::handleCommand_SpawnParticleBatch(NetworkPacket *pkt) +{ + std::stringstream particle_batch_data(std::ios::binary | std::ios::in | std::ios::out); + { + std::istringstream compressed(pkt->readLongString(), std::ios::binary); + decompressZstd(compressed, particle_batch_data); + } + + while (particle_batch_data.peek() != EOF) { + auto p = std::make_unique(); + { + std::istringstream particle_data(deSerializeString32(particle_batch_data), std::ios::binary); + p->deSerialize(particle_data, m_proto_ver); + } + + ClientEvent *event = new ClientEvent(); + event->type = CE_SPAWN_PARTICLE; + event->spawn_particle = p.release(); + + m_client_event_queue.push(event); + } +} + void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt) { std::string datastring(pkt->getString(0), pkt->getSize()); diff --git a/src/network/networkprotocol.cpp b/src/network/networkprotocol.cpp index 6f9e1fa40c..c7e2a9827b 100644 --- a/src/network/networkprotocol.cpp +++ b/src/network/networkprotocol.cpp @@ -68,10 +68,13 @@ PROTOCOL VERSION 49 Support of showing a player inventory using 'core.show_formspec' [scheduled bump for 5.13.0] + PROTOCOL VERSION 50 + Support for TOCLIENT_SPAWN_PARTICLE_BATCH + [scheduled bump for 5.14.0] */ // Note: Also update core.protocol_versions in builtin when bumping -const u16 LATEST_PROTOCOL_VERSION = 49; +const u16 LATEST_PROTOCOL_VERSION = 50; // See also formspec [Version History] in doc/lua_api.md const u16 FORMSPEC_API_VERSION = 10; diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h index cd8983b2f2..3f65795e4c 100644 --- a/src/network/networkprotocol.h +++ b/src/network/networkprotocol.h @@ -288,6 +288,8 @@ enum ToClientCommand : u16 TOCLIENT_SPAWN_PARTICLE = 0x46, /* + ParticleParameters params: + using range = RangedParameter { T min, max f32 bias @@ -692,7 +694,14 @@ enum ToClientCommand : u16 f32 center_weight_power */ - TOCLIENT_NUM_MSG_TYPES = 0x64, + TOCLIENT_SPAWN_PARTICLE_BATCH = 0x64, + /* + std::string data, zstd-compressed, for each particle: + u32 len + u8[len] serialized ParticleParameters + */ + + TOCLIENT_NUM_MSG_TYPES = 0x65, }; enum ToServerCommand : u16 diff --git a/src/network/serveropcodes.cpp b/src/network/serveropcodes.cpp index f75e1f5cde..376289508c 100644 --- a/src/network/serveropcodes.cpp +++ b/src/network/serveropcodes.cpp @@ -212,4 +212,5 @@ const ClientCommandFactory clientCommandFactoryTable[TOCLIENT_NUM_MSG_TYPES] = { "TOCLIENT_FORMSPEC_PREPEND", 0, true }, // 0x61 { "TOCLIENT_MINIMAP_MODES", 0, true }, // 0x62 { "TOCLIENT_SET_LIGHTING", 0, true }, // 0x63 + { "TOCLIENT_SPAWN_PARTICLE_BATCH", 0, true }, // 0x64 }; diff --git a/src/server.cpp b/src/server.cpp index 05a4fcc3d5..a4328fb5a9 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -3,9 +3,6 @@ // Copyright (C) 2010-2013 celeron55, Perttu Ahola #include "server.h" -#include -#include -#include #include "irr_v2d.h" #include "network/connection.h" #include "network/networkpacket.h" @@ -65,6 +62,10 @@ #include "gettext.h" #include "util/tracy_wrapper.h" +#include +#include +#include +#include #include class ClientNotFoundException : public BaseException @@ -805,6 +806,12 @@ void Server::AsyncRunStep(float dtime, bool initial_step) } #endif + // Send queued particles + { + EnvAutoLock envlock(this); + SendSpawnParticles(); + } + /* Check added and deleted active objects */ @@ -1603,49 +1610,71 @@ void Server::SendShowFormspecMessage(session_t peer_id, const std::string &forms Send(&pkt); } -// Spawns a particle on peer with peer_id -void Server::SendSpawnParticle(session_t peer_id, u16 protocol_version, - const ParticleParameters &p) +void Server::SendSpawnParticles(RemotePlayer *player, + const std::vector &particles) { static thread_local const float radius = - g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE * BS; + g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE * BS; + const float radius_sq = radius * radius; - if (peer_id == PEER_ID_INEXISTENT) { - std::vector clients = m_clients.getClientIDs(); - const v3f pos = p.pos * BS; - const float radius_sq = radius * radius; - - for (const session_t client_id : clients) { - RemotePlayer *player = m_env->getPlayer(client_id); - if (!player) - continue; - - PlayerSAO *sao = player->getPlayerSAO(); - if (!sao) - continue; - - // Do not send to distant clients - if (sao->getBasePosition().getDistanceFromSQ(pos) > radius_sq) - continue; - - SendSpawnParticle(client_id, player->protocol_version, p); - } + PlayerSAO *sao = player->getPlayerSAO(); + if (!sao) return; - } - assert(protocol_version != 0); - NetworkPacket pkt(TOCLIENT_SPAWN_PARTICLE, 0, peer_id); + std::ostringstream particle_batch_data(std::ios_base::binary); + for (const auto &particle : particles) { + if (sao->getBasePosition().getDistanceFromSQ(particle.pos * BS) > radius_sq) + continue; // out of range - { - // NetworkPacket and iostreams are incompatible... - std::ostringstream oss(std::ios_base::binary); - p.serialize(oss, protocol_version); - pkt.putRawString(oss.str()); + std::ostringstream particle_data(std::ios_base::binary); + particle.serialize(particle_data, player->protocol_version); + std::string particle_data_str = particle_data.str(); + SANITY_CHECK(particle_data_str.size() < U32_MAX); + if (player->protocol_version < 50) { + // Client only supports TOCLIENT_SPAWN_PARTICLE, + // so turn the written particle into a packet immediately + NetworkPacket pkt(TOCLIENT_SPAWN_PARTICLE, particle_data_str.size(), player->getPeerId()); + pkt.putRawString(particle_data_str); + Send(&pkt); + } else { + particle_batch_data << serializeString32(particle_data_str); + } } + if (particle_batch_data.tellp() == 0) + return; // no batch to send + + // Client supports TOCLIENT_SPAWN_PARTICLE_BATCH + assert(player->protocol_version >= 50); + std::ostringstream compressed(std::ios_base::binary); + compressZstd(particle_batch_data.str(), compressed); + + NetworkPacket pkt(TOCLIENT_SPAWN_PARTICLE_BATCH, + 4 + compressed.tellp(), player->getPeerId()); + pkt.putLongString(compressed.str()); Send(&pkt); } +void Server::SendSpawnParticles() +{ + for (const auto &[pname, particles] : m_particles_to_send) { + if (pname.empty()) + continue; // sent to all clients + + RemotePlayer *player = m_env->getPlayer(pname.c_str()); + if (!player) + continue; + + SendSpawnParticles(player, particles); + } + + for (auto *player : m_env->getPlayers()) { + SendSpawnParticles(player, m_particles_to_send[""]); + } + + m_particles_to_send.clear(); +} + void Server::SendAddParticleSpawner(const std::string &to_player, const std::string &exclude_player, const ParticleSpawnerParameters &p, u16 attached_id, u32 id) @@ -3621,17 +3650,7 @@ void Server::spawnParticle(const std::string &playername, if (!m_env) return; - session_t peer_id = PEER_ID_INEXISTENT; - u16 proto_ver = 0; - if (!playername.empty()) { - RemotePlayer *player = m_env->getPlayer(playername.c_str()); - if (!player) - return; - peer_id = player->getPeerId(); - proto_ver = player->protocol_version; - } - - SendSpawnParticle(peer_id, proto_ver, p); + m_particles_to_send[playername].push_back(p); } u32 Server::addParticleSpawner(const ParticleSpawnerParameters &p, diff --git a/src/server.h b/src/server.h index 2389e47d93..cc50f912a6 100644 --- a/src/server.h +++ b/src/server.h @@ -605,9 +605,11 @@ private: void SendDeleteParticleSpawner(session_t peer_id, u32 id); - // Spawns particle on peer with peer_id (PEER_ID_INEXISTENT == all) - void SendSpawnParticle(session_t peer_id, u16 protocol_version, - const ParticleParameters &p); + // Spawn particles for a specific client, batching them if clients support it. + void SendSpawnParticles(RemotePlayer *player, + const std::vector &particles); + // Spawn all particles for this step, batching them if clients support it. + void SendSpawnParticles(); void SendActiveObjectRemoveAdd(RemoteClient *client, PlayerSAO *playersao); void SendActiveObjectMessages(session_t peer_id, const std::string &datas, @@ -805,6 +807,10 @@ private: MetricCounterPtr m_packet_recv_counter; MetricCounterPtr m_packet_recv_processed_counter; MetricCounterPtr m_map_edit_event_counter; + + // Particles to send this server step + // [playername] = list of params, empty playername for broadcast + std::unordered_map> m_particles_to_send; }; /*