From: Daniel Karbach Date: Wed, 2 Sep 2015 15:27:01 +0000 (+0200) Subject: first draft for client/server architecture X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=9ebe2c320fd9f94266ab93fa2f9d9908a0a284d3;p=blank.git first draft for client/server architecture --- diff --git a/Makefile b/Makefile index 7bafccd..e6e24d9 100644 --- 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 diff --git a/building b/building index bc22d9b..ef83775 100644 --- 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 8638f43..7c089ff 100644 --- 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 to connect to in client mode + +--port + port number to connection to (client) or listen on (server) + World ----- diff --git a/src/app/Application.hpp b/src/app/Application.hpp index bae8435..359599e 100644 --- a/src/app/Application.hpp +++ b/src/app/Application.hpp @@ -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 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 states; }; diff --git a/src/app/Assets.hpp b/src/app/Assets.hpp index 65c72fe..d1a4ae7 100644 --- a/src/app/Assets.hpp +++ b/src/app/Assets.hpp @@ -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 index 0000000..c371235 --- /dev/null +++ b/src/app/ClientState.cpp @@ -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 index 0000000..f54e56f --- /dev/null +++ b/src/app/ClientState.hpp @@ -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 diff --git a/src/app/Environment.hpp b/src/app/Environment.hpp index 4c935d8..95813fc 100644 --- a/src/app/Environment.hpp +++ b/src/app/Environment.hpp @@ -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); }; diff --git a/src/app/Runtime.hpp b/src/app/Runtime.hpp index 81c015c..ee43853 100644 --- a/src/app/Runtime.hpp +++ b/src/app/Runtime.hpp @@ -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 index 0000000..d26f552 --- /dev/null +++ b/src/app/ServerState.cpp @@ -0,0 +1,46 @@ +#include "ServerState.hpp" + +#include "Environment.hpp" +#include "TextureIndex.hpp" +#include "../net/io.hpp" + +#include + + +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 index 0000000..fc9ac19 --- /dev/null +++ b/src/app/ServerState.hpp @@ -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 diff --git a/src/app/State.hpp b/src/app/State.hpp index 86ff0ad..f79bc60 100644 --- a/src/app/State.hpp +++ b/src/app/State.hpp @@ -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; diff --git a/src/app/StateControl.hpp b/src/app/StateControl.hpp index 818bce5..c072040 100644 --- a/src/app/StateControl.hpp +++ b/src/app/StateControl.hpp @@ -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 { diff --git a/src/app/WorldState.cpp b/src/app/WorldState.cpp index 243b0a1..03abe36 100644 --- a/src/app/WorldState.cpp +++ b/src/app/WorldState.cpp @@ -1,6 +1,7 @@ #include "WorldState.hpp" #include "Environment.hpp" +#include "init.hpp" #include "TextureIndex.hpp" #include @@ -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(); } diff --git a/src/app/app.cpp b/src/app/app.cpp index 0e2a639..48a5950 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -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 ®, TextureIndex &tex_index) const { +void AssetLoader::LoadBlockTypes(const std::string &set_name, BlockTypeRegistry ®, 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 ®, } } -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()) { diff --git a/src/app/init.cpp b/src/app/init.cpp index 18816a4..81c2e92 100644 --- a/src/app/init.cpp +++ b/src/app/init.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -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) diff --git a/src/app/init.hpp b/src/app/init.hpp index 13bffa1..66c3345 100644 --- a/src/app/init.hpp +++ b/src/app/init.hpp @@ -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; diff --git a/src/app/runtime.cpp b/src/app/runtime.cpp index d77cf47..2f0f82e 100644 --- a/src/app/runtime.cpp +++ b/src/app/runtime.cpp @@ -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 index 0000000..63c3fdd --- /dev/null +++ b/src/net/Client.hpp @@ -0,0 +1,45 @@ +#ifndef BLANK_NET_CLIENT_HPP_ +#define BLANK_NET_CLIENT_HPP_ + +#include "Connection.hpp" + +#include +#include + + +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 index 0000000..49e9cda --- /dev/null +++ b/src/net/Connection.hpp @@ -0,0 +1,42 @@ +#ifndef BLANK_NET_CONNECTION_HPP_ +#define BLANK_NET_CONNECTION_HPP_ + +#include "../app/IntervalTimer.hpp" + +#include + + +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 index 0000000..7b482e5 --- /dev/null +++ b/src/net/Packet.hpp @@ -0,0 +1,33 @@ +#ifndef BLANK_NET_PACKET_HPP_ +#define BLANK_NET_PACKET_HPP_ + +#include + + +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 index 0000000..b6a72a9 --- /dev/null +++ b/src/net/Server.hpp @@ -0,0 +1,47 @@ +#ifndef BLANK_NET_SERVER_HPP +#define BLANK_NET_SERVER_HPP + +#include +#include + + +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 clients; + + World &world; + +}; + +} + +#endif diff --git a/src/net/io.hpp b/src/net/io.hpp new file mode 100644 index 0000000..d3fbba2 --- /dev/null +++ b/src/net/io.hpp @@ -0,0 +1,13 @@ +#ifndef BLANK_NET_IO_HPP +#define BLANK_NET_IO_HPP + +#include + + +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 index 0000000..73ed73c --- /dev/null +++ b/src/net/net.cpp @@ -0,0 +1,247 @@ +#include "Client.hpp" +#include "Connection.hpp" +#include "io.hpp" +#include "Packet.hpp" +#include "Server.hpp" + +#include "../app/init.hpp" + +#include +#include + +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(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(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(&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(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::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; +} + +} diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index acf877f..e637661 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -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) { diff --git a/src/world/ChunkRenderer.hpp b/src/world/ChunkRenderer.hpp index 3240c3d..fe4f6a6 100644 --- a/src/world/ChunkRenderer.hpp +++ b/src/world/ChunkRenderer.hpp @@ -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; diff --git a/src/world/render.cpp b/src/world/render.cpp index f701faf..4a237e7 100644 --- a/src/world/render.cpp +++ b/src/world/render.cpp @@ -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(); }