#include "DistantUniverse.hpp" #include #include "Area.hpp" #include "../contouring/Abstract.hpp" #include "core/world/iterators.hpp" #include "core/utils/logger.hpp" #include "core/utils/io.hpp" #include "Chunk.hpp" #include #include #undef LOG_PREFIX #define LOG_PREFIX "Client: " using namespace world::client; DistantUniverse::DistantUniverse(const Universe::login& login, const connection& ct, const Universe::options& opt, const std::string& contouring): Universe(contouring), options(opt), serverDistance(0), peer(ct, [&](const memory::read_view& buf, net::PacketFlags flags){ return onPacket(buf, flags); }) { { auto packet = memory::write_buffer(net::client_packet_type::HELLO, sizeof(uint8_t) + (login.name.length()+1) + login.token.length()); packet.write(login.token.empty() ? 0 : 64); packet.write(login.name.c_str(), login.name.length()+1); if (!login.token.empty()) packet.write(login.token.data(), login.token.size()); peer.send(packet.finish()); } } DistantUniverse::~DistantUniverse() { } void DistantUniverse::update(voxel_pos pos, float deltaTime) { { //NOTE: triggers onPacket ZoneScopedN("Pull"); peer.pull(); } if (options.movePrediction) { elements.hierarchy.for_each([&](Elements::hierarchy_entry &entry) { //vel += acc * deltaTime entry.relative.position += entry.vel.position * deltaTime; entry.relative.rotation = glm::slerp(identity_pivot, entry.vel.rotation, deltaTime) * entry.relative.rotation; const auto node = elements.nodes.directly_at(entry.self); node->absolute = elements.computeAbsolute(entry.parent, entry.relative); //TODO: full physics }); } const auto notifyTick = contouringNotifier.mustNotify(pos, deltaTime); { // Update alive areas ZoneScopedN("World"); auto rng = std::mt19937(std::rand()); const auto contouringThreshold = UINT32_MAX / (1 + contouring->getQueueSize()); for (auto& ref: elements.areas) { ZoneScopedN("Area"); const area_id id = elements.withFlag(ref).val; const auto node = elements.findArea(id); const chunk_pos areaOff = glm::divide((lpivot)glm::conjugate(node->absolute.rotation) * ((world_pos)pos - node->absolute.position)); auto &chunks = node->get()->setChunks(); if (glm::length2(areaOff) <= glm::pow2(options.keepDistance + chunks.getRadius())) { ZoneScopedN("Alive"); for (auto it_c = chunks.begin(); it_c != chunks.end();) { const cell_pos chunkPos = node->absolute.computeChild(glm::multiply(it_c->first)); const auto chunkDist = glm::length2(glm::divide(chunkPos - pos)); if (chunkDist <= glm::pow2(options.keepDistance)) { if (const auto neighbors = it_c->second->setEdits()->update(deltaTime, rng() < contouringThreshold)) { contouring->onUpdate(area_chunk_pos{id.val, it_c->first}, chunkDist, chunks, neighbors.value()); } //MAYBE: if notifyTick ++it_c; } else { it_c = chunks.erase(it_c); } } //MAYBE: if dist to unloaded chunk < X if (mayQueryChunks && notifyTick) { // Request missing chunks ZoneScopedN("Missing"); std::vector missingChunks; std::vector missingRegions; std::vector missingDist; const auto area = std::static_pointer_cast(node->get()); //TODO: use easy sphere fill const auto queryDistance = getQueryDistance(); for (int x = -queryDistance; x <= queryDistance; x++) { for (int y = -queryDistance; y <= queryDistance; y++) { for (int z = -queryDistance; z <= queryDistance; z++) { const auto dist2 = x * x + y * y + z * z; const auto p = areaOff + chunk_pos(x, y, z); if (dist2 <= glm::pow2(queryDistance) && chunks.inRange(p)) { if (const auto it = chunks.find(p); it != chunks.end() && std::static_pointer_cast(it->second)->isTrusted(options.trustMajorant)) { contouring->onNotify(area_chunk_pos{id.val, p}, dist2, chunks); continue; } const auto rcPos = glm::split(p); if(auto it_r = area->regionCache.find(rcPos.first); it_r != 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); ck->setEdits().invalidate(geometry::Faces::All); chunks.emplace(p, ck); contouring->onNotify(area_chunk_pos{id.val, p}, dist2, chunks); if (ck->isTrusted(options.trustMajorant)) continue; } } if (!area->regionCache.contains(rcPos.first) && std::find(missingRegions.begin(), missingRegions.end(), region_pos(rcPos.first)) == missingRegions.end()) { missingRegions.push_back(rcPos.first); } if (missingDist.size() >= net::MAX_PENDING_CHUNK_COUNT) { if (dist2 > missingDist.front()) continue; missingDist.erase(missingDist.begin()); missingChunks.erase(missingChunks.begin()); } auto it = missingDist.begin(); auto itv = missingChunks.begin(); while (it != missingDist.end()) { if (dist2 > *it) break; ++it; ++itv; } missingDist.insert(it, dist2); missingChunks.insert(itv, p); } }}} if(!missingChunks.empty()) { LOG_T("Missing chunks " << missingChunks.size()); auto packet = memory::write_buffer(net::client_packet_type::REQUEST_CHUNKS, sizeof(node_id) + missingChunks.size() * sizeof(chunk_pos)); packet.write(id.val); packet.write(missingChunks.data(), missingChunks.size() * sizeof(chunk_pos)); peer.send(packet.finish()); mayQueryChunks = false; } if(!missingRegions.empty()) { auto packet = memory::write_buffer(net::client_packet_type::REQUEST_REGIONS, sizeof(area_id) + missingRegions.size() * sizeof(region_pos)); packet.write(id.val); packet.write(missingRegions.data(), missingRegions.size() * sizeof(region_pos)); peer.send(packet.finish()); } } } else if(notifyTick) { chunks.clear(); chunks.rehash(0); auto& regions = std::static_pointer_cast(node->get())->regionCache; regions.clear(); regions.rehash(0); } } } //TODO: remove out of range (instances, parts, null node) if (notifyTick) { const auto queryDist2 = glm::pow2(getQueryDistance()); for(const auto& ref: elements.parts) { const part_id id = elements.withFlag(ref).val; const auto node = elements.findPart(id); const auto dist = glm::length2(glm::divide((cell_pos)node->absolute.position - pos)); if (dist <= glm::pow2(options.keepDistance)) { const auto part = node->get(); if (part->loaded()) { contouring->onNotify(id, dist, part->chunkSize(), part->chunks, false); } else if (dist <= queryDist2) { const auto fullCount = part->chunkCount(); const auto &chunks = part->chunks; size_t size = 0; for (size_t i = 0; i < fullCount; i++) { if (i >= chunks.size() || chunks[i] == nullptr) size++; } LOG_T("Missing chunks in part " << size); auto packet = memory::write_buffer(net::client_packet_type::REQUEST_CHUNKS, sizeof(node_id) + size * sizeof(chunk_pos)); packet.write(id.val); for (size_t i = 0; i < fullCount; i++) { if (i >= chunks.size() || chunks[i] == nullptr) { packet.write(part->getPos(i)); } } peer.send(packet.finish()); } } else { auto &chunks = node->get()->chunks; chunks.clear(); chunks.shrink_to_fit(); } } //TODO: remove temporary on last instance removal elements.models.iter([&] (const model_id& id, const Model& model) { if (!model.instances.empty()) { io::vec_istream idata(model.dt); std::istream iss(&idata); contouring->onLoad(id, iss); } }); } contouring->update(pos, elements); } bool DistantUniverse::onPacket(const memory::read_view& buf, net::PacketFlags) { using namespace net; //MAYBE: client::Serializer auto packet = memory::read_buffer(buf, nullptr); server_packet_type type; if(!packet.read(type)) { LOG_D("Empty packet"); return true; } switch (type) { case server_packet_type::QUIT: { uint16_t reason = 42; packet.read(reason); LOG_E("Disconnected from server (" << reason << ')'); peer.disconnect(); break; } case server_packet_type::CAPABILITIES: { packet.read(serverDistance); auto predictable = false; packet.read(predictable); if (predictable) { packet.read(floodfillLimit); } else { options.editHandling = false; options.editPrediction = false; } auto packet = memory::write_buffer(client_packet_type::CAPABILITIES, sizeof(bool)); packet.write(options.editHandling); peer.send(packet.finish()); break; } case server_packet_type::COMPRESSION: { const auto remain = packet.readRemaining(); dict.emplace(remain.data(), remain.size()); LOG_T("Compression dictionnary loaded"); mayQueryChunks = true; break; } case server_packet_type::TELEPORT: { relative_transform pos; if (packet.read(playerId) && packet.read(pos)) { if (!elements.withFlag(playerId)) { LOG_W("FIXME: store if instance not reserved yet"); } onTeleport(pos, getAbsolute(pos)); } break; } case server_packet_type::MESSAGE: { const auto remain = packet.readRemaining(); onMessage(std::string((const char*)remain.data(), remain.size())); break; } case server_packet_type::REGION: { ZoneScopedN("Region"); area_region_ref pos(area_ref(), region_pos(0)); if(!packet.read(pos)) break; const auto id = elements.nodes.with_flag(pos.area.val); if (!Elements::Is(id)) break; const auto node = NodeOf::Make(elements.nodes.directly_at(id)); if (!node) { LOG_W("Region area not found " << pos.area.val); break; } auto ®ions = node->get()->regionCache; auto it_r = regions.find(pos.region); if (it_r == regions.end()) { it_r = regions.emplace(pos.region, 0).first; } // MAYBE: create virtual Chunk without voxels of single material while (!packet.isDone()) { region_chunk_pos cpos; Voxel voxel; if (!packet.read(cpos) || !packet.read(voxel)) break; it_r->second.insert_or_assign(cpos, voxel); } break; } case server_packet_type::CHUNK: { ZoneScopedN("Chunk"); if (!dict.has_value()) break; if (packet.isDone()) { mayQueryChunks = true; break; } node_chunk_ref pos{node_ref(), chunk_pos(0)}; if(!packet.read(pos)) break; const auto id = elements.nodes.with_flag(pos.node.val); const auto node = elements.nodes.directly_at(id); if (!node) { LOG_W("node not found " << pos.node.val); break; } const auto readChunk = [&] { std::vector buffer; if (auto err = dict.value().decompress(packet.readRemaining(), buffer)) { LOG_E("Corrupted chunk packet " << err.value()); return std::shared_ptr(nullptr); } io::vec_istream idata(buffer); std::istream iss(&idata); return std::make_shared(iss); }; switch (Elements::GetType(id)) { case Elements::Type::Area: { const auto area = NodeOf::Make(node)->get(); if(!area->getChunks().inRange(pos.chunk)) { LOG_W("Chunk out of area " << pos.node.val); break; } if (auto ck = readChunk()) { area->setChunks().insert_or_assign(pos.chunk, ck); } break; } case Elements::Type::Part: { const auto part = NodeOf::Make(node)->get(); if(!part->inRange(pos.chunk)) { LOG_W("Chunk out of part " << pos.node.val); break; } if (auto ck = readChunk()) { part->allocate(); part->emplace(pos.chunk, ck); } break; } default: LOG_W("Not chunk holding node type"); break; } break; } case server_packet_type::EDITS: { ZoneScopedN("Edits"); if (!options.editHandling) { LOG_W("Unhandled edit type"); break; } const auto fill = packet.read(); if (!fill) break; const auto anchor_id = elements.withFlag(fill->pos.node); const auto node = elements.findNode(anchor_id); if (!node) break; //MAYBE: make it void after air propagation setup const auto cleanVoxel = Voxel(0, Voxel::DENSITY_MAX); switch (Elements::GetType(anchor_id)) { case Elements::Type::Area: { const auto& chunks = NodeOf::Make(node)->get()->getChunks(); world::iterator::ApplyEraseSplitted(chunks, *fill, floodfillLimit, cleanVoxel, [&](std::shared_ptr& ck, const chunk_pos&, chunk_voxel_idx idx, Voxel/*prev*/, Voxel next, float delay) { return ck->setEdits().apply(ChunkEdits::Edit{next, delay, idx}); }); break; } /* TODO: case Elements::Type::Part: { const auto& chunks = *NodeOf::Make(node)->get()->chunks; world::iterator::ApplyEraseSplitted(chunks, *fill, floodfillLimit, cleanVoxel, [&](std::shared_ptr& ck, const chunk_pos&, chunk_voxel_idx idx, Voxel prev, Voxel next, float delay) { return ck->replace(ChunkEdits::Edit{next, delay, idx}); }); break; }*/ default: LOG_E("Unhandled fill target type " << (int)Elements::GetType(anchor_id)); break; } break; } case server_packet_type::RAW_EDITS: { ZoneScopedN("Raw Edits"); node_ref ref; if(!packet.read(ref)) break; const auto id = elements.nodes.with_flag(ref.val); const auto node = elements.nodes.directly_at(id); if (!node) { LOG_W("node not found " << ref.val); break; } const auto readEdits = [&](std::function(const chunk_pos&)> findInRange) { while(!packet.isDone()) { chunk_pos pos = chunk_pos(INT_MAX); packet.read(pos); chunk_voxel_idx count = 0; packet.read(count); if (auto chunk = findInRange(pos)) { const auto edits = chunk->setEdits(); for (auto i = 0; i < count && !packet.isDone(); i++) { ChunkEdits::Edit edit; packet.read(edit); edits->apply(edit); } } else { packet.skip(count * sizeof(ChunkEdits::Edit)); } } }; switch (Elements::GetType(id)) { case Elements::Type::Area: { const auto area = NodeOf::Make(node)->get(); const auto& chunks = area->setChunks(); readEdits([&](const world::chunk_pos &pos) { return chunks.findInRange(pos); }); break; } case Elements::Type::Part: { const auto part = NodeOf::Make(node)->get(); readEdits([&](const world::chunk_pos &pos) { return part->findInRange(pos); }); break; } default: LOG_W("Not chunk holding node type"); break; } break; } case server_packet_type::ENTITIES: { while(!packet.isDone()) { node_id id; if (!packet.read(id)) break; const auto added = Elements::Has(id); if (Elements::Has(id) || added) { MyElements::start_point state = MyElements::start_point(generational::id(), transform()); if (!packet.read(state)) break; if (added) { switch (Elements::GetType(id)) { case Elements::Type::Area: { Area::params params; if (packet.read(params)) { if (elements.withFlag(id)) { LOG_D("Added entity already exists " << id.val.val); break; } id = elements.createAt(id, state, std::make_shared(params)); elements.areas.push_back(id); } break; } case Elements::Type::Part: { glm::usvec3 size; if (packet.read(size)) { if (elements.withFlag(id)) { LOG_D("Added entity already exists " << id.val.val); break; } id = elements.createAt(id, state, std::make_shared(size)); elements.parts.push_back(id); } break; } case Elements::Type::Instance: { model_id mid; if (packet.read(mid)) { const auto model = elements.models.directly_at(mid); if (!model) { LOG_E("FIXME: must query model"); break; } if (elements.withFlag(id)) { LOG_D("Added entity already exists " << id.val.val); break; } id = elements.createAt(id, state, model->origin ? model->origin : std::make_shared(mid)); model->instances.push_back(id.val.index()); } break; } default: LOG_W("Unhandled entity type " << id.val.val); break; } } else if (elements.withFlag(id)) { elements.move(id, state); } else { LOG_W("Must query content " << id.val.val); } } else if (Elements::Has(id)) { elements.remove(id); } else { LOG_W("Corrupted entity with id " << id.val.val); break; } } break; } default: LOG_W("Bad packet from server"); break; } return true; } void DistantUniverse::emit(const world::action::packet &action) { if(const auto move = std::get_if(&action)) { peer.send(memory::read_buffer::Of(net::client_packet_type::MOVE, move->pos), net::client::queue::MOVE, 1); } else if(const auto message = std::get_if(&action)) { peer.send(memory::read_buffer::Of(net::client_packet_type::MESSAGE, message->text.data(), message->text.size())); } else if(const auto fill = std::get_if(&action)) { peer.send(memory::read_buffer::Of(net::client_packet_type::FILL_SHAPE, *fill)); if (options.editPrediction) { ZoneScopedN("Fill"); const auto keepDelay = 5 + (peer.getRTT() / 20000.f); // 5s + 50RTT const auto anchor_id = elements.withFlag(fill->pos.node); const auto node = elements.findNode(anchor_id); if (!node) return; //MAYBE: make it void after air propagation setup const auto cleanVoxel = Voxel(0, Voxel::DENSITY_MAX); switch (Elements::GetType(anchor_id)) { case Elements::Type::Area: { const auto& chunks = NodeOf::Make(node)->get()->getChunks(); world::iterator::ApplyEraseSplitted(chunks, *fill, floodfillLimit, cleanVoxel, [&](std::shared_ptr& ck, const chunk_pos&, chunk_voxel_idx idx, Voxel/*prev*/, Voxel next, float delay) { return ck->setEdits().addFutureEdit(ChunkEdits::Edit{next, keepDelay - delay * 2, idx}, delay); }); break; } default: LOG_E("Unhandled fill target type " << (int)Elements::GetType(anchor_id)); break; } } } else { LOG_W("Bad action " << action.index()); } } DistantUniverse::MyElements::MyElements(): EdittableElements(true) { } world::node_id DistantUniverse::MyElements::createAt(node_id id, const start_point& sp, const std::shared_ptr& el) { return EdittableElements::createAt(id, sp, el); }