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