This commit is contained in:
tovjemam 2025-12-29 21:33:54 +01:00
parent 9d581bae56
commit a80e405b3e
56 changed files with 3117 additions and 0 deletions

9
.clang-format Normal file
View File

@ -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

151
CMakeLists.txt Normal file
View File

@ -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")

62
CMakePresets.json Normal file
View File

@ -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"
}
]
}

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# FekalniGtacko

59
shell.html Normal file
View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PortalGame</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100%;
background: black;
}
canvas {
display: block;
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<canvas id="canvas" oncontextmenu="event.preventDefault()"></canvas>
<script>
var Module = {
preRun: [],
postRun: [],
canvas: (function() {
return document.getElementById('canvas');
})()
};
// Resize canvas to fit the window and inform SDL (via Emscripten)
function resizeCanvas() {
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// // Notify Emscripten SDL2
// if (typeof Module !== 'undefined' && Module.canvas) {
// const evt = new CustomEvent('resize');
// window.dispatchEvent(evt);
// }
}
window.addEventListener('resize', resizeCanvas);
window.addEventListener('load', resizeCanvas);
</script>
{{{ SCRIPT }}}
</body>
</html>

142
src/assets/animation.cpp Normal file
View File

@ -0,0 +1,142 @@
#include "animation.hpp"
#include "skeleton.hpp"
#include "utils/files.hpp"
#include <sstream>
#include <stdexcept>
std::shared_ptr<const assets::Animation> assets::Animation::LoadFromFile(const std::string& filename, const Skeleton* skeleton)
{
std::istringstream ifs = fs::ReadFileAsStream(filename);
std::shared_ptr<Animation> anim = std::make_shared<Animation>();
int last_frame = 0;
std::vector<size_t> 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;
}

43
src/assets/animation.hpp Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <vector>
#include <memory>
#include <string>
#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<AnimationChannel> channels_;
std::vector<const Transform*> frame_refs_;
std::vector<Transform> frames_;
public:
Animation() = default;
size_t GetNumFrames() const { return num_frames_; }
float GetTPS() const { return tps_; }
float GetDuration() const { return static_cast<float>(num_frames_) / tps_; }
size_t GetNumChannels() const { return channels_.size(); }
const AnimationChannel& GetChannel(int index) const { return channels_[index]; }
static std::shared_ptr<const Animation> LoadFromFile(const std::string& filename, const Skeleton* skeleton);
};
}

5
src/assets/cache.cpp Normal file
View File

@ -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_;

79
src/assets/cache.hpp Normal file
View File

@ -0,0 +1,79 @@
#pragma once
#include "model.hpp"
#include "skeleton.hpp"
#include "gfx/texture.hpp"
namespace assets
{
template <typename T>
class Cache
{
public:
using PtrType = std::shared_ptr<const T>;
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<std::string, std::weak_ptr<const T>> cache_;
};
class TextureCache final : public Cache<gfx::Texture>
{
protected:
PtrType Load(const std::string& key) override { return gfx::Texture::LoadFromFile(key); }
};
class SkeletonCache final : public Cache<Skeleton>
{
protected:
PtrType Load(const std::string& key) override { return Skeleton::LoadFromFile(key); }
};
class ModelCache final : public Cache<Model>
{
protected:
PtrType Load(const std::string& key) override { return Model::LoadFromFile(key); }
};
class CacheManager
{
public:
static std::shared_ptr<const gfx::Texture> GetTexture(const std::string& filename) {
return texture_cache_.Get(filename);
}
static std::shared_ptr<const Skeleton> GetSkeleton(const std::string& filename) {
return skeleton_cache_.Get(filename);
}
static std::shared_ptr<const Model> 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

25
src/assets/cmdfile.cpp Normal file
View File

@ -0,0 +1,25 @@
#include "cmdfile.hpp"
void assets::LoadCMDFile(const std::string& filename,
const std::function<void(const std::string& command, std::istringstream& iss)>& 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);
}
}

15
src/assets/cmdfile.hpp Normal file
View File

@ -0,0 +1,15 @@
#pragma once
#include "utils/files.hpp"
#include <functional>
#include <sstream>
#include <stdexcept>
#include <string>
namespace assets
{
void LoadCMDFile(const std::string& filename,
const std::function<void(const std::string& command, std::istringstream& iss)>& handler);
} // namespace assets

49
src/assets/map.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "map.hpp"
#include "cache.hpp"
#include "utils/files.hpp"
#include "cmdfile.hpp"
std::shared_ptr<const assets::Map> assets::Map::LoadFromFile(const std::string& filename)
{
auto map = std::make_shared<Map>();
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "basemodel")
{
std::string model_name;
iss >> model_name;
map->basemodel_ = CacheManager::GetModel("data/" + model_name + ".mdl");
}
else if (command == "static")
{
MapStaticObject obj;
std::string model_name;
iss >> model_name;
obj.model = assets::CacheManager::GetModel("data/" + model_name + ".mdl");
glm::vec3 angles;
iss >> obj.transform.position.x >> obj.transform.position.y >> obj.transform.position.z;
iss >> angles.x >> angles.y >> angles.z;
obj.transform.SetAngles(angles);
iss >> obj.transform.scale;
std::string flag;
while (iss >> flag)
{
if (flag == "+color")
{
iss >> obj.color.r >> obj.color.g >> obj.color.b;
}
}
map->static_objects_.push_back(std::move(obj));
}
});
return map;
}

30
src/assets/map.hpp Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include <memory>
#include <string>
#include "model.hpp"
#include "utils/transform.hpp"
namespace assets
{
struct MapStaticObject
{
Transform transform;
std::shared_ptr<const Model> model;
glm::vec3 color = glm::vec3(1.0f);
};
class Map
{
public:
Map() = default;
static std::shared_ptr<const Map> LoadFromFile(const std::string& filename);
private:
std::shared_ptr<const Model> basemodel_;
std::vector<MapStaticObject> static_objects_;
};
} // namespace assets

116
src/assets/mesh_builder.cpp Normal file
View File

@ -0,0 +1,116 @@
#include "mesh_builder.hpp"
#include <stdexcept>
assets::MeshBuilder::MeshBuilder(gfx::MeshFlags mflags) : mflags_(mflags) {}
void assets::MeshBuilder::BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr<const gfx::Texture> texture)
{
if (!mesh_)
mesh_ = std::make_shared<Mesh>();
FinalizeSurface();
gfx::Surface surface;
surface.sflags = sflags;
surface.texture = texture;
surface.first = tris_.size();
mesh_->surfaces.push_back(surface);
mesh_->surface_names[name] = mesh_->surfaces.size() - 1;
}
void assets::MeshBuilder::AddVertex(const MeshVertex& v)
{
verts_.push_back(v);
}
void assets::MeshBuilder::AddTriangle(const MeshTriangle& t)
{
if (!mesh_ || mesh_->surfaces.size() == 0)
{
throw std::runtime_error("Cannot add triangle without a surface. Call BeginSurface() first.");
}
tris_.push_back(t);
}
template <class T>
static void BufferPut(std::vector<char>& buffer, const T& val)
{
const char* data = reinterpret_cast<const char*>(&val);
buffer.insert(buffer.end(), data, data + sizeof(T));
}
static int GetVertexAttrFlags(gfx::MeshFlags mflags)
{
int attrs = gfx::VA_POSITION | gfx::VA_NORMAL | gfx::VA_UV;
if (mflags & gfx::MF_LIGHTMAP_UV)
{
attrs |= gfx::VA_LIGHTMAP_UV;
}
if (mflags & gfx::MF_SKELETAL)
{
attrs |= gfx::VA_BONE_INDICES | gfx::VA_BONE_WEIGHTS;
}
return attrs;
}
void assets::MeshBuilder::Build()
{
// Finalize last surface
FinalizeSurface();
// Generate VBO data
std::vector<char> buffer;
for (const MeshVertex& vert : verts_)
{
BufferPut(buffer, vert.pos);
BufferPut(buffer, vert.normal);
BufferPut(buffer, vert.uv);
if (mflags_ & gfx::MF_LIGHTMAP_UV)
{
BufferPut(buffer, vert.lightmap_uv);
}
if (mflags_ & gfx::MF_SKELETAL)
{
for (int i = 0; i < 4; ++i)
{
BufferPut(buffer, static_cast<int32_t>(vert.bones[i].bone_index));
}
for (int i = 0; i < 4; ++i)
{
BufferPut(buffer, vert.bones[i].weight);
}
}
}
// populate VA
std::shared_ptr<gfx::VertexArray> va =
std::make_shared<gfx::VertexArray>(GetVertexAttrFlags(mflags_), gfx::VF_CREATE_EBO);
va->SetVBOData(buffer.data(), buffer.size());
va->SetIndices(reinterpret_cast<const GLuint*>(tris_.data()), tris_.size() * 3U);
// Assign VA to surfaces & set mesh flags
for (gfx::Surface& surface : mesh_->surfaces)
{
surface.va = va;
surface.mflags = mflags_;
}
}
void assets::MeshBuilder::FinalizeSurface()
{
if (mesh_->surfaces.size() > 0)
{
// finalize previous surface
gfx::Surface& prev_surface = mesh_->surfaces.back();
prev_surface.count = tris_.size() - prev_surface.first;
}
}

