From 13e676a6e49128ebc6c63b8dd08bef51d360e8e9 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 9 Sep 2015 21:43:42 +0200 Subject: [PATCH] split chunk stuff storage, indexing, redering, loading, etc are now all separated so the different states can pull in what they need and it's more flexible and makes way for some optimizations as well --- src/ai/Spawner.cpp | 33 +- src/app/PreloadState.cpp | 2 +- src/app/Runtime.hpp | 2 + src/app/ServerState.cpp | 7 +- src/app/ServerState.hpp | 6 + src/app/UnloadState.cpp | 22 +- src/app/UnloadState.hpp | 8 +- src/app/WorldState.cpp | 28 +- src/app/WorldState.hpp | 7 +- src/app/runtime.cpp | 10 +- src/client/InteractiveState.hpp | 2 +- src/client/client.cpp | 24 +- src/io/WorldSave.cpp | 74 +++- src/io/WorldSave.hpp | 6 +- src/net/net.cpp | 2 +- src/ui/Interface.hpp | 7 +- src/ui/ui.cpp | 8 +- src/world/Chunk.hpp | 8 +- src/world/ChunkIndex.hpp | 70 ++++ src/world/ChunkLoader.hpp | 55 +-- src/world/ChunkRenderer.hpp | 34 +- src/world/ChunkStore.hpp | 61 +++ src/world/Generator.cpp | 6 +- src/world/Player.hpp | 21 ++ src/world/World.cpp | 90 ++--- src/world/World.hpp | 27 +- src/world/chunk.cpp | 632 +++++++++++++++++++------------- src/world/render.cpp | 174 --------- tst/world/ChunkTest.cpp | 13 +- 29 files changed, 786 insertions(+), 653 deletions(-) create mode 100644 src/world/ChunkIndex.hpp create mode 100644 src/world/ChunkStore.hpp create mode 100644 src/world/Player.hpp delete mode 100644 src/world/render.cpp diff --git a/src/ai/Spawner.cpp b/src/ai/Spawner.cpp index 8ba32ff..13a8f52 100644 --- a/src/ai/Spawner.cpp +++ b/src/ai/Spawner.cpp @@ -6,6 +6,7 @@ #include "../model/Skeletons.hpp" #include "../world/BlockLookup.hpp" #include "../world/BlockType.hpp" +#include "../world/ChunkIndex.hpp" #include "../world/Entity.hpp" #include "../world/World.hpp" @@ -51,11 +52,12 @@ void Spawner::CheckDespawn() noexcept { if (e.Dead()) { delete *iter; iter = controllers.erase(iter); + end = controllers.end(); continue; } bool safe = false; - for (const Entity *ref : refs) { - glm::vec3 diff(ref->AbsoluteDifference(e)); + for (const Player &ref : refs) { + glm::vec3 diff(ref.entity->AbsoluteDifference(e)); if (dot(diff, diff) < despawn_range) { safe = true; break; @@ -65,6 +67,7 @@ void Spawner::CheckDespawn() noexcept { e.Kill(); delete *iter; iter = controllers.erase(iter); + end = controllers.end(); } else { ++iter; } @@ -77,13 +80,11 @@ void Spawner::TrySpawn() { // select random player to punish auto &players = world.Players(); if (players.size() == 0) return; - Entity &player = *players[random.Next() % players.size()]; + const Player &player = players[random.Next() % players.size()]; - glm::ivec3 chunk( - (random.Next() % (chunk_range * 2 + 1)) - chunk_range, - (random.Next() % (chunk_range * 2 + 1)) - chunk_range, - (random.Next() % (chunk_range * 2 + 1)) - chunk_range - ); + int index = random.Next() % player.chunks->TotalChunks(); + + glm::ivec3 chunk(player.chunks->PositionOf(index)); glm::ivec3 pos( random.Next() % Chunk::width, @@ -92,16 +93,14 @@ void Spawner::TrySpawn() { ); // distance check - glm::vec3 diff(glm::vec3(chunk * Chunk::Extent() - pos) + player.Position()); - float dist = dot(diff, diff); - if (dist > despawn_range || dist < spawn_distance) { - return; - } + //glm::vec3 diff(glm::vec3(chunk * Chunk::Extent() - pos) + player.entity->Position()); + //float dist = dot(diff, diff); + //if (dist > despawn_range || dist < spawn_distance) { + // return; + //} // check if the spawn block and the one above it are loaded and inhabitable - BlockLookup spawn_block( - world.Loader().Loaded(player.ChunkCoords()), - chunk * Chunk::Extent() + pos); + BlockLookup spawn_block((*player.chunks)[index], pos); if (!spawn_block || spawn_block.GetType().collide_block) { return; } @@ -111,7 +110,7 @@ void Spawner::TrySpawn() { return; } - Spawn(player, player.ChunkCoords() + chunk, glm::vec3(pos) + glm::vec3(0.5f)); + Spawn(*player.entity, chunk, glm::vec3(pos) + glm::vec3(0.5f)); } void Spawner::Spawn(Entity &reference, const glm::ivec3 &chunk, const glm::vec3 &pos) { diff --git a/src/app/PreloadState.cpp b/src/app/PreloadState.cpp index 3b68610..7d5f372 100644 --- a/src/app/PreloadState.cpp +++ b/src/app/PreloadState.cpp @@ -27,7 +27,7 @@ void PreloadState::Handle(const SDL_Event &e) { void PreloadState::Update(int dt) { loader.LoadN(per_update); - if (loader.ToLoad() == 0) { + if (loader.ToLoad() <= 0) { env.state.Pop(); render.Update(render.MissingChunks()); } else { diff --git a/src/app/Runtime.hpp b/src/app/Runtime.hpp index 10a0e55..65ad408 100644 --- a/src/app/Runtime.hpp +++ b/src/app/Runtime.hpp @@ -5,6 +5,7 @@ #include "../net/Client.hpp" #include "../net/Server.hpp" #include "../ui/Interface.hpp" +#include "../world/Generator.hpp" #include "../world/World.hpp" #include @@ -44,6 +45,7 @@ public: int multisampling = 1; Client::Config client = Client::Config(); + Generator::Config gen = Generator::Config(); HeadlessEnvironment::Config env = HeadlessEnvironment::Config(); Interface::Config interface = Interface::Config(); Server::Config server = Server::Config(); diff --git a/src/app/ServerState.cpp b/src/app/ServerState.cpp index c2163ad..e839841 100644 --- a/src/app/ServerState.cpp +++ b/src/app/ServerState.cpp @@ -11,15 +11,18 @@ namespace blank { ServerState::ServerState( HeadlessEnvironment &env, + const Generator::Config &gc, const World::Config &wc, const WorldSave &ws, const Server::Config &sc ) : env(env) , block_types() -, world(block_types, wc, ws) +, world(block_types, wc) +, generator(gc) +, chunk_loader(world.Chunks(), generator, ws) , skeletons() -, spawner(world, skeletons, wc.gen.seed) +, spawner(world, skeletons, gc.seed) , server(sc, world) , push_timer(16) { TextureIndex tex_index; diff --git a/src/app/ServerState.hpp b/src/app/ServerState.hpp index aa7e779..0deb65a 100644 --- a/src/app/ServerState.hpp +++ b/src/app/ServerState.hpp @@ -7,12 +7,15 @@ #include "../model/Skeletons.hpp" #include "../net/Server.hpp" #include "../world/BlockTypeRegistry.hpp" +#include "../world/ChunkLoader.hpp" +#include "../world/Generator.hpp" #include "../world/World.hpp" namespace blank { class HeadlessEnvironment; +class WorldSave; class ServerState : public State { @@ -20,6 +23,7 @@ class ServerState public: ServerState( HeadlessEnvironment &, + const Generator::Config &, const World::Config &, const WorldSave &, const Server::Config & @@ -33,6 +37,8 @@ private: HeadlessEnvironment &env; BlockTypeRegistry block_types; World world; + Generator generator; + ChunkLoader chunk_loader; Skeletons skeletons; Spawner spawner; Server server; diff --git a/src/app/UnloadState.cpp b/src/app/UnloadState.cpp index 534a14a..43aae05 100644 --- a/src/app/UnloadState.cpp +++ b/src/app/UnloadState.cpp @@ -7,14 +7,18 @@ namespace blank { -UnloadState::UnloadState(Environment &env, ChunkLoader &loader) +UnloadState::UnloadState( + Environment &env, + ChunkStore &chunks, + const WorldSave &save) : env(env) -, loader(loader) +, chunks(chunks) +, save(save) , progress(env.assets.large_ui_font) -, cur(loader.Loaded().begin()) -, end(loader.Loaded().end()) +, cur(chunks.begin()) +, end(chunks.end()) , done(0) -, total(loader.Loaded().size()) +, total(chunks.NumLoaded()) , per_update(64) { progress.Position(glm::vec3(0.0f), Gravity::CENTER); progress.Template("Unloading chunks: %d/%d (%d%%)"); @@ -22,10 +26,10 @@ UnloadState::UnloadState(Environment &env, ChunkLoader &loader) void UnloadState::OnResume() { - cur = loader.Loaded().begin(); - end = loader.Loaded().end(); + cur = chunks.begin(); + end = chunks.end(); done = 0; - total = loader.Loaded().size(); + total = chunks.NumLoaded(); } @@ -36,7 +40,7 @@ void UnloadState::Handle(const SDL_Event &) { void UnloadState::Update(int dt) { for (std::size_t i = 0; i < per_update && cur != end; ++i, ++cur, ++done) { if (cur->ShouldUpdateSave()) { - loader.SaveFile().Write(*cur); + save.Write(*cur); } } if (cur == end) { diff --git a/src/app/UnloadState.hpp b/src/app/UnloadState.hpp index 877f8fb..ba7e809 100644 --- a/src/app/UnloadState.hpp +++ b/src/app/UnloadState.hpp @@ -12,14 +12,15 @@ namespace blank { class Chunk; -class ChunkLoader; +class ChunkStore; class Environment; +class WorldSave; class UnloadState : public State { public: - UnloadState(Environment &, ChunkLoader &); + UnloadState(Environment &, ChunkStore &, const WorldSave &); void OnResume(); @@ -29,7 +30,8 @@ public: private: Environment &env; - ChunkLoader &loader; + ChunkStore &chunks; + const WorldSave &save; Progress progress; std::list::iterator cur; std::list::iterator end; diff --git a/src/app/WorldState.cpp b/src/app/WorldState.cpp index 8c1e93d..5bb01bf 100644 --- a/src/app/WorldState.cpp +++ b/src/app/WorldState.cpp @@ -11,19 +11,22 @@ namespace blank { WorldState::WorldState( Environment &env, + const Generator::Config &gc, const Interface::Config &ic, const World::Config &wc, const WorldSave &save ) : env(env) , block_types() -, world(block_types, wc, save) -, chunk_renderer(world, wc.load.load_dist) +, world(block_types, wc) +, interface(ic, env, world, world.AddPlayer(ic.player_name)) +, generator(gc) +, chunk_loader(world.Chunks(), generator, save) +, chunk_renderer(*interface.GetPlayer().chunks) , skeletons() -, spawner(world, skeletons, wc.gen.seed) -, interface(ic, env, world, *world.AddPlayer(ic.player_name)) -, preload(env, world.Loader(), chunk_renderer) -, unload(env, world.Loader()) { +, spawner(world, skeletons, gc.seed) +, preload(env, chunk_loader, chunk_renderer) +, unload(env, world.Chunks(), save) { TextureIndex tex_index; env.loader.LoadBlockTypes("default", block_types, tex_index); chunk_renderer.LoadTextures(env.loader, tex_index); @@ -72,19 +75,22 @@ void WorldState::Update(int dt) { interface.Update(dt); spawner.Update(dt); world.Update(dt); - chunk_renderer.Rebase(interface.Player().ChunkCoords()); + chunk_loader.Update(dt); chunk_renderer.Update(dt); - glm::mat4 trans = interface.Player().Transform(interface.Player().ChunkCoords()); + Entity &player = *interface.GetPlayer().entity; + + glm::mat4 trans = player.Transform(player.ChunkCoords()); glm::vec3 dir(trans * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)); glm::vec3 up(trans * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)); - env.audio.Position(interface.Player().Position()); - env.audio.Velocity(interface.Player().Velocity()); + env.audio.Position(player.Position()); + env.audio.Velocity(player.Velocity()); env.audio.Orientation(dir, up); } void WorldState::Render(Viewport &viewport) { - viewport.WorldPosition(interface.Player().Transform(interface.Player().ChunkCoords())); + Entity &player = *interface.GetPlayer().entity; + viewport.WorldPosition(player.Transform(player.ChunkCoords())); chunk_renderer.Render(viewport); world.Render(viewport); interface.Render(viewport); diff --git a/src/app/WorldState.hpp b/src/app/WorldState.hpp index c0b4ce1..d5eac19 100644 --- a/src/app/WorldState.hpp +++ b/src/app/WorldState.hpp @@ -8,7 +8,9 @@ #include "../model/Skeletons.hpp" #include "../ui/Interface.hpp" #include "../world/BlockTypeRegistry.hpp" +#include "../world/ChunkLoader.hpp" #include "../world/ChunkRenderer.hpp" +#include "../world/Generator.hpp" #include "../world/World.hpp" @@ -22,6 +24,7 @@ class WorldState public: WorldState( Environment &, + const Generator::Config &, const Interface::Config &, const World::Config &, const WorldSave & @@ -40,10 +43,12 @@ private: Environment &env; BlockTypeRegistry block_types; World world; + Interface interface; + Generator generator; + ChunkLoader chunk_loader; ChunkRenderer chunk_renderer; Skeletons skeletons; Spawner spawner; - Interface interface; PreloadState preload; UnloadState unload; diff --git a/src/app/runtime.cpp b/src/app/runtime.cpp index e169a7b..8ce7b5d 100644 --- a/src/app/runtime.cpp +++ b/src/app/runtime.cpp @@ -220,7 +220,7 @@ void Runtime::ReadArgs(int argc, const char *const *argv) { cerr << "missing argument to -s" << endl; error = true; } else { - config.world.gen.seed = strtoul(argv[i], nullptr, 10); + config.gen.seed = strtoul(argv[i], nullptr, 10); } break; case 't': @@ -317,12 +317,14 @@ void Runtime::RunStandalone() { WorldSave save(config.env.GetWorldPath(config.world.name)); if (save.Exists()) { save.Read(config.world); + save.Read(config.gen); } else { save.Write(config.world); + save.Write(config.gen); } Application app(env); - WorldState world_state(env, config.interface, config.world, save); + WorldState world_state(env, config.gen, config.interface, config.world, save); app.PushState(&world_state); Run(app); } @@ -333,12 +335,14 @@ void Runtime::RunServer() { WorldSave save(config.env.GetWorldPath(config.world.name)); if (save.Exists()) { save.Read(config.world); + save.Read(config.gen); } else { save.Write(config.world); + save.Write(config.gen); } HeadlessApplication app(env); - ServerState server_state(env, config.world, save, config.server); + ServerState server_state(env, config.gen, config.world, save, config.server); app.PushState(&server_state); Run(app); } diff --git a/src/client/InteractiveState.hpp b/src/client/InteractiveState.hpp index 65c92d0..efaeae4 100644 --- a/src/client/InteractiveState.hpp +++ b/src/client/InteractiveState.hpp @@ -37,8 +37,8 @@ private: BlockTypeRegistry block_types; WorldSave save; World world; - ChunkRenderer chunk_renderer; Interface interface; + ChunkRenderer chunk_renderer; }; diff --git a/src/client/client.cpp b/src/client/client.cpp index 829cc6b..ba9fcc9 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -46,14 +46,14 @@ InteractiveState::InteractiveState(MasterState &master, uint32_t player_id) : master(master) , block_types() , save(master.GetEnv().config.GetWorldPath(master.GetWorldConf().name, master.GetClientConf().host)) -, world(block_types, master.GetWorldConf(), save) -, chunk_renderer(world, master.GetWorldConf().load.load_dist) +, world(block_types, master.GetWorldConf()) , interface( master.GetInterfaceConf(), master.GetEnv(), world, - *world.AddPlayer(master.GetInterfaceConf().player_name, player_id) -) { + world.AddPlayer(master.GetInterfaceConf().player_name, player_id) +) +, chunk_renderer(*interface.GetPlayer().chunks) { TextureIndex tex_index; master.GetEnv().loader.LoadBlockTypes("default", block_types, tex_index); chunk_renderer.LoadTextures(master.GetEnv().loader, tex_index); @@ -99,21 +99,23 @@ void InteractiveState::Update(int dt) { interface.Update(dt); world.Update(dt); - chunk_renderer.Rebase(interface.Player().ChunkCoords()); chunk_renderer.Update(dt); - master.GetClient().SendPlayerUpdate(interface.Player()); + Entity &player = *interface.GetPlayer().entity; - glm::mat4 trans = interface.Player().Transform(interface.Player().ChunkCoords()); + master.GetClient().SendPlayerUpdate(player); + + glm::mat4 trans = player.Transform(player.ChunkCoords()); glm::vec3 dir(trans * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f)); glm::vec3 up(trans * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f)); - master.GetEnv().audio.Position(interface.Player().Position()); - master.GetEnv().audio.Velocity(interface.Player().Velocity()); + master.GetEnv().audio.Position(player.Position()); + master.GetEnv().audio.Velocity(player.Velocity()); master.GetEnv().audio.Orientation(dir, up); } void InteractiveState::Render(Viewport &viewport) { - viewport.WorldPosition(interface.Player().Transform(interface.Player().ChunkCoords())); + Entity &player = *interface.GetPlayer().entity; + viewport.WorldPosition(player.Transform(player.ChunkCoords())); chunk_renderer.Render(viewport); world.Render(viewport); interface.Render(viewport); @@ -195,7 +197,7 @@ void MasterState::On(const Packet::Join &pack) { pack.ReadPlayerID(player_id); state.reset(new InteractiveState(*this, player_id)); - pack.ReadPlayer(state->GetInterface().Player()); + pack.ReadPlayer(*state->GetInterface().GetPlayer().entity); env.state.PopAfter(this); env.state.Push(state.get()); diff --git a/src/io/WorldSave.cpp b/src/io/WorldSave.cpp index bd041f2..5cfb7f9 100644 --- a/src/io/WorldSave.cpp +++ b/src/io/WorldSave.cpp @@ -17,7 +17,8 @@ namespace blank { WorldSave::WorldSave(const string &path) : root_path(path) -, conf_path(path + "world.conf") +, world_conf_path(path + "world.conf") +, gen_conf_path(path + "gen.conf") , chunk_path(path + "chunks/%d/%d/%d.gz") , chunk_bufsiz(chunk_path.length() + 3 * std::numeric_limits::digits10) , chunk_buf(new char[chunk_bufsiz]) { @@ -26,12 +27,13 @@ WorldSave::WorldSave(const string &path) bool WorldSave::Exists() const noexcept { - return is_dir(root_path) && is_file(conf_path); + return is_dir(root_path) && is_file(world_conf_path); } +// TODO: better implementation of config files void WorldSave::Read(World::Config &conf) const { - ifstream in(conf_path); + ifstream in(world_conf_path); if (!in) { throw runtime_error("failed to open world config"); } @@ -58,11 +60,11 @@ void WorldSave::Read(World::Config &conf) const { string name(line, name_begin, name_end - name_begin + 1); string value(line, value_begin, value_end - value_begin + 1); - if (name == "seed") { - conf.gen.seed = stoul(value); - } else { + // if (name == "seed") { + // conf.gen.seed = stoul(value); + // } else { throw runtime_error("unknown world option: " + name); - } + // } } if (in.bad()) { throw runtime_error("IO error reading world config"); @@ -74,8 +76,8 @@ void WorldSave::Write(const World::Config &conf) const { throw runtime_error("failed to create world save directory"); } - ofstream out(conf_path); - out << "seed = " << conf.gen.seed << endl; + ofstream out(world_conf_path); + //out << "seed = " << conf.gen.seed << endl; out.close(); if (!out) { @@ -84,6 +86,60 @@ void WorldSave::Write(const World::Config &conf) const { } +void WorldSave::Read(Generator::Config &conf) const { + ifstream in(gen_conf_path); + if (!in) { + throw runtime_error("failed to open generator config"); + } + + constexpr char spaces[] = "\n\r\t "; + + string line; + while (getline(in, line)) { + if (line.empty() || line[0] == '#') continue; + auto equals_pos = line.find_first_of('='); + + auto name_begin = line.find_first_not_of(spaces, 0, sizeof(spaces)); + auto name_end = equals_pos - 1; + while (name_end > name_begin && isspace(line[name_end])) { + --name_end; + } + + auto value_begin = line.find_first_not_of(spaces, equals_pos + 1, sizeof(spaces)); + auto value_end = line.length() - 1; + while (value_end > value_begin && isspace(line[value_end])) { + --value_end; + } + + string name(line, name_begin, name_end - name_begin + 1); + string value(line, value_begin, value_end - value_begin + 1); + + if (name == "seed") { + conf.seed = stoul(value); + } else { + throw runtime_error("unknown generator option: " + name); + } + } + if (in.bad()) { + throw runtime_error("IO error reading world config"); + } +} + +void WorldSave::Write(const Generator::Config &conf) const { + if (!make_dirs(root_path)) { + throw runtime_error("failed to create world save directory"); + } + + ofstream out(gen_conf_path); + out << "seed = " << conf.seed << endl; + out.close(); + + if (!out) { + throw runtime_error("failed to write generator config"); + } +} + + bool WorldSave::Exists(const Chunk::Pos &pos) const noexcept { return is_file(ChunkPath(pos)); } diff --git a/src/io/WorldSave.hpp b/src/io/WorldSave.hpp index 4095016..1796257 100644 --- a/src/io/WorldSave.hpp +++ b/src/io/WorldSave.hpp @@ -2,6 +2,7 @@ #define BLANK_IO_WORLDSAVE_HPP_ #include "../world/Chunk.hpp" +#include "../world/Generator.hpp" #include "../world/World.hpp" #include @@ -20,6 +21,8 @@ public: bool Exists() const noexcept; void Read(World::Config &) const; void Write(const World::Config &) const; + void Read(Generator::Config &) const; + void Write(const Generator::Config &) const; // single chunk bool Exists(const Chunk::Pos &) const noexcept; @@ -30,7 +33,8 @@ public: private: std::string root_path; - std::string conf_path; + std::string world_conf_path; + std::string gen_conf_path; std::string chunk_path; std::size_t chunk_bufsiz; std::unique_ptr chunk_buf; diff --git a/src/net/net.cpp b/src/net/net.cpp index d56e409..9fe282f 100644 --- a/src/net/net.cpp +++ b/src/net/net.cpp @@ -297,7 +297,7 @@ void ClientConnection::On(const Packet::Login &pack) { string name; pack.ReadPlayerName(name); - Entity *new_player = server.GetWorld().AddPlayer(name); + Entity *new_player = server.GetWorld().AddPlayer(name).entity; if (new_player) { // success! diff --git a/src/ui/Interface.hpp b/src/ui/Interface.hpp index 7c9a91e..72db5d1 100644 --- a/src/ui/Interface.hpp +++ b/src/ui/Interface.hpp @@ -11,6 +11,7 @@ #include "../model/OutlineModel.hpp" #include "../world/Block.hpp" #include "../world/EntityCollision.hpp" +#include "../world/Player.hpp" #include "../world/WorldCollision.hpp" #include @@ -41,10 +42,9 @@ public: bool visual_disabled = false; }; - Interface(const Config &, Environment &, World &, Entity &); + Interface(const Config &, Environment &, World &, const Player &); - Entity &Player() noexcept { return ctrl.Controlled(); } - const Entity &Player() const noexcept { return ctrl.Controlled(); } + const Player &GetPlayer() noexcept { return player; } void HandlePress(const SDL_KeyboardEvent &); void HandleRelease(const SDL_KeyboardEvent &); @@ -91,6 +91,7 @@ private: private: Environment &env; World &world; + Player player; FPSController ctrl; HUD hud; diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 2ec4d88..3994298 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -103,11 +103,11 @@ Interface::Interface( const Config &config, Environment &env, World &world, - Entity &player) + const Player &player) : env(env) , world(world) -// let's assume this succeeds and hope for the best for now -, ctrl(player) +, player(player) +, ctrl(*player.entity) , hud(world.BlockTypes(), env.assets.small_ui_font) , aim{{ 0, 0, 0 }, { 0, 0, -1 }} , aim_world() @@ -526,7 +526,7 @@ void Interface::UpdateOutline() { outl_buf.Clear(); aim_world.GetType().FillOutlineModel(outl_buf); outline.Update(outl_buf); - outline_transform = aim_world.GetChunk().Transform(Player().ChunkCoords()); + outline_transform = aim_world.GetChunk().Transform(player.entity->ChunkCoords()); outline_transform *= aim_world.BlockTransform(); outline_transform *= glm::scale(glm::vec3(1.005f)); } diff --git a/src/world/Chunk.hpp b/src/world/Chunk.hpp index 0440f9c..23ed869 100644 --- a/src/world/Chunk.hpp +++ b/src/world/Chunk.hpp @@ -99,11 +99,10 @@ public: bool IsSurface(const Block::Pos &pos) const noexcept { return IsSurface(Pos(pos)); } bool IsSurface(const Pos &pos) const noexcept; - void SetNeighbor(Chunk &) noexcept; + void SetNeighbor(Block::Face, Chunk &) noexcept; bool HasNeighbor(Block::Face f) const noexcept { return neighbor[f]; } Chunk &GetNeighbor(Block::Face f) noexcept { return *neighbor[f]; } const Chunk &GetNeighbor(Block::Face f) const noexcept { return *neighbor[f]; } - void ClearNeighbors() noexcept; void Unlink() noexcept; // check which faces of a block at given index are obstructed (and therefore invisible) @@ -159,6 +158,10 @@ public: const void *BlockData() const noexcept { return &blocks[0]; } static constexpr std::size_t BlockSize() noexcept { return sizeof(blocks) + sizeof(light); } + void Ref() noexcept { ++ref_count; } + void UnRef() noexcept { --ref_count; } + bool Referenced() const noexcept { return ref_count > 0; } + void Invalidate() noexcept { dirty_model = dirty_save = true; } void InvalidateModel() noexcept { dirty_model = true; } void ClearModel() noexcept { dirty_model = false; } @@ -174,6 +177,7 @@ private: Block blocks[size]; unsigned char light[size]; Pos position; + int ref_count; bool dirty_model; bool dirty_save; diff --git a/src/world/ChunkIndex.hpp b/src/world/ChunkIndex.hpp new file mode 100644 index 0000000..6749737 --- /dev/null +++ b/src/world/ChunkIndex.hpp @@ -0,0 +1,70 @@ +#ifndef BLANK_WORLD_CHUNKINDEX_HPP_ +#define BLANK_WORLD_CHUNKINDEX_HPP_ + +#include "Chunk.hpp" + +#include + + +namespace blank { + +class ChunkStore; + +class ChunkIndex { + +public: + ChunkIndex(ChunkStore &, const Chunk::Pos &base, int extent); + ~ChunkIndex(); + + ChunkIndex(const ChunkIndex &) = delete; + ChunkIndex &operator =(const ChunkIndex &) = delete; + +public: + bool InRange(const Chunk::Pos &) const noexcept; + int IndexOf(const Chunk::Pos &) const noexcept; + Chunk::Pos PositionOf(int) const noexcept; + /// returns nullptr if given position is out of range or the chunk + /// is not loaded, so also works as a "has" function + Chunk *Get(const Chunk::Pos &) noexcept; + const Chunk *Get(const Chunk::Pos &) const noexcept; + Chunk *operator [](int i) noexcept { return chunks[i]; } + const Chunk *operator [](int i) const noexcept { return chunks[i]; } + + void Register(Chunk &) noexcept; + + int TotalChunks() const noexcept { return total_length; } + int IndexedChunks() const noexcept { return total_indexed; } + int MissingChunks() const noexcept { return total_length - total_indexed; } + + Chunk::Pos NextMissing() noexcept; + + const Chunk::Pos &Base() const noexcept { return base; } + void Rebase(const Chunk::Pos &); + +private: + int GetCol(int) const noexcept; + + void Shift(Block::Face); + + void Clear() noexcept; + void Scan() noexcept; + + void Set(int index, Chunk &) noexcept; + void Unset(int index) noexcept; + +private: + ChunkStore &store; + Chunk::Pos base; + int extent; + int side_length; + int total_length; + int total_indexed; + int last_missing; + glm::ivec3 stride; + std::vector chunks; + +}; + +} + +#endif diff --git a/src/world/ChunkLoader.hpp b/src/world/ChunkLoader.hpp index 7cc8dfc..0198651 100644 --- a/src/world/ChunkLoader.hpp +++ b/src/world/ChunkLoader.hpp @@ -1,83 +1,40 @@ #ifndef BLANK_WORLD_CHUNKLOADER_HPP_ #define BLANK_WORLD_CHUNKLOADER_HPP_ -#include "Chunk.hpp" -#include "../app/IntervalTimer.hpp" - #include namespace blank { -class BlockTypeRegistry; +class ChunkStore; class Generator; class WorldSave; class ChunkLoader { public: - struct Config { - int load_dist = 6; - int unload_dist = 8; - int gen_limit = 16; - }; - ChunkLoader( - const Config &, - const BlockTypeRegistry &, + ChunkStore &, const Generator &, const WorldSave & ) noexcept; - void Queue(const Chunk::Pos &from, const Chunk::Pos &to); - void QueueSurrounding(const Chunk::Pos &); - - std::list &Loaded() noexcept { return loaded; } const WorldSave &SaveFile() const noexcept { return save; } - Chunk *Loaded(const Chunk::Pos &) noexcept; - bool Queued(const Chunk::Pos &) noexcept; - bool Known(const Chunk::Pos &) noexcept; - Chunk &ForceLoad(const Chunk::Pos &); - - bool OutOfRange(const Chunk &c) const noexcept { return OutOfRange(c.Position()); } - bool OutOfRange(const Chunk::Pos &) const noexcept; - - void Rebase(const Chunk::Pos &); void Update(int dt); - std::size_t ToLoad() const noexcept { return to_load.size(); } + int ToLoad() const noexcept; + // returns true if the chunk was generated + // (as opposed to loaded from file) bool LoadOne(); void LoadN(std::size_t n); private: - Chunk &Load(const Chunk::Pos &pos); - // link given chunk to all loaded neighbors - void Insert(Chunk &) noexcept; - // remove a loaded chunk - // this unlinks it from its neighbors as well as moves it to the free list - // given iterator must point to a chunk from the loaded list - // returns an iterator to the chunk following the removed one - // in the loaded list (end for the last one) - std::list::iterator Remove(std::list::iterator) noexcept; - -private: - Chunk::Pos base; - - const BlockTypeRegistry ® + ChunkStore &store; const Generator &gen; const WorldSave &save; - std::list loaded; - std::list to_load; - std::list to_free; - - IntervalTimer gen_timer; - - int load_dist; - int unload_dist; - }; } diff --git a/src/world/ChunkRenderer.hpp b/src/world/ChunkRenderer.hpp index fe4f6a6..d45d0fd 100644 --- a/src/world/ChunkRenderer.hpp +++ b/src/world/ChunkRenderer.hpp @@ -12,51 +12,31 @@ namespace blank { class AssetLoader; +class BlockModel; +class ChunkIndex; class TextureIndex; class Viewport; -class World; class ChunkRenderer { public: - /// render_distance in chunks, excluding the base chunk which is always rendered - ChunkRenderer(World &, int render_distance); + explicit ChunkRenderer(ChunkIndex &); + ~ChunkRenderer(); void LoadTextures(const AssetLoader &, const TextureIndex &); void FogDensity(float d) noexcept { fog_density = d; } - bool InRange(const Chunk::Pos &) const noexcept; - int IndexOf(const Chunk::Pos &) const noexcept; + int MissingChunks() const noexcept; - int TotalChunks() const noexcept { return total_length; } - int IndexedChunks() const noexcept { return total_indexed; } - int MissingChunks() const noexcept { return total_length - total_indexed; } - - void Rebase(const Chunk::Pos &); - void Rescan(); - void Scan(); void Update(int dt); void Render(Viewport &); private: - int GetCol(int) const noexcept; - - void Shift(Block::Face); - -private: - World &world; - ArrayTexture block_tex; - - int render_dist; - int side_length; - int total_length; - int total_indexed; - glm::ivec3 stride; + ChunkIndex &index; std::vector models; - std::vector chunks; - Chunk::Pos base; + ArrayTexture block_tex; float fog_density; diff --git a/src/world/ChunkStore.hpp b/src/world/ChunkStore.hpp new file mode 100644 index 0000000..68434bb --- /dev/null +++ b/src/world/ChunkStore.hpp @@ -0,0 +1,61 @@ +#ifndef BLANK_WORLD_CHUNKSTORE_HPP_ +#define BLANK_WORLD_CHUNKSTORE_HPP_ + +#include "Chunk.hpp" + +#include + + +namespace blank { + +class ChunkIndex; + +class ChunkStore { + +public: + ChunkStore(const BlockTypeRegistry &); + ~ChunkStore(); + + ChunkStore(const ChunkStore &) = delete; + ChunkStore &operator =(const ChunkStore &) = delete; + +public: + ChunkIndex &MakeIndex(const Chunk::Pos &base, int extent); + void UnregisterIndex(ChunkIndex &); + + /// returns nullptr if given position is not loaded + Chunk *Get(const Chunk::Pos &); + /// returns nullptr if given position is not indexed + Chunk *Allocate(const Chunk::Pos &); + + std::list::iterator begin() noexcept { return loaded.begin(); } + std::list::iterator end() noexcept { return loaded.end(); } + + std::size_t NumLoaded() const noexcept { return loaded.size(); } + + /// returns true if one of the indices is incomplete + bool HasMissing() const noexcept; + /// get the total number of missing chunks + /// this is an estimate and represents the upper bound since + /// chunks may be counted more than once if indices overlap + int EstimateMissing() const noexcept; + + /// get coordinates of a missing chunk + /// this will return garbage if none are actually missing + Chunk::Pos NextMissing() noexcept; + + void Clean(); + +private: + const BlockTypeRegistry &types; + + std::list loaded; + std::list free; + + std::list indices; + +}; + +} + +#endif diff --git a/src/world/Generator.cpp b/src/world/Generator.cpp index c8621e7..c37c18b 100644 --- a/src/world/Generator.cpp +++ b/src/world/Generator.cpp @@ -13,9 +13,10 @@ Generator::Generator(const Config &config) noexcept , typeNoise(config.seed) , stretch(1.0f/config.stretch) , solid_threshold(config.solid_threshold) +// TODO: stable dynamic generator configuration , space(0) -, light(0) -, solids() { +, light(13) +, solids({ 1, 4, 7, 10 }) { } @@ -47,7 +48,6 @@ void Generator::operator ()(Chunk &chunk) const noexcept { } } } - //chunk.CheckUpdate(); } } diff --git a/src/world/Player.hpp b/src/world/Player.hpp new file mode 100644 index 0000000..68aa352 --- /dev/null +++ b/src/world/Player.hpp @@ -0,0 +1,21 @@ +#ifndef BLANK_WORLD_PLAYER_HPP_ +#define BLANK_WORLD_PLAYER_HPP_ + +namespace blank { + +class ChunkIndex; +class Entity; + +struct Player { + + Entity *entity; + ChunkIndex *chunks; + + Player(Entity *e, ChunkIndex *i) + : entity(e), chunks(i) { } + +}; + +} + +#endif diff --git a/src/world/World.cpp b/src/world/World.cpp index 7b90954..8b43657 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -1,5 +1,6 @@ #include "World.hpp" +#include "ChunkIndex.hpp" #include "EntityCollision.hpp" #include "WorldCollision.hpp" #include "../app/Assets.hpp" @@ -14,56 +15,59 @@ namespace blank { -World::World(const BlockTypeRegistry &types, const Config &config, const WorldSave &save) +World::World(const BlockTypeRegistry &types, const Config &config) : config(config) , block_type(types) -, generate(config.gen) -, chunks(config.load, types, generate, save) +, chunks(types) +// TODO: set spawn base and extent from config +, spawn_index(chunks.MakeIndex(Chunk::Pos(0, 0, 0), 3)) , players() , entities() , light_direction(config.light_direction) , fog_density(config.fog_density) { - generate.Space(0); - generate.Light(13); - generate.Solids({ 1, 4, 7, 10 }); + +} + +World::~World() { + chunks.UnregisterIndex(spawn_index); } -Entity *World::AddPlayer(const std::string &name) { - for (Entity *e : players) { - if (e->Name() == name) { - return nullptr; +Player World::AddPlayer(const std::string &name) { + for (Player &p : players) { + if (p.entity->Name() == name) { + return { nullptr, nullptr }; } } - Entity &player = AddEntity(); - player.Name(name); + Entity &entity = AddEntity(); + entity.Name(name); // TODO: load from save file here - player.Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } }); - player.WorldCollidable(true); - player.Position(config.spawn); - players.push_back(&player); - chunks.QueueSurrounding(player.ChunkCoords()); - return &player; + entity.Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } }); + entity.WorldCollidable(true); + entity.Position(config.spawn); + ChunkIndex *index = &chunks.MakeIndex(entity.ChunkCoords(), 6); + players.emplace_back(&entity, index); + return players.back(); } -Entity *World::AddPlayer(const std::string &name, std::uint32_t id) { - for (Entity *e : players) { - if (e->Name() == name) { - return nullptr; +Player World::AddPlayer(const std::string &name, std::uint32_t id) { + for (Player &p : players) { + if (p.entity->Name() == name) { + return { nullptr, nullptr }; } } - Entity *player = AddEntity(id); - if (!player) { - return nullptr; + Entity *entity = AddEntity(id); + if (!entity) { + return { nullptr, nullptr }; } - player->Name(name); + entity->Name(name); // TODO: load from save file here - player->Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } }); - player->WorldCollidable(true); - player->Position(config.spawn); - players.push_back(player); - chunks.QueueSurrounding(player->ChunkCoords()); - return player; + entity->Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } }); + entity->WorldCollidable(true); + entity->Position(config.spawn); + ChunkIndex *index = &chunks.MakeIndex(entity->ChunkCoords(), 6); + players.emplace_back(entity, index); + return players.back(); } Entity &World::AddEntity() { @@ -134,7 +138,7 @@ bool World::Intersection( ) { candidates.clear(); - for (Chunk &cur_chunk : chunks.Loaded()) { + for (Chunk &cur_chunk : chunks) { float cur_dist; if (cur_chunk.Intersection(ray, M * cur_chunk.Transform(reference), cur_dist)) { candidates.push_back({ &cur_chunk, cur_dist }); @@ -194,7 +198,7 @@ bool World::Intersection(const Entity &e, std::vector &col) { Chunk::Pos reference = e.ChunkCoords(); glm::mat4 M = e.Transform(reference); bool any = false; - for (Chunk &cur_chunk : chunks.Loaded()) { + for (Chunk &cur_chunk : chunks) { if (manhattan_radius(cur_chunk.Position() - e.ChunkCoords()) > 1) { // chunk is not one of the 3x3x3 surrounding the entity // since there's no entity which can extent over 16 blocks, they can be skipped @@ -225,6 +229,9 @@ void World::Update(int dt) { Resolve(entity, col); } } + for (Player &player : players) { + player.chunks->Rebase(player.entity->ChunkCoords()); + } for (auto iter = entities.begin(), end = entities.end(); iter != end;) { if (iter->CanRemove()) { iter = RemoveEntity(iter); @@ -232,9 +239,6 @@ void World::Update(int dt) { ++iter; } } - // TODO: make flexible - chunks.Rebase(players[0]->ChunkCoords()); - chunks.Update(dt); } void World::Resolve(Entity &e, std::vector &col) { @@ -271,9 +275,13 @@ void World::Resolve(Entity &e, std::vector &col) { World::EntityHandle World::RemoveEntity(EntityHandle &eh) { // check for player - auto player = std::find(players.begin(), players.end(), &*eh); - if (player != players.end()) { - players.erase(player); + for (auto player = players.begin(), end = players.end(); player != end;) { + if (player->entity == &*eh) { + chunks.UnregisterIndex(*player->chunks); + player = players.erase(player); + } else { + ++player; + } } return entities.erase(eh); } @@ -285,7 +293,7 @@ void World::Render(Viewport &viewport) { entity_prog.SetFogDensity(fog_density); for (Entity &entity : entities) { - entity.Render(entity.ChunkTransform(players[0]->ChunkCoords()), entity_prog); + entity.Render(entity.ChunkTransform(players[0].entity->ChunkCoords()), entity_prog); } } diff --git a/src/world/World.hpp b/src/world/World.hpp index c605d77..8d82250 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -1,9 +1,10 @@ #ifndef BLANK_WORLD_WORLD_HPP_ #define BLANK_WORLD_WORLD_HPP_ -#include "ChunkLoader.hpp" +#include "ChunkStore.hpp" #include "Entity.hpp" #include "Generator.hpp" +#include "Player.hpp" #include #include @@ -33,12 +34,10 @@ public: // I chose 0.011 because it yields 91 and 124 for those, so // slightly less than 6 and 8 chunks float fog_density = 0.011f; - - Generator::Config gen = Generator::Config(); - ChunkLoader::Config load = ChunkLoader::Config(); }; - World(const BlockTypeRegistry &, const Config &, const WorldSave &); + World(const BlockTypeRegistry &, const Config &); + ~World(); const std::string &Name() const noexcept { return config.name; } @@ -66,21 +65,21 @@ public: void Resolve(Entity &e, std::vector &); const BlockTypeRegistry &BlockTypes() noexcept { return block_type; } - ChunkLoader &Loader() noexcept { return chunks; } + ChunkStore &Chunks() noexcept { return chunks; } /// add player with given name - /// returns nullptr if the name is already taken - Entity *AddPlayer(const std::string &name); + /// returns nullptr in entity if the name is already taken + Player AddPlayer(const std::string &name); /// add player with given name and ID - /// returns nullptr if the name or ID is already taken - Entity *AddPlayer(const std::string &name, std::uint32_t id); + /// returns nullptr in entity if the name or ID is already taken + Player AddPlayer(const std::string &name, std::uint32_t id); /// add an entity with an autogenerated ID Entity &AddEntity(); /// add entity with given ID /// returns nullptr if the ID is already taken Entity *AddEntity(std::uint32_t id); - const std::vector &Players() const noexcept { return players; } + const std::vector &Players() const noexcept { return players; } std::list &Entities() noexcept { return entities; } const std::list &Entities() const noexcept { return entities; } @@ -97,10 +96,10 @@ private: const BlockTypeRegistry &block_type; - Generator generate; - ChunkLoader chunks; + ChunkStore chunks; + ChunkIndex &spawn_index; - std::vector players; + std::vector players; std::list entities; glm::vec3 light_direction; diff --git a/src/world/chunk.cpp b/src/world/chunk.cpp index 11c9d36..1d1a118 100644 --- a/src/world/chunk.cpp +++ b/src/world/chunk.cpp @@ -1,10 +1,17 @@ #include "BlockLookup.hpp" #include "Chunk.hpp" +#include "ChunkIndex.hpp" #include "ChunkLoader.hpp" +#include "ChunkRenderer.hpp" +#include "ChunkStore.hpp" #include "Generator.hpp" #include "WorldCollision.hpp" +#include "../app/Assets.hpp" +#include "../graphics/BlockLighting.hpp" +#include "../graphics/Viewport.hpp" #include "../io/WorldSave.hpp" +#include "../model/BlockModel.hpp" #include #include @@ -26,6 +33,7 @@ Chunk::Chunk(const BlockTypeRegistry &types) noexcept , blocks{} , light{0} , position(0, 0, 0) +, ref_count(0) , dirty_model(false) , dirty_save(false) { @@ -212,11 +220,17 @@ void edge_light( } -void Chunk::SetNeighbor(Chunk &other) noexcept { - if (other.position == position + Pos(-1, 0, 0)) { - if (neighbor[Block::FACE_LEFT] != &other) { - neighbor[Block::FACE_LEFT] = &other; - other.neighbor[Block::FACE_RIGHT] = this; +void Chunk::SetNeighbor(Block::Face face, Chunk &other) noexcept { + neighbor[face] = &other; + other.neighbor[Block::Opposite(face)] = this; + + switch (face) { + + default: + // error + break; + + case Block::FACE_LEFT: for (int z = 0; z < depth; ++z) { for (int y = 0; y < height; ++y) { Pos my_pos(0, y, z); @@ -225,12 +239,9 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } - } else if (other.position == position + Pos(1, 0, 0)) { - if (neighbor[Block::FACE_RIGHT] != &other) { - neighbor[Block::FACE_RIGHT] = &other; - other.neighbor[Block::FACE_LEFT] = this; + break; + + case Block::FACE_RIGHT: for (int z = 0; z < depth; ++z) { for (int y = 0; y < height; ++y) { Pos my_pos(width - 1, y, z); @@ -239,12 +250,9 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } - } else if (other.position == position + Pos(0, -1, 0)) { - if (neighbor[Block::FACE_DOWN] != &other) { - neighbor[Block::FACE_DOWN] = &other; - other.neighbor[Block::FACE_UP] = this; + break; + + case Block::FACE_DOWN: for (int z = 0; z < depth; ++z) { for (int x = 0; x < width; ++x) { Pos my_pos(x, 0, z); @@ -253,12 +261,9 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } - } else if (other.position == position + Pos(0, 1, 0)) { - if (neighbor[Block::FACE_UP] != &other) { - neighbor[Block::FACE_UP] = &other; - other.neighbor[Block::FACE_DOWN] = this; + break; + + case Block::FACE_UP: for (int z = 0; z < depth; ++z) { for (int x = 0; x < width; ++x) { Pos my_pos(x, height - 1, z); @@ -267,12 +272,9 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } - } else if (other.position == position + Pos(0, 0, -1)) { - if (neighbor[Block::FACE_BACK] != &other) { - neighbor[Block::FACE_BACK] = &other; - other.neighbor[Block::FACE_FRONT] = this; + break; + + case Block::FACE_BACK: for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { Pos my_pos(x, y, 0); @@ -281,12 +283,9 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } - } else if (other.position == position + Pos(0, 0, 1)) { - if (neighbor[Block::FACE_FRONT] != &other) { - neighbor[Block::FACE_FRONT] = &other; - other.neighbor[Block::FACE_BACK] = this; + break; + + case Block::FACE_FRONT: for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { Pos my_pos(x, y, depth - 1); @@ -295,12 +294,13 @@ void Chunk::SetNeighbor(Chunk &other) noexcept { edge_light(other, other_pos, *this, my_pos); } } - work_light(); - } + break; + } + work_light(); } -void Chunk::ClearNeighbors() noexcept { +void Chunk::Unlink() noexcept { for (int face = 0; face < Block::FACE_COUNT; ++face) { if (neighbor[face]) { neighbor[face]->neighbor[Block::Opposite(Block::Face(face))] = nullptr; @@ -627,294 +627,416 @@ BlockLookup::BlockLookup(Chunk *c, const Chunk::Pos &p, Block::Face face) noexce ChunkLoader::ChunkLoader( - const Config &config, - const BlockTypeRegistry ®, + ChunkStore &store, const Generator &gen, const WorldSave &save ) noexcept -: base(0, 0, 0) -, reg(reg) +: store(store) , gen(gen) -, save(save) -, loaded() -, to_load() -, to_free() -, gen_timer(config.gen_limit) -, load_dist(config.load_dist) -, unload_dist(config.unload_dist) { - gen_timer.Start(); +, save(save) { + } -namespace { +void ChunkLoader::Update(int dt) { + // check if there's chunks waiting to be loaded + // load until one of load or generation limits was hit + constexpr int max_load = 10; + constexpr int max_gen = 1; + int loaded = 0; + int generated = 0; + while (loaded < max_load && generated < max_gen && store.HasMissing()) { + if (LoadOne()) { + ++generated; + } else { + ++loaded; + } + } + + // store a few chunks as well + constexpr int max_save = 10; + int saved = 0; + for (Chunk &chunk : store) { + if (chunk.ShouldUpdateSave()) { + save.Write(chunk); + ++saved; + if (saved >= max_save) { + break; + } + } + } +} -struct ChunkLess { +int ChunkLoader::ToLoad() const noexcept { + return store.EstimateMissing(); +} - explicit ChunkLess(const Chunk::Pos &base) noexcept - : base(base) { } +bool ChunkLoader::LoadOne() { + if (!store.HasMissing()) return false; - bool operator ()(const Chunk::Pos &a, const Chunk::Pos &b) const noexcept { - Chunk::Pos da(base - a); - Chunk::Pos db(base - b); - return - da.x * da.x + da.y * da.y + da.z * da.z < - db.x * db.x + db.y * db.y + db.z * db.z; + Chunk::Pos pos = store.NextMissing(); + Chunk *chunk = store.Allocate(pos); + if (!chunk) { + // chunk store corrupted? + return false; } - Chunk::Pos base; + if (save.Exists(pos)) { + save.Read(*chunk); + return false; + } else { + gen(*chunk); + return true; + } +} + +void ChunkLoader::LoadN(std::size_t n) { + std::size_t end = std::min(n, std::size_t(ToLoad())); + for (std::size_t i = 0; i < end && store.HasMissing(); ++i) { + LoadOne(); + } +} -}; + +ChunkRenderer::ChunkRenderer(ChunkIndex &index) +: index(index) +, models(index.TotalChunks()) +, block_tex() +, fog_density(0.0f) { } -void ChunkLoader::Queue(const Chunk::Pos &from, const Chunk::Pos &to) { - for (int z = from.z; z < to.z; ++z) { - for (int y = from.y; y < to.y; ++y) { - for (int x = from.x; x < to.x; ++x) { - Chunk::Pos pos(x, y, z); - if (Known(pos)) { - continue; - } else if (pos == base) { - Load(pos); - - // light testing - // for (int i = 0; i < 16; ++i) { - // for (int j = 0; j < 16; ++j) { - // loaded.back().SetBlock(Chunk::Pos{ i, j, 0 }, Block(1)); - // loaded.back().SetBlock(Chunk::Pos{ i, j, 15 }, Block(1)); - // loaded.back().SetBlock(Chunk::Pos{ 0, j, i }, Block(1)); - // loaded.back().SetBlock(Chunk::Pos{ 15, j, i }, Block(1)); - // } - // } - // loaded.back().SetBlock(Chunk::Pos{ 1, 0, 1 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 14, 0, 1 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 1, 0, 14 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 14, 0, 14 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 1, 15, 1 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 14, 15, 1 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 1, 15, 14 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 14, 15, 14 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 7, 7, 0 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 8, 7, 0 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 7, 8, 0 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 8, 8, 0 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 7, 7, 15 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 8, 7, 15 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 7, 8, 15 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 8, 8, 15 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 0, 7, 7 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 0, 7, 8 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 0, 8, 7 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 0, 8, 8 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 15, 7, 7 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 15, 7, 8 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 15, 8, 7 }, Block(13)); - // loaded.back().SetBlock(Chunk::Pos{ 15, 8, 8 }, Block(13)); - // loaded.back().Invalidate(); - // loaded.back().CheckUpdate(); - - // orientation testing - // for (int i = 0; i < Block::FACE_COUNT; ++i) { - // for (int j = 0; j < Block::TURN_COUNT; ++j) { - // loaded.back().BlockAt(512 * j + 2 * i) = Block(3 * (j + 1), Block::Face(i), Block::Turn(j)); - // } - // } - // loaded.back().Invalidate(); - // loaded.back().CheckUpdate(); - } else { - to_load.emplace_back(pos); - } +ChunkRenderer::~ChunkRenderer() { + +} + +int ChunkRenderer::MissingChunks() const noexcept { + return index.MissingChunks(); +} + +void ChunkRenderer::LoadTextures(const AssetLoader &loader, const TextureIndex &tex_index) { + block_tex.Bind(); + loader.LoadTextures(tex_index, block_tex); + block_tex.FilterNearest(); +} + +void ChunkRenderer::Update(int dt) { + for (int i = 0, updates = 0; updates < dt && i < index.TotalChunks(); ++i) { + if (index[i] && index[i]->ShouldUpdateModel()) { + index[i]->Update(models[i]); + ++updates; + } + } +} + +void ChunkRenderer::Render(Viewport &viewport) { + BlockLighting &chunk_prog = viewport.ChunkProgram(); + chunk_prog.SetTexture(block_tex); + chunk_prog.SetFogDensity(fog_density); + + for (int i = 0; i < index.TotalChunks(); ++i) { + if (!index[i]) continue; + glm::mat4 m(index[i]->Transform(index.Base())); + glm::mat4 mvp(chunk_prog.GetVP() * m); + if (!CullTest(Chunk::Bounds(), mvp)) { + if (index[i]->ShouldUpdateModel()) { + index[i]->Update(models[i]); } + chunk_prog.SetM(m); + models[i].Draw(); } } - to_load.sort(ChunkLess(base)); } -Chunk &ChunkLoader::Load(const Chunk::Pos &pos) { - loaded.emplace_back(reg); - Chunk &chunk = loaded.back(); - chunk.Position(pos); - if (save.Exists(pos)) { - save.Read(chunk); + +ChunkIndex::ChunkIndex(ChunkStore &store, const Chunk::Pos &base, int extent) +: store(store) +, base(base) +, extent(extent) +, side_length(2 * extent + 1) +, total_length(side_length * side_length * side_length) +, total_indexed(0) +, last_missing(0) +, stride(1, side_length, side_length * side_length) +, chunks(total_length, nullptr) { + Scan(); +} + +ChunkIndex::~ChunkIndex() { + Clear(); +} + +bool ChunkIndex::InRange(const Chunk::Pos &pos) const noexcept { + return manhattan_radius(pos - base) <= extent; +} + +int ChunkIndex::IndexOf(const Chunk::Pos &pos) const noexcept { + Chunk::Pos mod_pos( + GetCol(pos.x), + GetCol(pos.y), + GetCol(pos.z) + ); + return mod_pos.x * stride.x + + mod_pos.y * stride.y + + mod_pos.z * stride.z; +} + +Chunk::Pos ChunkIndex::PositionOf(int i) const noexcept { + Chunk::Pos zero_pos( + (i / stride.x) % side_length, + (i / stride.y) % side_length, + (i / stride.z) % side_length + ); + Chunk::Pos zero_base( + GetCol(base.x), + GetCol(base.y), + GetCol(base.z) + ); + Chunk::Pos base_relative(zero_pos - zero_base); + if (base_relative.x > extent) base_relative.x -= side_length; + else if (base_relative.x < -extent) base_relative.x += side_length; + if (base_relative.y > extent) base_relative.y -= side_length; + else if (base_relative.y < -extent) base_relative.y += side_length; + if (base_relative.z > extent) base_relative.z -= side_length; + else if (base_relative.z < -extent) base_relative.z += side_length; + return base + base_relative; +} + +Chunk *ChunkIndex::Get(const Chunk::Pos &pos) noexcept { + if (InRange(pos)) { + return chunks[IndexOf(pos)]; } else { - gen(chunk); + return nullptr; } - Insert(chunk); - return chunk; } -void ChunkLoader::Insert(Chunk &chunk) noexcept { - for (Chunk &other : loaded) { - chunk.SetNeighbor(other); +const Chunk *ChunkIndex::Get(const Chunk::Pos &pos) const noexcept { + if (InRange(pos)) { + return chunks[IndexOf(pos)]; + } else { + return nullptr; } } -std::list::iterator ChunkLoader::Remove(std::list::iterator chunk) noexcept { - // fetch next entry while chunk's still in the list - std::list::iterator next = chunk; - ++next; - // unlink neighbors so they won't reference a dead chunk - chunk->ClearNeighbors(); - // if it should be saved, do it now - if (chunk->ShouldUpdateSave()) { - save.Write(*chunk); +void ChunkIndex::Rebase(const Chunk::Pos &new_base) { + if (new_base == base) return; + + Chunk::Pos diff(new_base - base); + + if (manhattan_radius(diff) > extent) { + // that's more than half, so probably not worth shifting + base = new_base; + Clear(); + Scan(); + store.Clean(); + return; + } + + while (diff.x > 0) { + Shift(Block::FACE_RIGHT); + --diff.x; + } + while (diff.x < 0) { + Shift(Block::FACE_LEFT); + ++diff.x; + } + while (diff.y > 0) { + Shift(Block::FACE_UP); + --diff.y; } - // and move it from loaded to free list - to_free.splice(to_free.end(), loaded, chunk); - return next; + while (diff.y < 0) { + Shift(Block::FACE_DOWN); + ++diff.y; + } + while (diff.z > 0) { + Shift(Block::FACE_FRONT); + --diff.z; + } + while (diff.z < 0) { + Shift(Block::FACE_BACK); + ++diff.z; + } + store.Clean(); +} + +int ChunkIndex::GetCol(int c) const noexcept { + c %= side_length; + if (c < 0) c += side_length; + return c; } -Chunk *ChunkLoader::Loaded(const Chunk::Pos &pos) noexcept { - for (Chunk &chunk : loaded) { - if (chunk.Position() == pos) { - return &chunk; +void ChunkIndex::Shift(Block::Face f) { + int a_axis = Block::Axis(f); + int b_axis = (a_axis + 1) % 3; + int c_axis = (a_axis + 2) % 3; + int dir = Block::Direction(f); + base[a_axis] += dir; + int a = GetCol(base[a_axis] + (extent * dir)); + int a_stride = a * stride[a_axis]; + for (int b = 0; b < side_length; ++b) { + int b_stride = b * stride[b_axis]; + for (int c = 0; c < side_length; ++c) { + int bc_stride = b_stride + c * stride[c_axis]; + int index = a_stride + bc_stride; + Unset(index); + int neighbor = ((a - dir + side_length) % side_length) * stride[a_axis] + bc_stride; + if (chunks[neighbor] && chunks[neighbor]->HasNeighbor(f)) { + Set(index, chunks[neighbor]->GetNeighbor(f)); + } } } - return nullptr; } -bool ChunkLoader::Queued(const Chunk::Pos &pos) noexcept { - for (const Chunk::Pos &chunk : to_load) { - if (chunk == pos) { - return true; - } +void ChunkIndex::Clear() noexcept { + for (int i = 0; i < total_length && total_indexed > 0; ++i) { + Unset(i); } - return false; } -bool ChunkLoader::Known(const Chunk::Pos &pos) noexcept { - if (Loaded(pos)) return true; - return Queued(pos); +void ChunkIndex::Scan() noexcept { + for (Chunk &chunk : store) { + Register(chunk); + } } -Chunk &ChunkLoader::ForceLoad(const Chunk::Pos &pos) { - Chunk *chunk = Loaded(pos); - if (chunk) { - return *chunk; +void ChunkIndex::Register(Chunk &chunk) noexcept { + if (InRange(chunk.Position())) { + Set(IndexOf(chunk.Position()), chunk); + } +} + +void ChunkIndex::Set(int index, Chunk &chunk) noexcept { + Unset(index); + chunks[index] = &chunk; + chunk.Ref(); + ++total_indexed; +} + +void ChunkIndex::Unset(int index) noexcept { + if (chunks[index]) { + chunks[index]->UnRef(); + chunks[index] = nullptr; + --total_indexed; } +} - for (auto iter(to_load.begin()), end(to_load.end()); iter != end; ++iter) { - if (*iter == pos) { - to_load.erase(iter); +Chunk::Pos ChunkIndex::NextMissing() noexcept { + int roundtrip = last_missing; + while (chunks[last_missing]) { + ++last_missing; + last_missing %= total_length; + if (last_missing == roundtrip) { break; } } + return PositionOf(last_missing); +} + + +ChunkStore::ChunkStore(const BlockTypeRegistry &types) +: types(types) +, loaded() +, free() +, indices() { - return Load(pos); } -bool ChunkLoader::OutOfRange(const Chunk::Pos &pos) const noexcept { - return std::abs(base.x - pos.x) > unload_dist - || std::abs(base.y - pos.y) > unload_dist - || std::abs(base.z - pos.z) > unload_dist; +ChunkStore::~ChunkStore() { + } -void ChunkLoader::Rebase(const Chunk::Pos &new_base) { - if (new_base == base) { - return; - } - base = new_base; +ChunkIndex &ChunkStore::MakeIndex(const Chunk::Pos &pos, int extent) { + indices.emplace_back(*this, pos, extent); + return indices.back(); +} - // unload far away chunks - for (auto iter(loaded.begin()), end(loaded.end()); iter != end;) { - if (OutOfRange(*iter)) { - iter = Remove(iter); - } else { - ++iter; - } - } - // abort far away queued chunks - for (auto iter(to_load.begin()), end(to_load.end()); iter != end;) { - if (OutOfRange(*iter)) { - iter = to_load.erase(iter); +void ChunkStore::UnregisterIndex(ChunkIndex &index) { + for (auto i = indices.begin(), end = indices.end(); i != end; ++i) { + if (&*i == &index) { + indices.erase(i); + return; } else { - ++iter; + ++i; } } - // add missing new chunks - QueueSurrounding(base); } -void ChunkLoader::QueueSurrounding(const Chunk::Pos &pos) { - const Chunk::Pos offset(load_dist, load_dist, load_dist); - Queue(pos - offset, pos + offset); +Chunk *ChunkStore::Get(const Chunk::Pos &pos) { + for (ChunkIndex &index : indices) { + Chunk *chunk = index.Get(pos); + if (chunk) { + return chunk; + } + } + return nullptr; } -void ChunkLoader::Update(int dt) { - // check if a chunk load is scheduled for this frame - // and if there's chunks waiting to be loaded - gen_timer.Update(dt); - if (gen_timer.Hit()) { - // we may - // load until one of load or generation limits was hit - constexpr int max_load = 10; - constexpr int max_gen = 1; - int loaded = 0; - int generated = 0; - while (!to_load.empty() && loaded < max_load && generated < max_gen) { - if (LoadOne()) { - ++generated; - } else { - ++loaded; - } +Chunk *ChunkStore::Allocate(const Chunk::Pos &pos) { + Chunk *chunk = Get(pos); + if (chunk) { + return chunk; + } + if (free.empty()) { + loaded.emplace(loaded.begin(), types); + } else { + loaded.splice(loaded.begin(), free, free.begin()); + loaded.front().Unlink(); + } + chunk = &loaded.front(); + chunk->Position(pos); + for (ChunkIndex &index : indices) { + if (index.InRange(pos)) { + index.Register(*chunk); } } - - constexpr int max_save = 10; - int saved = 0; - for (Chunk &chunk : loaded) { - if (chunk.ShouldUpdateSave()) { - save.Write(chunk); - ++saved; - if (saved >= max_save) { - break; - } + for (int i = 0; i < Block::FACE_COUNT; ++i) { + Block::Face face = Block::Face(i); + Chunk::Pos neighbor_pos(pos + Block::FaceNormal(face)); + Chunk *neighbor = Get(neighbor_pos); + if (neighbor) { + chunk->SetNeighbor(face, *neighbor); } } + return chunk; } -void ChunkLoader::LoadN(std::size_t n) { - std::size_t end = std::min(n, ToLoad()); - for (std::size_t i = 0; i < end; ++i) { - LoadOne(); +bool ChunkStore::HasMissing() const noexcept { + for (const ChunkIndex &index : indices) { + if (index.MissingChunks() > 0) { + return true; + } } + return false; } -bool ChunkLoader::LoadOne() { - if (to_load.empty()) return false; - - // take position of next chunk in queue - Chunk::Pos pos(to_load.front()); - to_load.pop_front(); - - // look if the same chunk was already generated and still lingering - for (auto iter(to_free.begin()), end(to_free.end()); iter != end; ++iter) { - if (iter->Position() == pos) { - loaded.splice(loaded.end(), to_free, iter); - Insert(loaded.back()); - return false; - } +int ChunkStore::EstimateMissing() const noexcept { + int missing = 0; + for (const ChunkIndex &index : indices) { + missing += index.MissingChunks(); } + return missing; +} - // if the free list is empty, allocate a new chunk - // otherwise clear an unused one - if (to_free.empty()) { - loaded.emplace_back(reg); - } else { - to_free.front().ClearNeighbors(); - loaded.splice(loaded.end(), to_free, to_free.begin()); +Chunk::Pos ChunkStore::NextMissing() noexcept { + for (ChunkIndex &index : indices) { + if (index.MissingChunks()) { + return index.NextMissing(); + } } + return Chunk::Pos(0, 0, 0); +} - bool generated = false; - Chunk &chunk = loaded.back(); - chunk.Position(pos); - if (save.Exists(pos)) { - save.Read(chunk); - } else { - gen(chunk); - generated = true; +void ChunkStore::Clean() { + for (auto i = loaded.begin(), end = loaded.end(); i != end;) { + if (i->Referenced()) { + ++i; + } else { + auto chunk = i; + ++i; + free.splice(free.end(), loaded, chunk); + chunk->Unlink(); + chunk->InvalidateModel(); + } } - Insert(chunk); - return generated; } } diff --git a/src/world/render.cpp b/src/world/render.cpp deleted file mode 100644 index 4a237e7..0000000 --- a/src/world/render.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#include "ChunkRenderer.hpp" - -#include "World.hpp" -#include "../app/Assets.hpp" -#include "../graphics/BlockLighting.hpp" -#include "../graphics/Viewport.hpp" - - -namespace blank { - -ChunkRenderer::ChunkRenderer(World &world, int rd) -: world(world) -, block_tex() -, render_dist(rd) -, side_length(2 * rd + 1) -, total_length(side_length * side_length * side_length) -, total_indexed(0) -, stride(1, side_length, side_length * side_length) -, models(total_length) -, chunks(total_length) -, base(0, 0, 0) -, fog_density(0.0f) { - -} - - -void ChunkRenderer::LoadTextures(const AssetLoader &loader, const TextureIndex &tex_index) { - block_tex.Bind(); - loader.LoadTextures(tex_index, block_tex); - block_tex.FilterNearest(); -} - - -bool ChunkRenderer::InRange(const Chunk::Pos &pos) const noexcept { - return manhattan_radius(pos - base) <= render_dist; -} - -int ChunkRenderer::IndexOf(const Chunk::Pos &pos) const noexcept { - Chunk::Pos mod_pos( - GetCol(pos.x), - GetCol(pos.y), - GetCol(pos.z) - ); - return mod_pos.x * stride.x - + mod_pos.y * stride.y - + mod_pos.z * stride.z; -} - - -void ChunkRenderer::Rebase(const Chunk::Pos &new_base) { - if (new_base == base) return; - - Chunk::Pos diff(new_base - base); - - if (manhattan_radius(diff) > render_dist) { - // that's more than half, so probably not worth shifting - base = new_base; - Rescan(); - return; - } - - while (diff.x > 0) { - Shift(Block::FACE_RIGHT); - --diff.x; - } - while (diff.x < 0) { - Shift(Block::FACE_LEFT); - ++diff.x; - } - while (diff.y > 0) { - Shift(Block::FACE_UP); - --diff.y; - } - while (diff.y < 0) { - Shift(Block::FACE_DOWN); - ++diff.y; - } - while (diff.z > 0) { - Shift(Block::FACE_FRONT); - --diff.z; - } - while (diff.z < 0) { - Shift(Block::FACE_BACK); - ++diff.z; - } -} - -int ChunkRenderer::GetCol(int c) const noexcept { - c %= side_length; - if (c < 0) c += side_length; - return c; -} - -void ChunkRenderer::Shift(Block::Face f) { - int a_axis = Block::Axis(f); - int b_axis = (a_axis + 1) % 3; - int c_axis = (a_axis + 2) % 3; - int dir = Block::Direction(f); - base[a_axis] += dir; - int a = GetCol(base[a_axis] + (render_dist * dir)); - int a_stride = a * stride[a_axis]; - for (int b = 0; b < side_length; ++b) { - int b_stride = b * stride[b_axis]; - for (int c = 0; c < side_length; ++c) { - int bc_stride = b_stride + c * stride[c_axis]; - int index = a_stride + bc_stride; - if (chunks[index]) { - chunks[index] = nullptr; - --total_indexed; - } - int neighbor = ((a - dir + side_length) % side_length) * stride[a_axis] + bc_stride; - if (chunks[neighbor] && chunks[neighbor]->HasNeighbor(f)) { - chunks[index] = &chunks[neighbor]->GetNeighbor(f); - chunks[index]->InvalidateModel(); - ++total_indexed; - } - } - } -} - - -void ChunkRenderer::Rescan() { - chunks.assign(total_length, nullptr); - total_indexed = 0; - Scan(); -} - -void ChunkRenderer::Scan() { - for (Chunk &chunk : world.Loader().Loaded()) { - if (!InRange(chunk.Position())) continue; - int index = IndexOf(chunk.Position()); - if (!chunks[index]) { - chunks[index] = &chunk; - chunk.InvalidateModel(); - ++total_indexed; - } - } -} - -void ChunkRenderer::Update(int dt) { - if (MissingChunks()) { - Scan(); - } - - // maximum of 1000 per second too high? - for (int i = 0, updates = 0; i < total_length && updates < dt; ++i) { - if (chunks[i] && chunks[i]->ShouldUpdateModel()) { - chunks[i]->Update(models[i]); - ++updates; - } - } -} - - -void ChunkRenderer::Render(Viewport &viewport) { - BlockLighting &chunk_prog = viewport.ChunkProgram(); - chunk_prog.SetTexture(block_tex); - chunk_prog.SetFogDensity(fog_density); - - for (int i = 0; i < total_length; ++i) { - if (!chunks[i]) continue; - glm::mat4 m(chunks[i]->Transform(base)); - glm::mat4 mvp(chunk_prog.GetVP() * m); - if (!CullTest(Chunk::Bounds(), mvp)) { - if (chunks[i]->ShouldUpdateModel()) { - chunks[i]->Update(models[i]); - } - chunk_prog.SetM(m); - models[i].Draw(); - } - } -} - -} diff --git a/tst/world/ChunkTest.cpp b/tst/world/ChunkTest.cpp index c8dac83..0da68d0 100644 --- a/tst/world/ChunkTest.cpp +++ b/tst/world/ChunkTest.cpp @@ -305,7 +305,7 @@ void ChunkTest::testNeighbor() { for (int i = 0; i < Block::FACE_COUNT; ++i) { Block::Face face = Block::Face(i); neighbor->Position(Block::FaceNormal(face)); - chunk->SetNeighbor(*neighbor); + chunk->SetNeighbor(face, *neighbor); CPPUNIT_ASSERT_MESSAGE( "chunk did not link right neighbor", chunk->HasNeighbor(face) @@ -322,16 +322,7 @@ void ChunkTest::testNeighbor() { "chunk did not link correct neighbor", &*chunk, &neighbor->GetNeighbor(Block::Opposite(face)) ); - chunk->ClearNeighbors(); - } - - neighbor->Position({1, 1, 1}); - chunk->SetNeighbor(*neighbor); - for (int i = 0; i < Block::FACE_COUNT; ++i) { - CPPUNIT_ASSERT_MESSAGE( - "chunk linked with non-neighbor", - !chunk->HasNeighbor(Block::Face(i)) - ); + chunk->Unlink(); } } -- 2.39.2