CXX = g++ --std=c++11
LDXX = g++
-LIBS = sdl2 SDL2_image SDL2_ttf glew openal freealut
+LIBS = sdl2 SDL2_image SDL2_ttf glew openal freealut zlib
PKGFLAGS := $(shell pkg-config --cflags $(LIBS))
PKGLIBS := $(shell pkg-config --libs $(LIBS))
exchange of chunks and entities
-persistence
-
- unloaded chunks should be saved to disk and restored when they
- are loaded again
-
launcher ui
select or create a world with configurable parameters
Dependencies
============
- GLEW, GLM, SDL2, SDL2_image, SDL2_ttf, OpenAL, freealut
+ GLEW, GLM, SDL2, SDL2_image, SDL2_ttf, OpenAL, freealut, zlib
CppUnit for tests
-archlinux: pacman -S glew glm sdl2 sdl2_image sdl2_ttf openal freealut cppunit
+archlinux: pacman -S glew glm sdl2 sdl2_image sdl2_ttf openal freealut zlib cppunit
manual:
CppUnit http://sourceforge.net/projects/cppunit/
GLEW http://glew.sourceforge.net/
GLM http://glm.g-truc.net/0.9.6/index.html
+ OpenAL http://openal.org/
SDL http://www.libsdl.org/
+ zlib http://zlib.net/
Makefile
WorldState::WorldState(
Environment &env,
const Interface::Config &ic,
- const World::Config &wc
+ const World::Config &wc,
+ const WorldSave &save
)
: env(env)
-, world(env.assets, wc)
+, world(env.assets, wc, save)
, spawner(world)
, interface(ic, env, world) {
WorldState(
Environment &,
const Interface::Config &,
- const World::Config &
+ const World::Config &,
+ const WorldSave &
);
void Handle(const SDL_Event &) override;
if (save.Exists()) {
save.Read(config.world);
} else {
- save.Create(config.world);
+ save.Write(config.world);
}
Application app(env);
- WorldState world_state(env, config.interface, config.world);
+ WorldState world_state(env, config.interface, config.world, save);
app.PushState(&world_state);
PreloadState preloader(env, world_state.GetWorld().Loader());
next_pos -= aim_normal * glm::vec3(Chunk::Extent());
}
mod_chunk->SetBlock(next_pos, selection);
- mod_chunk->Invalidate();
if (config.audio_disabled) return;
const Entity &player = ctrl.Controlled();
void Interface::RemoveBlock() noexcept {
if (!aim_chunk) return;
aim_chunk->SetBlock(aim_block, remove);
- aim_chunk->Invalidate();
if (config.audio_disabled) return;
const Entity &player = ctrl.Controlled();
// check which faces of a block at given index are obstructed (and therefore invisible)
Block::FaceSet Obstructed(const Pos &) const noexcept;
- void Invalidate() noexcept { dirty = true; }
-
void SetBlock(int index, const Block &) noexcept;
void SetBlock(const Block::Pos &pos, const Block &block) noexcept { SetBlock(ToIndex(pos), block); }
void SetBlock(const Pos &pos, const Block &block) noexcept { SetBlock(ToIndex(pos), block); }
return glm::translate((position - offset) * Extent());
}
+ void *BlockData() noexcept { return &blocks[0]; }
+ const void *BlockData() const noexcept { return &blocks[0]; }
+ static constexpr std::size_t BlockSize() noexcept { return sizeof(blocks) + sizeof(light); }
+
+ void Invalidate() noexcept { dirty_model = dirty_save = true; }
+ void InvalidateModel() noexcept { dirty_model = true; }
+ void ClearModel() noexcept { dirty_model = false; }
+ void ClearSave() noexcept { dirty_save = false; }
+ bool ShouldUpdateModel() const noexcept { return dirty_model; }
+ bool ShouldUpdateSave() const noexcept { return dirty_save; }
+
void CheckUpdate() noexcept;
void Draw() noexcept;
unsigned char light[size];
BlockModel model;
Pos position;
- bool dirty;
+ bool dirty_model;
+ bool dirty_save;
};
class BlockTypeRegistry;
class Generator;
+class WorldSave;
class ChunkLoader {
int gen_limit = 16;
};
- ChunkLoader(const Config &, const BlockTypeRegistry &, const Generator &) noexcept;
+ ChunkLoader(
+ const Config &,
+ const BlockTypeRegistry &,
+ const Generator &,
+ const WorldSave &
+ ) noexcept;
- void Generate(const Chunk::Pos &from, const Chunk::Pos &to);
- void GenerateSurrounding(const Chunk::Pos &);
+ void Queue(const Chunk::Pos &from, const Chunk::Pos &to);
+ void QueueSurrounding(const Chunk::Pos &);
std::list<Chunk> &Loaded() noexcept { return loaded; }
void Rebase(const Chunk::Pos &);
void Update(int dt);
- std::size_t ToLoad() const noexcept { return to_generate.size(); }
+ std::size_t ToLoad() const noexcept { return to_load.size(); }
void LoadOne();
void LoadN(std::size_t n);
private:
- Chunk &Generate(const Chunk::Pos &pos);
+ Chunk &Load(const Chunk::Pos &pos);
// link given chunk to all loaded neighbors
void Insert(Chunk &) noexcept;
// remove a loaded chunk
const BlockTypeRegistry ®
const Generator &gen;
+ const WorldSave &save;
std::list<Chunk> loaded;
- std::list<Chunk::Pos> to_generate;
+ std::list<Chunk::Pos> to_load;
std::list<Chunk> to_free;
IntervalTimer gen_timer;
}
}
}
- chunk.Invalidate();
- chunk.CheckUpdate();
+ //chunk.CheckUpdate();
}
}
#include "../graphics/Format.hpp"
#include "../graphics/Viewport.hpp"
-#include <iostream>
#include <limits>
#include <glm/gtx/io.hpp>
#include <glm/gtx/transform.hpp>
namespace blank {
-World::World(const Assets &assets, const Config &config)
+World::World(const Assets &assets, const Config &config, const WorldSave &save)
: blockType()
, blockShape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f }})
, stairShape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f }}, { 0.0f, 0.0f })
, slabShape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.0f, 0.5f }})
, block_tex()
, generate(config.gen)
-, chunks(config.load, blockType, generate)
+, chunks(config.load, blockType, generate, save)
, player()
, entities()
, light_direction(config.light_direction)
player->WorldCollidable(true);
player->Position(config.spawn);
- chunks.GenerateSurrounding(player->ChunkCoords());
+ chunks.QueueSurrounding(player->ChunkCoords());
}
ChunkLoader::Config load = ChunkLoader::Config();
};
- World(const Assets &, const Config &);
+ World(const Assets &, const Config &, const WorldSave &);
bool Intersection(
const Ray &,
#include <cctype>
#include <fstream>
#include <iostream>
+#include <limits>
#include <stdexcept>
+#include <zlib.h>
using namespace std;
WorldSave::WorldSave(const string &path)
: root_path(path)
-, conf_path(path + "world.conf") {
+, conf_path(path + "world.conf")
+, chunk_path(path + "chunks/%d/%d/%d.gz")
+, chunk_bufsiz(chunk_path.length() + 3 * std::numeric_limits<int>::digits10)
+, chunk_buf(new char[chunk_bufsiz]) {
}
}
-void WorldSave::Create(const World::Config &conf) const {
- cout << "creating world save" << endl;
-
- if (!make_dirs(root_path)) {
- throw runtime_error("failed to create world save directory");
- }
-
- ofstream out(conf_path);
- out << "seed = " << conf.gen.seed << endl;
- out.close();
-
- if (!out) {
- throw runtime_error("failed to write world config");
- }
-}
-
void WorldSave::Read(World::Config &conf) const {
cout << "reading world save" << endl;
}
}
+void WorldSave::Write(const World::Config &conf) const {
+ cout << "writing world save" << endl;
+
+ if (!make_dirs(root_path)) {
+ throw runtime_error("failed to create world save directory");
+ }
+
+ ofstream out(conf_path);
+ out << "seed = " << conf.gen.seed << endl;
+ out.close();
+
+ if (!out) {
+ throw runtime_error("failed to write world config");
+ }
+}
+
+
+bool WorldSave::Exists(const Chunk::Pos &pos) const noexcept {
+ return is_file(ChunkPath(pos));
+}
+
+void WorldSave::Read(Chunk &chunk) const {
+ const char *path = ChunkPath(chunk.Position());
+ gzFile file = gzopen(path, "r");
+ if (!file) {
+ throw runtime_error("failed to open chunk file");
+ }
+ if (gzread(file, chunk.BlockData(), Chunk::BlockSize()) != Chunk::BlockSize()) {
+ throw runtime_error("failed to read chunk from file");
+ }
+ if (gzclose(file) != Z_OK) {
+ throw runtime_error("failed to read chunk file");
+ }
+ chunk.InvalidateModel();
+ chunk.ClearSave();
+}
+
+void WorldSave::Write(Chunk &chunk) const {
+ const char *path = ChunkPath(chunk.Position());
+ gzFile file = gzopen(path, "w");
+ if (!file) {
+ // check if it's because of a missing path component
+ if (errno != ENOENT) {
+ // nope, fatal
+ throw runtime_error(strerror(errno));
+ }
+ string dir_path(path);
+ dir_path.erase(dir_path.find_last_of("\\/"));
+ if (!make_dirs(dir_path)) {
+ throw runtime_error("failed to create dir for chunk file");
+ }
+ file = gzopen(path, "w");
+ if (!file) {
+ throw runtime_error("failed to open chunk file");
+ }
+ }
+ if (gzwrite(file, chunk.BlockData(), Chunk::BlockSize()) == 0) {
+ gzclose(file); // if this fails, it can't be helped
+ throw runtime_error("failed to write chunk to file");
+ }
+ if (gzclose(file) != Z_OK) {
+ throw runtime_error("failed to write chunk file");
+ }
+ chunk.ClearSave();
+}
+
+
+const char *WorldSave::ChunkPath(const Chunk::Pos &pos) const {
+ snprintf(chunk_buf.get(), chunk_bufsiz, chunk_path.c_str(), pos.x, pos.y, pos.z);
+ return chunk_buf.get();
+}
+
}
#ifndef BLANK_WORLD_WORLDSAVE_HPP_
#define BLANK_WORLD_WORLDSAVE_HPP_
+#include "Chunk.hpp"
#include "World.hpp"
+#include <memory>
#include <string>
explicit WorldSave(const std::string &path);
public:
+ // whole save
bool Exists() const noexcept;
-
- void Create(const World::Config &) const;
void Read(World::Config &) const;
+ void Write(const World::Config &) const;
+
+ // single chunk
+ bool Exists(const Chunk::Pos &) const noexcept;
+ void Read(Chunk &) const;
+ void Write(Chunk &) const;
+
+ const char *ChunkPath(const Chunk::Pos &) const;
private:
std::string root_path;
std::string conf_path;
+ std::string chunk_path;
+ std::size_t chunk_bufsiz;
+ std::unique_ptr<char[]> chunk_buf;
};
#include "Generator.hpp"
#include "WorldCollision.hpp"
+#include "WorldSave.hpp"
#include <algorithm>
-#include <iostream>
#include <limits>
+#include <ostream>
#include <queue>
, light{0}
, model()
, position(0, 0, 0)
-, dirty(false) {
+, dirty_model(false)
+, dirty_save(false) {
}
: types(other.types)
, model(std::move(other.model))
, position(other.position)
-, dirty(other.dirty) {
+, dirty_model(other.dirty_model)
+, dirty_save(other.dirty_save) {
std::copy(other.neighbor, other.neighbor + sizeof(neighbor), neighbor);
std::copy(other.blocks, other.blocks + sizeof(blocks), blocks);
std::copy(other.light, other.light + sizeof(light), light);
std::copy(other.light, other.light + sizeof(light), light);
model = std::move(other.model);
position = other.position;
- dirty = other.dirty;
+ dirty_model = other.dirty_save;
+ dirty_save = other.dirty_save;
return *this;
}
const BlockType &new_type = Type(block);
blocks[index] = block;
+ Invalidate();
if (&old_type == &new_type) return;
void Chunk::Draw() noexcept {
- if (dirty) {
+ if (ShouldUpdateModel()) {
Update();
}
model.Draw();
}
void Chunk::CheckUpdate() noexcept {
- if (dirty) {
+ if (ShouldUpdateModel()) {
Update();
}
}
}
model.Update(buf);
- dirty = false;
+ ClearModel();
}
Block::FaceSet Chunk::Obstructed(const Pos &pos) const noexcept {
}
-ChunkLoader::ChunkLoader(const Config &config, const BlockTypeRegistry ®, const Generator &gen) noexcept
+ChunkLoader::ChunkLoader(
+ const Config &config,
+ const BlockTypeRegistry ®,
+ const Generator &gen,
+ const WorldSave &save
+) noexcept
: base(0, 0, 0)
, reg(reg)
, gen(gen)
+, save(save)
, loaded()
-, to_generate()
+, to_load()
, to_free()
, gen_timer(config.gen_limit)
, load_dist(config.load_dist)
}
-void ChunkLoader::Generate(const Chunk::Pos &from, const Chunk::Pos &to) {
+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) {
if (Known(pos)) {
continue;
} else if (pos == base) {
- Generate(pos);
+ Load(pos);
// light testing
// for (int i = 0; i < 16; ++i) {
// loaded.back().Invalidate();
// loaded.back().CheckUpdate();
} else {
- to_generate.emplace_back(pos);
+ to_load.emplace_back(pos);
}
}
}
}
- to_generate.sort(ChunkLess(base));
+ to_load.sort(ChunkLess(base));
}
-Chunk &ChunkLoader::Generate(const Chunk::Pos &pos) {
+Chunk &ChunkLoader::Load(const Chunk::Pos &pos) {
loaded.emplace_back(reg);
Chunk &chunk = loaded.back();
chunk.Position(pos);
- gen(chunk);
+ if (save.Exists(pos)) {
+ save.Read(chunk);
+ } else {
+ gen(chunk);
+ }
Insert(chunk);
return chunk;
}
}
bool ChunkLoader::Queued(const Chunk::Pos &pos) noexcept {
- for (const Chunk::Pos &chunk : to_generate) {
+ for (const Chunk::Pos &chunk : to_load) {
if (chunk == pos) {
return true;
}
return *chunk;
}
- for (auto iter(to_generate.begin()), end(to_generate.end()); iter != end; ++iter) {
+ for (auto iter(to_load.begin()), end(to_load.end()); iter != end; ++iter) {
if (*iter == pos) {
- to_generate.erase(iter);
+ to_load.erase(iter);
break;
}
}
- return Generate(pos);
+ return Load(pos);
}
bool ChunkLoader::OutOfRange(const Chunk::Pos &pos) const noexcept {
}
}
// abort far away queued chunks
- for (auto iter(to_generate.begin()), end(to_generate.end()); iter != end;) {
+ for (auto iter(to_load.begin()), end(to_load.end()); iter != end;) {
if (OutOfRange(*iter)) {
- iter = to_generate.erase(iter);
+ iter = to_load.erase(iter);
} else {
++iter;
}
}
// add missing new chunks
- GenerateSurrounding(base);
+ QueueSurrounding(base);
}
-void ChunkLoader::GenerateSurrounding(const Chunk::Pos &pos) {
+void ChunkLoader::QueueSurrounding(const Chunk::Pos &pos) {
const Chunk::Pos offset(load_dist, load_dist, load_dist);
- Generate(pos - offset, pos + offset);
+ Queue(pos - offset, pos + offset);
}
void ChunkLoader::Update(int dt) {
if (gen_timer.Hit()) {
LoadOne();
}
+
+ constexpr int max_save = 10;
+ int saved = 0;
+ for (Chunk &chunk : loaded) {
+ if (chunk.ShouldUpdateSave()) {
+ save.Write(chunk);
+ ++saved;
+ if (saved >= max_save) {
+ break;
+ }
+ }
+ }
}
void ChunkLoader::LoadN(std::size_t n) {
}
void ChunkLoader::LoadOne() {
- if (to_generate.empty()) return;
+ if (to_load.empty()) return;
// take position of next chunk in queue
- Chunk::Pos pos(to_generate.front());
- to_generate.pop_front();
+ 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) {
Chunk &chunk = loaded.back();
chunk.Position(pos);
- gen(chunk);
+ if (save.Exists(pos)) {
+ save.Read(chunk);
+ } else {
+ gen(chunk);
+ }
Insert(chunk);
}