View File

@ -0,0 +1,64 @@
#pragma once
#include "gfx/surface.hpp"
#include <cstdint>
#include <glm/glm.hpp>
#include <vector>
#include <map>
namespace assets
{
struct MeshVertexBoneInfluence
{
int bone_index;
float weight;
};
struct MeshVertex
{
glm::vec3 pos;
glm::vec3 normal;
glm::vec2 uv;
glm::vec2 lightmap_uv;
MeshVertexBoneInfluence bones[4];
};
struct MeshTriangle
{
uint32_t vert[3];
};
struct Mesh
{
gfx::MeshFlags mflags = gfx::MF_NONE;
std::vector<gfx::Surface> surfaces;
std::map<std::string, size_t> surface_names;
};
class MeshBuilder
{
public:
MeshBuilder(gfx::MeshFlags mflags);
void BeginSurface(gfx::SurfaceFlags sflags, const std::string& name, std::shared_ptr<const gfx::Texture> texture);
void AddVertex(const MeshVertex& v);
void AddTriangle(const MeshTriangle& t);
void Build();
void SetMeshFlag(gfx::MeshFlags flag) { mflags_ |= flag; }
std::shared_ptr<const Mesh> GetMesh() const { return mesh_; }
private:
void FinalizeSurface();
private:
gfx::MeshFlags mflags_ = 0;
std::vector<MeshVertex> verts_;
std::vector<MeshTriangle> tris_;
std::shared_ptr<Mesh> mesh_;
};
} // namespace assets

71
src/assets/model.cpp Normal file
View File

@ -0,0 +1,71 @@
#include "model.hpp"
#include "cmdfile.hpp"
#include "cache.hpp"
std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::string& filename)
{
auto model = std::make_shared<Model>();
MeshBuilder mb(gfx::MF_NONE);
LoadCMDFile(filename, [&](const std::string& command, std::istringstream& iss) {
if (command == "v")
{
MeshVertex v;
iss >> v.pos.x >> v.pos.y >> v.pos.z;
iss >> v.normal.x >> v.normal.y >> v.normal.z;
iss >> v.uv.x >> v.uv.y;
// TODO: LUV & bone data
mb.AddVertex(v);
}
else if (command == "f")
{
MeshTriangle t;
iss >> t.vert[0] >> t.vert[1] >> t.vert[2];
mb.AddTriangle(t);
}
else if (command == "surface")
{
std::string surface_name, texture_name;
gfx::SurfaceFlags sflags = gfx::SF_NONE;
iss >> surface_name;
// Optional flags
std::string flag;
while (iss >> flag)
{
if (flag == "+texture")
iss >> texture_name;
else if (flag == "+doublesided")
sflags |= gfx::SF_DOUBLE_SIDED;
else if (flag == "+transparent")
sflags |= gfx::SF_TRANSPARENT;
else if (flag == "+ocolor")
sflags |= gfx::SF_OBJECT_COLOR;
}
std::shared_ptr<const gfx::Texture> texture;
if (!texture_name.empty())
{
texture = CacheManager::GetTexture("data/" + surface_name + ".png");
}
mb.BeginSurface(sflags, surface_name, texture);
}
else
{
throw std::runtime_error("Unknown command in model file: " + command);
}
// TODO: skeleton
});
return model;
}

21
src/assets/model.hpp Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include "mesh_builder.hpp"
namespace assets
{
class Model
{
public:
Model() = default;
static std::shared_ptr<const Model> LoadFromFile(const std::string& filename);
const std::shared_ptr<const Mesh>& GetMesh() const { return mesh_; }
private:
std::shared_ptr<const Mesh> mesh_;
};
}

95
src/assets/skeleton.cpp Normal file
View File

@ -0,0 +1,95 @@
#include "skeleton.hpp"
#include "utils/files.hpp"
#include <sstream>
#include <stdexcept>
void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform)
{
int index = static_cast<int>(bones_.size());
Bone& bone = bones_.emplace_back();
bone.name = name;
bone.parent_idx = GetBoneIndex(parent_name);
bone.bind_transform = transform;
bone.inv_bind_matrix = glm::inverse(transform.ToMatrix());
bone_map_[bone.name] = index;
}
int assets::Skeleton::GetBoneIndex(const std::string& name) const
{
auto it = bone_map_.find(name);
if (it != bone_map_.end()) {
return it->second;
}
return -1;
}
const assets::Animation* assets::Skeleton::GetAnimation(const std::string& name) const
{
auto it = anims_.find(name);
if (it != anims_.end()) {
return it->second.get();
}
return nullptr;
}
std::shared_ptr<const assets::Skeleton> assets::Skeleton::LoadFromFile(const std::string& filename)
{
std::istringstream ifs = fs::ReadFileAsStream(filename);
std::shared_ptr<Skeleton> skeleton = std::make_shared<Skeleton>();
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 == "b")
{
Transform t;
glm::vec3 angles;
std::string bone_name, parent_name;
iss >> bone_name >> parent_name;
iss >> t.position.x >> t.position.y >> t.position.z;
iss >> angles.x >> angles.y >> angles.z;
iss >> t.scale;
if (iss.fail())
{
throw std::runtime_error("Failed to parse bone definition in file: " + filename);
}
t.SetAngles(angles);
skeleton->AddBone(bone_name, parent_name, t);
}
else if (command == "anim")
{
std::string anim_name, anim_filename;
iss >> anim_name >> anim_filename;
if (iss.fail())
{
throw std::runtime_error("Failed to parse animation definition in file: " + filename);
}
std::shared_ptr<const Animation> anim = Animation::LoadFromFile("data/" + anim_filename + ".anim", skeleton.get());
skeleton->AddAnimation(anim_name, anim);
}
}
return skeleton;
}

47
src/assets/skeleton.hpp Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <memory>
#include "utils/transform.hpp"
#include "animation.hpp"
namespace assets
{
struct Bone
{
int parent_idx;
std::string name;
Transform bind_transform;
glm::mat4 inv_bind_matrix;
};
class Skeleton
{
std::vector<Bone> bones_;
std::map<std::string, int> bone_map_;
std::map<std::string, std::shared_ptr<const Animation>> anims_;
public:
Skeleton() = default;
void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform);
int GetBoneIndex(const std::string& name) const;
size_t GetNumBones() const { return bones_.size(); }
const Bone& GetBone(size_t idx) const { return bones_[idx]; }
const Animation* GetAnimation(const std::string& name) const;
static std::shared_ptr<const Skeleton> LoadFromFile(const std::string& filename);
private:
void AddAnimation(const std::string& name, const std::shared_ptr<const Animation>& anim) { anims_[name] = anim; }
};
}

50
src/client/app.cpp Normal file
View File

@ -0,0 +1,50 @@
#include "app.hpp"
#include <iostream>
App::App()
{
std::cout << "Initializing App..." << std::endl;
}
void App::Frame()
{
float delta_time = time_ - prev_time_;
prev_time_ = time_;
if (delta_time < 0.0f)
{
delta_time = 0.0f; // Prevent negative delta time
}
else if (delta_time > 0.1f)
{
delta_time = 0.1f; // Cap delta time to avoid large jumps
}
// detect inputs originating in this frame
game::PlayerInputFlags new_input = input_ & ~prev_input_;
prev_input_ = input_;
float aspect = static_cast<float>(viewport_size_.x) / static_cast<float>(viewport_size_.y);
renderer_.Begin(viewport_size_.x, viewport_size_.y);
// TODO: draw world
}
void App::MouseMove(const glm::vec2& delta)
{
float sensitivity = 0.002f; // Sensitivity factor for mouse movement
float delta_yaw = delta.x * sensitivity;
float delta_pitch = -delta.y * sensitivity;
// TODO: rotate
}
App::~App()
{
}

