command line
 
-       useful for development and later on world administration
+       more commands pls
+       and show me their output
 
 persistence
 
 
 networking
 
-       write tests
-       do some manual testing
-       some more testing
-       a little optimization
+       definitely needs throttling for the internets
+
+       players stats (who's connected, their ping, and game-relevant
+       things) should be sent to clients
+
 
 launcher ui
 
 
        if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
                throw SDLError("SDL_InitSubSystem(SDL_INIT_VIDEO)");
        }
+       // SDL seems to start out in text input state
+       SDL_StopTextInput();
 }
 
 InitVideo::~InitVideo() {
 
                case SDL_KEYDOWN:
                        // TODO: move to interface
                        if (event.key.keysym.sym == SDLK_RETURN) {
+                               chat.Clear();
+                               master.GetEnv().state.Push(&chat);
+                               hud.KeepMessages(true);
+                       } else if (event.key.keysym.sym == SDLK_SLASH) {
+                               chat.Preset("/");
                                master.GetEnv().state.Push(&chat);
                                hud.KeepMessages(true);
                        } else {
 
 #define BLANK_SERVER_SERVER_HPP
 
 #include "../app/Config.hpp"
+#include "../shared/CLI.hpp"
 #include "../world/World.hpp"
 #include "../world/WorldManipulator.hpp"
 
 
        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 &);
+
        /// send message to all connected clients
        void DistributeMessage(std::uint8_t type, std::uint32_t ref, const std::string &msg);
 
        const WorldSave &save;
        const Model *player_model;
 
+       CLI cli;
+
 };
 
 }
 
        pack.ReadMessage(msg);
 
        if (type == 1 && HasPlayer()) {
-               server.DistributeMessage(1, PlayerEntity().ID(), msg);
+               server.DispatchMessage(input->GetPlayer(), msg);
        }
 }
 
 , world(world)
 , spawn_index(world.Chunks().MakeIndex(wc.spawn, 3))
 , save(save)
-, player_model(nullptr) {
+, player_model(nullptr)
+, cli(world) {
        serv_sock = SDLNet_UDP_Open(conf.port);
        if (!serv_sock) {
                throw NetError("SDLNet_UDP_Open");
        }
 }
 
+void Server::DispatchMessage(Player &player, const string &msg) {
+       if (msg.empty()) {
+               return;
+       }
+       if (msg[0] == '/' && msg.size() > 1 && msg[1] != '/') {
+               cli.Execute(player, msg.substr(1));
+       } else {
+               DistributeMessage(1, player.GetEntity().ID(), msg);
+       }
+}
+
 void Server::DistributeMessage(uint8_t type, uint32_t ref, const string &msg) {
        auto pack = Packet::Make<Packet::Message>(serv_pack);
        pack.WriteType(type);
 
--- /dev/null
+#ifndef BLANK_SHARED_CLI_HPP_
+#define BLANK_SHARED_CLI_HPP_
+
+#include <map>
+#include <string>
+
+
+namespace blank {
+
+class Player;
+class TokenStreamReader;
+class World;
+
+class CLI {
+
+public:
+       struct Command {
+               virtual ~Command();
+               virtual void Execute(CLI &, Player &, TokenStreamReader &) = 0;
+       };
+
+public:
+       explicit CLI(World &);
+       ~CLI();
+
+       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);
+
+private:
+       World &world;
+       std::map<std::string, Command *> commands;
+
+};
+
+}
+
+#endif
 
 public:
        ChatState(Environment &env, State &parent, Responder &);
 
+       void Preset(const std::string &);
+       void Clear();
+
        void OnResume() override;
        void OnPause() override;
 
        State &parent;
        Responder &responder;
 
+       std::string preset;
        TextInput input;
 
 };
 
--- /dev/null
+#include "CLI.hpp"
+#include "commands.hpp"
+
+#include "../io/TokenStreamReader.hpp"
+#include "../world/Entity.hpp"
+#include "../world/Player.hpp"
+
+#include <iostream>
+#include <sstream>
+
+using namespace std;
+
+
+namespace blank {
+
+CLI::CLI(World &world)
+: world(world)
+, commands() {
+       AddCommand("tp", new TeleportCommand);
+}
+
+CLI::~CLI() {
+       for (auto &entry : commands) {
+               delete entry.second;
+       }
+}
+
+void CLI::AddCommand(const string &name, Command *cmd) {
+       commands[name] = cmd;
+}
+
+void CLI::Execute(Player &player, const string &line) {
+       stringstream s(line);
+       TokenStreamReader args(s);
+       if (!args.HasMore()) {
+               // ignore empty command line
+               return;
+       }
+       if (args.Peek().type != Token::IDENTIFIER) {
+               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");
+               return;
+       }
+       try {
+               entry->second->Execute(*this, player, args);
+       } catch (exception &e) {
+               Error(name + ": " + e.what());
+       } catch (...) {
+               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) {
+       glm::vec3 pos(args.GetFloat(), args.GetFloat(), args.GetFloat());
+       glm::ivec3 chunk(pos);
+       chunk /= Chunk::Extent();
+       pos -= chunk;
+       EntityState state = player.GetEntity().GetState();
+       state.chunk_pos = chunk;
+       state.block_pos = pos;
+       player.GetEntity().SetState(state);
+}
+
+}
 
--- /dev/null
+#ifndef BLANK_SHARED_COMMANDS_HPP_
+#define BLANK_SHARED_COMMANDS_HPP_
+
+#include "CLI.hpp"
+
+
+namespace blank {
+
+class TeleportCommand
+: public CLI::Command {
+
+       void Execute(CLI &, Player &, TokenStreamReader &) override;
+
+};
+
+}
+
+#endif
 
 : env(env)
 , parent(parent)
 , responder(responder)
+, preset()
 , input(env.assets.small_ui_font) {
        input.Position(glm::vec3(25.0f, -25.0f, -1.0f), Gravity::SOUTH_WEST, Gravity::SOUTH_WEST);
        input.Width(env.viewport.Width() - 50.0f);
        input.Background(glm::vec4(0.5f));
 }
 
+void ChatState::Preset(const std::string &text) {
+       preset = text;
+}
+
+void ChatState::Clear() {
+       preset.clear();
+}
+
 void ChatState::OnResume() {
        OnResize(env.viewport);
        input.Clear();
+       if (!preset.empty()) {
+               input.Insert(preset.c_str());
+       }
        input.Focus(env.viewport);
 }
 
 
 , 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) {
                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 {
 }
 
 void MasterState::OnLineSubmit(const std::string &line) {
-       if (!line.empty()) {
+       if (line.empty()) {
+               return;
+       }
+       if (line[0] == '/' && line.size() > 1 && line[1] != '/') {
+               cli.Execute(player, line.substr(1));
+       } else {
                hud.PostMessage(line);
        }
 }
 
 #include "../audio/SoundBank.hpp"
 #include "../graphics/SkyBox.hpp"
 #include "../shared/ChatState.hpp"
+#include "../shared/CLI.hpp"
 #include "../shared/WorldResources.hpp"
 #include "../ui/DirectInput.hpp"
 #include "../ui/HUD.hpp"
 
        SkyBox sky;
 
+       CLI cli;
+
        PreloadState preload;
        UnloadState unload;
        ChatState chat;