]> git.localhorst.tv Git - blank.git/commitdiff
make command output visible to player(s)
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 23 Nov 2015 13:47:30 +0000 (14:47 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Mon, 23 Nov 2015 14:00:30 +0000 (15:00 +0100)
15 files changed:
doc/todo
src/server/ClientConnection.hpp
src/server/NetworkCLIFeedback.hpp [new file with mode: 0644]
src/server/Server.hpp
src/server/net.cpp
src/shared/CLI.hpp
src/shared/CLIContext.hpp [new file with mode: 0644]
src/shared/cli.cpp
src/shared/commands.hpp
src/standalone/DirectCLIFeedback.hpp [new file with mode: 0644]
src/standalone/MasterState.cpp [deleted file]
src/standalone/MasterState.hpp
src/standalone/PreloadState.cpp [deleted file]
src/standalone/UnloadState.cpp [deleted file]
src/standalone/standalone.cpp [new file with mode: 0644]

index bbf432c5b1be2b8e7dd3a80990b278c8d06ae1a2..9df0ec47c2a0f193161f0b910ec79c923dc64d07 100644 (file)
--- a/doc/todo
+++ b/doc/todo
@@ -20,7 +20,6 @@ font rendering
 command line
 
        more commands pls
-       and show me their output
 
 persistence
 
index 57a34e49339a965655a170758a82603558bc6155..3ab0453e87a1d46e5c18262ad974bd3e01287f84 100644 (file)
@@ -2,6 +2,7 @@
 #define BLANK_SERVER_CLIENTCONNECTION_HPP_
 
 #include "ChunkTransmitter.hpp"
+#include "NetworkCLIFeedback.hpp"
 #include "Server.hpp"
 #include "../app/IntervalTimer.hpp"
 #include "../ui/DirectInput.hpp"
@@ -10,6 +11,7 @@
 #include "../world/EntityState.hpp"
 #include "../world/Player.hpp"
 
+#include <cstdint>
 #include <deque>
 #include <list>
 #include <memory>
@@ -39,6 +41,8 @@ public:
        Connection &GetConnection() noexcept { return conn; }
        bool Disconnected() const noexcept { return conn.Closed(); }
 
+       Server &GetServer() noexcept { return server; }
+
        /// prepare a packet of given type
        template<class Type>
        Type Prepare() const noexcept {
@@ -63,6 +67,8 @@ public:
 
        bool ChunkInRange(const glm::ivec3 &) const noexcept;
 
+       std::uint16_t SendMessage(std::uint8_t type, std::uint32_t from, const std::string &msg);
+
 private:
        struct SpawnStatus {
                // the entity in question
@@ -105,6 +111,7 @@ private:
        Server &server;
        Connection conn;
        std::unique_ptr<DirectInput> input;
+       std::unique_ptr<NetworkCLIFeedback> cli_ctx;
        const Model *player_model;
        std::list<SpawnStatus> spawns;
        unsigned int confirm_wait;
diff --git a/src/server/NetworkCLIFeedback.hpp b/src/server/NetworkCLIFeedback.hpp
new file mode 100644 (file)
index 0000000..25a8781
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef BLANK_SERVER_NETWORKCLIFEEDBACK_HPP_
+#define BLANK_SERVER_NETWORKCLIFEEDBACK_HPP_
+
+#include "../shared/CLIContext.hpp"
+
+
+namespace blank {
+
+namespace server {
+
+class ClientConnection;
+
+class NetworkCLIFeedback
+: public CLIContext {
+
+public:
+       NetworkCLIFeedback(Player &, ClientConnection &);
+
+       void Error(const std::string &) override;
+       void Message(const std::string &) override;
+       void Broadcast(const std::string &) override;
+
+private:
+       ClientConnection &conn;
+
+};
+
+}
+}
+
+#endif
index 40905c8ff7943b1fb4824a6b9830d436c2a8dbc0..9b89ab1bbc94692a400c7a3a6eda0054ca98a161 100644 (file)
@@ -14,6 +14,7 @@
 namespace blank {
 
 class ChunkIndex;
+class CLIContext;
 class Model;
 class Player;
 class WorldSave;
@@ -52,7 +53,7 @@ public:
        void SetBlock(Chunk &, int, const Block &) override;
 
        /// for use by client connections when they receive a line from the player
-       void DispatchMessage(Player &, const std::string &);
+       void DispatchMessage(CLIContext &, const std::string &);
 
        /// send message to all connected clients
        void DistributeMessage(std::uint8_t type, std::uint32_t ref, const std::string &msg);
index d16088991f22f2a840869dc1c9da89c040764cbe..e8325da1bd4ea1f0cfd08f0eda206d2e845e2f0e 100644 (file)
@@ -439,6 +439,8 @@ void ClientConnection::AttachPlayer(Player &player) {
        input.reset(new DirectInput(server.GetWorld(), player, server));
        PlayerEntity().Ref();
 
+       cli_ctx.reset(new NetworkCLIFeedback(player, *this));
+
        old_base = PlayerChunks().Base();
        ExactLocation::Coarse begin = PlayerChunks().CoordsBegin();
        ExactLocation::Coarse end = PlayerChunks().CoordsEnd();
@@ -469,6 +471,7 @@ void ClientConnection::DetachPlayer() {
        server.GetWorldSave().Write(input->GetPlayer());
        PlayerEntity().Kill();
        PlayerEntity().UnRef();
+       cli_ctx.reset();
        input.reset();
        transmitter.Abort();
        chunk_queue.clear();
@@ -629,11 +632,38 @@ void ClientConnection::On(const Packet::Message &pack) {
        pack.ReadReferral(ref);
        pack.ReadMessage(msg);
 
-       if (type == 1 && HasPlayer()) {
-               server.DispatchMessage(input->GetPlayer(), msg);
+       if (type == 1 && cli_ctx) {
+               server.DispatchMessage(*cli_ctx, msg);
        }
 }
 
+uint16_t ClientConnection::SendMessage(uint8_t type, uint32_t from, const string &msg) {
+       auto pack = Prepare<Packet::Message>();
+       pack.WriteType(type);
+       pack.WriteReferral(from);
+       pack.WriteMessage(msg);
+       return Send(Packet::Message::GetSize(msg));
+}
+
+
+NetworkCLIFeedback::NetworkCLIFeedback(Player &p, ClientConnection &c)
+: CLIContext(p)
+, conn(c) {
+
+}
+
+void NetworkCLIFeedback::Error(const string &msg) {
+       conn.SendMessage(0, 0, msg);
+}
+
+void NetworkCLIFeedback::Message(const string &msg) {
+       conn.SendMessage(0, 0, msg);
+}
+
+void NetworkCLIFeedback::Broadcast(const string &msg) {
+       conn.GetServer().DistributeMessage(0, GetPlayer().GetEntity().ID(), msg);
+}
+
 
 Server::Server(
        const Config::Network &conf,
@@ -788,14 +818,14 @@ void Server::SetBlock(Chunk &chunk, int index, const Block &block) {
        }
 }
 
-void Server::DispatchMessage(Player &player, const string &msg) {
+void Server::DispatchMessage(CLIContext &ctx, const string &msg) {
        if (msg.empty()) {
                return;
        }
        if (msg[0] == '/' && msg.size() > 1 && msg[1] != '/') {
-               cli.Execute(player, msg.substr(1));
+               cli.Execute(ctx, msg.substr(1));
        } else {
-               DistributeMessage(1, player.GetEntity().ID(), msg);
+               DistributeMessage(1, ctx.GetPlayer().GetEntity().ID(), msg);
        }
 }
 
index b601e572d554af0ffa8cd1e274e4c0f3541388b8..ab1e3eeaad60deaa701dd34850e674a546aa5c5c 100644 (file)
@@ -7,7 +7,7 @@
 
 namespace blank {
 
-class Player;
+class CLIContext;
 class TokenStreamReader;
 class World;
 
@@ -16,7 +16,7 @@ class CLI {
 public:
        struct Command {
                virtual ~Command();
-               virtual void Execute(CLI &, Player &, TokenStreamReader &) = 0;
+               virtual void Execute(CLI &, CLIContext &, TokenStreamReader &) = 0;
        };
 
 public:
@@ -25,10 +25,7 @@ public:
 
        void AddCommand(const std::string &name, Command *);
 
-       void Execute(Player &, const std::string &);
-
-       void Message(const std::string &msg);
-       void Error(const std::string &msg);
+       void Execute(CLIContext &, const std::string &);
 
 private:
        World &world;
diff --git a/src/shared/CLIContext.hpp b/src/shared/CLIContext.hpp
new file mode 100644 (file)
index 0000000..8de432b
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef BLANK_SHARED_CLICONTEXT_HPP_
+#define BLANK_SHARED_CLICONTEXT_HPP_
+
+#include <string>
+
+
+namespace blank {
+
+class Player;
+
+class CLIContext {
+
+public:
+       explicit CLIContext(Player &p)
+       : player(p) { }
+
+       /// get the player responsible for all this
+       Player &GetPlayer() { return player; }
+
+       /// an error has happened and the player should be notified
+       virtual void Error(const std::string &) = 0;
+
+       /// return to sender
+       /// use this for output concerning the originator of a command
+       virtual void Message(const std::string &) = 0;
+
+       /// send a status message to all players
+       /// use this to announce stuff which may be interesting to anyone
+       virtual void Broadcast(const std::string &) = 0;
+
+private:
+       Player &player;
+
+};
+
+}
+
+#endif
index 6942f78704ec3a887dabff21fd35df37e0a6ba51..d4cf525b90e5e1c346bd0dbc9e65356c85a41627 100644 (file)
@@ -1,4 +1,5 @@
 #include "CLI.hpp"
+#include "CLIContext.hpp"
 #include "commands.hpp"
 
 #include "../io/TokenStreamReader.hpp"
@@ -7,6 +8,7 @@
 
 #include <iostream>
 #include <sstream>
+#include <glm/gtx/io.hpp>
 
 using namespace std;
 
@@ -29,7 +31,7 @@ void CLI::AddCommand(const string &name, Command *cmd) {
        commands[name] = cmd;
 }
 
-void CLI::Execute(Player &player, const string &line) {
+void CLI::Execute(CLIContext &ctx, const string &line) {
        stringstream s(line);
        TokenStreamReader args(s);
        if (!args.HasMore()) {
@@ -37,44 +39,39 @@ void CLI::Execute(Player &player, const string &line) {
                return;
        }
        if (args.Peek().type != Token::IDENTIFIER) {
-               Error("I don't understand");
+               ctx.Error("I don't understand");
                return;
        }
        string name;
        args.ReadIdentifier(name);
        auto entry = commands.find(name);
        if (entry == commands.end()) {
-               Error(name + ": command not found");
+               ctx.Error(name + ": command not found");
                return;
        }
        try {
-               entry->second->Execute(*this, player, args);
+               entry->second->Execute(*this, ctx, args);
        } catch (exception &e) {
-               Error(name + ": " + e.what());
+               ctx.Error(name + ": " + e.what());
        } catch (...) {
-               Error(name + ": unknown execution error");
+               ctx.Error(name + ": unknown execution error");
        }
 }
 
-void CLI::Message(const string &msg) {
-       // TODO: display message to player
-       cout << msg << endl;
-}
-
-void CLI::Error(const string &msg) {
-       Message("CLI error: " + msg);
-}
-
 CLI::Command::~Command() {
 
 }
 
 
-void TeleportCommand::Execute(CLI &cli, Player &player, TokenStreamReader &args) {
+void TeleportCommand::Execute(CLI &cli, CLIContext &ctx, TokenStreamReader &args) {
        glm::vec3 pos(args.GetFloat(), args.GetFloat(), args.GetFloat());
-       EntityState state = player.GetEntity().GetState();
+       EntityState state = ctx.GetPlayer().GetEntity().GetState();
        state.pos = ExactLocation(pos).Sanitize();
-       player.GetEntity().SetState(state);
+       ctx.GetPlayer().GetEntity().SetState(state);
+
+       stringstream msg;
+       msg << ctx.GetPlayer().Name() << " teleported to " << pos;
+       ctx.Broadcast(msg.str());
 }
 
 }
index 53ab6977ab7339c01dcba36d56058858ee2e7c9d..1c00241850350ac80ce5be9b5390e3112483bb65 100644 (file)
@@ -9,7 +9,7 @@ namespace blank {
 class TeleportCommand
 : public CLI::Command {
 
-       void Execute(CLI &, Player &, TokenStreamReader &) override;
+       void Execute(CLI &, CLIContext &, TokenStreamReader &) override;
 
 };
 
diff --git a/src/standalone/DirectCLIFeedback.hpp b/src/standalone/DirectCLIFeedback.hpp
new file mode 100644 (file)
index 0000000..06f2038
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef BLANK_STANDALONE_DIRECTCLIFEEDBACK_HPP_
+#define BLANK_STANDALONE_DIRECTCLIFEEDBACK_HPP_
+
+#include "../shared/CLIContext.hpp"
+
+
+namespace blank {
+
+class HUD;
+
+namespace standalone {
+
+class DirectCLIFeedback
+: public CLIContext {
+
+public:
+       DirectCLIFeedback(Player &, HUD &);
+
+       void Error(const std::string &) override;
+       void Message(const std::string &) override;
+       void Broadcast(const std::string &) override;
+
+private:
+       HUD &hud;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/standalone/MasterState.cpp b/src/standalone/MasterState.cpp
deleted file mode 100644 (file)
index 6d45738..0000000
+++ /dev/null
@@ -1,234 +0,0 @@
-#include "MasterState.hpp"
-
-#include "../app/Config.hpp"
-#include "../app/Environment.hpp"
-#include "../app/init.hpp"
-#include "../geometry/distance.hpp"
-#include "../io/WorldSave.hpp"
-
-#include <SDL.h>
-
-
-namespace blank {
-namespace standalone {
-
-MasterState::MasterState(
-       Environment &env,
-       Config &config,
-       const Generator::Config &gc,
-       const World::Config &wc,
-       const WorldSave &save
-)
-: config(config)
-, env(env)
-, res()
-, sounds()
-, save(save)
-, world(res.block_types, wc)
-, spawn_index(world.Chunks().MakeIndex(wc.spawn, 3))
-, player(*world.AddPlayer(config.player.name))
-, spawn_player(false)
-, hud(env, config, player)
-, manip(env.audio, sounds, player.GetEntity())
-, input(world, player, manip)
-, interface(config, env.keymap, input, *this)
-, generator(gc)
-, chunk_loader(world.Chunks(), generator, save)
-, chunk_renderer(player.GetChunks())
-, spawner(world, res.models, env.rng)
-, sky(env.loader.LoadCubeMap("skybox"))
-, cli(world)
-, preload(env, chunk_loader, chunk_renderer)
-, unload(env, world.Chunks(), save)
-, chat(env, *this, *this) {
-       res.Load(env.loader, "default");
-       if (res.models.size() < 2) {
-               throw std::runtime_error("need at least two models to run");
-       }
-       res.models[0].Instantiate(player.GetEntity().GetModel());
-       sounds.Load(env.loader, res.snd_index);
-       spawner.LimitModels(1, res.models.size());
-       interface.SetInventorySlots(res.block_types.size() - 1);
-       generator.LoadTypes(res.block_types);
-       chunk_renderer.LoadTextures(env.loader, res.tex_index);
-       chunk_renderer.FogDensity(wc.fog_density);
-       if (save.Exists(player)) {
-               save.Read(player);
-       } else {
-               spawn_player = true;
-       }
-}
-
-MasterState::~MasterState() {
-       world.Chunks().UnregisterIndex(spawn_index);
-}
-
-
-void MasterState::OnResume() {
-       if (spawn_index.MissingChunks() > 0) {
-               env.state.Push(&preload);
-               return;
-       }
-       if (spawn_player) {
-               // TODO: spawn
-               spawn_player = false;
-       }
-       hud.KeepMessages(false);
-       OnFocus();
-}
-
-void MasterState::OnPause() {
-       OnBlur();
-}
-
-void MasterState::OnFocus() {
-       if (config.input.mouse) {
-               env.window.GrabMouse();
-       }
-       interface.Unlock();
-}
-
-void MasterState::OnBlur() {
-       env.window.ReleaseMouse();
-       interface.Lock();
-}
-
-
-void MasterState::Handle(const SDL_Event &event) {
-       switch (event.type) {
-               case SDL_KEYDOWN:
-                       // TODO: move to interface
-                       if (event.key.keysym.sym == SDLK_RETURN) {
-                               chat.Clear();
-                               env.state.Push(&chat);
-                               hud.KeepMessages(true);
-                       } else if (event.key.keysym.sym == SDLK_SLASH) {
-                               chat.Preset("/");
-                               env.state.Push(&chat);
-                               hud.KeepMessages(true);
-                       } else {
-                               interface.HandlePress(event.key);
-                       }
-                       break;
-               case SDL_KEYUP:
-                       interface.HandleRelease(event.key);
-                       break;
-               case SDL_MOUSEBUTTONDOWN:
-                       interface.HandlePress(event.button);
-                       break;
-               case SDL_MOUSEBUTTONUP:
-                       interface.HandleRelease(event.button);
-                       break;
-               case SDL_MOUSEMOTION:
-                       interface.Handle(event.motion);
-                       break;
-               case SDL_MOUSEWHEEL:
-                       interface.Handle(event.wheel);
-                       break;
-               case SDL_QUIT:
-                       Exit();
-                       break;
-               default:
-                       break;
-       }
-}
-
-void MasterState::Update(int dt) {
-       spawner.Update(dt);
-       world.Update(dt);
-       if (input.BlockFocus()) {
-               hud.FocusBlock(input.BlockFocus().GetChunk(), input.BlockFocus().block);
-       } else if (input.EntityFocus()) {
-               hud.FocusEntity(*input.EntityFocus().entity);
-       } else {
-               hud.FocusNone();
-       }
-       hud.Display(res.block_types[player.GetInventorySlot() + 1]);
-       hud.Update(dt);
-       chunk_loader.Update(dt);
-       chunk_renderer.Update(dt);
-
-       glm::mat4 trans = player.GetEntity().Transform(player.GetEntity().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(player.GetEntity().Position());
-       env.audio.Velocity(player.GetEntity().Velocity());
-       env.audio.Orientation(dir, up);
-}
-
-void MasterState::Render(Viewport &viewport) {
-       viewport.WorldPosition(player.GetEntity().ViewTransform(player.GetEntity().ChunkCoords()));
-       if (config.video.world) {
-               chunk_renderer.Render(viewport);
-               world.Render(viewport);
-               if (config.video.debug) {
-                       world.RenderDebug(viewport);
-               }
-               sky.Render(viewport);
-       }
-       hud.Render(viewport);
-}
-
-
-void MasterState::SetAudio(bool b) {
-       config.audio.enabled = b;
-       if (b) {
-               hud.PostMessage("Audio enabled");
-       } else {
-               hud.PostMessage("Audio disabled");
-       }
-}
-
-void MasterState::SetVideo(bool b) {
-       config.video.world = b;
-       if (b) {
-               hud.PostMessage("World rendering enabled");
-       } else {
-               hud.PostMessage("World rendering disabled");
-       }
-}
-
-void MasterState::SetHUD(bool b) {
-       config.video.hud = b;
-       if (b) {
-               hud.PostMessage("HUD rendering enabled");
-       } else {
-               hud.PostMessage("HUD rendering disabled");
-       }
-}
-
-void MasterState::SetDebug(bool b) {
-       config.video.debug = b;
-       if (b) {
-               hud.PostMessage("Debug rendering enabled");
-       } else {
-               hud.PostMessage("Debug rendering disabled");
-       }
-}
-
-void MasterState::NextCamera() {
-       if (iszero(env.viewport.CameraOffset())) {
-               env.viewport.OffsetCamera(glm::vec3(0.0f, 0.0f, -5.0f));
-       } else {
-               env.viewport.OffsetCamera(glm::vec3(0.0f, 0.0f, 0.0f));
-       }
-}
-
-void MasterState::Exit() {
-       save.Write(player);
-       env.state.Switch(&unload);
-}
-
-void MasterState::OnLineSubmit(const std::string &line) {
-       if (line.empty()) {
-               return;
-       }
-       if (line[0] == '/' && line.size() > 1 && line[1] != '/') {
-               cli.Execute(player, line.substr(1));
-       } else {
-               hud.PostMessage(line);
-       }
-}
-
-}
-}
index 610f13a128665ed615ce41435d436a53562662f4..04234e5ccea07d18eb99683e6cca088ee0e279c2 100644 (file)
@@ -4,6 +4,7 @@
 #include "../app/State.hpp"
 #include "../ui/ClientController.hpp"
 
+#include "DirectCLIFeedback.hpp"
 #include "PreloadState.hpp"
 #include "UnloadState.hpp"
 #include "../ai/Spawner.hpp"
@@ -90,6 +91,7 @@ private:
        SkyBox sky;
 
        CLI cli;
+       DirectCLIFeedback cli_ctx;
 
        PreloadState preload;
        UnloadState unload;
diff --git a/src/standalone/PreloadState.cpp b/src/standalone/PreloadState.cpp
deleted file mode 100644 (file)
index 554a458..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-#include "PreloadState.hpp"
-
-#include "../app/Environment.hpp"
-#include "../world/ChunkLoader.hpp"
-#include "../world/ChunkRenderer.hpp"
-
-
-namespace blank {
-namespace standalone {
-
-PreloadState::PreloadState(Environment &env, ChunkLoader &loader, ChunkRenderer &render)
-: ProgressState(env, "Preloading chunks: %d/%d (%d%%)")
-, env(env)
-, loader(loader)
-, render(render)
-, total(loader.ToLoad())
-, per_update(64) {
-
-}
-
-void PreloadState::Update(int dt) {
-       loader.LoadN(per_update);
-       if (loader.ToLoad() <= 0) {
-               env.state.Pop();
-               render.Update(render.MissingChunks());
-       } else {
-               SetProgress(total - loader.ToLoad(), total);
-       }
-}
-
-}
-}
diff --git a/src/standalone/UnloadState.cpp b/src/standalone/UnloadState.cpp
deleted file mode 100644 (file)
index 0b48904..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#include "UnloadState.hpp"
-
-#include "../app/Environment.hpp"
-#include "../io/WorldSave.hpp"
-#include "../world/ChunkLoader.hpp"
-
-
-namespace blank {
-namespace standalone {
-
-UnloadState::UnloadState(
-       Environment &env,
-       ChunkStore &chunks,
-       const WorldSave &save)
-: ProgressState(env, "Unloading chunks: %d/%d (%d%%)")
-, env(env)
-, chunks(chunks)
-, save(save)
-, cur(chunks.begin())
-, end(chunks.end())
-, done(0)
-, total(chunks.NumLoaded())
-, per_update(64) {
-
-}
-
-
-void UnloadState::OnResume() {
-       cur = chunks.begin();
-       end = chunks.end();
-       done = 0;
-       total = chunks.NumLoaded();
-}
-
-
-void UnloadState::Handle(const SDL_Event &) {
-       // ignore everything
-}
-
-void UnloadState::Update(int dt) {
-       for (std::size_t i = 0; i < per_update && cur != end; ++i, ++cur, ++done) {
-               if (cur->ShouldUpdateSave()) {
-                       save.Write(*cur);
-               }
-       }
-       if (cur == end) {
-               env.state.Pop();
-       } else {
-               SetProgress(done, total);
-       }
-}
-
-}
-}
diff --git a/src/standalone/standalone.cpp b/src/standalone/standalone.cpp
new file mode 100644 (file)
index 0000000..ad6450d
--- /dev/null
@@ -0,0 +1,323 @@
+#include "DirectCLIFeedback.hpp"
+#include "MasterState.hpp"
+#include "PreloadState.hpp"
+#include "UnloadState.hpp"
+
+#include "../app/Config.hpp"
+#include "../app/Environment.hpp"
+#include "../app/init.hpp"
+#include "../geometry/distance.hpp"
+#include "../io/WorldSave.hpp"
+#include "../world/ChunkLoader.hpp"
+#include "../world/ChunkRenderer.hpp"
+
+#include <SDL.h>
+
+
+namespace blank {
+namespace standalone {
+
+DirectCLIFeedback::DirectCLIFeedback(Player &p, HUD &h)
+: CLIContext(p)
+, hud(h) {
+
+}
+
+void DirectCLIFeedback::Error(const std::string &msg) {
+       hud.PostMessage(msg);
+}
+
+void DirectCLIFeedback::Message(const std::string &msg) {
+       hud.PostMessage(msg);
+}
+
+void DirectCLIFeedback::Broadcast(const std::string &msg) {
+       hud.PostMessage(msg);
+}
+
+
+MasterState::MasterState(
+       Environment &env,
+       Config &config,
+       const Generator::Config &gc,
+       const World::Config &wc,
+       const WorldSave &save
+)
+: config(config)
+, env(env)
+, res()
+, sounds()
+, save(save)
+, world(res.block_types, wc)
+, spawn_index(world.Chunks().MakeIndex(wc.spawn, 3))
+, player(*world.AddPlayer(config.player.name))
+, spawn_player(false)
+, hud(env, config, player)
+, manip(env.audio, sounds, player.GetEntity())
+, input(world, player, manip)
+, interface(config, env.keymap, input, *this)
+, generator(gc)
+, chunk_loader(world.Chunks(), generator, save)
+, chunk_renderer(player.GetChunks())
+, spawner(world, res.models, env.rng)
+, sky(env.loader.LoadCubeMap("skybox"))
+, cli(world)
+, cli_ctx(player, hud)
+, preload(env, chunk_loader, chunk_renderer)
+, unload(env, world.Chunks(), save)
+, chat(env, *this, *this) {
+       res.Load(env.loader, "default");
+       if (res.models.size() < 2) {
+               throw std::runtime_error("need at least two models to run");
+       }
+       res.models[0].Instantiate(player.GetEntity().GetModel());
+       sounds.Load(env.loader, res.snd_index);
+       spawner.LimitModels(1, res.models.size());
+       interface.SetInventorySlots(res.block_types.size() - 1);
+       generator.LoadTypes(res.block_types);
+       chunk_renderer.LoadTextures(env.loader, res.tex_index);
+       chunk_renderer.FogDensity(wc.fog_density);
+       if (save.Exists(player)) {
+               save.Read(player);
+       } else {
+               spawn_player = true;
+       }
+}
+
+MasterState::~MasterState() {
+       world.Chunks().UnregisterIndex(spawn_index);
+}
+
+
+void MasterState::OnResume() {
+       if (spawn_index.MissingChunks() > 0) {
+               env.state.Push(&preload);
+               return;
+       }
+       if (spawn_player) {
+               // TODO: spawn
+               spawn_player = false;
+       }
+       hud.KeepMessages(false);
+       OnFocus();
+}
+
+void MasterState::OnPause() {
+       OnBlur();
+}
+
+void MasterState::OnFocus() {
+       if (config.input.mouse) {
+               env.window.GrabMouse();
+       }
+       interface.Unlock();
+}
+
+void MasterState::OnBlur() {
+       env.window.ReleaseMouse();
+       interface.Lock();
+}
+
+
+void MasterState::Handle(const SDL_Event &event) {
+       switch (event.type) {
+               case SDL_KEYDOWN:
+                       // TODO: move to interface
+                       if (event.key.keysym.sym == SDLK_RETURN) {
+                               chat.Clear();
+                               env.state.Push(&chat);
+                               hud.KeepMessages(true);
+                       } else if (event.key.keysym.sym == SDLK_SLASH) {
+                               chat.Preset("/");
+                               env.state.Push(&chat);
+                               hud.KeepMessages(true);
+                       } else {
+                               interface.HandlePress(event.key);
+                       }
+                       break;
+               case SDL_KEYUP:
+                       interface.HandleRelease(event.key);
+                       break;
+               case SDL_MOUSEBUTTONDOWN:
+                       interface.HandlePress(event.button);
+                       break;
+               case SDL_MOUSEBUTTONUP:
+                       interface.HandleRelease(event.button);
+                       break;
+               case SDL_MOUSEMOTION:
+                       interface.Handle(event.motion);
+                       break;
+               case SDL_MOUSEWHEEL:
+                       interface.Handle(event.wheel);
+                       break;
+               case SDL_QUIT:
+                       Exit();
+                       break;
+               default:
+                       break;
+       }
+}
+
+void MasterState::Update(int dt) {
+       spawner.Update(dt);
+       world.Update(dt);
+       if (input.BlockFocus()) {
+               hud.FocusBlock(input.BlockFocus().GetChunk(), input.BlockFocus().block);
+       } else if (input.EntityFocus()) {
+               hud.FocusEntity(*input.EntityFocus().entity);
+       } else {
+               hud.FocusNone();
+       }
+       hud.Display(res.block_types[player.GetInventorySlot() + 1]);
+       hud.Update(dt);
+       chunk_loader.Update(dt);
+       chunk_renderer.Update(dt);
+
+       glm::mat4 trans = player.GetEntity().Transform(player.GetEntity().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(player.GetEntity().Position());
+       env.audio.Velocity(player.GetEntity().Velocity());
+       env.audio.Orientation(dir, up);
+}
+
+void MasterState::Render(Viewport &viewport) {
+       viewport.WorldPosition(player.GetEntity().ViewTransform(player.GetEntity().ChunkCoords()));
+       if (config.video.world) {
+               chunk_renderer.Render(viewport);
+               world.Render(viewport);
+               if (config.video.debug) {
+                       world.RenderDebug(viewport);
+               }
+               sky.Render(viewport);
+       }
+       hud.Render(viewport);
+}
+
+
+void MasterState::SetAudio(bool b) {
+       config.audio.enabled = b;
+       if (b) {
+               hud.PostMessage("Audio enabled");
+       } else {
+               hud.PostMessage("Audio disabled");
+       }
+}
+
+void MasterState::SetVideo(bool b) {
+       config.video.world = b;
+       if (b) {
+               hud.PostMessage("World rendering enabled");
+       } else {
+               hud.PostMessage("World rendering disabled");
+       }
+}
+
+void MasterState::SetHUD(bool b) {
+       config.video.hud = b;
+       if (b) {
+               hud.PostMessage("HUD rendering enabled");
+       } else {
+               hud.PostMessage("HUD rendering disabled");
+       }
+}
+
+void MasterState::SetDebug(bool b) {
+       config.video.debug = b;
+       if (b) {
+               hud.PostMessage("Debug rendering enabled");
+       } else {
+               hud.PostMessage("Debug rendering disabled");
+       }
+}
+
+void MasterState::NextCamera() {
+       if (iszero(env.viewport.CameraOffset())) {
+               env.viewport.OffsetCamera(glm::vec3(0.0f, 0.0f, -5.0f));
+       } else {
+               env.viewport.OffsetCamera(glm::vec3(0.0f, 0.0f, 0.0f));
+       }
+}
+
+void MasterState::Exit() {
+       save.Write(player);
+       env.state.Switch(&unload);
+}
+
+void MasterState::OnLineSubmit(const std::string &line) {
+       if (line.empty()) {
+               return;
+       }
+       if (line[0] == '/' && line.size() > 1 && line[1] != '/') {
+               cli.Execute(cli_ctx, line.substr(1));
+       } else {
+               hud.PostMessage(line);
+       }
+}
+
+
+PreloadState::PreloadState(Environment &env, ChunkLoader &loader, ChunkRenderer &render)
+: ProgressState(env, "Preloading chunks: %d/%d (%d%%)")
+, env(env)
+, loader(loader)
+, render(render)
+, total(loader.ToLoad())
+, per_update(64) {
+
+}
+
+void PreloadState::Update(int dt) {
+       loader.LoadN(per_update);
+       if (loader.ToLoad() <= 0) {
+               env.state.Pop();
+               render.Update(render.MissingChunks());
+       } else {
+               SetProgress(total - loader.ToLoad(), total);
+       }
+}
+
+
+UnloadState::UnloadState(
+       Environment &env,
+       ChunkStore &chunks,
+       const WorldSave &save)
+: ProgressState(env, "Unloading chunks: %d/%d (%d%%)")
+, env(env)
+, chunks(chunks)
+, save(save)
+, cur(chunks.begin())
+, end(chunks.end())
+, done(0)
+, total(chunks.NumLoaded())
+, per_update(64) {
+
+}
+
+
+void UnloadState::OnResume() {
+       cur = chunks.begin();
+       end = chunks.end();
+       done = 0;
+       total = chunks.NumLoaded();
+}
+
+
+void UnloadState::Handle(const SDL_Event &) {
+       // ignore everything
+}
+
+void UnloadState::Update(int dt) {
+       for (std::size_t i = 0; i < per_update && cur != end; ++i, ++cur, ++done) {
+               if (cur->ShouldUpdateSave()) {
+                       save.Write(*cur);
+               }
+       }
+       if (cur == end) {
+               env.state.Pop();
+       } else {
+               SetProgress(done, total);
+       }
+}
+
+}
+}