29
src/client/app.hpp Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "game/player_input.hpp"
#include "gfx/renderer.hpp"
class App
{
float time_ = 0.0f;
glm::ivec2 viewport_size_ = { 800, 600 };
game::PlayerInputFlags input_ = 0;
game::PlayerInputFlags prev_input_ = 0;
float prev_time_ = 0.0f;
gfx::Renderer renderer_;
public:
App();
void Frame();
void SetTime(float time) { time_ = time; }
void SetViewportSize(int width, int height) { viewport_size_ = { width, height }; }
void SetInput(game::PlayerInputFlags input) { input_ = input; }
void MouseMove(const glm::vec2& delta);
~App();
};

8
src/client/gl.hpp Normal file
View File

@ -0,0 +1,8 @@
#if defined(EMSCRIPTEN) || defined(ANDROID)
#define PG_GLES
#include <GLES3/gl3.h>
// #include <GLES3/gl2ext.h>
#else
#include <glad/glad.h>
#endif

243
src/client/main.cpp Normal file
View File

@ -0,0 +1,243 @@
#include <SDL.h>
#include <iostream>
#include <memory>
#include "app.hpp"
#ifdef EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/html5_webgl.h>
#endif
#include "gl.hpp"
static SDL_Window *s_window = nullptr;
static SDL_GLContext s_context = nullptr;
static bool s_quit = false;
static std::unique_ptr<App> s_app;
static void ThrowSDLError(const std::string& message)
{
std::string error = SDL_GetError();
throw std::runtime_error(message + ": " + error);
}
static void InitSDL()
{
std::cout << "Initializing SDL..." << std::endl;
if (SDL_Init(SDL_INIT_VIDEO) != 0)
{
ThrowSDLError("SDL_Init");
}
#ifdef PG_GLES
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
std::cout << "Creating SDL window..." << std::endl;
s_window = SDL_CreateWindow("PortalGame", 100, 100, 640, 480, SDL_WINDOW_SHOWN | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!s_window)
{
ThrowSDLError("SDL_CreateWindow");
}
}
#ifndef PG_GLES
static void APIENTRY GLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) {
//if (severity == 0x826b)
// return;
//
////std::cout << message << std::endl;
fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
(type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
type, severity, message);
}
#endif // PG_GLES
static void InitGL()
{
std::cout << "Creating OpenGL context..." << std::endl;
s_context = SDL_GL_CreateContext(s_window);
if (!s_context)
{
ThrowSDLError("SDL_GL_CreateContext");
}
// Make context current
if (SDL_GL_MakeCurrent(s_window, s_context) != 0)
{
SDL_GL_DeleteContext(s_context);
ThrowSDLError("SDL_GL_MakeCurrent");
}
#ifndef PG_GLES
// Initialize GLAD
std::cout << "Initializing GLAD..." << std::endl;
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{
SDL_GL_DeleteContext(s_context);
throw std::runtime_error("Failed to initialize GLAD");
}
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(GLDebugCallback, 0);
SDL_GL_SetSwapInterval(0);
#endif // PG_GLES
}
static void ShutdownGL()
{
if (s_context)
{
SDL_GL_DeleteContext(s_context);
s_context = nullptr;
}
}
static void ShutdownSDL()
{
if (s_window)
{
SDL_DestroyWindow(s_window);
s_window = nullptr;
}
SDL_Quit();
}
static void PollEvents()
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
switch (event.type)
{
case SDL_QUIT:
s_quit = true;
return;
case SDL_MOUSEMOTION:
int xrel = event.motion.xrel;
int yrel = event.motion.yrel;
if (xrel != 0 || yrel != 0)
{
s_app->MouseMove(glm::vec2(static_cast<float>(xrel), static_cast<float>(yrel)));
}
break;
}
}
}
static void Frame()
{
Uint32 current_time = SDL_GetTicks();
s_app->SetTime(current_time / 1000.0f); // Set time in seconds
PollEvents();
int width, height;
SDL_GetWindowSize(s_window, &width, &height);
s_app->SetViewportSize(width, height);
game::PlayerInputFlags input = 0;
const uint8_t* kbd_state = SDL_GetKeyboardState(nullptr);
if (kbd_state[SDL_GetScancodeFromKey(SDLK_w)])
input |= game::PI_FORWARD;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_s)])
input |= game::PI_BACKWARD;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_a)])
input |= game::PI_LEFT;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_d)])
input |= game::PI_RIGHT;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_SPACE)])
input |= game::PI_JUMP;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_LCTRL)])
input |= game::PI_CROUCH;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_e)])
input |= game::PI_USE;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F3)])
input |= game::PI_DEBUG1;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F4)])
input |= game::PI_DEBUG2;
if (kbd_state[SDL_GetScancodeFromKey(SDLK_F5)])
input |= game::PI_DEBUG3;
int mouse_state = SDL_GetMouseState(nullptr, nullptr);
if (mouse_state & SDL_BUTTON(SDL_BUTTON_LEFT))
input |= game::PI_ATTACK;
s_app->SetInput(input);
s_app->Frame();
SDL_GL_SwapWindow(s_window);
}
static void Main() {
InitSDL();
try
{
InitGL();
}
catch (...)
{
ShutdownSDL();
throw;
}
SDL_SetRelativeMouseMode(SDL_TRUE);
s_app = std::make_unique<App>();
#ifdef EMSCRIPTEN
emscripten_set_main_loop(Frame, 0, true);
#else
while (!s_quit)
{
Frame();
}
s_app.reset();
ShutdownGL();
ShutdownSDL();
#endif
}
int main(int argc, char *argv[])
{
try {
Main();
}
catch (const std::exception& e) {
std::cerr << "[ERROR] " << e.what() << std::endl;
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", e.what(), nullptr);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

13
src/client/utils.hpp Normal file
View File

@ -0,0 +1,13 @@
#pragma once
// Trida zajistujici, ze je objekt nekopirovatelny a nepresouvatelny
class NonCopyableNonMovable {
public:
NonCopyableNonMovable() = default;
NonCopyableNonMovable(const NonCopyableNonMovable&) = delete;
NonCopyableNonMovable& operator=(const NonCopyableNonMovable&) = delete;
NonCopyableNonMovable(NonCopyableNonMovable&&) = delete;
NonCopyableNonMovable& operator=(NonCopyableNonMovable&&) = delete;
};

54
src/collision/aabb.hpp Normal file
View File

@ -0,0 +1,54 @@
#pragma once
#include <glm/glm.hpp>
#include <limits>
namespace collision
{
template <size_t dim>
struct AABB
{
static constexpr float C_INFINITY = std::numeric_limits<float>::infinity();
using Vec = glm::vec<dim, float, glm::defaultp>;
Vec min = Vec(C_INFINITY);
Vec max = Vec(-C_INFINITY);
AABB() = default;
AABB(const Vec& min, const Vec& max) : min(min), max(max) {}
void AddPoint(const Vec& point)
{
min = glm::min(min, point);
max = glm::max(max, point);
}
bool CollidesWith(const AABB<dim>& other) const;
AABB<dim> Intersection(const AABB<dim>& other) const
{
Vec new_min = glm::max(min, other.min);
Vec new_max = glm::min(max, other.max);
return AABB<dim>(new_min, new_max);
}
};
template <>
inline bool AABB<2>::CollidesWith(const AABB<2>& other) const
{
return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y);
}
template <>
inline bool AABB<3>::CollidesWith(const AABB<3>& other) const
{
return (min.x <= other.max.x && max.x >= other.min.x) &&
(min.y <= other.max.y && max.y >= other.min.y) &&
(min.z <= other.max.z && max.z >= other.min.z);
}
using AABB2 = AABB<2>;
using AABB3 = AABB<3>;
}

View File

@ -0,0 +1,20 @@
#include "trianglemesh.hpp"
collision::TriangleMesh::TriangleMesh()
{
}
void collision::TriangleMesh::AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2)
{
btVector3 bt_v0(v0.x, v0.y, v0.z);
btVector3 bt_v1(v1.x, v1.y, v1.z);
btVector3 bt_v2(v2.x, v2.y, v2.z);
bt_mesh_.addTriangle(bt_v0, bt_v1, bt_v2, true);
}
void collision::TriangleMesh::Build()
{
bt_shape_ = std::make_unique<btBvhTriangleMeshShape>(&bt_mesh_, true, true);
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include <span>
#include <glm/glm.hpp>
#include "aabb.hpp"
#include <memory>
#include <btBulletCollisionCommon.h>
namespace collision
{
class TriangleMesh
{
btTriangleMesh bt_mesh_;
std::unique_ptr<btBvhTriangleMeshShape> bt_shape_;
public:
TriangleMesh();
void AddTriangle(const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2);
void Build();
btBvhTriangleMeshShape* GetShape() { return bt_shape_.get(); }
};
}

23
src/game/player_input.hpp Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
namespace game
{
using PlayerInputFlags = uint16_t;
enum PlayerInputFlag : PlayerInputFlags
{
PI_FORWARD = 1 << 0,
PI_BACKWARD = 1 << 1,
PI_LEFT = 1 << 2,
PI_RIGHT = 1 << 3,
PI_JUMP = 1 << 4,
PI_CROUCH = 1 << 5,
PI_USE = 1 << 6,
PI_ATTACK = 1 << 7,
PI_DEBUG1 = 1 << 8,
PI_DEBUG2 = 1 << 9,
PI_DEBUG3 = 1 << 10,
};
}

View File

@ -0,0 +1,29 @@
#pragma once
#include "utils/transform.hpp"
namespace game
{
struct TransformNode
{
const TransformNode* parent = nullptr;
Transform local_transform;
glm::mat4 matrix = glm::mat4(1.0f); // Global
TransformNode()
{
UpdateMatrix();
}
void UpdateMatrix()
{
matrix = local_transform.ToMatrix();
if (parent)
{
matrix = parent->matrix * matrix;
}
}
};
}

View File

@ -0,0 +1,12 @@
#pragma once
namespace game::view
{
class ClientSession
{
public:
private:
};
} // namespace game::view

View File

@ -0,0 +1,26 @@
#pragma once
#include "game/transform_node.hpp"
#include "gfx/draw_list.hpp"
class World;
namespace game::view
{
class EntityView
{
public:
EntityView(World& world) : world_(world) {}
virtual void Draw(gfx::DrawList& dlist) {}
virtual void Update() {}
protected:
World& world_;
TransformNode root_;
bool visible_ = false;
};
} // namespace game::view

View File

@ -0,0 +1,14 @@
#pragma once
namespace game::view
{
class WorldView
{
public:
private:
};
} // namespace game::view

29
src/gfx/buffer_object.cpp Normal file
View File

@ -0,0 +1,29 @@
#include <stdexcept>
#include "buffer_object.hpp"
gfx::BufferObject::BufferObject(GLenum target, GLenum usage) : m_target(target), m_usage(usage), m_size(0U) {
glGenBuffers(1, &m_id);
}
gfx::BufferObject::~BufferObject() {
glDeleteBuffers(1, &m_id);
}
void gfx::BufferObject::Bind() const {
glBindBuffer(m_target, m_id);
}
void gfx::BufferObject::Unbind() const {
glBindBuffer(m_target, 0);
}
void gfx::BufferObject::SetData(const void* data, size_t size) {
Bind();
if (size > m_size)
glBufferData(m_target, size, data, m_usage);
else
glBufferSubData(m_target, 0, size, data);
m_size = size;
}

30
src/gfx/buffer_object.hpp Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include "client/gl.hpp"
#include "client/utils.hpp"
namespace gfx
{
/**
* \brief Wrapper pro OpenGL buffer object
*/
class BufferObject : public NonCopyableNonMovable
{
GLuint m_id;
GLenum m_target;
GLenum m_usage;
size_t m_size;
public:
BufferObject(GLenum target, GLenum usage);
~BufferObject();
void Bind() const;
void Unbind() const;
void SetData(const void* data, size_t size);
GLuint GetId() const { return m_id; }
};
}

31
src/gfx/draw_list.hpp Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include "assets/map.hpp"
#include "assets/skeleton.hpp"
#include "surface.hpp"
namespace gfx
{
struct DrawSurfaceCmd
{
const Surface* surface;
const glm::mat4* matrices; // model matrix, continues in array of matrices for skeletal meshes
const glm::vec4* color; // optional tint
uint32_t first; // first triangle index
uint32_t count; // num triangles
float dist; // distance to camera - for transparnt sorting
};
struct DrawList
{
std::vector<DrawSurfaceCmd> surfaces;
void AddSurface(const DrawSurfaceCmd& cmd) { surfaces.emplace_back(cmd); }
void Clear() { surfaces.clear(); }
};
} // namespace gfx

178
src/gfx/renderer.cpp Normal file
View File

@ -0,0 +1,178 @@
#include "renderer.hpp"
#include <algorithm>
#include <ranges>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "client/gl.hpp"
#include "gfx/shader_sources.hpp"
gfx::Renderer::Renderer()
{
ShaderSources::MakeShader(mesh_shader_.shader, SS_MESH_VERT, SS_MESH_FRAG);
ShaderSources::MakeShader(skel_mesh_shader_.shader, SS_SKEL_MESH_VERT, SS_SKEL_MESH_FRAG);
ShaderSources::MakeShader(solid_shader_, SS_SOLID_VERT, SS_SOLID_FRAG);
}
void gfx::Renderer::Begin(size_t width, size_t height)
{
glViewport(0, 0, width, height);
}
void gfx::Renderer::ClearColor(const glm::vec3& color)
{
glClearColor(color.r, color.g, color.b, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
void gfx::Renderer::ClearDepth()
{
glClear(GL_DEPTH_BUFFER_BIT);
}
void gfx::Renderer::DrawList(gfx::DrawList& list, const DrawListParams& params)
{
DrawSurfaceList(list.surfaces, params);
}
void gfx::Renderer::InvalidateShaders()
{
InvalidateMeshShader(mesh_shader_);
InvalidateMeshShader(skel_mesh_shader_);
}
void gfx::Renderer::InvalidateMeshShader(MeshShader& mshader)
{
mshader.global_setup = false;
mshader.color = glm::vec4(-1.0f); // invalidate color
}
void gfx::Renderer::SetupMeshShader(MeshShader& mshader, const DrawListParams& params)
{
const Shader& shader = *mshader.shader;
if (current_shader_ != &shader)
{
glUseProgram(shader.GetId());
current_shader_ = &shader;
}
if (mshader.global_setup)
{
return; // Global uniforms are already set up
}
glUniformMatrix4fv(shader.U(gfx::SU_VIEW_PROJ), 1, GL_FALSE, &params.view_proj[0][0]);
mshader.global_setup = true;
}
void gfx::Renderer::DrawSurfaceList(std::span<DrawSurfaceCmd> list, const DrawListParams& params)
{
// sort the list to minimize state changes
std::ranges::sort(list, [](const DrawSurfaceCmd& a, const DrawSurfaceCmd& b) {
const Surface* sa = a.surface;
const Surface* sb = b.surface;
const bool trans_a = sa->sflags & SF_TRANSPARENT;
const bool trans_b = sb->sflags & SF_TRANSPARENT;
if (trans_a != trans_b)
return trans_b; // opaque first
if (trans_a) // both transparent
{
return a.dist > b.dist; // do not optimize transparent, sort by distance instead
}
if (auto cmp = sa <=> sb; cmp != 0)
return cmp < 0;
if (auto cmp = sa->texture <=> sb->texture; cmp != 0)
return cmp < 0;
if (auto cmp = sa->va.get() <=> sb->va.get(); cmp != 0)
return cmp < 0;
});
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
InvalidateShaders();
// cache to eliminate fake state changes
const gfx::Texture* last_texture = nullptr;
const gfx::VertexArray* last_vao = nullptr;
bool last_double_sided = false;
glActiveTexture(GL_TEXTURE0); // for all future bindings
for (const DrawSurfaceCmd& cmd : list)
{
const Surface* surface = cmd.surface;
// mesh flags
const bool skeletal_flag = surface->mflags & MF_SKELETAL;
// surface flags
const bool double_sided_flag = surface->sflags & SF_DOUBLE_SIDED;
const bool transparent_flag = surface->sflags & SF_TRANSPARENT;
const bool object_color_flag = surface->sflags & SF_OBJECT_COLOR;
// adjust state
if (last_double_sided != double_sided_flag)
{
if (double_sided_flag)
glDisable(GL_CULL_FACE);
else
glEnable(GL_CULL_FACE);
last_double_sided = double_sided_flag;
}
// select shader
MeshShader& mshader = skeletal_flag ? skel_mesh_shader_ : mesh_shader_;
SetupMeshShader(mshader, params);
// set model matrix
glUniformMatrix4fv(mshader.shader->U(gfx::SU_MODEL), 1, GL_FALSE, &cmd.matrices[0][0][0]);
// set color
glm::vec4 color = (object_color_flag && cmd.color) ? glm::vec4(*cmd.color) : glm::vec4(1.0f);
if (mshader.color != color)
{
glUniform4fv(mshader.shader->U(gfx::SU_COLOR), 1, &color[0]);
mshader.color = color;
}
// bind texture
if (last_texture != surface->texture.get())
{
GLuint tex_id = surface->texture ? surface->texture->GetId() : 0;
glBindTexture(GL_TEXTURE_2D, tex_id);
last_texture = surface->texture.get();
}
// bind VAO
if (last_vao != surface->va.get())
{
glBindVertexArray(surface->va->GetVAOId());
last_vao = surface->va.get();
}
size_t first_tri = surface->first + cmd.first;
size_t num_tris = cmd.count ? cmd.count : surface->count;
// draw
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(num_tris * 3U), GL_UNSIGNED_INT,
(void*)(first_tri * 3U * sizeof(GLuint)));
}
}

52
src/gfx/renderer.hpp Normal file
View File

@ -0,0 +1,52 @@
#pragma once
#include <memory>
#include <span>
#include "shader.hpp"
#include "draw_list.hpp"
namespace gfx
{
struct DrawListParams
{
glm::mat4 view_proj;
};
struct MeshShader
{
std::unique_ptr<Shader> shader;
// cached state to avoid redundant uniform updates which are expensive especially on WebGL
bool global_setup = false;
glm::vec4 color = glm::vec4(-1.0f); // invalid to force initial setup
};
class Renderer
{
public:
Renderer();
void Begin(size_t width, size_t height);
void ClearColor(const glm::vec3& color);
void ClearDepth();
void DrawList(gfx::DrawList& list, const DrawListParams& params);
private:
MeshShader mesh_shader_;
MeshShader skel_mesh_shader_;
std::unique_ptr<Shader> solid_shader_;
const Shader* current_shader_ = nullptr;
void InvalidateShaders();
void InvalidateMeshShader(MeshShader& mshader);
void SetupMeshShader(MeshShader& mshader, const DrawListParams& params);
void DrawSurfaceList(std::span<DrawSurfaceCmd> queue, const DrawListParams& params);
};
}

103
src/gfx/shader.cpp Normal file
View File

@ -0,0 +1,103 @@
#include <stdexcept>
#include <string>
#include "shader.hpp"
#define LOG_LENGTH 1024
// Nazvy uniformnich promennych v shaderech
static const char* const s_uni_names[] = {
"u_model", // SU_MODEL
"u_view_proj", // SU_VIEW_PROJ
"u_tex", // SU_TEX
"u_color" // SU_COLOR
};
// Vytvori shader z daneho zdroje
static GLuint CreateShader(const char* src, GLenum type) {
GLuint id = glCreateShader(type);
glShaderSource(id, 1, &src, NULL);
glCompileShader(id);
GLint is_compiled = 0;
glGetShaderiv(id, GL_COMPILE_STATUS, &is_compiled);
if (is_compiled == GL_FALSE) {
GLchar log[LOG_LENGTH];
glGetShaderInfoLog(id, LOG_LENGTH, NULL, log);
glDeleteShader(id);
throw std::runtime_error(std::string("Nelze zkompilovat shader: ") + log);
}
return id;
}
gfx::Shader::Shader(const char* vert_src, const char* frag_src) {
m_id = glCreateProgram();
if (!m_id)
throw std::runtime_error("Nelze vytvorit program!");
GLuint vert, frag;
// Vytvoreni vertex shaderu
vert = CreateShader(vert_src, GL_VERTEX_SHADER);
try {
// Vyvoreni fragment shaderu
frag = CreateShader(frag_src, GL_FRAGMENT_SHADER);
}
catch (std::exception& e) {
glDeleteShader(vert);
throw e;
}
// Pripojeni shaderu k programu
glAttachShader(m_id, vert);
glAttachShader(m_id, frag);
// Linknuti programu
glLinkProgram(m_id);
// Smazani shaderu
glDeleteShader(vert);
glDeleteShader(frag);
GLint is_linked = 0;
glGetProgramiv(m_id, GL_LINK_STATUS, &is_linked);
if (is_linked == GL_FALSE) {
GLchar log[LOG_LENGTH];
glGetProgramInfoLog(m_id, LOG_LENGTH, NULL, log);
glDeleteProgram(m_id);
throw std::runtime_error(std::string("Nelze linknout shader: ") + log);
}
// Ziskani lokaci uniformnich promennych
for (size_t i = 0; i < SU_COUNT; i++)
m_uni[i] = glGetUniformLocation(m_id, s_uni_names[i]);
SetupBindings();
}
gfx::Shader::~Shader() {
// Smazani programu
glDeleteProgram(m_id);
}
void gfx::Shader::SetupBindings()
{
glUseProgram(m_id);
glUniform1i(m_uni[SU_TEX], 0);
// Bones UBO
int ubo_index = glGetUniformBlockIndex(m_id, "Bones");
if (ubo_index != GL_INVALID_INDEX)
{
glUniformBlockBinding(m_id, ubo_index, 0); // bind to binding point 0
}
glUseProgram(0);
}

52
src/gfx/shader.hpp Normal file
View File

@ -0,0 +1,52 @@
#pragma once
#include "client/gl.hpp"
#include "client/utils.hpp"
namespace gfx
{
/**
* \brief Uniformy shaderu
*/
enum ShaderUniform
{
SU_MODEL,
SU_VIEW_PROJ,
SU_TEX,
SU_COLOR,
SU_COUNT
};
/**
* \brief Wrapper pro OpenGL shader program
*/
class Shader : public NonCopyableNonMovable
{
GLuint m_id;
GLint m_uni[SU_COUNT];
public:
Shader(const char* vert_src, const char* frag_src);
~Shader();
/**
* \brief Vr<EFBFBD>t<EFBFBD> lokaci uniformy
*
* \param u Uniforma
* \return Lokace
*/
GLint U(ShaderUniform u) const { return m_uni[u]; }
/**
* \brief Vr<EFBFBD>t<EFBFBD> OpenGL n<EFBFBD>zev shaderu
*
* \return N<EFBFBD>zev shaderu
*/
GLuint GetId() const { return m_id; }
private:
void SetupBindings();
};
}

4
src/gfx/shader_defs.hpp Normal file
View File

@ -0,0 +1,4 @@
#pragma once
#define SD_MAX_LIGHTS 4
#define SD_MAX_BONES 256

206
src/gfx/shader_sources.cpp Normal file
View File

@ -0,0 +1,206 @@
#include "shader_sources.hpp"
#include "shader_defs.hpp"
#include "client/gl.hpp"
#ifndef PG_GLES
#define GLSL_VERSION \
"#version 330 core\n" \
"\n"
#else
#define GLSL_VERSION \
"#version 300 es\n" \
"precision mediump float;\n" \
"\n"
#endif
#define STRINGIFY_HELPER(x) #x
#define STRINGIFY(x) STRINGIFY_HELPER(x)
#define SHADER_DEFS \
"#define MAX_LIGHTS " STRINGIFY(SD_MAX_LIGHTS) "\n" \
"#define MAX_BONES " STRINGIFY(SD_MAX_BONES) "\n" \
"\n"
#define SHADER_HEADER \
GLSL_VERSION \
SHADER_DEFS
#define MESH_MATRICES_GLSL R"GLSL(
uniform mat4 u_view_proj;
uniform mat4 u_model; // Transform matrix
)GLSL"
#define LIGHT_MATRICES_GLSL R"GLSL(
uniform vec3 u_ambient_light;
uniform int u_num_lights;
uniform vec3 u_light_positions[MAX_LIGHTS];
uniform vec4 u_light_colors_rs[MAX_LIGHTS]; // rgb = color, a = radius
)GLSL"
#define COMPUTE_LIGHTS_GLSL R"GLSL(
vec3 ComputeLights(in vec3 sector_pos, in vec3 sector_normal)
{
vec3 color = u_ambient_light;
for (int i = 0; i < u_num_lights; ++i) {
vec3 light_pos = u_light_positions[i];
vec3 light_color = u_light_colors_rs[i].rgb;
float light_radius = u_light_colors_rs[i].a;
vec3 to_light = light_pos - sector_pos.xyz;
float dist2 = dot(to_light, to_light);
if (dist2 < light_radius * light_radius) {
float dist = sqrt(dist2);
float attenuation = 1.0 - (dist / light_radius);
float dot = max(dot(sector_normal, normalize(to_light)), 0.0);
color += light_color * dot * attenuation;
}
}
return color;
}
)GLSL"
// Zdrojove kody shaderu
static const char* const s_srcs[] = {
// SS_MESH_VERT
SHADER_HEADER
R"GLSL(
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec3 a_normal;
layout (location = 2) in vec3 a_color;
layout (location = 3) in vec2 a_uv;
)GLSL"
MESH_MATRICES_GLSL
LIGHT_MATRICES_GLSL
COMPUTE_LIGHTS_GLSL
R"GLSL(
out vec2 v_uv;
out vec3 v_color;
void main() {
vec4 world_pos = u_model * vec4(a_pos, 1.0);
vec3 world_normal = normalize(mat3(u_model) * a_normal);
gl_Position = u_view_proj * world_pos;
v_uv = vec2(a_uv.x, 1.0 - a_uv.y);
v_color = ComputeLights(world_pos.xyz, world_normal) * a_color;
}
)GLSL",
// SS_MESH_FRAG
SHADER_HEADER
R"GLSL(
in vec2 v_uv;
in vec3 v_color;
uniform sampler2D u_tex;
layout (location = 0) out vec4 o_color;
void main() {
o_color = vec4(texture(u_tex, v_uv));
o_color.rgb *= v_color; // Apply vertex color
//o_color = vec4(1.0, 0.0, 0.0, 1.0);
}
)GLSL",
// SS_SKEL_MESH_VERT
SHADER_HEADER
R"GLSL(
layout (location = 0) in vec3 a_pos;
layout (location = 1) in vec3 a_normal;
layout (location = 3) in vec2 a_uv;
layout (location = 5) in ivec4 a_bone_ids;
layout (location = 6) in vec4 a_bone_weights;
layout (std140) uniform Bones {
mat4 u_bone_matrices[MAX_BONES];
};
)GLSL"
MESH_MATRICES_GLSL
LIGHT_MATRICES_GLSL
COMPUTE_LIGHTS_GLSL
R"GLSL(
out vec2 v_uv;
out vec3 v_color;
void main() {
mat4 bone_transform = mat4(0.0);
for (int i = 0; i < 4; ++i) {
int bone_id = a_bone_ids[i];
if (bone_id >= 0) {
bone_transform += u_bone_matrices[bone_id] * a_bone_weights[i];
}
}
vec4 world_pos = u_model * bone_transform * vec4(a_pos, 1.0);
vec3 world_normal = normalize(mat3(u_model) * mat3(bone_transform) * a_normal);
gl_Position = u_view_proj * world_pos;
v_uv = vec2(a_uv.x, 1.0 - a_uv.y);
v_color = ComputeLights(world_pos.xyz, world_normal);
}
)GLSL",
// SS_SKEL_MESH_FRAG
SHADER_HEADER
R"GLSL(
in vec2 v_uv;
in vec3 v_color;
uniform sampler2D u_tex;
layout (location = 0) out vec4 o_color;
void main() {
o_color = vec4(texture(u_tex, v_uv));
o_color.rgb *= v_color; // Apply vertex color
}
)GLSL",
// SS_SOLID_VERT
SHADER_HEADER
R"GLSL(
layout (location = 0) in vec3 a_pos;
layout (location = 2) in vec4 a_color;
uniform mat4 u_view_proj;
uniform vec4 u_color;
out vec4 v_color;
void main() {
gl_Position = u_view_proj * vec4(a_pos, 1.0);
v_color = a_color * u_color;
}
)GLSL",
// SS_SOLID_FRAG
SHADER_HEADER
R"GLSL(
in vec4 v_color;
layout (location = 0) out vec4 o_color;
void main() {
o_color = v_color;
}
)GLSL",
};
// Vrati zdrojovy kod shaderu
const char* gfx::ShaderSources::Get(ShaderSource ss) {
return s_srcs[ss];
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <memory>
#include "shader.hpp"
namespace gfx
{
enum ShaderSource
{
SS_MESH_VERT,
SS_MESH_FRAG,
SS_SKEL_MESH_VERT,
SS_SKEL_MESH_FRAG,
SS_SOLID_VERT,
SS_SOLID_FRAG,
};
class ShaderSources
{
public:
// Vrati zdrojovy kod shaderu
static const char* Get(ShaderSource ss);
static void MakeShader(std::unique_ptr<Shader>& shader, ShaderSource ss_vert, ShaderSource ss_frag) {
shader = std::make_unique<Shader>(
Get(ss_vert),
Get(ss_frag)
);
}
};
}

38
src/gfx/surface.hpp Normal file
View File

@ -0,0 +1,38 @@
#pragma once
#include "texture.hpp"
#include "vertex_array.hpp"
namespace gfx
{
using MeshFlags = uint8_t;
enum MeshFlag : MeshFlags
{
MF_NONE = 0x00,
MF_LIGHTMAP_UV = 0x01, // unused
MF_SKELETAL = 0x02,
};
using SurfaceFlags = uint8_t;
enum SurfaceFlag : SurfaceFlags
{
SF_NONE = 0x00,
SF_DOUBLE_SIDED = 0x01,
SF_TRANSPARENT = 0x02,
SF_OBJECT_COLOR = 0x08, // use object level tint
};
struct Surface
{
std::shared_ptr<const gfx::Texture> texture;
std::shared_ptr<const gfx::VertexArray> va;
size_t first = 0; // first triangle VA EBO
size_t count = 0; // number of triangles
MeshFlags mflags = MF_NONE;
SurfaceFlags sflags = SF_NONE;
};
} // namespace gfx

87
src/gfx/texture.cpp Normal file
View File

@ -0,0 +1,87 @@
#include "texture.hpp"
#include <stdexcept>
#include "utils/files.hpp"
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
static void GetGLFilterModes(bool linear, bool mipmaps, GLenum& filter_min, GLenum& filter_mag) {
if (linear)
{
filter_min = GL_LINEAR;
filter_mag = GL_LINEAR;
if (mipmaps)
{
filter_min = GL_LINEAR_MIPMAP_LINEAR;
}
}
else
{
filter_min = GL_NEAREST;
filter_mag = GL_NEAREST;
if (mipmaps)
{
// Mipmaps always linear
filter_min = GL_NEAREST_MIPMAP_LINEAR;
}
}
}
gfx::Texture::Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, bool linear, bool mipmaps) {
glGenTextures(1, &m_id);
if (!m_id)
throw std::runtime_error("Nelze vytvorit texturu!");
glBindTexture(GL_TEXTURE_2D, m_id);
glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, type, data);
GLenum filter_min, filter_mag;
GetGLFilterModes(linear, mipmaps, filter_min, filter_mag);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter_min);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter_mag);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
if (mipmaps)
{
glGenerateMipmap(GL_TEXTURE_2D);
}
glBindTexture(GL_TEXTURE_2D, 0);
}
gfx::Texture::~Texture() {
glDeleteTextures(1, &m_id);
}
std::shared_ptr<gfx::Texture> gfx::Texture::LoadFromFile(const std::string& filename)
{
printf("Loading texture from file: %s\n", filename.c_str());
std::string content = fs::ReadFileAsString(filename);
int width, height, channels;
unsigned char* data = stbi_load_from_memory(reinterpret_cast<const unsigned char*>(content.data()), content.size(), &width, &height, &channels, 4);
if (!data) {
throw std::runtime_error("Failed to load texture from file: " + filename);
}
std::shared_ptr<Texture> texture;
try {
texture = std::make_shared<Texture>(width, height, data, GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, false, true);
}
catch (const std::exception& e) {
stbi_image_free(data);
throw; // Rethrow the exception after freeing the data
}
stbi_image_free(data);
return texture;
}

