]> git.localhorst.tv Git - gong.git/commitdiff
code, assets, and other stuff stolen from blank
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 1 Dec 2016 08:47:46 +0000 (09:47 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 1 Dec 2016 08:47:46 +0000 (09:47 +0100)
87 files changed:
.gitignore [new file with mode: 0644]
.gitmodules [new file with mode: 0644]
Makefile [new file with mode: 0644]
assets [new submodule]
src/app/Application.hpp [new file with mode: 0644]
src/app/AssetLoader.hpp [new file with mode: 0644]
src/app/Assets.hpp [new file with mode: 0644]
src/app/Config.hpp [new file with mode: 0644]
src/app/Environment.hpp [new file with mode: 0644]
src/app/FrameCounter.hpp [new file with mode: 0644]
src/app/HeadlessApplication.hpp [new file with mode: 0644]
src/app/HeadlessEnvironment.hpp [new file with mode: 0644]
src/app/IntervalTimer.hpp [new file with mode: 0644]
src/app/MessageState.hpp [new file with mode: 0644]
src/app/ResourceIndex.hpp [new file with mode: 0644]
src/app/Runtime.hpp [new file with mode: 0644]
src/app/State.hpp [new file with mode: 0644]
src/app/StateControl.hpp [new file with mode: 0644]
src/app/app.cpp [new file with mode: 0644]
src/app/error.cpp [new file with mode: 0644]
src/app/error.hpp [new file with mode: 0644]
src/app/init.cpp [new file with mode: 0644]
src/app/init.hpp [new file with mode: 0644]
src/app/runtime.cpp [new file with mode: 0644]
src/audio/Audio.hpp [new file with mode: 0644]
src/audio/Sound.hpp [new file with mode: 0644]
src/audio/SoundBank.hpp [new file with mode: 0644]
src/audio/audio.cpp [new file with mode: 0644]
src/geometry/const.hpp [new file with mode: 0644]
src/geometry/distance.hpp [new file with mode: 0644]
src/geometry/geometry.cpp [new file with mode: 0644]
src/geometry/primitive.hpp [new file with mode: 0644]
src/geometry/rotation.hpp [new file with mode: 0644]
src/graphics/ArrayTexture.hpp [new file with mode: 0644]
src/graphics/BlendedSprite.hpp [new file with mode: 0644]
src/graphics/Camera.hpp [new file with mode: 0644]
src/graphics/Canvas.hpp [new file with mode: 0644]
src/graphics/CubeMap.hpp [new file with mode: 0644]
src/graphics/Font.hpp [new file with mode: 0644]
src/graphics/Format.hpp [new file with mode: 0644]
src/graphics/PlainColor.hpp [new file with mode: 0644]
src/graphics/PrimitiveMesh.hpp [new file with mode: 0644]
src/graphics/Program.hpp [new file with mode: 0644]
src/graphics/Shader.hpp [new file with mode: 0644]
src/graphics/SkyBox.hpp [new file with mode: 0644]
src/graphics/SkyBoxMesh.hpp [new file with mode: 0644]
src/graphics/SkyBoxShader.hpp [new file with mode: 0644]
src/graphics/SpriteMesh.hpp [new file with mode: 0644]
src/graphics/Texture.hpp [new file with mode: 0644]
src/graphics/TextureBase.hpp [new file with mode: 0644]
src/graphics/VertexArray.hpp [new file with mode: 0644]
src/graphics/VertexArray.inl [new file with mode: 0644]
src/graphics/Viewport.hpp [new file with mode: 0644]
src/graphics/align.hpp [new file with mode: 0644]
src/graphics/gl_traits.cpp [new file with mode: 0644]
src/graphics/gl_traits.hpp [new file with mode: 0644]
src/graphics/glm.hpp [new file with mode: 0644]
src/graphics/mesh.cpp [new file with mode: 0644]
src/graphics/render.cpp [new file with mode: 0644]
src/graphics/shader.cpp [new file with mode: 0644]
src/graphics/viewport.cpp [new file with mode: 0644]
src/io/LineBuffer.hpp [new file with mode: 0644]
src/io/Token.hpp [new file with mode: 0644]
src/io/TokenStreamReader.hpp [new file with mode: 0644]
src/io/Tokenizer.hpp [new file with mode: 0644]
src/io/event.cpp [new file with mode: 0644]
src/io/event.hpp [new file with mode: 0644]
src/io/filesystem.cpp [new file with mode: 0644]
src/io/filesystem.hpp [new file with mode: 0644]
src/io/token.cpp [new file with mode: 0644]
src/ui/FixedText.hpp [new file with mode: 0644]
src/ui/MessageBox.hpp [new file with mode: 0644]
src/ui/Text.hpp [new file with mode: 0644]
src/ui/TextInput.hpp [new file with mode: 0644]
src/ui/widgets.cpp [new file with mode: 0644]
tst/app/TimerTest.cpp [new file with mode: 0644]
tst/app/TimerTest.hpp [new file with mode: 0644]
tst/geometry/IntersectionTest.cpp [new file with mode: 0644]
tst/geometry/IntersectionTest.hpp [new file with mode: 0644]
tst/graphics/GLTraitsTest.cpp [new file with mode: 0644]
tst/graphics/GLTraitsTest.hpp [new file with mode: 0644]
tst/io/EventTest.cpp [new file with mode: 0644]
tst/io/EventTest.hpp [new file with mode: 0644]
tst/io/FilesystemTest.cpp [new file with mode: 0644]
tst/io/FilesystemTest.hpp [new file with mode: 0644]
tst/io/TokenTest.cpp [new file with mode: 0644]
tst/io/TokenTest.hpp [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..02af12c
--- /dev/null
@@ -0,0 +1,11 @@
+*.swp
+*.swo
+*.trace
+build
+cachegrind.out.*
+callgrind.out.*
+client-saves
+gong
+gong.*
+saves
+test.*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644 (file)
index 0000000..268aff3
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "assets"]
+       path = assets
+       url = http://git.localhorst.tv/repo/gong-assets.git
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..cf8a568
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,263 @@
+CXX = g++ --std=c++11
+LDXX = g++
+CPPCHECK = cppcheck -q --std=c++11 \
+       --enable=warning,style,performance,portability,unusedFunction,missingInclude \
+       --error-exitcode=1
+
+LIBS = sdl2 SDL2_image SDL2_net SDL2_ttf glew openal freealut zlib
+
+PKGFLAGS := $(shell pkg-config --cflags $(LIBS))
+PKGLIBS := $(shell pkg-config --libs $(LIBS))
+TESTFLAGS := $(shell pkg-config --cflags cppunit)
+TESTLIBS := $(shell pkg-config --libs cppunit)
+
+CPPFLAGS ?=
+CPPFLAGS += $(PKGFLAGS)
+CXXFLAGS ?=
+CXXFLAGS += -Wall -Wextra -Werror
+#CXXFLAGS += -march=native
+LDXXFLAGS ?=
+LDXXFLAGS += $(PKGLIBS)
+
+# source
+SOURCE_DIR := src
+TEST_SRC_DIR := tst
+
+# build configurations
+# cover:
+#   coverage reporting
+#   for use with gcov
+# debug:
+#   unoptimized and maximally annotated
+#   for use with gdb
+# profile:
+#   somewhat optimized and maximally annotated
+#   for use with valgrind
+# release:
+#   optimized, without debugging instructions and minimally
+#   annotated (mainly for stack traces)
+#   for use with people
+# tests:
+#   same flags as release, but with main replaced by cppunit
+#   test runner and tests (from tst dir) built in
+
+COVER_FLAGS = -g -O0 --coverage -I$(SOURCE_DIR) $(TESTFLAGS) -DGONG_SUFFIX=\".cover\"
+DEBUG_FLAGS = -g3 -O0
+PROFILE_FLAGS = -DNDEBUG -O1 -g3 -DGONG_PROFILING
+RELEASE_FLAGS = -DNDEBUG -O2 -g1
+TEST_FLAGS = -g -O2 -I$(SOURCE_DIR) $(TESTFLAGS) -DGONG_SUFFIX=\".test\"
+
+# destination
+COVER_DIR := build/cover
+DEBUG_DIR := build/debug
+PROFILE_DIR := build/profile
+RELEASE_DIR := build/release
+TEST_DIR := build/test
+
+DIR := $(RELEASE_DIR) $(COVER_DIR) $(DEBUG_DIR) $(PROFILE_DIR) $(TEST_DIR) build
+
+ASSET_DIR := assets
+ASSET_DEP := $(ASSET_DIR)/.git
+
+LIB_SRC := $(wildcard $(SOURCE_DIR)/*/*.cpp)
+BIN_SRC := $(wildcard $(SOURCE_DIR)/*.cpp)
+SRC := $(LIB_SRC) $(BIN_SRC)
+TEST_BIN_SRC := $(wildcard $(TEST_SRC_DIR)/*.cpp)
+TEST_LIB_SRC := $(wildcard $(TEST_SRC_DIR)/*/*.cpp)
+TEST_SRC := $(TEST_LIB_SRC) $(TEST_BIN_SRC)
+
+COVER_LIB_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(COVER_DIR)/src/%.o, $(LIB_SRC))
+COVER_TEST_LIB_OBJ := $(patsubst $(TEST_SRC_DIR)/%.cpp, $(COVER_DIR)/%.o, $(TEST_LIB_SRC))
+COVER_OBJ := $(patsubst $(TEST_SRC_DIR)/%.cpp, $(COVER_DIR)/%.o, $(TEST_SRC)) $(patsubst $(SOURCE_DIR)/%.cpp, $(COVER_DIR)/src/%.o, $(LIB_SRC))
+COVER_DEP := $(COVER_OBJ:.o=.d)
+COVER_BIN := gong.cover
+COVER_TEST_BIN := test.cover
+
+DEBUG_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(DEBUG_DIR)/%.o, $(SRC))
+DEBUG_LIB_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(DEBUG_DIR)/%.o, $(LIB_SRC))
+DEBUG_DEP := $(DEBUG_OBJ:.o=.d)
+DEBUG_BIN := gong.debug
+
+PROFILE_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(PROFILE_DIR)/%.o, $(SRC))
+PROFILE_LIB_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(PROFILE_DIR)/%.o, $(LIB_SRC))
+PROFILE_DEP := $(PROFILE_OBJ:.o=.d)
+PROFILE_BIN := gong.profile
+
+RELEASE_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(RELEASE_DIR)/%.o, $(SRC))
+RELEASE_LIB_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(RELEASE_DIR)/%.o, $(LIB_SRC))
+RELEASE_DEP := $(RELEASE_OBJ:.o=.d)
+RELEASE_BIN := gong
+
+TEST_LIB_OBJ := $(patsubst $(SOURCE_DIR)/%.cpp, $(TEST_DIR)/src/%.o, $(LIB_SRC))
+TEST_TEST_LIB_OBJ := $(patsubst $(TEST_SRC_DIR)/%.cpp, $(TEST_DIR)/%.o, $(TEST_LIB_SRC))
+TEST_OBJ := $(patsubst $(TEST_SRC_DIR)/%.cpp, $(TEST_DIR)/%.o, $(TEST_SRC)) $(patsubst $(SOURCE_DIR)/%.cpp, $(TEST_DIR)/src/%.o, $(LIB_SRC))
+TEST_DEP := $(TEST_OBJ:.o=.d)
+TEST_BIN := gong.test
+TEST_TEST_BIN := test.test
+
+OBJ := $(COVER_OBJ) $(DEBUG_OBJ) $(PROFILE_OBJ) $(RELEASE_OBJ) $(TEST_OBJ)
+DEP := $(COVER_DEP) $(DEBUG_DEP) $(PROFILE_DEP) $(RELEASE_DEP) $(TEST_DEP)
+BIN := $(COVER_BIN) $(DEBUG_BIN) $(PROFILE_BIN) $(RELEASE_BIN) $(TEST_BIN) $(COVER_TEST_BIN) $(TEST_TEST_BIN)
+
+release: $(RELEASE_BIN)
+
+info:
+       @echo "CXX:  $(CXX)"
+       @echo "LDXX: $(LDXX)"
+       @echo
+       @echo "LIBS: $(LIBS)"
+       @echo
+       @echo "CPPFLAGS:  $(CPPFLAGS)"
+       @echo "CXXFLAGS:  $(CXXFLAGS)"
+       @echo "LDXXFLAGS: $(LDXXFLAGS)"
+       @echo "TESTFLAGS: $(TESTFLAGS)"
+       @echo "TESTLIBS:  $(TESTLIBS)"
+       @echo
+       @-lsb_release -a
+       @git --version
+       @g++ --version
+
+all: $(BIN)
+
+cover: $(COVER_BIN) $(COVER_TEST_BIN)
+
+debug: $(DEBUG_BIN)
+
+profile: $(PROFILE_BIN)
+
+tests: $(TEST_BIN) $(TEST_TEST_BIN)
+
+run: $(ASSET_DEP) gong
+       ./gong --save-path saves/
+
+server: $(ASSET_DEP) gong
+       ./gong --server --save-path saves/
+
+client: $(ASSET_DEP) gong
+       ./gong --client --save-path saves/
+
+gdb: $(ASSET_DEP) gong.debug
+       gdb ./gong.debug
+
+cachegrind: $(ASSET_DEP) gong.profile
+       valgrind ./gong.profile --save-path saves/
+
+callgrind: $(ASSET_DEP) gong.profile
+       valgrind --tool=callgrind \
+               --collect-atstart=no --toggle-collect="gong::Runtime::RunStandalone()" \
+               --branch-sim=yes --cacheuse=yes --cache-sim=yes \
+               --collect-bus=yes --collect-systime=yes --collect-jumps=yes \
+               --dump-instr=yes --simulate-hwpref=yes --simulate-wb=yes \
+               ./gong.profile -n 256 -t 16 --no-keyboard --no-mouse -d --no-vsync --save-path saves/
+
+test: $(TEST_BIN) $(TEST_TEST_BIN) $(ASSET_DEP)
+       @echo run: test.test
+       @./test.test
+
+unittest: $(TEST_BIN) $(TEST_TEST_BIN) $(ASSET_DEP)
+       @echo run: test.test --headless
+       @./test.test --headless
+
+coverage: $(COVER_BIN) $(COVER_TEST_BIN) $(ASSET_DEP)
+       @echo run: test.cover
+       @./test.cover
+
+codecov: coverage
+       @echo run: codecov.io
+       @bash -c 'bash <(curl -s https://codecov.io/bash) -Z'
+
+lint:
+       @echo lint: source
+       @$(CPPCHECK) $(SOURCE_DIR)
+       @echo lint: tests
+       @$(CPPCHECK) -I $(SOURCE_DIR) $(TEST_SRC_DIR)
+
+clean:
+       rm -f $(OBJ)
+       rm -f $(DEP)
+       find build -type d -empty -delete
+
+distclean: clean
+       rm -f $(BIN) cachegrind.out.* callgrind.out.*
+       rm -Rf build client-saves saves
+
+.PHONY: all release cover debug profile tests run gdb cachegrind callgrind test unittest coverage codecov lint clean distclean
+
+-include $(DEP)
+
+
+$(COVER_BIN): %.cover: $(COVER_DIR)/src/%.o $(COVER_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(COVER_FLAGS)
+
+$(COVER_TEST_BIN): %.cover: $(COVER_DIR)/%.o $(COVER_LIB_OBJ) $(COVER_TEST_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(TESTLIBS) $(COVER_FLAGS)
+
+$(COVER_DIR)/%.o: $(TEST_SRC_DIR)/%.cpp | $(COVER_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(COVER_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+$(COVER_DIR)/src/%.o: $(SOURCE_DIR)/%.cpp | $(COVER_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(COVER_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+
+$(DEBUG_BIN): %.debug: $(DEBUG_DIR)/%.o $(DEBUG_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(DEBUG_FLAGS)
+
+$(DEBUG_DIR)/%.o: $(SOURCE_DIR)/%.cpp | $(DEBUG_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(DEBUG_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+
+$(PROFILE_BIN): %.profile: $(PROFILE_DIR)/%.o $(PROFILE_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(PROFILE_FLAGS)
+
+$(PROFILE_DIR)/%.o: $(SOURCE_DIR)/%.cpp | $(PROFILE_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(PROFILE_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+
+$(RELEASE_BIN): %: $(RELEASE_DIR)/%.o $(RELEASE_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(RELEASE_FLAGS)
+
+$(RELEASE_DIR)/%.o: $(SOURCE_DIR)/%.cpp | $(RELEASE_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(RELEASE_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+
+$(TEST_BIN): %.test: $(TEST_DIR)/src/%.o $(TEST_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(TEST_FLAGS)
+
+$(TEST_TEST_BIN): %.test: $(TEST_DIR)/%.o $(TEST_LIB_OBJ) $(TEST_TEST_LIB_OBJ)
+       @echo link: $@
+       @$(LDXX) $(CXXFLAGS) $^ -o $@ $(LDXXFLAGS) $(TESTLIBS) $(TEST_FLAGS)
+
+$(TEST_DIR)/%.o: $(TEST_SRC_DIR)/%.cpp | $(TEST_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(TEST_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+$(TEST_DIR)/src/%.o: $(SOURCE_DIR)/%.cpp | $(TEST_DIR)
+       @mkdir -p "$(@D)"
+       @echo compile: $@
+       @$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $(TEST_FLAGS) -o $@ -MMD -MP -MF"$(@:.o=.d)" -MT"$@" $<
+
+
+$(ASSET_DEP): .git/$(shell git symbolic-ref HEAD 2>/dev/null || echo HEAD)
+       @echo fetch: assets
+       @git submodule update --init >/dev/null
+       @touch $@
+
+$(DIR):
+       @mkdir -p "$@"
diff --git a/assets b/assets
new file mode 160000 (submodule)
index 0000000..d6ad423
--- /dev/null
+++ b/assets
@@ -0,0 +1 @@
+Subproject commit d6ad42354623df0d3466f3684797eac2ea93ac05
diff --git a/src/app/Application.hpp b/src/app/Application.hpp
new file mode 100644 (file)
index 0000000..b3669f6
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef GONG_APP_APPLICATION_HPP_
+#define GONG_APP_APPLICATION_HPP_
+
+#include "HeadlessApplication.hpp"
+
+#include <SDL.h>
+
+
+namespace gong {
+namespace app {
+
+class Environment;
+
+
+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 &);
+       void Handle(const SDL_WindowEvent &);
+       /// integrate to the next step with dt milliseconds passed
+       void Update(int dt);
+       /// push the current state to display
+       void Render();
+
+private:
+       Environment &env;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/AssetLoader.hpp b/src/app/AssetLoader.hpp
new file mode 100644 (file)
index 0000000..8b222d7
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef GONG_APP_ASSETLOADER_HPP_
+#define GONG_APP_ASSETLOADER_HPP_
+
+#include <string>
+
+
+namespace gong {
+namespace audio {
+       class Sound;
+}
+namespace graphics {
+       class ArrayTexture;
+       class CubeMap;
+       class Font;
+       class Texture;
+}
+
+namespace app {
+
+class AssetLoader {
+
+public:
+       explicit AssetLoader(const std::string &base);
+
+       graphics::CubeMap LoadCubeMap(const std::string &name) const;
+       graphics::Font LoadFont(const std::string &name, int size) const;
+       audio::Sound LoadSound(const std::string &name) const;
+       graphics::Texture LoadTexture(const std::string &name) const;
+       void LoadTexture(const std::string &name, graphics::ArrayTexture &, int layer) const;
+
+private:
+       std::string fonts;
+       std::string sounds;
+       std::string textures;
+       std::string data;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/Assets.hpp b/src/app/Assets.hpp
new file mode 100644 (file)
index 0000000..1657b9e
--- /dev/null
@@ -0,0 +1,25 @@
+#ifndef GONG_APP_ASSETS_HPP_
+#define GONG_APP_ASSETS_HPP_
+
+#include "../graphics/Font.hpp"
+
+
+namespace gong {
+namespace app {
+
+class AssetLoader;
+
+
+struct Assets {
+
+       graphics::Font large_ui_font;
+       graphics::Font small_ui_font;
+
+       explicit Assets(const AssetLoader &);
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/Config.hpp b/src/app/Config.hpp
new file mode 100644 (file)
index 0000000..d17cbb0
--- /dev/null
@@ -0,0 +1,64 @@
+#ifndef GONG_APP_CONFIG_HPP_
+#define GONG_APP_CONFIG_HPP_
+
+#include <cstdint>
+#include <iosfwd>
+#include <string>
+
+
+namespace gong {
+namespace app {
+
+struct Config {
+
+       struct Audio {
+
+               bool enabled = true;
+
+       } audio;
+
+       struct Input {
+
+               bool keyboard = true;
+               bool mouse = true;
+
+               float pitch_sensitivity = -0.0025f;
+               float yaw_sensitivity = -0.001f;
+
+       } input;
+
+       struct Network {
+
+               std::string host = "localhost";
+               std::uint16_t port = 12364;
+               std::uint16_t cmd_port = 0;
+
+       } net;
+
+       struct Player {
+
+               std::string name = "default";
+
+       } player;
+
+       struct Video {
+
+               bool dblbuf = true;
+               bool vsync = true;
+               int msaa = 1;
+
+               bool hud = true;
+               bool world = true;
+               bool debug = false;
+
+       } video;
+
+       void Load(std::istream &);
+       void Save(std::ostream &);
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/Environment.hpp b/src/app/Environment.hpp
new file mode 100644 (file)
index 0000000..2f4d687
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef GONG_APP_ENVIRONMENT_HPP_
+#define GONG_APP_ENVIRONMENT_HPP_
+
+#include "Assets.hpp"
+#include "HeadlessEnvironment.hpp"
+#include "MessageState.hpp"
+#include "../audio/Audio.hpp"
+#include "../graphics/Viewport.hpp"
+
+
+namespace gong {
+namespace app {
+
+class Window;
+
+
+struct Environment
+: public HeadlessEnvironment {
+
+       Assets assets;
+
+       audio::Audio audio;
+       graphics::Viewport viewport;
+       Window &window;
+
+       MessageState msg_state;
+
+
+       Environment(Window &win, const Config &);
+
+       void ShowMessage(const char *);
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/FrameCounter.hpp b/src/app/FrameCounter.hpp
new file mode 100644 (file)
index 0000000..a155723
--- /dev/null
@@ -0,0 +1,83 @@
+#ifndef GONG_APP_FRAMECOUNTER_HPP_
+#define GONG_APP_FRAMECOUNTER_HPP_
+
+#include <iosfwd>
+#include <SDL.h>
+
+
+namespace gong {
+namespace app {
+
+class FrameCounter {
+
+public:
+       template<class T>
+       struct Frame {
+               T handle;
+               T update;
+               T render;
+               T running;
+               T waiting;
+               T total;
+               Frame();
+       };
+
+
+public:
+       void EnterFrame() noexcept;
+       void EnterHandle() noexcept;
+       void ExitHandle() noexcept;
+       void EnterUpdate() noexcept;
+       void ExitUpdate() noexcept;
+       void EnterRender() noexcept;
+       void ExitRender() noexcept;
+       void ExitFrame() noexcept;
+
+       const Frame<int> &Peak() const noexcept { return peak; }
+       const Frame<float> &Average() const noexcept { return avg; }
+
+       bool Changed() const noexcept { return changed; }
+
+       void Print(std::ostream &) const;
+
+private:
+       int Tick() noexcept;
+
+       void Accumulate() noexcept;
+       void Push() noexcept;
+
+private:
+       static constexpr int NUM_FRAMES = 32;
+       static constexpr float factor = 1.0f / float(NUM_FRAMES);
+
+       Uint32 last_enter = 0;
+       Uint32 last_tick = 0;
+
+       int cur_frame = 0;
+       Frame<int> current = Frame<int>{};
+       Frame<int> sum = Frame<int>{};
+       Frame<int> max = Frame<int>{};
+
+       Frame<int> peak = Frame<int>{};
+       Frame<float> avg = Frame<float>{};
+
+       bool changed = false;
+
+};
+
+
+template<class T>
+FrameCounter::Frame<T>::Frame()
+: handle(0)
+, update(0)
+, render(0)
+, running(0)
+, waiting(0)
+, total(0) {
+
+}
+
+}
+}
+
+#endif
diff --git a/src/app/HeadlessApplication.hpp b/src/app/HeadlessApplication.hpp
new file mode 100644 (file)
index 0000000..4f7d670
--- /dev/null
@@ -0,0 +1,55 @@
+#ifndef GONG_APP_HEADLESSAPPLICATION_HPP_
+#define GONG_APP_HEADLESSAPPLICATION_HPP_
+
+#include <SDL.h>
+#include <stack>
+
+
+namespace gong {
+namespace app {
+
+class HeadlessEnvironment;
+class State;
+
+
+class HeadlessApplication {
+
+public:
+       explicit HeadlessApplication(HeadlessEnvironment &);
+       ~HeadlessApplication();
+
+       void PushState(State *);
+       State *PopState();
+       State *SwitchState(State *);
+       State &GetState();
+       void CommitStates();
+       bool HasState() const noexcept;
+
+       /// run until out of states
+       void Run();
+       /// evaluate a single frame of dt milliseconds
+       virtual void Loop(int dt);
+
+       /// run for n frames
+       void RunN(size_t n);
+       /// run for t milliseconds
+       void RunT(size_t t);
+       /// 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;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/HeadlessEnvironment.hpp b/src/app/HeadlessEnvironment.hpp
new file mode 100644 (file)
index 0000000..ff9aed1
--- /dev/null
@@ -0,0 +1,35 @@
+#ifndef GONG_APP_HEADLESSENVIRONMENT_HPP_
+#define GONG_APP_HEADLESSENVIRONMENT_HPP_
+
+#include "AssetLoader.hpp"
+#include "FrameCounter.hpp"
+#include "StateControl.hpp"
+
+#include <string>
+
+
+namespace gong {
+namespace app {
+
+struct HeadlessEnvironment {
+
+       struct Config {
+               std::string asset_path;
+               std::string save_path;
+       } config;
+
+       AssetLoader loader;
+
+       FrameCounter counter;
+
+       StateControl state;
+
+
+       explicit HeadlessEnvironment(const Config &);
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/IntervalTimer.hpp b/src/app/IntervalTimer.hpp
new file mode 100644 (file)
index 0000000..1d39d4d
--- /dev/null
@@ -0,0 +1,96 @@
+#ifndef GONG_APP_INTERVALTIMER_HPP
+#define GONG_APP_INTERVALTIMER_HPP
+
+#include <cmath>
+
+
+namespace gong {
+namespace app {
+
+/// Timer that hits every n Time units. Resolution is that of the
+/// delta values passed to Update().
+/// Also tracks the number of iterations as well as Time units
+/// passed.
+template<class Time = int>
+class IntervalTimer {
+
+public:
+       /// Create a timer that hits every interval Time units.
+       /// Initial state is stopped.
+       explicit IntervalTimer(Time interval_ms = Time(0)) noexcept
+       : intv(interval_ms) { }
+
+       void Start() noexcept {
+               speed = Time(1);
+       }
+       void Stop() noexcept {
+               value = Time(0);
+               speed = Time(0);
+       }
+       void Reset() noexcept {
+               value = Time(0);
+       }
+
+       bool Running() const noexcept {
+               return speed != Time(0);
+       }
+       /// true if an interval boundary was passed by the last call to Update()
+       bool Hit() const noexcept {
+               return Running() && IntervalElapsed() < last_dt;
+       }
+       bool HitOnce() const noexcept {
+               return Running() && value >= intv;
+       }
+       Time Elapsed() const noexcept {
+               return value;
+       }
+       Time Interval() const noexcept {
+               return intv;
+       }
+       Time IntervalElapsed() const noexcept {
+               return mod(value, intv);
+       }
+       Time IntervalRemain() const noexcept {
+               return intv - IntervalElapsed();
+       }
+       int Iteration() const noexcept {
+               return value / intv;
+       }
+       void PopIteration() noexcept {
+               value -= intv;
+       }
+
+       void Update(Time dt) noexcept {
+               value += dt * speed;
+               last_dt = dt;
+       }
+
+       static Time mod(Time val, Time m) noexcept {
+               return val % m;
+       }
+
+private:
+       Time intv;
+       Time value = Time(0);
+       Time speed = Time(0);
+       Time last_dt = Time(0);
+
+};
+
+using CoarseTimer = IntervalTimer<int>;
+using FineTimer = IntervalTimer<float>;
+
+template<>
+inline float IntervalTimer<float>::mod(float val, float m) noexcept {
+       return std::fmod(val, m);
+}
+
+template<>
+inline int IntervalTimer<float>::Iteration() const noexcept {
+       return std::floor(value / intv);
+}
+
+}
+}
+
+#endif
diff --git a/src/app/MessageState.hpp b/src/app/MessageState.hpp
new file mode 100644 (file)
index 0000000..683fb53
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef GONG_SHARED_MESSAGESTATE_HPP_
+#define GONG_SHARED_MESSAGESTATE_HPP_
+
+#include "State.hpp"
+
+#include "../ui/FixedText.hpp"
+
+
+namespace gong {
+namespace app {
+
+class Environment;
+
+
+class MessageState
+: public State {
+
+public:
+       explicit MessageState(Environment &);
+
+       void SetMessage(const char *);
+       void ClearMessage();
+
+       void Handle(const SDL_Event &) override;
+       void Update(int dt) override;
+       void Render(graphics::Viewport &) override;
+
+private:
+       Environment &env;
+       ui::FixedText message;
+       ui::FixedText press_key;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/ResourceIndex.hpp b/src/app/ResourceIndex.hpp
new file mode 100644 (file)
index 0000000..75d5dab
--- /dev/null
@@ -0,0 +1,31 @@
+#ifndef GONG_APP_RESOURCEINDEX_HPP_
+#define GONG_APP_RESOURCEINDEX_HPP_
+
+#include <map>
+#include <string>
+
+
+namespace gong {
+namespace app {
+
+class ResourceIndex {
+
+       using MapType = std::map<std::string, std::size_t>;
+
+public:
+       ResourceIndex();
+
+       std::size_t GetID(const std::string &);
+
+       std::size_t Size() const noexcept { return id_map.size(); }
+       const MapType &Entries() const noexcept { return id_map; }
+
+private:
+       MapType id_map;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/Runtime.hpp b/src/app/Runtime.hpp
new file mode 100644 (file)
index 0000000..d4118a0
--- /dev/null
@@ -0,0 +1,73 @@
+#ifndef GONG_APP_RUNTIME_HPP_
+#define GONG_APP_RUNTIME_HPP_
+
+#include "Config.hpp"
+#include "HeadlessEnvironment.hpp"
+
+#include <cstddef>
+#include <string>
+
+
+namespace gong {
+namespace app {
+
+class HeadlessApplication;
+
+/// Parse and interpret arguemnts, then set up the environment and execute.
+class Runtime {
+
+public:
+       enum Mode {
+               /// default behaviour: run until user quits, dynamic timesteps
+               NORMAL,
+               /// quit after n frames
+               FRAME_LIMIT,
+               /// quit after n milliseconds
+               TIME_LIMIT,
+               /// quit after n frames, use fixed timestap
+               FIXED_FRAME_LIMIT,
+               /// display error message and quit with failure
+               ERROR,
+       };
+
+       enum Target {
+               STANDALONE,
+               SERVER,
+               CLIENT,
+       };
+
+       struct Config {
+               app::Config game = app::Config();
+               HeadlessEnvironment::Config env = HeadlessEnvironment::Config();
+       };
+
+       Runtime() noexcept;
+
+       void Initialize(int argc, const char *const *argv);
+
+       int Execute();
+
+private:
+       void ReadArgs(int argc, const char *const *argv);
+       void ReadPreferences();
+
+       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;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/State.hpp b/src/app/State.hpp
new file mode 100644 (file)
index 0000000..7e96e65
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef GONG_APP_STATE_HPP_
+#define GONG_APP_STATE_HPP_
+
+#include <SDL.h>
+
+
+namespace gong {
+namespace graphics {
+       class Viewport;
+}
+
+namespace app {
+
+class Application;
+class HeadlessApplication;
+
+
+struct State {
+
+       friend class Application;
+       friend class HeadlessApplication;
+
+       virtual void Handle(const SDL_Event &) = 0;
+
+       virtual void Update(int dt) = 0;
+
+       virtual void Render(graphics::Viewport &) = 0;
+
+
+private:
+       int ref_count = 0;
+
+       virtual void OnEnter() { }
+       virtual void OnResume() { }
+       virtual void OnPause() { }
+       virtual void OnExit() { }
+
+       virtual void OnFocus() { }
+       virtual void OnBlur() { }
+       virtual void OnResize(graphics::Viewport &) { }
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/StateControl.hpp b/src/app/StateControl.hpp
new file mode 100644 (file)
index 0000000..221f154
--- /dev/null
@@ -0,0 +1,71 @@
+#ifndef GONG_APP_STATECONTROL_HPP_
+#define GONG_APP_STATECONTROL_HPP_
+
+#include <queue>
+
+
+namespace gong {
+namespace app {
+
+class HeadlessApplication;
+class State;
+
+class StateControl {
+
+public:
+       // add state to the front
+       void Push(State *s) {
+               cue.emplace(PUSH, s);
+       }
+
+       // swap state at the front
+       void Switch(State *s) {
+               cue.emplace(SWITCH, s);
+       }
+
+       // remove state at the front
+       void Pop() {
+               cue.emplace(POP);
+       }
+
+       // remove all states
+       // application will exit if nothing is pushed after this
+       void PopAll() {
+               cue.emplace(POP_ALL);
+       }
+
+       // pop states until this one is on top
+       void PopAfter(State *s) {
+               cue.emplace(POP_AFTER, s);
+       }
+
+       // pop states until this one is removed
+       void PopUntil(State *s) {
+               cue.emplace(POP_UNTIL, s);
+       }
+
+
+       void Commit(HeadlessApplication &);
+
+private:
+       enum Command {
+               PUSH,
+               SWITCH,
+               POP,
+               POP_ALL,
+               POP_AFTER,
+               POP_UNTIL,
+       };
+       struct Memo {
+               State *state;
+               Command cmd;
+               explicit Memo(Command c, State *s = nullptr): state(s), cmd(c) { }
+       };
+       std::queue<Memo> cue;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/app.cpp b/src/app/app.cpp
new file mode 100644 (file)
index 0000000..bd2c0a7
--- /dev/null
@@ -0,0 +1,560 @@
+#include "Application.hpp"
+#include "Assets.hpp"
+#include "Environment.hpp"
+#include "FrameCounter.hpp"
+#include "MessageState.hpp"
+#include "ResourceIndex.hpp"
+#include "State.hpp"
+#include "StateControl.hpp"
+
+#include "init.hpp"
+#include "../audio/Sound.hpp"
+#include "../graphics/ArrayTexture.hpp"
+#include "../graphics/CubeMap.hpp"
+#include "../graphics/Font.hpp"
+#include "../graphics/Texture.hpp"
+#include "../io/TokenStreamReader.hpp"
+
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <stdexcept>
+#include <SDL_image.h>
+
+using namespace std;
+
+
+namespace gong {
+namespace app {
+
+HeadlessApplication::HeadlessApplication(HeadlessEnvironment &e)
+: env(e)
+, states() {
+
+}
+
+HeadlessApplication::~HeadlessApplication() {
+
+}
+
+
+Application::Application(Environment &e)
+: HeadlessApplication(e)
+, env(e) {
+
+}
+
+Application::~Application() {
+       env.audio.StopAll();
+}
+
+
+void HeadlessApplication::RunN(size_t n) {
+       Uint32 last = SDL_GetTicks();
+       for (size_t i = 0; HasState() && i < n; ++i) {
+               Uint32 now = SDL_GetTicks();
+               int delta = now - last;
+               Loop(delta);
+               last = now;
+       }
+}
+
+void HeadlessApplication::RunT(size_t t) {
+       Uint32 last = SDL_GetTicks();
+       Uint32 finish = last + t;
+       while (HasState() && last < finish) {
+               Uint32 now = SDL_GetTicks();
+               int delta = now - last;
+               Loop(delta);
+               last = now;
+       }
+}
+
+void HeadlessApplication::RunS(size_t n, size_t t) {
+       for (size_t i = 0; HasState() && i < n; ++i) {
+               Loop(t);
+               cout << '.';
+               if (i % 32 == 31) {
+                       cout << setfill(' ') << setw(5) << right << (i + 1) << endl;
+               } else {
+                       cout << flush;
+               }
+       }
+}
+
+
+void HeadlessApplication::Run() {
+       Uint32 last = SDL_GetTicks();
+       while (HasState()) {
+               Uint32 now = SDL_GetTicks();
+               int delta = now - last;
+               Loop(delta);
+               last = now;
+       }
+}
+
+void HeadlessApplication::Loop(int dt) {
+       env.counter.EnterFrame();
+       HandleEvents();
+       if (!HasState()) return;
+       Update(dt);
+       CommitStates();
+       if (!HasState()) return;
+       env.counter.ExitFrame();
+}
+
+void Application::Loop(int dt) {
+       env.counter.EnterFrame();
+       HandleEvents();
+       if (!HasState()) return;
+       Update(dt);
+       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);
+               CommitStates();
+       }
+       env.counter.ExitHandle();
+}
+
+void Application::Handle(const SDL_Event &event) {
+       switch (event.type) {
+               case SDL_WINDOWEVENT:
+                       Handle(event.window);
+                       break;
+               default:
+                       GetState().Handle(event);
+                       break;
+       }
+}
+
+void Application::Handle(const SDL_WindowEvent &event) {
+       switch (event.event) {
+               case SDL_WINDOWEVENT_FOCUS_GAINED:
+                       GetState().OnFocus();
+                       break;
+               case SDL_WINDOWEVENT_FOCUS_LOST:
+                       GetState().OnBlur();
+                       break;
+               case SDL_WINDOWEVENT_RESIZED:
+                       env.viewport.Resize(event.data1, event.data2);
+                       GetState().OnResize(env.viewport);
+                       break;
+               default:
+                       break;
+       }
+}
+
+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);
+       if (HasState()) {
+               GetState().Update(dt);
+       }
+       env.counter.ExitUpdate();
+}
+
+void Application::Render() {
+       // gl implementation may (and will probably) delay vsync blocking until
+       // the first write after flipping, which is this clear call
+       env.viewport.Clear();
+       env.counter.EnterRender();
+
+       if (HasState()) {
+               GetState().Render(env.viewport);
+       }
+
+       env.counter.ExitRender();
+       env.window.Flip();
+}
+
+
+void HeadlessApplication::PushState(State *s) {
+       if (!states.empty()) {
+               states.top()->OnPause();
+       }
+       states.emplace(s);
+       ++s->ref_count;
+       if (s->ref_count == 1) {
+               s->OnEnter();
+       }
+       s->OnResume();
+}
+
+State *HeadlessApplication::PopState() {
+       State *s = states.top();
+       states.pop();
+       s->OnPause();
+       s->OnExit();
+       if (!states.empty()) {
+               states.top()->OnResume();
+       }
+       return s;
+}
+
+State *HeadlessApplication::SwitchState(State *s_new) {
+       State *s_old = states.top();
+       states.top() = s_new;
+       --s_old->ref_count;
+       ++s_new->ref_count;
+       s_old->OnPause();
+       if (s_old->ref_count == 0) {
+               s_old->OnExit();
+       }
+       if (s_new->ref_count == 1) {
+               s_new->OnEnter();
+       }
+       s_new->OnResume();
+       return s_old;
+}
+
+State &HeadlessApplication::GetState() {
+       return *states.top();
+}
+
+void HeadlessApplication::CommitStates() {
+       env.state.Commit(*this);
+}
+
+bool HeadlessApplication::HasState() const noexcept {
+       return !states.empty();
+}
+
+
+void StateControl::Commit(HeadlessApplication &app) {
+       while (!cue.empty()) {
+               Memo m(cue.front());
+               cue.pop();
+               switch (m.cmd) {
+                       case PUSH:
+                               app.PushState(m.state);
+                               break;
+                       case SWITCH:
+                               app.SwitchState(m.state);
+                               break;
+                       case POP:
+                               app.PopState();
+                               break;
+                       case POP_ALL:
+                               while (app.HasState()) {
+                                       app.PopState();
+                               }
+                               break;
+                       case POP_AFTER:
+                               while (app.HasState() && &app.GetState() != m.state) {
+                                       app.PopState();
+                               }
+                               break;
+                       case POP_UNTIL:
+                               while (app.HasState()) {
+                                       if (app.PopState() == m.state) {
+                                               break;
+                                       }
+                               }
+               }
+       }
+}
+
+
+AssetLoader::AssetLoader(const string &base)
+: fonts(base + "fonts/")
+, sounds(base + "sounds/")
+, textures(base + "textures/")
+, data(base + "data/") {
+
+}
+
+Assets::Assets(const AssetLoader &loader)
+: large_ui_font(loader.LoadFont("DejaVuSans", 24))
+, small_ui_font(loader.LoadFont("DejaVuSans", 16)) {
+
+}
+
+graphics::CubeMap AssetLoader::LoadCubeMap(const string &name) const {
+       string full = textures + name;
+       string right = full + "-right.png";
+       string left = full + "-left.png";
+       string top = full + "-top.png";
+       string bottom = full + "-bottom.png";
+       string back = full + "-back.png";
+       string front = full + "-front.png";
+
+       graphics::CubeMap cm;
+       cm.Bind();
+       SDL_Surface *srf;
+
+       if (!(srf = IMG_Load(right.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::RIGHT, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       if (!(srf = IMG_Load(left.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::LEFT, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       if (!(srf = IMG_Load(top.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::TOP, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       if (!(srf = IMG_Load(bottom.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::BOTTOM, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       if (!(srf = IMG_Load(back.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::BACK, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       if (!(srf = IMG_Load(front.c_str()))) throw SDLError("IMG_Load");
+       try {
+               cm.Data(graphics::CubeMap::FRONT, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+
+       cm.FilterNearest();
+       cm.WrapEdge();
+
+       return cm;
+}
+
+graphics::Font AssetLoader::LoadFont(const string &name, int size) const {
+       string full = fonts + name + ".ttf";
+       return graphics::Font(full.c_str(), size);
+}
+
+audio::Sound AssetLoader::LoadSound(const string &name) const {
+       string full = sounds + name + ".wav";
+       return audio::Sound(full.c_str());
+}
+
+graphics::Texture AssetLoader::LoadTexture(const string &name) const {
+       string full = textures + name + ".png";
+       graphics::Texture tex;
+       SDL_Surface *srf = IMG_Load(full.c_str());
+       if (!srf) {
+               throw SDLError("IMG_Load");
+       }
+       tex.Bind();
+       tex.Data(*srf);
+       SDL_FreeSurface(srf);
+       return tex;
+}
+
+void AssetLoader::LoadTexture(const string &name, graphics::ArrayTexture &tex, int layer) const {
+       string full = textures + name + ".png";
+       SDL_Surface *srf = IMG_Load(full.c_str());
+       if (!srf) {
+               throw SDLError("IMG_Load");
+       }
+       tex.Bind();
+       try {
+               tex.Data(layer, *srf);
+       } catch (...) {
+               SDL_FreeSurface(srf);
+               throw;
+       }
+       SDL_FreeSurface(srf);
+}
+
+
+void FrameCounter::EnterFrame() noexcept {
+       last_enter = SDL_GetTicks();
+       last_tick = last_enter;
+}
+
+void FrameCounter::EnterHandle() noexcept {
+       Tick();
+}
+
+void FrameCounter::ExitHandle() noexcept {
+       current.handle = Tick();
+}
+
+void FrameCounter::EnterUpdate() noexcept {
+       Tick();
+}
+
+void FrameCounter::ExitUpdate() noexcept {
+       current.update = Tick();
+}
+
+void FrameCounter::EnterRender() noexcept {
+       Tick();
+}
+
+void FrameCounter::ExitRender() noexcept {
+       current.render = Tick();
+}
+
+void FrameCounter::ExitFrame() noexcept {
+       Uint32 now = SDL_GetTicks();
+       current.total = now - last_enter;
+       current.running = current.handle + current.update + current.render;
+       current.waiting = current.total - current.running;
+       Accumulate();
+
+       ++cur_frame;
+       if (cur_frame >= NUM_FRAMES) {
+               Push();
+               cur_frame = 0;
+               changed = true;
+       } else {
+               changed = false;
+       }
+}
+
+int FrameCounter::Tick() noexcept {
+       Uint32 now = SDL_GetTicks();
+       int delta = now - last_tick;
+       last_tick = now;
+       return delta;
+}
+
+void FrameCounter::Accumulate() noexcept {
+       sum.handle += current.handle;
+       sum.update += current.update;
+       sum.render += current.render;
+       sum.running += current.running;
+       sum.waiting += current.waiting;
+       sum.total += current.total;
+
+       max.handle = std::max(current.handle, max.handle);
+       max.update = std::max(current.update, max.update);
+       max.render = std::max(current.render, max.render);
+       max.running = std::max(current.running, max.running);
+       max.waiting = std::max(current.waiting, max.waiting);
+       max.total = std::max(current.total, max.total);
+
+       current = Frame<int>();
+}
+
+void FrameCounter::Push() noexcept {
+       peak = max;
+       avg.handle = sum.handle * factor;
+       avg.update = sum.update * factor;
+       avg.render = sum.render * factor;
+       avg.running = sum.running * factor;
+       avg.waiting = sum.waiting * factor;
+       avg.total = sum.total * factor;
+
+       //Print(cout);
+
+       sum = Frame<int>();
+       max = Frame<int>();
+}
+
+void FrameCounter::Print(ostream &out) const {
+       out << fixed << right << setprecision(2) << setfill(' ')
+               << "PEAK handle: " << setw(2) << peak.handle
+               << ".00ms, update: " << setw(2) << peak.update
+               << ".00ms, render: " << setw(2) << peak.render
+               << ".00ms, running: " << setw(2) << peak.running
+               << ".00ms, waiting: " << setw(2) << peak.waiting
+               << ".00ms, total: " << setw(2) << peak.total
+               << ".00ms" << endl
+               << " AVG handle: " << setw(5) << avg.handle
+               << "ms, update: " << setw(5) << avg.update
+               << "ms, render: " << setw(5) << avg.render
+               << "ms, running: " << setw(5) << avg.running
+               << "ms, waiting: " << setw(5) << avg.waiting
+               << "ms, total: " << setw(5) << avg.total
+               << "ms" << endl;
+}
+
+
+MessageState::MessageState(Environment &env)
+: env(env) {
+       message.Position(glm::vec3(0.0f), graphics::Gravity::CENTER);
+       message.Hide();
+       press_key.Position(glm::vec3(0.0f, env.assets.large_ui_font.LineSkip(), 0.0f), graphics::Gravity::CENTER);
+       press_key.Set(env.assets.small_ui_font, "press any key to continue");
+       press_key.Show();
+}
+
+void MessageState::SetMessage(const char *msg) {
+       message.Set(env.assets.large_ui_font, msg);
+       message.Show();
+}
+
+void MessageState::ClearMessage() {
+       message.Hide();
+}
+
+void MessageState::Handle(const SDL_Event &e) {
+       if (e.type == SDL_QUIT || e.type == SDL_KEYDOWN) {
+               env.state.Pop();
+       }
+}
+
+void MessageState::Update(int) {
+
+}
+
+void MessageState::Render(graphics::Viewport &viewport) {
+       if (message.Visible()) {
+               message.Render(viewport);
+       }
+       if (press_key.Visible()) {
+               press_key.Render(viewport);
+       }
+}
+
+}
+}
diff --git a/src/app/error.cpp b/src/app/error.cpp
new file mode 100644 (file)
index 0000000..b646c39
--- /dev/null
@@ -0,0 +1,194 @@
+#include "error.hpp"
+
+#include <alut.h>
+#include <cerrno>
+#include <cstring>
+#include <SDL.h>
+#include <SDL_net.h>
+#include <SDL_ttf.h>
+#include <GL/glew.h>
+
+using namespace std;
+
+
+namespace {
+
+const char *al_error_string(ALenum num) {
+       switch (num) {
+               case AL_NO_ERROR:
+                       return "no error";
+               case AL_INVALID_NAME:
+                       return "invalid name";
+               case AL_INVALID_ENUM:
+                       return "invalid enum";
+               case AL_INVALID_VALUE:
+                       return "invalid value";
+               case AL_INVALID_OPERATION:
+                       return "invalid operation";
+               case AL_OUT_OF_MEMORY:
+                       return "out of memory";
+       }
+       return "unknown AL error";
+}
+
+std::string al_error_append(ALenum num, std::string msg) {
+       return msg + ": " + al_error_string(num);
+}
+
+string alut_error_append(ALenum num, string msg) {
+       const char *error = alutGetErrorString(num);
+       if (error && *error != '\0') {
+               msg += ": ";
+               msg += error;
+       }
+       return msg;
+}
+
+string gl_error_append(string msg) {
+       const GLubyte *error = gluErrorString(glGetError());
+       if (error && *error != '\0') {
+               const GLubyte *errEnd = error;
+               while (*errEnd != '\0') {
+                       ++errEnd;
+               }
+               msg += ": ";
+               msg.append(error, errEnd);
+       }
+       return msg;
+}
+
+string gl_error_get() {
+       string msg;
+       const GLubyte *error = gluErrorString(glGetError());
+       if (error && *error != '\0') {
+               const GLubyte *errEnd = error;
+               while (*errEnd != '\0') {
+                       ++errEnd;
+               }
+               msg.assign(error, errEnd);
+       }
+       return msg;
+}
+
+string net_error_append(string msg) {
+       const char *error = SDLNet_GetError();
+       if (*error != '\0') {
+               msg += ": ";
+               msg += error;
+       }
+       return msg;
+}
+
+string sdl_error_append(string msg) {
+       const char *error = SDL_GetError();
+       if (error && *error != '\0') {
+               msg += ": ";
+               msg += error;
+               SDL_ClearError();
+       }
+       return msg;
+}
+
+string ttf_error_append(string msg) {
+       const char *error = TTF_GetError();
+       if (error && *error != '\0') {
+               msg += ": ";
+               msg += error;
+       }
+       return msg;
+}
+
+}
+
+
+namespace gong {
+namespace app {
+
+ALError::ALError(ALenum num)
+: std::runtime_error(al_error_string(num)) {
+
+}
+
+ALError::ALError(ALenum num, const std::string &msg)
+: std::runtime_error(al_error_append(num, msg)) {
+
+}
+
+
+AlutError::AlutError(ALenum num)
+: runtime_error(alutGetErrorString(num)) {
+
+}
+
+AlutError::AlutError(ALenum num, const string &msg)
+: runtime_error(alut_error_append(num, msg)) {
+
+}
+
+
+GLError::GLError()
+: runtime_error(gl_error_get()) {
+
+}
+
+GLError::GLError(const string &msg)
+: runtime_error(gl_error_append(msg)) {
+
+}
+
+
+NetError::NetError()
+: runtime_error(SDLNet_GetError()) {
+
+}
+
+NetError::NetError(const string &msg)
+: runtime_error(net_error_append(msg)) {
+
+}
+
+
+SDLError::SDLError()
+: runtime_error(SDL_GetError()) {
+
+}
+
+SDLError::SDLError(const string &msg)
+: runtime_error(sdl_error_append(msg)) {
+
+}
+
+
+SysError::SysError()
+: SysError(errno) {
+
+}
+
+SysError::SysError(const string &msg)
+: SysError(errno, msg) {
+
+}
+
+SysError::SysError(int err_num)
+: runtime_error(strerror(err_num)) {
+
+}
+
+SysError::SysError(int err_num, const string &msg)
+: runtime_error(msg + ": " + strerror(err_num)) {
+
+}
+
+
+TTFError::TTFError()
+: runtime_error(TTF_GetError()) {
+
+}
+
+TTFError::TTFError(const string &msg)
+: runtime_error(ttf_error_append(msg)) {
+
+}
+
+}
+}
diff --git a/src/app/error.hpp b/src/app/error.hpp
new file mode 100644 (file)
index 0000000..43c461f
--- /dev/null
@@ -0,0 +1,86 @@
+#ifndef GONG_APP_ERROR_HPP_
+#define GONG_APP_ERROR_HPP_
+
+#include <al.h>
+#include <stdexcept>
+#include <string>
+
+
+namespace gong {
+namespace app {
+
+class ALError
+: public std::runtime_error {
+
+public:
+       explicit ALError(ALenum);
+       ALError(ALenum, const std::string &);
+
+};
+
+
+class AlutError
+: public std::runtime_error {
+
+public:
+       explicit AlutError(ALenum);
+       AlutError(ALenum, const std::string &);
+
+};
+
+
+class GLError
+: public std::runtime_error {
+
+public:
+       GLError();
+       explicit GLError(const std::string &);
+
+};
+
+
+class NetError
+: public std::runtime_error {
+
+public:
+       NetError();
+       explicit NetError(const std::string &);
+
+};
+
+
+class SDLError
+: public std::runtime_error {
+
+public:
+       SDLError();
+       explicit SDLError(const std::string &);
+
+};
+
+
+class SysError
+: public std::runtime_error {
+
+public:
+       SysError();
+       explicit SysError(const std::string &);
+       explicit SysError(int err_num);
+       SysError(int err_num, const std::string &);
+
+};
+
+
+class TTFError
+: public std::runtime_error {
+
+public:
+       TTFError();
+       explicit TTFError(const std::string &);
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/init.cpp b/src/app/init.cpp
new file mode 100644 (file)
index 0000000..87753a3
--- /dev/null
@@ -0,0 +1,201 @@
+#include "init.hpp"
+
+#include <algorithm>
+#include <alut.h>
+#include <SDL.h>
+#include <SDL_image.h>
+#include <SDL_net.h>
+#include <SDL_ttf.h>
+#include <string>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace app {
+
+InitSDL::InitSDL() {
+       if (SDL_Init(SDL_INIT_EVENTS) != 0) {
+               throw SDLError("SDL_Init(SDL_INIT_EVENTS)");
+       }
+}
+
+InitSDL::~InitSDL() {
+       SDL_Quit();
+}
+
+
+InitVideo::InitVideo() {
+       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() {
+       SDL_QuitSubSystem(SDL_INIT_VIDEO);
+}
+
+
+InitIMG::InitIMG() {
+       if (IMG_Init(IMG_INIT_PNG) == 0) {
+               throw SDLError("IMG_Init(IMG_INIT_PNG)");
+       }
+}
+
+InitIMG::~InitIMG() {
+       IMG_Quit();
+}
+
+
+InitNet::InitNet() {
+       if (SDLNet_Init() != 0) {
+               throw SDLError("SDLNet_Init()");
+       }
+}
+
+InitNet::~InitNet() {
+       SDLNet_Quit();
+}
+
+
+InitTTF::InitTTF() {
+       if (TTF_Init() != 0) {
+               throw SDLError("TTF_Init()");
+       }
+}
+
+InitTTF::~InitTTF() {
+       TTF_Quit();
+}
+
+
+InitAL::InitAL() {
+       if (!alutInit(nullptr, nullptr)) {
+               throw AlutError(alutGetError(), "alutInit");
+       }
+}
+
+InitAL::~InitAL() throw(AlutError) {
+       if (!alutExit()) {
+               throw AlutError(alutGetError(), "alutExit");
+       }
+}
+
+
+InitGL::InitGL(bool double_buffer, int sample_size) {
+       if (SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3) != 0) {
+               throw SDLError("SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3)");
+       }
+       if (SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3) != 0) {
+               throw SDLError("SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3)");
+       }
+       if (SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE) != 0) {
+               throw SDLError("SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE)");
+       }
+
+       if (!double_buffer) {
+               if (SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 0) != 0) {
+                       throw SDLError("SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 0)");
+               }
+       }
+
+       if (sample_size > 1) {
+               if (SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1) != 0) {
+                       throw SDLError("SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS)");
+               }
+               if (SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, sample_size) != 0) {
+                       throw SDLError("SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES)");
+               }
+       }
+}
+
+
+Window::Window()
+: handle(SDL_CreateWindow(
+       "gong",
+       SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+       960, 600,
+       SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE
+)) {
+       if (!handle) {
+               throw SDLError("SDL_CreateWindow");
+       }
+}
+
+Window::~Window() {
+       SDL_DestroyWindow(handle);
+}
+
+void Window::GrabInput() {
+       SDL_SetWindowGrab(handle, SDL_TRUE);
+}
+
+void Window::ReleaseInput() {
+       SDL_SetWindowGrab(handle, SDL_FALSE);
+}
+
+void Window::GrabMouse() {
+       if (SDL_SetRelativeMouseMode(SDL_TRUE) != 0) {
+               throw SDLError("SDL_SetRelativeMouseMode");
+       }
+}
+
+void Window::ReleaseMouse() {
+       if (SDL_SetRelativeMouseMode(SDL_FALSE) != 0) {
+               throw SDLError("SDL_SetRelativeMouseMode");
+       }
+}
+
+void Window::Flip() {
+       SDL_GL_SwapWindow(handle);
+}
+
+
+GLContext::GLContext(SDL_Window *win)
+: ctx(SDL_GL_CreateContext(win)) {
+       if (!ctx) {
+               throw SDLError("SDL_GL_CreateContext");
+       }
+}
+
+GLContext::~GLContext() {
+       SDL_GL_DeleteContext(ctx);
+}
+
+
+InitGLEW::InitGLEW() {
+       glewExperimental = GL_TRUE;
+       GLenum glew_err = glewInit();
+       if (glew_err != GLEW_OK) {
+               std::string msg("glewInit: ");
+               const GLubyte *errBegin = glewGetErrorString(glew_err);
+               const GLubyte *errEnd = errBegin;
+               while (*errEnd != '\0') {
+                       ++errEnd;
+               }
+               msg.append(errBegin, errEnd);
+               throw std::runtime_error(msg);
+       }
+}
+
+
+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)
+, window()
+, ctx(window.Handle())
+, init_glew() {
+
+}
+
+}
+}
diff --git a/src/app/init.hpp b/src/app/init.hpp
new file mode 100644 (file)
index 0000000..f2ec6e1
--- /dev/null
@@ -0,0 +1,173 @@
+#ifndef GONG_APP_INIT_HPP_
+#define GONG_APP_INIT_HPP_
+
+#include "error.hpp"
+
+#include <SDL.h>
+
+
+namespace gong {
+namespace app {
+
+class InitSDL {
+
+public:
+       InitSDL();
+       ~InitSDL();
+
+       InitSDL(const InitSDL &) = delete;
+       InitSDL &operator =(const InitSDL &) = delete;
+
+};
+
+
+class InitVideo {
+
+public:
+       InitVideo();
+       ~InitVideo();
+
+       InitVideo(const InitVideo &) = delete;
+       InitVideo &operator =(const InitVideo &) = delete;
+
+};
+
+
+class InitIMG {
+
+public:
+       InitIMG();
+       ~InitIMG();
+
+       InitIMG(const InitIMG &) = delete;
+       InitIMG &operator =(const InitIMG &) = delete;
+
+};
+
+
+class InitNet {
+
+public:
+       InitNet();
+       ~InitNet();
+
+       InitNet(const InitNet &) = delete;
+       InitNet &operator =(const InitNet &) = delete;
+
+};
+
+
+class InitTTF {
+
+public:
+       InitTTF();
+       ~InitTTF();
+
+       InitTTF(const InitTTF &) = delete;
+       InitTTF &operator =(const InitTTF &) = delete;
+
+};
+
+
+class InitAL {
+
+public:
+       InitAL();
+       ~InitAL() throw(AlutError);
+
+       InitAL(const InitAL &) = delete;
+       InitAL &operator =(const InitAL &) = delete;
+
+};
+
+
+class InitGL {
+
+public:
+       explicit InitGL(bool double_buffer = true, int sample_size = 1);
+
+       InitGL(const InitGL &) = delete;
+       InitGL &operator =(const InitGL &) = delete;
+
+};
+
+
+class Window {
+
+public:
+       Window();
+       ~Window();
+
+       Window(const Window &) = delete;
+       Window &operator =(const Window &) = delete;
+
+       void GrabInput();
+       void ReleaseInput();
+
+       void GrabMouse();
+       void ReleaseMouse();
+
+       SDL_Window *Handle() { return handle; }
+
+       void Flip();
+
+private:
+       SDL_Window *handle;
+
+};
+
+
+class GLContext {
+
+public:
+       explicit GLContext(SDL_Window *);
+       ~GLContext();
+
+       GLContext(const GLContext &) = delete;
+       GLContext &operator =(const GLContext &) = delete;
+
+private:
+       SDL_GLContext ctx;
+
+};
+
+
+class InitGLEW {
+
+public:
+       InitGLEW();
+
+       InitGLEW(const InitGLEW &) = delete;
+       InitGLEW &operator =(const InitGLEW &) = delete;
+
+};
+
+
+struct InitHeadless {
+
+       InitHeadless();
+
+       InitSDL init_sdl;
+       InitNet init_net;
+
+};
+
+struct Init {
+
+       Init(bool double_buffer = true, int sample_size = 1);
+
+       InitVideo init_video;
+       InitIMG init_img;
+       InitTTF init_ttf;
+       InitAL init_al;
+       InitGL init_gl;
+       Window window;
+       GLContext ctx;
+       InitGLEW init_glew;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/app/runtime.cpp b/src/app/runtime.cpp
new file mode 100644 (file)
index 0000000..c40338e
--- /dev/null
@@ -0,0 +1,428 @@
+#include "Application.hpp"
+#include "Environment.hpp"
+#include "Runtime.hpp"
+
+#include "init.hpp"
+#include "../io/filesystem.hpp"
+#include "../io/TokenStreamReader.hpp"
+
+#include <cctype>
+#include <cstdlib>
+#include <ctime>
+#include <fstream>
+#include <iostream>
+#include <SDL.h>
+
+using namespace std;
+
+
+namespace {
+
+string default_asset_path() {
+       char *base = SDL_GetBasePath();
+       string assets(base);
+       assets += "assets/";
+       SDL_free(base);
+       return assets;
+}
+
+string default_save_path() {
+#ifndef NDEBUG
+       char *base = SDL_GetBasePath();
+       string save(base);
+       save += "saves/";
+       SDL_free(base);
+       return save;
+#else
+       char *pref = SDL_GetPrefPath("localhorst", "gong");
+       string save(pref);
+       SDL_free(pref);
+       return save;
+#endif
+}
+
+}
+
+namespace gong {
+namespace app {
+
+void Config::Load(istream &is) {
+       io::TokenStreamReader in(is);
+       string name;
+       while (in.HasMore()) {
+               if (in.Peek().type == io::Token::STRING) {
+                       in.ReadString(name);
+               } else {
+                       in.ReadIdentifier(name);
+               }
+               in.Skip(io::Token::EQUALS);
+               if (name == "audio.enabled") {
+                       in.ReadBoolean(audio.enabled);
+               } else if (name == "input.keyboard") {
+                       in.ReadBoolean(input.keyboard);
+               } else if (name == "input.mouse") {
+                       in.ReadBoolean(input.mouse);
+               } else if (name == "input.pitch_sensitivity") {
+                       in.ReadNumber(input.pitch_sensitivity);
+               } else if (name == "input.yaw_sensitivity") {
+                       in.ReadNumber(input.yaw_sensitivity);
+               } else if (name == "net.host") {
+                       in.ReadString(net.host);
+               } else if (name == "net.port") {
+                       int port;
+                       in.ReadNumber(port);
+                       net.port = port;
+               } else if (name == "net.cmd_port") {
+                       int port;
+                       in.ReadNumber(port);
+                       net.cmd_port = port;
+               } else if (name == "player.name") {
+                       in.ReadString(player.name);
+               } else if (name == "video.dblbuf") {
+                       in.ReadBoolean(video.dblbuf);
+               } else if (name == "video.vsync") {
+                       in.ReadBoolean(video.vsync);
+               } else if (name == "video.msaa") {
+                       in.ReadNumber(video.msaa);
+               } else if (name == "video.hud") {
+                       in.ReadBoolean(video.hud);
+               } else if (name == "video.world") {
+                       in.ReadBoolean(video.world);
+               } else if (name == "video.debug") {
+                       in.ReadBoolean(video.debug);
+               }
+               if (in.HasMore() && in.Peek().type == io::Token::SEMICOLON) {
+                       in.Skip(io::Token::SEMICOLON);
+               }
+       }
+}
+
+void Config::Save(ostream &out) {
+       out << "audio.enabled = " << (audio.enabled ? "yes" : "no") << ';' << endl;
+       out << "input.keyboard = " << (input.keyboard ? "on" : "off") << ';' << endl;
+       out << "input.mouse = " << (input.keyboard ? "on" : "off") << ';' << endl;
+       out << "input.pitch_sensitivity = " << input.pitch_sensitivity << ';' << endl;
+       out << "input.yaw_sensitivity = " << input.yaw_sensitivity << ';' << endl;
+       out << "net.host = \"" << net.host << "\";" << endl;
+       out << "net.port = " << net.port << ';' << endl;
+       out << "net.cmd_port = " << net.cmd_port << ';' << endl;
+       out << "player.name = \"" << player.name << "\";" << endl;
+       out << "video.dblbuf = " << (video.dblbuf ? "on" : "off") << ';' << endl;
+       out << "video.vsync = " << (video.vsync ? "on" : "off") << ';' << endl;
+       out << "video.msaa = " << video.msaa << ';' << endl;
+       out << "video.hud = " << (video.hud ? "on" : "off") << ';' << endl;
+       out << "video.world = " << (video.world ? "on" : "off") << ';' << endl;
+       out << "video.debug = " << (video.debug ? "on" : "off") << ';' << endl;
+}
+
+
+HeadlessEnvironment::HeadlessEnvironment(const Config &config)
+: config(config)
+, loader(config.asset_path)
+, counter()
+, state() {
+
+}
+
+Environment::Environment(Window &win, const Config &config)
+: HeadlessEnvironment(config)
+, assets(loader)
+, audio()
+, viewport()
+, window(win)
+, msg_state(*this) {
+       viewport.Clear();
+       window.Flip();
+}
+
+void Environment::ShowMessage(const char *msg) {
+       cout << msg << endl;
+       msg_state.SetMessage(msg);
+       state.Push(&msg_state);
+}
+
+
+Runtime::Runtime() noexcept
+: name("gong")
+, mode(NORMAL)
+, target(STANDALONE)
+, n(0)
+, t(0)
+, config() {
+
+}
+
+
+void Runtime::Initialize(int argc, const char *const *argv) {
+       ReadArgs(argc, argv);
+       if (mode == ERROR) return;
+       ReadPreferences();
+       ReadArgs(argc, argv);
+}
+
+void Runtime::ReadArgs(int argc, const char *const *argv) {
+       if (argc <= 0) return;
+       name = argv[0];
+
+       bool options = true;
+       bool error = false;
+
+       for (int i = 1; i < argc; ++i) {
+               const char *arg = argv[i];
+               if (!arg || arg[0] == '\0') {
+                       cerr << "warning: found empty argument at position " << i << endl;
+                       continue;
+               }
+               if (options && arg[0] == '-') {
+                       if (arg[1] == '\0') {
+                               cerr << "warning: incomplete option list at position " << i << endl;
+                       } else if (arg[1] == '-') {
+                               if (arg[2] == '\0') {
+                                       // stopper
+                                       options = false;
+                               } else {
+                                       const char *param = arg + 2;
+                                       // long option
+                                       if (strcmp(param, "no-vsync") == 0) {
+                                               config.game.video.vsync = false;
+                                       } else if (strcmp(param, "no-keyboard") == 0) {
+                                               config.game.input.keyboard = false;
+                                       } else if (strcmp(param, "no-mouse") == 0) {
+                                               config.game.input.mouse = false;
+                                       } else if (strcmp(param, "no-hud") == 0) {
+                                               config.game.video.hud = false;
+                                       } else if (strcmp(param, "no-audio") == 0) {
+                                               config.game.audio.enabled = false;
+                                       } 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') {
+                                                       cerr << "missing argument to --asset-path" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.env.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.game.net.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.game.net.port = strtoul(argv[i], nullptr, 10);
+                                               }
+                                       } else if (strcmp(param, "cmd-port") == 0) {
+                                               ++i;
+                                               if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                       cerr << "missing argument to --cmd-port" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.game.net.cmd_port = strtoul(argv[i], nullptr, 10);
+                                               }
+                                       } else if (strcmp(param, "player-name") == 0) {
+                                               ++i;
+                                               if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                       cerr << "missing argument to --player-name" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.game.player.name = argv[i];
+                                               }
+                                       } else if (strcmp(param, "save-path") == 0) {
+                                               ++i;
+                                               if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                       cerr << "missing argument to --save-path" << endl;
+                                                       error = true;
+                                               } else {
+                                                       config.env.save_path = argv[i];
+                                               }
+                                       } else {
+                                               cerr << "unknown option " << arg << endl;
+                                               error = true;
+                                       }
+                               }
+                       } else {
+                               // short options
+                               for (int j = 1; arg[j] != '\0'; ++j) {
+                                       switch (arg[j]) {
+                                               case 'd':
+                                                       config.game.video.dblbuf = false;
+                                                       break;
+                                               case 'm':
+                                                       ++i;
+                                                       if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                               cerr << "missing argument to -m" << endl;
+                                                               error = true;
+                                                       } else {
+                                                               config.game.video.msaa = strtoul(argv[i], nullptr, 10);
+                                                       }
+                                                       break;
+                                               case 'n':
+                                                       ++i;
+                                                       if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                               cerr << "missing argument to -n" << endl;
+                                                               error = true;
+                                                       } else {
+                                                               n = strtoul(argv[i], nullptr, 10);
+                                                       }
+                                                       break;
+                                               case 't':
+                                                       ++i;
+                                                       if (i >= argc || argv[i] == nullptr || argv[i][0] == '\0') {
+                                                               cerr << "missing argument to -t" << endl;
+                                                               error = true;
+                                                       } else {
+                                                               t = strtoul(argv[i], nullptr, 10);
+                                                       }
+                                                       break;
+                                               case '-':
+                                                       // stopper
+                                                       options = false;
+                                                       break;
+                                               default:
+                                                       cerr << "unknown option " << arg[j] << endl;
+                                                       error = true;
+                                                       break;
+                                       }
+                               }
+                       }
+               } else {
+                       cerr << "unable to interpret argument "
+                               << i << " (" << arg << ")" << endl;
+                       error = true;
+               }
+       }
+
+       if (error) {
+               mode = ERROR;
+       } else if (n > 0) {
+               if (t > 0) {
+                       mode = FIXED_FRAME_LIMIT;
+               } else {
+                       mode = FRAME_LIMIT;
+               }
+       } else if (t > 0) {
+               mode = TIME_LIMIT;
+       } else {
+               mode = NORMAL;
+       }
+
+       if (config.env.asset_path.empty()) {
+               config.env.asset_path = default_asset_path();
+       } else if (
+               config.env.asset_path.back() != '/' &&
+               config.env.asset_path.back() != '\\'
+       ) {
+               config.env.asset_path += '/';
+       }
+       if (config.env.save_path.empty()) {
+               config.env.save_path = default_save_path();
+       } else if (
+               config.env.save_path.back() != '/' &&
+               config.env.save_path.back() != '\\'
+       ) {
+               config.env.save_path += '/';
+       }
+}
+
+void Runtime::ReadPreferences() {
+       string prefs_path = config.env.save_path + "prefs.conf";
+       if (io::is_file(prefs_path)) {
+               ifstream file(prefs_path);
+               config.game.Load(file);
+       } else {
+               io::make_dirs(config.env.save_path);
+               ofstream file(prefs_path);
+               config.game.Save(file);
+       }
+}
+
+int Runtime::Execute() {
+       if (mode == ERROR) {
+               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.game.video.dblbuf, config.game.video.msaa);
+
+       Environment env(init.window, config.env);
+       env.viewport.VSync(config.game.video.vsync);
+
+       throw invalid_argument("standalone mode not implemented");
+
+       //Application app(env);
+       //app.PushState(...);
+       //Run(app);
+}
+
+void Runtime::RunServer() {
+       HeadlessEnvironment env(config.env);
+
+       throw invalid_argument("server mode not implemented");
+
+       //HeadlessApplication app(env);
+       //app.PushState(...);
+       //Run(app);
+}
+
+void Runtime::RunClient() {
+       Init init(config.game.video.dblbuf, config.game.video.msaa);
+
+       Environment env(init.window, config.env);
+       env.viewport.VSync(config.game.video.vsync);
+       throw invalid_argument("client mode not implemented");
+
+       //Application app(env);
+       //app.PushState(...);
+       //Run(app);
+}
+
+void Runtime::Run(HeadlessApplication &app) {
+       switch (mode) {
+               default:
+               case NORMAL:
+                       app.Run();
+                       break;
+               case FRAME_LIMIT:
+                       app.RunN(n);
+                       break;
+               case TIME_LIMIT:
+                       app.RunT(t);
+                       break;
+               case FIXED_FRAME_LIMIT:
+                       app.RunS(n, t);
+                       break;
+       }
+}
+
+}
+}
diff --git a/src/audio/Audio.hpp b/src/audio/Audio.hpp
new file mode 100644 (file)
index 0000000..20652c6
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef GONG_AUDIO_AUDIO_HPP_
+#define GONG_AUDIO_AUDIO_HPP_
+
+#include "../app/IntervalTimer.hpp"
+#include "../graphics/glm.hpp"
+
+#include <al.h>
+
+
+namespace gong {
+namespace audio {
+
+class Sound;
+
+class Audio {
+
+public:
+       Audio();
+       ~Audio();
+
+       Audio(const Audio &) = delete;
+       Audio &operator =(const Audio &) = delete;
+
+public:
+       void Position(const glm::vec3 &) noexcept;
+       void Velocity(const glm::vec3 &) noexcept;
+       void Orientation(const glm::vec3 &dir, const glm::vec3 &up) noexcept;
+
+       void Play(
+               const Sound &,
+               const glm::vec3 &pos = glm::vec3(0.0f),
+               const glm::vec3 &vel = glm::vec3(0.0f),
+               const glm::vec3 &dir = glm::vec3(0.0f)
+       ) noexcept;
+
+       void StopAll() noexcept;
+
+       void Update(int dt) noexcept;
+
+private:
+       int NextFree() noexcept;
+
+private:
+       static constexpr std::size_t NUM_SRC = 16;
+       ALuint source[NUM_SRC];
+       app::CoarseTimer timer[NUM_SRC];
+       int last_free;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/audio/Sound.hpp b/src/audio/Sound.hpp
new file mode 100644 (file)
index 0000000..be90bce
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef GONG_AUDIO_SOUND_HPP_
+#define GONG_AUDIO_SOUND_HPP_
+
+#include <al.h>
+
+
+namespace gong {
+namespace audio {
+
+class Sound {
+
+public:
+       Sound();
+       explicit Sound(const char *);
+       ~Sound();
+
+       Sound(Sound &&);
+       Sound &operator =(Sound &&);
+
+       Sound(const Sound &) = delete;
+       Sound &operator =(const Sound &) = delete;
+
+public:
+       void Bind(ALuint src) const;
+
+       /// full duration in milliseconds
+       int Duration() const noexcept { return duration; }
+
+private:
+       ALuint handle;
+       int duration;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/audio/SoundBank.hpp b/src/audio/SoundBank.hpp
new file mode 100644 (file)
index 0000000..b77f0b6
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef GONG_AUDIO_SOUNDBANK_HPP_
+#define GONG_AUDIO_SOUNDBANK_HPP_
+
+#include "Sound.hpp"
+
+#include <vector>
+
+
+namespace gong {
+namespace app {
+       class AssetLoader;
+       class ResourceIndex;
+}
+namespace audio {
+
+class Audio;
+
+
+class SoundBank {
+
+public:
+       SoundBank();
+
+       void Load(const app::AssetLoader &, const app::ResourceIndex &);
+
+       const Sound &operator [](std::size_t i) const noexcept { return sounds[i]; }
+
+private:
+       std::vector<Sound> sounds;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp
new file mode 100644 (file)
index 0000000..f4ede82
--- /dev/null
@@ -0,0 +1,181 @@
+#include "Audio.hpp"
+#include "Sound.hpp"
+#include "SoundBank.hpp"
+
+#include "../app/AssetLoader.hpp"
+#include "../app/error.hpp"
+#include "../app/ResourceIndex.hpp"
+
+#include <algorithm>
+#include <alut.h>
+#include <iostream>
+#include <glm/gtc/type_ptr.hpp>
+#include <glm/gtx/io.hpp>
+
+
+namespace gong {
+namespace audio {
+
+Audio::Audio()
+: last_free(0) {
+       alGenSources(NUM_SRC, source);
+       ALenum err = alGetError();
+       if (err != AL_NO_ERROR) {
+               throw app::ALError(err, "alGenSources");
+       }
+       for (std::size_t i = 0; i < NUM_SRC; ++i) {
+               alSourcef(source[i], AL_REFERENCE_DISTANCE, 2.0f);
+               alSourcef(source[i], AL_ROLLOFF_FACTOR, 1.0f);
+       }
+}
+
+Audio::~Audio() {
+       alDeleteSources(NUM_SRC, source);
+       ALenum err = alGetError();
+       if (err != AL_NO_ERROR) {
+               app::ALError error(err, "alDeleteSources");
+               std::cerr << "warning: " << error.what() << std::endl;
+               //throw error;
+       }
+}
+
+void Audio::Position(const glm::vec3 &pos) noexcept {
+       alListenerfv(AL_POSITION, glm::value_ptr(pos));
+       //std::cout << "listener at " << pos << std::endl;
+}
+
+void Audio::Velocity(const glm::vec3 &vel) noexcept {
+       alListenerfv(AL_VELOCITY, glm::value_ptr(vel));
+}
+
+void Audio::Orientation(const glm::vec3 &dir, const glm::vec3 &up) noexcept {
+       ALfloat orient[6] = {
+               dir.x, dir.y, dir.z,
+               up.x, up.y, up.z,
+       };
+       alListenerfv(AL_ORIENTATION, orient);
+}
+
+void Audio::Play(
+       const Sound &sound,
+       const glm::vec3 &pos,
+       const glm::vec3 &vel,
+       const glm::vec3 &dir
+) noexcept {
+       int i = NextFree();
+       if (i < 0) {
+               std::cerr << "unable to find free audio source" << std::endl;
+               return;
+       }
+
+       ALuint src = source[i];
+       app::CoarseTimer &t = timer[i];
+
+       sound.Bind(src);
+       alSourcefv(src, AL_POSITION, glm::value_ptr(pos));
+       alSourcefv(src, AL_VELOCITY, glm::value_ptr(vel));
+       alSourcefv(src, AL_DIRECTION, glm::value_ptr(dir));
+       alSourcePlay(src);
+
+       t = app::CoarseTimer(sound.Duration());
+       t.Start();
+}
+
+void Audio::StopAll() noexcept {
+       alSourceStopv(NUM_SRC, source);
+       for (std::size_t i = 0; i < NUM_SRC; ++i) {
+               alSourcei(source[i], AL_BUFFER, AL_NONE);
+       }
+}
+
+void Audio::Update(int dt) noexcept {
+       for (std::size_t i = 0; i < NUM_SRC; ++i) {
+               timer[i].Update(dt);
+               if (timer[i].HitOnce()) {
+                       timer[i].Stop();
+                       alSourceStop(source[i]);
+                       alSourcei(source[i], AL_BUFFER, AL_NONE);
+                       last_free = i;
+               }
+       }
+}
+
+int Audio::NextFree() noexcept {
+       if (!timer[last_free].Running()) {
+               return last_free;
+       }
+       for (int i = (last_free + 1) % NUM_SRC; i != last_free; i = (i + 1) % NUM_SRC) {
+               if (!timer[i].Running()) {
+                       last_free = i;
+                       return i;
+               }
+       }
+       return -1;
+}
+
+
+Sound::Sound()
+: handle(AL_NONE)
+, duration(0) {
+
+}
+
+Sound::Sound(const char *file)
+: handle(alutCreateBufferFromFile(file)) {
+       if (handle == AL_NONE) {
+               throw app::ALError(alGetError(), "alutCreateBufferFromFile");
+       }
+
+       ALint size, channels, bits, freq;
+       alGetBufferi(handle, AL_SIZE, &size);
+       alGetBufferi(handle, AL_CHANNELS, &channels);
+       alGetBufferi(handle, AL_BITS, &bits);
+       alGetBufferi(handle, AL_FREQUENCY, &freq);
+
+       duration = size * 8 * 1000 / (channels * bits * freq);
+}
+
+Sound::~Sound() {
+       if (handle != AL_NONE) {
+               alDeleteBuffers(1, &handle);
+               ALenum err = alGetError();
+               if (err != AL_NO_ERROR) {
+                       app::ALError error(err, "alDeleteBuffers");
+                       std::cerr << "warning: " << error.what() << std::endl;
+                       //throw error;
+               }
+       }
+}
+
+Sound::Sound(Sound &&other)
+: handle(other.handle)
+, duration(other.duration) {
+       other.handle = AL_NONE;
+}
+
+Sound &Sound::operator =(Sound &&other) {
+       std::swap(handle, other.handle);
+       std::swap(duration, other.duration);
+       return *this;
+}
+
+void Sound::Bind(ALuint src) const {
+       alSourcei(src, AL_BUFFER, handle);
+}
+
+
+SoundBank::SoundBank()
+: sounds() {
+
+}
+
+void SoundBank::Load(const app::AssetLoader &loader, const app::ResourceIndex &index) {
+       sounds.clear();
+       sounds.resize(index.Size());
+       for (const auto &entry : index.Entries()) {
+               sounds[entry.second] = loader.LoadSound(entry.first);
+       }
+}
+
+}
+}
diff --git a/src/geometry/const.hpp b/src/geometry/const.hpp
new file mode 100644 (file)
index 0000000..18c5754
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef GONG_GEOMETRY_CONST_HPP_
+#define GONG_GEOMETRY_CONST_HPP_
+
+
+namespace gong {
+namespace geometry {
+
+constexpr float PI = 3.141592653589793238462643383279502884;
+constexpr float PI_0p25 = PI * 0.25f;
+constexpr float PI_0p5 = PI * 0.5f;
+constexpr float PI_1p5 = PI * 1.5f;
+constexpr float PI_2p0 = PI * 2.0f;
+
+constexpr float PI_inv = 1.0f / PI;
+constexpr float PI_0p5_inv = 1.0f / PI_0p5;
+
+}
+}
+
+#endif
diff --git a/src/geometry/distance.hpp b/src/geometry/distance.hpp
new file mode 100644 (file)
index 0000000..62e9ce4
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef GONG_GEOMETRY_DISTANCE_HPP_
+#define GONG_GEOMETRY_DISTANCE_HPP_
+
+#include "../graphics/glm.hpp"
+
+#include <algorithm>
+#include <limits>
+#include <glm/gtx/component_wise.hpp>
+#include <glm/gtx/norm.hpp>
+
+
+namespace gong {
+namespace geometry {
+
+template <class T>
+inline bool iszero(const T &v) noexcept {
+       return glm::length2(v) < std::numeric_limits<typename T::value_type>::epsilon();
+}
+
+template<class Vec>
+inline void limit(Vec &v, float max) noexcept {
+       float len2 = glm::length2(v);
+       float max2 = max * max;
+       if (len2 > max2) {
+               v = glm::normalize(v) * max;
+       }
+}
+
+template<class T, glm::precision P = glm::precision(0)>
+T manhattan_distance(const TVEC3<T, P> &a, const TVEC3<T, P> &b) noexcept {
+       return glm::compAdd(glm::abs(a - b));
+}
+
+template<class T, glm::precision P = glm::precision(0)>
+T manhattan_radius(const TVEC3<T, P> &v) noexcept {
+       return glm::compMax(glm::abs(v));
+}
+
+}
+}
+
+#endif
diff --git a/src/geometry/geometry.cpp b/src/geometry/geometry.cpp
new file mode 100644 (file)
index 0000000..8deed86
--- /dev/null
@@ -0,0 +1,282 @@
+#include "const.hpp"
+#include "distance.hpp"
+#include "primitive.hpp"
+#include "rotation.hpp"
+
+#include <limits>
+#include <ostream>
+#include <glm/gtx/io.hpp>
+#include <glm/gtx/matrix_cross_product.hpp>
+#include <glm/gtx/optimum_pow.hpp>
+#include <glm/gtx/transform.hpp>
+
+
+namespace gong {
+namespace geometry {
+
+glm::mat3 find_rotation(const glm::vec3 &a, const glm::vec3 &b) noexcept {
+       glm::vec3 v(glm::cross(a, b));
+       if (iszero(v)) {
+               // a and b are parallel
+               if (iszero(a - b)) {
+                       // a and b are identical
+                       return glm::mat3(1.0f);
+               } else {
+                       // a and b are opposite
+                       // create arbitrary unit vector perpendicular to a and
+                       // rotate 180° around it
+                       glm::vec3 arb(a);
+                       if (std::abs(a.x - 1.0f) > std::numeric_limits<float>::epsilon()) {
+                               arb.x += 1.0f;
+                       } else {
+                               arb.y += 1.0f;
+                       }
+                       glm::vec3 axis(glm::normalize(glm::cross(a, arb)));
+                       return glm::mat3(glm::rotate(PI, axis));
+               }
+       }
+       float mv = glm::length2(v);
+       float c = glm::dot(a, b);
+       float f = (1 - c) / mv;
+       glm::mat3 vx(glm::matrixCross3(v));
+       return glm::mat3(1.0f) + vx + (glm::pow2(vx) * f);
+}
+
+std::ostream &operator <<(std::ostream &out, const AABB &box) {
+       return out << "AABB(" << box.min << ", " << box.max << ')';
+}
+
+std::ostream &operator <<(std::ostream &out, const Ray &ray) {
+       return out << "Ray(" << ray.orig << ", " << ray.dir << ')';
+}
+
+bool Intersection(
+       const Ray &ray,
+       const AABB &box,
+       float &dist
+) noexcept {
+       float t_min = 0.0f;
+       float t_max = std::numeric_limits<float>::infinity();
+       for (int i = 0; i < 3; ++i) {
+               float t1 = (box.min[i] - ray.orig[i]) * ray.inv_dir[i];
+               float t2 = (box.max[i] - ray.orig[i]) * ray.inv_dir[i];
+               t_min = std::max(t_min, std::min(t1, t2));
+               t_max = std::min(t_max, std::max(t1, t2));
+       }
+       dist = t_min;
+       return t_max >= t_min;
+}
+
+bool Intersection(
+       const Ray &ray,
+       const AABB &aabb,
+       const glm::mat4 &M,
+       float *dist,
+       glm::vec3 *normal
+) noexcept {
+       float t_min = 0.0f;
+       float t_max = std::numeric_limits<float>::infinity();
+       const glm::vec3 aabb_pos(M[3].x, M[3].y, M[3].z);
+       const glm::vec3 delta = aabb_pos - ray.orig;
+
+       glm::vec3 t1(t_min, t_min, t_min), t2(t_max, t_max, t_max);
+
+       for (int i = 0; i < 3; ++i) {
+               const glm::vec3 axis(M[i].x, M[i].y, M[i].z);
+               const float e = glm::dot(axis, delta);
+               const float f = glm::dot(axis, ray.dir);
+
+               if (std::abs(f) > std::numeric_limits<float>::epsilon()) {
+                       t1[i] = (e + aabb.min[i]) / f;
+                       t2[i] = (e + aabb.max[i]) / f;
+
+                       t_min = std::max(t_min, std::min(t1[i], t2[i]));
+                       t_max = std::min(t_max, std::max(t1[i], t2[i]));
+
+                       if (t_max < t_min) {
+                               return false;
+                       }
+               } else {
+                       if (aabb.min[i] - e > 0.0f || aabb.max[i] - e < 0.0f) {
+                               return false;
+                       }
+               }
+       }
+
+       if (dist) {
+               *dist = t_min;
+       }
+       if (normal) {
+               glm::vec3 min_all(glm::min(t1, t2));
+               if (min_all.x > min_all.y) {
+                       if (min_all.x > min_all.z) {
+                               *normal = glm::vec3(t2.x < t1.x ? 1 : -1, 0, 0);
+                       } else {
+                               *normal = glm::vec3(0, 0, t2.z < t1.z ? 1 : -1);
+                       }
+               } else if (min_all.y > min_all.z) {
+                       *normal = glm::vec3(0, t2.y < t1.y ? 1 : -1, 0);
+               } else {
+                       *normal = glm::vec3(0, 0, t2.z < t1.z ? 1 : -1);
+               }
+       }
+       return true;
+}
+
+bool Intersection(
+       const AABB &a_box,
+       const glm::mat4 &a_m,
+       const AABB &b_box,
+       const glm::mat4 &b_m,
+       float &depth,
+       glm::vec3 &normal
+) noexcept {
+       glm::vec3 a_corners[8] = {
+               glm::vec3(a_m * glm::vec4(a_box.min.x, a_box.min.y, a_box.min.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.min.x, a_box.min.y, a_box.max.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.min.x, a_box.max.y, a_box.min.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.min.x, a_box.max.y, a_box.max.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.max.x, a_box.min.y, a_box.min.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.max.x, a_box.min.y, a_box.max.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.max.x, a_box.max.y, a_box.min.z, 1)),
+               glm::vec3(a_m * glm::vec4(a_box.max.x, a_box.max.y, a_box.max.z, 1)),
+       };
+
+       glm::vec3 b_corners[8] = {
+               glm::vec3(b_m * glm::vec4(b_box.min.x, b_box.min.y, b_box.min.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.min.x, b_box.min.y, b_box.max.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.min.x, b_box.max.y, b_box.min.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.min.x, b_box.max.y, b_box.max.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.max.x, b_box.min.y, b_box.min.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.max.x, b_box.min.y, b_box.max.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.max.x, b_box.max.y, b_box.min.z, 1)),
+               glm::vec3(b_m * glm::vec4(b_box.max.x, b_box.max.y, b_box.max.z, 1)),
+       };
+
+       glm::vec3 axes[15] = {
+               glm::vec3(a_m[0]),
+               glm::vec3(a_m[1]),
+               glm::vec3(a_m[2]),
+               glm::vec3(b_m[0]),
+               glm::vec3(b_m[1]),
+               glm::vec3(b_m[2]),
+               glm::normalize(glm::cross(glm::vec3(a_m[0]), glm::vec3(b_m[0]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[0]), glm::vec3(b_m[1]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[0]), glm::vec3(b_m[2]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[1]), glm::vec3(b_m[0]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[1]), glm::vec3(b_m[1]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[1]), glm::vec3(b_m[2]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[2]), glm::vec3(b_m[0]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[2]), glm::vec3(b_m[1]))),
+               glm::normalize(glm::cross(glm::vec3(a_m[2]), glm::vec3(b_m[2]))),
+       };
+
+       depth = std::numeric_limits<float>::infinity();
+       int min_axis = 0;
+
+       int cur_axis = 0;
+       for (const glm::vec3 &axis : axes) {
+               if (glm::any(glm::isnan(axis))) {
+                       // can result from the cross products if A and B have parallel axes
+                       ++cur_axis;
+                       continue;
+               }
+               float a_min = std::numeric_limits<float>::infinity();
+               float a_max = -std::numeric_limits<float>::infinity();
+               for (const glm::vec3 &corner : a_corners) {
+                       float val = glm::dot(corner, axis);
+                       a_min = std::min(a_min, val);
+                       a_max = std::max(a_max, val);
+               }
+
+               float b_min = std::numeric_limits<float>::infinity();
+               float b_max = -std::numeric_limits<float>::infinity();
+               for (const glm::vec3 &corner : b_corners) {
+                       float val = glm::dot(corner, axis);
+                       b_min = std::min(b_min, val);
+                       b_max = std::max(b_max, val);
+               }
+
+               if (a_max < b_min || b_max < a_min) return false;
+
+               float overlap = std::min(a_max, b_max) - std::max(a_min, b_min);
+               if (overlap < depth) {
+                       depth = overlap;
+                       min_axis = cur_axis;
+               }
+
+               ++cur_axis;
+       }
+
+       normal = axes[min_axis];
+       return true;
+}
+
+
+std::ostream &operator <<(std::ostream &out, const Plane &plane) {
+       return out << "Plane(" << plane.normal << ", " << plane.dist << ')';
+}
+
+std::ostream &operator <<(std::ostream &out, const Frustum &frustum) {
+       return out << "Frustum(" << std::endl
+               << "\tleft:   " << frustum.plane[0] << std::endl
+               << "\tright:  " << frustum.plane[1] << std::endl
+               << "\tbottom: " << frustum.plane[2] << std::endl
+               << "\ttop:    " << frustum.plane[3] << std::endl
+               << "\tnear:   " << frustum.plane[4] << std::endl
+               << "\tfar:    " << frustum.plane[5] << std::endl
+               << ')';
+}
+
+bool CullTest(const AABB &box, const glm::mat4 &MVP) noexcept {
+       // transform corners into clip space
+       glm::vec4 corners[8] = {
+               { box.min.x, box.min.y, box.min.z, 1.0f },
+               { box.min.x, box.min.y, box.max.z, 1.0f },
+               { box.min.x, box.max.y, box.min.z, 1.0f },
+               { box.min.x, box.max.y, box.max.z, 1.0f },
+               { box.max.x, box.min.y, box.min.z, 1.0f },
+               { box.max.x, box.min.y, box.max.z, 1.0f },
+               { box.max.x, box.max.y, box.min.z, 1.0f },
+               { box.max.x, box.max.y, box.max.z, 1.0f },
+       };
+
+       // check how many corners lie outside
+       int hits[6] = { 0, 0, 0, 0, 0, 0 };
+       for (glm::vec4 &corner : corners) {
+               corner = MVP * corner;
+               // replacing this with *= 1/w is effectively more expensive
+               corner /= corner.w;
+               hits[0] += (corner.x >  1.0f);
+               hits[1] += (corner.x < -1.0f);
+               hits[2] += (corner.y >  1.0f);
+               hits[3] += (corner.y < -1.0f);
+               hits[4] += (corner.z >  1.0f);
+               hits[5] += (corner.z < -1.0f);
+       }
+
+       // if all corners are outside any given clip plane, the test is true
+       for (int hit : hits) {
+               if (hit == 8) return true;
+       }
+
+       // otherwise the box might still get culled completely, but can't say for sure ;)
+       return false;
+}
+
+bool CullTest(const AABB &box, const Frustum &frustum) noexcept {
+       for (const Plane &plane : frustum.plane) {
+               const glm::vec3 np(
+                       ((plane.normal.x > 0.0f) ? box.max.x : box.min.x),
+                       ((plane.normal.y > 0.0f) ? box.max.y : box.min.y),
+                       ((plane.normal.z > 0.0f) ? box.max.z : box.min.z)
+               );
+               const float dp = glm::dot(plane.normal, np);
+               // cull if nearest point is on the "outside" side of the plane
+               if (dp < -plane.dist) return true;
+       }
+       return false;
+}
+}
+
+}
diff --git a/src/geometry/primitive.hpp b/src/geometry/primitive.hpp
new file mode 100644 (file)
index 0000000..4711306
--- /dev/null
@@ -0,0 +1,160 @@
+#ifndef GONG_GEOMETRY_PRIMITIVE_HPP_
+#define GONG_GEOMETRY_PRIMITIVE_HPP_
+
+#include "../graphics/glm.hpp"
+
+#include <algorithm>
+#include <iosfwd>
+#include <glm/gtx/norm.hpp>
+
+
+namespace gong {
+namespace geometry {
+
+struct AABB {
+       glm::vec3 min;
+       glm::vec3 max;
+
+       void Adjust() noexcept {
+               if (max.x < min.x) std::swap(max.x, min.x);
+               if (max.y < min.y) std::swap(max.y, min.y);
+               if (max.z < min.z) std::swap(max.z, min.z);
+       }
+
+       glm::vec3 Center() const noexcept {
+               return min + (max - min) * 0.5f;
+       }
+
+       /// return distance between origin and farthest vertex
+       float OriginRadius() const noexcept {
+               glm::vec3 high(glm::max(glm::abs(min), glm::abs(max)));
+               return glm::length(high);
+       }
+};
+
+std::ostream &operator <<(std::ostream &, const AABB &);
+
+// TODO: this should really use setters/getters for dir and inv_dir so
+//       manipulating code doesn't "forget" to call Update()
+struct Ray {
+       glm::vec3 orig;
+       glm::vec3 dir;
+
+       glm::vec3 inv_dir;
+
+       void Update() noexcept {
+               inv_dir = 1.0f / dir;
+       }
+
+       /// get shortest distance of this ray's line to given point
+       float Distance(const glm::vec3 &point) const noexcept {
+               // d = |(x2-x1)Ă—(x1-x0)|/|x2-x1|
+               // where x0 is point, and x1 and x2 are points on the line
+               // for derivation, see http://mathworld.wolfram.com/Point-LineDistance3-Dimensional.html
+               // x1 = orig
+               // x2-x1 = dir, which means |x2-x1| is 1.0
+               return glm::length(glm::cross(dir, orig - point));
+       }
+       float DistanceSquared(const glm::vec3 &point) const noexcept {
+               return glm::length2(glm::cross(dir, orig - point));
+       }
+};
+
+std::ostream &operator <<(std::ostream &, const Ray &);
+
+/// axis aligned boolean ray/box intersection test
+/// if true, dist constains distance from ray's origin to intersection point
+bool Intersection(
+       const Ray &,
+       const AABB &,
+       float &dist) noexcept;
+
+/// detailed oriented ray/box intersection test
+bool Intersection(
+       const Ray &,
+       const AABB &,
+       const glm::mat4 &M,
+       float *dist = nullptr,
+       glm::vec3 *normal = nullptr) noexcept;
+
+/// matrices may translate and rotate, but must not scale/shear/etc
+/// (basically the first three columns must have unit length)
+bool Intersection(
+       const AABB &a_box,
+       const glm::mat4 &a_m,
+       const AABB &b_box,
+       const glm::mat4 &b_m,
+       float &depth,
+       glm::vec3 &normal) noexcept;
+
+
+struct Plane {
+       glm::vec3 normal;
+       float dist;
+
+       float &A() noexcept { return normal.x; }
+       float &B() noexcept { return normal.y; }
+       float &C() noexcept { return normal.z; }
+       float &D() noexcept { return dist; }
+       float A() const noexcept { return normal.x; }
+       float B() const noexcept { return normal.y; }
+       float C() const noexcept { return normal.z; }
+       float D() const noexcept { return dist; }
+
+       Plane(const glm::vec3 &n, float d)
+       : normal(n), dist(d) { }
+       explicit Plane(const glm::vec4 &abcd)
+       : normal(abcd), dist(abcd.w) { }
+
+       void Normalize() noexcept {
+               const float l = glm::length(normal);
+               normal /= l;
+               dist /= l;
+       }
+};
+
+std::ostream &operator <<(std::ostream &, const Plane &);
+
+struct Frustum {
+       Plane plane[6];
+       Plane &Left() noexcept { return plane[0]; }
+       Plane &Right() noexcept { return plane[1]; }
+       Plane &Bottom() noexcept { return plane[2]; }
+       Plane &Top() noexcept { return plane[3]; }
+       Plane &Near() noexcept { return plane[4]; }
+       Plane &Far() noexcept { return plane[5]; }
+       const Plane &Left() const noexcept { return plane[0]; }
+       const Plane &Right() const noexcept { return plane[1]; }
+       const Plane &Bottom() const noexcept { return plane[2]; }
+       const Plane &Top() const noexcept { return plane[3]; }
+       const Plane &Near() const noexcept { return plane[4]; }
+       const Plane &Far() const noexcept { return plane[5]; }
+
+       /// create frustum from transposed MVP
+       explicit Frustum(const glm::mat4 &mat)
+       : plane{
+               Plane{ mat[3] + mat[0] },
+               Plane{ mat[3] - mat[0] },
+               Plane{ mat[3] + mat[1] },
+               Plane{ mat[3] - mat[1] },
+               Plane{ mat[3] + mat[2] },
+               Plane{ mat[3] - mat[2] },
+       } { }
+
+       void Normalize() noexcept {
+               for (Plane &p : plane) {
+                       p.Normalize();
+               }
+       }
+};
+
+std::ostream &operator <<(std::ostream &, const Plane &);
+std::ostream &operator <<(std::ostream &, const Frustum &);
+
+bool CullTest(const AABB &box, const glm::mat4 &) noexcept;
+bool CullTest(const AABB &box, const Frustum &) noexcept;
+
+}
+}
+
+#endif
diff --git a/src/geometry/rotation.hpp b/src/geometry/rotation.hpp
new file mode 100644 (file)
index 0000000..9baff3d
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef GONG_GEOMETRY_ROTATION_HPP_
+#define GONG_GEOMETRY_ROTATION_HPP_
+
+#include "../graphics/glm.hpp"
+
+
+namespace gong {
+
+glm::mat3 find_rotation(const glm::vec3 &a, const glm::vec3 &b) noexcept;
+
+}
+
+#endif
diff --git a/src/graphics/ArrayTexture.hpp b/src/graphics/ArrayTexture.hpp
new file mode 100644 (file)
index 0000000..9b9fbc3
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef GONG_GRAPHICS_ARRAYTEXTURE_HPP_
+#define GONG_GRAPHICS_ARRAYTEXTURE_HPP_
+
+#include "Format.hpp"
+#include "TextureBase.hpp"
+
+#include <GL/glew.h>
+
+struct SDL_Surface;
+
+
+namespace gong {
+namespace graphics {
+
+class ArrayTexture
+: public TextureBase<GL_TEXTURE_2D_ARRAY> {
+
+public:
+       ArrayTexture();
+       ~ArrayTexture();
+
+       ArrayTexture(ArrayTexture &&) noexcept;
+       ArrayTexture &operator =(ArrayTexture &&) noexcept;
+
+       ArrayTexture(const ArrayTexture &) = delete;
+       ArrayTexture &operator =(const ArrayTexture &) = delete;
+
+public:
+       GLsizei Width() const noexcept { return width; }
+       GLsizei Height() const noexcept { return height; }
+       GLsizei Depth() const noexcept { return depth; }
+
+       void Reserve(GLsizei w, GLsizei h, GLsizei d, const Format &) noexcept;
+       void Data(GLsizei l, const SDL_Surface &);
+       void Data(GLsizei l, const Format &, GLvoid *data) noexcept;
+
+private:
+       GLsizei width, height, depth;
+
+       Format format;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/BlendedSprite.hpp b/src/graphics/BlendedSprite.hpp
new file mode 100644 (file)
index 0000000..08f7485
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef GONG_GRAPHICS_BLENDEDSPRITE_HPP_
+#define GONG_GRAPHICS_BLENDEDSPRITE_HPP_
+
+#include "glm.hpp"
+#include "Program.hpp"
+
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+class Texture;
+
+class BlendedSprite {
+
+public:
+       BlendedSprite();
+
+       void Activate() noexcept;
+
+       void SetM(const glm::mat4 &m) noexcept;
+       void SetProjection(const glm::mat4 &p) noexcept;
+       void SetView(const glm::mat4 &v) noexcept;
+       void SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept;
+       void SetMVP(const glm::mat4 &m, const glm::mat4 &v, const glm::mat4 &p) noexcept;
+
+       void SetTexture(Texture &) noexcept;
+       void SetFG(const glm::vec4 &) noexcept;
+       void SetBG(const glm::vec4 &) noexcept;
+
+       const glm::mat4 &Projection() const noexcept { return projection; }
+       const glm::mat4 &View() const noexcept { return view; }
+       const glm::mat4 &GetVP() const noexcept { return vp; }
+
+private:
+       Program program;
+
+       glm::mat4 projection;
+       glm::mat4 view;
+       glm::mat4 vp;
+
+       GLuint mvp_handle;
+       GLuint sampler_handle;
+       GLuint fg_handle;
+       GLuint bg_handle;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Camera.hpp b/src/graphics/Camera.hpp
new file mode 100644 (file)
index 0000000..f61750d
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef GONG_GRAPHICS_CAMERA_HPP_
+#define GONG_GRAPHICS_CAMERA_HPP_
+
+#include "glm.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+class Camera {
+
+public:
+       Camera() noexcept;
+
+       /// FOV in radians
+       void FOV(float f) noexcept;
+       void Aspect(float r) noexcept;
+       void Aspect(float w, float h) noexcept;
+       void Clip(float near, float far) noexcept;
+
+       const glm::mat4 &Projection() const noexcept { return projection; }
+       const glm::mat4 &View() const noexcept { return view; }
+       void View(const glm::mat4 &v) noexcept;
+
+private:
+       void UpdateProjection() noexcept;
+
+private:
+       float fov;
+       float aspect;
+       float near;
+       float far;
+
+       glm::mat4 projection;
+       glm::mat4 view;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Canvas.hpp b/src/graphics/Canvas.hpp
new file mode 100644 (file)
index 0000000..8fc53da
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef GONG_GRAPHICS_CANVAS_HPP_
+#define GONG_GRAPHICS_CANVAS_HPP_
+
+#include "glm.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+class Canvas {
+
+public:
+       Canvas() noexcept;
+
+       void Resize(float w, float h) noexcept;
+
+       const glm::vec2 &Offset() const noexcept { return offset; }
+       const glm::vec2 &Size() const noexcept { return size; }
+
+       const glm::mat4 &Projection() const noexcept { return projection; }
+       const glm::mat4 &View() const noexcept { return view; }
+
+private:
+       void UpdateProjection() noexcept;
+
+private:
+       glm::vec2 offset;
+       glm::vec2 size;
+       float near;
+       float far;
+
+       glm::mat4 projection;
+       glm::mat4 view;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/CubeMap.hpp b/src/graphics/CubeMap.hpp
new file mode 100644 (file)
index 0000000..51446c8
--- /dev/null
@@ -0,0 +1,47 @@
+#ifndef GONG_GRAPHICS_CUBEMAP_HPP_
+#define GONG_GRAPHICS_CUBEMAP_HPP_
+
+#include "Format.hpp"
+#include "TextureBase.hpp"
+
+#include <GL/glew.h>
+
+struct SDL_Surface;
+
+
+namespace gong {
+namespace graphics {
+
+class CubeMap
+: public TextureBase<GL_TEXTURE_CUBE_MAP> {
+
+public:
+       enum Face {
+               RIGHT = GL_TEXTURE_CUBE_MAP_POSITIVE_X,
+               LEFT = GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+               TOP = GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
+               BOTTOM = GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
+               BACK = GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
+               FRONT = GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
+       };
+
+public:
+       CubeMap();
+       ~CubeMap();
+
+       CubeMap(CubeMap &&) noexcept;
+       CubeMap &operator =(CubeMap &&) noexcept;
+
+       CubeMap(const CubeMap &) = delete;
+       CubeMap &operator =(const CubeMap &) = delete;
+
+public:
+       void Data(Face, const SDL_Surface &);
+       void Data(Face, GLsizei w, GLsizei h, const Format &, GLvoid *data) noexcept;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Font.hpp b/src/graphics/Font.hpp
new file mode 100644 (file)
index 0000000..3c5ea05
--- /dev/null
@@ -0,0 +1,75 @@
+#ifndef GONG_GRAPHICS_FONT_HPP_
+#define GONG_GRAPHICS_FONT_HPP_
+
+#include "glm.hpp"
+
+#include <SDL_ttf.h>
+
+
+namespace gong {
+namespace graphics {
+
+class Texture;
+
+class Font {
+
+public:
+       enum FontStyle {
+               STYLE_NORMAL = TTF_STYLE_NORMAL,
+               STYLE_BOLD = TTF_STYLE_BOLD,
+               STYLE_ITALIC = TTF_STYLE_ITALIC,
+               STYLE_UNDERLINE = TTF_STYLE_UNDERLINE,
+               STYLE_STRIKE = TTF_STYLE_STRIKETHROUGH,
+       };
+       enum FontHinting {
+               HINT_NORMAL = TTF_HINTING_NORMAL,
+               HINT_LIGHT = TTF_HINTING_LIGHT,
+               HINT_MONO = TTF_HINTING_MONO,
+               HINT_NONE = TTF_HINTING_NONE,
+       };
+
+public:
+       Font(const char *src, int size, long index = 0);
+       ~Font();
+
+       Font(Font &&) noexcept;
+       Font &operator =(Font &&) noexcept;
+
+       Font(const Font &) = delete;
+       Font &operator =(const Font &) = delete;
+
+public:
+       int Style() const noexcept;
+       void Style(int) const noexcept;
+       int Outline() const noexcept;
+       void Outline(int) noexcept;
+
+       int Hinting() const noexcept;
+       void Hinting(int) const noexcept;
+       bool Kerning() const noexcept;
+       void Kerning(bool) noexcept;
+
+       int Height() const noexcept;
+       int Ascent() const noexcept;
+       int Descent() const noexcept;
+       int LineSkip() const noexcept;
+
+       const char *FamilyName() const noexcept;
+       const char *StyleName() const noexcept;
+
+       bool HasGlyph(Uint16) const noexcept;
+
+       glm::ivec2 TextSize(const char *) const;
+
+       Texture Render(const char *) const;
+       void Render(const char *, Texture &) const;
+
+private:
+       TTF_Font *handle;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Format.hpp b/src/graphics/Format.hpp
new file mode 100644 (file)
index 0000000..6add93b
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef GONG_GRAPHICS_FORMAT_HPP_
+#define GONG_GRAPHICS_FORMAT_HPP_
+
+#include <SDL.h>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+struct Format {
+
+       GLenum format;
+       GLenum type;
+       GLenum internal;
+
+       SDL_PixelFormat sdl_format;
+
+       Format() noexcept;
+       explicit Format(const SDL_PixelFormat &) noexcept;
+
+       bool Compatible(const Format &other) const noexcept;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/PlainColor.hpp b/src/graphics/PlainColor.hpp
new file mode 100644 (file)
index 0000000..fafb462
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef GONG_GRAPHICS_PLAINCOLOR_HPP_
+#define GONG_GRAPHICS_PLAINCOLOR_HPP_
+
+#include "glm.hpp"
+#include "Program.hpp"
+
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+class PlainColor {
+
+public:
+       PlainColor();
+
+       void Activate() noexcept;
+
+       void SetM(const glm::mat4 &m) noexcept;
+       void SetProjection(const glm::mat4 &p) noexcept;
+       void SetView(const glm::mat4 &v) noexcept;
+       void SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept;
+       void SetMVP(const glm::mat4 &m, const glm::mat4 &v, const glm::mat4 &p) noexcept;
+
+       const glm::mat4 &Projection() const noexcept { return projection; }
+       const glm::mat4 &View() const noexcept { return view; }
+       const glm::mat4 &GetVP() const noexcept { return vp; }
+
+private:
+       Program program;
+
+       glm::mat4 projection;
+       glm::mat4 view;
+       glm::mat4 vp;
+
+       GLuint mvp_handle;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/PrimitiveMesh.hpp b/src/graphics/PrimitiveMesh.hpp
new file mode 100644 (file)
index 0000000..99aec8f
--- /dev/null
@@ -0,0 +1,88 @@
+#ifndef GONG_GRAPHICS_PRIMITIVEMESH_HPP_
+#define GONG_GRAPHICS_PRIMITIVEMESH_HPP_
+
+#include "glm.hpp"
+#include "VertexArray.hpp"
+
+#include <vector>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace geometry {
+       struct AABB;
+}
+namespace graphics {
+
+class PrimitiveMesh {
+
+public:
+       using Position = glm::vec3;
+       using Color = TVEC4<unsigned char, glm::precision(0)>;
+       using Index = unsigned short;
+
+       using Positions = std::vector<Position>;
+       using Colors = std::vector<Color>;
+       using Indices = std::vector<Index>;
+
+       enum Attribute {
+               ATTRIB_VERTEX,
+               ATTRIB_COLOR,
+               ATTRIB_INDEX,
+               ATTRIB_COUNT,
+       };
+
+       struct Buffer {
+
+               Positions vertices;
+               Colors colors;
+               Indices indices;
+
+               void Clear() noexcept {
+                       vertices.clear();
+                       colors.clear();
+                       indices.clear();
+               }
+
+               void Reserve(size_t p, size_t i) {
+                       vertices.reserve(p);
+                       colors.reserve(p);
+                       indices.reserve(i);
+               }
+
+               void FillRect(
+                       float w, float h,
+                       const Color &color = Color(0),
+                       const glm::vec2 &pivot = glm::vec2(0.0f)
+               );
+
+               void OutlineBox(
+                       const geometry::AABB &,
+                       const Color &color = Color(0)
+               );
+
+       };
+
+       using VAO = VertexArray<ATTRIB_COUNT>;
+
+public:
+       void Update(const Buffer &) noexcept;
+
+       bool Empty() const noexcept {
+               return vao.Empty();
+       }
+
+       void DrawLines() const noexcept;
+       void DrawTriangles() const noexcept {
+               vao.DrawTriangleElements();
+       }
+
+private:
+       VAO vao;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Program.hpp b/src/graphics/Program.hpp
new file mode 100644 (file)
index 0000000..f5aa270
--- /dev/null
@@ -0,0 +1,51 @@
+#ifndef GONG_GRAPHICS_PROGRAM_HPP_
+#define GONG_GRAPHICS_PROGRAM_HPP_
+
+#include "glm.hpp"
+
+#include <iosfwd>
+#include <list>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+class Shader;
+
+class Program {
+
+public:
+       Program();
+       ~Program();
+
+       Program(const Program &) = delete;
+       Program &operator =(const Program &) = delete;
+
+       const Shader &LoadShader(GLenum type, const GLchar *src);
+       void Attach(Shader &) noexcept;
+       void Link() noexcept;
+       bool Linked() const noexcept;
+       void Log(std::ostream &) const;
+
+       GLint AttributeLocation(const GLchar *name) const noexcept;
+       GLint UniformLocation(const GLchar *name) const noexcept;
+
+       void Uniform(GLint, GLint) noexcept;
+       void Uniform(GLint, float) noexcept;
+       void Uniform(GLint, const glm::vec3 &) noexcept;
+       void Uniform(GLint, const glm::vec4 &) noexcept;
+       void Uniform(GLint, const glm::mat4 &) noexcept;
+
+       void Use() const noexcept { glUseProgram(handle); }
+
+private:
+       GLuint handle;
+       std::list<Shader> shaders;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Shader.hpp b/src/graphics/Shader.hpp
new file mode 100644 (file)
index 0000000..bfc5faf
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef GONG_GRAPHICS_SHADER_HPP_
+#define GONG_GRAPHICS_SHADER_HPP_
+
+#include <iosfwd>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+class Shader {
+
+public:
+       explicit Shader(GLenum type);
+       ~Shader();
+
+       Shader(Shader &&) noexcept;
+       Shader &operator =(Shader &&) noexcept;
+
+       Shader(const Shader &) = delete;
+       Shader &operator =(const Shader &) = delete;
+
+       void Source(const GLchar *src) noexcept;
+       void Compile() noexcept;
+       bool Compiled() const noexcept;
+       void Log(std::ostream &) const;
+
+       void AttachToProgram(GLuint id) const noexcept;
+
+private:
+       GLuint handle;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/SkyBox.hpp b/src/graphics/SkyBox.hpp
new file mode 100644 (file)
index 0000000..22d9916
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef GONG_GRAPHICS_SKYBOX_HPP_
+#define GONG_GRAPHICS_SKYBOX_HPP_
+
+#include "CubeMap.hpp"
+#include "SkyBoxMesh.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+class Viewport;
+
+class SkyBox {
+
+public:
+       explicit SkyBox(CubeMap &&);
+
+       void Render(Viewport &) noexcept;
+
+private:
+       CubeMap texture;
+       SkyBoxMesh mesh;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/SkyBoxMesh.hpp b/src/graphics/SkyBoxMesh.hpp
new file mode 100644 (file)
index 0000000..fb38587
--- /dev/null
@@ -0,0 +1,67 @@
+#ifndef GONG_GRAPHICS_SKYBOXMESH_HPP_
+#define GONG_GRAPHICS_SKYBOXMESH_HPP_
+
+#include "glm.hpp"
+#include "VertexArray.hpp"
+
+#include <vector>
+
+
+namespace gong {
+namespace graphics {
+
+class SkyBoxMesh {
+
+public:
+       using Position = glm::vec3;
+       using Index = unsigned int;
+
+       using Positions = std::vector<Position>;
+       using Indices = std::vector<Index>;
+
+       enum Attribute {
+               ATTRIB_VERTEX,
+               ATTRIB_INDEX,
+               ATTRIB_COUNT,
+       };
+
+       struct Buffer {
+
+               Positions vertices;
+               Indices indices;
+
+               void Clear() noexcept {
+                       vertices.clear();
+                       indices.clear();
+               }
+
+               void Reserve(size_t p, size_t i) {
+                       vertices.reserve(p);
+                       indices.reserve(i);
+               }
+
+       };
+
+       using VAO = VertexArray<ATTRIB_COUNT>;
+
+public:
+       void LoadUnitBox();
+       void Update(const Buffer &) noexcept;
+
+       bool Empty() const noexcept {
+               return vao.Empty();
+       }
+
+       void Draw() const noexcept {
+               vao.DrawTriangleElements();
+       }
+
+private:
+       VAO vao;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/SkyBoxShader.hpp b/src/graphics/SkyBoxShader.hpp
new file mode 100644 (file)
index 0000000..11ffeda
--- /dev/null
@@ -0,0 +1,42 @@
+#ifndef GONG_GRAPHICS_SKYBOXSHADER_HPP_
+#define GONG_GRAPHICS_SKYBOXSHADER_HPP_
+
+#include "CubeMap.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+class SkyBoxShader {
+
+public:
+       SkyBoxShader();
+
+       void Activate() noexcept;
+
+       void SetTexture(CubeMap &) noexcept;
+
+       void SetProjection(const glm::mat4 &p) noexcept;
+       void SetView(const glm::mat4 &v) noexcept;
+       void SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept;
+
+       const glm::mat4 &Projection() const noexcept { return projection; }
+       const glm::mat4 &View() const noexcept { return view; }
+       const glm::mat4 &GetVP() const noexcept { return vp; }
+
+private:
+       Program program;
+
+       glm::mat4 projection;
+       glm::mat4 view;
+       glm::mat4 vp;
+
+       GLuint vp_handle;
+       GLuint sampler_handle;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/SpriteMesh.hpp b/src/graphics/SpriteMesh.hpp
new file mode 100644 (file)
index 0000000..baf763c
--- /dev/null
@@ -0,0 +1,80 @@
+#ifndef GONG_GRPAHICS_SPRITEMESH_HPP_
+#define GONG_GRPAHICS_SPRITEMESH_HPP_
+
+#include "glm.hpp"
+#include "VertexArray.hpp"
+
+#include <vector>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+class SpriteMesh {
+
+public:
+       using Position = glm::vec3;
+       using TexCoord = glm::vec2;
+       using Index = unsigned short;
+
+       using Positions = std::vector<Position>;
+       using TexCoords = std::vector<TexCoord>;
+       using Indices = std::vector<Index>;
+
+       enum Attribute {
+               ATTRIB_VERTEX,
+               ATTRIB_TEXCOORD,
+               ATTRIB_INDEX,
+               ATTRIB_COUNT,
+       };
+
+       struct Buffer {
+
+               Positions vertices;
+               TexCoords coords;
+               Indices indices;
+
+               void Clear() noexcept {
+                       vertices.clear();
+                       coords.clear();
+                       indices.clear();
+               }
+
+               void Reserve(size_t p, size_t i) {
+                       vertices.reserve(p);
+                       coords.reserve(p);
+                       indices.reserve(i);
+               }
+
+               void LoadRect(
+                       float w, float h,
+                       const glm::vec2 &pivot = glm::vec2(0.0f),
+                       const glm::vec2 &tex_begin = glm::vec2(0.0f),
+                       const glm::vec2 &tex_end = glm::vec2(1.0f, 1.0f)
+               );
+
+       };
+
+       using VAO = VertexArray<ATTRIB_COUNT>;
+
+public:
+       void Update(const Buffer &) noexcept;
+
+       bool Empty() const noexcept {
+               return vao.Empty();
+       }
+
+       void Draw() const noexcept {
+               vao.DrawTriangleElements();
+       }
+
+private:
+       VAO vao;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/Texture.hpp b/src/graphics/Texture.hpp
new file mode 100644 (file)
index 0000000..de63cc7
--- /dev/null
@@ -0,0 +1,48 @@
+#ifndef GONG_GRAPHICS_TEXTURE_HPP_
+#define GONG_GRAPHICS_TEXTURE_HPP_
+
+#include "TextureBase.hpp"
+
+#include <GL/glew.h>
+
+struct SDL_Surface;
+
+
+namespace gong {
+namespace graphics {
+
+struct Format;
+
+class Texture
+: public TextureBase<GL_TEXTURE_2D> {
+
+public:
+       Texture();
+       ~Texture();
+
+       Texture(Texture &&) noexcept;
+       Texture &operator =(Texture &&) noexcept;
+
+       Texture(const Texture &) = delete;
+       Texture &operator =(const Texture &) = delete;
+
+public:
+       GLsizei Width() const noexcept { return width; }
+       GLsizei Height() const noexcept { return height; }
+
+       void Data(const SDL_Surface &, bool pad2 = true) noexcept;
+       void Data(GLsizei w, GLsizei h, const Format &, GLvoid *data) noexcept;
+
+       static void UnpackAlignment(GLint) noexcept;
+       static int UnpackAlignmentFromPitch(int) noexcept;
+       static void UnpackRowLength(GLint) noexcept;
+
+private:
+       GLsizei width, height;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/TextureBase.hpp b/src/graphics/TextureBase.hpp
new file mode 100644 (file)
index 0000000..294280c
--- /dev/null
@@ -0,0 +1,71 @@
+#ifndef GONG_GRAPHICS_TEXTUREBASE_HPP_
+#define GONG_GRAPHICS_TEXTUREBASE_HPP_
+
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+template<GLenum TARGET, GLsizei COUNT = 1>
+class TextureBase {
+
+public:
+       TextureBase();
+       ~TextureBase();
+
+       TextureBase(TextureBase &&other) noexcept;
+       TextureBase &operator =(TextureBase &&) noexcept;
+
+       TextureBase(const TextureBase &) = delete;
+       TextureBase &operator =(const TextureBase &) = delete;
+
+public:
+       void Bind(GLsizei which = 0) noexcept {
+               glBindTexture(TARGET, handle[which]);
+       }
+
+       void FilterNearest() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+               glTexParameteri(TARGET, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+       }
+       void FilterLinear() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+               glTexParameteri(TARGET, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+       }
+       void FilterTrilinear() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+               glTexParameteri(TARGET, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+               glGenerateMipmap(TARGET);
+       }
+
+       void WrapEdge() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+       }
+       void WrapBorder() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+       }
+       void WrapRepeat() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_R, GL_REPEAT);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_S, GL_REPEAT);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_T, GL_REPEAT);
+       }
+       void WrapMirror() noexcept {
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
+               glTexParameteri(TARGET, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
+       }
+
+private:
+       GLuint handle[COUNT];
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/VertexArray.hpp b/src/graphics/VertexArray.hpp
new file mode 100644 (file)
index 0000000..ee5313d
--- /dev/null
@@ -0,0 +1,67 @@
+#ifndef GONG_GRAPHICS_VERTEXARRAY_HPP_
+#define GONG_GRAPHICS_VERTEXARRAY_HPP_
+
+#include <vector>
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+template<std::size_t N>
+class VertexArray {
+
+public:
+       static constexpr std::size_t NUM_ATTRS = N;
+
+public:
+       VertexArray() noexcept;
+       ~VertexArray() noexcept;
+
+       VertexArray(const VertexArray<N> &) = delete;
+       VertexArray<N> &operator =(const VertexArray<N> &) = delete;
+
+       VertexArray(VertexArray<N> &&) noexcept;
+       VertexArray<N> &operator =(VertexArray<N> &&) noexcept;
+
+public:
+       bool Empty() const noexcept { return idx_count == 0; }
+
+       void Bind() const noexcept;
+
+       template <class T>
+       void PushAttribute(std::size_t which, const std::vector<T> &data, bool normalized = false) noexcept;
+
+       template<class T>
+       void PushIndices(std::size_t which, const std::vector<T> &indices) noexcept;
+
+       void DrawLineElements() const noexcept;
+       void DrawTriangleElements() const noexcept;
+
+private:
+       void BindAttribute(std::size_t which) const noexcept;
+       void EnableAttribute(std::size_t which) noexcept;
+       template <class T>
+       void AttributeData(const std::vector<T> &) noexcept;
+       template <class T>
+       void AttributePointer(std::size_t which, bool normalized = false) noexcept;
+
+       void BindIndex(std::size_t which) const noexcept;
+       template <class T>
+       void IndexData(const std::vector<T> &) noexcept;
+
+private:
+       GLuint array_id;
+       GLuint attr_id[NUM_ATTRS];
+
+       std::size_t idx_count;
+       GLenum idx_type;
+
+};
+
+}
+}
+
+#include "VertexArray.inl"
+
+#endif
diff --git a/src/graphics/VertexArray.inl b/src/graphics/VertexArray.inl
new file mode 100644 (file)
index 0000000..91e7c2f
--- /dev/null
@@ -0,0 +1,135 @@
+#include "../graphics/gl_traits.hpp"
+
+namespace gong {
+namespace graphics {
+
+template<std::size_t N>
+VertexArray<N>::VertexArray() noexcept
+: idx_count(0)
+, idx_type(GL_UNSIGNED_INT) {
+       glGenVertexArrays(1, &array_id);
+       glGenBuffers(N, attr_id);
+}
+
+template<std::size_t N>
+VertexArray<N>::~VertexArray() noexcept {
+       if (array_id != 0) {
+               glDeleteBuffers(N, attr_id);
+               glDeleteVertexArrays(1, &array_id);
+       }
+}
+
+template<std::size_t N>
+VertexArray<N>::VertexArray(VertexArray<N> &&other) noexcept
+: array_id(other.array_id)
+, idx_count(other.idx_count)
+, idx_type(other.idx_type) {
+       other.array_id = 0;
+       for (std::size_t i = 0; i < N; ++i) {
+               attr_id[i] = other.attr_id[i];
+               other.attr_id[i] = 0;
+       }
+}
+
+template<std::size_t N>
+VertexArray<N> &VertexArray<N>::operator =(VertexArray<N> &&other) noexcept {
+       std::swap(array_id, other.array_id);
+       for (std::size_t i = 0; i < N; ++i) {
+               std::swap(attr_id[i], other.attr_id[i]);
+       }
+       idx_count = other.idx_count;
+       idx_type = other.idx_type;
+       return *this;
+}
+
+template<std::size_t N>
+void VertexArray<N>::Bind() const noexcept {
+       glBindVertexArray(array_id);
+}
+
+template<std::size_t N>
+template <class T>
+void VertexArray<N>::PushAttribute(std::size_t which, const std::vector<T> &data, bool normalized) noexcept {
+       BindAttribute(which);
+       AttributeData(data);
+       EnableAttribute(which);
+       AttributePointer<T>(which, normalized);
+}
+
+template<std::size_t N>
+void VertexArray<N>::BindAttribute(std::size_t i) const noexcept {
+       assert(i < NUM_ATTRS && "vertex attribute ID out of bounds");
+       glBindBuffer(GL_ARRAY_BUFFER, attr_id[i]);
+}
+
+template<std::size_t N>
+void VertexArray<N>::EnableAttribute(std::size_t i) noexcept {
+       assert(i < NUM_ATTRS && "vertex attribute ID out of bounds");
+       glEnableVertexAttribArray(i);
+}
+
+template<std::size_t N>
+template<class T>
+void VertexArray<N>::AttributeData(const std::vector<T> &buf) noexcept {
+       glBufferData(GL_ARRAY_BUFFER, buf.size() * sizeof(T), buf.data(), GL_STATIC_DRAW);
+}
+
+template<std::size_t N>
+template <class T>
+void VertexArray<N>::AttributePointer(std::size_t which, bool normalized) noexcept {
+       glVertexAttribPointer(
+               which,              // program location
+               gl_traits<T>::size, // element size
+               gl_traits<T>::type, // element type
+               normalized,         // normalize to [-1,1] or [0,1] for unsigned types
+               0,                  // stride
+               nullptr             // offset
+       );
+}
+
+template<std::size_t N>
+template <class T>
+void VertexArray<N>::PushIndices(std::size_t which, const std::vector<T> &indices) noexcept {
+       BindIndex(which);
+       IndexData(indices);
+}
+
+template<std::size_t N>
+void VertexArray<N>::BindIndex(std::size_t i) const noexcept {
+       assert(i < NUM_ATTRS && "element index ID out of bounds");
+       glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, attr_id[i]);
+}
+
+template<std::size_t N>
+template<class T>
+void VertexArray<N>::IndexData(const std::vector<T> &buf) noexcept {
+       glBufferData(GL_ELEMENT_ARRAY_BUFFER, buf.size() * sizeof(T), buf.data(), GL_STATIC_DRAW);
+       idx_count = buf.size();
+       idx_type = gl_traits<T>::type;
+}
+
+
+template<std::size_t N>
+void VertexArray<N>::DrawLineElements() const noexcept {
+       Bind();
+       glDrawElements(
+               GL_LINES,  // how
+               idx_count, // count
+               idx_type,  // type
+               nullptr    // offset
+       );
+}
+
+template<std::size_t N>
+void VertexArray<N>::DrawTriangleElements() const noexcept {
+       Bind();
+       glDrawElements(
+               GL_TRIANGLES, // how
+               idx_count,    // count
+               idx_type,     // type
+               nullptr       // offset
+       );
+}
+
+}
+}
diff --git a/src/graphics/Viewport.hpp b/src/graphics/Viewport.hpp
new file mode 100644 (file)
index 0000000..27bcf14
--- /dev/null
@@ -0,0 +1,88 @@
+#ifndef GONG_GRAPHICS_VIEWPORT_HPP_
+#define GONG_GRAPHICS_VIEWPORT_HPP_
+
+#include "align.hpp"
+#include "BlendedSprite.hpp"
+#include "Camera.hpp"
+#include "Canvas.hpp"
+#include "glm.hpp"
+#include "PlainColor.hpp"
+#include "SkyBoxShader.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+class Viewport {
+
+public:
+       Viewport();
+
+       Viewport(const Viewport &) = delete;
+       Viewport &operator =(const Viewport &) = delete;
+
+       void VSync(bool b);
+
+       void EnableDepthTest() noexcept;
+       void EqualDepthTest() noexcept;
+       void DisableDepthTest() noexcept;
+
+       void EnableBackfaceCulling() noexcept;
+       void DisableBackfaceCulling() noexcept;
+
+       void EnableAlphaBlending() noexcept;
+       void EnableInvertBlending() noexcept;
+       void DisableBlending() noexcept;
+
+       void Resize(int w, int h) noexcept;
+
+       float Width() const noexcept { return canv.Size().x; }
+       float Height() const noexcept { return canv.Size().y; }
+
+       void Clear() noexcept;
+       void ClearDepth() noexcept;
+
+       glm::vec2 GetPosition(const glm::vec2 &off, Gravity grav) const noexcept;
+
+       void SetCursor(const glm::vec3 &) noexcept;
+       void SetCursor(const glm::vec3 &, Gravity) noexcept;
+       void MoveCursor(const glm::vec3 &) noexcept;
+       const glm::mat4 &Cursor() const noexcept { return cursor; }
+
+       void OffsetCamera(const glm::vec3 &o) noexcept { cam_offset = o; }
+       const glm::vec3 &CameraOffset() const noexcept { return cam_offset; }
+
+       PlainColor &HUDColorProgram() noexcept;
+       SkyBoxShader &SkyBoxProgram() noexcept;
+       BlendedSprite &SpriteProgram() noexcept;
+
+       void WorldPosition(const glm::mat4 &) noexcept;
+
+       const glm::mat4 &Perspective() const noexcept { return cam.Projection(); }
+       const glm::mat4 &Ortho() const noexcept { return canv.Projection(); }
+
+private:
+       Camera cam;
+       Canvas canv;
+
+       glm::mat4 cursor;
+
+       glm::vec3 cam_offset;
+
+       PlainColor color_prog;
+       SkyBoxShader sky_prog;
+       BlendedSprite sprite_prog;
+
+       enum {
+               NONE,
+               COLOR_HUD,
+               SKY_BOX,
+               SPRITE,
+       } active_prog;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/graphics/align.hpp b/src/graphics/align.hpp
new file mode 100644 (file)
index 0000000..298d6dd
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef GONG_GRAPHICS_ALIGN_HPP_
+#define GONG_GRAPHICS_ALIGN_HPP_
+
+#include "glm.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+enum class Align {
+       BEGIN,
+       MIDDLE,
+       END,
+};
+
+enum class Gravity {
+       NORTH_WEST,
+       NORTH,
+       NORTH_EAST,
+       WEST,
+       CENTER,
+       EAST,
+       SOUTH_WEST,
+       SOUTH,
+       SOUTH_EAST,
+};
+
+inline Align get_x(Gravity g) noexcept {
+       return Align(int(g) % 3);
+}
+
+inline Align get_y(Gravity g) noexcept {
+       return Align(int(g) / 3);
+}
+
+inline Gravity get_gravity(Align x, Align y) noexcept {
+       return Gravity(int(y) * 3 + int(x));
+}
+
+inline glm::vec2 align(
+       Gravity g,
+       const glm::vec2 &size,
+       const glm::vec2 &offset = glm::vec2(0.0f, 0.0f)
+) {
+       return glm::vec2(
+               size.x * 0.5 * int(get_x(g)) + offset.x,
+               size.y * 0.5 * int(get_y(g)) + offset.y
+       );
+}
+
+}
+}
+
+#endif
diff --git a/src/graphics/gl_traits.cpp b/src/graphics/gl_traits.cpp
new file mode 100644 (file)
index 0000000..cdbe7ce
--- /dev/null
@@ -0,0 +1,32 @@
+#include "gl_traits.hpp"
+
+
+namespace gong {
+namespace graphics {
+
+constexpr GLint gl_traits<signed char>::size;
+constexpr GLenum gl_traits<signed char>::type;
+
+constexpr GLint gl_traits<unsigned char>::size;
+constexpr GLenum gl_traits<unsigned char>::type;
+
+constexpr GLint gl_traits<short>::size;
+constexpr GLenum gl_traits<short>::type;
+
+constexpr GLint gl_traits<unsigned short>::size;
+constexpr GLenum gl_traits<unsigned short>::type;
+
+constexpr GLint gl_traits<int>::size;
+constexpr GLenum gl_traits<int>::type;
+
+constexpr GLint gl_traits<unsigned int>::size;
+constexpr GLenum gl_traits<unsigned int>::type;
+
+constexpr GLint gl_traits<float>::size;
+constexpr GLenum gl_traits<float>::type;
+
+constexpr GLint gl_traits<double>::size;
+constexpr GLenum gl_traits<double>::type;
+
+}
+}
diff --git a/src/graphics/gl_traits.hpp b/src/graphics/gl_traits.hpp
new file mode 100644 (file)
index 0000000..f7cb52e
--- /dev/null
@@ -0,0 +1,122 @@
+#ifndef GONG_GRAPHICS_GL_TRAITS_HPP_
+#define GONG_GRAPHICS_GL_TRAITS_HPP_
+
+#include "glm.hpp"
+
+#include <GL/glew.h>
+
+
+namespace gong {
+namespace graphics {
+
+template<class T>
+struct gl_traits {
+
+       /// number of components per generic attribute
+       /// must be 1, 2, 3, 4
+       // static constexpr GLint size;
+
+       /// component type
+       /// accepted values are:
+       ///   GL_BYTE, GL_UNSIGNED_BYTE,
+       ///   GL_SHORT, GL_UNSIGNED_SHORT,
+       ///   GL_INT, GL_UNSIGNED_INT,
+       ///   GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE,
+       ///   GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV
+       // static constexpr GLenum type;
+
+};
+
+
+// basic types
+
+template<> struct gl_traits<signed char> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_BYTE;
+};
+
+template<> struct gl_traits<unsigned char> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_UNSIGNED_BYTE;
+};
+
+template<> struct gl_traits<short> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_SHORT;
+};
+
+template<> struct gl_traits<unsigned short> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_UNSIGNED_SHORT;
+};
+
+template<> struct gl_traits<int> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_INT;
+};
+
+template<> struct gl_traits<unsigned int> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_UNSIGNED_INT;
+};
+
+template<> struct gl_traits<float> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_FLOAT;
+};
+
+template<> struct gl_traits<double> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = GL_DOUBLE;
+};
+
+// composite types
+
+template<>
+template<class T, glm::precision P>
+struct gl_traits<TVEC1<T, P>> {
+       static constexpr GLint size = 1;
+       static constexpr GLenum type = gl_traits<T>::type;
+};
+template<class T, glm::precision P>
+constexpr GLint gl_traits<TVEC1<T, P>>::size;
+template<class T, glm::precision P>
+constexpr GLenum gl_traits<TVEC1<T, P>>::type;
+
+template<>
+template<class T, glm::precision P>
+struct gl_traits<TVEC2<T, P>> {
+       static constexpr GLint size = 2;
+       static constexpr GLenum type = gl_traits<T>::type;
+};
+template<class T, glm::precision P>
+constexpr GLint gl_traits<TVEC2<T, P>>::size;
+template<class T, glm::precision P>
+constexpr GLenum gl_traits<TVEC2<T, P>>::type;
+
+template<>
+template<class T, glm::precision P>
+struct gl_traits<TVEC3<T, P>> {
+       static constexpr GLint size = 3;
+       static constexpr GLenum type = gl_traits<T>::type;
+};
+template<class T, glm::precision P>
+constexpr GLint gl_traits<TVEC3<T, P>>::size;
+template<class T, glm::precision P>
+constexpr GLenum gl_traits<TVEC3<T, P>>::type;
+
+template<>
+template<class T, glm::precision P>
+struct gl_traits<TVEC4<T, P>> {
+       static constexpr GLint size = 4;
+       static constexpr GLenum type = gl_traits<T>::type;
+};
+template<class T, glm::precision P>
+constexpr GLint gl_traits<TVEC4<T, P>>::size;
+template<class T, glm::precision P>
+constexpr GLenum gl_traits<TVEC4<T, P>>::type;
+
+}
+}
+
+#endif
diff --git a/src/graphics/glm.hpp b/src/graphics/glm.hpp
new file mode 100644 (file)
index 0000000..ae8b373
--- /dev/null
@@ -0,0 +1,24 @@
+#ifndef GONG_GRAPHICS_GLM_HPP_
+#define GONG_GRAPHICS_GLM_HPP_
+
+#ifndef GLM_FORCE_RADIANS
+#  define GLM_FORCE_RADIANS 1
+#endif
+
+#include <glm/glm.hpp>
+
+// GLM moved tvec[1234] from glm::detail to glm in 0.9.6
+
+#if GLM_VERSION < 96
+#  define TVEC1 glm::detail::tvec1
+#  define TVEC2 glm::detail::tvec2
+#  define TVEC3 glm::detail::tvec3
+#  define TVEC4 glm::detail::tvec4
+#else
+#  define TVEC1 glm::tvec1
+#  define TVEC2 glm::tvec2
+#  define TVEC3 glm::tvec3
+#  define TVEC4 glm::tvec4
+#endif
+
+#endif
diff --git a/src/graphics/mesh.cpp b/src/graphics/mesh.cpp
new file mode 100644 (file)
index 0000000..d31dbd3
--- /dev/null
@@ -0,0 +1,143 @@
+#include "PrimitiveMesh.hpp"
+#include "SkyBoxMesh.hpp"
+#include "SpriteMesh.hpp"
+
+#include "../geometry/primitive.hpp"
+
+#include <algorithm>
+#include <iostream>
+
+
+namespace gong {
+namespace graphics {
+
+void PrimitiveMesh::Buffer::FillRect(
+       float w, float h,
+       const Color &color,
+       const glm::vec2 &pivot
+) {
+       Clear();
+       Reserve(4, 6);
+
+       vertices.emplace_back( -pivot.x,  -pivot.y, 0.0f);
+       vertices.emplace_back(w-pivot.x,  -pivot.y, 0.0f);
+       vertices.emplace_back( -pivot.x, h-pivot.y, 0.0f);
+       vertices.emplace_back(w-pivot.x, h-pivot.y, 0.0f);
+
+       colors.resize(4, color);
+
+       indices.assign({ 0, 2, 1, 1, 2, 3 });
+}
+
+void PrimitiveMesh::Buffer::OutlineBox(const geometry::AABB &box, const Color &color) {
+       Clear();
+       Reserve(8, 24);
+
+       vertices.emplace_back(box.min.x, box.min.y, box.min.z);
+       vertices.emplace_back(box.min.x, box.min.y, box.max.z);
+       vertices.emplace_back(box.min.x, box.max.y, box.min.z);
+       vertices.emplace_back(box.min.x, box.max.y, box.max.z);
+       vertices.emplace_back(box.max.x, box.min.y, box.min.z);
+       vertices.emplace_back(box.max.x, box.min.y, box.max.z);
+       vertices.emplace_back(box.max.x, box.max.y, box.min.z);
+       vertices.emplace_back(box.max.x, box.max.y, box.max.z);
+
+       colors.resize(8, color);
+
+       indices.assign({
+               0, 1, 1, 3, 3, 2, 2, 0, // left
+               4, 5, 5, 7, 7, 6, 6, 4, // right
+               0, 4, 1, 5, 3, 7, 2, 6, // others
+       });
+}
+
+
+void PrimitiveMesh::Update(const Buffer &buf) noexcept {
+#ifndef NDEBUG
+       if (buf.colors.size() < buf.vertices.size()) {
+               std::cerr << "PrimitiveMesh: not enough colors!" << std::endl;
+       }
+#endif
+
+       vao.Bind();
+       vao.PushAttribute(ATTRIB_VERTEX, buf.vertices);
+       vao.PushAttribute(ATTRIB_COLOR, buf.colors, true);
+       vao.PushIndices(ATTRIB_INDEX, buf.indices);
+}
+
+
+void PrimitiveMesh::DrawLines() const noexcept {
+       glEnable(GL_LINE_SMOOTH);
+       glLineWidth(2.0f);
+       vao.DrawLineElements();
+}
+
+
+void SkyBoxMesh::LoadUnitBox() {
+       Buffer buffer;
+       buffer.vertices = {
+               {  1.0f,  1.0f,  1.0f },
+               {  1.0f,  1.0f, -1.0f },
+               {  1.0f, -1.0f,  1.0f },
+               {  1.0f, -1.0f, -1.0f },
+               { -1.0f,  1.0f,  1.0f },
+               { -1.0f,  1.0f, -1.0f },
+               { -1.0f, -1.0f,  1.0f },
+               { -1.0f, -1.0f, -1.0f },
+       };
+       buffer.indices = {
+               5, 7, 3,  3, 1, 5,
+               6, 7, 5,  5, 4, 6,
+               3, 2, 0,  0, 1, 3,
+               6, 4, 0,  0, 2, 6,
+               5, 1, 0,  0, 4, 5,
+               7, 6, 3,  3, 6, 2,
+       };
+       Update(buffer);
+}
+
+void SkyBoxMesh::Update(const Buffer &buf) noexcept {
+       vao.Bind();
+       vao.PushAttribute(ATTRIB_VERTEX, buf.vertices);
+       vao.PushIndices(ATTRIB_INDEX, buf.indices);
+}
+
+
+void SpriteMesh::Buffer::LoadRect(
+       float w, float h,
+       const glm::vec2 &pivot,
+       const glm::vec2 &tex_begin,
+       const glm::vec2 &tex_end
+) {
+       Clear();
+       Reserve(4, 6);
+
+       vertices.emplace_back( -pivot.x,  -pivot.y, 0.0f);
+       vertices.emplace_back(w-pivot.x,  -pivot.y, 0.0f);
+       vertices.emplace_back( -pivot.x, h-pivot.y, 0.0f);
+       vertices.emplace_back(w-pivot.x, h-pivot.y, 0.0f);
+
+       coords.emplace_back(tex_begin.x, tex_begin.y);
+       coords.emplace_back(tex_end.x,   tex_begin.y);
+       coords.emplace_back(tex_begin.x, tex_end.y);
+       coords.emplace_back(tex_end.x,   tex_end.y);
+
+       indices.assign({ 0, 2, 1, 1, 2, 3 });
+}
+
+
+void SpriteMesh::Update(const Buffer &buf) noexcept {
+#ifndef NDEBUG
+       if (buf.coords.size() < buf.vertices.size()) {
+               std::cerr << "SpriteMesh: not enough coords!" << std::endl;
+       }
+#endif
+
+       vao.Bind();
+       vao.PushAttribute(ATTRIB_VERTEX, buf.vertices);
+       vao.PushAttribute(ATTRIB_TEXCOORD, buf.coords);
+       vao.PushIndices(ATTRIB_INDEX, buf.indices);
+}
+
+}
+}
diff --git a/src/graphics/render.cpp b/src/graphics/render.cpp
new file mode 100644 (file)
index 0000000..4686867
--- /dev/null
@@ -0,0 +1,469 @@
+#include "ArrayTexture.hpp"
+#include "CubeMap.hpp"
+#include "Font.hpp"
+#include "Format.hpp"
+#include "Texture.hpp"
+#include "TextureBase.hpp"
+#include "Viewport.hpp"
+
+#include "../app/error.hpp"
+
+#include <algorithm>
+#include <cstring>
+#include <iostream>
+#include <memory>
+#include <stdexcept>
+
+
+namespace gong {
+namespace graphics {
+
+Font::Font(const char *src, int size, long index)
+: handle(TTF_OpenFontIndex(src, size, index)) {
+       if (!handle) {
+               throw app::TTFError("TTF_OpenFontIndex");
+       }
+}
+
+Font::~Font() {
+       if (handle) {
+               TTF_CloseFont(handle);
+       }
+}
+
+Font::Font(Font &&other) noexcept
+: handle(other.handle) {
+       other.handle = nullptr;
+}
+
+Font &Font::operator =(Font &&other) noexcept {
+       std::swap(handle, other.handle);
+       return *this;
+}
+
+
+int Font::Style() const noexcept {
+       return TTF_GetFontStyle(handle);
+}
+
+void Font::Style(int s) const noexcept {
+       TTF_SetFontStyle(handle, s);
+}
+
+int Font::Outline() const noexcept {
+       return TTF_GetFontOutline(handle);
+}
+
+void Font::Outline(int px) noexcept {
+       TTF_SetFontOutline(handle, px);
+}
+
+
+int Font::Hinting() const noexcept {
+       return TTF_GetFontHinting(handle);
+}
+
+void Font::Hinting(int h) const noexcept {
+       TTF_SetFontHinting(handle, h);
+}
+
+bool Font::Kerning() const noexcept {
+       return TTF_GetFontKerning(handle);
+}
+
+void Font::Kerning(bool b) noexcept {
+       TTF_SetFontKerning(handle, b);
+}
+
+
+int Font::Height() const noexcept {
+       return TTF_FontHeight(handle);
+}
+
+int Font::Ascent() const noexcept {
+       return TTF_FontAscent(handle);
+}
+
+int Font::Descent() const noexcept {
+       return TTF_FontDescent(handle);
+}
+
+int Font::LineSkip() const noexcept {
+       return TTF_FontLineSkip(handle);
+}
+
+
+const char *Font::FamilyName() const noexcept {
+       return TTF_FontFaceFamilyName(handle);
+}
+
+const char *Font::StyleName() const noexcept {
+       return TTF_FontFaceStyleName(handle);
+}
+
+
+bool Font::HasGlyph(Uint16 c) const noexcept {
+       return TTF_GlyphIsProvided(handle, c);
+}
+
+
+glm::ivec2 Font::TextSize(const char *text) const {
+       glm::ivec2 size;
+       if (TTF_SizeUTF8(handle, text, &size.x, &size.y) != 0) {
+               throw app::TTFError("TTF_SizeUTF8");
+       }
+       return size;
+}
+
+Texture Font::Render(const char *text) const {
+       Texture tex;
+       Render(text, tex);
+       return tex;
+}
+
+void Font::Render(const char *text, Texture &tex) const {
+       SDL_Surface *srf = TTF_RenderUTF8_Blended(handle, text, { 0xFF, 0xFF, 0xFF, 0xFF });
+       if (!srf) {
+               throw app::TTFError("TTF_RenderUTF8_Blended");
+       }
+       tex.Bind();
+       tex.Data(*srf, false);
+       tex.FilterLinear();
+       SDL_FreeSurface(srf);
+}
+
+Format::Format() noexcept
+: format(GL_BGRA)
+, type(GL_UNSIGNED_INT_8_8_8_8_REV)
+, internal(GL_RGBA8) {
+       sdl_format.format = SDL_PIXELFORMAT_ARGB8888;
+       sdl_format.palette = nullptr;
+       sdl_format.BitsPerPixel = 32;
+       sdl_format.BytesPerPixel = 4;
+       sdl_format.Rmask = 0x00FF0000;
+       sdl_format.Gmask = 0x0000FF00;
+       sdl_format.Bmask = 0x000000FF;
+       sdl_format.Amask = 0xFF000000;
+       sdl_format.Rloss = 0;
+       sdl_format.Gloss = 0;
+       sdl_format.Bloss = 0;
+       sdl_format.Aloss = 0;
+       sdl_format.Rshift = 16;
+       sdl_format.Gshift = 8;
+       sdl_format.Bshift = 0;
+       sdl_format.Ashift = 24;
+       sdl_format.refcount = 1;
+       sdl_format.next = nullptr;
+}
+
+Format::Format(const SDL_PixelFormat &fmt) noexcept
+: sdl_format(fmt) {
+       if (fmt.BytesPerPixel == 4) {
+               if (fmt.Amask == 0xFF) {
+                       if (fmt.Rmask == 0xFF00) {
+                               format = GL_BGRA;
+                       } else {
+                               format = GL_RGBA;
+                       }
+                       type = GL_UNSIGNED_INT_8_8_8_8;
+               } else {
+                       if (fmt.Rmask == 0xFF) {
+                               format = GL_RGBA;
+                       } else {
+                               format = GL_BGRA;
+                       }
+                       type = GL_UNSIGNED_INT_8_8_8_8_REV;
+               }
+               internal = GL_RGBA8;
+       } else {
+               if (fmt.Rmask == 0xFF) {
+                       format = GL_RGB;
+               } else {
+                       format = GL_BGR;
+               }
+               type = GL_UNSIGNED_BYTE;
+               internal = GL_RGB8;
+       }
+}
+
+bool Format::Compatible(const Format &other) const noexcept {
+       return format == other.format && type == other.type && internal == other.internal;
+}
+
+
+template<GLenum TARGET, GLsizei COUNT>
+TextureBase<TARGET, COUNT>::TextureBase() {
+       glGenTextures(COUNT, handle);
+}
+
+template<GLenum TARGET, GLsizei COUNT>
+TextureBase<TARGET, COUNT>::~TextureBase() {
+       glDeleteTextures(COUNT, handle);
+}
+
+template<GLenum TARGET, GLsizei COUNT>
+TextureBase<TARGET, COUNT>::TextureBase(TextureBase &&other) noexcept {
+       std::memcpy(handle, other.handle, sizeof(handle));
+       std::memset(other.handle, 0, sizeof(handle));
+}
+
+template<GLenum TARGET, GLsizei COUNT>
+TextureBase<TARGET, COUNT> &TextureBase<TARGET, COUNT>::operator =(TextureBase &&other) noexcept {
+       std::swap(handle, other.handle);
+       return *this;
+}
+
+
+Texture::Texture()
+: TextureBase()
+, width(0)
+, height(0) {
+
+}
+
+Texture::~Texture() {
+
+}
+
+Texture::Texture(Texture &&other) noexcept
+: TextureBase(std::move(other)) {
+       width = other.width;
+       height = other.height;
+}
+
+Texture &Texture::operator =(Texture &&other) noexcept {
+       TextureBase::operator =(std::move(other));
+       width = other.width;
+       height = other.height;
+       return *this;
+}
+
+
+namespace {
+       bool ispow2(unsigned int i) {
+               // don't care about i == 0 here
+               return !(i & (i - 1));
+       }
+}
+
+void Texture::Data(const SDL_Surface &srf, bool pad2) noexcept {
+       Format format(*srf.format);
+
+       if (!pad2 || (ispow2(srf.w) && ispow2(srf.h))) {
+               int align = UnpackAlignmentFromPitch(srf.pitch);
+
+               int pitch = (srf.w * srf.format->BytesPerPixel + align - 1) / align * align;
+               if (srf.pitch - pitch >= align) {
+                       UnpackRowLength(srf.pitch / srf.format->BytesPerPixel);
+               } else {
+                       UnpackRowLength(0);
+               }
+
+               Data(srf.w, srf.h, format, srf.pixels);
+
+               UnpackRowLength(0);
+       } else if (srf.w > (1 << 30) || srf.h > (1 << 30)) {
+               // That's at least one gigapixel in either or both dimensions.
+               // If this is not an error, that's an insanely large or high
+               // resolution texture.
+#ifndef NDEBUG
+               std::cerr << "texture size exceeds 2^30, aborting data import" << std::endl;
+#endif
+       } else {
+               GLsizei width = 1, height = 1;
+               while (width < srf.w) {
+                       width <<= 1;
+               }
+               while (height < srf.h) {
+                       height <<= 1;
+               }
+               size_t pitch = width * srf.format->BytesPerPixel;
+               size_t size = pitch * height;
+               size_t row_pad = pitch - srf.pitch;
+               std::unique_ptr<unsigned char[]> data(new unsigned char[size]);
+               unsigned char *src = reinterpret_cast<unsigned char *>(srf.pixels);
+               unsigned char *dst = data.get();
+               for (int row = 0; row < srf.h; ++row) {
+                       std::memcpy(dst, src, srf.pitch);
+                       src += srf.pitch;
+                       dst += srf.pitch;
+                       std::memset(dst, 0, row_pad);
+                       dst += row_pad;
+               }
+               std::memset(dst, 0, (height - srf.h) * pitch);
+               UnpackAlignmentFromPitch(pitch);
+               Data(width, height, format, data.get());
+       }
+
+       UnpackAlignment(4);
+}
+
+void Texture::Data(GLsizei w, GLsizei h, const Format &format, GLvoid *data) noexcept {
+       glTexImage2D(
+               GL_TEXTURE_2D,
+               0, format.internal,
+               w, h,
+               0,
+               format.format, format.type,
+               data
+       );
+       width = w;
+       height = h;
+}
+
+
+void Texture::UnpackAlignment(GLint i) noexcept {
+       glPixelStorei(GL_UNPACK_ALIGNMENT, i);
+}
+
+int Texture::UnpackAlignmentFromPitch(int pitch) noexcept {
+       int align = 8;
+       while (pitch % align) {
+               align >>= 1;
+       }
+       UnpackAlignment(align);
+       return align;
+}
+
+void Texture::UnpackRowLength(GLint i) noexcept {
+       glPixelStorei(GL_UNPACK_ROW_LENGTH, i);
+}
+
+
+ArrayTexture::ArrayTexture()
+: TextureBase()
+, width(0)
+, height(0)
+, depth(0) {
+
+}
+
+ArrayTexture::~ArrayTexture() {
+
+}
+
+ArrayTexture::ArrayTexture(ArrayTexture &&other) noexcept
+: TextureBase(std::move(other)) {
+       width = other.width;
+       height = other.height;
+       depth = other.depth;
+}
+
+ArrayTexture &ArrayTexture::operator =(ArrayTexture &&other) noexcept {
+       TextureBase::operator =(std::move(other));
+       width = other.width;
+       height = other.height;
+       depth = other.depth;
+       return *this;
+}
+
+
+void ArrayTexture::Reserve(GLsizei w, GLsizei h, GLsizei d, const Format &f) noexcept {
+       glTexStorage3D(
+               GL_TEXTURE_2D_ARRAY, // which
+               1,                   // mipmap count
+               f.internal,          // format
+               w, h,                // dimensions
+               d                    // layer count
+       );
+       width = w;
+       height = h;
+       depth = d;
+       format = f;
+}
+
+void ArrayTexture::Data(GLsizei l, const SDL_Surface &srf) {
+       Format fmt(*srf.format);
+       if (format.Compatible(fmt)) {
+               Data(l, fmt, srf.pixels);
+       } else {
+               SDL_Surface *converted = SDL_ConvertSurface(
+                       const_cast<SDL_Surface *>(&srf),
+                       &format.sdl_format,
+                       0
+               );
+               if (!converted) {
+                       throw app::SDLError("SDL_ConvertSurface");
+               }
+               Format new_fmt(*converted->format);
+               if (!format.Compatible(new_fmt)) {
+                       SDL_FreeSurface(converted);
+                       throw std::runtime_error("unable to convert texture input");
+               }
+               Data(l, new_fmt, converted->pixels);
+               SDL_FreeSurface(converted);
+       }
+}
+
+void ArrayTexture::Data(GLsizei l, const Format &f, GLvoid *data) noexcept {
+       glTexSubImage3D(
+               GL_TEXTURE_2D_ARRAY, // which
+               0,                   // mipmap lavel
+               0, 0,                // dest X and Y offset
+               l,                   // layer offset
+               width, height,
+               1,                   // layer count
+               f.format, f.type,
+               data
+       );
+}
+
+
+CubeMap::CubeMap()
+: TextureBase() {
+
+}
+
+CubeMap::~CubeMap() {
+
+}
+
+CubeMap::CubeMap(CubeMap &&other) noexcept
+: TextureBase(std::move(other)) {
+
+}
+
+CubeMap &CubeMap::operator =(CubeMap &&other) noexcept {
+       TextureBase::operator =(std::move(other));
+       return *this;
+}
+
+
+void CubeMap::Data(Face f, const SDL_Surface &srf) {
+       Format format;
+       Format fmt(*srf.format);
+       if (format.Compatible(fmt)) {
+               Data(f, srf.w, srf.h, fmt, srf.pixels);
+       } else {
+               SDL_Surface *converted = SDL_ConvertSurface(
+                       const_cast<SDL_Surface *>(&srf),
+                       &format.sdl_format,
+                       0
+               );
+               if (!converted) {
+                       throw app::SDLError("SDL_ConvertSurface");
+               }
+               Format new_fmt(*converted->format);
+               if (!format.Compatible(new_fmt)) {
+                       SDL_FreeSurface(converted);
+                       throw std::runtime_error("unable to convert texture input");
+               }
+               Data(f, converted->w, converted->h, new_fmt, converted->pixels);
+               SDL_FreeSurface(converted);
+       }
+}
+
+void CubeMap::Data(Face face, GLsizei w, GLsizei h, const Format &f, GLvoid *data) noexcept {
+       glTexImage2D(
+               face,             // which
+               0,                // mipmap level
+               f.internal,       // internal format
+               w, h,             // size
+               0,                // border
+               f.format, f.type, // pixel format
+               data              // pixel data
+       );
+}
+
+}
+}
diff --git a/src/graphics/shader.cpp b/src/graphics/shader.cpp
new file mode 100644 (file)
index 0000000..1e4c7f5
--- /dev/null
@@ -0,0 +1,386 @@
+#include "BlendedSprite.hpp"
+#include "PlainColor.hpp"
+#include "Program.hpp"
+#include "Shader.hpp"
+#include "SkyBoxShader.hpp"
+
+#include "ArrayTexture.hpp"
+#include "CubeMap.hpp"
+#include "Texture.hpp"
+#include "../app/error.hpp"
+
+#include <algorithm>
+#include <iostream>
+#include <memory>
+#include <ostream>
+#include <stdexcept>
+#include <string>
+#include <glm/gtc/type_ptr.hpp>
+
+
+namespace gong {
+namespace graphics {
+
+Shader::Shader(GLenum type)
+: handle(glCreateShader(type)) {
+       if (handle == 0) {
+               throw app::GLError("glCreateShader");
+       }
+}
+
+Shader::~Shader() {
+       if (handle != 0) {
+               glDeleteShader(handle);
+       }
+}
+
+Shader::Shader(Shader &&other) noexcept
+: handle(other.handle) {
+       other.handle = 0;
+}
+
+Shader &Shader::operator =(Shader &&other) noexcept {
+       std::swap(handle, other.handle);
+       return *this;
+}
+
+
+void Shader::Source(const GLchar *src) noexcept {
+       const GLchar* src_arr[] = { src };
+       glShaderSource(handle, 1, src_arr, nullptr);
+}
+
+void Shader::Compile() noexcept {
+       glCompileShader(handle);
+}
+
+bool Shader::Compiled() const noexcept {
+       GLint compiled = GL_FALSE;
+       glGetShaderiv(handle, GL_COMPILE_STATUS, &compiled);
+       return compiled == GL_TRUE;
+}
+
+void Shader::Log(std::ostream &out) const {
+       int log_len = 0, max_len = 0;
+       glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &max_len);
+       std::unique_ptr<char[]> log(new char[max_len]);
+       glGetShaderInfoLog(handle, max_len, &log_len, log.get());
+       out.write(log.get(), log_len);
+}
+
+
+void Shader::AttachToProgram(GLuint id) const noexcept {
+       glAttachShader(id, handle);
+}
+
+
+Program::Program()
+: handle(glCreateProgram()) {
+       if (handle == 0) {
+               throw app::GLError("glCreateProgram");
+       }
+}
+
+Program::~Program() {
+       if (handle != 0) {
+               glDeleteProgram(handle);
+       }
+}
+
+
+const Shader &Program::LoadShader(GLenum type, const GLchar *src) {
+       shaders.emplace_back(type);
+       Shader &shader = shaders.back();
+       shader.Source(src);
+       shader.Compile();
+       if (!shader.Compiled()) {
+               shader.Log(std::cerr);
+               throw std::runtime_error("compile shader");
+       }
+       Attach(shader);
+       return shader;
+}
+
+void Program::Attach(Shader &shader) noexcept {
+       shader.AttachToProgram(handle);
+}
+
+void Program::Link() noexcept {
+       glLinkProgram(handle);
+}
+
+bool Program::Linked() const noexcept {
+       GLint linked = GL_FALSE;
+       glGetProgramiv(handle, GL_LINK_STATUS, &linked);
+       return linked == GL_TRUE;
+}
+
+void Program::Log(std::ostream &out) const {
+       int log_len = 0, max_len = 0;
+       glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &max_len);
+       std::unique_ptr<char[]> log(new char[max_len]);
+       glGetProgramInfoLog(handle, max_len, &log_len, log.get());
+       out.write(log.get(), log_len);
+}
+
+
+GLint Program::AttributeLocation(const GLchar *name) const noexcept {
+       return glGetAttribLocation(handle, name);
+}
+
+GLint Program::UniformLocation(const GLchar *name) const noexcept {
+       return glGetUniformLocation(handle, name);
+}
+
+
+void Program::Uniform(GLint loc, GLint val) noexcept {
+       glUniform1i(loc, val);
+}
+
+void Program::Uniform(GLint loc, float val) noexcept {
+       glUniform1f(loc, val);
+}
+
+void Program::Uniform(GLint loc, const glm::vec3 &val) noexcept {
+       glUniform3fv(loc, 1, glm::value_ptr(val));
+}
+
+void Program::Uniform(GLint loc, const glm::vec4 &val) noexcept {
+       glUniform4fv(loc, 1, glm::value_ptr(val));
+}
+
+void Program::Uniform(GLint loc, const glm::mat4 &val) noexcept {
+       glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value_ptr(val));
+}
+
+
+BlendedSprite::BlendedSprite()
+: program()
+, vp(1.0f)
+, mvp_handle(0)
+, sampler_handle(0) {
+       program.LoadShader(
+               GL_VERTEX_SHADER,
+               "#version 330 core\n"
+               "layout(location = 0) in vec3 vtx_position;\n"
+               "layout(location = 1) in vec2 vtx_tex_uv;\n"
+               "uniform mat4 MVP;\n"
+               "out vec2 frag_tex_uv;\n"
+               "void main() {\n"
+                       "gl_Position = MVP * vec4(vtx_position, 1);\n"
+                       "frag_tex_uv = vtx_tex_uv;\n"
+               "}\n"
+       );
+       program.LoadShader(
+               GL_FRAGMENT_SHADER,
+               "#version 330 core\n"
+               "in vec2 frag_tex_uv;\n"
+               "uniform sampler2D tex_sampler;\n"
+               "uniform vec4 fg_factor;\n"
+               "uniform vec4 bg_factor;\n"
+               "out vec4 color;\n"
+               "void main() {\n"
+                       "vec4 tex_color = texture(tex_sampler, frag_tex_uv);\n"
+                       "vec4 factor = mix(bg_factor, fg_factor, tex_color.a);\n"
+                       "color = tex_color * factor;\n"
+                       "color.a = factor.a;\n"
+               "}\n"
+       );
+       program.Link();
+       if (!program.Linked()) {
+               program.Log(std::cerr);
+               throw std::runtime_error("link program");
+       }
+
+       mvp_handle = program.UniformLocation("MVP");
+       sampler_handle = program.UniformLocation("tex_sampler");
+       fg_handle = program.UniformLocation("fg_factor");
+       bg_handle = program.UniformLocation("bg_factor");
+
+       Activate();
+       SetFG(glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
+       SetBG(glm::vec4(1.0f, 1.0f, 1.0f, 0.0f));
+}
+
+
+void BlendedSprite::Activate() noexcept {
+       program.Use();
+}
+
+void BlendedSprite::SetM(const glm::mat4 &m) noexcept {
+       program.Uniform(mvp_handle, vp * m);
+}
+
+void BlendedSprite::SetProjection(const glm::mat4 &p) noexcept {
+       projection = p;
+       vp = p * view;
+}
+
+void BlendedSprite::SetView(const glm::mat4 &v) noexcept {
+       view = v;
+       vp = projection * v;
+}
+
+void BlendedSprite::SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept {
+       projection = p;
+       view = v;
+       vp = p * v;
+}
+
+void BlendedSprite::SetMVP(const glm::mat4 &m, const glm::mat4 &v, const glm::mat4 &p) noexcept {
+       SetVP(v, p);
+       SetM(m);
+}
+
+void BlendedSprite::SetTexture(Texture &tex) noexcept {
+       glActiveTexture(GL_TEXTURE0);
+       tex.Bind();
+       program.Uniform(sampler_handle, GLint(0));
+}
+
+void BlendedSprite::SetFG(const glm::vec4 &v) noexcept {
+       program.Uniform(fg_handle, v);
+}
+
+void BlendedSprite::SetBG(const glm::vec4 &v) noexcept {
+       program.Uniform(bg_handle, v);
+}
+
+
+SkyBoxShader::SkyBoxShader()
+: program()
+, vp(1.0f)
+, vp_handle(0)
+, sampler_handle(0) {
+       program.LoadShader(
+               GL_VERTEX_SHADER,
+               "#version 330 core\n"
+               "layout(location = 0) in vec3 vtx_position;\n"
+               "uniform mat4 VP;\n"
+               "out vec3 vtx_viewspace;\n"
+               "void main() {\n"
+                       "gl_Position = VP * vec4(vtx_position, 1);\n"
+                       "gl_Position.z = gl_Position.w;\n"
+                       "vtx_viewspace = vtx_position;\n"
+               "}\n"
+       );
+       program.LoadShader(
+               GL_FRAGMENT_SHADER,
+               "#version 330 core\n"
+               "in vec3 vtx_viewspace;\n"
+               "uniform samplerCube tex_sampler;\n"
+               "out vec3 color;\n"
+               "void main() {\n"
+                       "color = texture(tex_sampler, vtx_viewspace).rgb;\n"
+                       //"color = vec3(1,0,0);\n"
+               "}\n"
+       );
+       program.Link();
+       if (!program.Linked()) {
+               program.Log(std::cerr);
+               throw std::runtime_error("link program");
+       }
+
+       vp_handle = program.UniformLocation("VP");
+       sampler_handle = program.UniformLocation("tex_sampler");
+}
+
+
+void SkyBoxShader::Activate() noexcept {
+       program.Use();
+}
+
+void SkyBoxShader::SetTexture(CubeMap &tex) noexcept {
+       glActiveTexture(GL_TEXTURE0);
+       tex.Bind();
+       program.Uniform(sampler_handle, GLint(0));
+}
+
+void SkyBoxShader::SetProjection(const glm::mat4 &p) noexcept {
+       projection = p;
+       vp = p * view;
+       program.Uniform(vp_handle, vp);
+}
+
+void SkyBoxShader::SetView(const glm::mat4 &v) noexcept {
+       view = v;
+       view[0].w = 0.0f;
+       view[1].w = 0.0f;
+       view[2].w = 0.0f;
+       view[3] = glm::vec4(0.0f, 0.0f, 0.0f, 1.0f);
+       vp = projection * view;
+       program.Uniform(vp_handle, vp);
+}
+
+void SkyBoxShader::SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept {
+       projection = p;
+       SetView(v);
+}
+
+
+PlainColor::PlainColor()
+: program()
+, vp(1.0f)
+, mvp_handle(0) {
+       program.LoadShader(
+               GL_VERTEX_SHADER,
+               "#version 330 core\n"
+               "layout(location = 0) in vec3 vtx_position;\n"
+               "layout(location = 1) in vec4 vtx_color;\n"
+               "uniform mat4 MVP;\n"
+               "out vec4 frag_color;\n"
+               "void main() {\n"
+                       "gl_Position = MVP * vec4(vtx_position, 1);\n"
+                       "frag_color = vtx_color;\n"
+               "}\n"
+       );
+       program.LoadShader(
+               GL_FRAGMENT_SHADER,
+               "#version 330 core\n"
+               "in vec4 frag_color;\n"
+               "out vec4 color;\n"
+               "void main() {\n"
+                       "color = frag_color;\n"
+               "}\n"
+       );
+       program.Link();
+       if (!program.Linked()) {
+               program.Log(std::cerr);
+               throw std::runtime_error("link program");
+       }
+
+       mvp_handle = program.UniformLocation("MVP");
+}
+
+
+void PlainColor::Activate() noexcept {
+       program.Use();
+}
+
+void PlainColor::SetM(const glm::mat4 &m) noexcept {
+       program.Uniform(mvp_handle, vp * m);
+}
+
+void PlainColor::SetProjection(const glm::mat4 &p) noexcept {
+       projection = p;
+       vp = p * view;
+}
+
+void PlainColor::SetView(const glm::mat4 &v) noexcept {
+       view = v;
+       vp = projection * v;
+}
+
+void PlainColor::SetVP(const glm::mat4 &v, const glm::mat4 &p) noexcept {
+       projection = p;
+       view = v;
+       vp = p * v;
+}
+
+void PlainColor::SetMVP(const glm::mat4 &m, const glm::mat4 &v, const glm::mat4 &p) noexcept {
+       SetVP(v, p);
+       SetM(m);
+}
+
+}
+}
diff --git a/src/graphics/viewport.cpp b/src/graphics/viewport.cpp
new file mode 100644 (file)
index 0000000..78f8e11
--- /dev/null
@@ -0,0 +1,218 @@
+#include "Camera.hpp"
+#include "Canvas.hpp"
+#include "Viewport.hpp"
+
+#include "../app/error.hpp"
+#include "../geometry/const.hpp"
+
+#include <GL/glew.h>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtx/transform.hpp>
+#include <SDL.h>
+
+
+namespace gong {
+namespace graphics {
+
+Camera::Camera() noexcept
+: fov(geometry::PI_0p25)
+, aspect(1.0f)
+, near(0.1f)
+, far(256.0f)
+, projection(glm::perspective(fov, aspect, near, far))
+, view(1.0f) {
+
+}
+
+
+void Camera::FOV(float f) noexcept {
+       fov = f;
+       UpdateProjection();
+}
+
+void Camera::Aspect(float r) noexcept {
+       aspect = r;
+       UpdateProjection();
+}
+
+void Camera::Aspect(float w, float h) noexcept {
+       Aspect(w / h);
+}
+
+void Camera::Clip(float n, float f) noexcept {
+       near = n;
+       far = f;
+       UpdateProjection();
+}
+
+void Camera::View(const glm::mat4 &v) noexcept {
+       view = v;
+}
+
+void Camera::UpdateProjection() noexcept {
+       projection = glm::perspective(fov, aspect, near, far);
+}
+
+
+Canvas::Canvas() noexcept
+: offset(0.0f, 0.0f)
+, size(1.0f, 1.0f)
+, near(100.0f)
+, far(-100.0f)
+, projection(glm::ortho(offset.x, size.x, size.y, offset.y, near, far))
+, view(1.0f) {
+
+}
+
+
+void Canvas::Resize(float w, float h) noexcept {
+       size.x = w;
+       size.y = h;
+       UpdateProjection();
+}
+
+
+void Canvas::UpdateProjection() noexcept {
+       projection = glm::ortho(offset.x, size.x, size.y, offset.y, near, far);
+}
+
+
+Viewport::Viewport()
+: cam()
+, canv()
+, cursor(1.0f)
+, cam_offset(0.0f)
+, color_prog()
+, sky_prog()
+, sprite_prog()
+, active_prog(NONE) {
+       glClearColor(0.0, 0.0, 0.0, 1.0);
+}
+
+void Viewport::VSync(bool b) {
+       if (SDL_GL_SetSwapInterval(b) != 0) {
+               if (b) {
+                       throw app::SDLError("SDL_GL_SetSwapInterval(1)");
+               } else {
+                       // allow failure, because this usually means there's no vsync
+                       // support at all, i.e. "it's off"
+               }
+       }
+}
+
+void Viewport::EnableDepthTest() noexcept {
+       glEnable(GL_DEPTH_TEST);
+       glDepthFunc(GL_LESS);
+}
+
+void Viewport::EqualDepthTest() noexcept {
+       glEnable(GL_DEPTH_TEST);
+       glDepthFunc(GL_LEQUAL);
+}
+
+void Viewport::DisableDepthTest() noexcept {
+       glDisable(GL_DEPTH_TEST);
+}
+
+void Viewport::EnableBackfaceCulling() noexcept {
+       glEnable(GL_CULL_FACE);
+}
+
+void Viewport::DisableBackfaceCulling() noexcept {
+       glDisable(GL_CULL_FACE);
+}
+
+void Viewport::EnableAlphaBlending() noexcept {
+       glEnable(GL_BLEND);
+       glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+}
+
+void Viewport::EnableInvertBlending() noexcept {
+       glEnable(GL_BLEND);
+       glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
+}
+
+void Viewport::DisableBlending() noexcept {
+       glDisable(GL_BLEND);
+}
+
+void Viewport::Resize(int w, int h) noexcept {
+       glViewport(0, 0, w, h);
+       float fw = w;
+       float fh = h;
+       cam.Aspect(fw, fh);
+       canv.Resize(fw, fh);
+
+       SkyBoxProgram().SetProjection(Perspective());
+       SpriteProgram().SetProjection(Ortho());
+}
+
+void Viewport::Clear() noexcept {
+       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+}
+
+void Viewport::ClearDepth() noexcept {
+       glClear(GL_DEPTH_BUFFER_BIT);
+}
+
+
+glm::vec2 Viewport::GetPosition(const glm::vec2 &off, Gravity grav) const noexcept {
+       return align(grav, canv.Size(), off + canv.Offset());
+}
+
+void Viewport::SetCursor(const glm::vec3 &pos) noexcept {
+       cursor[3].x = pos.x;
+       cursor[3].y = pos.y;
+       cursor[3].z = pos.z;
+}
+
+void Viewport::SetCursor(const glm::vec3 &pos, Gravity grav) noexcept {
+       glm::vec2 p(GetPosition(glm::vec2(pos), grav));
+       cursor[3].x = p.x;
+       cursor[3].y = p.y;
+       cursor[3].z = pos.z;
+}
+
+void Viewport::MoveCursor(const glm::vec3 &d) noexcept {
+       cursor[3].x += d.x;
+       cursor[3].y += d.y;
+       cursor[3].z += d.z;
+}
+
+
+PlainColor &Viewport::HUDColorProgram() noexcept {
+       if (active_prog != COLOR_HUD) {
+               color_prog.Activate();
+               color_prog.SetVP(canv.View(), canv.Projection());
+               active_prog = COLOR_HUD;
+       }
+       return color_prog;
+}
+
+SkyBoxShader &Viewport::SkyBoxProgram() noexcept {
+       if (active_prog != SKY_BOX) {
+               sky_prog.Activate();
+               DisableBlending();
+               DisableBackfaceCulling();
+               EqualDepthTest();
+               active_prog = SKY_BOX;
+       }
+       return sky_prog;
+}
+
+BlendedSprite &Viewport::SpriteProgram() noexcept {
+       if (active_prog != SPRITE) {
+               sprite_prog.Activate();
+               EnableAlphaBlending();
+               active_prog = SPRITE;
+       }
+       return sprite_prog;
+}
+
+
+void Viewport::WorldPosition(const glm::mat4 &t) noexcept {
+       cam.View(glm::translate(glm::inverse(t), glm::vec3(t * glm::vec4(cam_offset, 0.0f))));
+}
+
+}
+}
diff --git a/src/io/LineBuffer.hpp b/src/io/LineBuffer.hpp
new file mode 100644 (file)
index 0000000..383dbce
--- /dev/null
@@ -0,0 +1,74 @@
+#ifndef GONG_IO_LINEBUFFER_HPP_
+#define GONG_IO_LINEBUFFER_HPP_
+
+#include <algorithm>
+#include <cassert>
+#include <string>
+
+
+namespace gong {
+namespace io {
+
+template<std::size_t size>
+class LineBuffer {
+
+public:
+       explicit LineBuffer(char term = '\n') noexcept
+       : buffer{0}
+       , term(term)
+       , head(buffer) { }
+
+       char *begin() noexcept {
+               return buffer;
+       }
+       const char *begin() const noexcept {
+               return buffer;
+       }
+       char *end() noexcept {
+               return head;
+       }
+       const char *end() const noexcept {
+               return head;
+       }
+
+       /// extract one line from the buffer, terminator not included
+       /// @return false if the buffer does not contain a complete line
+       bool Extract(std::string &line) {
+               char *line_end = std::find(begin(), end(), term);
+               if (line_end == end()) {
+                       return false;
+               }
+               line.assign(begin(), line_end);
+               ++line_end;
+               std::move(line_end, end(), begin());
+               head -= std::distance(begin(), line_end);
+               return true;
+       }
+
+       /// get a pointer to append data to the buffer
+       /// it is safe to write at most Remain() bytes
+       char *WriteHead() noexcept {
+               return head;
+       }
+
+       // call when data has been written to WriteHead()
+       void Update(std::size_t written) {
+               assert(written <= Remain());
+               head += written;
+       }
+
+       std::size_t Remain() const noexcept {
+               return std::distance(end(), buffer + size);
+       }
+
+private:
+       char buffer[size];
+       char term;
+       char *head;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/io/Token.hpp b/src/io/Token.hpp
new file mode 100644 (file)
index 0000000..285ecc4
--- /dev/null
@@ -0,0 +1,40 @@
+#ifndef GONG_IO_TOKEN_HPP_
+#define GONG_IO_TOKEN_HPP_
+
+#include <iosfwd>
+#include <string>
+
+
+namespace gong {
+namespace io {
+
+struct Token {
+       enum Type {
+               UNKNOWN = 0,
+               ANGLE_BRACKET_OPEN = '{',
+               ANGLE_BRACKET_CLOSE = '}',
+               CHEVRON_OPEN = '<',
+               CHEVRON_CLOSE = '>',
+               BRACKET_OPEN = '[',
+               BRACKET_CLOSE = ']',
+               PARENTHESIS_OPEN = '(',
+               PARENTHESIS_CLOSE = ')',
+               COLON = ':',
+               SEMICOLON = ';',
+               COMMA = ',',
+               EQUALS = '=',
+               NUMBER = '0',
+               STRING = '"',
+               IDENTIFIER = 'a',
+               COMMENT = '#',
+       } type = UNKNOWN;
+       std::string value;
+};
+
+std::ostream &operator <<(std::ostream &, Token::Type);
+std::ostream &operator <<(std::ostream &, const Token &);
+
+}
+}
+
+#endif
diff --git a/src/io/TokenStreamReader.hpp b/src/io/TokenStreamReader.hpp
new file mode 100644 (file)
index 0000000..ada2fe2
--- /dev/null
@@ -0,0 +1,77 @@
+#ifndef GONG_IO_TOKENSTREAMREADER_HPP_
+#define GONG_IO_TOKENSTREAMREADER_HPP_
+
+#include "Token.hpp"
+#include "Tokenizer.hpp"
+#include "../graphics/glm.hpp"
+
+#include <iosfwd>
+#include <string>
+
+
+namespace gong {
+namespace io {
+
+class TokenStreamReader {
+
+public:
+       explicit TokenStreamReader(std::istream &);
+
+       bool HasMore();
+       const Token &Next();
+       const Token &Peek();
+
+       void Skip(Token::Type);
+
+       void ReadBoolean(bool &);
+       void ReadIdentifier(std::string &);
+       void ReadNumber(float &);
+       void ReadNumber(int &);
+       void ReadNumber(unsigned long &);
+       void ReadString(std::string &);
+       // like ReadString, but does not require the value to be
+       // written as a string literal in source
+       void ReadRelaxedString(std::string &);
+
+       void ReadVec(glm::vec2 &);
+       void ReadVec(glm::vec3 &);
+       void ReadVec(glm::vec4 &);
+
+       void ReadVec(glm::ivec2 &);
+       void ReadVec(glm::ivec3 &);
+       void ReadVec(glm::ivec4 &);
+
+       void ReadQuat(glm::quat &);
+
+       // the Get* functions advance to the next token
+       // the As* functions try to cast the current token
+       // if the value could not be converted, a std::runtime_error is thrown
+       // conversion to string is always possible
+
+       bool GetBool();
+       bool AsBool() const;
+       float GetFloat();
+       float AsFloat() const;
+       int GetInt();
+       int AsInt() const;
+       unsigned long GetULong();
+       unsigned long AsULong() const;
+       const std::string &GetString();
+       const std::string &AsString() const;
+
+private:
+       void SkipComments();
+
+       void Assert(Token::Type) const;
+       Token::Type GetType() const noexcept;
+       const std::string &GetValue() const noexcept;
+
+       Tokenizer in;
+       bool cached;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/io/Tokenizer.hpp b/src/io/Tokenizer.hpp
new file mode 100644 (file)
index 0000000..f432c95
--- /dev/null
@@ -0,0 +1,39 @@
+#ifndef GONG_IO_TOKENIZER_HPP_
+#define GONG_IO_TOKENIZER_HPP_
+
+#include "Token.hpp"
+
+#include <iosfwd>
+
+
+namespace gong {
+namespace io {
+
+class Tokenizer {
+
+public:
+
+public:
+       explicit Tokenizer(std::istream &in);
+
+       bool HasMore();
+       const Token &Next();
+       const Token &Current() const noexcept { return current; }
+
+private:
+       void ReadToken();
+
+       void ReadNumber();
+       void ReadString();
+       void ReadComment();
+       void ReadIdentifier();
+
+       std::istream &in;
+       Token current;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/io/event.cpp b/src/io/event.cpp
new file mode 100644 (file)
index 0000000..7f8ef07
--- /dev/null
@@ -0,0 +1,458 @@
+#include "event.hpp"
+
+#include <cctype>
+#include <ostream>
+
+using std::ostream;
+
+
+namespace gong {
+namespace io {
+
+ostream &operator <<(ostream &out, const SDL_Event &evt) {
+       switch (evt.type) {
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+               case SDL_AUDIODEVICEADDED:
+                       out << "audio device added: " << evt.adevice;
+                       break;
+               case SDL_AUDIODEVICEREMOVED:
+                       out << "audio device removed: " << evt.adevice;
+                       break;
+#endif
+               case SDL_CONTROLLERAXISMOTION:
+                       out << "controller axis motion: " << evt.caxis;
+                       break;
+               case SDL_CONTROLLERBUTTONDOWN:
+                       out << "controller button down: " << evt.cbutton;
+                       break;
+               case SDL_CONTROLLERBUTTONUP:
+                       out << "controller button up: " << evt.cbutton;
+                       break;
+               case SDL_CONTROLLERDEVICEADDED:
+                       out << "controller device added: " << evt.cdevice;
+                       break;
+               case SDL_CONTROLLERDEVICEREMOVED:
+                       out << "controller device removed: " << evt.cdevice;
+                       break;
+               case SDL_CONTROLLERDEVICEREMAPPED:
+                       out << "controller device remapped: " << evt.cdevice;
+                       break;
+               case SDL_DOLLARGESTURE:
+                       out << "dollar gesture: " << evt.dgesture;
+                       break;
+               case SDL_DOLLARRECORD:
+                       out << "dollar record: " << evt.dgesture;
+                       break;
+               case SDL_DROPFILE:
+                       out << "drop file: " << evt.drop;
+                       break;
+               case SDL_FINGERMOTION:
+                       out << "finger motion: " << evt.tfinger;
+                       break;
+               case SDL_FINGERDOWN:
+                       out << "finger down: " << evt.tfinger;
+                       break;
+               case SDL_FINGERUP:
+                       out << "finger up: " << evt.tfinger;
+                       break;
+               case SDL_KEYDOWN:
+                       out << "key down: " << evt.key;
+                       break;
+               case SDL_KEYUP:
+                       out << "key up: " << evt.key;
+                       break;
+               case SDL_JOYAXISMOTION:
+                       out << "joystick axis motion: " << evt.jaxis;
+                       break;
+               case SDL_JOYBALLMOTION:
+                       out << "joystick ball motion: " << evt.jball;
+                       break;
+               case SDL_JOYHATMOTION:
+                       out << "joystick hat motion: " << evt.jhat;
+                       break;
+               case SDL_JOYBUTTONDOWN:
+                       out << "joystick button down: " << evt.jbutton;
+                       break;
+               case SDL_JOYBUTTONUP:
+                       out << "joystick button up: " << evt.jbutton;
+                       break;
+               case SDL_JOYDEVICEADDED:
+                       out << "joystick device added: " << evt.jdevice;
+                       break;
+               case SDL_JOYDEVICEREMOVED:
+                       out << "joystick device removed: " << evt.jdevice;
+                       break;
+               case SDL_MOUSEMOTION:
+                       out << "mouse motion: " << evt.motion;
+                       break;
+               case SDL_MOUSEBUTTONDOWN:
+                       out << "mouse button down: " << evt.button;
+                       break;
+               case SDL_MOUSEBUTTONUP:
+                       out << "mouse button up: " << evt.button;
+                       break;
+               case SDL_MOUSEWHEEL:
+                       out << "mouse wheel: " << evt.wheel;
+                       break;
+               case SDL_MULTIGESTURE:
+                       out << "multi gesture: " << evt.mgesture;
+                       break;
+               case SDL_QUIT:
+                       out << "quit: " << evt.quit;
+                       break;
+               case SDL_SYSWMEVENT:
+                       out << "sys wm: " << evt.syswm;
+                       break;
+               case SDL_TEXTEDITING:
+                       out << "text editing: " << evt.edit;
+                       break;
+               case SDL_TEXTINPUT:
+                       out << "text input: " << evt.text;
+                       break;
+               case SDL_USEREVENT:
+                       out << "user: " << evt.user;
+                       break;
+               case SDL_WINDOWEVENT:
+                       out << "window: " << evt.window;
+                       break;
+               default:
+                       out << "unknown";
+                       break;
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_WindowEvent &evt) {
+       switch (evt.event) {
+               case SDL_WINDOWEVENT_SHOWN:
+                       out << "shown, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_HIDDEN:
+                       out << "hidden, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_EXPOSED:
+                       out << "exposed, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_MOVED:
+                       out << "moved, window ID: " << evt.windowID
+                               << ", position: " << evt.data1 << ' ' << evt.data2;
+                       break;
+               case SDL_WINDOWEVENT_RESIZED:
+                       out << "resized, window ID: " << evt.windowID
+                               << ", size: " << evt.data1 << 'x' << evt.data2;
+                       break;
+               case SDL_WINDOWEVENT_SIZE_CHANGED:
+                       out << "size changed, window ID: " << evt.windowID
+                               << ", size: " << evt.data1 << 'x' << evt.data2;
+                       break;
+               case SDL_WINDOWEVENT_MINIMIZED:
+                       out << "minimized, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_MAXIMIZED:
+                       out << "maximized, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_RESTORED:
+                       out << "restored, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_ENTER:
+                       out << "mouse entered, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_LEAVE:
+                       out << "mouse left, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_FOCUS_GAINED:
+                       out << "focus gained, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_FOCUS_LOST:
+                       out << "focus lost, window ID: " << evt.windowID;
+                       break;
+               case SDL_WINDOWEVENT_CLOSE:
+                       out << "closed, window ID: " << evt.windowID;
+                       break;
+               default:
+                       out << "unknown";
+                       break;
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_KeyboardEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", state: " << (evt.state == SDL_PRESSED ? "pressed" : "released")
+               << ", repeat: " << (evt.repeat ? "yes" : "no")
+               << ", keysym: " << evt.keysym;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_Keysym &keysym) {
+       out << "scancode: " << int(keysym.scancode)
+               << ", sym: " << int(keysym.sym)
+               << " (\"" << SDL_GetKeyName(keysym.sym) << "\")";
+       if (keysym.mod) {
+               out << ", mod:";
+               if (keysym.mod & KMOD_LSHIFT) {
+                       out << " LSHIFT";
+               }
+               if (keysym.mod & KMOD_RSHIFT) {
+                       out << " RSHIFT";
+               }
+               if (keysym.mod & KMOD_LCTRL) {
+                       out << " LCTRL";
+               }
+               if (keysym.mod & KMOD_RCTRL) {
+                       out << " RCTRL";
+               }
+               if (keysym.mod & KMOD_LALT) {
+                       out << " LALT";
+               }
+               if (keysym.mod & KMOD_RALT) {
+                       out << " RALT";
+               }
+               if (keysym.mod & KMOD_LGUI) {
+                       out << " LSUPER";
+               }
+               if (keysym.mod & KMOD_RGUI) {
+                       out << " RSUPER";
+               }
+               if (keysym.mod & KMOD_NUM) {
+                       out << " NUM";
+               }
+               if (keysym.mod & KMOD_CAPS) {
+                       out << " CAPS";
+               }
+               if (keysym.mod & KMOD_MODE) {
+                       out << " ALTGR";
+               }
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_TextEditingEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", text: \"" << evt.text
+               << "\", start: " << evt.start
+               << ", length: " << evt.length;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_TextInputEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", text: \"" << evt.text << '"';
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_MouseMotionEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", mouse ID: " << evt.which
+               << ", position: " << evt.x << ' ' << evt.y
+               << ", delta: " << evt.xrel << ' ' << evt.yrel;
+       if (evt.state) {
+               out << ", buttons:";
+               if (evt.state & SDL_BUTTON_LMASK) {
+                       out << " left";
+               }
+               if (evt.state & SDL_BUTTON_MMASK) {
+                       out << " middle";
+               }
+               if (evt.state & SDL_BUTTON_RMASK) {
+                       out << " right";
+               }
+               if (evt.state & SDL_BUTTON_X1MASK) {
+                       out << " X1";
+               }
+               if (evt.state & SDL_BUTTON_X2MASK) {
+                       out << " X2";
+               }
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_MouseButtonEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", mouse ID: " << evt.which
+               << ", button: ";
+       switch (evt.button) {
+               case SDL_BUTTON_LEFT:
+                       out << "left";
+                       break;
+               case SDL_BUTTON_MIDDLE:
+                       out << "middle";
+                       break;
+               case SDL_BUTTON_RIGHT:
+                       out << "right";
+                       break;
+               case SDL_BUTTON_X1:
+                       out << "X1";
+                       break;
+               case SDL_BUTTON_X2:
+                       out << "X2";
+                       break;
+               default:
+                       out << int(evt.button);
+                       break;
+       }
+       out << ", state: " << (evt.state == SDL_PRESSED ? "pressed" : "released")
+               << ", clicks: " << int(evt.clicks)
+               << ", position: " << evt.x << ' ' << evt.y;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_MouseWheelEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", mouse ID: " << evt.which
+               << ", delta: " << evt.x << ' ' << evt.y
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+               << ", direction: " << (evt.direction == SDL_MOUSEWHEEL_NORMAL ? "normal" : "flipped")
+#endif
+               ;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_JoyAxisEvent &evt) {
+       out << "joystick ID: " << evt.which
+               << ", axis ID: " << int(evt.axis)
+               << ", value: " << (float(evt.value) / 32768.0f);
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_JoyBallEvent &evt) {
+       out << "joystick ID: " << evt.which
+               << ", ball ID: " << int(evt.ball)
+               << ", delta: " << evt.xrel << ' ' << evt.yrel;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_JoyHatEvent &evt) {
+       out << "joystick ID: " << evt.which
+               << ", hat ID: " << int(evt.hat)
+               << ", value: ";
+       switch (evt.value) {
+               case SDL_HAT_LEFTUP:
+                       out << "left up";
+                       break;
+               case SDL_HAT_UP:
+                       out << "up";
+                       break;
+               case SDL_HAT_RIGHTUP:
+                       out << "right up";
+                       break;
+               case SDL_HAT_LEFT:
+                       out << "left";
+                       break;
+               case SDL_HAT_CENTERED:
+                       out << "center";
+                       break;
+               case SDL_HAT_RIGHT:
+                       out << "right";
+                       break;
+               case SDL_HAT_LEFTDOWN:
+                       out << "left down";
+                       break;
+               case SDL_HAT_DOWN:
+                       out << "down";
+                       break;
+               case SDL_HAT_RIGHTDOWN:
+                       out << "right down";
+                       break;
+               default:
+                       out << "unknown";
+                       break;
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_JoyButtonEvent &evt) {
+       out << "joystick ID: " << evt.which
+               << ", button ID: " << int(evt.button)
+               << ", state: " << (evt.state == SDL_PRESSED ? "pressed" : "released");
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_JoyDeviceEvent &evt) {
+       out << "joystick ID: " << evt.which;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_ControllerAxisEvent &evt) {
+       out << "controller ID: " << evt.which
+               << ", axis ID: " << int(evt.axis)
+               << ", value: " << (float(evt.value) / 32768.0f);
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_ControllerButtonEvent &evt) {
+       out << "controller ID: " << evt.which
+               << ", button ID: " << int(evt.button)
+               << ", state: " << (evt.state == SDL_PRESSED ? "pressed" : "released");
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_ControllerDeviceEvent &evt) {
+       out << "controller ID: " << evt.which;
+       return out;
+}
+
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+ostream &operator <<(ostream &out, const SDL_AudioDeviceEvent &evt) {
+       out << "device ID: " << evt.which
+               << ", capture: " << (evt.iscapture ? "yes" : "no");
+       return out;
+}
+#endif
+
+ostream &operator <<(ostream &out, const SDL_QuitEvent &) {
+       out << "quit";
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_UserEvent &evt) {
+       out << "window ID: " << evt.windowID
+               << ", code: " << evt.code
+               << ", data 1: " << evt.data1
+               << ", data 2: " << evt.data2;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_SysWMEvent &evt) {
+       if (evt.msg) {
+               out << "with message";
+       } else {
+               out << "without message";
+       }
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_TouchFingerEvent &evt) {
+       out << "device ID: " << evt.touchId
+               << ", finger ID: " << evt.fingerId
+               << ", position: " << evt.x << ' ' << evt.y
+               << ", delta: " << evt.dx << ' ' << evt.dy
+               << ", pressure: " << evt.pressure;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_MultiGestureEvent &evt) {
+       out << "device ID: " << evt.touchId
+               << ", theta: " << evt.dTheta
+               << ", distance: " << evt.dDist
+               << ", position: " << evt.x << ' ' << evt.y
+               << ", fingers: " << evt.numFingers;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_DollarGestureEvent &evt) {
+       out << "device ID: " << evt.touchId
+               << ", gesture ID: " << evt.gestureId
+               << ", fingers: " << evt.numFingers
+               << ", error: " << evt.error
+               << ", position: " << evt.x << ' ' << evt.y;
+       return out;
+}
+
+ostream &operator <<(ostream &out, const SDL_DropEvent &evt) {
+       out << "file: " << evt.file;
+       return out;
+}
+
+}
+}
diff --git a/src/io/event.hpp b/src/io/event.hpp
new file mode 100644 (file)
index 0000000..7012199
--- /dev/null
@@ -0,0 +1,45 @@
+#ifndef GONG_IO_EVENT_HPP_
+#define GONG_IO_EVENT_HPP_
+
+#include <iosfwd>
+#include <SDL.h>
+#include <SDL_version.h>
+
+
+namespace gong {
+namespace io {
+
+std::ostream &operator <<(std::ostream &, const SDL_Event &);
+
+std::ostream &operator <<(std::ostream &, const SDL_WindowEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_KeyboardEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_TextEditingEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_TextInputEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_MouseMotionEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_MouseButtonEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_MouseWheelEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_JoyAxisEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_JoyBallEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_JoyHatEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_JoyButtonEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_JoyDeviceEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_ControllerAxisEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_ControllerButtonEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_ControllerDeviceEvent &);
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+std::ostream &operator <<(std::ostream &, const SDL_AudioDeviceEvent &);
+#endif
+std::ostream &operator <<(std::ostream &, const SDL_QuitEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_UserEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_SysWMEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_TouchFingerEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_MultiGestureEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_DollarGestureEvent &);
+std::ostream &operator <<(std::ostream &, const SDL_DropEvent &);
+
+std::ostream &operator <<(std::ostream &, const SDL_Keysym &);
+
+}
+}
+
+#endif
diff --git a/src/io/filesystem.cpp b/src/io/filesystem.cpp
new file mode 100644 (file)
index 0000000..7f65198
--- /dev/null
@@ -0,0 +1,253 @@
+#include "filesystem.hpp"
+
+#include "../app/error.hpp"
+
+#include <cerrno>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+#ifdef _WIN32
+#  include <conio.h>
+#  include <direct.h>
+#  include <windows.h>
+#else
+#  include <dirent.h>
+#  include <sys/types.h>
+#endif
+#include <sys/stat.h>
+
+using namespace std;
+
+
+namespace gong {
+namespace io {
+
+namespace {
+#ifdef _WIN32
+       using Stat = struct _stat;
+       int do_stat(const char *path, Stat &info) {
+               return _stat(path, &info);
+       }
+       bool is_dir(const Stat &info) {
+               return (info.st_mode & _S_IFDIR) != 0;
+       }
+       bool is_file(const Stat &info) {
+               return (info.st_mode & _S_IFEG) != 0;
+       }
+#else
+       using Stat = struct stat;
+       int do_stat(const char *path, Stat &info) {
+               return stat(path, &info);
+       }
+       bool is_dir(const Stat &info) {
+               return S_ISDIR(info.st_mode);
+       }
+       bool is_file(const Stat &info) {
+               return S_ISREG(info.st_mode);
+       }
+#endif
+       time_t get_mtime(const Stat &info) {
+#ifdef __APPLE__
+               return info.st_mtimespec.tv_sec;
+#else
+       return info.st_mtime;
+#endif
+       }
+}
+
+bool is_dir(const char *path) {
+       Stat info;
+       if (do_stat(path, info) != 0) {
+               return false;
+       }
+       return is_dir(info);
+}
+
+bool is_file(const char *path) {
+       Stat info;
+       if (do_stat(path, info) != 0) {
+               return false;
+       }
+       return is_file(info);
+}
+
+time_t file_mtime(const char *path) {
+       Stat info;
+       if (do_stat(path, info) != 0) {
+               return 0;
+       }
+       return get_mtime(info);
+}
+
+
+bool make_dir(const char *path) {
+#ifdef _WIN32
+       int ret = _mkdir(path);
+#else
+       int ret = mkdir(path, 0777);
+#endif
+       return ret == 0;
+}
+
+
+bool make_dirs(const string &path) {
+       if (make_dir(path)) {
+               return true;
+       }
+
+       switch (errno) {
+
+               case ENOENT:
+                       // missing component
+                       {
+#ifdef _WIN32
+                               auto pos = path.find_last_of("\\/");
+#else
+                               auto pos = path.find_last_of('/');
+#endif
+                               if (pos == string::npos) {
+                                       return false;
+                               }
+                               if (pos == path.length() - 1) {
+                                       // trailing separator, would make final make_dir fail
+#ifdef _WIN32
+                                        pos = path.find_last_of("\\/", pos - 1);
+#else
+                                        pos = path.find_last_of('/', pos - 1);
+#endif
+                                       if (pos == string::npos) {
+                                               return false;
+                                       }
+                               }
+                               if (!make_dirs(path.substr(0, pos))) {
+                                       return false;
+                               }
+                       }
+                       // try again
+                       return make_dir(path);
+
+               case EEXIST:
+                       // something's there, check if it's a dir and we're good
+                       return is_dir(path);
+
+               default:
+                       // whatever else went wrong, it can't be good
+                       return false;
+
+       }
+}
+
+
+bool remove_file(const string &path) {
+       return remove(path.c_str()) == 0;
+}
+
+
+bool remove_dir(const string &path) {
+#ifdef _WIN32
+
+       // shamelessly stolen from http://www.codeguru.com/forum/showthread.php?t=239271
+       const string pattern = path + "\\*.*";
+       WIN32_FIND_DATA info;
+       HANDLE file = FindFirstFile(pattern.c_str(), &info);
+       if (file == INVALID_HANDLE_VALUE) {
+               // already non-existing
+               return true;
+       }
+
+       do {
+               if (
+                       strncmp(info.cFileName, ".", 2) == 0 ||
+                       strncmp(info.cFileName, "..", 3) == 0
+               ) {
+                       continue;
+               }
+               const string sub_path = path + '\\' + info.cFileName;
+               if ((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
+                       if (!remove_dir(sub_path)) {
+                               return false;
+                       }
+               } else {
+                       if (!SetFileAttributes(sub_path.c_str(), FILE_ATTRIBUTE_NORMAL)) {
+                               return false;
+                       }
+                       if (!remove_file(sub_path)) {
+                               return false;
+                       }
+               }
+       } while (FindNextFile(file, &info));
+       FindClose(file);
+
+       DWORD error = GetLastError();
+       if (error != ERROR_NO_MORE_FILES) {
+               return false;
+       }
+       // is this (NORMAL vs DIRECTORY) really correct?
+       if (!SetFileAttributes(path.c_str(), FILE_ATTRIBUTE_NORMAL)) {
+               return false;
+       }
+       return RemoveDirectory(path.c_str());
+
+#else
+
+       DIR *dir = opendir(path.c_str());
+       for (dirent *entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
+               if (
+                       strncmp(entry->d_name, ".", 2) == 0 ||
+                       strncmp(entry->d_name, "..", 3) == 0
+               ) {
+                       continue;
+               }
+               const string sub_path = path + '/' + entry->d_name;
+               if (is_dir(sub_path)) {
+                       if (!remove_dir(sub_path)) {
+                               return false;
+                       }
+               } else {
+                       if (!remove_file(sub_path)) {
+                               return false;
+                       }
+               }
+       }
+       return remove(path.c_str()) == 0;
+
+#endif
+}
+
+
+TempDir::TempDir() {
+#if _DEFAULT_SOURCE || _BSD_SOURCE || _POSIX_C_SOURCE >= 200809L
+       char tmpl[] = "gong.XXXXXX";
+       const char *name = mkdtemp(tmpl);
+       if (!name) {
+               throw app::SysError("unable to create temporary directory");
+       }
+       path = name;
+#else
+       char name[L_tmpnam];
+       tmpnam(name);
+       constexpr int max_tries = 10;
+       int tries = 0;
+       while (!make_dirs(name) && tries < max_tries) {
+               tmpnam(name);
+               ++tries;
+       }
+       if (tries == max_tries) {
+               throw runtime_error("unable to create temporary directory");
+       }
+#endif
+       path = name;
+}
+
+TempDir::~TempDir() {
+       try {
+               remove_dir(path);
+       } catch (...) {
+               cerr << "warning: could not remove temp dir " << path << endl;
+       }
+}
+
+}
+}
diff --git a/src/io/filesystem.hpp b/src/io/filesystem.hpp
new file mode 100644 (file)
index 0000000..6051f17
--- /dev/null
@@ -0,0 +1,70 @@
+#ifndef GONG_IO_FILESYSTEM_HPP_
+#define GONG_IO_FILESYSTEM_HPP_
+
+#include <ctime>
+#include <string>
+
+
+namespace gong {
+namespace io {
+
+/// check if give path points to an existing directory
+bool is_dir(const char *);
+inline bool is_dir(const std::string &s) {
+       return is_dir(s.c_str());
+}
+/// check if give path points to an existing file
+bool is_file(const char *);
+inline bool is_file(const std::string &s) {
+       return is_file(s.c_str());
+}
+/// get timestamp of last modification
+std::time_t file_mtime(const char *);
+inline std::time_t file_mtime(const std::string &s) {
+       return file_mtime(s.c_str());
+}
+
+/// create given directory
+/// @return true if the directory was created
+///         the directory might already exist, see errno
+bool make_dir(const char *);
+inline bool make_dir(const std::string &s) {
+       return make_dir(s.c_str());
+}
+/// create given directory and all parents
+/// @return true if the directory was created or already exists
+bool make_dirs(const std::string &);
+
+/// remove given file
+/// @return true on success
+bool remove_file(const std::string &);
+/// recursively remove given directory
+/// may leave the directory partially removed on failure
+/// @return true if the directory was completely removed
+bool remove_dir(const std::string &);
+
+
+/// Create a temporary directory with lifetime tie to the instance's.
+/// Note that the directory may survive its object if removal fails
+/// for any reason, e.g. another process changing permissions.
+class TempDir {
+
+public:
+       TempDir();
+       ~TempDir();
+
+       TempDir(const TempDir &) = delete;
+       TempDir &operator =(const TempDir &) = delete;
+
+public:
+       const std::string &Path() const noexcept { return path; }
+
+private:
+       std::string path;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/io/token.cpp b/src/io/token.cpp
new file mode 100644 (file)
index 0000000..e7690ac
--- /dev/null
@@ -0,0 +1,485 @@
+#include "Token.hpp"
+#include "Tokenizer.hpp"
+#include "TokenStreamReader.hpp"
+
+#include <cctype>
+#include <istream>
+#include <ostream>
+#include <sstream>
+#include <stdexcept>
+#include <glm/gtc/quaternion.hpp>
+
+using namespace std;
+
+
+namespace gong {
+namespace io {
+
+ostream &operator <<(ostream &out, Token::Type t) {
+       switch (t) {
+               case Token::ANGLE_BRACKET_OPEN:
+                       return out << "ANGLE_BRACKET_OPEN";
+               case Token::ANGLE_BRACKET_CLOSE:
+                       return out << "ANGLE_BRACKET_CLOSE";
+               case Token::CHEVRON_OPEN:
+                       return out << "CHEVRON_OPEN";
+               case Token::CHEVRON_CLOSE:
+                       return out << "CHEVRON_CLOSE";
+               case Token::BRACKET_OPEN:
+                       return out << "BRACKET_OPEN";
+               case Token::BRACKET_CLOSE:
+                       return out << "BRACKET_CLOSE";
+               case Token::PARENTHESIS_OPEN:
+                       return out << "PARENTHESIS_OPEN";
+               case Token::PARENTHESIS_CLOSE:
+                       return out << "PARENTHESIS_CLOSE";
+               case Token::COLON:
+                       return out << "COLON";
+               case Token::SEMICOLON:
+                       return out << "SEMICOLON";
+               case Token::COMMA:
+                       return out << "COMMA";
+               case Token::EQUALS:
+                       return out << "EQUALS";
+               case Token::NUMBER:
+                       return out << "NUMBER";
+               case Token::STRING:
+                       return out << "STRING";
+               case Token::IDENTIFIER:
+                       return out << "IDENTIFIER";
+               case Token::COMMENT:
+                       return out << "COMMENT";
+               default:
+                       return out << "UNKNOWN";
+       }
+}
+
+ostream &operator <<(ostream &out, const Token &t) {
+       out << t.type;
+       switch (t.type) {
+               case Token::UNKNOWN:
+               case Token::NUMBER:
+               case Token::STRING:
+               case Token::IDENTIFIER:
+               case Token::COMMENT:
+                       return out << '(' << t.value << ')';
+               default:
+                       return out;
+       }
+}
+
+Tokenizer::Tokenizer(istream &in)
+: in(in)
+, current() {
+
+}
+
+
+bool Tokenizer::HasMore() {
+       return bool(istream::sentry(in));
+}
+
+const Token &Tokenizer::Next() {
+       ReadToken();
+       return Current();
+}
+
+void Tokenizer::ReadToken() {
+       current.type = Token::UNKNOWN;
+       current.value.clear();
+
+       istream::sentry s(in);
+       if (!s) {
+               throw runtime_error("read past the end of stream");
+               return;
+       }
+
+       istream::char_type c;
+       in.get(c);
+       switch (c) {
+               case '{': case '}':
+               case '<': case '>':
+               case '[': case ']':
+               case '(': case ')':
+               case ';': case ':':
+               case ',': case '=':
+                       current.type = Token::Type(c);
+                       break;
+               case '+': case '-': case '.':
+               case '0': case '1': case '2': case '3': case '4':
+               case '5': case '6': case '7': case '8': case '9':
+                       in.putback(c);
+                       ReadNumber();
+                       break;
+               case '"':
+                       ReadString();
+                       break;
+               case '#':
+               case '/':
+                       in.putback(c);
+                       ReadComment();
+                       break;
+               default:
+                       in.putback(c);
+                       ReadIdentifier();
+                       break;
+       }
+}
+
+namespace {
+
+bool is_num_char(istream::char_type c) {
+       return isxdigit(c)
+               || c == '.'
+               || c == '-'
+               || c == '+'
+       ;
+}
+
+}
+
+void Tokenizer::ReadNumber() {
+       current.type = Token::NUMBER;
+       istream::char_type c;
+       while (in.get(c)) {
+               if (is_num_char(c)) {
+                       current.value += c;
+               } else {
+                       in.putback(c);
+                       break;
+               }
+       }
+}
+
+void Tokenizer::ReadString() {
+       current.type = Token::STRING;
+       bool escape = false;
+
+       istream::char_type c;
+       while (in.get(c)) {
+               if (escape) {
+                       escape = false;
+                       switch (c) {
+                               case 'n':
+                                       current.value += '\n';
+                                       break;
+                               case 'r':
+                                       current.value += '\r';
+                                       break;
+                               case 't':
+                                       current.value += '\t';
+                                       break;
+                               default:
+                                       current.value += c;
+                                       break;
+                       }
+               } else if (c == '"') {
+                       break;
+               } else if (c == '\\') {
+                       escape = true;
+               } else {
+                       current.value += c;
+               }
+       }
+}
+
+void Tokenizer::ReadComment() {
+       current.type = Token::COMMENT;
+       istream::char_type c;
+       in.get(c);
+
+       if (c == '#') {
+               while (in.get(c) && c != '\n') {
+                       current.value += c;
+               }
+               return;
+       }
+
+       // c is guaranteed to be '/' now
+       if (!in.get(c)) {
+               throw runtime_error("unexpected end of stream");
+       }
+       if (c == '/') {
+               while (in.get(c) && c != '\n') {
+                       current.value += c;
+               }
+               return;
+       } else if (c != '*') {
+               throw runtime_error("invalid character after /");
+       }
+
+       while (in.get(c)) {
+               if (c == '*') {
+                       istream::char_type c2;
+                       if (!in.get(c2)) {
+                               throw runtime_error("unexpected end of stream");
+                       }
+                       if (c2 == '/') {
+                               break;
+                       } else {
+                               current.value += c;
+                               current.value += c2;
+                       }
+               } else {
+                       current.value += c;
+               }
+       }
+}
+
+void Tokenizer::ReadIdentifier() {
+       current.type = Token::IDENTIFIER;
+
+       istream::char_type c;
+       while (in.get(c)) {
+               if (isalnum(c) || c == '_' || c == '.') {
+                       current.value += c;
+               } else {
+                       in.putback(c);
+                       break;
+               }
+       }
+}
+
+
+TokenStreamReader::TokenStreamReader(istream &in)
+: in(in)
+, cached(false) {
+
+}
+
+
+bool TokenStreamReader::HasMore() {
+       if (cached) {
+               return true;
+       }
+       SkipComments();
+       return cached;
+}
+
+const Token &TokenStreamReader::Next() {
+       SkipComments();
+       cached = false;
+       return in.Current();
+}
+
+void TokenStreamReader::SkipComments() {
+       if (cached) {
+               if (in.Current().type == Token::COMMENT) {
+                       cached = false;
+               } else {
+                       return;
+               }
+       }
+       while (in.HasMore()) {
+               if (in.Next().type != Token::COMMENT) {
+                       cached = true;
+                       return;
+               }
+       }
+}
+
+const Token &TokenStreamReader::Peek() {
+       if (!cached) {
+               Next();
+               cached = true;
+       }
+       return in.Current();
+}
+
+
+void TokenStreamReader::Assert(Token::Type t) const {
+       if (GetType() != t) {
+               stringstream s;
+               s << "unexpected token in input stream: expected " << t << ", but got " << in.Current();
+               throw runtime_error(s.str());
+       }
+}
+
+Token::Type TokenStreamReader::GetType() const noexcept {
+       return in.Current().type;
+}
+
+const std::string &TokenStreamReader::GetValue() const noexcept {
+       return in.Current().value;
+}
+
+void TokenStreamReader::Skip(Token::Type t) {
+       Next();
+       Assert(t);
+}
+
+
+void TokenStreamReader::ReadBoolean(bool &b) {
+       b = GetBool();
+}
+
+void TokenStreamReader::ReadIdentifier(string &out) {
+       Next();
+       Assert(Token::IDENTIFIER);
+       out = GetValue();
+}
+
+void TokenStreamReader::ReadNumber(float &n) {
+       n = GetFloat();
+}
+
+void TokenStreamReader::ReadNumber(int &n) {
+       n = GetInt();
+}
+
+void TokenStreamReader::ReadNumber(unsigned long &n) {
+       n = GetULong();
+}
+
+void TokenStreamReader::ReadString(string &out) {
+       Next();
+       Assert(Token::STRING);
+       out = GetValue();
+}
+
+void TokenStreamReader::ReadRelaxedString(string &out) {
+       out = GetString();
+}
+
+
+void TokenStreamReader::ReadVec(glm::vec2 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadVec(glm::vec3 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::COMMA);
+       ReadNumber(v.z);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadVec(glm::vec4 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::COMMA);
+       ReadNumber(v.z);
+       Skip(Token::COMMA);
+       ReadNumber(v.w);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadVec(glm::ivec2 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadVec(glm::ivec3 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::COMMA);
+       ReadNumber(v.z);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadVec(glm::ivec4 &v) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(v.x);
+       Skip(Token::COMMA);
+       ReadNumber(v.y);
+       Skip(Token::COMMA);
+       ReadNumber(v.z);
+       Skip(Token::COMMA);
+       ReadNumber(v.w);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+void TokenStreamReader::ReadQuat(glm::quat &q) {
+       Skip(Token::BRACKET_OPEN);
+       ReadNumber(q.w);
+       Skip(Token::COMMA);
+       ReadNumber(q.x);
+       Skip(Token::COMMA);
+       ReadNumber(q.y);
+       Skip(Token::COMMA);
+       ReadNumber(q.z);
+       Skip(Token::BRACKET_CLOSE);
+}
+
+
+bool TokenStreamReader::GetBool() {
+       Next();
+       return AsBool();
+}
+
+bool TokenStreamReader::AsBool() const {
+       switch (GetType()) {
+               case Token::NUMBER:
+                       return AsInt() != 0;
+               case Token::IDENTIFIER:
+               case Token::STRING:
+                       if (GetValue() == "true" || GetValue() == "yes" || GetValue() == "on") {
+                               return true;
+                       } else if (GetValue() == "false" || GetValue() == "no" || GetValue() == "off") {
+                               return false;
+                       } else {
+                               throw runtime_error("unexpected value in input stream: cannot cast " + GetValue() + " to bool");
+                       }
+               default:
+                       {
+                               stringstream s;
+                               s << "unexpected token in input stream: cannot cast " << in.Current() << " to bool";
+                               throw runtime_error(s.str());
+                       }
+       }
+}
+
+float TokenStreamReader::GetFloat() {
+       Next();
+       return AsFloat();
+}
+
+float TokenStreamReader::AsFloat() const {
+       Assert(Token::NUMBER);
+       return stof(GetValue());
+}
+
+int TokenStreamReader::GetInt() {
+       Next();
+       return AsInt();
+}
+
+int TokenStreamReader::AsInt() const {
+       Assert(Token::NUMBER);
+       return stoi(GetValue());
+}
+
+unsigned long TokenStreamReader::GetULong() {
+       Next();
+       return AsULong();
+}
+
+unsigned long TokenStreamReader::AsULong() const {
+       Assert(Token::NUMBER);
+       return stoul(GetValue());
+}
+
+const string &TokenStreamReader::GetString() {
+       Next();
+       return AsString();
+}
+
+const string &TokenStreamReader::AsString() const {
+       return GetValue();
+}
+
+}
+}
diff --git a/src/ui/FixedText.hpp b/src/ui/FixedText.hpp
new file mode 100644 (file)
index 0000000..cd9df0a
--- /dev/null
@@ -0,0 +1,59 @@
+#ifndef GONG_UI_FIXEDTEXT_HPP_
+#define GONG_UI_FIXEDTEXT_HPP_
+
+#include "Text.hpp"
+
+
+namespace gong {
+namespace ui {
+
+class FixedText
+: public Text {
+
+public:
+       FixedText() noexcept;
+
+       void Position(const glm::vec3 &p) noexcept {
+               pos = p;
+       }
+       void Position(
+               const glm::vec3 &p,
+               graphics::Gravity g
+       ) noexcept {
+               pos = p;
+               grav = g;
+               Pivot(g);
+       }
+       void Position(
+               const glm::vec3 &p,
+               graphics::Gravity g,
+               graphics::Gravity pv
+       ) noexcept {
+               pos = p;
+               grav = g;
+               Pivot(pv);
+       }
+
+       void Foreground(const glm::vec4 &col) noexcept { fg = col; }
+       void Background(const glm::vec4 &col) noexcept { bg = col; }
+
+       void Show() noexcept { visible = true; }
+       void Hide() noexcept { visible = false; }
+       void Toggle() noexcept { visible = !visible; }
+       bool Visible() const noexcept { return visible; }
+
+       void Render(graphics::Viewport &) noexcept;
+
+private:
+       glm::vec4 bg;
+       glm::vec4 fg;
+       glm::vec3 pos;
+       graphics::Gravity grav;
+       bool visible;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/MessageBox.hpp b/src/ui/MessageBox.hpp
new file mode 100644 (file)
index 0000000..4d30e2a
--- /dev/null
@@ -0,0 +1,62 @@
+#ifndef GONG_UI_MESSAGEBOX_HPP_
+#define GONG_UI_MESSAGEBOX_HPP_
+
+#include "Text.hpp"
+#include "../graphics/align.hpp"
+#include "../graphics/glm.hpp"
+#include "../graphics/PrimitiveMesh.hpp"
+
+#include <deque>
+#include <string>
+
+
+namespace gong {
+namespace graphics {
+       class Font;
+       class Viewport;
+}
+namespace ui {
+
+class MessageBox {
+
+public:
+       explicit MessageBox(const graphics::Font &);
+
+       void Position(const glm::vec3 &, graphics::Gravity) noexcept;
+
+       void Foreground(const glm::vec4 &col) noexcept { fg = col; }
+       void Background(const glm::vec4 &col) noexcept { bg = col; dirty = true; }
+
+       void PushLine(const char *);
+       void PushLine(const std::string &l) {
+               PushLine(l.c_str());
+       }
+
+       void Render(graphics::Viewport &) noexcept;
+
+private:
+       void Recalc();
+
+private:
+       const graphics::Font &font;
+       std::deque<Text> lines;
+       std::size_t max_lines;
+
+       glm::vec3 pos;
+       glm::vec3 adv;
+       glm::vec2 size;
+
+       graphics::PrimitiveMesh::Color bg;
+       graphics::PrimitiveMesh::Color fg;
+
+       graphics::PrimitiveMesh bg_mesh;
+
+       graphics::Gravity grav;
+       bool dirty;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/Text.hpp b/src/ui/Text.hpp
new file mode 100644 (file)
index 0000000..8f9bdea
--- /dev/null
@@ -0,0 +1,54 @@
+#ifndef GONG_UI_TEXT_HPP_
+#define GONG_UI_TEXT_HPP_
+
+#include "../graphics/align.hpp"
+#include "../graphics/glm.hpp"
+#include "../graphics/Texture.hpp"
+#include "../graphics/SpriteMesh.hpp"
+
+#include <string>
+
+
+namespace gong {
+namespace graphics {
+       class Font;
+       class Viewport;
+}
+namespace ui {
+
+class Text {
+
+public:
+       Text() noexcept;
+
+       void Set(const graphics::Font &, const char *);
+       void Set(const graphics::Font &f, const std::string &s) {
+               Set(f, s.c_str());
+       }
+
+       graphics::Gravity Pivot() const noexcept { return pivot; }
+       void Pivot(graphics::Gravity p) noexcept {
+               pivot = p;
+               dirty = true;
+       }
+
+       const glm::vec2 &Size() const noexcept { return size; }
+
+       void Render(graphics::Viewport &) noexcept;
+
+private:
+       void Update();
+
+private:
+       graphics::Texture tex;
+       graphics::SpriteMesh sprite;
+       glm::vec2 size;
+       graphics::Gravity pivot;
+       bool dirty;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/TextInput.hpp b/src/ui/TextInput.hpp
new file mode 100644 (file)
index 0000000..6aa6c78
--- /dev/null
@@ -0,0 +1,81 @@
+#ifndef GONG_UI_TEXTINPUT_HPP_
+#define GONG_UI_TEXTINPUT_HPP_
+
+#include "Text.hpp"
+#include "../graphics/PrimitiveMesh.hpp"
+
+#include <string>
+#include <SDL.h>
+
+
+namespace gong {
+namespace graphics {
+       class Viewport;
+}
+namespace ui {
+
+class TextInput {
+
+public:
+       explicit TextInput(const graphics::Font &);
+
+       const std::string &GetInput() const noexcept { return input; }
+
+       void Focus(graphics::Viewport &) noexcept;
+       void Blur() noexcept;
+
+       void Clear() noexcept;
+       void Backspace() noexcept;
+       void Delete() noexcept;
+
+       void MoveBegin() noexcept;
+       void MoveBackward() noexcept;
+       void MoveForward() noexcept;
+       void MoveEnd() noexcept;
+
+       void Insert(const char *);
+
+       bool AtBegin() const noexcept;
+       bool AtEnd() const noexcept;
+
+       void Position(const glm::vec3 &p, graphics::Gravity g, graphics::Gravity pv) noexcept;
+       void Width(float) noexcept;
+
+       void Foreground(const graphics::PrimitiveMesh::Color &col) noexcept { fg = col; dirty_cursor = true; }
+       void Background(const graphics::PrimitiveMesh::Color &col) noexcept { bg = col; dirty_box = true; }
+
+       void Handle(const SDL_TextInputEvent &);
+       void Handle(const SDL_TextEditingEvent &);
+
+       void Render(graphics::Viewport &);
+
+private:
+       void Refresh();
+
+private:
+       const graphics::Font &font;
+       std::string input;
+       std::string::size_type cursor;
+       Text text;
+
+       graphics::PrimitiveMesh bg_mesh;
+       graphics::PrimitiveMesh cursor_mesh;
+
+       graphics::PrimitiveMesh::Color bg;
+       graphics::PrimitiveMesh::Color fg;
+
+       glm::vec3 position;
+       glm::vec2 size;
+       graphics::Gravity gravity;
+
+       bool active;
+       bool dirty_box;
+       bool dirty_cursor;
+       bool dirty_text;
+
+};
+
+}
+}
+
+#endif
diff --git a/src/ui/widgets.cpp b/src/ui/widgets.cpp
new file mode 100644 (file)
index 0000000..12d4a46
--- /dev/null
@@ -0,0 +1,332 @@
+#include "FixedText.hpp"
+#include "MessageBox.hpp"
+#include "Text.hpp"
+#include "TextInput.hpp"
+
+#include "../graphics/Font.hpp"
+#include "../graphics/Viewport.hpp"
+
+#include <cstdio>
+#include <cstring>
+#include <limits>
+
+using namespace std;
+
+
+namespace gong {
+namespace ui {
+
+MessageBox::MessageBox(const graphics::Font &f)
+: font(f)
+, lines()
+, max_lines(10)
+, pos(0.0f)
+, adv(0.0f, font.LineSkip(), 0.0f)
+, bg(1.0f, 1.0f, 1.0f, 0.0f)
+, fg(1.0f, 1.0f, 1.0f, 1.0f)
+, grav(graphics::Gravity::NORTH_WEST)
+, dirty(true) {
+
+}
+
+void MessageBox::Position(const glm::vec3 &p, graphics::Gravity g) noexcept {
+       pos = p;
+       grav = g;
+       if (get_y(g) == graphics::Align::END) {
+               adv.y = -font.LineSkip();
+       } else {
+               adv.y = font.LineSkip();
+       }
+       for (Text &txt : lines) {
+               txt.Pivot(g);
+       }
+       dirty = true;
+}
+
+void MessageBox::PushLine(const char *text) {
+       lines.emplace_front();
+       Text &txt = lines.front();
+       txt.Set(font, text);
+       txt.Pivot(grav);
+
+       while (lines.size() > max_lines) {
+               lines.pop_back();
+       }
+       dirty = true;
+}
+
+namespace {
+
+graphics::PrimitiveMesh::Buffer bg_buf;
+
+}
+
+void MessageBox::Recalc() {
+       size = glm::vec2(0.0f, 0.0f);
+       for (const Text &line : lines) {
+               size.x = max(size.x, line.Size().x);
+               size.y += line.Size().y;
+       }
+       bg_buf.FillRect(size.x, size.y, bg, align(grav, size));
+       bg_mesh.Update(bg_buf);
+       bg_buf.Clear();
+       dirty = false;
+}
+
+void MessageBox::Render(graphics::Viewport &viewport) noexcept {
+       viewport.SetCursor(pos, grav);
+       if (bg.a > numeric_limits<float>::epsilon()) {
+               if (dirty) {
+                       Recalc();
+               }
+               graphics::PlainColor &prog = viewport.HUDColorProgram();
+               prog.SetM(viewport.Cursor());
+               bg_mesh.DrawTriangles();
+               viewport.MoveCursor(glm::vec3(0.0f, 0.0f, -1.0f));
+       }
+       graphics::BlendedSprite &prog = viewport.SpriteProgram();
+       prog.SetBG(glm::vec4(0.0f));
+       prog.SetFG(glm::vec4(fg) * (1.0f / 255.0f));
+       for (Text &txt : lines) {
+               prog.SetM(viewport.Cursor());
+               txt.Render(viewport);
+               viewport.MoveCursor(adv);
+       }
+}
+
+
+Text::Text() noexcept
+: tex()
+, sprite()
+, size(0.0f)
+, pivot(graphics::Gravity::NORTH_WEST)
+, dirty(false) {
+
+}
+
+FixedText::FixedText() noexcept
+: Text()
+, bg(1.0f, 1.0f, 1.0f, 0.0f)
+, fg(1.0f, 1.0f, 1.0f, 1.0f)
+, pos(0.0f)
+, grav(graphics::Gravity::NORTH_WEST)
+, visible(false) {
+
+}
+
+void Text::Set(const graphics::Font &font, const char *text) {
+       font.Render(text, tex);
+       size = font.TextSize(text);
+       dirty = true;
+}
+
+namespace {
+
+graphics::SpriteMesh::Buffer sprite_buf;
+
+}
+
+void Text::Update() {
+       sprite_buf.LoadRect(size.x, size.y, align(pivot, size));
+       sprite.Update(sprite_buf);
+       dirty = false;
+}
+
+void FixedText::Render(graphics::Viewport &viewport) noexcept {
+       graphics::BlendedSprite &prog = viewport.SpriteProgram();
+       viewport.SetCursor(pos, grav);
+       prog.SetM(viewport.Cursor());
+       prog.SetBG(bg);
+       prog.SetFG(fg);
+       Text::Render(viewport);
+}
+
+void Text::Render(graphics::Viewport &viewport) noexcept {
+       if (dirty) {
+               Update();
+       }
+       graphics::BlendedSprite &prog = viewport.SpriteProgram();
+       prog.SetTexture(tex);
+       sprite.Draw();
+}
+
+
+TextInput::TextInput(const graphics::Font &font)
+: font(font)
+, input()
+, cursor(0)
+, text()
+, bg_mesh()
+, cursor_mesh()
+, bg(1.0f, 1.0f, 1.0f, 0.0f)
+, fg(1.0f, 1.0f, 1.0f, 1.0f)
+, position(0.0f)
+, size(font.LineSkip())
+, gravity(graphics::Gravity::NORTH_WEST)
+, active(false)
+, dirty_box(true)
+, dirty_cursor(true)
+, dirty_text(true) {
+
+}
+
+void TextInput::Focus(graphics::Viewport &viewport) noexcept {
+       SDL_StartTextInput();
+       active = true;
+
+       glm::vec2 p(viewport.GetPosition(glm::vec2(position), gravity));
+       SDL_Rect rect;
+       rect.x = p.x;
+       rect.y = p.y;
+       rect.w = size.x;
+       rect.h = size.y;
+       SDL_SetTextInputRect(&rect);
+}
+
+void TextInput::Blur() noexcept {
+       SDL_StopTextInput();
+       active = false;
+}
+
+void TextInput::Clear() noexcept {
+       input.clear();
+       cursor = 0;
+       dirty_text = true;
+}
+
+void TextInput::Backspace() noexcept {
+       string::size_type previous(cursor);
+       MoveBackward();
+       input.erase(cursor, previous - cursor);
+       dirty_text = true;
+}
+
+void TextInput::Delete() noexcept {
+       string::size_type previous(cursor);
+       MoveForward();
+       input.erase(previous, cursor - previous);
+       cursor = previous;
+       dirty_text = true;
+}
+
+void TextInput::MoveBegin() noexcept {
+       cursor = 0;
+}
+
+void TextInput::MoveBackward() noexcept {
+       if (AtBegin()) return;
+       --cursor;
+       while (cursor > 0 && (input[cursor] & 0xC0) == 0x80) {
+               --cursor;
+       }
+}
+
+void TextInput::MoveForward() noexcept {
+       if (AtEnd()) return;
+       ++cursor;
+       while (cursor <= input.size() && (input[cursor] & 0xC0) == 0x80) {
+               ++cursor;
+       }
+}
+
+void TextInput::MoveEnd() noexcept {
+       cursor = input.size();
+}
+
+void TextInput::Insert(const char *str) {
+       size_t len = strlen(str);
+       input.insert(cursor, str, len);
+       cursor += len;
+       dirty_text = true;
+}
+
+bool TextInput::AtBegin() const noexcept {
+       return cursor == 0;
+}
+
+bool TextInput::AtEnd() const noexcept {
+       return cursor == input.size();
+}
+
+void TextInput::Position(const glm::vec3 &p, graphics::Gravity g, graphics::Gravity pv) noexcept {
+       position = p;
+       gravity = g;
+       text.Pivot(pv);
+       dirty_box = true;
+}
+
+void TextInput::Width(float w) noexcept {
+       size.x = w;
+       dirty_box = true;
+}
+
+void TextInput::Handle(const SDL_TextInputEvent &e) {
+       Insert(e.text);
+}
+
+void TextInput::Handle(const SDL_TextEditingEvent &) {
+
+}
+
+void TextInput::Refresh() {
+       if (dirty_box) {
+               bg_buf.FillRect(size.x, size.y, bg, align(gravity, size));
+               bg_mesh.Update(bg_buf);
+               bg_buf.Clear();
+               dirty_box = false;
+       }
+       if (dirty_cursor) {
+               bg_buf.Reserve(2, 2);
+               bg_buf.vertices.emplace_back(0.0f, 0.0f, 0.0f);
+               bg_buf.vertices.emplace_back(0.0f, float(font.LineSkip()), 0.0f);
+               bg_buf.colors.resize(2, fg);
+               bg_buf.indices.push_back(0);
+               bg_buf.indices.push_back(1);
+               cursor_mesh.Update(bg_buf);
+               bg_buf.Clear();
+               dirty_cursor = false;
+       }
+       if (dirty_text) {
+               if (!input.empty()) {
+                       text.Set(font, input.c_str());
+               }
+               dirty_text = false;
+       }
+}
+
+void TextInput::Render(graphics::Viewport &viewport) {
+       Refresh();
+       viewport.SetCursor(position, gravity);
+       if (bg.a > numeric_limits<float>::epsilon()) {
+               viewport.EnableAlphaBlending();
+               graphics::PlainColor &prog = viewport.HUDColorProgram();
+               prog.SetM(viewport.Cursor());
+               bg_mesh.DrawTriangles();
+               viewport.MoveCursor(glm::vec3(0.0f, 0.0f, -1.0f));
+       }
+       if (!input.empty()) {
+               graphics::BlendedSprite &prog = viewport.SpriteProgram();
+               prog.SetBG(glm::vec4(0.0f));
+               prog.SetFG(glm::vec4(fg) * (1.0f / 255.0f));
+               prog.SetM(viewport.Cursor());
+               text.Render(viewport);
+       }
+       if (active) {
+               glm::vec2 offset(0.0f);
+               if (input.empty() || AtBegin()) {
+                       // a okay
+                       offset = -align(text.Pivot(), glm::vec2(0.0f, font.LineSkip()));
+               } else if (AtEnd()) {
+                       offset = -align(text.Pivot(), text.Size(), glm::vec2(-text.Size().x, 0.0f));
+               } else {
+                       offset = -align(text.Pivot(), text.Size(), glm::vec2(-font.TextSize(input.substr(0, cursor).c_str()).x, 0.0f));
+               }
+               viewport.MoveCursor(glm::vec3(offset, -1.0f));
+               graphics::PlainColor &prog = viewport.HUDColorProgram();
+               prog.SetM(viewport.Cursor());
+               cursor_mesh.DrawLines();
+       }
+}
+
+}
+}
diff --git a/tst/app/TimerTest.cpp b/tst/app/TimerTest.cpp
new file mode 100644 (file)
index 0000000..9ec0224
--- /dev/null
@@ -0,0 +1,255 @@
+#include "TimerTest.hpp"
+
+#include "app/IntervalTimer.hpp"
+
+#include <limits>
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::app::test::TimerTest);
+
+
+namespace gong {
+namespace app {
+namespace test {
+
+void TimerTest::setUp() {
+}
+
+void TimerTest::tearDown() {
+}
+
+
+void TimerTest::testCoarseTimer() {
+       CoarseTimer timer(50);
+       CPPUNIT_ASSERT_MESSAGE(
+               "fresh coarse timer is running",
+               !timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "fresh coarse timer hit",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "fresh coarse timer with non-zero elapsed time",
+               0, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "fresh coarse timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+
+       timer.Start();
+       CPPUNIT_ASSERT_MESSAGE(
+               "startet coarse timer is not running",
+               timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "started coarse timer hit without update",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "started, but not updated coarse timer with non-zero elapsed time",
+               0, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "started, but not updated coarse timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+
+       timer.Update(25);
+       CPPUNIT_ASSERT_MESSAGE(
+               "updated coarse timer is not running",
+               timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "coarse timer hit after update, but before it should",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong elapsed time on updated coarse timer",
+               25, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration on updated coarse timer",
+               0, timer.Iteration()
+       );
+
+       timer.Update(25);
+       CPPUNIT_ASSERT_MESSAGE(
+               "coarse timer not hit after updating to its exact interval time",
+               timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong elapsed time on updated coarse timer",
+               50, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration on updated coarse timer at exact interval time",
+               1, timer.Iteration()
+       );
+
+       timer.Update(49);
+       CPPUNIT_ASSERT_MESSAGE(
+               "coarse timer hit after updating from exact interval time to just before the next",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong elapsed time on updated coarse timer",
+               99, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration after updating coarse timer from exact interval time to just before the next",
+               1, timer.Iteration()
+       );
+
+       timer.Update(2);
+       CPPUNIT_ASSERT_MESSAGE(
+               "coarse timer not hit after updating across interval time boundary",
+               timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong elapsed time on updated coarse timer",
+               101, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration after updating across interval time boundary",
+               2, timer.Iteration()
+       );
+
+       timer.Stop();
+       CPPUNIT_ASSERT_MESSAGE(
+               "stopped coarse timer is running",
+               !timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "stopped coarse timer hit",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "stopped coarse timer has non-zero elapsed time",
+               0, timer.Elapsed()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "stopped coarse timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+}
+
+void TimerTest::testFineTimer() {
+       FineTimer timer(0.5f);
+       CPPUNIT_ASSERT_MESSAGE(
+               "fresh fine timer is running",
+               !timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "fresh fine timer hit",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "fresh fine timer with non-zero elapsed time",
+               0.0f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "fresh fine timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+
+       timer.Start();
+       CPPUNIT_ASSERT_MESSAGE(
+               "startet fine timer is not running",
+               timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "started fine timer hit without update",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "started, but not updated fine timer with non-zero elapsed time",
+               0.0f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "started, but not updated fine timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+
+       timer.Update(0.25f);
+       CPPUNIT_ASSERT_MESSAGE(
+               "updated fine timer is not running",
+               timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "fine timer hit after update, but before it should",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "wrong elapsed time on updated fine timer",
+               0.25f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration on updated fine timer",
+               0, timer.Iteration()
+       );
+
+       timer.Update(0.25f);
+       CPPUNIT_ASSERT_MESSAGE(
+               "fine timer not hit after updating to its exact interval time",
+               timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "wrong elapsed time on updated fine timer",
+               0.5f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration on updated fine timer at exact interval time",
+               1, timer.Iteration()
+       );
+
+       timer.Update(0.49f);
+       CPPUNIT_ASSERT_MESSAGE(
+               "fine timer hit after updating from exact interval time to just before the next",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "wrong elapsed time on updated fine timer",
+               0.99f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration after updating fine timer from exact interval time to just before the next",
+               1, timer.Iteration()
+       );
+
+       timer.Update(0.02f);
+       CPPUNIT_ASSERT_MESSAGE(
+               "fine timer not hit after updating across interval time boundary",
+               timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "wrong elapsed time on updated fine timer",
+               1.01f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong iteration after updating across interval time boundary",
+               2, timer.Iteration()
+       );
+
+       timer.Stop();
+       CPPUNIT_ASSERT_MESSAGE(
+               "stopped fine timer is running",
+               !timer.Running()
+       );
+       CPPUNIT_ASSERT_MESSAGE(
+               "stopped fine timer hit",
+               !timer.Hit()
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "stopped fine timer has non-zero elapsed time",
+               0.0f, timer.Elapsed(), std::numeric_limits<float>::epsilon()
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "stopped fine timer at non-zero iteration",
+               0, timer.Iteration()
+       );
+}
+
+}
+}
+}
diff --git a/tst/app/TimerTest.hpp b/tst/app/TimerTest.hpp
new file mode 100644 (file)
index 0000000..671a4bf
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef GONG_TEST_APP_TIMERTEST_H_
+#define GONG_TEST_APP_TIMERTEST_H_
+
+#include <cppunit/extensions/HelperMacros.h>
+
+
+namespace gong {
+namespace app {
+namespace test {
+
+class TimerTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(TimerTest);
+
+CPPUNIT_TEST(testCoarseTimer);
+CPPUNIT_TEST(testFineTimer);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testCoarseTimer();
+       void testFineTimer();
+
+};
+
+}
+}
+}
+
+#endif
diff --git a/tst/geometry/IntersectionTest.cpp b/tst/geometry/IntersectionTest.cpp
new file mode 100644 (file)
index 0000000..7aac189
--- /dev/null
@@ -0,0 +1,272 @@
+#include "IntersectionTest.hpp"
+
+#include "geometry/const.hpp"
+#include "geometry/primitive.hpp"
+
+#include <limits>
+#include <glm/gtx/io.hpp>
+#include <glm/gtx/transform.hpp>
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::geometry::test::IntersectionTest);
+
+
+namespace gong {
+namespace geometry {
+namespace test {
+
+void IntersectionTest::setUp() {
+}
+
+void IntersectionTest::tearDown() {
+}
+
+
+void IntersectionTest::testSimpleRayBoxIntersection() {
+       Ray ray{ { 0, 0, 0 }, { 1, 0, 0 }, { } }; // at origin, pointing right
+       ray.Update();
+       AABB box{ { -1, -1, -1 }, { 1, 1, 1 } }; // 2x2x2 cube centered around origin
+
+       const float delta = std::numeric_limits<float>::epsilon();
+
+       float distance = 0;
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray at origin not intersecting box at origin",
+               Intersection(ray, box, distance)
+       );
+
+       // move ray outside the box, but have it still point at it
+       // should be 4 units to the left now
+       ray.orig.x = -5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box doesn't intersect",
+               Intersection(ray, box, distance)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+
+       // move ray to the other side, so it's pointing away now
+       ray.orig.x = 5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing away from box still intersects",
+               !Intersection(ray, box, distance)
+       );
+
+       // 45 deg down from 4 units away, so should be about 4 * sqrt(2)
+       ray.orig = { -5.0f, 4.5f, 0.0f };
+       ray.dir = { 0.70710678118654752440f, -0.70710678118654752440f, 0.0f };
+       ray.Update();
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box doesn't intersect",
+               Intersection(ray, box, distance)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               5.65685424949238019520f, distance, delta
+       );
+}
+
+void IntersectionTest::testRayBoxIntersection() {
+       Ray ray{ { 0, 0, 0 }, { 1, 0, 0 }, { } }; // at origin, pointing right
+       AABB box{ { -1, -1, -1 }, { 1, 1, 1 } }; // 2x2x2 cube centered around origin
+       glm::mat4 M(1); // no transformation
+
+       const float delta = std::numeric_limits<float>::epsilon();
+
+       float distance = 0;
+       glm::vec3 normal(0);
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray at origin not intersecting box at origin",
+               Intersection(ray, box, M, &distance)
+       );
+       // normal undefined, so can't test
+
+       // move ray outside the box, but have it still point at it
+       // should be 4 units to the left now
+       ray.orig.x = -5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box to the right doesn't intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(-1, 0, 0), normal
+       );
+
+       // move ray to the other side, so it's pointing away now
+       ray.orig.x = 5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing away from box to the left still intersects",
+               !Intersection(ray, box, M)
+       );
+
+       // turn ray around
+       ray.dir.x = -1;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box to the left does not intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(1, 0, 0), normal
+       );
+
+       // ray below
+       ray.orig = { 0, -5, 0 };
+       ray.dir = { 0, 1, 0 };
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box above does not intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(0, -1, 0), normal
+       );
+
+       // turn ray around
+       ray.dir.y = -1;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing away from box above still intersects",
+               !Intersection(ray, box, M)
+       );
+
+       // move ray above
+       ray.orig.y = 5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box below does not intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(0, 1, 0), normal
+       );
+
+       // ray behind
+       ray.orig = { 0, 0, -5 };
+       ray.dir = { 0, 0, 1 };
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box in front does not intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(0, 0, -1), normal
+       );
+
+       // turn ray around
+       ray.dir.z = -1;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing away from box in front still intersects",
+               !Intersection(ray, box, M)
+       );
+
+       // move ray in front
+       ray.orig.z = 5;
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box behind does not intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               4.0f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(0, 0, 1), normal
+       );
+
+       // 45 deg down from 4 units away, so should be about 4 * sqrt(2)
+       ray.orig = { -5.0f, 4.5f, 0.0f };
+       ray.dir = { 0.70710678118654752440f, -0.70710678118654752440f, 0.0f };
+       CPPUNIT_ASSERT_MESSAGE(
+               "ray pointing at box doesn't intersect",
+               Intersection(ray, box, M, &distance, &normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "intersection distance way off",
+               5.65685424949238019520f, distance, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "wrong surface normal at intersection point",
+               glm::vec3(-1, 0, 0), normal
+       );
+}
+
+void IntersectionTest::testBoxBoxIntersection() {
+       const float delta = std::numeric_limits<float>::epsilon();
+       float depth = 0;
+       glm::vec3 normal(0);
+
+       AABB box{ { -1, -1, -1 }, { 1, 1, 1 } }; // 2x2x2 cube centered around origin
+       glm::mat4 Ma(1); // identity
+       glm::mat4 Mb(1); // identity
+       // they're identical, so should probably intersect ^^
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "identical OBBs don't intersect",
+               Intersection(box, Ma, box, Mb, depth, normal)
+       );
+       // depth is two, but normal can be any
+       // (will probably be the first axis of box a, but any is valid)
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "penetration depth of coincidental 2x2x2 boxes is not 2",
+               2.0f, depth, delta
+       );
+
+       Ma = glm::translate(glm::vec3(-2, 0, 0)); // 2 to the left
+       Mb = glm::translate(glm::vec3(2, 0, 0)); // 2 to the right
+       CPPUNIT_ASSERT_MESSAGE(
+               "distant OBBs intersect (2 apart, no rotation)",
+               !Intersection(box, Ma, box, Mb, depth, normal)
+       );
+       // depth and normal undefined for non-intersecting objects
+
+       Ma = glm::rotate(PI_0p25, glm::vec3(0, 0, 1)); // rotated 45° around Z
+       Mb = glm::translate(glm::vec3(2.4, 0, 0)); // 2.4 to the right
+       // they should barely touch. intersect by about sqrt(2) - 1.4 if my head works
+       CPPUNIT_ASSERT_MESSAGE(
+               "OBBs don't intersect (one rotated by 45°)",
+               Intersection(box, Ma, box, Mb, depth, normal)
+       );
+       CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
+               "bad penetration depth (with rotation)",
+               0.01421356237309504880f, depth, delta
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad intersection normal (with rotation)",
+               glm::vec3(1, 0, 0), glm::abs(normal) // normal can be in + or - x, therefore abs()
+       );
+
+       Mb = glm::translate(glm::vec3(3, 0, 0)); // 3 to the right
+       CPPUNIT_ASSERT_MESSAGE(
+               "OBBs intersect (one rotated by 45°)",
+               !Intersection(box, Ma, box, Mb, depth, normal)
+       );
+}
+
+}
+}
+}
diff --git a/tst/geometry/IntersectionTest.hpp b/tst/geometry/IntersectionTest.hpp
new file mode 100644 (file)
index 0000000..19c25e6
--- /dev/null
@@ -0,0 +1,36 @@
+#ifndef GONG_TEST_GEOMETRY_INTERSECTIONTEST_H_
+#define GONG_TEST_GEOMETRY_INTERSECTIONTEST_H_
+
+#include <cppunit/extensions/HelperMacros.h>
+
+
+namespace gong {
+namespace geometry {
+namespace test {
+
+class IntersectionTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(IntersectionTest);
+
+CPPUNIT_TEST(testSimpleRayBoxIntersection);
+CPPUNIT_TEST(testRayBoxIntersection);
+CPPUNIT_TEST(testBoxBoxIntersection);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testSimpleRayBoxIntersection();
+       void testRayBoxIntersection();
+       void testBoxBoxIntersection();
+
+};
+
+}
+}
+}
+
+#endif
diff --git a/tst/graphics/GLTraitsTest.cpp b/tst/graphics/GLTraitsTest.cpp
new file mode 100644 (file)
index 0000000..4931ab2
--- /dev/null
@@ -0,0 +1,145 @@
+#include "GLTraitsTest.hpp"
+
+#include "graphics/gl_traits.hpp"
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::graphics::test::GLTraitsTest);
+
+
+namespace gong {
+namespace graphics {
+namespace test {
+
+void GLTraitsTest::setUp() {
+
+}
+
+void GLTraitsTest::tearDown() {
+
+}
+
+
+void GLTraitsTest::testSize() {
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for byte",
+               1, gl_traits<signed char>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for ubyte",
+               1, gl_traits<unsigned char>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for short",
+               1, gl_traits<short>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for ushort",
+               1, gl_traits<unsigned short>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for int",
+               1, gl_traits<int>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for uint",
+               1, gl_traits<unsigned int>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for float",
+               1, gl_traits<float>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for double",
+               1, gl_traits<double>::size
+       );
+
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec2",
+               2, gl_traits<glm::vec2>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec3",
+               3, gl_traits<glm::vec3>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec4",
+               4, gl_traits<glm::vec4>::size
+       );
+
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec2i",
+               2, gl_traits<glm::ivec2>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec3i",
+               3, gl_traits<glm::ivec3>::size
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad number of components for vec4i",
+               4, gl_traits<glm::ivec4>::size
+       );
+}
+
+void GLTraitsTest::testType() {
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for byte",
+               GLenum(GL_BYTE), gl_traits<signed char>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for ubyte",
+               GLenum(GL_UNSIGNED_BYTE), gl_traits<unsigned char>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for short",
+               GLenum(GL_SHORT), gl_traits<short>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for ushort",
+               GLenum(GL_UNSIGNED_SHORT), gl_traits<unsigned short>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for int",
+               GLenum(GL_INT), gl_traits<int>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for uint",
+               GLenum(GL_UNSIGNED_INT), gl_traits<unsigned int>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for float",
+               GLenum(GL_FLOAT), gl_traits<float>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for double",
+               GLenum(GL_DOUBLE), gl_traits<double>::type
+       );
+
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec2",
+               GLenum(GL_FLOAT), gl_traits<glm::vec2>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec3",
+               GLenum(GL_FLOAT), gl_traits<glm::vec3>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec4",
+               GLenum(GL_FLOAT), gl_traits<glm::vec4>::type
+       );
+
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec2i",
+               GLenum(GL_INT), gl_traits<glm::ivec2>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec3i",
+               GLenum(GL_INT), gl_traits<glm::ivec3>::type
+       );
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "bad component type for vec4i",
+               GLenum(GL_INT), gl_traits<glm::ivec4>::type
+       );
+}
+
+}
+}
+}
diff --git a/tst/graphics/GLTraitsTest.hpp b/tst/graphics/GLTraitsTest.hpp
new file mode 100644 (file)
index 0000000..29bb3c0
--- /dev/null
@@ -0,0 +1,33 @@
+#ifndef GONG_TEST_GRAPHICS_GLTRAITSTEST_HPP_
+#define GONG_TEST_GRAPHICS_GLTRAITSTEST_HPP_
+
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace gong {
+namespace graphics {
+namespace test {
+
+class GLTraitsTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(GLTraitsTest);
+
+CPPUNIT_TEST(testSize);
+CPPUNIT_TEST(testType);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testSize();
+       void testType();
+
+};
+
+}
+}
+}
+
+#endif
diff --git a/tst/io/EventTest.cpp b/tst/io/EventTest.cpp
new file mode 100644 (file)
index 0000000..e68e2b7
--- /dev/null
@@ -0,0 +1,604 @@
+#include "EventTest.hpp"
+
+#include "io/event.hpp"
+
+#include <sstream>
+#include <string>
+#include <SDL_syswm.h>
+
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::io::test::EventTest);
+
+using namespace std;
+
+namespace gong {
+namespace io {
+namespace test {
+
+void EventTest::setUp() {
+
+}
+
+void EventTest::tearDown() {
+
+}
+
+
+namespace {
+
+template<class T>
+string string_cast(const T &val) {
+       stringstream str;
+       str << val;
+       return str.str();
+}
+
+}
+
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+
+void EventTest::testAudioDevice() {
+       SDL_Event event;
+       event.type = SDL_AUDIODEVICEADDED;
+       event.adevice.which = 1;
+       event.adevice.iscapture = false;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL audio device event",
+               string("audio device added: device ID: 1, capture: no"), string_cast(event));
+       event.adevice.which = 2;
+       event.adevice.iscapture = true;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL audio device event",
+               string("audio device added: device ID: 2, capture: yes"), string_cast(event));
+               event.type = SDL_AUDIODEVICEREMOVED;
+       event.adevice.which = 3;
+       event.adevice.iscapture = false;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL audio device event",
+               string("audio device removed: device ID: 3, capture: no"), string_cast(event));
+       event.adevice.which = 4;
+       event.adevice.iscapture = true;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL audio device event",
+               string("audio device removed: device ID: 4, capture: yes"), string_cast(event));
+}
+
+#endif
+
+void EventTest::testController() {
+       SDL_Event event;
+       event.type = SDL_CONTROLLERAXISMOTION;
+       event.caxis.which = 0;
+       event.caxis.axis = 1;
+       event.caxis.value = 16384;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller axis event",
+               string("controller axis motion: controller ID: 0, axis ID: 1, value: 0.5"), string_cast(event));
+
+       event.type = SDL_CONTROLLERBUTTONDOWN;
+       event.cbutton.which = 2;
+       event.cbutton.button = 3;
+       event.cbutton.state = SDL_PRESSED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller button event",
+               string("controller button down: controller ID: 2, button ID: 3, state: pressed"), string_cast(event));
+       event.type = SDL_CONTROLLERBUTTONUP;
+       event.cbutton.which = 4;
+       event.cbutton.button = 5;
+       event.cbutton.state = SDL_RELEASED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller button event",
+               string("controller button up: controller ID: 4, button ID: 5, state: released"), string_cast(event));
+
+       event.type = SDL_CONTROLLERDEVICEADDED;
+       event.cdevice.which = 6;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller device event",
+               string("controller device added: controller ID: 6"), string_cast(event));
+       event.type = SDL_CONTROLLERDEVICEREMOVED;
+       event.cdevice.which = 7;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller device event",
+               string("controller device removed: controller ID: 7"), string_cast(event));
+       event.type = SDL_CONTROLLERDEVICEREMAPPED;
+       event.cdevice.which = 8;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL controller device event",
+               string("controller device remapped: controller ID: 8"), string_cast(event));
+}
+
+void EventTest::testDollar() {
+       SDL_Event event;
+       event.type = SDL_DOLLARGESTURE;
+       event.dgesture.touchId = 0;
+       event.dgesture.gestureId = 1;
+       event.dgesture.numFingers = 2;
+       event.dgesture.error = 3;
+       event.dgesture.x = 4;
+       event.dgesture.y = 5;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL dollar gesture event",
+               string("dollar gesture: device ID: 0, gesture ID: 1, fingers: 2, error: 3, position: 4 5"), string_cast(event));
+
+       event.type = SDL_DOLLARRECORD;
+       event.dgesture.touchId = 6;
+       event.dgesture.gestureId = 7;
+       event.dgesture.numFingers = 8;
+       event.dgesture.error = 9;
+       event.dgesture.x = 10;
+       event.dgesture.y = 11;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL dollar record event",
+               string("dollar record: device ID: 6, gesture ID: 7, fingers: 8, error: 9, position: 10 11"), string_cast(event));
+}
+
+void EventTest::testDrop() {
+       char filename[] = "/dev/random";
+       SDL_Event event;
+       event.type = SDL_DROPFILE;
+       event.drop.file = filename;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL drop file event",
+               string("drop file: file: ") + filename, string_cast(event));
+}
+
+void EventTest::testFinger() {
+       SDL_Event event;
+       event.type = SDL_FINGERMOTION;
+       event.tfinger.touchId = 0;
+       event.tfinger.fingerId = 1;
+       event.tfinger.x = 2;
+       event.tfinger.y = 3;
+       event.tfinger.dx = 4;
+       event.tfinger.dy = 5;
+       event.tfinger.pressure = 6;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL finger motion event",
+               string("finger motion: device ID: 0, finger ID: 1, position: 2 3, delta: 4 5, pressure: 6"), string_cast(event));
+
+       event.type = SDL_FINGERDOWN;
+       event.tfinger.touchId = 7;
+       event.tfinger.fingerId = 8;
+       event.tfinger.x = 9;
+       event.tfinger.y = 10;
+       event.tfinger.dx = 11;
+       event.tfinger.dy = 12;
+       event.tfinger.pressure = 13;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL finger down event",
+               string("finger down: device ID: 7, finger ID: 8, position: 9 10, delta: 11 12, pressure: 13"), string_cast(event));
+
+       event.type = SDL_FINGERUP;
+       event.tfinger.touchId = 14;
+       event.tfinger.fingerId = 15;
+       event.tfinger.x = 16;
+       event.tfinger.y = 17;
+       event.tfinger.dx = 18;
+       event.tfinger.dy = 19;
+       event.tfinger.pressure = 20;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL finger up event",
+               string("finger up: device ID: 14, finger ID: 15, position: 16 17, delta: 18 19, pressure: 20"), string_cast(event));
+}
+
+void EventTest::testKey() {
+       SDL_Event event;
+       event.type = SDL_KEYDOWN;
+       event.key.windowID = 0;
+       event.key.state = SDL_PRESSED;
+       event.key.repeat = 0;
+       event.key.keysym.scancode = SDL_SCANCODE_0;
+       event.key.keysym.sym = SDLK_0;
+       event.key.keysym.mod = KMOD_NONE;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL key down event",
+               string("key down: window ID: 0, state: pressed, repeat: no, keysym: "
+                       "scancode: ") + to_string(int(SDL_SCANCODE_0)) + ", sym: "
+                       + to_string(int(SDLK_0)) +" (\"0\")", string_cast(event));
+       event.key.windowID = 2;
+       event.key.repeat = 1;
+       event.key.keysym.scancode = SDL_SCANCODE_BACKSPACE;
+       event.key.keysym.sym = SDLK_BACKSPACE;
+       event.key.keysym.mod = KMOD_LCTRL | KMOD_LALT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL key down event",
+               string("key down: window ID: 2, state: pressed, repeat: yes, keysym: "
+                       "scancode: ") + to_string(int(SDL_SCANCODE_BACKSPACE)) + ", sym: "
+                       + to_string(int(SDLK_BACKSPACE)) +" (\"Backspace\"), mod: LCTRL LALT", string_cast(event));
+
+       event.type = SDL_KEYUP;
+       event.key.windowID = 1;
+       event.key.state = SDL_RELEASED;
+       event.key.repeat = 0;
+       event.key.keysym.scancode = SDL_SCANCODE_SYSREQ;
+       event.key.keysym.sym = SDLK_SYSREQ;
+       event.key.keysym.mod = KMOD_LSHIFT | KMOD_RALT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL key up event",
+               string("key up: window ID: 1, state: released, repeat: no, keysym: "
+                       "scancode: ") + to_string(int(SDL_SCANCODE_SYSREQ)) + ", sym: "
+                       + to_string(int(SDLK_SYSREQ)) +" (\"SysReq\"), mod: LSHIFT RALT", string_cast(event));
+       event.key.windowID = 3;
+       event.key.repeat = 1;
+       event.key.keysym.scancode = SDL_SCANCODE_L;
+       event.key.keysym.sym = SDLK_l;
+       event.key.keysym.mod = KMOD_RSHIFT | KMOD_RCTRL | KMOD_LGUI;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL key up event",
+               string("key up: window ID: 3, state: released, repeat: yes, keysym: "
+                       "scancode: ") + to_string(int(SDL_SCANCODE_L)) + ", sym: "
+                       + to_string(int(SDLK_l)) +" (\"L\"), mod: RSHIFT RCTRL LSUPER", string_cast(event));
+       event.key.windowID = 4;
+       event.key.repeat = 2;
+       event.key.keysym.scancode = SDL_SCANCODE_VOLUMEUP;
+       event.key.keysym.sym = SDLK_VOLUMEUP;
+       event.key.keysym.mod = KMOD_RGUI | KMOD_NUM | KMOD_CAPS | KMOD_MODE;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL key up event",
+               string("key up: window ID: 4, state: released, repeat: yes, keysym: "
+                       "scancode: ") + to_string(int(SDL_SCANCODE_VOLUMEUP)) + ", sym: "
+                       + to_string(int(SDLK_VOLUMEUP)) +" (\"VolumeUp\"), mod: RSUPER NUM CAPS ALTGR", string_cast(event));
+}
+
+void EventTest::testJoystick() {
+       SDL_Event event;
+       event.type = SDL_JOYAXISMOTION;
+       event.jaxis.which = 0;
+       event.jaxis.axis = 1;
+       event.jaxis.value = 16384;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick axis motion event",
+               string("joystick axis motion: joystick ID: 0, axis ID: 1, value: 0.5"), string_cast(event));
+
+       event.type = SDL_JOYBALLMOTION;
+       event.jball.which = 2;
+       event.jball.ball = 3;
+       event.jball.xrel = 4;
+       event.jball.yrel = 5;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick ball motion event",
+               string("joystick ball motion: joystick ID: 2, ball ID: 3, delta: 4 5"), string_cast(event));
+
+       event.type = SDL_JOYHATMOTION;
+       event.jhat.which = 6;
+       event.jhat.hat = 7;
+       event.jhat.value = SDL_HAT_LEFTUP;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: left up"), string_cast(event));
+       event.jhat.value = SDL_HAT_UP;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: up"), string_cast(event));
+       event.jhat.value = SDL_HAT_RIGHTUP;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: right up"), string_cast(event));
+       event.jhat.value = SDL_HAT_LEFT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: left"), string_cast(event));
+       event.jhat.value = SDL_HAT_CENTERED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: center"), string_cast(event));
+       event.jhat.value = SDL_HAT_RIGHT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: right"), string_cast(event));
+       event.jhat.value = SDL_HAT_LEFTDOWN;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: left down"), string_cast(event));
+       event.jhat.value = SDL_HAT_DOWN;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: down"), string_cast(event));
+       event.jhat.value = SDL_HAT_RIGHTDOWN;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: right down"), string_cast(event));
+       event.jhat.value = -1;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick hat motion event",
+               string("joystick hat motion: joystick ID: 6, hat ID: 7, value: unknown"), string_cast(event));
+
+       event.type = SDL_JOYBUTTONDOWN;
+       event.jbutton.which = 8;
+       event.jbutton.button = 9;
+       event.jbutton.state = SDL_PRESSED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick button down event",
+               string("joystick button down: joystick ID: 8, button ID: 9, state: pressed"), string_cast(event));
+       event.type = SDL_JOYBUTTONUP;
+       event.jbutton.which = 10;
+       event.jbutton.button = 11;
+       event.jbutton.state = SDL_RELEASED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick button up event",
+               string("joystick button up: joystick ID: 10, button ID: 11, state: released"), string_cast(event));
+
+       event.type = SDL_JOYDEVICEADDED;
+       event.jdevice.which = 12;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick device added event",
+               string("joystick device added: joystick ID: 12"), string_cast(event));
+       event.type = SDL_JOYDEVICEREMOVED;
+       event.jdevice.which = 13;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL joystick device removed event",
+               string("joystick device removed: joystick ID: 13"), string_cast(event));
+}
+
+void EventTest::testMouse() {
+       SDL_Event event;
+       event.type = SDL_MOUSEMOTION;
+       event.motion.windowID = 0;
+       event.motion.which = 1;
+       event.motion.x = 2;
+       event.motion.y = 3;
+       event.motion.xrel = 4;
+       event.motion.yrel = 5;
+       event.motion.state = 0;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse motion event",
+               string("mouse motion: window ID: 0, mouse ID: 1, position: 2 3, delta: 4 5"), string_cast(event));
+       event.motion.windowID = 6;
+       event.motion.which = 7;
+       event.motion.x = 8;
+       event.motion.y = 9;
+       event.motion.xrel = 10;
+       event.motion.yrel = 11;
+       event.motion.state = SDL_BUTTON_LMASK | SDL_BUTTON_MMASK | SDL_BUTTON_RMASK;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse motion event",
+               string("mouse motion: window ID: 6, mouse ID: 7, position: 8 9, delta: 10 11, buttons: left middle right"), string_cast(event));
+       event.motion.state = SDL_BUTTON_X1MASK | SDL_BUTTON_X2MASK;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse motion event",
+               string("mouse motion: window ID: 6, mouse ID: 7, position: 8 9, delta: 10 11, buttons: X1 X2"), string_cast(event));
+
+       event.type = SDL_MOUSEBUTTONDOWN;
+       event.button.windowID = 0;
+       event.button.which = 1;
+       event.button.button = SDL_BUTTON_LEFT;
+       event.button.state = SDL_PRESSED;
+       event.button.clicks = 2;
+       event.button.x = 3;
+       event.button.y = 4;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button down event",
+               string("mouse button down: window ID: 0, mouse ID: 1, button: left, state: pressed, clicks: 2, position: 3 4"), string_cast(event));
+       event.type = SDL_MOUSEBUTTONUP;
+       event.button.windowID = 5;
+       event.button.which = 6;
+       event.button.button = SDL_BUTTON_MIDDLE;
+       event.button.clicks = 7;
+       event.button.x = 8;
+       event.button.y = 9;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button up event",
+               string("mouse button up: window ID: 5, mouse ID: 6, button: middle, state: pressed, clicks: 7, position: 8 9"), string_cast(event));
+       event.button.button = SDL_BUTTON_RIGHT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button up event",
+               string("mouse button up: window ID: 5, mouse ID: 6, button: right, state: pressed, clicks: 7, position: 8 9"), string_cast(event));
+       event.button.button = SDL_BUTTON_X1;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button up event",
+               string("mouse button up: window ID: 5, mouse ID: 6, button: X1, state: pressed, clicks: 7, position: 8 9"), string_cast(event));
+       event.button.button = SDL_BUTTON_X2;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button up event",
+               string("mouse button up: window ID: 5, mouse ID: 6, button: X2, state: pressed, clicks: 7, position: 8 9"), string_cast(event));
+       event.button.button = SDL_BUTTON_X2 + 1;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse button up event",
+               string("mouse button up: window ID: 5, mouse ID: 6, button: ") + to_string(int(SDL_BUTTON_X2 + 1)) + ", state: pressed, clicks: 7, position: 8 9", string_cast(event));
+
+       event.type = SDL_MOUSEWHEEL;
+       event.wheel.windowID = 0;
+       event.wheel.which = 1;
+       event.wheel.x = 2;
+       event.wheel.y = 3;
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+       event.wheel.direction = SDL_MOUSEWHEEL_NORMAL;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse wheel event",
+               string("mouse wheel: window ID: 0, mouse ID: 1, delta: 2 3, direction: normal"), string_cast(event));
+       event.wheel.windowID = 4;
+       event.wheel.which = 5;
+       event.wheel.x = 6;
+       event.wheel.y = 7;
+       event.wheel.direction = SDL_MOUSEWHEEL_FLIPPED;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse wheel event",
+               string("mouse wheel: window ID: 4, mouse ID: 5, delta: 6 7, direction: flipped"), string_cast(event));
+#else
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL mouse wheel event",
+               string("mouse wheel: window ID: 0, mouse ID: 1, delta: 2 3"), string_cast(event));
+#endif
+}
+
+void EventTest::testMultiGesture() {
+       SDL_Event event;
+       event.type = SDL_MULTIGESTURE;
+       event.mgesture.touchId = 0;
+       event.mgesture.dTheta = 1;
+       event.mgesture.dDist = 2;
+       event.mgesture.x = 3;
+       event.mgesture.y = 4;
+       event.mgesture.numFingers = 5;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL multi gesture event",
+               string("multi gesture: device ID: 0, theta: 1, distance: 2, position: 3 4, fingers: 5"), string_cast(event));
+}
+
+void EventTest::testQuit() {
+       SDL_Event event;
+       event.type = SDL_QUIT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL quit event",
+               string("quit: quit"), string_cast(event));
+}
+
+void EventTest::testSysWM() {
+       SDL_Event event;
+       event.type = SDL_SYSWMEVENT;
+       event.syswm.msg = nullptr;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL sys wm event",
+               string("sys wm: without message"), string_cast(event));
+       SDL_SysWMmsg msg;
+       event.syswm.msg = &msg;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL sys wm event",
+               string("sys wm: with message"), string_cast(event));
+}
+
+void EventTest::testText() {
+       SDL_Event event;
+       event.type = SDL_TEXTEDITING;
+       event.edit.windowID = 0;
+       event.edit.text[0] = '\303';
+       event.edit.text[1] = '\244';
+       event.edit.text[2] = '\0';
+       event.edit.start = 1;
+       event.edit.length = 2;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL text editing event",
+               string("text editing: window ID: 0, text: \"ä\", start: 1, length: 2"), string_cast(event));
+
+       event.type = SDL_TEXTINPUT;
+       event.text.windowID = 3;
+       event.text.text[0] = '\0';
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL text input event",
+               string("text input: window ID: 3, text: \"\""), string_cast(event));
+}
+
+void EventTest::testUser() {
+       SDL_Event event;
+       event.type = SDL_USEREVENT;
+       event.user.windowID = 0;
+       event.user.code = 1;
+       event.user.data1 = nullptr;
+       event.user.data2 = reinterpret_cast<void *>(1);
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL user event",
+               string("user: window ID: 0, code: 1, data 1: 0, data 2: 0x1"), string_cast(event));
+}
+
+void EventTest::testWindow() {
+       SDL_Event event;
+       event.type = SDL_WINDOWEVENT;
+       event.window.event = SDL_WINDOWEVENT_SHOWN;
+       event.window.windowID = 0;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: shown, window ID: 0"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_HIDDEN;
+       event.window.windowID = 1;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: hidden, window ID: 1"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_EXPOSED;
+       event.window.windowID = 2;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: exposed, window ID: 2"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_MOVED;
+       event.window.windowID = 3;
+       event.window.data1 = 4;
+       event.window.data2 = 5;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: moved, window ID: 3, position: 4 5"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_RESIZED;
+       event.window.windowID = 6;
+       event.window.data1 = 7;
+       event.window.data2 = 8;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: resized, window ID: 6, size: 7x8"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_SIZE_CHANGED;
+       event.window.windowID = 9;
+       event.window.data1 = 10;
+       event.window.data2 = 11;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: size changed, window ID: 9, size: 10x11"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_MINIMIZED;
+       event.window.windowID = 12;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: minimized, window ID: 12"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_MAXIMIZED;
+       event.window.windowID = 13;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: maximized, window ID: 13"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_RESTORED;
+       event.window.windowID = 14;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: restored, window ID: 14"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_ENTER;
+       event.window.windowID = 15;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: mouse entered, window ID: 15"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_LEAVE;
+       event.window.windowID = 16;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: mouse left, window ID: 16"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_FOCUS_GAINED;
+       event.window.windowID = 17;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: focus gained, window ID: 17"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_FOCUS_LOST;
+       event.window.windowID = 18;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: focus lost, window ID: 18"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_CLOSE;
+       event.window.windowID = 19;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: closed, window ID: 19"), string_cast(event));
+
+       event.window.event = SDL_WINDOWEVENT_NONE;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL window event",
+               string("window: unknown"), string_cast(event));
+}
+
+void EventTest::testUnknown() {
+       SDL_Event event;
+       // SDL_LASTEVENT holds the number of entries in the enum and therefore
+       // shouldn't be recognized as any valid event type
+       event.type = SDL_LASTEVENT;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "output format of SDL user event",
+               string("unknown"), string_cast(event));
+}
+
+}
+}
+}
diff --git a/tst/io/EventTest.hpp b/tst/io/EventTest.hpp
new file mode 100644 (file)
index 0000000..d273f1f
--- /dev/null
@@ -0,0 +1,68 @@
+#ifndef GONG_TEST_IO_EVENTTEST_HPP
+#define GONG_TEST_IO_EVENTTEST_HPP
+
+#include <cppunit/extensions/HelperMacros.h>
+
+#include <SDL_version.h>
+
+namespace gong {
+namespace io {
+namespace test {
+
+class EventTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(EventTest);
+
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+CPPUNIT_TEST(testAudioDevice);
+#endif
+
+CPPUNIT_TEST(testController);
+CPPUNIT_TEST(testDollar);
+CPPUNIT_TEST(testDrop);
+CPPUNIT_TEST(testFinger);
+CPPUNIT_TEST(testKey);
+CPPUNIT_TEST(testJoystick);
+CPPUNIT_TEST(testMouse);
+CPPUNIT_TEST(testMultiGesture);
+CPPUNIT_TEST(testQuit);
+CPPUNIT_TEST(testSysWM);
+CPPUNIT_TEST(testText);
+CPPUNIT_TEST(testUser);
+CPPUNIT_TEST(testWindow);
+CPPUNIT_TEST(testUnknown);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+#if SDL_VERSION_ATLEAST(2, 0, 4)
+       void testAudioDevice();
+#endif
+
+       void testController();
+       void testDollar();
+       void testDrop();
+       void testFinger();
+       void testKey();
+       void testJoystick();
+       void testMouse();
+       void testMultiGesture();
+       void testQuit();
+       void testSysWM();
+       void testText();
+       void testUser();
+       void testWindow();
+       void testUnknown();
+
+};
+
+}
+}
+}
+
+
+#endif
diff --git a/tst/io/FilesystemTest.cpp b/tst/io/FilesystemTest.cpp
new file mode 100644 (file)
index 0000000..c754a32
--- /dev/null
@@ -0,0 +1,160 @@
+#include "FilesystemTest.hpp"
+
+#include "io/filesystem.hpp"
+
+#include <algorithm>
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::io::test::FilesystemTest);
+
+using namespace std;
+
+
+namespace gong {
+namespace io {
+namespace test {
+
+void FilesystemTest::setUp() {
+       test_dir.reset(new TempDir());
+}
+
+void FilesystemTest::tearDown() {
+       test_dir.reset();
+}
+
+
+void FilesystemTest::testFile() {
+#ifdef _WIN32
+       const string test_file = test_dir->Path() + "\\test-file.txt";
+#else
+       const string test_file = test_dir->Path() + "/test-file";
+#endif
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "inexistant file is file",
+               !is_file(test_file));
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "mtime of inexistant file should be zero",
+               time_t(0), file_mtime(test_file));
+       CPPUNIT_ASSERT_MESSAGE(
+               "inexistant file is a directory",
+               !is_dir(test_file));
+
+       { // create file
+               ofstream file(test_file);
+               file << "hello" << endl;
+       }
+       time_t now = time(nullptr);
+       CPPUNIT_ASSERT_MESSAGE(
+               "existing file not a file",
+               is_file(test_file));
+       CPPUNIT_ASSERT_MESSAGE(
+               "mtime of existing file should be somewhere around now",
+               // let's assume that creating the file takes less than five seconds
+               abs(now - file_mtime(test_file) < 5));
+       CPPUNIT_ASSERT_MESSAGE(
+               "regular file is a directory",
+               !is_dir(test_file));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to remove test file",
+               remove_file(test_file));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed file is still a file",
+               !is_file(test_file));
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "mtime of removed file should be zero",
+               time_t(0), file_mtime(test_file));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed file became a directory",
+               !is_dir(test_file));
+}
+
+void FilesystemTest::testDirectory() {
+#ifdef _WIN32
+       const string test_subdir = test_dir->Path() + "\\a";
+       const string test_subsubdir = test_subdir + "\\b";
+       const string test_file = test_subsubdir + "\\c.txt";
+#else
+       const string test_subdir = test_dir->Path() + "/a";
+       const string test_subsubdir = test_subdir + "/b";
+       const string test_file = test_subsubdir + "/c";
+#endif
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "inexistant directory is a file",
+               !is_file(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "inexistant directory is a directory",
+               !is_dir(test_subdir));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to create test subdir",
+               make_dir(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is a file",
+               !is_file(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is not a directory",
+               is_dir(test_subdir));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to remove test subdir",
+               remove_dir(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory became a file",
+               !is_file(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory is still a directory",
+               !is_dir(test_subdir));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to create test subdirs",
+               make_dirs(test_subsubdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is a file",
+               !is_file(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is not a directory",
+               is_dir(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is a file",
+               !is_file(test_subsubdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "created directory is not a directory",
+               is_dir(test_subsubdir));
+
+       { // create file
+               ofstream file(test_file);
+               file << "hello" << endl;
+       }
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to create test file",
+               is_file(test_file));
+
+       CPPUNIT_ASSERT_MESSAGE(
+               "failed to remove test subdir",
+               remove_dir(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory became a file",
+               !is_file(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory is still a directory",
+               !is_dir(test_subdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory became a file",
+               !is_file(test_subsubdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed directory is still a directory",
+               !is_dir(test_subsubdir));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed file became a directory",
+               !is_dir(test_file));
+       CPPUNIT_ASSERT_MESSAGE(
+               "removed file is still a file",
+               !is_file(test_file));
+}
+
+}
+}
+}
diff --git a/tst/io/FilesystemTest.hpp b/tst/io/FilesystemTest.hpp
new file mode 100644 (file)
index 0000000..b484475
--- /dev/null
@@ -0,0 +1,41 @@
+#ifndef GONG_TEST_IO_FILESYSTEMTEST_HPP
+#define GONG_TEST_IO_FILESYSTEMTEST_HPP
+
+#include "io/filesystem.hpp"
+
+#include <memory>
+#include <string>
+#include <cppunit/extensions/HelperMacros.h>
+
+
+namespace gong {
+namespace io {
+namespace test {
+
+class FilesystemTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(FilesystemTest);
+
+CPPUNIT_TEST(testFile);
+CPPUNIT_TEST(testDirectory);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testFile();
+       void testDirectory();
+
+private:
+       std::unique_ptr<TempDir> test_dir;
+
+};
+
+}
+}
+}
+
+#endif
diff --git a/tst/io/TokenTest.cpp b/tst/io/TokenTest.cpp
new file mode 100644 (file)
index 0000000..f4a39f7
--- /dev/null
@@ -0,0 +1,490 @@
+#include "TokenTest.hpp"
+
+#include "io/TokenStreamReader.hpp"
+
+#include <sstream>
+#include <stdexcept>
+#include <glm/gtx/io.hpp>
+
+CPPUNIT_TEST_SUITE_REGISTRATION(gong::io::test::TokenTest);
+
+using namespace std;
+
+namespace gong {
+namespace io {
+namespace test {
+
+void TokenTest::setUp() {
+
+}
+
+void TokenTest::tearDown() {
+
+}
+
+
+void TokenTest::testTypeIO() {
+       AssertStreamOutput(Token::UNKNOWN, "UNKNOWN");
+       AssertStreamOutput(Token::ANGLE_BRACKET_OPEN, "ANGLE_BRACKET_OPEN");
+       AssertStreamOutput(Token::ANGLE_BRACKET_CLOSE, "ANGLE_BRACKET_CLOSE");
+       AssertStreamOutput(Token::CHEVRON_OPEN, "CHEVRON_OPEN");
+       AssertStreamOutput(Token::CHEVRON_CLOSE, "CHEVRON_CLOSE");
+       AssertStreamOutput(Token::BRACKET_OPEN, "BRACKET_OPEN");
+       AssertStreamOutput(Token::BRACKET_CLOSE, "BRACKET_CLOSE");
+       AssertStreamOutput(Token::PARENTHESIS_OPEN, "PARENTHESIS_OPEN");
+       AssertStreamOutput(Token::PARENTHESIS_CLOSE, "PARENTHESIS_CLOSE");
+       AssertStreamOutput(Token::COLON, "COLON");
+       AssertStreamOutput(Token::SEMICOLON, "SEMICOLON");
+       AssertStreamOutput(Token::COMMA, "COMMA");
+       AssertStreamOutput(Token::EQUALS, "EQUALS");
+       AssertStreamOutput(Token::NUMBER, "NUMBER");
+       AssertStreamOutput(Token::STRING, "STRING");
+       AssertStreamOutput(Token::IDENTIFIER, "IDENTIFIER");
+       AssertStreamOutput(Token::COMMENT, "COMMENT");
+}
+
+void TokenTest::testTokenIO() {
+       Token t;
+       t.value = "why oh why";
+       AssertStreamOutput(t, "UNKNOWN(why oh why)");
+       t.type = Token::UNKNOWN;
+       t.value = "do I have no purpose";
+       AssertStreamOutput(t, "UNKNOWN(do I have no purpose)");
+       t.type = Token::ANGLE_BRACKET_OPEN;
+       AssertStreamOutput(t, "ANGLE_BRACKET_OPEN");
+       t.type = Token::ANGLE_BRACKET_CLOSE;
+       AssertStreamOutput(t, "ANGLE_BRACKET_CLOSE");
+       t.type = Token::CHEVRON_OPEN;
+       AssertStreamOutput(t, "CHEVRON_OPEN");
+       t.type = Token::CHEVRON_CLOSE;
+       AssertStreamOutput(t, "CHEVRON_CLOSE");
+       t.type = Token::BRACKET_OPEN;
+       AssertStreamOutput(t, "BRACKET_OPEN");
+       t.type = Token::BRACKET_CLOSE;
+       AssertStreamOutput(t, "BRACKET_CLOSE");
+       t.type = Token::PARENTHESIS_OPEN;
+       AssertStreamOutput(t, "PARENTHESIS_OPEN");
+       t.type = Token::PARENTHESIS_CLOSE;
+       AssertStreamOutput(t, "PARENTHESIS_CLOSE");
+       t.type = Token::COLON;
+       AssertStreamOutput(t, "COLON");
+       t.type = Token::SEMICOLON;
+       AssertStreamOutput(t, "SEMICOLON");
+       t.type = Token::COMMA;
+       AssertStreamOutput(t, "COMMA");
+       t.type = Token::EQUALS;
+       AssertStreamOutput(t, "EQUALS");
+       t.type = Token::NUMBER;
+       t.value = "15";
+       AssertStreamOutput(t, "NUMBER(15)");
+       t.type = Token::STRING;
+       t.value = "hello world";
+       AssertStreamOutput(t, "STRING(hello world)");
+       t.type = Token::IDENTIFIER;
+       t.value = "foo";
+       AssertStreamOutput(t, "IDENTIFIER(foo)");
+       t.type = Token::COMMENT;
+       t.value = "WITHOUT ANY WARRANTY";
+       AssertStreamOutput(t, "COMMENT(WITHOUT ANY WARRANTY)");
+}
+
+void TokenTest::testTokenizer() {
+       stringstream stream;
+       stream << "[{0},<.5>+3=/**\n * test\n */ (-1.5); foo_bar.baz:\"hello\\r\\n\\t\\\"world\\\"\" ] // this line\n#that line";
+       Tokenizer in(stream);
+
+       AssertHasMore(in);
+       Token token(in.Next());
+       AssertToken(token.type, token.value, in.Current());
+       AssertToken(Token::BRACKET_OPEN, token);
+
+       AssertHasMore(in);
+       AssertToken(Token::ANGLE_BRACKET_OPEN, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::NUMBER, "0", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::ANGLE_BRACKET_CLOSE, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::COMMA, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::CHEVRON_OPEN, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::NUMBER, ".5", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::CHEVRON_CLOSE, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::NUMBER, "+3", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::EQUALS, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::COMMENT, "*\n * test\n ", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::PARENTHESIS_OPEN, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::NUMBER, "-1.5", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::PARENTHESIS_CLOSE, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::SEMICOLON, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::IDENTIFIER, "foo_bar.baz", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::COLON, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::STRING, "hello\r\n\t\"world\"", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::BRACKET_CLOSE, in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::COMMENT, " this line", in.Next());
+       AssertHasMore(in);
+       AssertToken(Token::COMMENT, "that line", in.Next());
+       CPPUNIT_ASSERT_MESSAGE("expected end of stream", !in.HasMore());
+       CPPUNIT_ASSERT_THROW_MESSAGE(
+               "extracting token after EOS",
+               in.Next(), std::runtime_error);
+}
+
+void TokenTest::testTokenizerBrokenComment() {
+       {
+               stringstream stream;
+               stream << "/* just one more thing…*";
+               Tokenizer in(stream);
+               AssertHasMore(in);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "half-closed comment should throw",
+                       in.Next(), std::runtime_error);
+       }
+       {
+               stringstream stream;
+               stream << "  /";
+               Tokenizer in(stream);
+               AssertHasMore(in);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "sole '/' at end of stream should throw",
+                       in.Next(), std::runtime_error);
+       }
+       {
+               stringstream stream;
+               stream << "/.";
+               Tokenizer in(stream);
+               AssertHasMore(in);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "'/' followed by garbage should throw",
+                       in.Next(), std::runtime_error);
+       }
+}
+
+
+namespace {
+
+template<class T>
+void assert_read(std::string message, T expected, T actual, TokenStreamReader &in) {
+       stringstream msg;
+       msg << message << ", current token: " << in.Peek();
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               msg.str(),
+               expected, actual);
+}
+
+}
+
+void TokenTest::testReader() {
+       stringstream ss;
+       ss <<
+               "/* booleans */\n"
+               "true false yes no on off\n"
+               "\"true\" \"false\" \"yes\" \"no\" \"on\" \"off\"\n"
+               "1 0 -1\n"
+               "# identifiers\n"
+               "foo foo_bar vec.y\n"
+               "// numbers\n"
+               "0 1 +2 -3 4.5\n"
+               ".5 1.5 0.25 -1.75 0.625\n"
+               "0 1 -1 2.5\n"
+               // strings
+               "\"hello\" \"\" \"\\r\\n\\t\\\"\"\n"
+               "\"world\" foo 12\n"
+               // vectors
+               "[1,0] [ 0.707, 0.707 ] // vec2\n"
+               "[.577,.577 ,0.577] [ 1,-2,3] // vec3\n"
+               "[ 0, 0, 0, 1 ] [1,0,0,-1.0] // vec4\n"
+               "[640, 480] [3, 4, 5] [0, -10, 100, -1000] # ivecs\n"
+               "[ -0.945, 0, -0.326, 0] # quat\n"
+               ;
+       TokenStreamReader in(ss);
+
+       // booleans
+
+       bool value_bool;
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean true", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean false", false, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean yes", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean no", false, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean on", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean off", false, value_bool, in);
+
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"true\"", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"false\"", false, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"yes\"", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"no\"", false, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"on\"", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean \"off\"", false, value_bool, in);
+
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean 1", true, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean 0", false, value_bool, in);
+       in.ReadBoolean(value_bool);
+       assert_read("reading boolean -1", true, value_bool, in);
+
+       // identifiers
+
+       string value_ident;
+       in.ReadIdentifier(value_ident);
+       assert_read<string>("reading identifier foo", "foo", value_ident, in);
+       in.ReadIdentifier(value_ident);
+       assert_read<string>("reading identifier foo_bar", "foo_bar", value_ident, in);
+       in.ReadIdentifier(value_ident);
+       assert_read<string>("reading identifier vec.y", "vec.y", value_ident, in);
+
+       // numbers
+       int value_int;
+       in.ReadNumber(value_int);
+       assert_read("reading integer 0", 0, value_int, in);
+       in.ReadNumber(value_int);
+       assert_read("reading integer 1", 1, value_int, in);
+       in.ReadNumber(value_int);
+       assert_read("reading integer +2", 2, value_int, in);
+       in.ReadNumber(value_int);
+       assert_read("reading integer -3", -3, value_int, in);
+       in.ReadNumber(value_int);
+       assert_read("reading integer 4.5", 4, value_int, in);
+
+       float value_float;
+       in.ReadNumber(value_float);
+       assert_read("reading float .5", .5f, value_float, in);
+       in.ReadNumber(value_float);
+       assert_read("reading float 1.5", 1.5f, value_float, in);
+       in.ReadNumber(value_float);
+       assert_read("reading float 0.25", .25f, value_float, in);
+       in.ReadNumber(value_float);
+       assert_read("reading float -1.75", -1.75f, value_float, in);
+       in.ReadNumber(value_float);
+       assert_read("reading float 0.625", 0.625f, value_float, in);
+
+       unsigned long value_uint;
+       in.ReadNumber(value_uint);
+       assert_read("reading unsigned integer 0", 0ul, value_uint, in);
+       in.ReadNumber(value_uint);
+       assert_read("reading unsigned integer 1", 1ul, value_uint, in);
+       in.ReadNumber(value_uint);
+       assert_read("reading unsigned integer -1", -1ul, value_uint, in);
+       in.ReadNumber(value_uint);
+       assert_read("reading unsigned integer 2.5", 2ul, value_uint, in);
+
+       // strings
+
+       string value_string;
+       in.ReadString(value_string);
+       assert_read<string>(
+               "reading string \"hello\"",
+               "hello", value_string, in);
+       in.ReadString(value_string);
+       assert_read<string>(
+               "reading string \"\"",
+               "", value_string, in);
+       in.ReadString(value_string);
+       assert_read<string>(
+               "reading string \"\\r\\n\\t\\\"\"",
+               "\r\n\t\"", value_string, in);
+
+       in.ReadRelaxedString(value_string);
+       assert_read<string>(
+               "reading relaxed string \"world\"",
+               "world", value_string, in);
+
+       in.ReadRelaxedString(value_string);
+       assert_read<string>(
+               "reading relaxed string foo",
+               "foo", value_string, in);
+
+       in.ReadRelaxedString(value_string);
+       assert_read<string>(
+               "reading relaxed string 12",
+               "12", value_string, in);
+
+       // vectors
+
+       glm::vec2 value_vec2;
+       in.ReadVec(value_vec2);
+       assert_read(
+               "reading vector [1,0]",
+               glm::vec2(1, 0), value_vec2, in);
+       in.ReadVec(value_vec2);
+       assert_read(
+               "reading vector [ 0.707, 0.707 ]",
+               glm::vec2(.707, .707), value_vec2, in);
+
+       glm::vec3 value_vec3;
+       in.ReadVec(value_vec3);
+       assert_read(
+               "reading vector [.577,.577 ,0.577]",
+               glm::vec3(.577, .577, .577), value_vec3, in);
+       in.ReadVec(value_vec3);
+       assert_read(
+               "reading vector [ 1,-2,3]",
+               glm::vec3(1, -2, 3), value_vec3, in);
+
+       glm::vec4 value_vec4;
+       in.ReadVec(value_vec4);
+       assert_read(
+               "reading vector [ 0, 0, 0, 1 ]",
+               glm::vec4(0, 0, 0, 1), value_vec4, in);
+       in.ReadVec(value_vec4);
+       assert_read(
+               "reading vector [1,0,0,-1.0]",
+               glm::vec4(1, 0, 0, -1), value_vec4, in);
+
+       glm::ivec2 value_ivec2;
+       in.ReadVec(value_ivec2);
+       assert_read(
+               "reading integer vector [640, 480]",
+               glm::ivec2(640, 480), value_ivec2, in);
+       glm::ivec3 value_ivec3;
+       in.ReadVec(value_ivec3);
+       assert_read(
+               "reading integer vector [3, 4, 5]",
+               glm::ivec3(3, 4, 5), value_ivec3, in);
+       glm::ivec4 value_ivec4;
+       in.ReadVec(value_ivec4);
+       assert_read(
+               "reading integer vector [0, -10, 100, -1000]",
+               glm::ivec4(0, -10, 100, -1000), value_ivec4, in);
+
+       glm::quat value_quat;
+       in.ReadQuat(value_quat);
+       assert_read(
+               "reading quaternion [ -0.945, 0, -0.326, 0]",
+               glm::quat(-0.945, 0, -0.326, 0), value_quat, in);
+       // TODO: comment at end of stream makes it think there's more?
+       //CPPUNIT_ASSERT_MESSAGE("expected end of stream", !in.HasMore());
+       // TODO: and it even works??
+       //CPPUNIT_ASSERT_THROW_MESSAGE(
+       //      "extracting token after EOS",
+       //      in.Next(), std::runtime_error);
+}
+
+void TokenTest::testReaderEmpty() {
+       { // zero length stream
+               stringstream ss;
+               ss << "";
+               TokenStreamReader in(ss);
+               CPPUNIT_ASSERT_MESSAGE(
+                       "empty stream shouldn't have tokens",
+                       !in.HasMore());
+       }
+       { // stream consisting solely of comments
+               stringstream ss;
+               ss <<
+                       "/*\n"
+                       " * hello\n"
+                       " */\n"
+                       "#hello\n"
+                       "// is there anybody out there\n"
+                       ;
+               TokenStreamReader in(ss);
+               CPPUNIT_ASSERT_MESSAGE(
+                       "comment stream shouldn't have tokens",
+                       !in.HasMore());
+       }
+}
+
+void TokenTest::testReaderMalformed() {
+       {
+               stringstream ss;
+               ss << "a";
+               TokenStreamReader in(ss);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "unexpected token type should throw",
+                       in.GetInt(), std::runtime_error);
+       }
+       {
+               stringstream ss;
+               ss << ":";
+               TokenStreamReader in(ss);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "casting ':' to bool should throw",
+                       in.GetBool(), std::runtime_error);
+       }
+       {
+               stringstream ss;
+               ss << "hello";
+               TokenStreamReader in(ss);
+               CPPUNIT_ASSERT_THROW_MESSAGE(
+                       "casting \"hello\" to bool should throw",
+                       in.GetBool(), std::runtime_error);
+       }
+}
+
+
+void TokenTest::AssertStreamOutput(
+       Token::Type t,
+       string expected
+) {
+       stringstream conv;
+       conv << t;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "unexpected std::ostream << Token::Type result",
+               expected, conv.str());
+}
+
+void TokenTest::AssertStreamOutput(
+       const Token &t,
+       string expected
+) {
+       stringstream conv;
+       conv << t;
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "unexpected std::ostream << Token result",
+               expected, conv.str());
+}
+
+void TokenTest::AssertHasMore(Tokenizer &in) {
+       CPPUNIT_ASSERT_MESSAGE("unexpected end of stream", in.HasMore());
+}
+
+void TokenTest::AssertToken(
+       Token::Type expected_type,
+       const Token &actual_token
+) {
+       AssertToken(expected_type, "", actual_token);
+}
+
+void TokenTest::AssertToken(
+       Token::Type expected_type,
+       string expected_value,
+       const Token &actual_token
+) {
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "unexpected token type",
+               expected_type, actual_token.type);
+       CPPUNIT_ASSERT_EQUAL_MESSAGE(
+               "unexpected token value",
+               expected_value, actual_token.value);
+}
+
+}
+}
+}
diff --git a/tst/io/TokenTest.hpp b/tst/io/TokenTest.hpp
new file mode 100644 (file)
index 0000000..da705f4
--- /dev/null
@@ -0,0 +1,61 @@
+#ifndef GONG_TEST_IO_TOKENTEST_HPP
+#define GONG_TEST_IO_TOKENTEST_HPP
+
+#include "io/Token.hpp"
+#include "io/Tokenizer.hpp"
+
+#include <string>
+#include <cppunit/extensions/HelperMacros.h>
+
+
+namespace gong {
+namespace io {
+namespace test {
+
+class TokenTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(TokenTest);
+
+CPPUNIT_TEST(testTypeIO);
+CPPUNIT_TEST(testTokenIO);
+CPPUNIT_TEST(testTokenizer);
+CPPUNIT_TEST(testTokenizerBrokenComment);
+CPPUNIT_TEST(testReader);
+CPPUNIT_TEST(testReaderEmpty);
+CPPUNIT_TEST(testReaderMalformed);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testTypeIO();
+       void testTokenIO();
+       void testTokenizer();
+       void testTokenizerBrokenComment();
+
+       void testReader();
+       void testReaderEmpty();
+       void testReaderMalformed();
+
+       static void AssertStreamOutput(
+               Token::Type, std::string expected);
+       static void AssertStreamOutput(
+               const Token &, std::string expected);
+
+       static void AssertHasMore(Tokenizer &);
+       static void AssertToken(
+               Token::Type expected_type, const Token &actual_token);
+       static void AssertToken(
+               Token::Type expected_type, std::string expected_value,
+               const Token &actual_token);
+
+};
+
+}
+}
+}
+
+#endif