diff --git a/TODO.md b/TODO.md index d56534f..95eab38 100644 --- a/TODO.md +++ b/TODO.md @@ -20,30 +20,30 @@ ## Hello world - [~] Map stream - - [ ] Modifications compression - [ ] Local prediction - [ ] Contouring service - [~] Edit + - [ ] Compression (raw) - [x] Shape iterators - [ ] Anchor - [x] Prevent suffocation - - [ ] Local prediction + - [x] Local prediction - [~] Occlusion Culling - - [ ] Iterator ray + - [~] Iterator ray - [ ] Cast from chunk center - [x] Transparency - [~] Entities - [ ] Collide - [ ] Get models - [ ] Reduce compile unit count - - [ ] Review documentation + - [~] Review documentation - [ ] Clean kick ## Hello universe - - [ ] CI build - - CMake package - - GitLab / Drone pipeline + - [~] CI build + - [ ] CMake package + - [x] GitLab CI - [ ] Universe - [ ] Galaxy - [ ] Rotation @@ -79,7 +79,7 @@ - [ ] Slash screen - [ ] Start/Pause menu - [x] QUIC protocol - - [ ] 8 for 1 contouring problem + - [~] 8 for 1 contouring problem - [ ] Use in memory protocol (to replace server_handle) - [ ] Octree - [ ] Better Lod diff --git a/src/client/config.hpp b/src/client/config.hpp index c987542..8443f74 100644 --- a/src/client/config.hpp +++ b/src/client/config.hpp @@ -66,6 +66,8 @@ public: world.keepDistance = config["world"]["keep_distance"].value_or(world.keepDistance); world.useAverages = config["world"]["use_averages"].value_or(world.useAverages); world.trustMajorant = config["world"]["trust_majorant"].value_or(world.trustMajorant); + world.editPrediction = config["world"]["edit_prediction"].value_or(world.editPrediction); + world.editHandling = config["world"]["edit_handling"].value_or(world.editHandling); voxel_density = config["world"]["voxel_density"].value_or(voxel_density); #ifndef STANDALONE @@ -144,6 +146,8 @@ public: {"keep_distance", world.keepDistance}, {"use_averages", world.useAverages}, {"trust_majorant", world.trustMajorant}, + {"edit_prediction", world.editPrediction}, + {"edit_handling", world.editHandling}, {"voxel_density", voxel_density} })); if(connection.has_value()) { diff --git a/src/client/world/Chunk.hpp b/src/client/world/Chunk.hpp index 92209a8..a847fc2 100644 --- a/src/client/world/Chunk.hpp +++ b/src/client/world/Chunk.hpp @@ -4,12 +4,48 @@ namespace world::client { - class Chunk final: public EdittableChunk { - public: - Chunk(): world::Chunk(), EdittableChunk() { } - }; +class Chunk final: public EdittableChunk { +public: + Chunk(std::istream &is): world::Chunk(is) { } + /// Create from average + Chunk(Voxel val): EdittableChunk(), isAverage(true), isMajorant(val.swap()) { voxels.fill(Voxel(val.material(), val.density())); } + Chunk(): world::Chunk(), EdittableChunk() { } - /// Chunk full of air - static const std::shared_ptr EMPTY_CHUNK = std::make_shared(); + std::optional update(float deltaTime, bool animate) override { + for (auto it = futureEdits.begin(); it != futureEdits.end();) { + it->second -= deltaTime; + if (it->second <= 0 && animate) { + invalidate(it->first.idx); + edits.emplace_back(it->first); + it = futureEdits.erase(it); + } else { + it++; + } + } + return EdittableChunk::update(deltaTime, animate); + } + + constexpr bool isTrusted(bool allowMajorant) const { return !isAverage || (allowMajorant && isMajorant); } + void unsetMajorant() { + assert(isMajorant); + isMajorant = false; + } + + /// Add future edit + void addFutureEdit(Chunk::Edit edit, float delay) { futureEdits.emplace_back(edit, delay); } + +protected: + /// Is temporary average + const bool isAverage = false; + /// Is temporary full valued + bool isMajorant = false; + + /// Temporary animated changes + /// MAYBE: sort by delay + std::vector> futureEdits; +}; + +/// Chunk full of air +static const std::shared_ptr EMPTY_CHUNK = std::make_shared(); } \ No newline at end of file diff --git a/src/client/world/DistantUniverse.cpp b/src/client/world/DistantUniverse.cpp index 36dda93..d838de6 100644 --- a/src/client/world/DistantUniverse.cpp +++ b/src/client/world/DistantUniverse.cpp @@ -5,6 +5,7 @@ #include "Area.hpp" #include "../contouring/Abstract.hpp" #include "../../core/world/raycast.hpp" +#include "../../core/world/iterators.hpp" #include "../../core/net/io.hpp" #include "../../core/utils/logger.hpp" #include "Chunk.hpp" @@ -40,7 +41,7 @@ void DistantUniverse::update(voxel_pos pos, float deltaTime) { if (glm::length2(diff - it_c->first) > glm::pow2(options.keepDistance)) { it_c = chunks.erase(it_c); } else { - if(const auto neighbors = std::dynamic_pointer_cast(it_c->second)->update(deltaTime, true /*MAYBE: random update*/)) { + if(const auto neighbors = std::dynamic_pointer_cast(it_c->second)->update(deltaTime, true /*MAYBE: random update*/)) { contouring->onUpdate(std::make_pair(area.first, it_c->first), diff, chunks, neighbors.value()); } ++it_c; @@ -63,7 +64,7 @@ void DistantUniverse::update(voxel_pos pos, float deltaTime) { const auto p = diff + chunk_pos(x, y, z); if (dist2 <= queryDistance * queryDistance && chunks.inRange(p)) { if (auto it = chunks.find(p); it != chunks.end() && - std::dynamic_pointer_cast(it->second)->isTrusted(options.trustMajorant)) + std::dynamic_pointer_cast(it->second)->isTrusted(options.trustMajorant)) //TODO: config accept perfect average { contouring->onNotify(std::make_pair(area.first, p), diff, chunks); @@ -73,7 +74,7 @@ void DistantUniverse::update(voxel_pos pos, float deltaTime) { const auto rcPos = glm::split(p); if(auto it_r = cl_area->regionCache.find(rcPos.first); it_r != cl_area->regionCache.end()) { if (auto it_rc = it_r->second.find(rcPos.second); it_rc != it_r->second.end() && (it_rc->second.swap() || options.useAverages)) { - auto ck = std::make_shared(it_rc->second); + auto ck = std::make_shared(it_rc->second); ck->invalidate(geometry::Faces::All); chunks.emplace(p, std::dynamic_pointer_cast(ck)); contouring->onNotify(std::make_pair(area.first, p), diff, chunks); @@ -111,7 +112,6 @@ void DistantUniverse::update(voxel_pos pos, float deltaTime) { packet.write(area.first); packet.write(missingChunks.data(), missingChunks.size() * sizeof(chunk_pos)); peer.send(packet.finish()); - LOG("Query " << missingChunks.size()); } if(!missingRegions.empty()) { auto packet = net::PacketWriter(net::client_packet_type::MISSING_REGIONS, sizeof(area_id) + missingRegions.size() * sizeof(region_pos)); @@ -151,8 +151,19 @@ bool DistantUniverse::onPacket(const data::out_view& buf, net::PacketFlags) { break; } - case server_packet_type::CAPABILITIES: - return packet.read(serverDistance); + case server_packet_type::CAPABILITIES: { + packet.read(serverDistance); + auto predictable = false; + packet.read(predictable); + if (!predictable) { + options.editHandling = false; + options.editPrediction = false; + } + auto packet = PacketWriter(client_packet_type::CAPABILITIES, sizeof(bool)); + packet.write(options.editHandling); + peer.send(packet.finish()); + break; + } case server_packet_type::COMPRESSION: { const auto remain = packet.readAll(); @@ -222,7 +233,6 @@ bool DistantUniverse::onPacket(const data::out_view& buf, net::PacketFlags) { if (voxel.swap()) full++; } - LOG(full << "/" << total); break; } @@ -257,7 +267,7 @@ bool DistantUniverse::onPacket(const data::out_view& buf, net::PacketFlags) { } data::vec_istream idata(buffer); std::istream iss(&idata); - auto ck = std::make_shared(iss); + auto ck = std::make_shared(iss); ck->invalidate(geometry::Faces::All); auto ptr = std::dynamic_pointer_cast(ck); auto &chunks = it->second->setChunks(); @@ -271,6 +281,38 @@ bool DistantUniverse::onPacket(const data::out_view& buf, net::PacketFlags) { case server_packet_type::EDITS: { ZoneScopedN("Edits"); + if (!options.editHandling) { + LOG_W("Unhandled edit type"); + break; + } + + const auto fill = packet.read(); + if (!fill) + break; + + if(const auto it = areas.find(fill->pos.first); it != areas.end()) { + auto &chunks = it->second->setChunks(); + auto iterator = world::iterator::Get(fill->shape, fill->radius); + world::iterator::pair point; + while (iterator->next(point)) { + const voxel_pos offset = point.first; + const auto split = glm::splitIdx(fill->pos.second + offset); + if(chunks.inRange(split.first)) { + if(const auto chunk = it->second->setChunks().findInRange(split.first)) { + auto ck = std::dynamic_pointer_cast(chunk.value()); + auto prev = ck->get(split.second); + const auto next = prev.filled(fill->val, point.second); + const auto delay = glm::length2(offset) / fill->radius * .05f; + ck->apply(Chunk::Edit{split.second, next, delay}); + } + } + } + } + + break; + } + case server_packet_type::RAW_EDITS: { + ZoneScopedN("Raw Edits"); area_id id; if(!packet.read(id)) break; @@ -287,7 +329,7 @@ bool DistantUniverse::onPacket(const data::out_view& buf, net::PacketFlags) { chunk_voxel_idx count = 0; packet.read(count); if (auto ptr = it->second->setChunks().findInRange(pos)) { - auto chunk = std::dynamic_pointer_cast(ptr.value()); + auto chunk = std::dynamic_pointer_cast(ptr.value()); for (auto i = 0; i < count && !packet.isFull(); i++) { Chunk::Edit edit; packet.read(edit); @@ -351,6 +393,30 @@ void DistantUniverse::emit(const action::packet &action) { peer.send(net::PacketWriter::Of(net::client_packet_type::MESSAGE, message->text.data(), message->text.size())); } else if(const auto fill = std::get_if(&action)) { peer.send(net::PacketWriter::Of(net::client_packet_type::FILL_SHAPE, *fill)); + if (options.editPrediction) { + ZoneScopedN("Fill"); + const auto keepDelay = 10 + (peer.getRTT() / 20000.f); // 10s + 50RTT + if(const auto it = areas.find(fill->pos.first); it != areas.end()) { + auto &chunks = it->second->setChunks(); + auto iterator = world::iterator::Get(fill->shape, fill->radius); + world::iterator::pair point; + while (iterator->next(point)) { + const voxel_pos offset = point.first; + const auto split = glm::splitIdx(fill->pos.second + offset); + if(chunks.inRange(split.first)) { + if(const auto chunk = it->second->setChunks().findInRange(split.first)) { + auto ck = std::dynamic_pointer_cast(chunk.value()); + auto prev = ck->get(split.second); + const auto next = prev.filled(fill->val, point.second); + if(prev.value != next.value) { + const auto delay = glm::length2(offset) / fill->radius * .05f; + ck->addFutureEdit(Chunk::Edit{split.second, next, keepDelay - delay * 2}, delay); + } + } + } + } + } + } } else { LOG_W("Bad action " << action.index()); } diff --git a/src/client/world/Universe.hpp b/src/client/world/Universe.hpp index 8990df5..b515958 100644 --- a/src/client/world/Universe.hpp +++ b/src/client/world/Universe.hpp @@ -25,6 +25,10 @@ namespace world::client { /// Dont query chunks with absolute majorant average /// Avoid querying whole space but may cause missed edits in those chunks bool trustMajorant = true; + /// Apply edits locally while sending + bool editPrediction = true; + /// Get only edits commands and compute locally + bool editHandling = true; }; struct connection: net::address { connection(): net::address{"localhost", 4242} { } diff --git a/src/core/net/Context.cpp b/src/core/net/Context.cpp index 2f9d156..4d94587 100644 --- a/src/core/net/Context.cpp +++ b/src/core/net/Context.cpp @@ -321,6 +321,9 @@ void Connection::release(uint16_t reason) { } } +uint64_t Connection::getRTT() { + return cnx != nullptr ? picoquic_get_rtt(cnx) : INT64_MAX; +} uint16_t Connection::getErrorCode(bool is_app) { return cnx != nullptr ? (is_app ? cnx->remote_application_error : cnx->remote_error) : 42; } diff --git a/src/core/net/Context.hpp b/src/core/net/Context.hpp index 4bfc63f..f581ad6 100644 --- a/src/core/net/Context.hpp +++ b/src/core/net/Context.hpp @@ -103,7 +103,10 @@ public: void setCallback(picoquic_stream_data_cb_fn cb_fn, void *ctx); + /// Average roundtrip time in microseconds + uint64_t getRTT(); uint16_t getErrorCode(bool is_app); + /// Ip address and port std::string getAddress(); /// Send reliable data diff --git a/src/core/net/data.hpp b/src/core/net/data.hpp index 1592693..7740990 100644 --- a/src/core/net/data.hpp +++ b/src/core/net/data.hpp @@ -45,30 +45,33 @@ enum class server_packet_type: uint8_t { /// {area_, zstd} /// empty: all sent CHUNK = 18, - /// Chunk changes - /// {area_id, {chunk_pos, ushort(count), Chunk::Edit[]}[]} notify - /// MAYBE: compress - EDITS = 19, + /// Uncompressed chunk changes + /// {area_id, {chunk_pos, ushort(count), Chunk::Edit[]}[]} + RAW_EDITS = 19, + /// Chunk changes instruction + /// action::FillShape + EDITS = 20, /// Declare entities types /// {size_t(index), vec3(size), vec3(scale)} /// TODO: zstd - ENTITY_TYPES = 20, + ENTITY_TYPES = 32, /// Update entities instances position and velocity /// {size_t(entity), size_t(count), {size_t(index), ifvec3(pos), vec3(velocity)}[]}[] - ENTITIES = 21, + ENTITIES = 33, /// World compression dictionary /// zstd dict - COMPRESSION = 24, + COMPRESSION = 64, /// Server capabilities - /// ushort(loadDistance), MAYBE: more - CAPABILITIES = 25, + /// ushort(loadDistance), bool(predictable) + //MAYBE: use uint8_t flags + CAPABILITIES = 65, /// Public chat message /// char[] (not null terminated) - MESSAGE = 29, + MESSAGE = 129, }; /// Packets from client to server enum class client_packet_type: uint8_t { @@ -91,6 +94,11 @@ enum class client_packet_type: uint8_t { /// Position update (unreliable) /// (sequence)uint64_t voxel_pos MOVE = 16, + + /// Client capabilities + /// bool(edit_handling) + //MAYBE: use uint8_t flags + CAPABILITIES = 65, }; constexpr auto MAX_PENDING_CHUNK_COUNT = 256; diff --git a/src/core/world/EdittableChunk.cpp b/src/core/world/EdittableChunk.cpp index 5f754b4..8b68d60 100644 --- a/src/core/world/EdittableChunk.cpp +++ b/src/core/world/EdittableChunk.cpp @@ -7,8 +7,6 @@ using namespace world::client; EdittableChunk::EdittableChunk(): world::Chunk() { } -EdittableChunk::EdittableChunk(std::istream &is): world::Chunk(is) { } -EdittableChunk::EdittableChunk(Voxel val): world::Chunk(), isAverage(true), isMajorant(val.swap()) { voxels.fill(Voxel(val.material(), val.density())); } EdittableChunk::~EdittableChunk() { } std::optional EdittableChunk::update(float deltaTime, bool animate) { diff --git a/src/core/world/EdittableChunk.hpp b/src/core/world/EdittableChunk.hpp index 5650da5..5b5125c 100644 --- a/src/core/world/EdittableChunk.hpp +++ b/src/core/world/EdittableChunk.hpp @@ -7,9 +7,6 @@ using namespace geometry; namespace world::client { class EdittableChunk: public virtual world::Chunk { public: - EdittableChunk(std::istream &is); - /// Create from average - EdittableChunk(Voxel val); virtual ~EdittableChunk(); /// Update voxels @@ -30,21 +27,11 @@ namespace world::client { static std::optional getNeighborIdx(chunk_voxel_idx idx, Face dir); - constexpr bool isTrusted(bool allowMajorant) const { return !isAverage || (allowMajorant && isMajorant); } - void unsetMajorant() { - assert(isMajorant); - isMajorant = false; - } - protected: EdittableChunk(); - /// Is temporary average - const bool isAverage = false; - /// Is temporary full valued - bool isMajorant = false; - - /// Temporary changes + /// Animated changes + /// MAYBE: sort by delay std::vector edits; /// Require update bool upToDate = true; diff --git a/src/core/world/Voxel.hpp b/src/core/world/Voxel.hpp index 79959d1..9e91f49 100644 --- a/src/core/world/Voxel.hpp +++ b/src/core/world/Voxel.hpp @@ -68,6 +68,21 @@ namespace world { constexpr inline bool is_full() const { return density() == DENSITY_MAX && !materials::transparency[material()]; } + + /// Value after fill operation + inline Voxel const filled(Voxel in, float ratio) { + if (ratio >= 1) + return in; + + const world::Voxel::density_t dst = in.density() * ratio; + if (!in.is_material()) + return world::Voxel(material(), world::Voxel::DENSITY_MAX-dst, swap()); + + if (is_material() && density() > dst) + return Voxel(value); + + return world::Voxel(in.material(), dst, in.swap()); + } }; /// Stock of material struct Item { diff --git a/src/server/net/Server.cpp b/src/server/net/Server.cpp index 7278600..b507a92 100644 --- a/src/server/net/Server.cpp +++ b/src/server/net/Server.cpp @@ -16,6 +16,8 @@ namespace net::server { picoquic_set_callback(cnx, connection_callback, peer); } assert(peer == nullptr || peer->contains(cnx) || fin_or_event == picoquic_callback_close); + } else if (peer == (void*)server) { + peer = nullptr; } assert(v_stream_ctx == nullptr || ((net::stream_ctx*)v_stream_ctx)->stream_id == stream_id); diff --git a/src/server/net/Server.hpp b/src/server/net/Server.hpp index 2ecec50..e67c10d 100644 --- a/src/server/net/Server.hpp +++ b/src/server/net/Server.hpp @@ -10,6 +10,7 @@ enum queue: uint8_t { GLOBAL = 0, ENTITY, CHUNK, + EDIT, count }; diff --git a/src/server/world/Universe.cpp b/src/server/world/Universe.cpp index d1505f8..26a893a 100644 --- a/src/server/world/Universe.cpp +++ b/src/server/world/Universe.cpp @@ -13,8 +13,9 @@ using namespace world::server; -const auto AREAS_FILE = "/areas.idx"; -const auto COMPRESSION_PREFIX = (uint8_t)net::server_packet_type::COMPRESSION; +constexpr auto AREAS_FILE = "/areas.idx"; +constexpr auto COMPRESSION_PREFIX = (uint8_t)net::server_packet_type::COMPRESSION; +constexpr auto PREDICTABLE = true; Universe::Universe(const Universe::options &options): host(options.connection, [&](net::server::Peer* peer) { return onConnect(peer); }, @@ -164,6 +165,10 @@ struct net_client { } pendingChunks.insert(it, std::make_pair(in, dist)); } + + bool handleEdits = false; + // MAYBE: client size ordering + std::queue pendingEdits; }; void Universe::saveAll(bool remove) { @@ -223,23 +228,30 @@ void Universe::pull() { host.pull(); host.iterPeers([&](net::server::Peer *peer) { auto data = peer->getCtx(); - if (data == nullptr || data->pendingChunks.empty()) + if (data == nullptr) return; - //TODO: use congestion - area_ pending; - size_t i = peer->queueSize(net::server::queue::CHUNK); - while(i <= 4 && data->popPendingChunk(pending)) { - if (const auto it = areas.find(pending.first); it != areas.end()) { - if (const auto chunk = it->second->getChunks().findInRange(pending.second)) { - peer->send(serializeChunk(pending, std::dynamic_pointer_cast(chunk.value())), net::server::queue::CHUNK); - i++; - } - } + if (data->pendingEdits.empty() && peer->queueSize(net::server::queue::EDIT) == 0) { + peer->send(net::PacketWriter::Of(net::server_packet_type::EDITS, data->pendingEdits.front())); + data->pendingEdits.pop(); } - if (data->pendingChunks.empty()) - peer->send(net::PacketWriter(net::server_packet_type::CHUNK, 0).finish()); //FIXME: must be received after last chunk + if (!data->pendingChunks.empty()) { + //TODO: use congestion + area_ pending; + size_t i = peer->queueSize(net::server::queue::CHUNK); + while(i <= 4 && data->popPendingChunk(pending)) { + if (const auto it = areas.find(pending.first); it != areas.end()) { + if (const auto chunk = it->second->getChunks().findInRange(pending.second)) { + peer->send(serializeChunk(pending, std::dynamic_pointer_cast(chunk.value())), net::server::queue::CHUNK); + i++; + } + } + } + + if (data->pendingChunks.empty()) + peer->send(net::PacketWriter(net::server_packet_type::CHUNK, 0).finish()); //FIXME: must be received after last chunk + } }); } @@ -451,17 +463,23 @@ void Universe::update(float deltaTime) { std::optional Universe::onConnect(net::server::Peer* peer) { ZoneScopedN("Connect"); + using namespace net; LOG_I("Client connect from " << peer->getAddress()); net_client* client = new net_client(entities.at(PLAYER_ENTITY_ID).instances.emplace(Entity::Instance{spawnPoint, glm::vec3(0)})); peer->ctx = client; - peer->send(net::PacketWriter::Of(net::server_packet_type::CAPABILITIES, loadDistance)); + { + auto packet = PacketWriter(server_packet_type::CAPABILITIES, sizeof(loadDistance) + sizeof(bool)); + packet.write(loadDistance); + packet.write(PREDICTABLE); + peer->send(packet.finish()); + } //TODO: lock while not received peer->send(data::out_buffer(data::out_view((uint8_t*)dict_content.data(), dict_content.size()), nullptr), net::server::queue::CHUNK); { auto player = findEntity(PLAYER_ENTITY_ID, client->instanceId); - auto packet = net::PacketWriter(net::server_packet_type::TELEPORT, sizeof(size_t) + sizeof(voxel_pos)); + auto packet = PacketWriter(server_packet_type::TELEPORT, sizeof(size_t) + sizeof(voxel_pos)); packet.write(client->instanceId.index); packet.write(player->pos.as_voxel()); peer->send(packet.finish()); @@ -469,7 +487,7 @@ std::optional Universe::onConnect(net::server::Peer* peer) { } { constexpr auto ITEM_SIZE = sizeof(entity_id::index) + sizeof(glm::vec3) * 2; - auto packet = net::PacketWriter(net::server_packet_type::ENTITY_TYPES, ITEM_SIZE * entities.size()); + auto packet = PacketWriter(server_packet_type::ENTITY_TYPES, ITEM_SIZE * entities.size()); entities.iter([&](entity_id id, const Entity &entity) { packet.write(id.index); packet.write(entity.size); @@ -507,6 +525,14 @@ bool Universe::onPacket(net::server::Peer *peer, const data::out_view &buf, net: } switch (type) { + case client_packet_type::CAPABILITIES: { + auto data = peer->getCtx(); + packet.read(data->handleEdits); + if (!PREDICTABLE && data->handleEdits) { + LOG_E("Client misread capabilities"); + } + break; + } case client_packet_type::MOVE: { auto data = peer->getCtx(); if (voxel_pos pos; !packet.read(pos) || @@ -522,6 +548,12 @@ bool Universe::onPacket(net::server::Peer *peer, const data::out_view &buf, net: LOG_T("Entity in solid fill area"); break; } + host.iterPeers([&](net::server::Peer *peer) { + auto data = peer->getCtx(); + //MAYBE: only in range + if (data && data->handleEdits) + data->pendingEdits.push(*fill); + }); set(fill->pos, fill->radius, fill->shape, fill->val); //TODO: handle inventory } else { @@ -672,19 +704,7 @@ world::ItemList Universe::set(const area_& pos, int radius, action::S if(const auto chunk = it->second->setChunks().findInRange(split.first)) { auto ck = std::dynamic_pointer_cast(chunk.value()); auto prev = ck->get(split.second); - const auto next = [&] { - if (point.second == 1) - return val; - - const world::Voxel::density_t density = val.density() * point.second; - if (!val.is_material()) - return world::Voxel(prev.material(), world::Voxel::DENSITY_MAX-density); - - if (prev.is_material() && prev.density() > density) - return prev; - - return world::Voxel(val.material(), density); - }(); + const auto next = prev.filled(val, point.second); if(prev.value != next.value) { //TODO: apply break table //TODO: inventory @@ -696,22 +716,35 @@ world::ItemList Universe::set(const area_& pos, int radius, action::S } } - ZoneScopedN("Packet"); - size_t size = sizeof(area_id); - for(const auto& part: edits) { - size += sizeof(chunk_pos); - size += sizeof(chunk_voxel_idx); - size += sizeof(Chunk::Edit) * part.second.size(); + bool stupidClient = false; + host.iterPeers([&](net::server::Peer *peer) { + auto data = peer->getCtx(); + if (data && !data->handleEdits) + stupidClient = true; + }); + if (stupidClient) { + ZoneScopedN("Packet"); + size_t size = sizeof(area_id); + for(const auto& part: edits) { + size += sizeof(chunk_pos); + size += sizeof(chunk_voxel_idx); + size += sizeof(Chunk::Edit) * part.second.size(); + } + auto packet = net::PacketWriter(net::server_packet_type::EDITS, size); + packet.write(pos.first); + for(const auto& part: edits) { + packet.write(part.first); + packet.write(part.second.size()); + packet.write(part.second.data(), part.second.size() * sizeof(Chunk::Edit)); + } + auto buffer = packet.finish(); + host.iterPeers([&](net::server::Peer *peer) { + //MAYBE: only in range + auto data = peer->getCtx(); + if (data && !data->handleEdits) + peer->send(buffer, net::server::queue::CHUNK); + }); } - auto packet = net::PacketWriter(net::server_packet_type::EDITS, size); - packet.write(pos.first); - for(const auto& part: edits) { - packet.write(part.first); - packet.write(part.second.size()); - packet.write(part.second.data(), part.second.size() * sizeof(Chunk::Edit)); - } - //MAYBE: only in range - host.sendBroadcast(packet.finish(), net::server::queue::CHUNK); } return list; }