26
src/gfx/texture.hpp Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include <memory>
#include <string>
#include "client/utils.hpp"
#include "client/gl.hpp"
namespace gfx
{
/**
* \brief Wrapper pro OpenGL texturu
*/
class Texture : public NonCopyableNonMovable
{
GLuint m_id;
public:
Texture(GLuint width, GLuint height, const void* data, GLint internalformat, GLenum format, GLenum type, bool linear, bool mipmaps);
~Texture();
GLuint GetId() const { return m_id; }
static std::shared_ptr<Texture> LoadFromFile(const std::string& filename);
};
}

View File

@ -0,0 +1,21 @@
#pragma once
#include "buffer_object.hpp"
namespace gfx
{
template<class T>
class UniformBuffer : public BufferObject
{
public:
UniformBuffer() : BufferObject(GL_UNIFORM_BUFFER, GL_DYNAMIC_DRAW)
{
}
void SetData(const T* data, size_t count = 1)
{
BufferObject::SetData(data, sizeof(T) * count);
}
};
}

98
src/gfx/vertex_array.cpp Normal file
View File

@ -0,0 +1,98 @@
#include "vertex_array.hpp"
#include <cstdint>
// Struktura pro informace o jednotlivych vertex atributu
struct VertexAttribInfo {
GLuint index;
GLint size;
GLenum type;
GLboolean normalized;
size_t byte_size;
};
// Informace o jednotlivych vertex atributech
static const VertexAttribInfo s_ATTR_INFO[] = {
{ 0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_POSITION
{ 1, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 3 }, // VA_NORMAL
{ 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, 4 }, // VA_COLOR
{ 3, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_UV
{ 4, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2 }, // VA_LIGHTMAP_UV
{ 5, 4, GL_INT, GL_FALSE, sizeof(int32_t) * 4 }, // VA_BONE_INDICES
{ 6, 4, GL_FLOAT, GL_FALSE, sizeof(float) * 4 }, // VA_BONE_WEIGHTS
};
// Pocet typu vertex atributu
static const size_t s_ATTR_COUNT = 7;
gfx::VertexArray::VertexArray(int attrs, int flags) : m_usage(GL_STATIC_DRAW), m_num_indices(0) {
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
if (flags & VF_DYNAMIC)
m_usage = GL_DYNAMIC_DRAW;
// vytvori buffer pro vrcholy
m_vbo = std::make_unique<BufferObject>(GL_ARRAY_BUFFER, m_usage);
m_vbo->Bind();
// vypocet velikosti jednoho vrcholu
size_t stride = 0U;
for (size_t i = 0; i < s_ATTR_COUNT; i++) {
if (attrs & (1 << i))
stride += s_ATTR_INFO[i].byte_size;
}
// povoli a nastavi jednotlive vertex atributy
size_t offset = 0U;
for (size_t i = 0; i < s_ATTR_COUNT; i++) {
const auto& info = s_ATTR_INFO[i];
if (attrs & (1 << i)) {
glEnableVertexAttribArray(info.index);
if (info.type != GL_INT)
{
glVertexAttribPointer(info.index, info.size, info.type, info.normalized, stride, (const void*)offset);
}
else
{
glVertexAttribIPointer(info.index, info.size, info.type, stride, (const void*)offset);
}
offset += info.byte_size;
} else if (i == 2)
{
// pokud neni barva, nastavime defaultni hodnotu 1.0f,1.0f,1.0f,1.0f
glDisableVertexAttribArray(info.index);
glVertexAttrib4f(info.index, 1.0f, 1.0f, 1.0f, 1.0f);
}
}
// vytvori buffer pro indexy
if (flags & VF_CREATE_EBO) {
m_ebo = std::make_unique<BufferObject>(GL_ELEMENT_ARRAY_BUFFER, m_usage);
m_ebo->Bind();
}
glBindVertexArray(0);
}
gfx::VertexArray::~VertexArray() {
// uklid
glDeleteVertexArrays(1, &m_vao);
}
// Nastavi data do VBO
void gfx::VertexArray::SetVBOData(const void* data, size_t size) {
glBindVertexArray(m_vao);
m_vbo->SetData(data, size);
glBindVertexArray(0);
}
// Nastavi indexy do EBO
void gfx::VertexArray::SetIndices(const GLuint* data, size_t size) {
glBindVertexArray(m_vao);
m_ebo->SetData(data, size * sizeof(*data));
m_num_indices = size;
glBindVertexArray(0);
}

62
src/gfx/vertex_array.hpp Normal file
View File

@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
#include <cassert>
#include <memory>
#include "client/gl.hpp"
#include "client/utils.hpp"
#include "buffer_object.hpp"
namespace gfx
{
// Vycet jednotlivych atributu vrcholu
enum VertexAttr
{
VA_POSITION = 1 << 0,
VA_NORMAL = 1 << 1,
VA_COLOR = 1 << 2,
VA_UV = 1 << 3,
VA_LIGHTMAP_UV = 1 << 4,
VA_BONE_INDICES = 1 << 5,
VA_BONE_WEIGHTS = 1 << 6,
};
// Informace pro vytvoreni VertexArraye
enum VertexArrayFlags
{
VF_CREATE_EBO = 1 << 0,
VF_DYNAMIC = 1 << 1,
};
// VertexArray - trida pro praci s modely
class VertexArray : public NonCopyableNonMovable
{
GLuint m_vao;// , m_vbo, m_ebo;
std::unique_ptr<BufferObject> m_vbo;
std::unique_ptr<BufferObject> m_ebo;
size_t m_num_indices;
GLenum m_usage;
public:
VertexArray(int attrs, int flags);
~VertexArray();
// Nastavi data do VBO
void SetVBOData(const void* data, size_t size);
// Nastavi indexy do EBO
void SetIndices(const GLuint* data, size_t size);
GLuint GetVAOId() const { return m_vao; }
GLuint GetVBOId() const { return m_vbo->GetId(); }
GLuint GetEBOId() const {
assert(m_ebo && "GetEBOId() ale !ebo");
return m_ebo->GetId();
}
//size_t GetVBOSize() const { return m_vbo->; }
size_t GetNumIndices() const { return m_num_indices; }
};
}

53
src/net/defs.hpp Normal file
View File

@ -0,0 +1,53 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "fixed_str.hpp"
#include "quantized.hpp"
namespace net
{
enum MessageType : uint8_t
{
MSG_NONE,
/*~~~~~~~~ Client->Server ~~~~~~~~*/
// IN <PlayerInputFlags> <ViewYaw> <ViewPitch>
MSG_IN,
/*~~~~~~~~ World ~~~~~~~~*/
// CHWORLD <MapName>
MSG_CHWORLD,
/*~~~~~~~~ Entity ~~~~~~~~*/
// ENTSPAWN <EntNum> <EntType> data...
MSG_ENTSPAWN,
// ENTMSG <EntNum> data...
MSG_ENTMSG,
// ENTRM <EntNum>
MSG_ENTRM,
/*~~~~~~~~~~~~~~~~*/
MSG_COUNT,
};
using MapName = FixedStr<32>;
using ViewYaw = Quantized<uint16_t, 0, 360>;
using ViewPitch = Quantized<uint16_t, -180, 180>;
using EntNum = uint16_t;
enum EntType : uint8_t
{
ET_NONE,
ET_PAWN,
ET_CAR,
ET_COUNT,
};
} // namespace net

34
src/net/fixed_str.hpp Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
namespace net
{
template <size_t N>
struct FixedStr
{
size_t len = 0;
char str[N];
size_t MaxLen() const { return N; }
FixedStr& operator=(const std::string& stdstr)
{
size_t putsize = std::min(N, stdstr.size());
len = putsize;
memcpy(str, stdstr.data(), putsize);
}
operator std::string() { return std::string(str, len); }
operator std::string_view() { return std::string_view(str, len); }
};
template <size_t N>
using FixedStrLen = std::conditional_t<(N > 255), uint16_t, uint8_t>;
} // namespace net

121
src/net/inmessage.hpp Normal file
View File

@ -0,0 +1,121 @@
#pragma once
#include <cstdint>
#include <concepts>
#include <stdexcept>
#include "fixed_str.hpp"
#include "quantized.hpp"
namespace net
{
class InMessage
{
public:
InMessage() : ptr_(nullptr), end_(nullptr) {}
InMessage(const char* ptr, size_t len) : ptr_(ptr), end_(ptr_ + len) {}
const char* Ptr() const { return ptr_; }
const char* End() const { return end_; }
bool CheckAvail(size_t n)
{
return ptr_ + n <= end_;
}
bool Read(char* dest, size_t n)
{
if (!CheckAvail(n))
return false;
memcpy(dest, ptr_, n);
ptr_ += n;
return true;
}
template <std::integral T>
bool Read(T& value)
{
if (!CheckAvail(sizeof(T)))
return false;
value = *(reinterpret_cast<const T*>(ptr_));
ptr_ += sizeof(T);
return true;
}
template <size_t N>
bool Read(FixedStr<N>& str)
{
FixedStrLen<N> len;
if (!Read(len) || len > N || !Read(str.str, len))
return false;
str.len = len;
return true;
}
template <typename T, long long MinL, long long MaxL>
bool Read(Quantized<T, MinL, MaxL>& quant)
{
T value;
if (!Read(value))
return false;
quant.value = value;
return true;
}
bool ReadVarInt(int64_t& value)
{
uint64_t uvalue = 0;
uint8_t p;
if (!Read(p))
return false;
bool next = p & 0b10000000;
bool negative = p & 0b01000000;
uvalue |= p & 0b00111111;
size_t shift = 6;
while (next)
{
if (!Read(p))
return false;
next = p & 0b10000000;
uvalue |= static_cast<uint64_t>(p & 0b01111111) << shift;
shift += 7;
}
value = static_cast<int64_t>(uvalue);
if (negative)
value = -value;
return true;
}
bool SubMessage(InMessage& sub, size_t n)
{
if (!CheckAvail(n))
return false;
sub.ptr_ = ptr_;
ptr_ += n;
sub.end_ = ptr_;
return true;
}
private:
const char* ptr_;
const char* end_;
};
} // namespace net

83
src/net/outmessage.hpp Normal file
View File

@ -0,0 +1,83 @@
#pragma once
#include <concepts>
#include <vector>
#include "fixed_str.hpp"
#include "quantized.hpp"
namespace net
{
class OutMessage
{
public:
OutMessage(std::vector<char>& buffer) : buffer_(buffer) { buffer.clear(); }
template <std::integral T>
size_t Reserve()
{
size_t pos = buffer_.size();
buffer_.resize(pos + sizeof(T));
return pos;
}
template <std::integral T>
void WriteAt(size_t pos, T value)
{
*(reinterpret_cast<T*>(&buffer_[pos])) = value;
}
template <std::integral T>
void Write(T value)
{
WriteAt(Reserve<T>(), value);
}
void Write(const char* str, size_t n)
{
size_t pos = buffer_.size();
buffer_.resize(pos + n);
memcpy(&buffer_[pos], str, n);
}
template <size_t N>
void Write(const FixedStr<N>& str)
{
Write(static_cast<FixedStrLen<N>>(str.len));
Write(str.str, str.len);
}
template <typename T, long long MinL, long long MaxL>
void Write(Quantized<T, MinL, MaxL> quant)
{
Write(quant.value);
}
void WriteVarInt(int64_t value)
{
const bool negative = value < 0;
uint64_t uvalue = static_cast<uint64_t>(negative ? -value : value);
char p[10];
p[0] = negative ? 0b11000000 : 0b10000000;
p[0] |= uvalue & 0b00111111;
uvalue >>= 6;
size_t i;
for (i = 1; uvalue; ++i)
{
p[i] = 0b10000000 | (uvalue & 0b01111111);
uvalue >>= 7;
}
p[i - 1] &= 0b01111111; // end mark
Write(p, i);
}
private:
std::vector<char>& buffer_;
};
} // namespace net

40
src/net/quantized.hpp Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <algorithm>
#include <limits>
#include <concepts>
namespace net
{
template <std::unsigned_integral T, long long MinL = -1, long long MaxL = 1, long long DivisorL = 1>
struct Quantized
{
static constexpr float Min = static_cast<float>(MinL) / static_cast<float>(DivisorL);
static constexpr float Max = static_cast<float>(MaxL) / static_cast<float>(DivisorL);
static constexpr T max_int = std::numeric_limits<T>::max();
static constexpr float range = Max - Min;
static constexpr float scale = static_cast<float>(max_int) / range;
static constexpr float inv_scale = range / static_cast<float>(max_int);
public:
T value;
Quantized(T value) : value(value) {}
Quantized(float fvalue) { Encode(value); }
void Encode(float fvalue) noexcept
{
fvalue = std::clamp(fvalue, Min, Max);
float normalized = (fvalue - Min) * scale;
value = static_cast<T>(normalized + 0.5f); // round
}
float Decode() const noexcept { return Min + static_cast<float>(value) * inv_scale; }
static constexpr float MaxError() noexcept { return inv_scale * 0.5f; }
};
} // namespace net

33
src/utils/files.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "files.hpp"
#include <SDL.h>
#include <SDL_rwops.h>
std::string fs::ReadFileAsString(const std::string& path)
{
SDL_RWops *rw = SDL_RWFromFile(path.c_str(), "rb");
if (!rw)
{
throw std::runtime_error("Failed to open file: " + path);
}
Sint64 res_size = SDL_RWsize(rw);
if (res_size < 0)
{
SDL_RWclose(rw);
throw std::runtime_error("Failed to get file size: " + path);
}
std::string content;
content.resize(static_cast<size_t>(res_size));
SDL_RWread(rw, content.data(), 1, res_size);
SDL_RWclose(rw);
return content;
}
std::istringstream fs::ReadFileAsStream(const std::string& path)
{
std::string content = ReadFileAsString(path);
return std::istringstream(content);
}

9
src/utils/files.hpp Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include <string>
#include <sstream>
namespace fs
{
std::string ReadFileAsString(const std::string& path);
std::istringstream ReadFileAsStream(const std::string& path);
}

58
src/utils/transform.hpp Normal file
View File

@ -0,0 +1,58 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
struct Transform
{
glm::vec3 position = glm::vec3(0.0f);
glm::quat rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity quaternion
float scale = 1.0f;
glm::mat4 ToMatrix() const
{
float x = rotation.x;
float y = rotation.y;
float z = rotation.z;
float w = rotation.w;
float x2 = x + x;
float y2 = y + y;
float z2 = z + z;
float xx = x * x2;
float xy = x * y2;
float xz = x * z2;
float yy = y * y2;
float yz = y * z2;
float zz = z * z2;
float wx = w * x2;
float wy = w * y2;
float wz = w * z2;
float sx = scale;
float sy = scale;
float sz = scale;
return glm::mat4(
(1.0f - (yy + zz)) * sx, (xy + wz) * sx, (xz - wy) * sx, 0.0f,
(xy - wz) * sy, (1.0f - (xx + zz)) * sy, (yz + wx) * sy, 0.0f,
(xz + wy) * sz, (yz - wx) * sz, (1.0f - (xx + yy)) * sz, 0.0f,
position.x, position.y, position.z, 1.0f
);
}
void SetAngles(const glm::vec3& angles_deg)
{
glm::vec3 angles_rad = glm::radians(angles_deg);
rotation = glm::quat(angles_rad);
}
static Transform Lerp(const Transform& a, const Transform& b, float t)
{
Transform result;
result.position = glm::mix(a.position, b.position, t);
result.rotation = glm::slerp(a.rotation, b.rotation, t);
result.scale = glm::mix(a.scale, b.scale, t);
return result;
}
};