diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..ea1fc60
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,9 @@
+BasedOnStyle: Microsoft
+DerivePointerAlignment: false
+PointerAlignment: Left
+IndentWidth: 4 # spaces per indent level
+TabWidth: 4 # width of a tab character
+UseTab: Never # options: Never, ForIndentation, Alwayss
+AccessModifierOffset: -4
+BreakTemplateDeclarations: Yes
+AllowShortFunctionsOnASingleLine: Inline
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..2b23f0f
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,151 @@
+cmake_minimum_required(VERSION 3.15)
+project(FekalniGtacko)
+
+# Enable C++20
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+set(SOURCES
+ "src/client/main.cpp"
+ "src/client/app.hpp"
+ "src/client/app.cpp"
+ "src/client/gl.hpp"
+ "src/client/utils.hpp"
+ "src/assets/animation.hpp"
+ "src/assets/animation.cpp"
+ "src/assets/cache.hpp"
+ "src/assets/cache.cpp"
+ "src/assets/cmdfile.hpp"
+ "src/assets/cmdfile.cpp"
+ "src/assets/model.hpp"
+ "src/assets/model.cpp"
+ "src/assets/mesh_builder.hpp"
+ "src/assets/mesh_builder.cpp"
+ "src/assets/map.hpp"
+ "src/assets/map.cpp"
+ "src/assets/skeleton.hpp"
+ "src/assets/skeleton.cpp"
+ "src/collision/trianglemesh.cpp"
+ # "src/game/entity.hpp"
+ # "src/game/entity.cpp"
+ # "src/game/meshinstance.hpp"
+ # "src/game/meshinstance.cpp"
+ "src/game/player_input.hpp"
+ # "src/game/player.hpp"
+ # "src/game/player.cpp"
+ "src/game/transform_node.hpp"
+ # "src/game/world.hpp"
+ # "src/game/world.cpp"
+ "src/gameview/client_session.hpp"
+ "src/gameview/entityview.hpp"
+ "src/gameview/worldview.hpp"
+ "src/gfx/buffer_object.cpp"
+ "src/gfx/buffer_object.hpp"
+ "src/gfx/draw_list.hpp"
+ "src/gfx/renderer.hpp"
+ "src/gfx/renderer.cpp"
+ "src/gfx/shader_defs.hpp"
+ "src/gfx/shader_sources.hpp"
+ "src/gfx/shader_sources.cpp"
+ "src/gfx/shader.hpp"
+ "src/gfx/shader.cpp"
+ "src/gfx/surface.hpp"
+ "src/gfx/texture.cpp"
+ "src/gfx/texture.hpp"
+ "src/gfx/uniform_buffer.hpp"
+ "src/gfx/vertex_array.cpp"
+ "src/gfx/vertex_array.hpp"
+ "src/net/defs.hpp"
+ "src/net/fixed_str.hpp"
+ "src/net/inmessage.hpp"
+ "src/net/outmessage.hpp"
+ "src/net/quantized.hpp"
+ "src/utils/files.hpp"
+ "src/utils/files.cpp"
+ "src/utils/transform.hpp"
+)
+
+
+if(ANDROID)
+ # Android-specific setup
+ set(MAIN_NAME "main")
+ add_library(${MAIN_NAME} SHARED ${SOURCES})
+ target_link_libraries(${MAIN_NAME} PRIVATE GLESv3 log android)
+else()
+ # Desktop build
+ set(MAIN_NAME "FekalniGtacko")
+ add_executable(${MAIN_NAME} ${SOURCES})
+endif()
+
+
+# Include directories
+target_include_directories(${MAIN_NAME} PRIVATE
+ "src"
+)
+
+# Platform-specific SDL2 handling
+if (CMAKE_SYSTEM_NAME STREQUAL Emscripten)
+
+ # Emscripten provides SDL2 via its system libraries
+ message(STATUS "Target platform: WebAssembly (Emscripten)")
+ set(CMAKE_EXECUTABLE_SUFFIX ".html") # Optional: build HTML page
+
+ target_compile_options(${MAIN_NAME} PRIVATE
+ "-sUSE_SDL=2"
+ "-sNO_DISABLE_EXCEPTION_CATCHING=1"
+ )
+
+ target_link_options(${MAIN_NAME} PRIVATE
+ "-sUSE_SDL=2"
+ "-sASYNCIFY"
+ "-sUSE_WEBGL2=1"
+ "-sNO_DISABLE_EXCEPTION_CATCHING=1"
+ "-sALLOW_MEMORY_GROWTH=1"
+ "--shell-file" "${CMAKE_SOURCE_DIR}/shell.html"
+ "--preload-file" "${CMAKE_SOURCE_DIR}/assets/@/"
+ )
+
+else()
+ message(STATUS "Target platform: Native")
+ # Native platform
+ # find_package(SDL2 REQUIRED)
+ # SDL2 build options to avoid unwanted components
+ set(SDL_TEST OFF CACHE BOOL "" FORCE)
+
+if(ANDROID)
+ set(SDL_SHARED ON CACHE BOOL "" FORCE)
+ set(SDL_STATIC OFF CACHE BOOL "" FORCE)
+else()
+ set(SDL_SHARED OFF CACHE BOOL "" FORCE)
+ set(SDL_STATIC ON CACHE BOOL "" FORCE)
+endif()
+
+ add_subdirectory(external/SDL)
+ target_include_directories(${MAIN_NAME} PRIVATE "external/SDL/include")
+
+ target_link_libraries(${MAIN_NAME} PRIVATE SDL2main)
+
+if(ANDROID)
+ target_link_libraries(${MAIN_NAME} PRIVATE SDL2)
+else()
+ target_link_libraries(${MAIN_NAME} PRIVATE SDL2-static)
+endif()
+
+ add_subdirectory(external/glad)
+ target_link_libraries(${MAIN_NAME} PRIVATE glad)
+
+endif()
+
+add_subdirectory(external/glm)
+target_link_libraries(${MAIN_NAME} PRIVATE glm)
+target_include_directories(${MAIN_NAME} PRIVATE "external/stb")
+
+set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
+set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE)
+set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE)
+set(BUILD_PYBULLET OFF CACHE BOOL "" FORCE)
+set(BUILD_OPENGL3_DEMOS OFF CACHE BOOL "" FORCE)
+
+add_subdirectory(external/bullet3)
+target_link_libraries(${MAIN_NAME} PRIVATE BulletCollision LinearMath Bullet3Common)
+target_include_directories(${MAIN_NAME} PRIVATE "external/bullet3/src")
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000..e55a882
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,62 @@
+{
+ "version": 4,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 21,
+ "patch": 0
+ },
+ "configurePresets": [
+ {
+ "name": "vs",
+ "displayName": "Visual Studio 18 2026",
+ "description": "Build with VS compiler using Multi-Config generator",
+ "generator": "Visual Studio 18 2026",
+ "toolset": "host=x64",
+ "architecture": "x64",
+ "binaryDir": "${sourceDir}/build/vs",
+ "cacheVariables": {
+ "CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
+ "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}",
+ "CMAKE_C_COMPILER": "cl.exe",
+ "CMAKE_CXX_COMPILER": "cl.exe"
+ }
+ },
+ {
+ "name": "emscripten",
+ "displayName": "Emscripten Multi-Config Build",
+ "description": "Build with Emscripten toolchain using Multi-Config generator",
+ "generator": "Ninja Multi-Config",
+ "binaryDir": "${sourceDir}/build/wasm",
+ "toolchainFile": "c:/dev/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
+ "cacheVariables": {
+ "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
+ }
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "vs-release",
+ "displayName": "VS Release",
+ "configurePreset": "vs",
+ "configuration": "Release"
+ },
+ {
+ "name": "vs-debug",
+ "displayName": "VS Debug",
+ "configurePreset": "vs",
+ "configuration": "Debug"
+ },
+ {
+ "name": "emscripten-release",
+ "displayName": "Emscripten Release",
+ "configurePreset": "emscripten",
+ "configuration": "Release"
+ },
+ {
+ "name": "emscripten-debug",
+ "displayName": "Emscripten Debug",
+ "configurePreset": "emscripten",
+ "configuration": "Debug"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2d00f4c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# FekalniGtacko
+
diff --git a/shell.html b/shell.html
new file mode 100644
index 0000000..347c9f3
--- /dev/null
+++ b/shell.html
@@ -0,0 +1,59 @@
+
+
+
+
+
+ PortalGame
+
+
+
+
+
+
+
+
+
+
+ {{{ SCRIPT }}}
+
+
+
\ No newline at end of file
diff --git a/src/assets/animation.cpp b/src/assets/animation.cpp
new file mode 100644
index 0000000..f44263e
--- /dev/null
+++ b/src/assets/animation.cpp
@@ -0,0 +1,142 @@
+#include "animation.hpp"
+#include "skeleton.hpp"
+
+#include "utils/files.hpp"
+#include
+#include
+
+std::shared_ptr assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton)
+{
+ std::istringstream ifs = fs::ReadFileAsStream(filename);
+
+ std::shared_ptr anim = std::make_shared();
+
+ int last_frame = 0;
+ std::vector frame_indices;
+
+ auto FillFrameRefs = [&](int end_frame)
+ {
+ int channel_start = ((int)anim->channels_.size() - 1) * (int)anim->num_frames_;
+ int target_size = channel_start + end_frame;
+ size_t num_frame_refs = frame_indices.size();
+
+ if (num_frame_refs >= target_size)
+ {
+ return; // Already filled
+ }
+
+ if (num_frame_refs % anim->num_frames_ == 0)
+ {
+ throw std::runtime_error("Cannot fill frames of channel that has 0 frames: " + filename);
+ }
+
+ size_t last_frame_idx = frame_indices.back();
+ frame_indices.resize(target_size, last_frame_idx);
+ };
+
+ std::string line;
+
+ while (std::getline(ifs, line))
+ {
+ if (line.empty() || line[0] == '#') // Skip empty lines and comments
+ continue;
+
+ std::istringstream iss(line);
+
+ std::string command;
+ iss >> command;
+
+ if (command == "f")
+ {
+ if (anim->num_frames_ == 0)
+ {
+ throw std::runtime_error("Frame data specified before number of frames in animation file: " + filename);
+ }
+
+ if (anim->channels_.empty())
+ {
+ throw std::runtime_error("Frame data specified before any channels in animation file: " + filename);
+ }
+
+ int frame_index;
+ iss >> frame_index;
+
+ if (frame_index < 0 || frame_index >= (int)anim->num_frames_)
+ {
+ throw std::runtime_error("Frame index out of bounds in animation file: " + filename);
+ }
+
+ if (frame_index < last_frame)
+ {
+ throw std::runtime_error("Frame indices must be in ascending order in animation file: " + filename);
+ }
+
+ last_frame = frame_index;
+
+ Transform t;
+ iss >> t.position.x >> t.position.y >> t.position.z;
+ glm::vec3 angles_deg;
+ iss >> angles_deg.x >> angles_deg.y >> angles_deg.z;
+ t.SetAngles(angles_deg);
+ iss >> t.scale;
+
+ size_t idx = anim->frames_.size();
+ anim->frames_.push_back(t);
+
+ FillFrameRefs(frame_index); // Fill to current frame
+ frame_indices.push_back(idx);
+ }
+ else if (command == "ch")
+ {
+ std::string name;
+ iss >> name;
+
+ int bone_index = skeleton->GetBoneIndex(name);
+
+ if (bone_index < 0)
+ {
+ throw std::runtime_error("Bone referenced in animation not found in provided skeleton: " + name);
+ }
+
+ FillFrameRefs(anim->num_frames_); // Fill to end for last channel
+
+ AnimationChannel& channel = anim->channels_.emplace_back();
+ channel.bone_index = bone_index;
+ channel.frames = nullptr; // Will be set up later
+
+ last_frame = 0;
+
+ }
+ else if (command == "frames")
+ {
+ iss >> anim->num_frames_;
+ }
+ else if (command == "fps")
+ {
+ iss >> anim->tps_;
+ }
+ }
+
+ if (anim->channels_.empty())
+ {
+ throw std::runtime_error("No channels found in animation file: " + filename);
+ }
+
+ FillFrameRefs(anim->num_frames_); // Fill to end for last channel
+
+ // Set up frame pointers
+ anim->frame_refs_.resize(frame_indices.size());
+ for (size_t i = 0; i < frame_indices.size(); ++i)
+ {
+ anim->frame_refs_[i] = &anim->frames_[frame_indices[i]];
+ }
+
+ // Set up channel frame pointers
+ for (size_t i = 0; i < anim->channels_.size(); ++i)
+ {
+ AnimationChannel& channel = anim->channels_[i];
+ channel.frames = &anim->frame_refs_[i * anim->num_frames_];
+ }
+
+ return anim;
+}
diff --git a/src/assets/animation.hpp b/src/assets/animation.hpp
new file mode 100644
index 0000000..6ef01f8
--- /dev/null
+++ b/src/assets/animation.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include
+#include
+#include
+
+#include "utils/transform.hpp"
+
+namespace assets
+{
+ class Skeleton;
+
+ struct AnimationChannel
+ {
+ int bone_index;
+ const Transform* const* frames;
+ };
+
+ class Animation
+ {
+ size_t num_frames_ = 0;
+ float tps_ = 24.0f;
+
+ std::vector channels_;
+ std::vector frame_refs_;
+ std::vector frames_;
+
+ public:
+ Animation() = default;
+
+ size_t GetNumFrames() const { return num_frames_; }
+ float GetTPS() const { return tps_; }
+ float GetDuration() const { return static_cast(num_frames_) / tps_; }
+
+ size_t GetNumChannels() const { return channels_.size(); }
+ const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
+
+ static std::shared_ptr LoadFromFile(const std::string& filename, const Skeleton* skeleton);
+
+ };
+
+
+}
\ No newline at end of file
diff --git a/src/assets/cache.cpp b/src/assets/cache.cpp
new file mode 100644
index 0000000..9d366d5
--- /dev/null
+++ b/src/assets/cache.cpp
@@ -0,0 +1,5 @@
+#include "cache.hpp"
+
+assets::TextureCache assets::CacheManager::texture_cache_;
+assets::SkeletonCache assets::CacheManager::skeleton_cache_;
+assets::ModelCache assets::CacheManager::model_cache_;
diff --git a/src/assets/cache.hpp b/src/assets/cache.hpp
new file mode 100644
index 0000000..ac728f6
--- /dev/null
+++ b/src/assets/cache.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "model.hpp"
+#include "skeleton.hpp"
+#include "gfx/texture.hpp"
+
+namespace assets
+{
+
+template
+class Cache
+{
+public:
+ using PtrType = std::shared_ptr;
+
+ PtrType Get(const std::string& key)
+ {
+ auto it = cache_.find(key);
+ if (it != cache_.end())
+ {
+ if (auto ptr = it->second.lock())
+ {
+ return ptr; // Return cached object
+ }
+ }
+
+ PtrType obj = Load(key);
+ cache_[key] = obj; // Cache the loaded object
+ return obj;
+ }
+
+protected:
+ virtual PtrType Load(const std::string& key) = 0;
+
+private:
+ std::map> cache_;
+};
+
+class TextureCache final : public Cache
+{
+protected:
+ PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); }
+};
+
+class SkeletonCache final : public Cache
+{
+protected:
+ PtrType Load(const std::string& key) override { return Skeleton::LoadFromFile(key); }
+};
+
+class ModelCache final : public Cache
+{
+protected:
+ PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); }
+};
+
+class CacheManager
+{
+public:
+ static std::shared_ptr GetTexture(const std::string& filename) {
+ return texture_cache_.Get(filename);
+ }
+
+ static std::shared_ptr GetSkeleton(const std::string& filename) {
+ return skeleton_cache_.Get(filename);
+ }
+
+ static std::shared_ptr GetModel(const std::string& filename) {
+ return model_cache_.Get(filename);
+ }
+
+private:
+ static TextureCache texture_cache_;
+ static SkeletonCache skeleton_cache_;
+ static ModelCache model_cache_;
+
+};
+
+} // namespace assets
\ No newline at end of file
diff --git a/src/assets/cmdfile.cpp b/src/assets/cmdfile.cpp
new file mode 100644
index 0000000..e1880bd
--- /dev/null
+++ b/src/assets/cmdfile.cpp
@@ -0,0 +1,25 @@
+#include "cmdfile.hpp"
+
+void assets::LoadCMDFile(const std::string& filename,
+ const std::function& handler)
+{
+ std::istringstream file = fs::ReadFileAsStream(filename);
+
+ std::string line;
+ while (std::getline(file, line))
+ {
+ if (line.empty() || line[0] == '#') // Skip empty lines and comments
+ continue;
+
+ // skip whitespace
+ line.erase(0, line.find_first_not_of(" \t"));
+
+ std::istringstream iss(line);
+
+ std::string command;
+ iss >> command;
+
+ handler(command, iss);
+ }
+
+}
diff --git a/src/assets/cmdfile.hpp b/src/assets/cmdfile.hpp
new file mode 100644
index 0000000..d7999f6
--- /dev/null
+++ b/src/assets/cmdfile.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include "utils/files.hpp"
+#include
+#include
+#include
+#include
+
+namespace assets
+{
+
+void LoadCMDFile(const std::string& filename,
+ const std::function& handler);
+
+} // namespace assets
\ No newline at end of file
diff --git a/src/assets/map.cpp b/src/assets/map.cpp
new file mode 100644
index 0000000..68ca82f
--- /dev/null
+++ b/src/assets/map.cpp
@@ -0,0 +1,49 @@
+#include "map.hpp"
+#include "cache.hpp"
+#include "utils/files.hpp"
+#include "cmdfile.hpp"
+
+std::shared_ptr assets::Map::LoadFromFile(const std::string& filename)
+{
+ auto map = std::make_shared