Kajetan Johannes Hammerle il y a 1 an
commit
5b597e8b2a

+ 3 - 0
.clangd

@@ -0,0 +1,3 @@
+CompileFlags:
+  Add: [-ferror-limit=0, -std=c2x, -DERROR_SIMULATOR=true]
+  CompilationDatabase: ./build_debug/

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+build_debug
+build_profile
+build_release
+install
+profile
+compiler
+.cache
+*.swp
+*.swo
+*.cpp
+*.hpp

+ 79 - 0
CMakeLists.txt

@@ -0,0 +1,79 @@
+cmake_minimum_required(VERSION 3.25)
+project(gaming_core)
+
+set(CMAKE_C_STANDARD 23)
+
+set(SRC
+    "src/ImageReader.c"
+    "src/Network.c"
+    "src/Window.c"
+)
+
+set(SRC_TESTS
+    "test/Main.c"
+    "test/modules/ImageReaderTests.c"
+    "test/modules/NetworkTests.c"
+)
+
+if("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
+    set(COMPILE_OPTIONS "")
+    set(LINK_OPTIONS "")
+    set(LOG_LEVEL 2)
+    set(DEFINITIONS "")
+else()
+    set(DEFINITIONS ERROR_SIMULATOR CORE_CHECK_MEMORY)
+    if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+        set(COMPILE_OPTIONS --coverage)
+        set(LINK_OPTIONS gcov)
+    elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
+        set(COMPILE_OPTIONS -fprofile-instr-generate -fcoverage-mapping)
+        set(LINK_OPTIONS ${COMPILE_OPTIONS})
+    endif()
+    set(LOG_LEVEL 4)
+endif()
+
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
+    include("cmake/gcc_warnings.cmake")
+    set(DEFINITIONS ${DEFINITIONS}
+        bool=_Bool
+        true=1
+        false=0
+        nullptr=0
+        static_assert=_Static_assert
+    )
+elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
+    include("cmake/clang_warnings.cmake")
+endif()
+
+add_library(gaming_core STATIC ${SRC})
+target_compile_options(gaming_core PUBLIC
+    ${COMPILE_OPTIONS}
+    ${WARNINGS}
+    -fdiagnostics-color=always
+)
+target_compile_definitions(gaming_core 
+    PUBLIC CORE_LOG_LEVEL=${LOG_LEVEL}
+    PRIVATE ${DEFINITIONS}
+)
+target_link_libraries(gaming_core 
+    PRIVATE core ${LINK_OPTIONS}
+)
+target_include_directories(gaming_core SYSTEM 
+    PUBLIC ${CMAKE_INSTALL_PREFIX}/include
+)
+target_link_directories(gaming_core SYSTEM 
+    PUBLIC ${CMAKE_INSTALL_PREFIX}/lib
+)
+target_sources(gaming_core PUBLIC 
+    FILE_SET HEADERS
+    BASE_DIRS include
+    FILES 
+        ./include/core/ImageReader.h
+        ./include/core/Network.h
+        ./include/core/Window.h
+)
+install(TARGETS gaming_core FILE_SET HEADERS)
+
+add_executable(test ${SRC_TESTS})
+target_link_libraries(test PRIVATE gaming_core)
+target_compile_definitions(test PRIVATE ${DEFINITIONS})

+ 60 - 0
cmake/clang_warnings.cmake

@@ -0,0 +1,60 @@
+set(WARNINGS
+    -Wall
+    -Walloca
+    -Warray-parameter
+    -Wbad-function-cast
+    -Wcast-qual
+    -Wconditional-uninitialized
+    -Wconversion
+    -Wdate-time
+    -Wdisabled-optimization
+    -Wdouble-promotion
+    -Wenum-compare
+    -Wenum-conversion
+    -Werror
+    -Wextra
+    -Wextra-semi-stmt
+    -Wfloat-equal
+    -Wformat=2
+    -Wframe-larger-than=8388608
+    -Winfinite-recursion
+    -Winit-self
+    -Winvalid-pch
+    -Wlarger-than=1073741824
+    -Wmissing-braces
+    -Wmissing-declarations
+    -Wmissing-include-dirs
+    -Wmissing-noreturn 
+    -Wmissing-prototypes
+    -Wmissing-variable-declarations
+    -Wmultichar
+    -Wnarrowing
+    -Wnested-externs
+    -Wnull-dereference
+    -Wold-style-definition
+    -Woverlength-strings
+    -Wredundant-decls
+    -Wshadow
+    -Wsign-conversion
+    -Wstack-protector
+    -Wstrict-overflow=2
+    -Wstrict-prototypes
+    -Wswitch-enum
+    -Wundef
+    -Wunreachable-code
+    -Wvla
+    -Wwrite-strings
+    -pedantic
+    -pedantic-errors
+)
+
+if(0)
+    set(WARNINGS ${WARNINGS} 
+        -Weverything
+        -Wno-unsafe-buffer-usage
+        -Wno-c++98-compat
+        -Wno-declaration-after-statement
+        -Wno-pre-c2x-compat
+        -Wno-padded
+    )
+endif()

+ 70 - 0
cmake/gcc_warnings.cmake

@@ -0,0 +1,70 @@
+set(WARNINGS
+    -Wno-attributes
+
+    -Wall
+    -Walloc-zero
+    -Walloca
+    -Wanalyzer-too-complex
+    -Warith-conversion
+    -Warray-bounds=2
+    -Warray-parameter
+    -Wattribute-alias=2
+    -Wbad-function-cast
+    -Wbidi-chars=any
+    -Wcast-align=strict
+    -Wcast-qual
+    -Wconversion
+    -Wdate-time
+    -Wdisabled-optimization
+    -Wdouble-promotion
+    -Wduplicated-branches
+    -Wduplicated-cond
+    -Wenum-compare
+    -Wenum-conversion
+    -Werror
+    -Wextra
+    -Wfloat-equal
+    -Wformat-overflow=2
+    -Wformat-signedness
+    -Wformat-truncation=2
+    -Wformat=2
+    -Wframe-larger-than=8388608
+    -Wimplicit-fallthrough=5
+    -Winfinite-recursion
+    -Winit-self
+    -Winvalid-pch
+    -Wjump-misses-init
+    -Wlarger-than=1073741824
+    -Wlogical-op
+    -Wmissing-braces
+    -Wmissing-declarations
+    -Wmissing-include-dirs
+    -Wmissing-prototypes
+    -Wmultichar
+    -Wnarrowing
+    -Wnested-externs
+    -Wnormalized=nfkc
+    -Wnull-dereference
+    -Wold-style-definition
+    -Woverlength-strings
+    -Wredundant-decls
+    -Wshadow
+    -Wshift-overflow=2
+    -Wsign-conversion
+    -Wstack-protector
+    -Wstack-usage=8388608
+    -Wstrict-overflow=2
+    -Wstrict-prototypes
+    -Wstringop-overflow=4
+    -Wswitch-enum
+    -Wtrampolines
+    -Wtrivial-auto-var-init
+    -Wundef
+    -Wunreachable-code
+    -Wunused-const-variable=2
+    -Wuse-after-free=3
+    -Wvla
+    -Wwrite-strings
+    -pedantic
+    -pedantic-errors
+)

+ 21 - 0
include/core/ImageReader.h

@@ -0,0 +1,21 @@
+#ifndef CORE_IMAGE_READER_H
+#define CORE_IMAGE_READER_H
+
+void readImage(void);
+/*namespace ImageReader {
+    class Image final {
+        static void clean(ColorChannel*& c);
+
+    public:
+        Image();
+        Cleaner<ColorChannel*, clean> data;
+        unsigned int width;
+        unsigned int height;
+        unsigned int channels;
+        unsigned int bitdepth;
+    };
+
+    Error load(Image& image, const char* path);
+}*/
+
+#endif

+ 95 - 0
include/core/Network.h

@@ -0,0 +1,95 @@
+#ifndef CORE_NETWORK_H
+#define CORE_NETWORK_H
+
+#include "core/Buffer.h"
+#include "core/Types.h"
+
+typedef enum {
+    CORE_RELIABLE,
+    CORE_SEQUENCED,
+    CORE_UNSEQUENCED
+} CorePacketSendMode;
+
+typedef struct {
+    const char* data;
+    size_t size;
+    size_t index;
+} CoreInPacket;
+
+void coreInitInPacket(CoreInPacket* in, const void* data, size_t n);
+bool coreInPacketReadU8(CoreInPacket* in, u8* u);
+bool coreInPacketReadU16(CoreInPacket* in, u16* u);
+bool coreInPacketReadU32(CoreInPacket* in, u32* u);
+bool coreInPacketReadI8(CoreInPacket* in, i8* i);
+bool coreInPacketReadI16(CoreInPacket* in, i16* i);
+bool coreInPacketReadI32(CoreInPacket* in, i32* i);
+bool coreInPacketReadFloat(CoreInPacket* in, float* f);
+size_t coreInPacketReadString(CoreInPacket* in, char* buffer, size_t n);
+bool coreInPacketRead(CoreInPacket* in, void* buffer, size_t n);
+
+typedef struct {
+    CoreBuffer data;
+} CoreOutPacket;
+
+void coreInitOutPacket(CoreOutPacket* out);
+void coreDestroyOutPacket(CoreOutPacket* out);
+CoreOutPacket* coreOutPacketWriteU8(CoreOutPacket* out, u8 u);
+CoreOutPacket* coreOutPacketWriteU16(CoreOutPacket* out, u16 u);
+CoreOutPacket* coreOutPacketWriteU32(CoreOutPacket* out, u32 u);
+CoreOutPacket* coreOutPacketWriteI8(CoreOutPacket* out, i8 i);
+CoreOutPacket* coreOutPacketWriteI16(CoreOutPacket* out, i16 i);
+CoreOutPacket* coreOutPacketWriteI32(CoreOutPacket* out, i32 i);
+CoreOutPacket* coreOutPacketWriteFloat(CoreOutPacket* out, float f);
+CoreOutPacket* coreOutPacketWriteString(CoreOutPacket* out, const char* buffer,
+                                        size_t n);
+CoreOutPacket* coreOutPacketWrite(CoreOutPacket* out, const void* buffer,
+                                  size_t n);
+
+/*namespace Client {
+    typedef uint16 Port;
+    typedef void (*OnConnect)();
+    typedef void (*OnDisconnect)();
+    typedef void (*OnPacket)(InPacket&);
+
+    Error start();
+    void stop();
+
+    Error connect(const char* server, Port port, int timeoutTicks);
+    void disconnect(int timeoutTicks);
+
+    void tick();
+
+    void send(OutPacket& p, PacketSendMode mode);
+
+    void setConnectHandler(OnConnect oc);
+    void setDisconnectHandler(OnDisconnect od);
+    void setPacketHandler(OnPacket op);
+    void resetHandler();
+
+    bool isConnecting();
+    bool isConnected();
+}
+namespace Server {
+    typedef uint16 Port;
+    typedef int Client;
+    typedef void (*OnConnect)(Client);
+    typedef void (*OnDisconnect)(Client);
+    typedef void (*OnPacket)(Client, InPacket&);
+
+    Error start(Port port, int maxClients);
+    void stop();
+
+    void tick();
+
+    void send(const OutPacket& p, PacketSendMode mode);
+    void send(Client client, const OutPacket& p, PacketSendMode mode);
+
+    void disconnect(Client client);
+
+    void setConnectHandler(OnConnect oc);
+    void setDisconnectHandler(OnDisconnect od);
+    void setPacketHandler(OnPacket op);
+    void resetHandler();
+}*/
+
+#endif

+ 111 - 0
include/core/Window.h

@@ -0,0 +1,111 @@
+#ifndef CORE_WINDOW_H
+#define CORE_WINDOW_H
+
+void window(void);
+/*namespace Window {
+    typedef bool (*ShouldRun)();
+    typedef void (*Tick)();
+    typedef void (*Render)(float);
+
+    struct Options final {
+        int majorVersion;
+        int minorVersion;
+        const IntVector2& size;
+        bool fullscreen;
+        bool es;
+        bool vsync;
+        const char* name;
+
+        Options(int majorVersion, int minorVersion, const IntVector2& size,
+                bool es, const char* name);
+    };
+
+    Error open(const Options& options);
+    void close();
+
+    float getTicksPerSecond();
+    float getFramesPerSecond();
+
+    void trapCursor();
+    void freeCursor();
+    bool isCursorTrapped();
+
+    const IntVector2& getSize();
+    bool hasSizeChanged();
+
+    void show();
+    bool shouldClose();
+
+    Error startFrame(Clock::Nanos& n);
+    void endFrame();
+    Error tick();
+    void postTick();
+
+    template<ShouldRun SR, Tick T, Render R>
+    Error run(Clock::Nanos nanosPerTick) {
+        Clock::Nanos lag = 0;
+        while(SR()) {
+            Clock::Nanos passedTime = 0;
+            Error e = startFrame(passedTime);
+            if(e.has()) {
+                return e;
+            }
+            lag += passedTime;
+            while(lag >= nanosPerTick) {
+                lag -= nanosPerTick;
+                e = tick();
+                if(e.has()) {
+                    return e;
+                }
+                T();
+                postTick();
+            }
+            R(static_cast<float>(lag) / static_cast<float>(nanosPerTick));
+            endFrame();
+        }
+        return Error();
+    }
+
+    namespace Input {
+        void setLimit(int limit);
+        void reset();
+        void enable();
+        void disable();
+        bool isEnabled();
+        void fill(const char* s);
+        int getCursor();
+        void setCursor(int index);
+        const List<uint32>& getUnicode();
+
+        template<int N>
+        void toString(StringBuffer<N>& s) {
+            for(uint32 c : getUnicode()) {
+                s.appendUnicode(c);
+            }
+        }
+    }
+
+    namespace Controls {
+        typedef StringBuffer<32> ButtonName;
+        typedef int ButtonId;
+
+        ButtonId add(const ButtonName& name);
+        void bindKey(ButtonId id, int key);
+        void bindGamepad(ButtonId id, int gamepadButton);
+        void bindMouse(ButtonId id, int mouseButton);
+
+        Vector2 getLastMousePosition();
+        Vector2 getMousePosition();
+        Vector2 getLeftGamepadAxis();
+        Vector2 getRightGamepadAxis();
+        float getLeftGamepadTrigger();
+        float getRightGamepadTrigger();
+
+        bool isDown(ButtonId id);
+        int getDownTime(ButtonId id);
+        bool wasReleased(ButtonId id);
+        const ButtonName& getName(ButtonId id);
+    }
+}*/
+
+#endif

+ 40 - 0
src/ImageReader.c

@@ -0,0 +1,40 @@
+#include "core/ImageReader.h"
+
+/*void ImageReader::Image::clean(ColorChannel*& c) {
+    free(c);
+}
+
+static void cleanRawData(ColorChannel*& c) {
+    free(c);
+}
+
+static void cleanState(LodePNGState& state) {
+    lodepng_state_cleanup(&state);
+}
+
+ImageReader::Image::Image() : data(nullptr) {
+}
+
+Error ImageReader::load(Image& image, const char* path) {
+    Cleaner<ColorChannel*, cleanRawData> rawData(nullptr);
+    size_t rawSize;
+    unsigned int error = lodepng_load_file(&rawData, &rawSize, path);
+    if(error) {
+        Error e{"cannot load file '"};
+        e.message.append(path).append("': ").append(lodepng_error_text(error));
+        return e;
+    }
+    Cleaner<LodePNGState, cleanState> state;
+    lodepng_state_init(&state);
+    state->decoder.color_convert = false;
+    error = lodepng_decode(&image.data, &image.width, &image.height, &state,
+                           rawData, rawSize);
+    if(error) {
+        Error e{"cannot decode file '"};
+        e.message.append(path).append("'").append(lodepng_error_text(error));
+        return e;
+    }
+    image.channels = lodepng_get_channels(&state->info_png.color);
+    image.bitdepth = state->info_png.color.bitdepth;
+    return {};
+}*/

+ 496 - 0
src/Network.c

@@ -0,0 +1,496 @@
+#include "core/Network.h"
+
+#include <arpa/inet.h>
+#include <string.h>
+
+void coreInitInPacket(CoreInPacket* in, const void* data, size_t n) {
+    in->data = data;
+    in->size = n;
+    in->index = 0;
+}
+
+bool coreInPacketReadU8(CoreInPacket* in, u8* u) {
+    return coreInPacketRead(in, u, sizeof(*u));
+}
+
+bool coreInPacketReadU16(CoreInPacket* in, u16* u) {
+    if(coreInPacketRead(in, u, sizeof(*u))) {
+        return true;
+    }
+    *u = ntohs(*u);
+    return false;
+}
+
+bool coreInPacketReadU32(CoreInPacket* in, u32* u) {
+    if(coreInPacketRead(in, u, sizeof(*u))) {
+        return true;
+    }
+    *u = ntohl(*u);
+    return false;
+}
+
+bool coreInPacketReadI8(CoreInPacket* in, i8* i) {
+    u8 u;
+    if(coreInPacketReadU8(in, &u)) {
+        return true;
+    }
+    *i = (i8)((i32)u - (i32)128);
+    return false;
+}
+
+bool coreInPacketReadI16(CoreInPacket* in, i16* i) {
+    u16 u;
+    if(coreInPacketReadU16(in, &u)) {
+        return true;
+    }
+    *i = (i16)((i32)u - (i32)32768);
+    return false;
+}
+
+bool coreInPacketReadI32(CoreInPacket* in, i32* i) {
+    u32 u;
+    if(coreInPacketReadU32(in, &u)) {
+        return true;
+    }
+    if(u < 2147483648) {
+        *i = (i32)((i32)u - (i32)2147483648);
+    } else {
+        *i = (i32)(u - (u32)2147483648);
+    }
+    return false;
+}
+
+bool coreInPacketReadFloat(CoreInPacket* in, float* f) {
+    u32 u;
+    static_assert(sizeof(u) == sizeof(*f), "float and u32 size do not match");
+    if(coreInPacketReadU32(in, &u)) {
+        return true;
+    }
+    memcpy(&f, &u, sizeof(float));
+    return false;
+}
+
+size_t coreInPacketReadString(CoreInPacket* in, char* buffer, size_t n) {
+    if(n == 0) {
+        return 0;
+    }
+    u16 size;
+    if(coreInPacketReadU16(in, &size)) {
+        return 0;
+    }
+    size_t end = size;
+    n--;
+    while(n-- > 0 && end-- > 0) {
+        u8 u;
+        if(coreInPacketReadU8(in, &u)) {
+            break;
+        }
+        *(buffer++) = (char)u;
+    }
+    *buffer = '\0';
+    return size;
+}
+
+bool coreInPacketRead(CoreInPacket* in, void* buffer, size_t n) {
+    if(in->index + n > in->size) {
+        return true;
+    }
+    memcpy(buffer, in->data + in->index, n);
+    in->index += n;
+    return false;
+}
+
+void coreInitOutPacket(CoreOutPacket* out) {
+    coreInitBuffer(&out->data);
+}
+
+void coreDestroyOutPacket(CoreOutPacket* out) {
+    coreDestroyBuffer(&out->data);
+}
+
+CoreOutPacket* coreOutPacketWriteU8(CoreOutPacket* out, u8 u) {
+    coreAddSizedBufferData(&out->data, &u, sizeof(u));
+    return out;
+}
+
+CoreOutPacket* coreOutPacketWriteU16(CoreOutPacket* out, u16 u) {
+    u = htons(u);
+    coreAddSizedBufferData(&out->data, &u, sizeof(u));
+    return out;
+}
+
+CoreOutPacket* coreOutPacketWriteU32(CoreOutPacket* out, u32 u) {
+    u = htonl(u);
+    coreAddSizedBufferData(&out->data, &u, sizeof(u));
+    return out;
+}
+
+CoreOutPacket* coreOutPacketWriteI8(CoreOutPacket* out, i8 i) {
+    if(i < 0) {
+        return coreOutPacketWriteU8(out, (u8)((i32)i + (i32)128));
+    }
+    return coreOutPacketWriteU8(out, (u8)((u32)i + (u32)128));
+}
+
+CoreOutPacket* coreOutPacketWriteI16(CoreOutPacket* out, i16 i) {
+    if(i < 0) {
+        return coreOutPacketWriteU16(out, (u16)((i32)i + (i32)32768));
+    }
+    return coreOutPacketWriteU16(out, (u16)((u32)i + (u32)32768));
+}
+
+CoreOutPacket* coreOutPacketWriteI32(CoreOutPacket* out, i32 i) {
+    if(i < 0) {
+        return coreOutPacketWriteU32(out, (u32)(i + (i32)2147483648));
+    }
+    return coreOutPacketWriteU32(out, (u32)((u32)i + (u32)2147483648));
+}
+
+CoreOutPacket* coreOutPacketWriteFloat(CoreOutPacket* out, float f) {
+    u32 u;
+    static_assert(sizeof(u) == sizeof(f), "float and u32 size do not match");
+    memcpy(&u, &f, sizeof(float));
+    return coreOutPacketWriteU32(out, u);
+}
+
+CoreOutPacket* coreOutPacketWriteString(CoreOutPacket* out, const char* buffer,
+                                        size_t n) {
+    size_t end = n > 65535 ? 65535 : n;
+    coreOutPacketWriteU16(out, (u16)end);
+    for(size_t i = 0; i < end; i++) {
+        coreOutPacketWriteU8(out, (u8)(*(buffer++)));
+    }
+    return out;
+}
+
+CoreOutPacket* coreOutPacketWrite(CoreOutPacket* out, const void* buffer,
+                                  size_t n) {
+    coreAddSizedBufferData(&out->data, buffer, n);
+    return out;
+}
+
+/*static int enetCounter = 0;
+bool ENet::add() {
+    if(enetCounter == 0 && enet_initialize() != 0) {
+        return true;
+    }
+    enetCounter++;
+    return false;
+}
+
+void ENet::remove() {
+    if(enetCounter > 0 && --enetCounter == 0) {
+        enet_deinitialize();
+    }
+}
+
+static_assert(sizeof(enet_uint16) == sizeof(Client::Port),
+              "client port has wrong type");
+
+static ENetHost* client = nullptr;
+static ENetPeer* connection = nullptr;
+static int connectTicks = 0;
+static int connectTimeoutTicks = 0;
+static int disconnectTicks = 0;
+static int disconnectTimeoutTicks = 0;
+static Client::OnConnect onConnect = []() {};
+static Client::OnDisconnect onDisconnect = []() {};
+static Client::OnPacket onPacket = [](InPacket&) {};
+
+Error Client::start() {
+    if(client != nullptr) {
+        return {"already started"};
+    } else if(ENet::add()) {
+        return {"cannot initialize enet"};
+    }
+    client = enet_host_create(nullptr, 1, 2, 0, 0);
+    if(client == nullptr) {
+        ENet::remove();
+        return {"cannot create enet client host"};
+    }
+    return {};
+}
+
+void Client::stop() {
+    if(connection != nullptr) {
+        onDisconnect();
+        enet_peer_disconnect_now(connection, 0);
+        connection = nullptr;
+    }
+    if(client != nullptr) {
+        enet_host_destroy(client);
+        ENet::remove();
+        client = nullptr;
+    }
+    connectTicks = 0;
+    disconnectTicks = 0;
+}
+
+Error Client::connect(const char* server, Port port, int timeoutTicks) {
+    if(client == nullptr) {
+        return {"client not started"};
+    } else if(connection != nullptr) {
+        return {"connection already exists"};
+    }
+
+    ENetAddress address;
+    memset(&address, 0, sizeof(ENetAddress));
+    enet_address_set_host(&address, server);
+    address.port = port;
+
+    connection = enet_host_connect(client, &address, 3, 0);
+    if(connection == nullptr) {
+        return {"cannot create connection"};
+    }
+    connectTicks = 1;
+    connectTimeoutTicks = timeoutTicks;
+    return {};
+}
+
+void Client::disconnect(int timeoutTicks) {
+    if(connection == nullptr) {
+        return;
+    }
+    connectTicks = 0;
+    enet_peer_disconnect(connection, 0);
+    disconnectTicks = 1;
+    disconnectTimeoutTicks = timeoutTicks;
+}
+
+void Client::send(OutPacket& p, PacketSendMode mode) {
+    if(client != nullptr && connection != nullptr && connectTicks < 0) {
+        constexpr enet_uint32 flags[] = {ENET_PACKET_FLAG_RELIABLE, 0,
+                                         ENET_PACKET_FLAG_UNSEQUENCED};
+        enet_uint8 index = static_cast<enet_uint8>(mode);
+        enet_peer_send(connection, index,
+                       enet_packet_create(
+                           p.buffer,
+static_cast<size_t>(p.buffer.getLength()), flags[index]));
+    }
+}
+
+void Client::tick() {
+    if(client == nullptr) {
+        return;
+    }
+    ENetEvent e;
+    while(enet_host_service(client, &e, 0) > 0) {
+        switch(e.type) {
+            case ENET_EVENT_TYPE_CONNECT:
+                connectTicks = -1;
+                onConnect();
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
+            case ENET_EVENT_TYPE_DISCONNECT:
+                disconnectTicks = 0;
+                onDisconnect();
+                connection = nullptr;
+                break;
+            case ENET_EVENT_TYPE_NONE: return;
+            case ENET_EVENT_TYPE_RECEIVE:
+                InPacket in(e.packet->data,
+                            static_cast<int>(e.packet->dataLength));
+                onPacket(in);
+                enet_packet_destroy(e.packet);
+                break;
+        }
+    }
+    if(connectTicks >= 1 && ++connectTicks > connectTimeoutTicks) {
+        connectTicks = 0;
+        disconnect(connectTimeoutTicks);
+    }
+    if(disconnectTicks >= 1 && ++disconnectTicks > disconnectTimeoutTicks) {
+        disconnectTicks = 0;
+        onDisconnect();
+        if(connection != nullptr) {
+            enet_peer_reset(connection);
+            connection = nullptr;
+        }
+    }
+}
+
+void Client::setConnectHandler(OnConnect oc) {
+    onConnect = oc;
+}
+
+void Client::setDisconnectHandler(OnDisconnect od) {
+    onDisconnect = od;
+}
+
+void Client::setPacketHandler(OnPacket op) {
+    onPacket = op;
+}
+
+void Client::resetHandler() {
+    onConnect = []() {};
+    onDisconnect = []() {};
+    onPacket = [](InPacket&) {};
+}
+
+bool Client::isConnecting() {
+    return connectTicks >= 1;
+}
+
+bool Client::isConnected() {
+    return connectTicks < 0;
+}
+
+static_assert(sizeof(enet_uint16) == sizeof(Server::Port),
+              "client port has wrong type");
+
+static ENetHost* server;
+static HashMap<Server::Client, ENetPeer*> clients;
+static Server::Client idCounter = 1;
+static Server::OnConnect onConnect = [](Server::Client) {};
+static Server::OnDisconnect onDisconnect = [](Server::Client) {};
+static Server::OnPacket onPacket = [](Server::Client, InPacket&) {};
+
+Error Server::start(Port port, int maxClients) {
+    if(maxClients <= 0) {
+        return {"invalid max client amount"};
+    } else if(server != nullptr) {
+        return {"already started"};
+    } else if(ENet::add()) {
+        return {"cannot initialize enet"};
+    }
+
+    ENetAddress address;
+    memset(&address, 0, sizeof(ENetAddress));
+    address.host = ENET_HOST_ANY;
+    address.port = port;
+
+    server = enet_host_create(&address, static_cast<unsigned
+int>(maxClients), 3, 0, 0); if(server == nullptr) { ENet::remove(); return
+{"cannot create enet server host"};
+    }
+    return {};
+}
+
+void Server::stop() {
+    if(server == nullptr) {
+        return;
+    }
+    for(ENetPeer* peer : clients.values()) {
+        enet_peer_reset(peer);
+    }
+    enet_host_destroy(server);
+    server = nullptr;
+    ENet::remove();
+}
+
+static void writeId(ENetPeer* peer, Server::Client id) {
+    static_assert(sizeof(peer->data) >= sizeof(id),
+                  "private data not big enough for id");
+    memcpy(&(peer->data), &id, sizeof(id));
+}
+
+static Server::Client getId(ENetPeer* peer) {
+    Server::Client id = -1;
+    memcpy(&id, &(peer->data), sizeof(id));
+    return id;
+}
+
+static void handleConnect(ENetEvent& e) {
+    Server::Client id = idCounter++;
+    if(clients.tryEmplace(id, e.peer)) {
+        LOG_WARNING("id is connected twice");
+        return;
+    }
+    writeId(e.peer, id);
+    onConnect(id);
+}
+
+static void handlePacket(ENetEvent& e) {
+    if(e.peer->data == nullptr) {
+        LOG_WARNING("client without data sent package");
+        return;
+    }
+    Server::Client id = getId(e.peer);
+    InPacket in(e.packet->data, static_cast<int>(e.packet->dataLength));
+    onPacket(id, in);
+}
+
+static void handleDisconnect(ENetEvent& e) {
+    if(e.peer->data == nullptr) {
+        LOG_WARNING("client without data disconnected");
+        return;
+    }
+    Server::Client id = getId(e.peer);
+    onDisconnect(id);
+    if(clients.remove(id)) {
+        LOG_WARNING("removed non existing client");
+    }
+}
+
+void Server::tick() {
+    if(server == nullptr) {
+        return;
+    }
+    ENetEvent e;
+    while(enet_host_service(server, &e, 0) > 0) {
+        switch(e.type) {
+            case ENET_EVENT_TYPE_CONNECT: handleConnect(e); break;
+            case ENET_EVENT_TYPE_RECEIVE:
+                handlePacket(e);
+                enet_packet_destroy(e.packet);
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
+            case ENET_EVENT_TYPE_DISCONNECT: handleDisconnect(e); break;
+            case ENET_EVENT_TYPE_NONE: return;
+        }
+    }
+}
+
+static ENetPacket* fromBuffer(const Buffer& buffer, int index) {
+    constexpr enet_uint32 flags[] = {ENET_PACKET_FLAG_RELIABLE, 0,
+                                     ENET_PACKET_FLAG_UNSEQUENCED};
+    return enet_packet_create(
+        buffer, static_cast<unsigned int>(buffer.getLength()),
+flags[index]);
+}
+
+void Server::send(const OutPacket& p, PacketSendMode mode) {
+    if(server != nullptr) {
+        int index = static_cast<int>(mode);
+        enet_host_broadcast(server, static_cast<enet_uint8>(index),
+                            fromBuffer(p.buffer, index));
+    }
+}
+
+void Server::send(Server::Client client, const OutPacket& p,
+                  PacketSendMode mode) {
+    if(server == nullptr) {
+        return;
+    }
+    ENetPeer** peer = clients.search(client);
+    if(peer != nullptr) {
+        int index = static_cast<int>(mode);
+        enet_peer_send(*peer, static_cast<enet_uint8>(index),
+                       fromBuffer(p.buffer, index));
+    }
+}
+
+void Server::disconnect(Client client) {
+    ENetPeer** peer = clients.search(client);
+    if(peer != nullptr) {
+        enet_peer_disconnect(*peer, 0);
+    }
+}
+
+void Server::setConnectHandler(OnConnect oc) {
+    onConnect = oc;
+}
+
+void Server::setDisconnectHandler(OnDisconnect od) {
+    onDisconnect = od;
+}
+
+void Server::setPacketHandler(OnPacket op) {
+    onPacket = op;
+}
+
+void Server::resetHandler() {
+    onConnect = [](Server::Client) {};
+    onDisconnect = [](Server::Client) {};
+    onPacket = [](Server::Client, InPacket&) {};
+}*/

+ 447 - 0
src/Window.c

@@ -0,0 +1,447 @@
+#include "core/Window.h"
+
+/*static GLFWwindow* window = nullptr;
+static Clock fps;
+static Clock tps;
+static IntVector2 size{0, 0};
+static bool sizeChanged = false;
+
+static List<uint32> input;
+static int inputCursor = 0;
+static int inputLimit = 256;
+static bool inputActive = false;
+
+struct Button final {
+    Window::Controls::ButtonName name;
+    int key;
+    int downTime;
+    int upEvents;
+    int downEvents;
+    int controllerUp;
+    int controllerDown;
+    bool released;
+
+    Button(const Window::Controls::ButtonName& name_)
+        : name(name_), key(0), downTime(0), upEvents(0), downEvents(0),
+          controllerUp(0), controllerDown(0), released(false) {
+    }
+
+    void tick() {
+        bool down = (downEvents > 0) || (controllerDown > 0);
+        bool up = (upEvents == downEvents) && (controllerUp == controllerDown);
+
+        if(released) {
+            downTime = 0;
+        }
+        downTime += down;
+        released = down && up;
+
+        downEvents -= upEvents;
+        upEvents = 0;
+
+        controllerDown -= controllerUp;
+        controllerUp = 0;
+    }
+};
+static Button fallbackButton{"unknown"};
+static List<Button> buttons;
+static HashMap<int, Window::Controls::ButtonId> keyToButtonId;
+static HashMap<int, Window::Controls::ButtonId> gamepadToButtonId;
+static HashMap<int, Window::Controls::ButtonId> mouseToButtonId;
+static Vector2 lastMousePosition;
+static Vector2 mousePosition;
+static int activeController = -1;
+static GLFWgamepadstate lastControllerState;
+
+Window::Options::Options(int majorVersion_, int minorVersion_,
+                         const IntVector2& size_, bool es_, const char* name_)
+    : majorVersion(majorVersion_), minorVersion(minorVersion_), size(size_),
+      fullscreen(false), es(es_), vsync(true), name(name_) {
+}
+
+static void onButton(HashMap<int, Window::Controls::ButtonId>& map, int key,
+                     int action) {
+    Window::Controls::ButtonId* b = map.search(key);
+    if(b == nullptr) {
+        return;
+    }
+    Window::Controls::ButtonId id = *b;
+    if(id < 0 || id >= buttons.getLength()) {
+        return;
+    }
+    if(action == GLFW_RELEASE) {
+        buttons[id].upEvents++;
+    } else if(action == GLFW_PRESS) {
+        buttons[id].downEvents++;
+    }
+}
+
+static void addError(Error& e) {
+    const char* description = nullptr;
+    int errorCode = glfwGetError(&description);
+    if(errorCode == GLFW_NO_ERROR) {
+        return;
+    }
+    e.message.append(": ").append(description);
+}
+
+static void handleInputKey(int key, int action) {
+    if(action == GLFW_RELEASE) {
+        return;
+    }
+    switch(key) {
+        case GLFW_KEY_BACKSPACE:
+            if(input.getLength() > inputCursor - 1 && inputCursor > 0) {
+                input.remove(inputCursor - 1);
+                inputCursor--;
+            }
+            break;
+        case GLFW_KEY_LEFT: inputCursor -= inputCursor > 0; break;
+        case GLFW_KEY_RIGHT:
+            inputCursor += inputCursor < input.getLength();
+            break;
+    }
+}
+
+static void onKey(GLFWwindow*, int key, int scancode, int action, int mods) {
+    (void)scancode;
+    (void)mods;
+    if(inputActive) {
+        handleInputKey(key, action);
+    }
+    onButton(keyToButtonId, key, action);
+}
+
+static void addUnicode(uint32 codepoint) {
+    if(input.getLength() >= inputLimit) {
+        return;
+    }
+    input.add(codepoint);
+    for(int i = input.getLength() - 1; i > inputCursor; i--) {
+        Core::swap(input[i], input[i - 1]);
+    }
+    inputCursor++;
+}
+
+static void onChar(GLFWwindow*, uint32 codepoint) {
+    if(inputActive) {
+        addUnicode(codepoint);
+    }
+}
+
+static void onResize(GLFWwindow*, int width, int height) {
+    sizeChanged = true;
+    size[0] = width;
+    size[1] = height;
+}
+
+static void onMouse(GLFWwindow*, int button, int action, int mods) {
+    (void)mods;
+    onButton(mouseToButtonId, button, action);
+}
+
+static void onMouseMove(GLFWwindow*, double x, double y) {
+    mousePosition = Vector<2, double>(x, y).toFloat();
+}
+
+Error Window::open(const Options& o) {
+    if(!glfwInit()) {
+        Error e{"could not initialize GLFW"};
+        addError(e);
+        return e;
+    }
+
+    glfwDefaultWindowHints();
+    glfwWindowHint(GLFW_VISIBLE, false);
+    glfwWindowHint(GLFW_RESIZABLE, true);
+    glfwWindowHint(GLFW_DECORATED, !o.fullscreen);
+    glfwWindowHint(GLFW_DOUBLEBUFFER, true);
+
+    if(o.es) {
+        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
+    } else {
+        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+    }
+    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, o.majorVersion);
+    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, o.minorVersion);
+
+    GLFWmonitor* m = o.fullscreen ? glfwGetPrimaryMonitor() : nullptr;
+    window = glfwCreateWindow(o.size[0], o.size[1], o.name, m, nullptr);
+    if(window == nullptr) {
+        Error e{"could not create window"};
+        addError(e);
+        close();
+        return e;
+    }
+    size = o.size;
+    glfwSetKeyCallback(window, onKey);
+    glfwSetCharCallback(window, onChar);
+    glfwSetFramebufferSizeCallback(window, onResize);
+    glfwSetMouseButtonCallback(window, onMouse);
+    glfwSetCursorPosCallback(window, onMouseMove);
+
+    glfwMakeContextCurrent(window);
+    glfwSwapInterval(o.vsync);
+
+    GLenum err = glewInit();
+    if(err != GLEW_OK) {
+        Error e{"could not initialize GLEW: "};
+        e.message.append(glewGetErrorString(err));
+        close();
+        return e;
+    }
+    return {};
+}
+
+void Window::close() {
+    if(window != nullptr) {
+        glfwDestroyWindow(window);
+        window = nullptr;
+    }
+    glfwTerminate();
+}
+
+float Window::getTicksPerSecond() {
+    return tps.getUpdatesPerSecond();
+}
+
+float Window::getFramesPerSecond() {
+    return fps.getUpdatesPerSecond();
+}
+
+const IntVector2& Window::getSize() {
+    return size;
+}
+
+bool Window::hasSizeChanged() {
+    return sizeChanged;
+}
+
+void Window::trapCursor() {
+    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
+}
+
+void Window::freeCursor() {
+    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
+}
+
+bool Window::isCursorTrapped() {
+    return glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
+}
+
+void Window::show() {
+    glfwShowWindow(window);
+}
+
+bool Window::shouldClose() {
+    return glfwWindowShouldClose(window);
+}
+
+Error Window::startFrame(Clock::Nanos& n) {
+    return fps.update(n);
+}
+
+static bool searchForGamepad() {
+    if(activeController != -1) {
+        return true;
+    }
+    for(int i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_LAST; i++) {
+        if(glfwJoystickIsGamepad(i)) {
+            activeController = i;
+            return true;
+        }
+    }
+    return false;
+}
+
+static void checkGamepad() {
+    GLFWgamepadstate state;
+    if(!glfwGetGamepadState(activeController, &state)) {
+        activeController = -1;
+        return;
+    }
+    for(int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++) {
+        Window::Controls::ButtonId* idp = gamepadToButtonId.search(i);
+        if(idp == nullptr) {
+            continue;
+        }
+        Window::Controls::ButtonId id = *idp;
+        if(id < 0 || id >= buttons.getLength()) {
+            continue;
+        }
+        if(!lastControllerState.buttons[i] && state.buttons[i]) {
+            buttons[id].controllerDown++;
+        } else if(lastControllerState.buttons[i] && !state.buttons[i]) {
+            buttons[id].controllerUp++;
+        }
+    }
+    lastControllerState = state;
+}
+
+void Window::endFrame() {
+    glfwSwapBuffers(window);
+    sizeChanged = false;
+    glfwPollEvents();
+    if(searchForGamepad()) {
+        checkGamepad();
+    }
+}
+
+Error Window::tick() {
+    Clock::Nanos n = 0;
+    Error e = tps.update(n);
+    if(e.has()) {
+        return e;
+    }
+    for(Button& b : buttons) {
+        b.tick();
+    }
+    return Error();
+}
+
+void Window::postTick() {
+    lastMousePosition = mousePosition;
+}
+
+void Window::Input::setLimit(int l) {
+    inputLimit = l;
+    while(input.getLength() > inputLimit) {
+        input.removeBySwap(inputLimit);
+    }
+}
+
+void Window::Input::reset() {
+    input.clear();
+    inputCursor = 0;
+}
+
+void Window::Input::enable() {
+    inputActive = true;
+}
+
+void Window::Input::disable() {
+    inputActive = false;
+}
+
+bool Window::Input::isEnabled() {
+    return inputActive;
+}
+
+static uint32 read(int& index, const char* s) {
+    if(s[index] == '\0') {
+        return '\0';
+    }
+    return static_cast<uint32>(s[index++]);
+}
+
+static uint32 readUnicode(int& index, const char* s) {
+    uint32 c = read(index, s);
+    if((c & 0xE0) == 0xC0) {
+        c = ((c & 0x1F) << 6) | (read(index, s) & 0x3F);
+    } else if((c & 0xF0) == 0xE0) {
+        c = ((c & 0xF) << 12) | ((read(index, s) & 0x3F) << 6);
+        c |= read(index, s) & 0x3F;
+    } else if((c & 0xF8) == 0xF0) {
+        c = ((c & 0x7) << 18) | ((read(index, s) & 0x3F) << 12);
+        c |= (read(index, s) & 0x3F) << 6;
+        c |= read(index, s) & 0x3F;
+    }
+    return c;
+}
+
+void Window::Input::fill(const char* s) {
+    int index = 0;
+    reset();
+    while(true) {
+        uint32 c = readUnicode(index, s);
+        if(c == '\0') {
+            break;
+        }
+        addUnicode(c);
+    }
+}
+
+int Window::Input::getCursor() {
+    return inputCursor;
+}
+
+void Window::Input::setCursor(int index) {
+    if(index < 0) {
+        inputCursor = 0;
+    } else if(index > input.getLength()) {
+        inputCursor = input.getLength();
+    } else {
+        inputCursor = index;
+    }
+}
+
+const List<uint32>& Window::Input::getUnicode() {
+    return input;
+}
+
+Window::Controls::ButtonId Window::Controls::add(const ButtonName& name) {
+    ButtonId id = buttons.getLength();
+    buttons.add(name);
+    return id;
+}
+
+void Window::Controls::bindKey(ButtonId id, int key) {
+    keyToButtonId.add(key, id);
+}
+
+void Window::Controls::bindGamepad(ButtonId id, int gamepadButton) {
+    gamepadToButtonId.add(gamepadButton, id);
+}
+
+void Window::Controls::bindMouse(ButtonId id, int mouseButton) {
+    mouseToButtonId.add(mouseButton, id);
+}
+
+Vector2 Window::Controls::getLastMousePosition() {
+    return lastMousePosition;
+}
+
+Vector2 Window::Controls::getMousePosition() {
+    return mousePosition;
+}
+
+Vector2 Window::Controls::getLeftGamepadAxis() {
+    return Vector2(lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_X],
+                   lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]);
+}
+
+Vector2 Window::Controls::getRightGamepadAxis() {
+    return Vector2(lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_X],
+                   lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]);
+}
+
+float Window::Controls::getLeftGamepadTrigger() {
+    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER];
+}
+
+float Window::Controls::getRightGamepadTrigger() {
+    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER];
+}
+
+static const Button& getButton(Window::Controls::ButtonId id) {
+    if(id < 0 || id >= buttons.getLength()) {
+        return fallbackButton;
+    }
+    return buttons[id];
+}
+
+bool Window::Controls::isDown(ButtonId id) {
+    return getButton(id).downTime > 0;
+}
+
+int Window::Controls::getDownTime(ButtonId id) {
+    return getButton(id).downTime;
+}
+
+bool Window::Controls::wasReleased(ButtonId id) {
+    return getButton(id).released;
+}
+
+const Window::Controls::ButtonName& Window::Controls::getName(ButtonId id) {
+    return getButton(id).name;
+}*/

+ 201 - 0
tasks

@@ -0,0 +1,201 @@
+#!/bin/bash
+set -e
+clear
+cd $(dirname $0)
+
+compiler="gcc"
+if [ -e compiler ]; then
+    compiler=$(cat compiler)
+    echo "compiling with $compiler"
+fi
+
+printHelpExit() {
+    echo "$0 clean           | remove build results"
+    echo "$0 build <type>    | build everything"
+    echo "$0 install         | move build results into the install folder"
+    echo "$0 test <type>     | run the tests"
+    echo "$0 valgrind <type> | run the tests with valgrind"
+    echo "$0 coverage        | generate code coverage"
+    echo "$0 performance     | run the performance tests"
+    echo "$0 stats           | run the performance tests with stats"
+    echo "$0 time            | check build time"
+    exit 0
+}
+
+task=$1
+if [ -z "$task" ]; then
+    printHelpExit
+fi
+
+# task vars
+build_debug=false
+build_profile=false
+build_release=false
+
+test_debug=false
+test_release=false
+
+valgrind=""
+performance=false
+time=false
+install=false
+coverage=false
+stats=false
+
+export CMAKE_EXPORT_COMPILE_COMMANDS=true
+
+# parsing
+if [ "$task" = "clean" ]; then
+    rm -rf build_debug build_profile build_release install
+elif [ "$task" = "build" ]; then
+    type=$2
+    if [ "$type" = "debug" ]; then
+        build_debug=true
+    elif [ "$type" = "release" ]; then
+        build_release=true
+    elif [ "$type" = "all" ]; then
+        build_debug=true
+        build_release=true
+    else
+        echo "Valid build types are: debug, release, all"
+        printHelpExit
+    fi
+elif [ "$task" = "install" ]; then
+    build_release=true
+    install=true
+elif [ "$task" = "coverage" ]; then
+    build_debug=true
+    test_debug=true
+    coverage=true
+elif [ "$task" = "test" ]; then
+    type=$2
+    if [ "$type" = "debug" ]; then
+        build_debug=true
+        test_debug=true
+    elif [ "$type" = "release" ]; then
+        build_release=true
+        test_release=true
+    elif [ "$type" = "all" ]; then
+        build_debug=true
+        test_debug=true
+        build_release=true
+        test_release=true
+    else
+        echo "Valid test types are: debug, release, all"
+        printHelpExit
+    fi
+elif [ "$task" = "valgrind" ]; then
+    type=$2
+    if [ "$type" = "debug" ]; then
+        build_debug=true
+        test_debug=true
+    elif [ "$type" = "release" ]; then
+        build_release=true
+        test_release=true
+    elif [ "$type" = "all" ]; then
+        build_debug=true
+        test_debug=true
+        build_release=true
+        test_release=true
+    else
+        echo "Valid valgrind types are: debug, release, all"
+        printHelpExit
+    fi
+    valgrind="valgrind"
+elif [ "$task" = "performance" ]; then
+    build_profile=true
+    performance=true
+elif [ "$task" = "stats" ]; then
+    build_profile=true
+    performance=true
+    stats=true
+elif [ "$task" = "time" ]; then
+    build_release=true
+    time=true
+else
+    echo "unknown task"
+    printHelpExit
+fi
+
+# task execution
+buildProfile() {
+    folder=$1
+    shift 1
+    if [ ! -e "$folder" ]; then 
+        cmake -B "$folder" -S . -G Ninja -DCMAKE_C_COMPILER=${compiler} -DCMAKE_INSTALL_PREFIX=../install $@
+    fi
+    ninja -C "$folder"
+}
+
+if $build_debug; then
+    buildProfile build_debug -DCMAKE_BUILD_TYPE=Debug
+fi
+if $build_profile; then
+    buildProfile build_profile -DCMAKE_BUILD_TYPE=RelWithDebInfo
+fi
+if $build_release; then
+    buildProfile build_release -DCMAKE_BUILD_TYPE=Release
+fi
+if $install; then
+    ninja -C build_release install
+fi
+
+function runTests() {
+    LLVM_PROFILE_FILE="default.profraw" $valgrind ./test $valgrind || true
+}
+
+if $test_debug; then
+    cd build_debug
+    runTests
+    cd ..
+fi
+if $test_release; then
+    cd build_release
+    runTests
+    cd ..
+fi
+if $performance; then
+    cd build_profile
+    if $stats; then
+        user=$(whoami)
+        sudo perf record ./performance
+        sudo chown $user:$user perf.data
+        sudo chown $user:$user default.profraw
+        perf report
+    else
+        ./performance
+    fi
+    cd ..
+fi
+if $time; then
+    lines=$(cat build_release/.ninja_log | grep "^[0-9]")
+
+    startMillis=0
+    endMillis=0
+    name=""
+    i=0
+    output=""
+    for arg in $lines; do
+        if [ $i == 0 ]; then
+            startMillis=$arg
+        elif [ $i == 1 ]; then
+            endMillis=$arg
+        elif [ $i == 3 ]; then
+            name=$arg
+            diff=$(expr $endMillis - $startMillis)
+            output="${output}\n$diff $name"
+        fi
+        i=$(expr $(expr $i + 1) % 5) && true
+    done
+    printf "$output" | sort -n
+fi
+if $coverage; then
+    if [ $compiler = "gcc" ]; then
+        gcovr -r . build_debug -e test -e performance \
+            --exclude-lines-by-pattern ".*CoverageIgnore.*"
+    else
+        files=$(find build_debug -name *.profraw)
+        llvm-profdata-16 merge -sparse $files -o build_debug/default.profdata
+        llvm-cov-16 show ./build_debug/test -instr-profile=build_debug/default.profdata --ignore-filename-regex="test/" -line-coverage-lt=100
+    fi
+fi 

+ 40 - 0
test/Main.c

@@ -0,0 +1,40 @@
+#include <core/Logger.h>
+
+#include "Tests.h"
+// #include "core/Window.h"
+
+/*static int ticks = 40;
+
+static bool isRunning() {
+    return true;
+    // return !Window::shouldClose() && ticks > 0;
+}
+
+static void tick() {
+    ticks -= ticks > 0;
+}
+
+static void render(float) {
+}*/
+
+int main(int argAmount, char** args) {
+    (void)args;
+    if(argAmount < 2) {
+        CORE_LOG_ERROR("missing path to images");
+        return 0;
+    }
+    coreTestImageReader(args[1]);
+    coreTestNetwork();
+
+    // Window::Options options(4, 3, {800, 480}, false, "Test");
+    // Error error = Window::open(options);
+    // if(error.has()) {
+    //     LOG_ERROR(error.message);
+    //     return 0;
+    // }
+    // Window::show();
+    // Window::run<isRunning, tick, render>(50'000'000);
+    // Window::close();
+
+    return 0;
+}

+ 9 - 0
test/Tests.h

@@ -0,0 +1,9 @@
+#ifndef CORE_TESTS_H
+#define CORE_TESTS_H
+
+#include "core/Test.h"
+
+void coreTestNetwork(void);
+void coreTestImageReader(const char* path);
+
+#endif

+ 32 - 0
test/modules/ImageReaderTests.c

@@ -0,0 +1,32 @@
+#include "../Tests.h"
+
+/*static void testReadPNG(Test& test, const char* path, const char* name,
+                        unsigned int channels, unsigned int bitdepth) {
+    ImageReader::Image image;
+    Error error =
+        ImageReader::load(image, String(path).append(name).append(".png"));
+    if(error.has()) {
+        test.checkEqual(false, true,
+                        String("read ").append(name).append(" error"));
+        return;
+    }
+    test.checkEqual(32u, image.width, String(name).append(" width"));
+    test.checkEqual(64u, image.height, String(name).append(" height"));
+    test.checkEqual(channels, image.channels, String(name).append(" channels"));
+    test.checkEqual(bitdepth, image.bitdepth, String(name).append(" bitdepth"));
+    test.checkEqual(true, image.data != nullptr, String(name).append(" data"));
+}*/
+
+void coreTestImageReader(const char* path) {
+    (void)path;
+    // Test test("ImageReader");
+    // testReadPNG(test, path, "rgb8", 3, 8);
+    // testReadPNG(test, path, "rgb16", 3, 16);
+    // testReadPNG(test, path, "rgba8", 4, 8);
+    // testReadPNG(test, path, "rgba16", 4, 16);
+    // testReadPNG(test, path, "gray8", 1, 8);
+    // testReadPNG(test, path, "gray16", 1, 16);
+    // testReadPNG(test, path, "graya8", 2, 8);
+    // testReadPNG(test, path, "graya16", 2, 16);
+    // test.finalize();
+}

+ 210 - 0
test/modules/NetworkTests.c

@@ -0,0 +1,210 @@
+#include "../Tests.h"
+
+/*static bool checkError(Test& test, const Error e, const char* msg) {
+    if(e.has()) {
+        test.checkFalse(true, msg);
+        return true;
+    }
+    return false;
+}
+
+static void tickClient(int ticks) {
+    for(int i = 0; i < ticks; i++) {
+        Client::tick();
+    }
+}
+
+static void tick(int ticks) {
+    for(int i = 0; i < ticks; i++) {
+        Client::tick();
+        Server::tick();
+    }
+}
+
+static void testConnect(Test& test, PacketSendMode mode) {
+    static bool clientConnected = false;
+    static bool clientDisconnected = false;
+    static bool clientPackage = false;
+    clientConnected = false;
+    clientDisconnected = false;
+    clientPackage = false;
+
+    static int packageCounter = 0;
+    static bool serverConnected = false;
+    static bool serverDisconnect = false;
+    static uint8 data1 = 0;
+    static uint16 data2 = 0;
+    static uint32 data3 = 0;
+    static int8 data4 = 0;
+    static int16 data5 = 0;
+    static int32 data6 = 0;
+    static int8 data7 = 0;
+    static int16 data8 = 0;
+    static int32 data9 = 0;
+    static StringBuffer<20> data10;
+    static float data11 = 0.0f;
+    static IntVector3 data12;
+    static Vector3 data13;
+
+    serverConnected = false;
+    serverDisconnect = false;
+    data1 = 0;
+    data2 = 0;
+    data3 = 0;
+    data4 = 0;
+    data5 = 0;
+    data6 = 0;
+    data7 = 0;
+    data8 = 0;
+    data9 = 0;
+    data10.clear();
+    data11 = 0.0f;
+    data12 = IntVector3();
+    data13 = Vector3();
+
+    Server::resetHandler();
+    Server::setConnectHandler([](Server::Client) { serverConnected = true; });
+    Server::setDisconnectHandler(
+        [](Server::Client) { serverDisconnect = true; });
+    Server::setPacketHandler([](Server::Client client, InPacket& in) {
+        in.read(data1);
+        in.read(data2);
+        in.read(data3);
+        in.read(data4);
+        in.read(data5);
+        in.read(data6);
+        in.read(data7);
+        in.read(data8);
+        in.read(data9);
+        in.read(data10);
+        in.read(data11);
+        in.read(data12);
+        in.read(data13);
+
+        if(packageCounter == 0) {
+            OutPacket out(0);
+            Server::send(client, out, PacketSendMode::RELIABLE);
+        } else if(packageCounter == 1) {
+            OutPacket out(0);
+            Server::send(client, out, PacketSendMode::SEQUENCED);
+        } else if(packageCounter == 2) {
+            OutPacket out(0);
+            Server::send(client, out, PacketSendMode::UNSEQUENCED);
+        }
+        packageCounter++;
+    });
+
+    Client::resetHandler();
+    Client::setConnectHandler([]() { clientConnected = true; });
+    Client::setDisconnectHandler([]() { clientDisconnected = true; });
+    Client::setPacketHandler([](InPacket&) { clientPackage = true; });
+
+    if(checkError(test, Server::start(54321, 5), "server can initialize")) {
+        return;
+    } else if(checkError(test, Client::start(), "client can initialize")) {
+        return;
+    } else if(checkError(test, Client::connect("127.0.0.1", 54321, 90),
+                         "start connection failed")) {
+        return;
+    }
+    test.checkFalse(Client::isConnected(), "client not connected yet");
+    test.checkTrue(Client::isConnecting(), "client is connecting");
+
+    tick(100);
+
+    test.checkTrue(clientConnected, "client called connect callback");
+    test.checkTrue(Client::isConnected(), "client is connected");
+    test.checkFalse(Client::isConnecting(), "client is no more connecting");
+
+    OutPacket out(50);
+    out.writeU8(0xF1).writeU16(0xF123).writeU32(0xF1234567);
+    out.writeS8(-0x71).writeS16(-0x7123).writeS32(-0x71234567);
+    out.writeS8(0x71).writeS16(0x7123).writeS32(0x71234567);
+    StringBuffer<20> s("Hi there");
+    out.writeString(s);
+    out.writeFloat(252345.983f);
+    out.writeVector(IntVector3(1, 2, 3));
+    out.writeVector(Vector3(1.5f, 2.5f, 3.5f));
+    Client::send(out, mode);
+
+    tick(100);
+
+    test.checkTrue(clientPackage, "client has received data");
+    test.checkTrue(serverConnected, "server has connection");
+
+    test.checkUnsigned8(0xF1, data1, "correct value is sent 1");
+    test.checkUnsigned16(0xF123, data2, "correct value is sent 2");
+    test.checkEqual(0xF1234567u, data3, "correct value is sent 3");
+    test.checkSigned8(-0x71, data4, "correct value is sent 4");
+    test.checkSigned16(-0x7123, data5, "correct value is sent 5");
+    test.checkEqual(-0x71234567, data6, "correct value is sent 6");
+    test.checkSigned8(0x71, data7, "correct value is sent 7");
+    test.checkSigned16(0x7123, data8, "correct value is sent 8");
+    test.checkEqual(0x71234567, data9, "correct value is sent 9");
+    test.checkEqual(s, data10, "correct value is sent 10");
+    test.checkFloat(252345.983f, data11, 0.01f, "correct value is sent 11");
+    test.checkEqual(1, data12[0], "correct value is sent 12|1");
+    test.checkEqual(2, data12[1], "correct value is sent 12|2");
+    test.checkEqual(3, data12[2], "correct value is sent 12|3");
+    test.checkFloat(1.5f, data13[0], 0.01f, "correct value is sent 13|1");
+    test.checkFloat(2.5f, data13[1], 0.01f, "correct value is sent 13|2");
+    test.checkFloat(3.5f, data13[2], 0.01f, "correct value is sent 13|3");
+
+    Client::disconnect(90);
+    test.checkFalse(Client::isConnected(), "client was disconnected");
+    test.checkFalse(Client::isConnecting(), "client is not connecting");
+    tick(100);
+    test.checkTrue(clientDisconnected, "client has disconnected");
+    test.checkTrue(serverDisconnect, "client has disconnected on server");
+
+    Client::stop();
+    Server::stop();
+}
+
+static void testDisconnect(Test& test) {
+    static bool disconnected = false;
+    disconnected = false;
+    Client::resetHandler();
+    Client::setDisconnectHandler([]() { disconnected = true; });
+    if(checkError(test, Client::start(), "client can initialize")) {
+        return;
+    } else if(checkError(test, Client::connect("127.0.0.1", 54321, 90),
+                         "start connection failed")) {
+        return;
+    }
+    test.checkFalse(Client::isConnected(), "client not connected yet");
+    test.checkTrue(Client::isConnecting(), "client is connecting");
+    Client::disconnect(50);
+    tickClient(100);
+    test.checkFalse(Client::isConnected(), "client was disconnected");
+    test.checkFalse(Client::isConnecting(), "client is not connecting");
+    test.checkTrue(disconnected, "client has disconnected");
+    Client::stop();
+}
+
+static void testStop(Test& test) {
+    static bool disconnected = false;
+    disconnected = false;
+    Client::resetHandler();
+    Client::setDisconnectHandler([]() { disconnected = true; });
+    if(checkError(test, Client::start(), "client can initialize")) {
+        return;
+    } else if(checkError(test, Client::connect("127.0.0.1", 54321, 90),
+                         "start connection failed")) {
+        return;
+    }
+    test.checkFalse(Client::isConnected(), "client not connected yet");
+    test.checkTrue(Client::isConnecting(), "client is connecting");
+    Client::stop();
+    test.checkFalse(Client::isConnected(), "client was disconnected");
+    test.checkFalse(Client::isConnecting(), "client is not connecting");
+    test.checkTrue(disconnected, "client has disconnected");
+}*/
+
+void coreTestNetwork() {
+    // testConnect(test, PacketSendMode::UNSEQUENCED);
+    // testConnect(test, PacketSendMode::RELIABLE);
+    // testConnect(test, PacketSendMode::SEQUENCED);
+    // testDisconnect(test);
+    // testStop(test);
+}

BIN
test/resources/gray16.png


BIN
test/resources/gray8.png


BIN
test/resources/graya16.png


BIN
test/resources/graya8.png


BIN
test/resources/rgb16.png


BIN
test/resources/rgb8.png


BIN
test/resources/rgba16.png


BIN
test/resources/rgba8.png