]> git.localhorst.tv Git - blank.git/commitdiff
first draft for client/server architecture
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 2 Sep 2015 15:27:01 +0000 (17:27 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 2 Sep 2015 15:34:26 +0000 (17:34 +0200)
27 files changed:
Makefile
building
running
src/app/Application.hpp
src/app/Assets.hpp
src/app/ClientState.cpp [new file with mode: 0644]
src/app/ClientState.hpp [new file with mode: 0644]
src/app/Environment.hpp
src/app/Runtime.hpp
src/app/ServerState.cpp [new file with mode: 0644]
src/app/ServerState.hpp [new file with mode: 0644]
src/app/State.hpp
src/app/StateControl.hpp
src/app/WorldState.cpp
src/app/app.cpp
src/app/init.cpp
src/app/init.hpp
src/app/runtime.cpp
src/net/Client.hpp [new file with mode: 0644]
src/net/Connection.hpp [new file with mode: 0644]
src/net/Packet.hpp [new file with mode: 0644]
src/net/Server.hpp [new file with mode: 0644]
src/net/io.hpp [new file with mode: 0644]
src/net/net.cpp [new file with mode: 0644]
src/ui/ui.cpp
src/world/ChunkRenderer.hpp
src/world/render.cpp

index 7bafccd6a6e74e2c436e4b0f218736d30237219b..e6e24d9d2845caea6a24b673c9752d48bbeeda00 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 CXX = g++ --std=c++11
 LDXX = g++
 
-LIBS = sdl2 SDL2_image SDL2_ttf glew openal freealut zlib
+LIBS = sdl2 SDL2_image SDL2_net SDL2_ttf glew openal freealut zlib
 
 PKGFLAGS := $(shell pkg-config --cflags $(LIBS))
 PKGLIBS := $(shell pkg-config --libs $(LIBS))
@@ -64,6 +64,9 @@ tests: $(TEST_BIN)
 run: $(ASSET_DEP) blank
        ./blank --save-path saves/
 
+server: $(ASSET_DEP) blank
+       ./blank --server --save-path saves/
+
 gdb: $(ASSET_DEP) blank.debug
        gdb ./blank.debug
 
index bc22d9b55423ad9a8db25396434f5b8cd639b34c..ef837750adae5fea4f624e5d7357822fc4fa47ce 100644 (file)
--- a/building
+++ b/building
@@ -1,11 +1,11 @@
 Dependencies
 ============
 
-       GLEW, GLM, SDL2, SDL2_image, SDL2_ttf, OpenAL, freealut, zlib
+       GLEW, GLM, SDL2, SDL2_image, SDL2_net, SDL2_ttf, OpenAL, freealut, zlib
 
        CppUnit for tests
 
-archlinux: pacman -S glew glm sdl2 sdl2_image sdl2_ttf openal freealut zlib cppunit
+archlinux: pacman -S glew glm sdl2 sdl2_image sdl2_net sdl2_ttf openal freealut zlib cppunit
 
 manual:
        CppUnit http://sourceforge.net/projects/cppunit/
@@ -29,7 +29,10 @@ release (default), debug, profile:
        build executables tuned for running, debugging, and profiling
 
 run:
-       build and execute the main binary
+       build and execute the main binary with state path set to ./saves
+
+server:
+       same as run, only in server mode
 
 test:
        build and run unittests
diff --git a/running b/running
index 8638f4338144b0b9c10e9326fafa1f87873d296d..7c089ff27db902799bb68b1024f1f8ac5bfb0091 100644 (file)
--- a/running
+++ b/running
@@ -34,6 +34,15 @@ Application
 --no-vsync
        disable vsync
 
+--standalone
+       run as standalone (the default)
+
+--client
+       run as client
+
+--server
+       run as server
+
 Interface
 ---------
 
@@ -51,6 +60,15 @@ Interface
        the audio device and sounds will still be allocated
        it just stops the interface from queueing buffers
 
+Network
+-------
+
+--host <hostname>
+       hostname to connect to in client mode
+
+--port <number>
+       port number to connection to (client) or listen on (server)
+
 World
 -----
 
index bae8435a0084811151300e825b9c5fec402f35d8..359599ef73750de04c0ee3de16c489690114a005 100644 (file)
@@ -8,22 +8,27 @@
 namespace blank {
 
 class Environment;
+class HeadlessEnvironment;
 class State;
 class Window;
 
-class Application {
+class HeadlessApplication {
 
 public:
-       explicit Application(Environment &);
-       ~Application();
+       explicit HeadlessApplication(HeadlessEnvironment &);
+       ~HeadlessApplication();
 
-       Application(const Application &) = delete;
-       Application &operator =(const Application &) = delete;
+       void PushState(State *);
+       State *PopState();
+       State *SwitchState(State *);
+       State &GetState();
+       void CommitStates();
+       bool HasState() const noexcept;
 
-       /// run until user quits
+       /// run until out of states
        void Run();
        /// evaluate a single frame of dt milliseconds
-       void Loop(int dt);
+       virtual void Loop(int dt);
 
        /// run for n frames
        void RunN(size_t n);
@@ -32,6 +37,31 @@ public:
        /// run for n frames, assuming t milliseconds for each
        void RunS(size_t n, size_t t);
 
+       /// process all events in SDL's queue
+       void HandleEvents();
+       void Handle(const SDL_Event &);
+       /// integrate to the next step with dt milliseconds passed
+       void Update(int dt);
+
+private:
+       HeadlessEnvironment &env;
+       std::stack<State *> states;
+
+};
+
+
+class Application
+: public HeadlessApplication {
+
+public:
+       explicit Application(Environment &);
+       ~Application();
+
+       Application(const Application &) = delete;
+       Application &operator =(const Application &) = delete;
+
+       void Loop(int dt) override;
+
        /// process all events in SDL's queue
        void HandleEvents();
        void Handle(const SDL_Event &);
@@ -41,15 +71,8 @@ public:
        /// push the current state to display
        void Render();
 
-       void PushState(State *);
-       State *PopState();
-       State *SwitchState(State *);
-       State &GetState();
-       bool HasState() const noexcept;
-
 private:
        Environment &env;
-       std::stack<State *> states;
 
 };
 
index 65c72fe04d73ad30a1fcf9d215244ad3dfe4ec66..d1a4ae710d0cc832fbcb155b2756eebcc9718cba 100644 (file)
@@ -14,10 +14,10 @@ class Sound;
 class Texture;
 class TextureIndex;
 
-class Assets {
+class AssetLoader {
 
 public:
-       explicit Assets(const std::string &base);
+       explicit AssetLoader(const std::string &base);
 
        void LoadBlockTypes(const std::string &set_name, BlockTypeRegistry &, TextureIndex &) const;
        Font LoadFont(const std::string &name, int size) const;
@@ -32,11 +32,15 @@ private:
        std::string textures;
        std::string data;
 
-public:
-       // common assets shared by may states
+};
+
+struct Assets {
+
        Font large_ui_font;
        Font small_ui_font;
 
+       Assets(const AssetLoader &);
+
 };
 
 }
diff --git a/src/app/ClientState.cpp b/src/app/ClientState.cpp
new file mode 100644 (file)
index 0000000..c371235
--- /dev/null
@@ -0,0 +1,42 @@
+#include "ClientState.hpp"
+
+#include "Environment.hpp"
+#include "TextureIndex.hpp"
+
+namespace blank {
+
+ClientState::ClientState(
+       Environment &env,
+       const World::Config &wc,
+       const WorldSave &ws,
+       const Client::Config &cc
+)
+: env(env)
+, block_types()
+, world(block_types, wc, ws)
+, client(cc, world) {
+
+}
+
+
+void ClientState::Handle(const SDL_Event &event) {
+       if (event.type == SDL_QUIT) {
+               env.state.PopAll();
+       }
+}
+
+
+void ClientState::Update(int dt) {
+       client.Handle();
+       client.Update(dt);
+       if (client.TimedOut()) {
+               env.state.Pop();
+       }
+}
+
+
+void ClientState::Render(Viewport &viewport) {
+
+}
+
+}
diff --git a/src/app/ClientState.hpp b/src/app/ClientState.hpp
new file mode 100644 (file)
index 0000000..f54e56f
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef BLANK_APP_CLIENTSTATE_HPP_
+#define BLANK_APP_CLIENTSTATE_HPP_
+
+#include "State.hpp"
+#include "../net/Client.hpp"
+#include "../world/BlockTypeRegistry.hpp"
+#include "../world/World.hpp"
+
+
+namespace blank {
+
+class Environment;
+
+class ClientState
+: public State {
+
+public:
+       ClientState(
+               Environment &,
+               const World::Config &,
+               const WorldSave &,
+               const Client::Config &
+       );
+
+       void Handle(const SDL_Event &) override;
+       void Update(int dt) override;
+       void Render(Viewport &) override;
+
+private:
+       Environment &env;
+       BlockTypeRegistry block_types;
+       World world;
+       Client client;
+
+};
+
+}
+
+#endif
index 4c935d815e2f4a13483452e791a203dcfce31db7..95813fc12dd7ad43f5e6352924939a80c26040e2 100644 (file)
@@ -15,20 +15,33 @@ namespace blank {
 
 class Window;
 
-struct Environment {
+struct HeadlessEnvironment {
+
+       AssetLoader loader;
+
+       FrameCounter counter;
+
+       StateControl state;
+
+
+       explicit HeadlessEnvironment(const std::string &asset_path);
+
+};
+
+
+struct Environment
+: public HeadlessEnvironment {
+
+       Assets assets;
 
        Audio audio;
        Viewport viewport;
        Window &window;
 
-       Assets assets;
        Keymap keymap;
-       FrameCounter counter;
-
-       StateControl state;
 
 
-       explicit Environment(Window &win, const std::string &asset_path);
+       Environment(Window &win, const std::string &asset_path);
 
 };
 
index 81c015c3668456360b1c84dc77587ba026848b1a..ee438537a345ef43e0f884f24ca799588b15ba4a 100644 (file)
@@ -1,6 +1,8 @@
 #ifndef BLANK_RUNTIME_HPP_
 #define BLANK_RUNTIME_HPP_
 
+#include "../net/Client.hpp"
+#include "../net/Server.hpp"
 #include "../ui/Interface.hpp"
 #include "../world/World.hpp"
 
@@ -10,6 +12,8 @@
 
 namespace blank {
 
+class HeadlessApplication;
+
 /// Parse and interpret arguemnts, then set up the environment and execute.
 class Runtime {
 
@@ -27,6 +31,12 @@ public:
                ERROR,
        };
 
+       enum Target {
+               STANDALONE,
+               SERVER,
+               CLIENT,
+       };
+
        struct Config {
                bool vsync = true;
                bool doublebuf = true;
@@ -36,7 +46,9 @@ public:
                std::string save_path;
                std::string world_name = "default";
 
+               Client::Config client = Client::Config();
                Interface::Config interface = Interface::Config();
+               Server::Config server = Server::Config();
                World::Config world = World::Config();
        };
 
@@ -46,9 +58,17 @@ public:
 
        int Execute();
 
+private:
+       void RunStandalone();
+       void RunServer();
+       void RunClient();
+
+       void Run(HeadlessApplication &);
+
 private:
        const char *name;
        Mode mode;
+       Target target;
        std::size_t n;
        std::size_t t;
        Config config;
diff --git a/src/app/ServerState.cpp b/src/app/ServerState.cpp
new file mode 100644 (file)
index 0000000..d26f552
--- /dev/null
@@ -0,0 +1,46 @@
+#include "ServerState.hpp"
+
+#include "Environment.hpp"
+#include "TextureIndex.hpp"
+#include "../net/io.hpp"
+
+#include <iostream>
+
+
+namespace blank {
+
+ServerState::ServerState(
+       HeadlessEnvironment &env,
+       const World::Config &wc,
+       const WorldSave &ws,
+       const Server::Config &sc
+)
+: env(env)
+, block_types()
+, world(block_types, wc, ws)
+, server(sc, world) {
+       TextureIndex tex_index;
+       env.loader.LoadBlockTypes("default", block_types, tex_index);
+
+       std::cout << "listening on UDP port " << sc.port << std::endl;
+}
+
+
+void ServerState::Handle(const SDL_Event &event) {
+       if (event.type == SDL_QUIT) {
+               env.state.PopAll();
+       }
+}
+
+
+void ServerState::Update(int dt) {
+       server.Handle();
+       server.Update(dt);
+}
+
+
+void ServerState::Render(Viewport &viewport) {
+
+}
+
+}
diff --git a/src/app/ServerState.hpp b/src/app/ServerState.hpp
new file mode 100644 (file)
index 0000000..fc9ac19
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef BLANK_APP_SERVERSTATE_HPP_
+#define BLANK_APP_SERVERSTATE_HPP_
+
+#include "State.hpp"
+#include "../net/Server.hpp"
+#include "../world/BlockTypeRegistry.hpp"
+#include "../world/World.hpp"
+
+
+namespace blank {
+
+class HeadlessEnvironment;
+
+class ServerState
+: public State {
+
+public:
+       ServerState(
+               HeadlessEnvironment &,
+               const World::Config &,
+               const WorldSave &,
+               const Server::Config &
+       );
+
+       void Handle(const SDL_Event &) override;
+       void Update(int dt) override;
+       void Render(Viewport &) override;
+
+private:
+       HeadlessEnvironment &env;
+       BlockTypeRegistry block_types;
+       World world;
+       Server server;
+
+};
+
+}
+
+#endif
index 86ff0ad36c84852167f4bbb34b321dcdc7d8210b..f79bc60f03d7c96e6c0668b73ad62688fc0c44c2 100644 (file)
@@ -6,12 +6,12 @@
 
 namespace blank {
 
-class Application;
+class HeadlessApplication;
 class Viewport;
 
 struct State {
 
-       friend class Application;
+       friend class HeadlessApplication;
 
        virtual void Handle(const SDL_Event &) = 0;
 
index 818bce5c0369ab0987d3736a28ea516bc52564c4..c072040bc12f57de2a31abc6a9465fbfa1e3a551 100644 (file)
@@ -6,7 +6,7 @@
 
 namespace blank {
 
-class Application;
+class HeadlessApplication;
 class State;
 
 class StateControl {
@@ -29,7 +29,7 @@ public:
        }
 
 
-       void Commit(Application &);
+       void Commit(HeadlessApplication &);
 
 private:
        enum Command {
index 243b0a1dc519d42ba18d1e6350e60864896480ba..03abe36c163c4b7c120b725fd74a9d3a337f9bd8 100644 (file)
@@ -1,6 +1,7 @@
 #include "WorldState.hpp"
 
 #include "Environment.hpp"
+#include "init.hpp"
 #include "TextureIndex.hpp"
 
 #include <SDL.h>
@@ -23,8 +24,8 @@ WorldState::WorldState(
 , preload(env, world.Loader(), chunk_renderer)
 , unload(env, world.Loader()) {
        TextureIndex tex_index;
-       env.assets.LoadBlockTypes("default", block_types, tex_index);
-       chunk_renderer.LoadTextures(env.assets, tex_index);
+       env.loader.LoadBlockTypes("default", block_types, tex_index);
+       chunk_renderer.LoadTextures(env.loader, tex_index);
        chunk_renderer.FogDensity(wc.fog_density);
        // TODO: better solution for initializing HUD
        interface.SelectNext();
@@ -33,6 +34,7 @@ WorldState::WorldState(
 
 void WorldState::OnEnter() {
        env.state.Push(&preload);
+       env.window.GrabMouse();
 }
 
 
index 0e2a639f21053ad234353424efef7a2fd2383bd9..48a59502ad974dac9d98eed82d49eaaeaa13487b 100644 (file)
@@ -29,18 +29,29 @@ using std::string;
 
 namespace blank {
 
-Application::Application(Environment &e)
+HeadlessApplication::HeadlessApplication(HeadlessEnvironment &e)
 : env(e)
 , states() {
 
 }
 
+HeadlessApplication::~HeadlessApplication() {
+
+}
+
+
+Application::Application(Environment &e)
+: HeadlessApplication(e)
+, env(e) {
+
+}
+
 Application::~Application() {
        env.audio.StopAll();
 }
 
 
-void Application::RunN(size_t n) {
+void HeadlessApplication::RunN(size_t n) {
        Uint32 last = SDL_GetTicks();
        for (size_t i = 0; HasState() && i < n; ++i) {
                Uint32 now = SDL_GetTicks();
@@ -50,7 +61,7 @@ void Application::RunN(size_t n) {
        }
 }
 
-void Application::RunT(size_t t) {
+void HeadlessApplication::RunT(size_t t) {
        Uint32 last = SDL_GetTicks();
        Uint32 finish = last + t;
        while (HasState() && last < finish) {
@@ -61,7 +72,7 @@ void Application::RunT(size_t t) {
        }
 }
 
-void Application::RunS(size_t n, size_t t) {
+void HeadlessApplication::RunS(size_t n, size_t t) {
        for (size_t i = 0; HasState() && i < n; ++i) {
                Loop(t);
                std::cout << '.';
@@ -74,9 +85,8 @@ void Application::RunS(size_t n, size_t t) {
 }
 
 
-void Application::Run() {
+void HeadlessApplication::Run() {
        Uint32 last = SDL_GetTicks();
-       env.window.GrabMouse();
        while (HasState()) {
                Uint32 now = SDL_GetTicks();
                int delta = now - last;
@@ -85,24 +95,47 @@ void Application::Run() {
        }
 }
 
+void HeadlessApplication::Loop(int dt) {
+       env.counter.EnterFrame();
+       Update(dt);
+       CommitStates();
+       if (!HasState()) return;
+       env.counter.ExitFrame();
+}
+
 void Application::Loop(int dt) {
        env.counter.EnterFrame();
        HandleEvents();
        if (!HasState()) return;
        Update(dt);
-       env.state.Commit(*this);
+       CommitStates();
        if (!HasState()) return;
        Render();
        env.counter.ExitFrame();
 }
 
 
+void HeadlessApplication::HandleEvents() {
+       env.counter.EnterHandle();
+       SDL_Event event;
+       while (HasState() && SDL_PollEvent(&event)) {
+               Handle(event);
+               CommitStates();
+       }
+       env.counter.ExitHandle();
+}
+
+void HeadlessApplication::Handle(const SDL_Event &event) {
+       GetState().Handle(event);
+}
+
+
 void Application::HandleEvents() {
        env.counter.EnterHandle();
        SDL_Event event;
        while (HasState() && SDL_PollEvent(&event)) {
                Handle(event);
-               env.state.Commit(*this);
+               CommitStates();
        }
        env.counter.ExitHandle();
 }
@@ -134,6 +167,14 @@ void Application::Handle(const SDL_WindowEvent &event) {
        }
 }
 
+void HeadlessApplication::Update(int dt) {
+       env.counter.EnterUpdate();
+       if (HasState()) {
+               GetState().Update(dt);
+       }
+       env.counter.ExitUpdate();
+}
+
 void Application::Update(int dt) {
        env.counter.EnterUpdate();
        env.audio.Update(dt);
@@ -158,7 +199,7 @@ void Application::Render() {
 }
 
 
-void Application::PushState(State *s) {
+void HeadlessApplication::PushState(State *s) {
        if (!states.empty()) {
                states.top()->OnPause();
        }
@@ -170,7 +211,7 @@ void Application::PushState(State *s) {
        s->OnResume();
 }
 
-State *Application::PopState() {
+State *HeadlessApplication::PopState() {
        State *s = states.top();
        states.pop();
        s->OnPause();
@@ -181,7 +222,7 @@ State *Application::PopState() {
        return s;
 }
 
-State *Application::SwitchState(State *s_new) {
+State *HeadlessApplication::SwitchState(State *s_new) {
        State *s_old = states.top();
        states.top() = s_new;
        --s_old->ref_count;
@@ -197,16 +238,20 @@ State *Application::SwitchState(State *s_new) {
        return s_old;
 }
 
-State &Application::GetState() {
+State &HeadlessApplication::GetState() {
        return *states.top();
 }
 
-bool Application::HasState() const noexcept {
+void HeadlessApplication::CommitStates() {
+       env.state.Commit(*this);
+}
+
+bool HeadlessApplication::HasState() const noexcept {
        return !states.empty();
 }
 
 
-void StateControl::Commit(Application &app) {
+void StateControl::Commit(HeadlessApplication &app) {
        while (!cue.empty()) {
                Memo m(cue.front());
                cue.pop();
@@ -230,13 +275,17 @@ void StateControl::Commit(Application &app) {
 }
 
 
-Assets::Assets(const string &base)
+AssetLoader::AssetLoader(const string &base)
 : fonts(base + "fonts/")
 , sounds(base + "sounds/")
 , textures(base + "textures/")
-, data(base + "data/")
-, large_ui_font(LoadFont("DejaVuSans", 24))
-, small_ui_font(LoadFont("DejaVuSans", 16)) {
+, data(base + "data/") {
+
+}
+
+Assets::Assets(const AssetLoader &loader)
+: large_ui_font(loader.LoadFont("DejaVuSans", 24))
+, small_ui_font(loader.LoadFont("DejaVuSans", 16)) {
 
 }
 
@@ -248,7 +297,7 @@ CuboidShape slab_shape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.0f, 0.5f }});
 
 }
 
-void Assets::LoadBlockTypes(const std::string &set_name, BlockTypeRegistry &reg, TextureIndex &tex_index) const {
+void AssetLoader::LoadBlockTypes(const std::string &set_name, BlockTypeRegistry &reg, TextureIndex &tex_index) const {
        string full = data + set_name + ".types";
        std::ifstream file(full);
        if (!file) {
@@ -314,17 +363,17 @@ void Assets::LoadBlockTypes(const std::string &set_name, BlockTypeRegistry &reg,
        }
 }
 
-Font Assets::LoadFont(const string &name, int size) const {
+Font AssetLoader::LoadFont(const string &name, int size) const {
        string full = fonts + name + ".ttf";
        return Font(full.c_str(), size);
 }
 
-Sound Assets::LoadSound(const string &name) const {
+Sound AssetLoader::LoadSound(const string &name) const {
        string full = sounds + name + ".wav";
        return Sound(full.c_str());
 }
 
-Texture Assets::LoadTexture(const string &name) const {
+Texture AssetLoader::LoadTexture(const string &name) const {
        string full = textures + name + ".png";
        Texture tex;
        SDL_Surface *srf = IMG_Load(full.c_str());
@@ -337,7 +386,7 @@ Texture Assets::LoadTexture(const string &name) const {
        return tex;
 }
 
-void Assets::LoadTexture(const string &name, ArrayTexture &tex, int layer) const {
+void AssetLoader::LoadTexture(const string &name, ArrayTexture &tex, int layer) const {
        string full = textures + name + ".png";
        SDL_Surface *srf = IMG_Load(full.c_str());
        if (!srf) {
@@ -353,7 +402,7 @@ void Assets::LoadTexture(const string &name, ArrayTexture &tex, int layer) const
        SDL_FreeSurface(srf);
 }
 
-void Assets::LoadTextures(const TextureIndex &index, ArrayTexture &tex) const {
+void AssetLoader::LoadTextures(const TextureIndex &index, ArrayTexture &tex) const {
        // TODO: where the hell should that size come from?
        tex.Reserve(16, 16, index.Size(), Format());
        for (const auto &entry : index.Entries()) {
index 18816a424bdb3a972341311d72a9cda19fa19ac6..81c2e9291cec57c61d039e223922c73d52adf8fd 100644 (file)
@@ -4,6 +4,7 @@
 #include <alut.h>
 #include <SDL.h>
 #include <SDL_image.h>
+#include <SDL_net.h>
 #include <SDL_ttf.h>
 #include <GL/glew.h>
 
@@ -20,6 +21,15 @@ std::string sdl_error_append(std::string msg) {
        return msg;
 }
 
+std::string net_error_append(std::string msg) {
+       const char *error = SDLNet_GetError();
+       if (*error != '\0') {
+               msg += ": ";
+               msg += error;
+       }
+       return msg;
+}
+
 std::string alut_error_append(ALenum num, std::string msg) {
        const char *error = alutGetErrorString(num);
        if (*error != '\0') {
@@ -44,6 +54,17 @@ AlutError::AlutError(ALenum num, const std::string &msg)
 }
 
 
+NetError::NetError()
+: std::runtime_error(SDLNet_GetError()) {
+
+}
+
+NetError::NetError(const std::string &msg)
+: std::runtime_error(net_error_append(msg)) {
+
+}
+
+
 SDLError::SDLError()
 : std::runtime_error(SDL_GetError()) {
 
@@ -56,8 +77,8 @@ SDLError::SDLError(const std::string &msg)
 
 
 InitSDL::InitSDL() {
-       if (SDL_Init(SDL_INIT_VIDEO) != 0) {
-               throw SDLError("SDL_Init(SDL_INIT_VIDEO)");
+       if (SDL_Init(0) != 0) {
+               throw SDLError("SDL_Init(0)");
        }
 }
 
@@ -66,6 +87,17 @@ InitSDL::~InitSDL() {
 }
 
 
+InitVideo::InitVideo() {
+       if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
+               throw SDLError("SDL_InitSubSystem(SDL_INIT_VIDEO)");
+       }
+}
+
+InitVideo::~InitVideo() {
+       SDL_QuitSubSystem(SDL_INIT_VIDEO);
+}
+
+
 InitIMG::InitIMG() {
        if (IMG_Init(IMG_INIT_PNG) == 0) {
                throw SDLError("IMG_Init(IMG_INIT_PNG)");
@@ -77,6 +109,17 @@ InitIMG::~InitIMG() {
 }
 
 
+InitNet::InitNet() {
+       if (SDLNet_Init() != 0) {
+               throw SDLError("SDLNet_Init()");
+       }
+}
+
+InitNet::~InitNet() {
+       SDLNet_Quit();
+}
+
+
 InitTTF::InitTTF() {
        if (TTF_Init() != 0) {
                throw SDLError("TTF_Init()");
@@ -198,8 +241,14 @@ InitGLEW::InitGLEW() {
 }
 
 
-Init::Init(bool double_buffer, int sample_size)
+InitHeadless::InitHeadless()
 : init_sdl()
+, init_net() {
+
+}
+
+Init::Init(bool double_buffer, int sample_size)
+: init_video()
 , init_img()
 , init_ttf()
 , init_gl(double_buffer, sample_size)
index 13bffa1f30e8886087ac441319f0f9af766f8aa3..66c3345fa170a108ee71189648a0384ddc54c2b5 100644 (file)
@@ -27,6 +27,15 @@ public:
 
 };
 
+class NetError
+: public std::runtime_error {
+
+public:
+       NetError();
+       explicit NetError(const std::string &);
+
+};
+
 
 class InitSDL {
 
@@ -40,6 +49,18 @@ public:
 };
 
 
+class InitVideo {
+
+public:
+       InitVideo();
+       ~InitVideo();
+
+       InitVideo(const InitVideo &) = delete;
+       InitVideo &operator =(const InitVideo &) = delete;
+
+};
+
+
 class InitIMG {
 
 public:
@@ -52,6 +73,18 @@ public:
 };
 
 
+class InitNet {
+
+public:
+       InitNet();
+       ~InitNet();
+
+       InitNet(const InitNet &) = delete;
+       InitNet &operator =(const InitNet &) = delete;
+
+};
+
+
 class InitTTF {
 
 public:
@@ -138,11 +171,20 @@ public:
 };
 
 
+struct InitHeadless {
+
+       InitHeadless();
+
+       InitSDL init_sdl;
+       InitNet init_net;
+
+};
+
 struct Init {
 
        Init(bool double_buffer = true, int sample_size = 1);
 
-       InitSDL init_sdl;
+       InitVideo init_video;
        InitIMG init_img;
        InitTTF init_ttf;
        InitAL init_al;
index d77cf47c228fbbdbc573f1e5e26f2473f2ab00ed..2f0f82ec1522e845602236aa32cf745d0cfc90f6 100644 (file)
@@ -1,6 +1,8 @@
 #include "Application.hpp"
+#include "ClientState.hpp"
 #include "Environment.hpp"
 #include "Runtime.hpp"
+#include "ServerState.hpp"
 #include "WorldState.hpp"
 
 #include "init.hpp"
@@ -45,12 +47,20 @@ string default_save_path() {
 
 namespace blank {
 
+HeadlessEnvironment::HeadlessEnvironment(const string &asset_path)
+: loader(asset_path)
+, counter()
+, state() {
+
+}
+
 Environment::Environment(Window &win, const string &asset_path)
-: audio()
+: HeadlessEnvironment(asset_path)
+, assets(loader)
+, audio()
 , viewport()
 , window(win)
-, assets(asset_path)
-, counter() {
+, keymap() {
        viewport.Clear();
        window.Flip();
        keymap.LoadDefault();
@@ -60,6 +70,7 @@ Environment::Environment(Window &win, const string &asset_path)
 Runtime::Runtime() noexcept
 : name("blank")
 , mode(NORMAL)
+, target(STANDALONE)
 , n(0)
 , t(0)
 , config() {
@@ -100,6 +111,12 @@ void Runtime::ReadArgs(int argc, const char *const *argv) {
                                                config.interface.visual_disabled = true;
                                        } else if (strcmp(param, "no-audio") == 0) {
                                                config.interface.audio_disabled = true;
+                                       } else if (strcmp(param, "standalone") == 0) {
+                                               target = STANDALONE;
+                                       } else if (strcmp(param, "server") == 0) {
+                                               target = SERVER;
+                                       } else if (strcmp(param, "client") == 0) {
+                                               target = CLIENT;
                                        } else if (strcmp(param, "asset-path") == 0) {
                                                ++i;
                                                if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
@@ -108,6 +125,23 @@ void Runtime::ReadArgs(int argc, const char *const *argv) {
                                                } else {
                                                        config.asset_path = argv[i];
                                                }
+                                       } else if (strcmp(param, "host") == 0) {
+                                               ++i;
+                                               if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                       cerr << "missing argument to --host" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.client.host = argv[i];
+                                               }
+                                       } else if (strcmp(param, "port") == 0) {
+                                               ++i;
+                                               if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                       cerr << "missing argument to --port" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.server.port = strtoul(argv[i], nullptr, 10);
+                                                       config.client.port = config.server.port;
+                                               }
                                        } else if (strcmp(param, "save-path") == 0) {
                                                ++i;
                                                if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
@@ -230,11 +264,37 @@ int Runtime::Execute() {
                return 1;
        }
 
+       InitHeadless init_headless;
+
+       switch (target) {
+               default:
+               case STANDALONE:
+                       RunStandalone();
+                       break;
+               case SERVER:
+                       RunServer();
+                       break;
+               case CLIENT:
+                       RunClient();
+                       break;
+       }
+
+       return 0;
+}
+
+void Runtime::RunStandalone() {
        Init init(config.doublebuf, config.multisampling);
 
        Environment env(init.window, config.asset_path);
        env.viewport.VSync(config.vsync);
 
+       WorldSave save(config.save_path + config.world_name + '/');
+       if (save.Exists()) {
+               save.Read(config.world);
+       } else {
+               save.Write(config.world);
+       }
+
        std::string keys_path = config.save_path + "keys.conf";
        if (!is_file(keys_path)) {
                std::ofstream file(keys_path);
@@ -244,6 +304,33 @@ int Runtime::Execute() {
                env.keymap.Load(file);
        }
 
+       Application app(env);
+       WorldState world_state(env, config.interface, config.world, save);
+       app.PushState(&world_state);
+       Run(app);
+}
+
+void Runtime::RunServer() {
+       HeadlessEnvironment env(config.asset_path);
+
+       WorldSave save(config.save_path + config.world_name + '/');
+       if (save.Exists()) {
+               save.Read(config.world);
+       } else {
+               save.Write(config.world);
+       }
+
+       HeadlessApplication app(env);
+       ServerState server_state(env, config.world, save, config.server);
+       app.PushState(&server_state);
+       Run(app);
+}
+
+void Runtime::RunClient() {
+       Init init(config.doublebuf, config.multisampling);
+
+       Environment env(init.window, config.asset_path);
+       env.viewport.VSync(config.vsync);
 
        WorldSave save(config.save_path + config.world_name + '/');
        if (save.Exists()) {
@@ -252,11 +339,22 @@ int Runtime::Execute() {
                save.Write(config.world);
        }
 
-       Application app(env);
+       std::string keys_path = config.save_path + "keys.conf";
+       if (!is_file(keys_path)) {
+               std::ofstream file(keys_path);
+               env.keymap.Save(file);
+       } else {
+               std::ifstream file(keys_path);
+               env.keymap.Load(file);
+       }
 
-       WorldState world_state(env, config.interface, config.world, save);
-       app.PushState(&world_state);
+       Application app(env);
+       ClientState client_state(env, config.world, save, config.client);
+       app.PushState(&client_state);
+       Run(app);
+}
 
+void Runtime::Run(HeadlessApplication &app) {
        switch (mode) {
                default:
                case NORMAL:
@@ -272,8 +370,6 @@ int Runtime::Execute() {
                        app.RunS(n, t);
                        break;
        }
-
-       return 0;
 }
 
 }
diff --git a/src/net/Client.hpp b/src/net/Client.hpp
new file mode 100644 (file)
index 0000000..63c3fdd
--- /dev/null
@@ -0,0 +1,45 @@
+#ifndef BLANK_NET_CLIENT_HPP_
+#define BLANK_NET_CLIENT_HPP_
+
+#include "Connection.hpp"
+
+#include <string>
+#include <SDL_net.h>
+
+
+namespace blank {
+
+class World;
+
+class Client {
+
+public:
+       struct Config {
+               std::string host = "localhost";
+               Uint16 port = 12354;
+       };
+
+public:
+       Client(const Config &, World &);
+       ~Client();
+
+       void Handle();
+
+       void Update(int dt);
+
+       bool TimedOut() { return conn.TimedOut(); }
+
+private:
+       void HandlePacket(const UDPpacket &);
+
+private:
+       World &world;
+       Connection conn;
+       UDPsocket client_sock;
+       UDPpacket client_pack;
+
+};
+
+}
+
+#endif
diff --git a/src/net/Connection.hpp b/src/net/Connection.hpp
new file mode 100644 (file)
index 0000000..49e9cda
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef BLANK_NET_CONNECTION_HPP_
+#define BLANK_NET_CONNECTION_HPP_
+
+#include "../app/IntervalTimer.hpp"
+
+#include <SDL_net.h>
+
+
+namespace blank {
+
+class Connection {
+
+public:
+       explicit Connection(const IPaddress &);
+
+       const IPaddress &Address() const noexcept { return addr; }
+
+       bool Matches(const IPaddress &) const noexcept;
+
+       void FlagSend() noexcept;
+       void FlagRecv() noexcept;
+
+       bool ShouldPing() const noexcept;
+       bool TimedOut() const noexcept;
+
+       void Update(int dt);
+
+
+       void SendPing(UDPpacket &, UDPsocket);
+
+       void Send(UDPpacket &, UDPsocket);
+
+private:
+       IPaddress addr;
+       IntervalTimer send_timer;
+       IntervalTimer recv_timer;
+
+};
+
+}
+
+#endif
diff --git a/src/net/Packet.hpp b/src/net/Packet.hpp
new file mode 100644 (file)
index 0000000..7b482e5
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef BLANK_NET_PACKET_HPP_
+#define BLANK_NET_PACKET_HPP_
+
+#include <cstdint>
+
+
+namespace blank {
+
+struct Packet {
+
+       static constexpr std::uint32_t TAG = 0xFB1AB1AF;
+
+       enum Type {
+               PING,
+       };
+
+       struct Header {
+               std::uint32_t tag;
+               std::uint8_t type;
+       } header;
+
+       std::uint8_t payload[500 - sizeof(Header)];
+
+
+       void Tag() noexcept;
+
+       std::size_t Ping() noexcept;
+
+};
+
+}
+
+#endif
diff --git a/src/net/Server.hpp b/src/net/Server.hpp
new file mode 100644 (file)
index 0000000..b6a72a9
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef BLANK_NET_SERVER_HPP
+#define BLANK_NET_SERVER_HPP
+
+#include <list>
+#include <SDL_net.h>
+
+
+namespace blank {
+
+class Connection;
+class World;
+
+class Server {
+
+public:
+       struct Config {
+               Uint16 port = 12354;
+       };
+
+public:
+       Server(const Config &, World &);
+       ~Server();
+
+       void Handle();
+
+       void Update(int dt);
+
+private:
+       void HandlePacket(const UDPpacket &);
+
+       Connection &GetClient(const IPaddress &);
+
+       void OnConnect(Connection &);
+       void OnDisconnect(Connection &);
+
+private:
+       UDPsocket serv_sock;
+       UDPpacket serv_pack;
+       std::list<Connection> clients;
+
+       World &world;
+
+};
+
+}
+
+#endif
diff --git a/src/net/io.hpp b/src/net/io.hpp
new file mode 100644 (file)
index 0000000..d3fbba2
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef BLANK_NET_IO_HPP
+#define BLANK_NET_IO_HPP
+
+#include <iosfwd>
+
+
+namespace blank {
+
+std::ostream &operator <<(std::ostream &, const IPaddress &);
+
+}
+
+#endif
diff --git a/src/net/net.cpp b/src/net/net.cpp
new file mode 100644 (file)
index 0000000..73ed73c
--- /dev/null
@@ -0,0 +1,247 @@
+#include "Client.hpp"
+#include "Connection.hpp"
+#include "io.hpp"
+#include "Packet.hpp"
+#include "Server.hpp"
+
+#include "../app/init.hpp"
+
+#include <cstring>
+#include <iostream>
+
+using namespace std;
+
+
+namespace blank {
+
+namespace {
+
+UDPsocket client_bind(Uint16 port) {
+       UDPsocket sock = SDLNet_UDP_Open(port);
+       if (!sock) {
+               throw NetError("SDLNet_UDP_Open");
+       }
+       return sock;
+}
+
+IPaddress client_resolve(const char *host, Uint16 port) {
+       IPaddress addr;
+       if (SDLNet_ResolveHost(&addr, host, port) != 0) {
+               throw NetError("SDLNet_ResolveHost");
+       }
+       return addr;
+}
+
+}
+
+Client::Client(const Config &conf, World &world)
+: world(world)
+, conn(client_resolve(conf.host.c_str(), conf.port))
+, client_sock(client_bind(0))
+, client_pack{ -1, nullptr, 0 } {
+       client_pack.data = new Uint8[sizeof(Packet)];
+       client_pack.maxlen = sizeof(Packet);
+       // establish connection
+       conn.SendPing(client_pack, client_sock);
+}
+
+Client::~Client() {
+       delete[] client_pack.data;
+       SDLNet_UDP_Close(client_sock);
+}
+
+
+void Client::Handle() {
+       int result = SDLNet_UDP_Recv(client_sock, &client_pack);
+       while (result > 0) {
+               HandlePacket(client_pack);
+               result = SDLNet_UDP_Recv(client_sock, &client_pack);
+       }
+       if (result == -1) {
+               // a boo boo happened
+               throw NetError("SDLNet_UDP_Recv");
+       }
+}
+
+void Client::HandlePacket(const UDPpacket &udp_pack) {
+       if (!conn.Matches(udp_pack.address)) {
+               // packet came from somewhere else, drop
+               return;
+       }
+       const Packet &pack = *reinterpret_cast<const Packet *>(udp_pack.data);
+       if (pack.header.tag != Packet::TAG) {
+               // mistagged packet, drop
+               return;
+       }
+
+       conn.FlagRecv();
+       cout << "I got something!" << endl;
+}
+
+void Client::Update(int dt) {
+       conn.Update(dt);
+       if (conn.TimedOut()) {
+               cout << "connection timed out :(" << endl;
+       } else if (conn.ShouldPing()) {
+               conn.SendPing(client_pack, client_sock);
+       }
+}
+
+
+Connection::Connection(const IPaddress &addr)
+: addr(addr)
+, send_timer(5000)
+, recv_timer(10000) {
+       send_timer.Start();
+       recv_timer.Start();
+}
+
+bool Connection::Matches(const IPaddress &remote) const noexcept {
+       return memcmp(&addr, &remote, sizeof(IPaddress)) == 0;
+}
+
+void Connection::FlagSend() noexcept {
+       send_timer.Reset();
+}
+
+void Connection::FlagRecv() noexcept {
+       recv_timer.Reset();
+}
+
+bool Connection::ShouldPing() const noexcept {
+       return send_timer.HitOnce();
+}
+
+bool Connection::TimedOut() const noexcept {
+       return recv_timer.HitOnce();
+}
+
+void Connection::Update(int dt) {
+       send_timer.Update(dt);
+       recv_timer.Update(dt);
+}
+
+
+void Connection::Send(UDPpacket &pack, UDPsocket sock) {
+       pack.address = addr;
+       if (SDLNet_UDP_Send(sock, -1, &pack) == 0) {
+               throw NetError("SDLNet_UDP_Send");
+       }
+       FlagSend();
+}
+
+void Connection::SendPing(UDPpacket &udp_pack, UDPsocket sock) {
+       Packet &pack = *reinterpret_cast<Packet *>(udp_pack.data);
+       udp_pack.len = pack.Ping();
+       Send(udp_pack, sock);
+}
+
+
+ostream &operator <<(ostream &out, const IPaddress &addr) {
+       const unsigned char *host = reinterpret_cast<const unsigned char *>(&addr.host);
+       out << int(host[0])
+               << '.' << int(host[1])
+               << '.' << int(host[2])
+               << '.' << int(host[3]);
+       if (addr.port) {
+               out << ':' << SDLNet_Read16(&addr.port);
+       }
+       return out;
+}
+
+
+void Packet::Tag() noexcept {
+       header.tag = TAG;
+}
+
+size_t Packet::Ping() noexcept {
+       Tag();
+       header.type = PING;
+       return sizeof(Header);
+}
+
+
+Server::Server(const Config &conf, World &world)
+: serv_sock(nullptr)
+, serv_pack{ -1, nullptr, 0 }
+, clients()
+, world(world) {
+       serv_sock = SDLNet_UDP_Open(conf.port);
+       if (!serv_sock) {
+               throw NetError("SDLNet_UDP_Open");
+       }
+
+       serv_pack.data = new Uint8[sizeof(Packet)];
+       serv_pack.maxlen = sizeof(Packet);
+}
+
+Server::~Server() {
+       delete[] serv_pack.data;
+       SDLNet_UDP_Close(serv_sock);
+}
+
+
+void Server::Handle() {
+       int result = SDLNet_UDP_Recv(serv_sock, &serv_pack);
+       while (result > 0) {
+               HandlePacket(serv_pack);
+               result = SDLNet_UDP_Recv(serv_sock, &serv_pack);
+       }
+       if (result == -1) {
+               // a boo boo happened
+               throw NetError("SDLNet_UDP_Recv");
+       }
+}
+
+void Server::HandlePacket(const UDPpacket &udp_pack) {
+       if (udp_pack.len < int(sizeof(Packet::Header))) {
+               // packet too small, drop
+               return;
+       }
+       const Packet &pack = *reinterpret_cast<const Packet *>(udp_pack.data);
+       if (pack.header.tag != Packet::TAG) {
+               // mistagged packet, drop
+               return;
+       }
+
+       Connection &client = GetClient(udp_pack.address);
+       client.FlagRecv();
+}
+
+Connection &Server::GetClient(const IPaddress &addr) {
+       for (Connection &client : clients) {
+               if (client.Matches(addr)) {
+                       return client;
+               }
+       }
+       clients.emplace_back(addr);
+       OnConnect(clients.back());
+       return clients.back();
+}
+
+void Server::OnConnect(Connection &client) {
+       cout << "new connection from " << client.Address() << endl;
+       // tell it we're alive
+       client.SendPing(serv_pack, serv_sock);
+}
+
+void Server::Update(int dt) {
+       for (list<Connection>::iterator client(clients.begin()), end(clients.end()); client != end;) {
+               client->Update(dt);
+               if (client->TimedOut()) {
+                       OnDisconnect(*client);
+                       client = clients.erase(client);
+               } else {
+                       if (client->ShouldPing()) {
+                               client->SendPing(serv_pack, serv_sock);
+                       }
+                       ++client;
+               }
+       }
+}
+
+void Server::OnDisconnect(Connection &client) {
+       cout << "connection timeout from " << client.Address() << endl;
+}
+
+}
index acf877fd1da6811dca1679d80461d7c86105f0ee..e6376614dacd3525bf4cfed6999d467bb99c2bea 100644 (file)
@@ -125,8 +125,8 @@ Interface::Interface(
 , remove_timer(256)
 , remove(0)
 , selection(0)
-, place_sound(env.assets.LoadSound("thump"))
-, remove_sound(env.assets.LoadSound("plop"))
+, place_sound(env.loader.LoadSound("thump"))
+, remove_sound(env.loader.LoadSound("plop"))
 , fwd(0)
 , rev(0)
 , debug(false) {
index 3240c3de8349f14257b30fa8d08855faf200693a..fe4f6a66d0062a6a81fac5ed5fcee89a291d62f5 100644 (file)
@@ -11,7 +11,7 @@
 
 namespace blank {
 
-class Assets;
+class AssetLoader;
 class TextureIndex;
 class Viewport;
 class World;
@@ -22,7 +22,7 @@ public:
        /// render_distance in chunks, excluding the base chunk which is always rendered
        ChunkRenderer(World &, int render_distance);
 
-       void LoadTextures(const Assets &, const TextureIndex &);
+       void LoadTextures(const AssetLoader &, const TextureIndex &);
        void FogDensity(float d) noexcept { fog_density = d; }
 
        bool InRange(const Chunk::Pos &) const noexcept;
index f701faf724f4c9e407950f27cf9ef1907941260c..4a237e72cfa7cbd9ff68ba03606874c3f1f3f376 100644 (file)
@@ -24,9 +24,9 @@ ChunkRenderer::ChunkRenderer(World &world, int rd)
 }
 
 
-void ChunkRenderer::LoadTextures(const Assets &assets, const TextureIndex &tex_index) {
+void ChunkRenderer::LoadTextures(const AssetLoader &loader, const TextureIndex &tex_index) {
        block_tex.Bind();
-       assets.LoadTextures(tex_index, block_tex);
+       loader.LoadTextures(tex_index, block_tex);
        block_tex.FilterNearest();
 }