707 lines
31 KiB
C++
707 lines
31 KiB
C++
#include "Universe.hpp"
|
|
|
|
#include <Tracy.hpp> //NOLINT
|
|
#include <common/TracySystem.hpp> // NOLINT
|
|
#include <filesystem>
|
|
#include <random>
|
|
|
|
#include "Chunk.hpp"
|
|
#include "../../core/world/raycast.hpp"
|
|
#include "../../core/world/actions.hpp"
|
|
#include "../../core/net/PacketView.hpp"
|
|
|
|
using namespace world::server;
|
|
|
|
const auto AREAS_FILE = "/areas.idx";
|
|
|
|
Universe::Universe(const Universe::options &options): host(options.connection, options.maxPlayers),
|
|
dict_content({options.folderPath + "/zstd.dict", "content/zstd.dict"}), dicts(dict_content), dict_write_ctx(dicts.make_writer()) {
|
|
setOptions(options);
|
|
folderPath = options.folderPath;
|
|
running = true;
|
|
|
|
std::filesystem::create_directories(folderPath);
|
|
{
|
|
std::ifstream index(folderPath + AREAS_FILE);
|
|
if(index.good()) {
|
|
size_t size = 0;
|
|
index.read(reinterpret_cast<char *>(&size), sizeof(size));
|
|
robin_hood::unordered_map<size_t, Area::params> tmp;
|
|
while(!index.eof()) {
|
|
size_t id = UINT32_MAX;
|
|
index.read(reinterpret_cast<char *>(&id), sizeof(size_t));
|
|
Area::params params{voxel_pos(0), 0, generator::params()};
|
|
index.read(reinterpret_cast<char *>(¶ms.center.x), sizeof(voxel_pos::value_type));
|
|
index.read(reinterpret_cast<char *>(¶ms.center.y), sizeof(voxel_pos::value_type));
|
|
index.read(reinterpret_cast<char *>(¶ms.center.z), sizeof(voxel_pos::value_type));
|
|
index.read(reinterpret_cast<char *>(¶ms.radius), sizeof(int));
|
|
index.read(reinterpret_cast<char *>(¶ms.properties), sizeof(generator::params));
|
|
[[maybe_unused]]
|
|
auto ok = tmp.emplace(id, params).second;
|
|
assert(ok && "Duplicated area");
|
|
index.peek();
|
|
}
|
|
assert(tmp.size() == size && "Corrupted areas index");
|
|
far_areas = data::generational::vector<Area::params>(tmp);
|
|
LOG_T(far_areas.size() << " areas loaded");
|
|
} else {
|
|
LOG_E("No index file!!! Probably a new world...");
|
|
//TODO: generate universe
|
|
const auto radius = 1 << 4;
|
|
far_areas.emplace(Area::params{glm::multiply(voxel_pos(radius, 0, 0)), radius,
|
|
generator::params(std::in_place_type<generator::RoundPlanet::Params>, radius * CHUNK_LENGTH * 3 / 4, 42)});
|
|
//far_areas.emplace(Area::params{voxel_pos(0), 1 << 20, generator::params(std::in_place_type<generator::Cave::Params>, 42)});
|
|
}
|
|
spawnPoint = voxel_pos(100, 0, 0); //TODO: save in index
|
|
index.close();
|
|
}
|
|
|
|
{
|
|
[[maybe_unused]]
|
|
const auto type_id = entities.emplace(glm::vec3(1), glm::vec3(2));
|
|
assert(type_id == PLAYER_ENTITY_ID);
|
|
}
|
|
|
|
// Workers
|
|
for (size_t i = 0; i < std::max<uint32_t>(1, std::thread::hardware_concurrency() / 2 - 1); i++) {
|
|
workers.emplace_back([&] {
|
|
#if TRACY_ENABLE
|
|
tracy::SetThreadName("Chunks");
|
|
#endif
|
|
const auto read_ctx = dicts.make_reader();
|
|
const auto write_ctx = dicts.make_writer();
|
|
while (running) {
|
|
if (std::pair<area_<chunk_pos>, std::shared_ptr<Area>> task; loadQueue.pop(task)) {
|
|
//MAYBE: loadQueue.take to avoid duplicated work on fast move
|
|
ZoneScopedN("ProcessLoad");
|
|
const auto &pos = task.first;
|
|
const auto rcPos = glm::split(pos.second);
|
|
const auto reg = task.second->getRegion(folderPath, std::make_pair(pos.first, rcPos.first));
|
|
Region::data data;
|
|
if(reg->read(rcPos.second, read_ctx, data)) {
|
|
ZoneScopedN("ProcessRead");
|
|
vec_istream idata(data);
|
|
std::istream iss(&idata);
|
|
loadedQueue.push({pos, createChunk(iss)});
|
|
} else {
|
|
ZoneScopedN("ProcessGenerate");
|
|
loadedQueue.push({pos, createChunk(pos.second, task.second->getGenerator())});
|
|
}
|
|
} else if(save_task_t task; saveQueue.pop(task)) {
|
|
//MAYBE: queue.take to avoid concurent write or duplicated work on fast move
|
|
ZoneScopedN("ProcessSave");
|
|
if(task.second.second->isModified()) {
|
|
std::ostringstream out;
|
|
task.second.second->write(out);
|
|
const auto rcPos = glm::split(task.second.first);
|
|
const auto reg = task.first.second->getRegion(folderPath, std::make_pair(task.first.first, rcPos.first));
|
|
reg->write(rcPos.second, write_ctx, out.str());
|
|
}
|
|
} else {
|
|
loadQueue.wait();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
Universe::~Universe() {
|
|
saveAll(false);
|
|
saveAreas();
|
|
|
|
running = false;
|
|
loadQueue.notify_all();
|
|
|
|
for (auto &worker: workers) {
|
|
if (worker.joinable())
|
|
worker.join();
|
|
}
|
|
LOG_D("Universe disappeared");
|
|
}
|
|
|
|
void Universe::saveAll(bool remove) {
|
|
for(auto& area: areas) {
|
|
auto& chunks = area.second->setChunks();
|
|
for (auto it_c = chunks.begin(); it_c != chunks.end(); remove ? it_c = chunks.erase(it_c) : ++it_c) {
|
|
saveQueue.emplace(area, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second)));
|
|
}
|
|
}
|
|
loadQueue.notify_all();
|
|
|
|
if (auto size = saveQueue.size(); size > 0) {
|
|
LOG_I("Saving " << size << " chunks");
|
|
const auto SAVE_CHECK_TIME = 500;
|
|
do {
|
|
loadQueue.notify_all();
|
|
std::cout << "\rSaving... " << size << " " << std::flush;
|
|
std::this_thread::sleep_for(std::chrono::microseconds(SAVE_CHECK_TIME));
|
|
size = saveQueue.size();
|
|
} while (size > 0);
|
|
std::cout << std::endl;
|
|
}
|
|
}
|
|
|
|
// Write areas index (warn: file io)
|
|
void Universe::saveAreas() const {
|
|
std::ofstream index(folderPath + AREAS_FILE, std::ios::out | std::ios::binary);
|
|
if(!index.good()) {
|
|
LOG_E("Areas index write error");
|
|
return;
|
|
}
|
|
{
|
|
size_t size = areas.size() + far_areas.size();
|
|
index.write(reinterpret_cast<char *>(&size), sizeof(size));
|
|
}
|
|
std::function write = [&](area_id id, Area::params params) {
|
|
auto idx = id.index;
|
|
index.write(reinterpret_cast<char *>(&idx), sizeof(size_t));
|
|
index.write(reinterpret_cast<char *>(¶ms.center.x), sizeof(voxel_pos::value_type));
|
|
index.write(reinterpret_cast<char *>(¶ms.center.y), sizeof(voxel_pos::value_type));
|
|
index.write(reinterpret_cast<char *>(¶ms.center.z), sizeof(voxel_pos::value_type));
|
|
index.write(reinterpret_cast<char *>(¶ms.radius), sizeof(int));
|
|
index.write(reinterpret_cast<char *>(¶ms.properties), sizeof(generator::params)); // MAYBE: use binary structured format (protobuf/msgpack)
|
|
};
|
|
for(const auto& area: areas) {
|
|
write(area.first, area.second->getParams());
|
|
}
|
|
far_areas.iter(write);
|
|
if(!index.good())
|
|
LOG_E("Areas index write error");
|
|
|
|
index.close();
|
|
}
|
|
|
|
void Universe::update(float deltaTime) {
|
|
ZoneScopedN("Universe");
|
|
|
|
pullNetwork();
|
|
|
|
std::vector<voxel_pos> moves;
|
|
{
|
|
moves.reserve(movedPlayers.size());
|
|
for (const auto& id: movedPlayers) {
|
|
if (auto player = findEntity(PLAYER_ENTITY_ID, id))
|
|
moves.push_back(player->pos.as_voxel());
|
|
}
|
|
movedPlayers.clear();
|
|
}
|
|
|
|
if (!moves.empty()) {
|
|
ZoneScopedN("Far");
|
|
bool extracted = false;
|
|
far_areas.extract([&](area_id id, Area::params params) {
|
|
for(const auto& move: moves) {
|
|
if(const chunk_pos diff = glm::divide(move - params.center);
|
|
glm::length2(diff) <= glm::pow2(loadDistance + params.radius)) {
|
|
|
|
LOG_I("Load area " << id.index);
|
|
areas.emplace(id, std::make_shared<Area>(params));
|
|
extracted = true;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
if(extracted)
|
|
broadcastAreas();
|
|
}
|
|
const auto &players = entities.at(PLAYER_ENTITY_ID).instances;
|
|
{ // Update alive areas
|
|
ZoneScopedN("World");
|
|
#if TRACY_ENABLE
|
|
size_t chunk_count = 0;
|
|
size_t region_count = 0;
|
|
#endif
|
|
const bool queuesEmpty = loadQueue.empty() && saveQueue.empty();
|
|
bool allLazy = true;
|
|
auto it = areas.begin();
|
|
while (it != areas.end()) {
|
|
ZoneScopedN("Area");
|
|
//FIXME: const auto areaChunkChange = it->second->move(glm::vec3(deltaTime));
|
|
const auto areaRange = glm::pow2(keepDistance + it->second->getChunks().getRadius());
|
|
const auto areaDiff = glm::divide(it->second->getOffset().as_voxel());
|
|
|
|
std::vector<chunk_pos> inAreaPlayers;
|
|
players.iter([&](entity_id, Entity::Instance player) {
|
|
const chunk_pos diff = glm::divide(player.pos.as_voxel() - it->second->getOffset().as_voxel());
|
|
if (glm::length2(diff) <= areaRange)
|
|
inAreaPlayers.push_back(diff);
|
|
});
|
|
|
|
auto &chunks = it->second->setChunks();
|
|
if (inAreaPlayers.empty()) {
|
|
auto it_c = chunks.begin();
|
|
while(it_c != chunks.end()) {
|
|
saveQueue.emplace(*it, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second)));
|
|
it_c = chunks.erase(it_c);
|
|
}
|
|
LOG_I("Unload area " << it->first.index);
|
|
[[maybe_unused]]
|
|
auto ok = far_areas.put(it->first, it->second->getParams());
|
|
assert(ok);
|
|
it = areas.erase(it);
|
|
saveAreas();
|
|
} else {
|
|
bool lazyArea = queuesEmpty;
|
|
{ // Update alive chunks
|
|
ZoneScopedN("Alive");
|
|
auto it_c = chunks.begin();
|
|
while(it_c != chunks.end()) {
|
|
if ([&] {
|
|
const auto keepDist = glm::pow2(keepDistance);
|
|
for(const auto& diff: inAreaPlayers) {
|
|
if (glm::length2(diff - it_c->first) < keepDist)
|
|
return true;
|
|
}
|
|
return false;
|
|
}()) {
|
|
updateChunk(it, it_c, areaDiff, deltaTime);
|
|
++it_c;
|
|
#if TRACY_ENABLE
|
|
chunk_count++;
|
|
#endif
|
|
} else {
|
|
saveQueue.emplace(*it, std::make_pair(it_c->first, std::dynamic_pointer_cast<Chunk>(it_c->second))); //MAYBE: take look
|
|
lazyArea = false;
|
|
it_c = chunks.erase(it_c);
|
|
}
|
|
}
|
|
}
|
|
{ // Enqueue missing chunks
|
|
ZoneScopedN("Missing");
|
|
auto handle = loadQueue.inserter();
|
|
for (const auto& to: moves) {
|
|
const chunk_pos diff = glm::divide(to - it->second->getOffset().as_voxel());
|
|
if (glm::length2(diff) > areaRange)
|
|
continue;
|
|
|
|
//TODO: need dist so no easy sphere fill
|
|
for (int x = -loadDistance; x <= loadDistance; x++) {
|
|
for (int y = -loadDistance; y <= loadDistance; y++) {
|
|
for (int z = -loadDistance; z <= loadDistance; z++) {
|
|
const auto dist2 = x * x + y * y + z * z;
|
|
if (dist2 <= loadDistance * loadDistance) {
|
|
const auto p = diff + chunk_pos(x, y, z);
|
|
if (chunks.inRange(p) && chunks.find(p) == chunks.end()) {
|
|
handle.first(std::make_pair(it->first, p), it->second, -dist2);
|
|
lazyArea = false;
|
|
}
|
|
}
|
|
}}}
|
|
}
|
|
if(!lazyArea)
|
|
loadQueue.notify_all();
|
|
}
|
|
allLazy &= lazyArea;
|
|
if (lazyArea) { // Clear un-used regions
|
|
ZoneScopedN("Region");
|
|
const auto unique = it->second->getRegions(); // MAYBE: shared then unique
|
|
#if TRACY_ENABLE
|
|
region_count += unique->size();
|
|
#endif
|
|
for (auto it_r = unique->begin(); it_r != unique->end(); ++it_r) {
|
|
if([&] {
|
|
const auto keepDist = glm::pow2(keepDistance + REGION_LENGTH * 2);
|
|
for(const auto& diff: inAreaPlayers) {
|
|
if (glm::length2(diff - glm::lvec3(it_r->first) * glm::lvec3(REGION_LENGTH)) <= keepDist)
|
|
return false;
|
|
}
|
|
return true;
|
|
}()) {
|
|
unique->erase(it_r); //FIXME: may wait for os file access (long)
|
|
break; //NOTE: save one only max per frame
|
|
}
|
|
}
|
|
}
|
|
++it;
|
|
}
|
|
}
|
|
#if TRACY_ENABLE
|
|
TracyPlot("ChunkCount", static_cast<int64_t>(chunk_count));
|
|
if(allLazy) {
|
|
TracyPlot("Region", static_cast<int64_t>(region_count));
|
|
}
|
|
TracyPlot("ChunkLoad", static_cast<int64_t>(loadQueue.size()));
|
|
TracyPlot("ChunkUnload", static_cast<int64_t>(saveQueue.size()));
|
|
#endif
|
|
}
|
|
{ // Update entities
|
|
ZoneScopedN("Entities");
|
|
size_t item_count = 0;
|
|
entities.for_each([&](entity_id type, Entity &val) {
|
|
val.instances.remove([&](entity_id, Entity::Instance &inst) {
|
|
if (type == PLAYER_ENTITY_ID) {
|
|
//MAYBE: update players ?
|
|
item_count++;
|
|
return false;
|
|
}
|
|
|
|
inst.pos += inst.velocity * deltaTime;
|
|
if (true /*FIXME: remove far entities ? glm::length2(glm::divide(pos - inst.pos.as_voxel())) <= glm::pow2(keepDistance);*/) {
|
|
item_count++;
|
|
return false;
|
|
}
|
|
return true;
|
|
//MAYBE: Store in region ?
|
|
//MAYBE: Save to files
|
|
});
|
|
});
|
|
TracyPlot("EntityCount", static_cast<int64_t>(item_count));
|
|
|
|
constexpr auto CAT_SIZE = sizeof(entity_id::index) + sizeof(size_t);
|
|
constexpr auto ITEM_SIZE = sizeof(entity_id::index) + sizeof(glm::ifvec3) + sizeof(glm::vec3);
|
|
auto packet = net::Server::makePacket(net::server_packet_type::ENTITIES, NULL, CAT_SIZE * entities.size() + ITEM_SIZE * item_count, 0);
|
|
entities.iter([&](entity_id id, const Entity &entity) {
|
|
packet.write(id.index);
|
|
packet.write(entity.instances.size());
|
|
entity.instances.iter([&](entity_id i, const Entity::Instance &inst) {
|
|
packet.write(i.index);
|
|
packet.write(inst.pos);
|
|
packet.write(inst.velocity);
|
|
});
|
|
});
|
|
|
|
assert(packet.isFull());
|
|
host.broadcast(packet.get(), net::channel_type::RELIABLE);
|
|
}
|
|
|
|
{ // Store loaded chunks
|
|
ZoneScopedN("Load");
|
|
robin_hood::pair<area_<chunk_pos>, std::shared_ptr<Chunk>> loaded;
|
|
for (auto handle = loadedQueue.extractor(); handle.first(loaded);) {
|
|
if (const auto it = areas.find(loaded.first.first); it != areas.end()) {
|
|
it->second->setChunks().emplace(loaded.first.second, loaded.second);
|
|
loadChunk(loaded.first, glm::divide(it->second->getOffset().as_voxel()), it->second->getChunks());
|
|
// MAYBE: limit chunks per update
|
|
host.broadcast(serializeChunk(loaded), net::channel_type::RELIABLE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct net_client {
|
|
net_client(net::salt_t salt, data::generational::id id): salt(salt), instanceId(id) { }
|
|
net::salt_t salt;
|
|
data::generational::id instanceId;
|
|
};
|
|
|
|
void Universe::pullNetwork() {
|
|
ZoneScopedN("Network");
|
|
using namespace net;
|
|
host.pull(
|
|
[&](peer_t *peer, salt_t salt) {
|
|
ZoneScopedN("Connect");
|
|
LOG_I("Client connect from " << peer->address);
|
|
net_client* client = new net_client(salt, entities.at(PLAYER_ENTITY_ID).instances.emplace(Entity::Instance{spawnPoint, glm::vec3(0)}));
|
|
peer->data = client;
|
|
|
|
const salt_t rnd = std::rand();
|
|
host.sendTo<salt_t>(peer, server_packet_type::CHALLENGE, rnd, channel_type::RELIABLE);
|
|
client->salt = salt ^ rnd;
|
|
|
|
host.send(peer, server_packet_type::CAPABILITIES, loadDistance, channel_type::RELIABLE);
|
|
host.send(peer, server_packet_type::COMPRESSION, dict_content.data(), dict_content.size(), channel_type::RELIABLE);
|
|
{
|
|
auto player = findEntity(PLAYER_ENTITY_ID, client->instanceId);
|
|
struct tp { size_t bindEntity; voxel_pos position; };
|
|
host.sendTo(peer, server_packet_type::TELEPORT, tp{client->instanceId.index, player->pos.as_voxel()}, channel_type::RELIABLE);
|
|
movedPlayers.insert(client->instanceId);
|
|
}
|
|
{
|
|
constexpr auto ITEM_SIZE = sizeof(entity_id::index) + sizeof(glm::vec3) * 2;
|
|
auto packet = net::Server::makePacket(net::server_packet_type::ENTITY_TYPES, NULL, ITEM_SIZE * entities.size(), ENET_PACKET_FLAG_RELIABLE);
|
|
entities.iter([&](entity_id id, const Entity &entity) {
|
|
packet.write(id.index);
|
|
packet.write(entity.size);
|
|
packet.write(entity.scale);
|
|
});
|
|
|
|
assert(packet.isFull());
|
|
host.broadcast(packet.get(), net::channel_type::RELIABLE);
|
|
}
|
|
broadcastMessage("> Player" + std::to_string(client->instanceId.index) + " has joined us");
|
|
broadcastAreas();
|
|
},
|
|
[&](peer_t *peer, disconnect_reason reason) {
|
|
ZoneScopedN("Disconnect");
|
|
LOG_I("Client disconnect from " << peer->address << " with " << (enet_uint32)reason);
|
|
if (const auto data = Server::GetPeerData<net_client>(peer)) {
|
|
broadcastMessage("> Player" + std::to_string(data->instanceId.index) + " has left our universe");
|
|
entities.at(PLAYER_ENTITY_ID).instances.free(data->instanceId);
|
|
delete data;
|
|
}
|
|
},
|
|
[&](peer_t *peer, packet_t* packet, channel_type) {
|
|
ZoneScopedN("Data");
|
|
if(packet->dataLength < sizeof(client_packet_type) + sizeof(salt_t)) {
|
|
LOG_T("Empty packet from " << peer->address);
|
|
return;
|
|
}
|
|
if (memcmp(peer->data, packet->data + sizeof(client_packet_type), sizeof(salt_t)) != 0) {
|
|
LOG_D("Wrong salt from " << peer->address);
|
|
host.disconnect(peer, disconnect_reason::WRONG_SALT);
|
|
return;
|
|
}
|
|
|
|
const auto type = static_cast<client_packet_type>(*packet->data);
|
|
switch (type) {
|
|
case client_packet_type::MOVE: {
|
|
if(voxel_pos pos; !PacketReader(packet).read(pos) ||
|
|
!movePlayer(Server::GetPeerData<net_client>(peer)->instanceId, pos)) {
|
|
LOG_T("Bad move");
|
|
}
|
|
break;
|
|
}
|
|
case client_packet_type::FILL_SHAPE: {
|
|
if(const auto fill = PacketReader(packet).read<world::action::FillShape>()) {
|
|
//TODO: check ray
|
|
//TODO: check entities
|
|
//TODO: handle inventory
|
|
if (fill->shape == world::action::Shape::Cube)
|
|
setCube(fill->pos, fill->val, fill->radius);
|
|
else
|
|
setSphere(fill->pos, fill->val, fill->radius);
|
|
} else {
|
|
LOG_T("Bad fill");
|
|
}
|
|
break;
|
|
}
|
|
case client_packet_type::MESSAGE: {
|
|
const auto ref = PacketReader(packet).remaning();
|
|
broadcastMessage("Player" + std::to_string(Server::GetPeerData<net_client>(peer)->instanceId.index)
|
|
+ ": " + std::string(static_cast<const char *>(ref.data()), ref.size()));
|
|
break;
|
|
}
|
|
case client_packet_type::MISSING_CHUNKS: {
|
|
if (auto player = findEntity(PLAYER_ENTITY_ID,Server::GetPeerData<net_client>(peer)->instanceId )) {
|
|
const auto pos = player->pos.as_voxel();
|
|
|
|
auto reader = PacketReader(packet);
|
|
area_id id = *reader.read<area_id>();
|
|
if(auto area = areas.find(id); area != areas.end()) {
|
|
auto &chunks = area->second->getChunks();
|
|
const chunk_pos diff = glm::divide(pos - area->second->getOffset().as_voxel());
|
|
while(!reader.isFull()) {
|
|
chunk_pos cpos = *reader.read<chunk_pos>();
|
|
if(glm::length2(diff - cpos) <= glm::pow2(loadDistance) && chunks.inRange(cpos)) {
|
|
if(auto chunk = chunks.find(cpos); chunk != chunks.end()) {
|
|
host.send(peer, serializeChunk({std::make_pair(id, cpos), std::dynamic_pointer_cast<Chunk>(chunk->second)}), net::channel_type::RELIABLE);
|
|
}
|
|
} else {
|
|
LOG_T("Request out of range chunk");
|
|
}
|
|
}
|
|
} else {
|
|
LOG_T("Bad chunk request");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
LOG_T("Bad packet from " << peer->address);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
void Universe::broadcastAreas() {
|
|
constexpr size_t ITEM_SIZE = sizeof(area_id) + sizeof(world::Area::params);
|
|
|
|
auto packet = net::Server::makePacket(net::server_packet_type::AREAS, NULL, ITEM_SIZE * areas.size(), ENET_PACKET_FLAG_RELIABLE);
|
|
for(const auto& area: areas) {
|
|
const auto params = area.second->getParams();
|
|
packet.write(area.first);
|
|
packet.write(world::Area::params{params.center, params.radius, area.second->getCurvature()});
|
|
}
|
|
|
|
assert(packet.isFull());
|
|
host.broadcast(packet.get(), net::channel_type::RELIABLE);
|
|
}
|
|
net::packet_t* Universe::serializeChunk(const robin_hood::pair<area_<chunk_pos>, std::shared_ptr<Chunk>> &pair) {
|
|
std::ostringstream out;
|
|
pair.second->write(out);
|
|
std::vector<char> buffer;
|
|
dict_write_ctx.compress(out.str(), buffer);
|
|
auto packet = net::Server::makePacket(net::server_packet_type::CHUNK, NULL, sizeof(pair.first) + buffer.size(), ENET_PACKET_FLAG_RELIABLE);
|
|
packet.write(pair.first);
|
|
packet.write(buffer.data(), buffer.size());
|
|
assert(packet.isFull());
|
|
return packet.get();
|
|
}
|
|
void Universe::broadcastMessage(const std::string& text) {
|
|
host.broadcast(net::server_packet_type::MESSAGE, text.data(), text.size(), net::channel_type::RELIABLE);
|
|
}
|
|
|
|
void Universe::updateChunk(area_map::iterator &, world::ChunkContainer::iterator &, chunk_pos, float /*deltaTime*/) {}
|
|
void Universe::loadChunk(area_<chunk_pos>, chunk_pos, const world::ChunkContainer &) {}
|
|
|
|
void Universe::setOptions(const Universe::options& options) {
|
|
loadDistance = options.loadDistance;
|
|
keepDistance = options.keepDistance;
|
|
}
|
|
|
|
Universe::ray_result Universe::raycast(const geometry::Ray &ray) const {
|
|
return Raycast(ray, areas);
|
|
}
|
|
|
|
std::optional<world::Item> Universe::set(const area_<voxel_pos>& pos, const Voxel& val) {
|
|
if(const auto it = areas.find(pos.first); it != areas.end()) {
|
|
auto &chunks = it->second->setChunks();
|
|
const auto split = glm::splitIdx(pos.second);
|
|
if(chunks.inRange(split.first))
|
|
if(const auto chunk = chunks.findInRange(split.first))
|
|
return {std::dynamic_pointer_cast<Chunk>(chunk.value())->replace(split.second, val)};
|
|
}
|
|
return {};
|
|
}
|
|
world::ItemList Universe::setCube(const area_<voxel_pos>& pos, const Voxel& val, int radius) {
|
|
ZoneScopedN("Fill");
|
|
ItemList list;
|
|
if(const auto it = areas.find(pos.first); it != areas.end()) {
|
|
robin_hood::unordered_map<chunk_pos, std::vector<Chunk::Edit>> edits;
|
|
auto &chunks = it->second->setChunks();
|
|
for (int z = -radius; z <= radius; z++) {
|
|
for (int y = -radius; y <= radius; y++) {
|
|
for (int x = -radius; x <= radius; x++) {
|
|
//TODO: list.pop(val)
|
|
const auto offset = voxel_pos(x, y, z);
|
|
const auto split = glm::splitIdx(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>(chunk.value());
|
|
auto prev = ck->get(split.second);
|
|
if(prev.value != val.value) {
|
|
//TODO: apply break table
|
|
//TODO: inventory
|
|
const auto delay = glm::length2(offset) / radius * .05f;
|
|
edits[split.first].push_back(Chunk::Edit{split.second, val, delay});
|
|
ck->replace(split.second, val, delay);
|
|
}
|
|
}
|
|
}}}
|
|
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::Server::makePacket(net::server_packet_type::EDITS, NULL, size, 0);
|
|
packet.write(pos.first);
|
|
for(const auto& part: edits) {
|
|
packet.write(part.first);
|
|
packet.write<chunk_voxel_idx>(part.second.size());
|
|
packet.write(part.second.data(), part.second.size() * sizeof(Chunk::Edit));
|
|
}
|
|
|
|
assert(packet.isFull());
|
|
host.broadcast(packet.get(), net::channel_type::NOTIFY);
|
|
}
|
|
return list;
|
|
}
|
|
world::ItemList Universe::setSphere(const area_<voxel_pos>& pos, const Voxel& val, int radius) {
|
|
ZoneScopedN("FillSphere");
|
|
ItemList list;
|
|
if(const auto it = areas.find(pos.first); it != areas.end()) {
|
|
robin_hood::unordered_map<chunk_pos, std::vector<Chunk::Edit>> edits;
|
|
auto &chunks = it->second->setChunks();
|
|
for (int z = -radius; z <= radius; z++) {
|
|
for (int y = -radius; y <= radius; y++) {
|
|
for (int x = -radius; x <= radius; x++) {
|
|
const auto offset = voxel_pos(x, y, z);
|
|
//FIXME: refactor with voxel_pos iterator
|
|
if (glm::length2(offset) > glm::pow2(radius))
|
|
continue;
|
|
|
|
//TODO: list.pop(val)
|
|
const auto split = glm::splitIdx(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>(chunk.value());
|
|
auto prev = ck->get(split.second);
|
|
if(prev.value != val.value) {
|
|
//TODO: apply break table
|
|
//TODO: inventory
|
|
const auto delay = glm::length2(offset) / radius * .05f;
|
|
edits[split.first].push_back(Chunk::Edit{split.second, val, delay});
|
|
ck->replace(split.second, val, delay);
|
|
}
|
|
}
|
|
}}}
|
|
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::Server::makePacket(net::server_packet_type::EDITS, NULL, size, 0);
|
|
packet.write(pos.first);
|
|
for(const auto& part: edits) {
|
|
packet.write(part.first);
|
|
packet.write<chunk_voxel_idx>(part.second.size());
|
|
packet.write(part.second.data(), part.second.size() * sizeof(Chunk::Edit));
|
|
}
|
|
|
|
assert(packet.isFull());
|
|
host.broadcast(packet.get(), net::channel_type::NOTIFY);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
bool Universe::collide_end(const glm::ifvec3 &pos, const glm::vec3 &vel, int density, float radius) const {
|
|
return std::holds_alternative<ray_target>(raycast(geometry::Ray((pos + vel) * density, vel, radius)));
|
|
}
|
|
bool Universe::collide_point(const glm::ifvec3 &pos, const glm::vec3 &vel, int density) const {
|
|
const auto target = ((pos + vel) * density).as_voxel();
|
|
for(auto& area: areas) {
|
|
if(area.second->getBounding().contains(target)) {
|
|
const auto &offset = area.second->getOffset().as_voxel();
|
|
const auto &chunks = area.second->getChunks();
|
|
const auto pos = target - offset;
|
|
const chunk_pos cPos = glm::divide(pos);
|
|
if (const auto it = chunks.find(cPos); it != chunks.end()) {
|
|
const auto voxel = it->second->getAt(glm::modulo(pos));
|
|
if (voxel.is_solid()) {
|
|
return true;
|
|
}
|
|
} else if(chunks.inRange(cPos)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
entity_instance_id Universe::addEntity(entity_id type, const Entity::Instance &instance) {
|
|
return std::make_pair(type, entities.at(type).instances.push(instance));
|
|
}
|
|
|
|
Universe::Entity::Instance* Universe::findEntity(entity_id type, entity_id id) {
|
|
if(!entities.contains(type))
|
|
return nullptr;
|
|
|
|
if(!entities.at(type).instances.contains(id))
|
|
return nullptr;
|
|
|
|
return &entities.at(type).instances.at(id);
|
|
}
|
|
|
|
bool Universe::movePlayer(data::generational::id id, glm::ifvec3 pos) {
|
|
if (auto player = findEntity(PLAYER_ENTITY_ID, id)) {
|
|
const auto initialPos = player->pos.as_voxel();
|
|
if (initialPos == pos.as_voxel())
|
|
return true;
|
|
|
|
//TODO: check dist + collision from a to b
|
|
movedPlayers.insert(id);
|
|
player->pos = pos;
|
|
return true;
|
|
} else
|
|
return false;
|
|
}
|
|
|
|
std::shared_ptr<Chunk> Universe::createChunk(const chunk_pos &pos, const std::unique_ptr<generator::Abstract> &rnd) const {
|
|
return std::make_shared<Chunk>(pos, rnd);
|
|
}
|
|
std::shared_ptr<Chunk> Universe::createChunk(std::istream &str) const {
|
|
return std::make_shared<Chunk>(str);
|
|
} |