--- /dev/null
+*.swp
+*.swo
+*.trace
+build
+cachegrind.out.*
+callgrind.out.*
+client-saves
+gong
+gong.*
+saves
+test.*
--- /dev/null
+[submodule "assets"]
+ path = assets
+ url = http://git.localhorst.tv/repo/gong-assets.git
--- /dev/null
+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 "$@"
--- /dev/null
+Subproject commit d6ad42354623df0d3466f3684797eac2ea93ac05
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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);
+ }
+}
+
+}
+}
--- /dev/null
+#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)) {
+
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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() {
+
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+ }
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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);
+ }
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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;
+}
+}
+
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
+ );
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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;
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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);
+}
+
+}
+}
--- /dev/null
+#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
+ );
+}
+
+}
+}
--- /dev/null
+#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);
+}
+
+}
+}
--- /dev/null
+#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))));
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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 ∈
+ Token current;
+
+};
+
+}
+}
+
+#endif
--- /dev/null
+#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;
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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;
+ }
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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();
+}
+
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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();
+ }
+}
+
+}
+}
--- /dev/null
+#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()
+ );
+}
+
+}
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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)
+ );
+}
+
+}
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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
+ );
+}
+
+}
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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));
+}
+
+}
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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));
+}
+
+}
+}
+}
--- /dev/null
+#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
--- /dev/null
+#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);
+}
+
+}
+}
+}
--- /dev/null
+#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