Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0260b4a0a0 | |||
| b71f740b9a | |||
| 0e639abcd2 | |||
| 2e95c941ba | |||
| 2f0662e2bb | |||
| a097f4d4b0 | |||
| f4174eb0c7 |
+11
-1
@@ -1,6 +1,16 @@
|
||||
build/
|
||||
**/build/
|
||||
target/
|
||||
Release/
|
||||
Debug/
|
||||
Testing/
|
||||
cmake-build-debug/
|
||||
.git/
|
||||
.zed/
|
||||
.clangd/
|
||||
.idea/
|
||||
.cache/
|
||||
compile_commands.json
|
||||
*.md
|
||||
|
||||
# cloned by FetchContent into the build tree at configure time
|
||||
external/tracy/
|
||||
|
||||
@@ -2,6 +2,7 @@ build/
|
||||
Debug/
|
||||
Release/
|
||||
Testing/
|
||||
dist/
|
||||
.cache
|
||||
imgui.ini
|
||||
|
||||
@@ -10,3 +11,6 @@ imgui.ini
|
||||
cmake-build-debug/
|
||||
.opencode/
|
||||
compile_commands.json
|
||||
|
||||
# conan (machine-specific, generated by `conan install`)
|
||||
CMakeUserPresets.json
|
||||
|
||||
+4
-8
@@ -1,7 +1,9 @@
|
||||
cmake_minimum_required(VERSION 3.26)
|
||||
project(towards)
|
||||
project(tw_common)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include(cmake/CompilerOptions.cmake)
|
||||
include(cmake/Dependencies.cmake)
|
||||
@@ -40,18 +42,12 @@ target_include_directories(${PROJECT_NAME}
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw::network
|
||||
tw::protocol
|
||||
loft::common
|
||||
loft::base
|
||||
loft::render_graph
|
||||
spdlog::spdlog
|
||||
glm::glm
|
||||
EnTT::EnTT
|
||||
Jolt
|
||||
tl::expected
|
||||
Tracy::TracyClient
|
||||
pqxx
|
||||
pq
|
||||
)
|
||||
|
||||
include(cmake/Modules.cmake)
|
||||
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
# ── build stage ──────────────────────────────────────────────────────────────
|
||||
FROM ubuntu:24.04 AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
gcc-13 g++-13 \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
mold \
|
||||
libvulkan-dev \
|
||||
libsdl2-dev \
|
||||
libprotobuf-dev protobuf-compiler \
|
||||
libpq-dev \
|
||||
libpqxx-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN cmake -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=gcc-13 \
|
||||
-DCMAKE_CXX_COMPILER=g++-13 \
|
||||
-DCMAKE_PREFIX_PATH=/usr/lib/x86_64-linux-gnu \
|
||||
&& cmake --build build --target tw_server -j$(nproc)
|
||||
|
||||
# ── runtime stage ─────────────────────────────────────────────────────────────
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libvulkan1 \
|
||||
libsdl2-2.0-0 \
|
||||
libprotobuf32t64 \
|
||||
libpq5 \
|
||||
libpqxx-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /src/build/modules/server/tw_server /usr/local/bin/tw_server
|
||||
|
||||
EXPOSE 8101/udp 8102/udp
|
||||
|
||||
ENTRYPOINT ["tw_server"]
|
||||
@@ -1,28 +1,40 @@
|
||||
# mmo
|
||||
# Towards
|
||||
|
||||
Game engine demo with built-in networking for multiplayer.
|
||||
Game engine built as a modular monolith.
|
||||
|
||||
## Building
|
||||
|
||||
### Locally
|
||||
Dependencies:
|
||||
- SDL2
|
||||
- Conan 2 (`pipx install conan`, or `pip install conan`)
|
||||
- Vulkan SDK
|
||||
|
||||
Both need to be downloaded before building. Do not forget to set PATH on Windows.
|
||||
The Vulkan SDK must be installed before building. Do not forget to set PATH on Windows.
|
||||
|
||||
On Linux, Conan builds SDL2 from source and still needs the system OpenGL/X11/Wayland/ALSA development headers present.
|
||||
|
||||
This command will initialize the build directory: `build/`
|
||||
First time only, create a Conan profile describing your compiler:
|
||||
|
||||
```bash
|
||||
$ cmake -S . -B ./build/
|
||||
$ conan profile detect
|
||||
```
|
||||
|
||||
This will build the entire project.
|
||||
Install the pinned dependencies and generate the CMake toolchain + presets:
|
||||
|
||||
```bash
|
||||
$ conan install . --output-folder=build --build=missing -s build_type=Debug
|
||||
```
|
||||
|
||||
Configure and build using the generated preset:
|
||||
|
||||
```bash
|
||||
$ cmake -S . -B ./build -DCMAKE_TOOLCHAIN_FILE="build/conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Debug
|
||||
$ cmake --build ./build
|
||||
```
|
||||
|
||||
For a release build, repeat both steps with `-s build_type=Release` and the
|
||||
`conan-release` preset.
|
||||
|
||||
There are now two executables:
|
||||
|
||||
Server is at `build/modules/client/mmo_server`
|
||||
@@ -31,3 +43,42 @@ Client is at `build/modules/client/mmo_client`
|
||||
|
||||
First run the server, so that client can connect. The server will try to use port 8080, but if it is already occupied, it will try use the next free higher one.
|
||||
|
||||
### Docker
|
||||
|
||||
It also possible to build the project using Docker. Advantage is that you do not have to set up the environment. The current Dockerfile also uses older linux distribution to be backwards compatible with older systems. To build everything, build the Dockerfile at `docker/Dockerfile` with until target `builder`:
|
||||
|
||||
```
|
||||
$ docker build -f docker/Dockerfile --target builder -t tw_builder:jammy .
|
||||
```
|
||||
|
||||
To easily export the built executables, run the target `export` of the same Dockerfile.
|
||||
|
||||
```
|
||||
$ docker build -f docker/Dockerfile --target export --output type=local,dest=dist .
|
||||
```
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
### Server
|
||||
|
||||
To run the system as inteded, run the `tw_server` executable first. It should output something like this:
|
||||
|
||||
```
|
||||
[2026-08-05 16:43:53.806] [info] quicr-port=8101 cluster-port=8102 timescaledb=disabled
|
||||
[2026-08-05 16:43:53.807] [info] Running on port: 8101
|
||||
[2026-08-05 16:43:53.807] [info] ZoneClusterLink listening on port 8102
|
||||
[2026-08-05 16:43:53.810] [info] Registered as zone 1 (-5000,-5000) (0,5000)
|
||||
```
|
||||
|
||||
Note the `Running on port: 8101` as it reports on which port it is currently running. Use it when connecting the clients.
|
||||
|
||||
### Client
|
||||
|
||||
Run the `tw_client` for connecting to the game. It should open into a lobby, where you can input an address of the server. It should remember the last address used and you can make some favourites. This is useful for testing on multiple cloud instances for example.
|
||||
|
||||
### Mock Client
|
||||
|
||||
The `tw_mock_client` module is for stress testing the server. It simulates 300 connected clients and moves them randomly. All those simulated players are actual established connections, which means the bandwidth of the server will be accurate.
|
||||
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ set(LFT_ENABLE_EXAMPLES OFF CACHE BOOL "Disable loft examples" FORCE)
|
||||
set(FASTNOISE2_NOISETOOL OFF CACHE BOOL "Disable FastNoise2 graph tool" FORCE)
|
||||
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(Protobuf CONFIG REQUIRED)
|
||||
find_package(SDL3 CONFIG REQUIRED)
|
||||
find_library(LIBDECOR_LIB decor-0)
|
||||
find_package(Protobuf CONFIG REQUIRED VERSION 3.75)
|
||||
|
||||
include(FetchContent)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;Distribution")
|
||||
|
||||
+4
-1
@@ -1,6 +1,7 @@
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/io/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/metrics/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/network/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/protocol/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/messaging/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/chat_service/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/server/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/client/)
|
||||
@@ -9,3 +10,5 @@ add_subdirectory(${CMAKE_SOURCE_DIR}/modules/gui/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/server_gui/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/serialization/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/peer_to_peer/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/quicr/)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/modules/message_protocol/)
|
||||
|
||||
+22
-7
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
timescaledb:
|
||||
image: timescale/timescaledb:latest-pg16
|
||||
image: timescale/timescaledb:latest-pg15
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: mmo
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- timescaledb_data:/var/lib/postgresql/data
|
||||
- timescaledb_data:/var/lib/towards_db/data
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
@@ -24,13 +24,28 @@ services:
|
||||
depends_on:
|
||||
- timescaledb
|
||||
|
||||
zone-server:
|
||||
build: .
|
||||
chat-server:
|
||||
profiles: [e2e]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: chat-server
|
||||
ports:
|
||||
- "8101:8101/udp"
|
||||
- "8102:8102/udp"
|
||||
- "8099:8099/udp"
|
||||
|
||||
chat-mock-client:
|
||||
profiles: [e2e]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: chat-mock-client
|
||||
# Address.hpp parses peers with inet_addr, which takes literal IPv4 only and
|
||||
# never consults DNS, so a service name is not addressable. Sharing the
|
||||
# server's network namespace makes its default 127.0.0.1 land on the server.
|
||||
network_mode: "service:chat-server"
|
||||
depends_on:
|
||||
- timescaledb
|
||||
- chat-server
|
||||
command: ["--channel", "1"]
|
||||
|
||||
volumes:
|
||||
timescaledb_data:
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[requires]
|
||||
protobuf/7.35.0
|
||||
sdl/3.4.8
|
||||
libpqxx/8.0.2
|
||||
|
||||
[tool_requires]
|
||||
protobuf/7.35.0
|
||||
|
||||
[options]
|
||||
protobuf/*:shared=False
|
||||
libpqxx/*:shared=False
|
||||
libpq/*:shared=True
|
||||
sdl/*:shared=False
|
||||
sdl/*:camera=False
|
||||
sdl/*:dialog=False
|
||||
sdl/*:tray=False
|
||||
sdl/*:sensor=False
|
||||
sdl/*:alsa=True
|
||||
sdl/*:pulseaudio=False
|
||||
sdl/*:sndio=False
|
||||
sdl/*:opengl=False
|
||||
sdl/*:opengles=False
|
||||
sdl/*:vulkan=True
|
||||
sdl/*:joystick=False
|
||||
sdl/*:haptic=False
|
||||
sdl/*:hidapi=False
|
||||
|
||||
[generators]
|
||||
CMakeDeps
|
||||
CMakeToolchain
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Builds against Ubuntu 22.04 (glibc 2.35) so the binaries also run on
|
||||
# distributions older than the developer machine. Two things make that work:
|
||||
#
|
||||
# * GCC 14 from the toolchain PPA — jammy's own gcc-11 has no <print>, which
|
||||
# eight translation units use.
|
||||
# * -static-libstdc++ -static-libgcc — the target then needs nothing newer
|
||||
# than the glibc of this base image.
|
||||
#
|
||||
# docker build -f docker/Dockerfile --target mock-client -t tw_mock_client .
|
||||
# docker build -f docker/Dockerfile --target test .
|
||||
# docker build -f docker/Dockerfile --target export --output type=local,dest=dist .
|
||||
|
||||
ARG UBUNTU_VERSION=22.04
|
||||
|
||||
FROM ubuntu:${UBUNTU_VERSION} AS toolchain
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
ARG VULKAN_SDK_VERSION=1.4.313
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
software-properties-common ca-certificates gnupg wget \
|
||||
&& add-apt-repository -y ppa:ubuntu-toolchain-r/test \
|
||||
&& wget -qO /etc/apt/trusted.gpg.d/lunarg.asc \
|
||||
https://packages.lunarg.com/lunarg-signing-key-pub.asc \
|
||||
&& wget -qO /etc/apt/sources.list.d/lunarg-vulkan.list \
|
||||
"https://packages.lunarg.com/vulkan/${VULKAN_SDK_VERSION}/lunarg-vulkan-${VULKAN_SDK_VERSION}-jammy.list" \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc-14 g++-14 \
|
||||
git make ninja-build ccache pkg-config \
|
||||
python3-pip \
|
||||
vulkan-headers libvulkan-dev glslang-tools \
|
||||
libdecor-0-dev libpqxx-dev
|
||||
|
||||
ARG CMAKE_VERSION=4.4.0
|
||||
ARG CONAN_VERSION=2.31.1
|
||||
RUN pip3 install --no-cache-dir "cmake==${CMAKE_VERSION}" "conan==${CONAN_VERSION}"
|
||||
|
||||
RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 \
|
||||
--slave /usr/bin/g++ g++ /usr/bin/g++-14 \
|
||||
--slave /usr/bin/gcov gcov /usr/bin/gcov-14 \
|
||||
&& update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-14 100 \
|
||||
&& update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-14 100
|
||||
|
||||
ENV CC=gcc
|
||||
ENV CXX=g++
|
||||
|
||||
COPY docker/conan/jammy /etc/conan/jammy
|
||||
|
||||
|
||||
|
||||
# toolchain installs Conan packages
|
||||
FROM toolchain AS deps
|
||||
|
||||
COPY conanfile.txt /src/conanfile.txt
|
||||
|
||||
RUN conan install /src/conanfile.txt \
|
||||
--profile:all=/etc/conan/jammy \
|
||||
--build=missing \
|
||||
--output-folder=/deps
|
||||
|
||||
|
||||
|
||||
# Builds the project
|
||||
FROM deps AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN --mount=type=cache,target=/build \
|
||||
--mount=type=cache,target=/root/.ccache \
|
||||
cmake -S /src -B /build -G Ninja \
|
||||
-DCMAKE_TOOLCHAIN_FILE=/deps/conan_toolchain.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DUSE_MOLD_LINKER=OFF \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-static-libstdc++ -static-libgcc" \
|
||||
&& cmake --build /build \
|
||||
&& mkdir -p /out/bin /out/shaders \
|
||||
&& cp /build/modules/client/tw_client \
|
||||
/build/modules/mock_client/tw_mock_client \
|
||||
/build/modules/server/tw_server \
|
||||
/build/modules/chat_service/chat_server_exe/tw_chat_server_exe \
|
||||
/build/modules/chat_service/chat_service/tests/chat_mock_client/tw_chat_mock_client \
|
||||
/out/bin/ \
|
||||
&& cp /build/modules/client/shaders/*.spirv /out/shaders/
|
||||
|
||||
|
||||
|
||||
# Extracts client
|
||||
FROM scratch AS export
|
||||
|
||||
COPY --from=builder /out/bin/tw_client /tw_client
|
||||
COPY --from=builder /out/bin/tw_server /tw_server
|
||||
COPY --from=builder /out/bin/tw_mock_client /tw_mock_client
|
||||
COPY --from=builder /out/shaders/ /shaders/
|
||||
|
||||
|
||||
|
||||
# Mock client
|
||||
FROM gcr.io/distroless/base-debian12:nonroot AS mock-client
|
||||
|
||||
COPY --from=builder /out/bin/tw_mock_client /usr/local/bin/
|
||||
ENTRYPOINT ["/usr/local/bin/tw_mock_client"]
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[settings]
|
||||
arch=x86_64
|
||||
build_type=Release
|
||||
compiler=gcc
|
||||
compiler.version=14
|
||||
compiler.cppstd=gnu23
|
||||
compiler.libcxx=libstdc++11
|
||||
os=Linux
|
||||
|
||||
[conf]
|
||||
# xorg/system and egl/system resolve to apt packages; the build runs as root so
|
||||
# no sudo is available or needed.
|
||||
tools.system.package_manager:mode=install
|
||||
tools.system.package_manager:sudo=False
|
||||
Vendored
+2
-4
@@ -3,10 +3,9 @@ project(imgui)
|
||||
file(GLOB FILES
|
||||
./*.cpp
|
||||
./backends/imgui_impl_vulkan.cpp
|
||||
./backends/imgui_impl_sdl2.cpp
|
||||
./backends/imgui_impl_sdl3.cpp
|
||||
)
|
||||
|
||||
find_package(SDL2 QUIET)
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
add_library(${PROJECT_NAME} OBJECT ${FILES})
|
||||
@@ -21,11 +20,10 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
volk::volk
|
||||
${SDL2_LIBRARIES}
|
||||
sdl::sdl
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${volk_INCLUDE_DIRS}
|
||||
${SDL2_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
Vendored
+1
-1
@@ -25,7 +25,7 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
volk::volk
|
||||
${SDL2_LIBRARIES}
|
||||
sdl::sdl
|
||||
imgui::imgui
|
||||
)
|
||||
#
|
||||
|
||||
Vendored
+2
-3
@@ -5,13 +5,13 @@ option(LFT_ENABLE_EXAMPLES "Enable building examples" ON)
|
||||
if (WIN32)
|
||||
set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_WIN32_KHR)
|
||||
elseif(UNIX)
|
||||
set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_XCB_KHR)
|
||||
set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_XCB_KHR VK_USE_PLATFORM_WAYLAND_KHR)
|
||||
endif()
|
||||
|
||||
|
||||
# Please set default paths for vulkan and SDL2
|
||||
set(VULKAN_PATH "$ENV{VULKAN_SDK}" CACHE STRING "Path to vulkan directory")
|
||||
set(SDL2_PATH "$ENV{VULKAN_SDK}/cmake" CACHE STRING "Path to SDL2 directory")
|
||||
# set(SDL2_PATH "$ENV{VULKAN_SDK}/cmake" CACHE STRING "Path to SDL2 directory")
|
||||
|
||||
list(APPEND CMAKE_PREFIX_PATH "$ENV{VULKAN_SDK}/cmake")
|
||||
|
||||
@@ -51,7 +51,6 @@ add_definitions(
|
||||
)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
|
||||
Vendored
-140
@@ -1,140 +0,0 @@
|
||||
# CMAKE generated file: DO NOT EDIT!
|
||||
# Generated by "Unix Makefiles" Generator, CMake Version 3.30
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
default_target: all
|
||||
.PHONY : default_target
|
||||
|
||||
# Allow only one "make -f Makefile2" at a time, but pass parallelism.
|
||||
.NOTPARALLEL:
|
||||
|
||||
#=============================================================================
|
||||
# Special targets provided by cmake.
|
||||
|
||||
# Disable implicit rules so canonical targets will work.
|
||||
.SUFFIXES:
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : %,v
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : RCS/%
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : RCS/%,v
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : SCCS/s.%
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : s.%
|
||||
|
||||
.SUFFIXES: .hpux_make_needs_suffix_list
|
||||
|
||||
# Command-line flag to silence nested $(MAKE).
|
||||
$(VERBOSE)MAKESILENT = -s
|
||||
|
||||
#Suppress display of executed commands.
|
||||
$(VERBOSE).SILENT:
|
||||
|
||||
# A target that is always out of date.
|
||||
cmake_force:
|
||||
.PHONY : cmake_force
|
||||
|
||||
#=============================================================================
|
||||
# Set environment variables for the build.
|
||||
|
||||
# The shell in which to execute make rules.
|
||||
SHELL = /bin/sh
|
||||
|
||||
# The CMake executable.
|
||||
CMAKE_COMMAND = /usr/bin/cmake
|
||||
|
||||
# The command to remove a file.
|
||||
RM = /usr/bin/cmake -E rm -f
|
||||
|
||||
# Escaping for special characters.
|
||||
EQUALS = =
|
||||
|
||||
# The top-level source directory on which CMake was run.
|
||||
CMAKE_SOURCE_DIR = /home/martin/projects/mmo
|
||||
|
||||
# The top-level build directory on which CMake was run.
|
||||
CMAKE_BINARY_DIR = /home/martin/projects/mmo
|
||||
|
||||
#=============================================================================
|
||||
# Targets provided globally by CMake.
|
||||
|
||||
# Special rule for the target edit_cache
|
||||
edit_cache:
|
||||
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..."
|
||||
/usr/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
|
||||
.PHONY : edit_cache
|
||||
|
||||
# Special rule for the target edit_cache
|
||||
edit_cache/fast: edit_cache
|
||||
.PHONY : edit_cache/fast
|
||||
|
||||
# Special rule for the target rebuild_cache
|
||||
rebuild_cache:
|
||||
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..."
|
||||
/usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
|
||||
.PHONY : rebuild_cache
|
||||
|
||||
# Special rule for the target rebuild_cache
|
||||
rebuild_cache/fast: rebuild_cache
|
||||
.PHONY : rebuild_cache/fast
|
||||
|
||||
# The main all target
|
||||
all: cmake_check_build_system
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -E cmake_progress_start /home/martin/projects/mmo/CMakeFiles /home/martin/projects/mmo/external/loft//CMakeFiles/progress.marks
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/all
|
||||
$(CMAKE_COMMAND) -E cmake_progress_start /home/martin/projects/mmo/CMakeFiles 0
|
||||
.PHONY : all
|
||||
|
||||
# The main clean target
|
||||
clean:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/clean
|
||||
.PHONY : clean
|
||||
|
||||
# The main clean target
|
||||
clean/fast: clean
|
||||
.PHONY : clean/fast
|
||||
|
||||
# Prepare targets for installation.
|
||||
preinstall: all
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/preinstall
|
||||
.PHONY : preinstall
|
||||
|
||||
# Prepare targets for installation.
|
||||
preinstall/fast:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/preinstall
|
||||
.PHONY : preinstall/fast
|
||||
|
||||
# clear depends
|
||||
depend:
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1
|
||||
.PHONY : depend
|
||||
|
||||
# Help Target
|
||||
help:
|
||||
@echo "The following are some of the valid targets for this Makefile:"
|
||||
@echo "... all (the default if no target is provided)"
|
||||
@echo "... clean"
|
||||
@echo "... depend"
|
||||
@echo "... edit_cache"
|
||||
@echo "... rebuild_cache"
|
||||
.PHONY : help
|
||||
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# Special targets to cleanup operation of make.
|
||||
|
||||
# Special rule to run CMake to check the build system integrity.
|
||||
# No rule that depends on this can have commands that come from listfiles
|
||||
# because they might be regenerated.
|
||||
cmake_check_build_system:
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
|
||||
.PHONY : cmake_check_build_system
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ target_link_libraries(
|
||||
|
||||
target_link_libraries(
|
||||
${PROJECT_NAME} PRIVATE
|
||||
${SDL2_LIBRARIES}
|
||||
sdl::sdl
|
||||
)
|
||||
|
||||
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
@@ -17,7 +17,8 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
volk::volk
|
||||
${SDL2_LIBRARIES})
|
||||
sdl::sdl
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
|
||||
Vendored
-560
@@ -1,560 +0,0 @@
|
||||
# CMAKE generated file: DO NOT EDIT!
|
||||
# Generated by "Unix Makefiles" Generator, CMake Version 3.30
|
||||
|
||||
# Default target executed when no arguments are given to make.
|
||||
default_target: all
|
||||
.PHONY : default_target
|
||||
|
||||
# Allow only one "make -f Makefile2" at a time, but pass parallelism.
|
||||
.NOTPARALLEL:
|
||||
|
||||
#=============================================================================
|
||||
# Special targets provided by cmake.
|
||||
|
||||
# Disable implicit rules so canonical targets will work.
|
||||
.SUFFIXES:
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : %,v
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : RCS/%
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : RCS/%,v
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : SCCS/s.%
|
||||
|
||||
# Disable VCS-based implicit rules.
|
||||
% : s.%
|
||||
|
||||
.SUFFIXES: .hpux_make_needs_suffix_list
|
||||
|
||||
# Command-line flag to silence nested $(MAKE).
|
||||
$(VERBOSE)MAKESILENT = -s
|
||||
|
||||
#Suppress display of executed commands.
|
||||
$(VERBOSE).SILENT:
|
||||
|
||||
# A target that is always out of date.
|
||||
cmake_force:
|
||||
.PHONY : cmake_force
|
||||
|
||||
#=============================================================================
|
||||
# Set environment variables for the build.
|
||||
|
||||
# The shell in which to execute make rules.
|
||||
SHELL = /bin/sh
|
||||
|
||||
# The CMake executable.
|
||||
CMAKE_COMMAND = /usr/bin/cmake
|
||||
|
||||
# The command to remove a file.
|
||||
RM = /usr/bin/cmake -E rm -f
|
||||
|
||||
# Escaping for special characters.
|
||||
EQUALS = =
|
||||
|
||||
# The top-level source directory on which CMake was run.
|
||||
CMAKE_SOURCE_DIR = /home/martin/projects/mmo
|
||||
|
||||
# The top-level build directory on which CMake was run.
|
||||
CMAKE_BINARY_DIR = /home/martin/projects/mmo
|
||||
|
||||
#=============================================================================
|
||||
# Targets provided globally by CMake.
|
||||
|
||||
# Special rule for the target edit_cache
|
||||
edit_cache:
|
||||
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake cache editor..."
|
||||
/usr/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
|
||||
.PHONY : edit_cache
|
||||
|
||||
# Special rule for the target edit_cache
|
||||
edit_cache/fast: edit_cache
|
||||
.PHONY : edit_cache/fast
|
||||
|
||||
# Special rule for the target rebuild_cache
|
||||
rebuild_cache:
|
||||
@$(CMAKE_COMMAND) -E cmake_echo_color "--switch=$(COLOR)" --cyan "Running CMake to regenerate build system..."
|
||||
/usr/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
|
||||
.PHONY : rebuild_cache
|
||||
|
||||
# Special rule for the target rebuild_cache
|
||||
rebuild_cache/fast: rebuild_cache
|
||||
.PHONY : rebuild_cache/fast
|
||||
|
||||
# The main all target
|
||||
all: cmake_check_build_system
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -E cmake_progress_start /home/martin/projects/mmo/CMakeFiles /home/martin/projects/mmo/external/loft/modules/base//CMakeFiles/progress.marks
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/modules/base/all
|
||||
$(CMAKE_COMMAND) -E cmake_progress_start /home/martin/projects/mmo/CMakeFiles 0
|
||||
.PHONY : all
|
||||
|
||||
# The main clean target
|
||||
clean:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/modules/base/clean
|
||||
.PHONY : clean
|
||||
|
||||
# The main clean target
|
||||
clean/fast: clean
|
||||
.PHONY : clean/fast
|
||||
|
||||
# Prepare targets for installation.
|
||||
preinstall: all
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/modules/base/preinstall
|
||||
.PHONY : preinstall
|
||||
|
||||
# Prepare targets for installation.
|
||||
preinstall/fast:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/modules/base/preinstall
|
||||
.PHONY : preinstall/fast
|
||||
|
||||
# clear depends
|
||||
depend:
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 1
|
||||
.PHONY : depend
|
||||
|
||||
# Convenience name for target.
|
||||
external/loft/modules/base/CMakeFiles/loft_base.dir/rule:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 external/loft/modules/base/CMakeFiles/loft_base.dir/rule
|
||||
.PHONY : external/loft/modules/base/CMakeFiles/loft_base.dir/rule
|
||||
|
||||
# Convenience name for target.
|
||||
loft_base: external/loft/modules/base/CMakeFiles/loft_base.dir/rule
|
||||
.PHONY : loft_base
|
||||
|
||||
# fast build rule for target.
|
||||
loft_base/fast:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/build
|
||||
.PHONY : loft_base/fast
|
||||
|
||||
src/FramebufferBuilder.o: src/FramebufferBuilder.cpp.o
|
||||
.PHONY : src/FramebufferBuilder.o
|
||||
|
||||
# target to build an object file
|
||||
src/FramebufferBuilder.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/FramebufferBuilder.cpp.o
|
||||
.PHONY : src/FramebufferBuilder.cpp.o
|
||||
|
||||
src/FramebufferBuilder.i: src/FramebufferBuilder.cpp.i
|
||||
.PHONY : src/FramebufferBuilder.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/FramebufferBuilder.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/FramebufferBuilder.cpp.i
|
||||
.PHONY : src/FramebufferBuilder.cpp.i
|
||||
|
||||
src/FramebufferBuilder.s: src/FramebufferBuilder.cpp.s
|
||||
.PHONY : src/FramebufferBuilder.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/FramebufferBuilder.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/FramebufferBuilder.cpp.s
|
||||
.PHONY : src/FramebufferBuilder.cpp.s
|
||||
|
||||
src/Gpu.o: src/Gpu.cpp.o
|
||||
.PHONY : src/Gpu.o
|
||||
|
||||
# target to build an object file
|
||||
src/Gpu.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Gpu.cpp.o
|
||||
.PHONY : src/Gpu.cpp.o
|
||||
|
||||
src/Gpu.i: src/Gpu.cpp.i
|
||||
.PHONY : src/Gpu.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/Gpu.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Gpu.cpp.i
|
||||
.PHONY : src/Gpu.cpp.i
|
||||
|
||||
src/Gpu.s: src/Gpu.cpp.s
|
||||
.PHONY : src/Gpu.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/Gpu.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Gpu.cpp.s
|
||||
.PHONY : src/Gpu.cpp.s
|
||||
|
||||
src/Instance.o: src/Instance.cpp.o
|
||||
.PHONY : src/Instance.o
|
||||
|
||||
# target to build an object file
|
||||
src/Instance.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Instance.cpp.o
|
||||
.PHONY : src/Instance.cpp.o
|
||||
|
||||
src/Instance.i: src/Instance.cpp.i
|
||||
.PHONY : src/Instance.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/Instance.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Instance.cpp.i
|
||||
.PHONY : src/Instance.cpp.i
|
||||
|
||||
src/Instance.s: src/Instance.cpp.s
|
||||
.PHONY : src/Instance.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/Instance.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/Instance.cpp.s
|
||||
.PHONY : src/Instance.cpp.s
|
||||
|
||||
src/io/file.o: src/io/file.cpp.o
|
||||
.PHONY : src/io/file.o
|
||||
|
||||
# target to build an object file
|
||||
src/io/file.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/io/file.cpp.o
|
||||
.PHONY : src/io/file.cpp.o
|
||||
|
||||
src/io/file.i: src/io/file.cpp.i
|
||||
.PHONY : src/io/file.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/io/file.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/io/file.cpp.i
|
||||
.PHONY : src/io/file.cpp.i
|
||||
|
||||
src/io/file.s: src/io/file.cpp.s
|
||||
.PHONY : src/io/file.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/io/file.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/io/file.cpp.s
|
||||
.PHONY : src/io/file.cpp.s
|
||||
|
||||
src/resources/Buffer.o: src/resources/Buffer.cpp.o
|
||||
.PHONY : src/resources/Buffer.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/Buffer.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Buffer.cpp.o
|
||||
.PHONY : src/resources/Buffer.cpp.o
|
||||
|
||||
src/resources/Buffer.i: src/resources/Buffer.cpp.i
|
||||
.PHONY : src/resources/Buffer.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/Buffer.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Buffer.cpp.i
|
||||
.PHONY : src/resources/Buffer.cpp.i
|
||||
|
||||
src/resources/Buffer.s: src/resources/Buffer.cpp.s
|
||||
.PHONY : src/resources/Buffer.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/Buffer.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Buffer.cpp.s
|
||||
.PHONY : src/resources/Buffer.cpp.s
|
||||
|
||||
src/resources/BufferBusWriter.o: src/resources/BufferBusWriter.cpp.o
|
||||
.PHONY : src/resources/BufferBusWriter.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/BufferBusWriter.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/BufferBusWriter.cpp.o
|
||||
.PHONY : src/resources/BufferBusWriter.cpp.o
|
||||
|
||||
src/resources/BufferBusWriter.i: src/resources/BufferBusWriter.cpp.i
|
||||
.PHONY : src/resources/BufferBusWriter.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/BufferBusWriter.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/BufferBusWriter.cpp.i
|
||||
.PHONY : src/resources/BufferBusWriter.cpp.i
|
||||
|
||||
src/resources/BufferBusWriter.s: src/resources/BufferBusWriter.cpp.s
|
||||
.PHONY : src/resources/BufferBusWriter.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/BufferBusWriter.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/BufferBusWriter.cpp.s
|
||||
.PHONY : src/resources/BufferBusWriter.cpp.s
|
||||
|
||||
src/resources/DefaultAllocator.o: src/resources/DefaultAllocator.cpp.o
|
||||
.PHONY : src/resources/DefaultAllocator.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/DefaultAllocator.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/DefaultAllocator.cpp.o
|
||||
.PHONY : src/resources/DefaultAllocator.cpp.o
|
||||
|
||||
src/resources/DefaultAllocator.i: src/resources/DefaultAllocator.cpp.i
|
||||
.PHONY : src/resources/DefaultAllocator.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/DefaultAllocator.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/DefaultAllocator.cpp.i
|
||||
.PHONY : src/resources/DefaultAllocator.cpp.i
|
||||
|
||||
src/resources/DefaultAllocator.s: src/resources/DefaultAllocator.cpp.s
|
||||
.PHONY : src/resources/DefaultAllocator.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/DefaultAllocator.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/DefaultAllocator.cpp.s
|
||||
.PHONY : src/resources/DefaultAllocator.cpp.s
|
||||
|
||||
src/resources/Image.o: src/resources/Image.cpp.o
|
||||
.PHONY : src/resources/Image.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/Image.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Image.cpp.o
|
||||
.PHONY : src/resources/Image.cpp.o
|
||||
|
||||
src/resources/Image.i: src/resources/Image.cpp.i
|
||||
.PHONY : src/resources/Image.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/Image.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Image.cpp.i
|
||||
.PHONY : src/resources/Image.cpp.i
|
||||
|
||||
src/resources/Image.s: src/resources/Image.cpp.s
|
||||
.PHONY : src/resources/Image.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/Image.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/Image.cpp.s
|
||||
.PHONY : src/resources/Image.cpp.s
|
||||
|
||||
src/resources/ImageBusWriter.o: src/resources/ImageBusWriter.cpp.o
|
||||
.PHONY : src/resources/ImageBusWriter.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/ImageBusWriter.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageBusWriter.cpp.o
|
||||
.PHONY : src/resources/ImageBusWriter.cpp.o
|
||||
|
||||
src/resources/ImageBusWriter.i: src/resources/ImageBusWriter.cpp.i
|
||||
.PHONY : src/resources/ImageBusWriter.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/ImageBusWriter.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageBusWriter.cpp.i
|
||||
.PHONY : src/resources/ImageBusWriter.cpp.i
|
||||
|
||||
src/resources/ImageBusWriter.s: src/resources/ImageBusWriter.cpp.s
|
||||
.PHONY : src/resources/ImageBusWriter.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/ImageBusWriter.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageBusWriter.cpp.s
|
||||
.PHONY : src/resources/ImageBusWriter.cpp.s
|
||||
|
||||
src/resources/ImageView.o: src/resources/ImageView.cpp.o
|
||||
.PHONY : src/resources/ImageView.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/ImageView.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageView.cpp.o
|
||||
.PHONY : src/resources/ImageView.cpp.o
|
||||
|
||||
src/resources/ImageView.i: src/resources/ImageView.cpp.i
|
||||
.PHONY : src/resources/ImageView.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/ImageView.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageView.cpp.i
|
||||
.PHONY : src/resources/ImageView.cpp.i
|
||||
|
||||
src/resources/ImageView.s: src/resources/ImageView.cpp.s
|
||||
.PHONY : src/resources/ImageView.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/ImageView.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/ImageView.cpp.s
|
||||
.PHONY : src/resources/ImageView.cpp.s
|
||||
|
||||
src/resources/MipmapGenerator.o: src/resources/MipmapGenerator.cpp.o
|
||||
.PHONY : src/resources/MipmapGenerator.o
|
||||
|
||||
# target to build an object file
|
||||
src/resources/MipmapGenerator.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/MipmapGenerator.cpp.o
|
||||
.PHONY : src/resources/MipmapGenerator.cpp.o
|
||||
|
||||
src/resources/MipmapGenerator.i: src/resources/MipmapGenerator.cpp.i
|
||||
.PHONY : src/resources/MipmapGenerator.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/resources/MipmapGenerator.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/MipmapGenerator.cpp.i
|
||||
.PHONY : src/resources/MipmapGenerator.cpp.i
|
||||
|
||||
src/resources/MipmapGenerator.s: src/resources/MipmapGenerator.cpp.s
|
||||
.PHONY : src/resources/MipmapGenerator.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/resources/MipmapGenerator.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/resources/MipmapGenerator.cpp.s
|
||||
.PHONY : src/resources/MipmapGenerator.cpp.s
|
||||
|
||||
src/shaders/GlslShaderBuilder.o: src/shaders/GlslShaderBuilder.cpp.o
|
||||
.PHONY : src/shaders/GlslShaderBuilder.o
|
||||
|
||||
# target to build an object file
|
||||
src/shaders/GlslShaderBuilder.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/GlslShaderBuilder.cpp.o
|
||||
.PHONY : src/shaders/GlslShaderBuilder.cpp.o
|
||||
|
||||
src/shaders/GlslShaderBuilder.i: src/shaders/GlslShaderBuilder.cpp.i
|
||||
.PHONY : src/shaders/GlslShaderBuilder.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/shaders/GlslShaderBuilder.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/GlslShaderBuilder.cpp.i
|
||||
.PHONY : src/shaders/GlslShaderBuilder.cpp.i
|
||||
|
||||
src/shaders/GlslShaderBuilder.s: src/shaders/GlslShaderBuilder.cpp.s
|
||||
.PHONY : src/shaders/GlslShaderBuilder.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/shaders/GlslShaderBuilder.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/GlslShaderBuilder.cpp.s
|
||||
.PHONY : src/shaders/GlslShaderBuilder.cpp.s
|
||||
|
||||
src/shaders/Pipeline.o: src/shaders/Pipeline.cpp.o
|
||||
.PHONY : src/shaders/Pipeline.o
|
||||
|
||||
# target to build an object file
|
||||
src/shaders/Pipeline.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/Pipeline.cpp.o
|
||||
.PHONY : src/shaders/Pipeline.cpp.o
|
||||
|
||||
src/shaders/Pipeline.i: src/shaders/Pipeline.cpp.i
|
||||
.PHONY : src/shaders/Pipeline.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/shaders/Pipeline.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/Pipeline.cpp.i
|
||||
.PHONY : src/shaders/Pipeline.cpp.i
|
||||
|
||||
src/shaders/Pipeline.s: src/shaders/Pipeline.cpp.s
|
||||
.PHONY : src/shaders/Pipeline.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/shaders/Pipeline.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/Pipeline.cpp.s
|
||||
.PHONY : src/shaders/Pipeline.cpp.s
|
||||
|
||||
src/shaders/PipelineBuilder.o: src/shaders/PipelineBuilder.cpp.o
|
||||
.PHONY : src/shaders/PipelineBuilder.o
|
||||
|
||||
# target to build an object file
|
||||
src/shaders/PipelineBuilder.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/PipelineBuilder.cpp.o
|
||||
.PHONY : src/shaders/PipelineBuilder.cpp.o
|
||||
|
||||
src/shaders/PipelineBuilder.i: src/shaders/PipelineBuilder.cpp.i
|
||||
.PHONY : src/shaders/PipelineBuilder.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/shaders/PipelineBuilder.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/PipelineBuilder.cpp.i
|
||||
.PHONY : src/shaders/PipelineBuilder.cpp.i
|
||||
|
||||
src/shaders/PipelineBuilder.s: src/shaders/PipelineBuilder.cpp.s
|
||||
.PHONY : src/shaders/PipelineBuilder.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/shaders/PipelineBuilder.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/PipelineBuilder.cpp.s
|
||||
.PHONY : src/shaders/PipelineBuilder.cpp.s
|
||||
|
||||
src/shaders/SpirvShaderBuilder.o: src/shaders/SpirvShaderBuilder.cpp.o
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.o
|
||||
|
||||
# target to build an object file
|
||||
src/shaders/SpirvShaderBuilder.cpp.o:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/SpirvShaderBuilder.cpp.o
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.cpp.o
|
||||
|
||||
src/shaders/SpirvShaderBuilder.i: src/shaders/SpirvShaderBuilder.cpp.i
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.i
|
||||
|
||||
# target to preprocess a source file
|
||||
src/shaders/SpirvShaderBuilder.cpp.i:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/SpirvShaderBuilder.cpp.i
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.cpp.i
|
||||
|
||||
src/shaders/SpirvShaderBuilder.s: src/shaders/SpirvShaderBuilder.cpp.s
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.s
|
||||
|
||||
# target to generate assembly for a file
|
||||
src/shaders/SpirvShaderBuilder.cpp.s:
|
||||
cd /home/martin/projects/mmo && $(MAKE) $(MAKESILENT) -f external/loft/modules/base/CMakeFiles/loft_base.dir/build.make external/loft/modules/base/CMakeFiles/loft_base.dir/src/shaders/SpirvShaderBuilder.cpp.s
|
||||
.PHONY : src/shaders/SpirvShaderBuilder.cpp.s
|
||||
|
||||
# Help Target
|
||||
help:
|
||||
@echo "The following are some of the valid targets for this Makefile:"
|
||||
@echo "... all (the default if no target is provided)"
|
||||
@echo "... clean"
|
||||
@echo "... depend"
|
||||
@echo "... edit_cache"
|
||||
@echo "... rebuild_cache"
|
||||
@echo "... loft_base"
|
||||
@echo "... src/FramebufferBuilder.o"
|
||||
@echo "... src/FramebufferBuilder.i"
|
||||
@echo "... src/FramebufferBuilder.s"
|
||||
@echo "... src/Gpu.o"
|
||||
@echo "... src/Gpu.i"
|
||||
@echo "... src/Gpu.s"
|
||||
@echo "... src/Instance.o"
|
||||
@echo "... src/Instance.i"
|
||||
@echo "... src/Instance.s"
|
||||
@echo "... src/io/file.o"
|
||||
@echo "... src/io/file.i"
|
||||
@echo "... src/io/file.s"
|
||||
@echo "... src/resources/Buffer.o"
|
||||
@echo "... src/resources/Buffer.i"
|
||||
@echo "... src/resources/Buffer.s"
|
||||
@echo "... src/resources/BufferBusWriter.o"
|
||||
@echo "... src/resources/BufferBusWriter.i"
|
||||
@echo "... src/resources/BufferBusWriter.s"
|
||||
@echo "... src/resources/DefaultAllocator.o"
|
||||
@echo "... src/resources/DefaultAllocator.i"
|
||||
@echo "... src/resources/DefaultAllocator.s"
|
||||
@echo "... src/resources/Image.o"
|
||||
@echo "... src/resources/Image.i"
|
||||
@echo "... src/resources/Image.s"
|
||||
@echo "... src/resources/ImageBusWriter.o"
|
||||
@echo "... src/resources/ImageBusWriter.i"
|
||||
@echo "... src/resources/ImageBusWriter.s"
|
||||
@echo "... src/resources/ImageView.o"
|
||||
@echo "... src/resources/ImageView.i"
|
||||
@echo "... src/resources/ImageView.s"
|
||||
@echo "... src/resources/MipmapGenerator.o"
|
||||
@echo "... src/resources/MipmapGenerator.i"
|
||||
@echo "... src/resources/MipmapGenerator.s"
|
||||
@echo "... src/shaders/GlslShaderBuilder.o"
|
||||
@echo "... src/shaders/GlslShaderBuilder.i"
|
||||
@echo "... src/shaders/GlslShaderBuilder.s"
|
||||
@echo "... src/shaders/Pipeline.o"
|
||||
@echo "... src/shaders/Pipeline.i"
|
||||
@echo "... src/shaders/Pipeline.s"
|
||||
@echo "... src/shaders/PipelineBuilder.o"
|
||||
@echo "... src/shaders/PipelineBuilder.i"
|
||||
@echo "... src/shaders/PipelineBuilder.s"
|
||||
@echo "... src/shaders/SpirvShaderBuilder.o"
|
||||
@echo "... src/shaders/SpirvShaderBuilder.i"
|
||||
@echo "... src/shaders/SpirvShaderBuilder.s"
|
||||
.PHONY : help
|
||||
|
||||
|
||||
|
||||
#=============================================================================
|
||||
# Special targets to cleanup operation of make.
|
||||
|
||||
# Special rule to run CMake to check the build system integrity.
|
||||
# No rule that depends on this can have commands that come from listfiles
|
||||
# because they might be regenerated.
|
||||
cmake_check_build_system:
|
||||
cd /home/martin/projects/mmo && $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-build-system CMakeFiles/Makefile.cmake 0
|
||||
.PHONY : cmake_check_build_system
|
||||
|
||||
@@ -9,7 +9,6 @@ set(LIBS
|
||||
loft_base
|
||||
loft_common
|
||||
loft_window
|
||||
${SDL2_LIBRARIES}
|
||||
volk)
|
||||
|
||||
set(FILES
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@ file(GLOB FILES src/*.c src/*.cpp)
|
||||
|
||||
add_library(${PROJECT_NAME} ${FILES})
|
||||
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(SDL3 REQUIRED)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/include/"
|
||||
@@ -13,7 +13,8 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
loft_base
|
||||
${SDL2_LIBRARIES}
|
||||
sdl::sdl
|
||||
${LIBDECOR_LIB}
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
#define LOFT_SDLWINDOW_H
|
||||
|
||||
#include "Window.hpp"
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <utility>
|
||||
#include <volk.h>
|
||||
#include <string>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <volk.h>
|
||||
#include <SDL2/SDL_events.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
+23
-19
@@ -1,10 +1,12 @@
|
||||
#include <SDL2/SDL_error.h>
|
||||
#include <SDL_video.h>
|
||||
#include "SDLWindow.h"
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <volk.h>
|
||||
#include <stdexcept>
|
||||
#include "SDLWindow.h"
|
||||
|
||||
#include "LoftException.hpp"
|
||||
#include <SDL2/SDL_vulkan.h>
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
@@ -27,16 +29,16 @@ VkExtent2D SDLWindow::get_size() const {
|
||||
SDLWindow::SDLWindow(std::string name, VkRect2D rect) :
|
||||
m_extent{rect.extent}
|
||||
{
|
||||
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVERYTHING) < 0) {
|
||||
if(!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
throw std::runtime_error("Failed to initialize sdl");
|
||||
}
|
||||
|
||||
m_pWindow = SDL_CreateWindow(name.c_str(),
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
// SDL_WINDOWPOS_CENTERED,
|
||||
// SDL_WINDOWPOS_CENTERED,
|
||||
rect.extent.width,
|
||||
rect.extent.height,
|
||||
SDL_WINDOW_VULKAN | SDL_WINDOW_ALLOW_HIGHDPI |
|
||||
SDL_WINDOW_VULKAN | SDL_WINDOW_HIGH_PIXEL_DENSITY |
|
||||
SDL_WINDOW_RESIZABLE);
|
||||
|
||||
if(m_pWindow == nullptr) {
|
||||
@@ -46,7 +48,7 @@ m_extent{rect.extent}
|
||||
|
||||
Surface SDLWindow::create_surface(const Instance* instance) const {
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
SDL_Vulkan_CreateSurface(m_pWindow, instance->instance(), &surface);
|
||||
SDL_Vulkan_CreateSurface(m_pWindow, instance->instance(), NULL, &surface);
|
||||
return Surface(instance, surface);
|
||||
}
|
||||
|
||||
@@ -60,19 +62,21 @@ int32_t SDLWindow::poll_event(SDL_Event *pOutEvent) const {
|
||||
|
||||
std::vector<std::string> SDLWindow::get_required_extensions() const {
|
||||
uint32_t count = 0;
|
||||
SDL_Vulkan_GetInstanceExtensions(m_pWindow, &count, nullptr);
|
||||
const char* const *instance_extensions = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||
|
||||
std::vector<const char*> extensions(count);
|
||||
SDL_Vulkan_GetInstanceExtensions(m_pWindow, &count, extensions.data());
|
||||
std::vector<std::string> extensions(count);
|
||||
for(int i = 0; i < count; i++) {
|
||||
extensions[i] = std::string(instance_extensions[i]);
|
||||
}
|
||||
|
||||
std::vector<std::string> extension_strings(count);
|
||||
std::transform(extensions.begin(), extensions.end(),
|
||||
extension_strings.begin(),
|
||||
[](const char* str) {
|
||||
return std::string(str);
|
||||
});
|
||||
// std::vector<std::string> extension_strings(count);
|
||||
// std::transform(extensions.begin(), extensions.end(),
|
||||
// extension_strings.begin(),
|
||||
// [](const char* str) {
|
||||
// return std::string(str);
|
||||
// });
|
||||
|
||||
return extension_strings;
|
||||
return extensions;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw::network
|
||||
tl::expected
|
||||
tw::messaging
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
protobuf::libprotobuf
|
||||
tw::chat::lib
|
||||
spdlog::spdlog
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "MessageSession.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "SendChatMessage.hpp"
|
||||
#include "ChatClientError.hpp"
|
||||
#include "models/ChatMessage.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
class ChatClient {
|
||||
tw::MessageSession m_session;
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
std::function<void(ChatMessage)> m_on_message;
|
||||
std::function<void(tl::expected<void, ChatClientError>)> m_on_send_response;
|
||||
|
||||
@@ -22,13 +22,39 @@ tl::expected<void, tw::chat::ChatClientError> from_error_code(mmo::chat::ChatErr
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
namespace {
|
||||
|
||||
std::unique_ptr<tw::msg::MessageEndpoint> create_endpoint() {
|
||||
auto endpoint_r = tw::msg::MessageEndpoint::create();
|
||||
if (!endpoint_r) {
|
||||
throw std::runtime_error("Chat client failed to create an endpoint: " +
|
||||
endpoint_r.error().message());
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
tw::msg::MessageConnection* connect_to_server(tw::msg::MessageEndpoint* endpoint,
|
||||
const std::string& server_address,
|
||||
int16_t port) {
|
||||
auto server_r = endpoint->connect(server_address, port);
|
||||
if (!server_r) {
|
||||
throw std::runtime_error("Chat client failed to connect: " + server_r.error().message());
|
||||
}
|
||||
|
||||
return server_r.value();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ChatClient::ChatClient(const std::string& server_address, int16_t port)
|
||||
: m_session(net::Address(server_address, port))
|
||||
: m_endpoint(create_endpoint())
|
||||
, m_server(connect_to_server(m_endpoint.get(), server_address, port))
|
||||
, m_messages(m_endpoint.get())
|
||||
{
|
||||
m_session.set_handler(CHAT_MESSAGE_BROADCAST_REQUEST, [this](std::span<const std::byte> data) {
|
||||
m_messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
|
||||
[this](msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& proto) {
|
||||
if (!m_on_message) return;
|
||||
mmo::chat::ChatMessageBroadcastRequest proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
ChatMessage msg;
|
||||
msg.channel_id = proto.channel_id();
|
||||
msg.client_id = proto.sender_id();
|
||||
@@ -37,30 +63,27 @@ ChatClient::ChatClient(const std::string& server_address, int16_t port)
|
||||
m_on_message(std::move(msg));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_SEND_MESSAGE_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
m_messages.set_handler<mmo::chat::SendChatMessageResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::SendChatMessageResponse& proto) {
|
||||
if (!m_on_send_response) return;
|
||||
mmo::chat::SendChatMessageResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_send_response(from_error_code(proto.error()));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_JOIN_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
m_messages.set_handler<mmo::chat::JoinChannelResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::JoinChannelResponse& proto) {
|
||||
if (!m_on_join_response) return;
|
||||
mmo::chat::JoinChannelResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_join_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
|
||||
m_session.set_handler(CHAT_LEAVE_CHANNEL_RESPONSE, [this](std::span<const std::byte> data) {
|
||||
m_messages.set_handler<mmo::chat::LeaveChannelResponse>(
|
||||
[this](msg::PeerId, const mmo::chat::LeaveChannelResponse& proto) {
|
||||
if (!m_on_leave_response) return;
|
||||
mmo::chat::LeaveChannelResponse proto;
|
||||
if (!proto.ParseFromArray(data.data(), static_cast<int>(data.size()))) return;
|
||||
m_on_leave_response(static_cast<uint32_t>(proto.channel_id()), from_error_code(proto.error()));
|
||||
});
|
||||
}
|
||||
|
||||
void ChatClient::update() {
|
||||
m_session.update();
|
||||
m_endpoint->update();
|
||||
}
|
||||
|
||||
tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage message) {
|
||||
@@ -68,11 +91,7 @@ tl::expected<void, ChatClientError> ChatClient::send_mesg(SendChatMessage messag
|
||||
mesg.set_channel_id(message.channel_id);
|
||||
mesg.set_message(message.message);
|
||||
|
||||
std::vector<std::byte> buf(mesg.ByteSizeLong());
|
||||
(void)mesg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
|
||||
|
||||
auto send_r = m_session.send(Message<mmo::chat::SendChatMessageRequest>::value,
|
||||
std::span(buf), true);
|
||||
auto send_r = m_messages.send(m_server, mesg, true);
|
||||
if (!send_r)
|
||||
return tl::make_unexpected(ChatClientError::PermissionDenied);
|
||||
return {};
|
||||
|
||||
@@ -13,8 +13,9 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
tw::chat::service
|
||||
tw::messaging
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::network
|
||||
tw::quicr
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "Chat.pb.h"
|
||||
#include "MessageRegistry.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
@@ -16,108 +16,68 @@ static mmo::chat::ChatErrorCode to_error_code(tl::expected<void, ChatServerError
|
||||
return mmo::chat::CHAT_ERROR_CODE_CHANNEL_NOT_FOUND;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static std::vector<std::byte> serialize(const T& msg) {
|
||||
std::vector<std::byte> buf(msg.ByteSizeLong());
|
||||
(void)msg.SerializeToArray(buf.data(), static_cast<int>(buf.size()));
|
||||
return buf;
|
||||
static std::unique_ptr<msg::MessageEndpoint> bind_endpoint(int port) {
|
||||
auto endpoint_r = msg::MessageEndpoint::bind(port);
|
||||
if (!endpoint_r) {
|
||||
throw std::runtime_error("Chat server failed to bind to port " + std::to_string(port) +
|
||||
": " + endpoint_r.error().message());
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
ChatServerController::ChatServerController(int port)
|
||||
: m_endpoint(net::quicr::QuicrEndpoint::create_and_bind(port).value())
|
||||
, m_listener(net::quicr::QuicrConnectionListener::listen(m_endpoint.get()).value())
|
||||
: m_endpoint(bind_endpoint(port))
|
||||
, m_messages(m_endpoint.get())
|
||||
, m_service([this](uint64_t id, const ChatMessage& msg) { broadcast(id, msg); })
|
||||
{
|
||||
m_endpoint->set_on_peer_connected([](msg::PeerId client_id) {
|
||||
spdlog::info("Chat client connected: {}", client_id);
|
||||
});
|
||||
|
||||
register_handlers();
|
||||
spdlog::info("Chat server listening on port {}", port);
|
||||
}
|
||||
|
||||
void ChatServerController::register_handlers() {
|
||||
m_handlers[Message<mmo::chat::SendChatMessageRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::SendChatMessageRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::SendChatMessageRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::SendChatMessageRequest& msg) {
|
||||
mmo::chat::SendChatMessageResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(to_error_code(m_service.send_message(client_id, msg.channel_id(), msg.message())));
|
||||
send_to(client_id, Message<mmo::chat::SendChatMessageResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
|
||||
m_handlers[Message<mmo::chat::JoinChannelRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::JoinChannelRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::JoinChannelRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::JoinChannelRequest& msg) {
|
||||
m_service.join_channel(client_id, msg.channel_id());
|
||||
mmo::chat::JoinChannelResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
|
||||
send_to(client_id, Message<mmo::chat::JoinChannelResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
|
||||
m_handlers[Message<mmo::chat::LeaveChannelRequest>::value] =
|
||||
[this](uint64_t client_id, std::span<const std::byte> data) {
|
||||
mmo::chat::LeaveChannelRequest msg;
|
||||
msg.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
m_messages.set_handler<mmo::chat::LeaveChannelRequest>(
|
||||
[this](msg::PeerId client_id, const mmo::chat::LeaveChannelRequest& msg) {
|
||||
m_service.leave_channel(client_id, msg.channel_id());
|
||||
mmo::chat::LeaveChannelResponse r;
|
||||
r.set_channel_id(msg.channel_id());
|
||||
r.set_error(mmo::chat::CHAT_ERROR_CODE_OK);
|
||||
send_to(client_id, Message<mmo::chat::LeaveChannelResponse>::value, serialize(r));
|
||||
};
|
||||
(void)m_messages.send_to(client_id, r, false);
|
||||
});
|
||||
}
|
||||
|
||||
void ChatServerController::update() {
|
||||
m_endpoint->poll();
|
||||
|
||||
net::quicr::QuicrConnection* conn = nullptr;
|
||||
while ((conn = m_listener->listen())) {
|
||||
m_connections.emplace(conn->self_id(), conn);
|
||||
spdlog::info("Chat client connected: {}", conn->self_id());
|
||||
m_endpoint->update();
|
||||
}
|
||||
|
||||
for (auto& [client_id, conn] : m_connections) {
|
||||
auto r = conn->read_into(m_recv_buf);
|
||||
if (!r || *r == 0) continue;
|
||||
dispatch(client_id, std::span(m_recv_buf.data(), *r));
|
||||
}
|
||||
}
|
||||
|
||||
void ChatServerController::dispatch(uint64_t client_id, std::span<const std::byte> data) {
|
||||
constexpr size_t HEADER = sizeof(uint32_t) * 2;
|
||||
if (data.size() < HEADER) {
|
||||
spdlog::warn("ChatServerController: dropped short datagram ({} bytes)", data.size());
|
||||
return;
|
||||
}
|
||||
uint32_t type{};
|
||||
std::memcpy(&type, data.data(), sizeof(type));
|
||||
|
||||
if (type >= m_handlers.size() || !m_handlers[type]) {
|
||||
spdlog::warn("ChatServerController: no handler for type {}", type);
|
||||
return;
|
||||
}
|
||||
m_handlers[type](client_id, data.subspan(HEADER));
|
||||
}
|
||||
|
||||
void ChatServerController::send_to(uint64_t client_id, uint32_t type,
|
||||
std::span<const std::byte> payload, bool reliable) {
|
||||
auto it = m_connections.find(client_id);
|
||||
if (it == m_connections.end()) return;
|
||||
|
||||
constexpr uint32_t SEQ_NONE = 0;
|
||||
std::vector<std::byte> buf(sizeof(type) + sizeof(SEQ_NONE) + payload.size());
|
||||
std::memcpy(buf.data(), &type, sizeof(type));
|
||||
std::memcpy(buf.data() + sizeof(type), &SEQ_NONE, sizeof(SEQ_NONE));
|
||||
std::memcpy(buf.data() + sizeof(type) + sizeof(SEQ_NONE), payload.data(), payload.size());
|
||||
(void)it->second->send_message(std::span(buf), reliable);
|
||||
}
|
||||
|
||||
void ChatServerController::broadcast(uint64_t client_id, const ChatMessage& msg) {
|
||||
void ChatServerController::broadcast(msg::PeerId client_id, const ChatMessage& msg) {
|
||||
mmo::chat::ChatMessageBroadcastRequest bcast;
|
||||
bcast.set_channel_id(msg.channel_id);
|
||||
bcast.set_sender_id(msg.client_id);
|
||||
bcast.set_message(msg.message);
|
||||
send_to(client_id, Message<mmo::chat::ChatMessageBroadcastRequest>::value,
|
||||
serialize(bcast), true);
|
||||
|
||||
(void)m_messages.send_to(client_id, bcast, true);
|
||||
}
|
||||
|
||||
} // namespace tw::chat
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "ChatService.hpp"
|
||||
#include "protocol/quicr/QuicrEndpoint.hpp"
|
||||
#include "protocol/quicr/QuicrConnectionListener.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <span>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace tw::chat {
|
||||
|
||||
class ChatServerController {
|
||||
static constexpr size_t MAX_TYPES = 32;
|
||||
|
||||
std::unique_ptr<net::quicr::QuicrEndpoint> m_endpoint;
|
||||
std::unique_ptr<net::quicr::QuicrConnectionListener> m_listener;
|
||||
std::unordered_map<uint64_t, net::quicr::QuicrConnection*> m_connections;
|
||||
std::vector<std::byte> m_recv_buf{64 * 1024};
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
ProtobufMessages m_messages;
|
||||
ChatService m_service;
|
||||
|
||||
std::array<std::function<void(uint64_t, std::span<const std::byte>)>, MAX_TYPES> m_handlers{};
|
||||
|
||||
public:
|
||||
explicit ChatServerController(int port = CHAT_DEFAULT_PORT);
|
||||
|
||||
@@ -31,10 +21,7 @@ public:
|
||||
|
||||
private:
|
||||
void register_handlers();
|
||||
void dispatch(uint64_t client_id, std::span<const std::byte> data);
|
||||
void send_to(uint64_t client_id, uint32_t type, std::span<const std::byte> payload,
|
||||
bool reliable = false);
|
||||
void broadcast(uint64_t client_id, const ChatMessage& msg);
|
||||
void broadcast(msg::PeerId client_id, const ChatMessage& msg);
|
||||
};
|
||||
|
||||
} // namespace tw::chat
|
||||
|
||||
@@ -5,7 +5,8 @@ add_executable(${PROJECT_NAME} ChatMockClient.cpp)
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
tw::protocol
|
||||
tw::messaging
|
||||
tw::message_protocol
|
||||
tw::network
|
||||
tw::quicr
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "Address.hpp"
|
||||
#include "MessageSession.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "MessageRegistry.hpp"
|
||||
#include "Chat.pb.h"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <atomic>
|
||||
@@ -52,43 +53,55 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
if (!channel_set) {
|
||||
std::println(stderr, "Usage: {} [--host <ip>] [--port <port>] --channel <id>", argv[0]);
|
||||
// std::println(stderr, "Usage: {} [--host <ip>] [--port <port>] --channel <id>", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
tw::MessageSession session(tw::net::Address{std::string{host}, port});
|
||||
auto endpoint_r = tw::msg::MessageEndpoint::create();
|
||||
if (!endpoint_r) {
|
||||
spdlog::error("Failed to create an endpoint: {}", endpoint_r.error().message());
|
||||
return 1;
|
||||
}
|
||||
auto& endpoint = endpoint_r.value();
|
||||
|
||||
auto server_r = endpoint->connect(host, port);
|
||||
if (!server_r) {
|
||||
spdlog::error("Failed to connect: {}", server_r.error().message());
|
||||
return 1;
|
||||
}
|
||||
auto* server = server_r.value();
|
||||
|
||||
tw::ProtobufMessages messages(endpoint.get());
|
||||
|
||||
spdlog::info("Connecting to {}:{}...", host, port);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!session.is_established()) {
|
||||
while (!server->is_established()) {
|
||||
if (std::chrono::steady_clock::now() > deadline) {
|
||||
spdlog::error("Connection timed out");
|
||||
return 1;
|
||||
}
|
||||
session.update();
|
||||
endpoint->update();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
spdlog::info("Connected. Joining channel {}...", channel_id);
|
||||
|
||||
session.set_handler(tw::Message<mmo::chat::ChatMessageBroadcastRequest>::value,
|
||||
[](std::span<const std::byte> data) {
|
||||
mmo::chat::ChatMessageBroadcastRequest bcast;
|
||||
bcast.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
|
||||
messages.set_handler<mmo::chat::ChatMessageBroadcastRequest>(
|
||||
[](tw::msg::PeerId, const mmo::chat::ChatMessageBroadcastRequest& bcast) {
|
||||
// std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
|
||||
});
|
||||
|
||||
mmo::chat::JoinChannelRequest join;
|
||||
join.set_channel_id(channel_id);
|
||||
(void)session.request(
|
||||
(void)server->request(
|
||||
tw::Message<mmo::chat::JoinChannelRequest>::value,
|
||||
serialize(join),
|
||||
[channel_id](std::span<const std::byte> data) {
|
||||
mmo::chat::JoinChannelResponse resp;
|
||||
resp.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
std::println("Joined channel {} successfully.", channel_id);
|
||||
// std::println("Joined channel {} successfully.", channel_id);
|
||||
});
|
||||
|
||||
std::println("Joined channel {}. Type a message and press Enter. Ctrl+C to quit.", channel_id);
|
||||
// std::println("Joined channel {}. Type a message and press Enter. Ctrl+C to quit.", channel_id);
|
||||
|
||||
while (!g_quit.load()) {
|
||||
fd_set fds;
|
||||
@@ -102,18 +115,18 @@ int main(int argc, char* argv[]) {
|
||||
mmo::chat::SendChatMessageRequest msg;
|
||||
msg.set_channel_id(channel_id);
|
||||
msg.set_message(line);
|
||||
(void)session.request(
|
||||
(void)server->request(
|
||||
tw::Message<mmo::chat::SendChatMessageRequest>::value,
|
||||
serialize(msg),
|
||||
[channel_id](std::span<const std::byte> data) {
|
||||
mmo::chat::SendChatMessageResponse resp;
|
||||
resp.ParseFromArray(data.data(), static_cast<int>(data.size()));
|
||||
std::println("Message sent to channel {}.", channel_id);
|
||||
// std::println("Message sent to channel {}.", channel_id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
session.update();
|
||||
endpoint->update();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -4,12 +4,15 @@ add_subdirectory(shaders)
|
||||
|
||||
file(GLOB FILES
|
||||
src/*.cpp
|
||||
src/app/*.cpp
|
||||
src/network/*.cpp
|
||||
src/world/*.cpp
|
||||
src/world/controllers/*.cpp
|
||||
src/io/*.cpp
|
||||
src/draw/*.cpp
|
||||
src/draw/RenderPasses/*.cpp
|
||||
src/debug/*.cpp
|
||||
src/debug/metrics/*.cpp
|
||||
src/debug/tools/*.cpp
|
||||
)
|
||||
|
||||
@@ -18,13 +21,17 @@ add_dependencies(tw_shaders imgui::plot imgui::plot)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
towards
|
||||
tw_common
|
||||
tw::network
|
||||
tw::metrics
|
||||
tw::quicr
|
||||
loft::common
|
||||
loft::base
|
||||
${LIBDECOR_LIB}
|
||||
loft_window
|
||||
loft::render_graph
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::serialization
|
||||
tw::gui
|
||||
imgui::imgui
|
||||
@@ -34,8 +41,6 @@ target_link_libraries(${PROJECT_NAME}
|
||||
Jolt
|
||||
Tracy::TracyClient
|
||||
TracyClient
|
||||
pqxx
|
||||
pq
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
layout(location = 0) in vec3 inNormal;
|
||||
|
||||
void main() {
|
||||
float diffuse = max(0.1, dot(vec3(0.8, 1.5, -1.0), inNormal));
|
||||
outColor = vec4(1.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
layout(location = 0) in vec3 pos;
|
||||
layout(location = 1) in vec3 norm;
|
||||
layout(location = 0) out vec3 outNormal;
|
||||
|
||||
layout(set = 0, binding = 0) uniform Camera {
|
||||
mat4 proj;
|
||||
@@ -21,5 +22,6 @@ void main() {
|
||||
// outPos = pos.xyz * 0.05;
|
||||
// outNormal = norm.xyz;
|
||||
// outUV = uv;
|
||||
outNormal = norm;
|
||||
gl_Position = cam.proj * cam.view * PushConstants.transform * vec4(pos, 1.0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "ClientArgs.hpp"
|
||||
|
||||
#include <charconv>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string_view trim(std::string_view str) {
|
||||
// Skip leading whitespace
|
||||
size_t start = 0;
|
||||
while(start < str.length() && std::isspace(static_cast<unsigned char>(str[start]))) {
|
||||
++start;
|
||||
}
|
||||
|
||||
// Skip trailing whitespace
|
||||
size_t end = str.length();
|
||||
while(end > start && std::isspace(static_cast<unsigned char>(str[end - 1]))) {
|
||||
--end;
|
||||
}
|
||||
|
||||
return str.substr(start, end - start);
|
||||
}
|
||||
|
||||
tl::expected<int, std::string> parse_port(std::string_view port_str) {
|
||||
if(port_str.empty()) {
|
||||
return 8080; // Default port
|
||||
}
|
||||
|
||||
int port = 0;
|
||||
const char* end = port_str.data() + port_str.length();
|
||||
auto result = std::from_chars(port_str.data(), end, port);
|
||||
|
||||
// from_chars stops at the first character it cannot use, so a partially
|
||||
// numeric port like "80x" would otherwise be accepted as 80.
|
||||
if(result.ec != std::errc() || result.ptr != end) {
|
||||
return tl::make_unexpected("port must be numeric");
|
||||
}
|
||||
|
||||
if(port < 1 || port > 65535) {
|
||||
return tl::make_unexpected("port must be between 1 and 65535");
|
||||
}
|
||||
|
||||
return port;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
tl::expected<net::Address, std::string> parse_address(std::string_view text) {
|
||||
// Trim whitespace
|
||||
text = trim(text);
|
||||
|
||||
if(text.empty()) {
|
||||
return tl::make_unexpected("address cannot be empty");
|
||||
}
|
||||
|
||||
std::string_view host;
|
||||
std::string_view port_str;
|
||||
|
||||
// An IPv6 literal carries colons of its own, so the brackets it is written
|
||||
// in are what says where the host ends. This is the form to_string() emits.
|
||||
if(text.front() == '[') {
|
||||
size_t closing = text.find(']');
|
||||
if(closing == std::string_view::npos) {
|
||||
return tl::make_unexpected("address is missing a closing bracket");
|
||||
}
|
||||
|
||||
host = text.substr(1, closing - 1);
|
||||
port_str = text.substr(closing + 1);
|
||||
|
||||
if(!port_str.empty()) {
|
||||
if(port_str.front() != ':') {
|
||||
return tl::make_unexpected("expected a port after the closing bracket");
|
||||
}
|
||||
port_str.remove_prefix(1);
|
||||
}
|
||||
} else if(size_t colon_pos = text.rfind(':'); colon_pos == std::string_view::npos) {
|
||||
bool all_digits = !text.empty() && std::all_of(text.begin(), text.end(),
|
||||
[](unsigned char c) { return std::isdigit(c); });
|
||||
|
||||
if(all_digits) {
|
||||
host = "127.0.0.1";
|
||||
port_str = text;
|
||||
} else {
|
||||
host = text;
|
||||
port_str = "";
|
||||
}
|
||||
} else {
|
||||
host = text.substr(0, colon_pos);
|
||||
port_str = text.substr(colon_pos + 1);
|
||||
}
|
||||
|
||||
// Validate host
|
||||
if(host.empty()) {
|
||||
return tl::make_unexpected("host cannot be empty");
|
||||
}
|
||||
|
||||
// Parse port
|
||||
auto port_result = parse_port(port_str);
|
||||
if(!port_result) {
|
||||
return tl::make_unexpected(port_result.error());
|
||||
}
|
||||
|
||||
int port = port_result.value();
|
||||
|
||||
auto address_r = net::Address::resolve(std::string(host), port);
|
||||
if(!address_r) {
|
||||
return tl::make_unexpected(address_r.error().message());
|
||||
}
|
||||
|
||||
return address_r.value();
|
||||
}
|
||||
|
||||
std::optional<std::string> server_arg(int argc, char** argv) {
|
||||
for(int i = 1; i < argc - 1; ++i) {
|
||||
if(std::string_view(argv[i]) == "--server") {
|
||||
return std::string(argv[i + 1]);
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Parse an address string into a network address.
|
||||
*
|
||||
* Accepts "host:port" or a bare port number, where the host may be a name as
|
||||
* well as an address literal; an IPv6 literal has to be bracketed, as
|
||||
* "[::1]:8080". Bare port uses 127.0.0.1. Missing port defaults to 8080.
|
||||
* Trims surrounding whitespace.
|
||||
*
|
||||
* Looks the host up, so it blocks for as long as that takes, and returns an
|
||||
* error string for a host that does not resolve or an invalid port.
|
||||
*/
|
||||
tl::expected<net::Address, std::string> parse_address(std::string_view text);
|
||||
|
||||
/**
|
||||
* Extract the --server argument value from the command line.
|
||||
*
|
||||
* Scans argv for --server and returns the following argument,
|
||||
* or nothing if the flag is absent or has no value.
|
||||
*/
|
||||
std::optional<std::string> server_arg(int argc, char** argv);
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "FavouriteServers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
FavouriteServers::FavouriteServers()
|
||||
: FileAddressList("favourite_servers.txt") {
|
||||
}
|
||||
|
||||
const char* FavouriteServers::name() const {
|
||||
return "Favourites";
|
||||
}
|
||||
|
||||
void FavouriteServers::add(const std::string& entry) {
|
||||
// Check if already present
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
return; // Already present, do nothing
|
||||
}
|
||||
|
||||
// Append at the end
|
||||
m_entries.push_back(entry);
|
||||
|
||||
// Cap at MAX_ENTRIES
|
||||
if(m_entries.size() > MAX_ENTRIES) {
|
||||
m_entries.resize(MAX_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Manages a list of favourite server addresses.
|
||||
*
|
||||
* Persists addresses to a text file, one "ip:port" per line.
|
||||
* Appended in the order they are added, de-duplicated,
|
||||
* capped at 32 entries. File path is
|
||||
* $XDG_CONFIG_HOME/towards/favourite_servers.txt, falling back to
|
||||
* $HOME/.config/towards/favourite_servers.txt. If both variables are
|
||||
* unset, keeps the list in memory only.
|
||||
*/
|
||||
class FavouriteServers : public FileAddressList {
|
||||
static constexpr size_t MAX_ENTRIES = 32;
|
||||
|
||||
public:
|
||||
FavouriteServers();
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
/**
|
||||
* Add an address to the favourites list.
|
||||
* Appended at the end if not already present, capped at 32.
|
||||
* Does not persist to disk; call save() after modifying.
|
||||
*/
|
||||
void add(const std::string& entry) override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string get_config_dir() {
|
||||
// Try XDG_CONFIG_HOME first
|
||||
const char* xdg_config_home = std::getenv("XDG_CONFIG_HOME");
|
||||
if(xdg_config_home && xdg_config_home[0] != '\0') {
|
||||
return std::string(xdg_config_home) + "/towards";
|
||||
}
|
||||
|
||||
// Fall back to $HOME/.config/towards
|
||||
const char* home = std::getenv("HOME");
|
||||
if(home && home[0] != '\0') {
|
||||
return std::string(home) + "/.config/towards";
|
||||
}
|
||||
|
||||
// Both unset
|
||||
return "";
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
FileAddressList::FileAddressList(const std::string& file_name) {
|
||||
std::string config_dir = get_config_dir();
|
||||
|
||||
if(config_dir.empty()) {
|
||||
spdlog::debug("XDG_CONFIG_HOME and HOME not set; address lists will not be persisted");
|
||||
m_can_save = false;
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = config_dir + "/" + file_name;
|
||||
m_can_save = true;
|
||||
}
|
||||
|
||||
void FileAddressList::load() {
|
||||
if(m_path.empty()) {
|
||||
return; // No config path available
|
||||
}
|
||||
|
||||
std::ifstream file(m_path);
|
||||
if(!file.is_open()) {
|
||||
// File doesn't exist or can't be read; this is not an error
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries.clear();
|
||||
std::string line;
|
||||
while(std::getline(file, line)) {
|
||||
// Trim whitespace from the line
|
||||
size_t start = line.find_first_not_of(" \t\r\n");
|
||||
size_t end = line.find_last_not_of(" \t\r\n");
|
||||
|
||||
if(start != std::string::npos) {
|
||||
line = line.substr(start, end - start + 1);
|
||||
if(!line.empty()) {
|
||||
m_entries.push_back(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FileAddressList::save() const {
|
||||
if(!m_can_save || m_path.empty()) {
|
||||
return; // Cannot save without config path
|
||||
}
|
||||
|
||||
// Create the directory if needed
|
||||
std::filesystem::path config_path(m_path);
|
||||
std::filesystem::path config_dir = config_path.parent_path();
|
||||
|
||||
try {
|
||||
std::filesystem::create_directories(config_dir);
|
||||
} catch(const std::filesystem::filesystem_error&) {
|
||||
// If we can't create the directory, silently fail to save
|
||||
return;
|
||||
}
|
||||
|
||||
// Write entries to file
|
||||
std::ofstream file(m_path);
|
||||
if(!file.is_open()) {
|
||||
return; // Can't open file for writing; silently fail
|
||||
}
|
||||
|
||||
for(const auto& entry : m_entries) {
|
||||
file << entry << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void FileAddressList::remove(const std::string& entry) {
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
m_entries.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::string>& FileAddressList::entries() const {
|
||||
return m_entries;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "ServerAddressProvider.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Shared behaviour of address lists backed by a file.
|
||||
*
|
||||
* One "ip:port" per line. Resolves file path to
|
||||
* $XDG_CONFIG_HOME/towards/<file_name>, falling back to
|
||||
* $HOME/.config/towards/<file_name>.
|
||||
* add() stays pure virtual: subclasses define their own order.
|
||||
*/
|
||||
class FileAddressList : public ServerAddressProvider {
|
||||
protected:
|
||||
std::vector<std::string> m_entries;
|
||||
std::string m_path;
|
||||
bool m_can_save = false;
|
||||
|
||||
explicit FileAddressList(const std::string& file_name);
|
||||
|
||||
public:
|
||||
void load() override;
|
||||
void save() const override;
|
||||
void remove(const std::string& entry) override;
|
||||
const std::vector<std::string>& entries() const override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindowRegistry.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Bundle of runtime-owned subsystems that the game state needs.
|
||||
*/
|
||||
struct GameContext {
|
||||
tw::World* world;
|
||||
tw::JoltPhysicsWorld* physics_world;
|
||||
tw::drw::WorldRenderer* renderer;
|
||||
tw::io::InputManager* input_manager;
|
||||
tw::dbg::NetworkMetrics* metrics;
|
||||
tw::dbg::DebugWindowRegistry* debug_windows;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "GameState.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
GameState::GameState(GameContext context, std::unique_ptr<tw::net::ServerConnection> connection)
|
||||
: m_context(context),
|
||||
m_connection(std::move(connection)),
|
||||
m_entity_gui(context.world),
|
||||
m_network_gui(*context.metrics)
|
||||
{
|
||||
m_controller = std::make_unique<tw::ClientWorldController>(
|
||||
m_context.input_manager,
|
||||
m_context.world,
|
||||
m_context.physics_world,
|
||||
m_context.renderer,
|
||||
m_connection.get(),
|
||||
m_context.metrics
|
||||
);
|
||||
|
||||
m_context.debug_windows->add(&m_entity_gui);
|
||||
m_context.debug_windows->add(&m_network_gui);
|
||||
}
|
||||
|
||||
GameState::~GameState() {
|
||||
m_context.debug_windows->remove(&m_entity_gui);
|
||||
m_context.debug_windows->remove(&m_network_gui);
|
||||
}
|
||||
|
||||
void GameState::update(double delta_time) {
|
||||
m_controller->update(delta_time);
|
||||
m_context.world->step(delta_time);
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "GameContext.hpp"
|
||||
#include "debug/tools/EntityManagerGui.hpp"
|
||||
#include "debug/tools/NetworkStatsGui.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
#include "world/controllers/ClientWorldController.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* The game world state.
|
||||
* Responsible for updating the game simulation and rendering debug information.
|
||||
*/
|
||||
class GameState {
|
||||
GameContext m_context;
|
||||
std::unique_ptr<tw::net::ServerConnection> m_connection;
|
||||
std::unique_ptr<tw::ClientWorldController> m_controller;
|
||||
tw::dbg::tools::EntityManagerGui m_entity_gui;
|
||||
tw::dbg::tools::NetworkStatsGui m_network_gui;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs the game state with the given context and connection.
|
||||
* The connection must be established before creating the game state.
|
||||
*/
|
||||
GameState(GameContext context, std::unique_ptr<tw::net::ServerConnection> connection);
|
||||
|
||||
/**
|
||||
* Takes the debug panels back out of the menu.
|
||||
*/
|
||||
~GameState();
|
||||
|
||||
/**
|
||||
* Updates the game state: draws debug GUIs, updates the controller,
|
||||
* and steps the world physics.
|
||||
*/
|
||||
void update(double delta_time);
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "LobbyState.hpp"
|
||||
|
||||
#include "ClientArgs.hpp"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
namespace {
|
||||
|
||||
const ImVec4 FAVOURITES_COLOUR{1.0f, 0.8f, 0.2f, 1.0f};
|
||||
const ImVec4 RECENT_COLOUR{0.7f, 0.7f, 0.7f, 1.0f};
|
||||
|
||||
}
|
||||
|
||||
LobbyState::LobbyState(std::optional<tw::net::Address> auto_connect) {
|
||||
m_recent.load();
|
||||
m_favourites.load();
|
||||
|
||||
set_address_input(m_recent.entries().empty()
|
||||
? "127.0.0.1:8080"
|
||||
: m_recent.entries().front());
|
||||
|
||||
if(auto_connect) {
|
||||
begin_connect(*auto_connect);
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::set_address_input(const std::string& address) {
|
||||
std::snprintf(m_address_input, sizeof(m_address_input), "%s", address.c_str());
|
||||
}
|
||||
|
||||
void LobbyState::begin_connect(tw::net::Address address) {
|
||||
m_error.clear();
|
||||
m_connection = std::make_unique<tw::net::ServerConnection>(address);
|
||||
|
||||
auto started = m_connection->start();
|
||||
if(!started) {
|
||||
m_error = started.error().message();
|
||||
m_connection.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::draw_form() {
|
||||
const float input_width = 200.0f;
|
||||
|
||||
ImGui::SetNextItemWidth(input_width);
|
||||
bool submitted = ImGui::InputText("##address", m_address_input, sizeof(m_address_input),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||
|
||||
ImGui::SameLine();
|
||||
submitted |= ImGui::Button("Connect");
|
||||
|
||||
if(!submitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto parsed = parse_address(m_address_input);
|
||||
if(parsed) {
|
||||
begin_connect(*parsed);
|
||||
} else {
|
||||
m_error = parsed.error();
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::draw_favourite_toggle(const std::string& entry) {
|
||||
const char* label = m_favourites.contains(entry) ? "[*]" : "[ ]";
|
||||
|
||||
// The label alone would collide between the rows drawn in one frame, so the
|
||||
// entry it acts on is what identifies the button.
|
||||
std::string button_id = std::string(label) + "##fav_" + entry;
|
||||
|
||||
if(ImGui::SmallButton(button_id.c_str())) {
|
||||
toggle_favourite(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::toggle_favourite(const std::string& entry) {
|
||||
if(m_favourites.contains(entry)) {
|
||||
m_favourites.remove(entry);
|
||||
} else {
|
||||
m_favourites.add(entry);
|
||||
}
|
||||
m_favourites.save();
|
||||
}
|
||||
|
||||
void LobbyState::draw_provider(ServerAddressProvider& provider, const ImVec4& header_colour) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, header_colour);
|
||||
ImGui::SeparatorText(provider.name());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// BeginChild is one of the two calls whose End must run even when it
|
||||
// returns false, so the result only decides whether rows are submitted.
|
||||
if(ImGui::BeginChild(provider.name(), ImVec2(0, 120), ImGuiChildFlags_Borders)) {
|
||||
for(const auto& entry : provider.entries()) {
|
||||
draw_favourite_toggle(entry);
|
||||
ImGui::SameLine();
|
||||
|
||||
if(ImGui::Selectable(entry.c_str(), false)) {
|
||||
set_address_input(entry);
|
||||
}
|
||||
|
||||
if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
auto parsed = parse_address(entry);
|
||||
if(parsed) {
|
||||
begin_connect(*parsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void LobbyState::draw_status() {
|
||||
if(m_connection) {
|
||||
switch(m_connection->status()) {
|
||||
case tw::net::ConnectionStatus::Idle:
|
||||
break;
|
||||
case tw::net::ConnectionStatus::Connecting: {
|
||||
ImGui::TextUnformatted("Connecting...");
|
||||
ImGui::SameLine();
|
||||
draw_favourite_toggle(m_connection->address().to_string());
|
||||
ImGui::SameLine();
|
||||
if(ImGui::Button("Cancel")) {
|
||||
m_connection.reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case tw::net::ConnectionStatus::Connected:
|
||||
ImGui::TextColored(ImVec4(0, 1, 0, 1), "Connected!");
|
||||
m_recent.add(m_connection->address().to_string());
|
||||
m_recent.save();
|
||||
m_result = LobbyResult{std::move(m_connection)};
|
||||
break;
|
||||
case tw::net::ConnectionStatus::Failed: {
|
||||
std::string failed_msg = "Connection failed: " + m_connection->error();
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "%s", failed_msg.c_str());
|
||||
if(ImGui::Button("Dismiss")) {
|
||||
m_connection.reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(!m_error.empty()) {
|
||||
std::string error_msg = "Error: " + m_error;
|
||||
ImGui::TextColored(ImVec4(1, 0, 0, 1), "%s", error_msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void LobbyState::update(double delta_time) {
|
||||
if(m_connection) {
|
||||
m_connection->update();
|
||||
}
|
||||
|
||||
// Center the window
|
||||
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_FirstUseEver,
|
||||
ImVec2(0.5f, 0.5f));
|
||||
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGui::Begin("Lobby", nullptr, ImGuiWindowFlags_NoMove);
|
||||
|
||||
draw_form();
|
||||
draw_provider(m_favourites, FAVOURITES_COLOUR);
|
||||
draw_provider(m_recent, RECENT_COLOUR);
|
||||
ImGui::Separator();
|
||||
draw_status();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
std::optional<LobbyResult> LobbyState::take_result() {
|
||||
// Moving out of an optional leaves it engaged, which would hand the caller
|
||||
// a second, empty result on the next frame.
|
||||
auto result = std::move(m_result);
|
||||
m_result.reset();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
#include "RecentServers.hpp"
|
||||
#include "FavouriteServers.hpp"
|
||||
#include "ServerAddressProvider.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
|
||||
struct ImVec4;
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* The edge out of the lobby: a connection that finished its handshake.
|
||||
*/
|
||||
struct LobbyResult {
|
||||
std::unique_ptr<tw::net::ServerConnection> connection;
|
||||
};
|
||||
|
||||
/**
|
||||
* The lobby screen state. Renders an address input field, recent server list,
|
||||
* and manages a connection attempt in flight.
|
||||
*/
|
||||
class LobbyState {
|
||||
static constexpr size_t ADDRESS_INPUT_SIZE = 64;
|
||||
|
||||
tw::app::RecentServers m_recent;
|
||||
tw::app::FavouriteServers m_favourites;
|
||||
|
||||
/**
|
||||
* Edited in place by the input field, so it has to outlive the frame that
|
||||
* draws it rather than being rebuilt from a string every time.
|
||||
*/
|
||||
char m_address_input[ADDRESS_INPUT_SIZE];
|
||||
|
||||
std::string m_error;
|
||||
std::unique_ptr<tw::net::ServerConnection> m_connection;
|
||||
std::optional<LobbyResult> m_result;
|
||||
|
||||
void begin_connect(tw::net::Address address);
|
||||
void set_address_input(const std::string& address);
|
||||
|
||||
void draw_form();
|
||||
|
||||
/**
|
||||
* Renders one address list. The colour is what tells the lists apart on
|
||||
* screen, so it belongs to the lobby rather than to the list itself.
|
||||
*/
|
||||
void draw_provider(ServerAddressProvider& provider, const ImVec4& header_colour);
|
||||
|
||||
void draw_favourite_toggle(const std::string& entry);
|
||||
void toggle_favourite(const std::string& entry);
|
||||
void draw_status();
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs the lobby state, optionally starting an auto-connect if a
|
||||
* server address is provided.
|
||||
*/
|
||||
explicit LobbyState(std::optional<tw::net::Address> auto_connect);
|
||||
|
||||
/**
|
||||
* Updates the lobby: draws the UI, pumps the connection attempt if one
|
||||
* is in flight.
|
||||
*/
|
||||
void update(double delta_time);
|
||||
|
||||
/**
|
||||
* Returns the transition result if the lobby is done. Moves the result
|
||||
* out, leaving none behind.
|
||||
*/
|
||||
std::optional<LobbyResult> take_result();
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "RecentServers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
RecentServers::RecentServers()
|
||||
: FileAddressList("recent_servers.txt") {
|
||||
}
|
||||
|
||||
const char* RecentServers::name() const {
|
||||
return "Recent";
|
||||
}
|
||||
|
||||
void RecentServers::add(const std::string& entry) {
|
||||
// Remove if already in list (de-duplicate)
|
||||
auto it = std::find(m_entries.begin(), m_entries.end(), entry);
|
||||
if(it != m_entries.end()) {
|
||||
m_entries.erase(it);
|
||||
}
|
||||
|
||||
// Add to front (most recent first)
|
||||
m_entries.insert(m_entries.begin(), entry);
|
||||
|
||||
// Cap at MAX_ENTRIES
|
||||
if(m_entries.size() > MAX_ENTRIES) {
|
||||
m_entries.resize(MAX_ENTRIES);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "FileAddressList.hpp"
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Manages a list of recently used server addresses.
|
||||
*
|
||||
* Persists addresses to a text file, one "ip:port" per line.
|
||||
* Most recent first, capped at 8 entries. File path is
|
||||
* $XDG_CONFIG_HOME/towards/recent_servers.txt, falling back to
|
||||
* $HOME/.config/towards/recent_servers.txt. If both variables are
|
||||
* unset, keeps the list in memory only.
|
||||
*/
|
||||
class RecentServers : public FileAddressList {
|
||||
static constexpr size_t MAX_ENTRIES = 8;
|
||||
|
||||
public:
|
||||
RecentServers();
|
||||
|
||||
const char* name() const override;
|
||||
|
||||
/**
|
||||
* Add an address to the recents list.
|
||||
* Most recent first, de-duplicated, capped at 8.
|
||||
* Does not persist to disk; call save() after connecting.
|
||||
*/
|
||||
void add(const std::string& entry) override;
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace tw::app {
|
||||
|
||||
/**
|
||||
* Abstract interface for server address lists.
|
||||
*
|
||||
* Implementations manage a list of "ip:port" entries, load/save them,
|
||||
* and provide a human-readable name for the UI.
|
||||
*/
|
||||
class ServerAddressProvider {
|
||||
public:
|
||||
virtual ~ServerAddressProvider() = default;
|
||||
|
||||
/** Human-readable list name, shown as the section header in the lobby. */
|
||||
virtual const char* name() const = 0;
|
||||
|
||||
virtual const std::vector<std::string>& entries() const = 0;
|
||||
virtual void load() = 0;
|
||||
virtual void save() const = 0;
|
||||
virtual void add(const std::string& entry) = 0;
|
||||
virtual void remove(const std::string& entry) = 0;
|
||||
|
||||
/**
|
||||
* Check whether an entry exists in the list.
|
||||
* Implemented over entries() — subclasses need not override.
|
||||
*/
|
||||
bool contains(const std::string& entry) const {
|
||||
const auto& vec = entries();
|
||||
for(const auto& e : vec) {
|
||||
if(e == entry) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::app
|
||||
@@ -10,34 +10,5 @@ class tw::dbg::ComponentGui<tw::CharacterController> {
|
||||
public:
|
||||
void draw(tw::CharacterController* instance) {
|
||||
ImGui::SeparatorText("Character Controller Component");
|
||||
|
||||
if(ImGui::BeginTable("history", 3)) {
|
||||
for(auto key : instance->input_history().buffer()) {
|
||||
// ImGui::TableNextRow();
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%u", key.first);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// if(instance->input_history().get(key.first).has_value()) {
|
||||
// glm::vec3 input_vec = *instance->input_history().get(key.first).value();
|
||||
// std::string input = std::format("{} {} {}", input_vec.x, input_vec.y, input_vec.z);
|
||||
// ImGui::Text("%s", input.c_str());
|
||||
// } else {
|
||||
// ImGui::Text("None");
|
||||
// }
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// if(instance->position_history().get(key).has_value()) {
|
||||
// glm::vec3 position_vec = *instance->position_history().get(key).value();
|
||||
// std::string position = std::format("{} {} {}", position_vec.x, position_vec.y, position_vec.z);
|
||||
// ImGui::Text("%s", position.c_str());
|
||||
// } else {
|
||||
// ImGui::Text("None");
|
||||
// }
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "DebugUI.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
void DebugUI::draw_dockspace() {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
ImGui::SetNextWindowPos(viewport->WorkPos);
|
||||
ImGui::SetNextWindowSize(viewport->WorkSize);
|
||||
ImGui::SetNextWindowViewport(viewport->ID);
|
||||
|
||||
const ImGuiWindowFlags flags =
|
||||
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus |
|
||||
ImGuiWindowFlags_NoBackground;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
|
||||
ImGui::Begin("##debug_dockspace_host", nullptr, flags);
|
||||
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
// A pass through centre leaves the middle empty until something is docked
|
||||
// there, which is where the world is drawn.
|
||||
ImGui::DockSpace(ImGui::GetID("debug_dockspace"), ImVec2(0.0f, 0.0f),
|
||||
ImGuiDockNodeFlags_PassthruCentralNode);
|
||||
|
||||
if(ImGui::BeginMenuBar()) {
|
||||
m_windows.draw_menu();
|
||||
ImGui::EndMenuBar();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void DebugUI::draw_windows() {
|
||||
m_windows.draw_windows();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindowRegistry.hpp"
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* The full screen host the debug panels live in: a menu bar to toggle them and
|
||||
* a dock space to arrange them in. Draws no background of its own, so the world
|
||||
* stays visible underneath.
|
||||
*/
|
||||
class DebugUI {
|
||||
DebugWindowRegistry m_windows;
|
||||
|
||||
public:
|
||||
DebugWindowRegistry& windows() {
|
||||
return m_windows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the host for this frame. Has to run before anything that should be
|
||||
* dockable is drawn, since the dock space has to exist by then.
|
||||
*/
|
||||
void draw_dockspace();
|
||||
|
||||
void draw_windows();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "DebugWindow.hpp"
|
||||
#include <imgui.h>
|
||||
#include <utility>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
DebugWindow::DebugWindow(std::string id, std::string title, std::string category)
|
||||
: m_id(std::move(id)),
|
||||
m_title(std::move(title)),
|
||||
m_category(std::move(category)),
|
||||
m_label(m_title + "##" + m_id)
|
||||
{
|
||||
}
|
||||
|
||||
void DebugWindow::draw() {
|
||||
if(!m_open) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(ImGui::Begin(m_label.c_str(), &m_open)) {
|
||||
draw_contents();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* A debug panel that can be toggled from the menu bar.
|
||||
*
|
||||
* The frame around a panel is drawn here so every one of them gets the same
|
||||
* close button and docking behaviour; subclasses only fill in the contents.
|
||||
*/
|
||||
class DebugWindow {
|
||||
std::string m_id;
|
||||
std::string m_title;
|
||||
std::string m_category;
|
||||
|
||||
/**
|
||||
* The label handed to the ui, "title##id". Saved positions are keyed by the
|
||||
* whole label, so the visible half can change without losing the layout.
|
||||
*/
|
||||
std::string m_label;
|
||||
|
||||
bool m_open = false;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Fills the panel. Called only while it is open, between begin and end.
|
||||
*/
|
||||
virtual void draw_contents() = 0;
|
||||
|
||||
public:
|
||||
DebugWindow(std::string id, std::string title, std::string category);
|
||||
virtual ~DebugWindow() = default;
|
||||
|
||||
DebugWindow(const DebugWindow&) = delete;
|
||||
DebugWindow& operator=(const DebugWindow&) = delete;
|
||||
|
||||
const std::string& id() const { return m_id; }
|
||||
const std::string& title() const { return m_title; }
|
||||
const std::string& category() const { return m_category; }
|
||||
|
||||
bool is_open() const { return m_open; }
|
||||
void set_open(bool open) { m_open = open; }
|
||||
|
||||
/**
|
||||
* The flag the menu item toggles, and the one the close button clears.
|
||||
*/
|
||||
bool* open_flag() { return &m_open; }
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "DebugWindowRegistry.hpp"
|
||||
#include "DebugWindow.hpp"
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
void DebugWindowRegistry::add(DebugWindow* window) {
|
||||
auto remembered = m_open_state.find(window->id());
|
||||
if(remembered != m_open_state.end()) {
|
||||
window->set_open(remembered->second);
|
||||
}
|
||||
|
||||
m_windows.push_back(window);
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::remove(DebugWindow* window) {
|
||||
m_open_state[window->id()] = window->is_open();
|
||||
std::erase(m_windows, window);
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::draw_menu() {
|
||||
if(!ImGui::BeginMenu("Windows")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ordered by the first panel that asked for the category, so the menu does
|
||||
// not reshuffle as panels come and go.
|
||||
std::vector<std::string> categories;
|
||||
for(auto* window : m_windows) {
|
||||
if(std::find(categories.begin(), categories.end(), window->category()) == categories.end()) {
|
||||
categories.push_back(window->category());
|
||||
}
|
||||
}
|
||||
|
||||
for(const auto& category : categories) {
|
||||
if(!ImGui::BeginMenu(category.c_str())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(auto* window : m_windows) {
|
||||
if(window->category() == category) {
|
||||
ImGui::MenuItem(window->title().c_str(), nullptr, window->open_flag());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
void DebugWindowRegistry::draw_windows() {
|
||||
for(auto* window : m_windows) {
|
||||
window->draw();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
class DebugWindow;
|
||||
|
||||
/**
|
||||
* The debug panels that exist right now.
|
||||
*
|
||||
* Panels are listed by whatever owns them, for as long as it lives, so the menu
|
||||
* follows what the client is currently doing. Whether a panel was open is kept
|
||||
* here rather than on the panel, since the owner is built again on every
|
||||
* reconnect and the panel would come back closed.
|
||||
*/
|
||||
class DebugWindowRegistry {
|
||||
std::vector<DebugWindow*> m_windows;
|
||||
|
||||
/**
|
||||
* Keyed by panel id, remembered across the panels themselves.
|
||||
*/
|
||||
std::unordered_map<std::string, bool> m_open_state;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Lists a panel, restoring whether it was open last time one with the same
|
||||
* id was listed. Ownership stays with the caller, which has to remove it
|
||||
* again before the panel dies.
|
||||
*/
|
||||
void add(DebugWindow* window);
|
||||
|
||||
void remove(DebugWindow* window);
|
||||
|
||||
/**
|
||||
* One submenu per category, listing every panel in it. Expects to be called
|
||||
* inside a menu bar.
|
||||
*/
|
||||
void draw_menu();
|
||||
|
||||
void draw_windows();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,92 +1,238 @@
|
||||
#pragma once
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <format>
|
||||
#include <ratio>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "debug/ComponentGui.hpp"
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
#include "network/EntityInterpolation.hpp"
|
||||
#include "world/Transform.hpp"
|
||||
|
||||
|
||||
template<>
|
||||
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
|
||||
class tw::dbg::ComponentGui<tw::Transform> {
|
||||
private:
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
/** Ten seconds of frames at sixty a second. */
|
||||
static constexpr size_t CAPACITY = 600;
|
||||
|
||||
/** How much of the trace the plot shows. */
|
||||
static constexpr float WINDOW_IN_SECONDS = 10.0f;
|
||||
|
||||
inline static const Transform* m_subject = nullptr;
|
||||
inline static int m_last_frame = -1;
|
||||
inline static Clock::time_point m_started_at {};
|
||||
inline static size_t m_head = 0;
|
||||
inline static size_t m_count = 0;
|
||||
inline static std::vector<float> m_times;
|
||||
inline static std::vector<float> m_positions;
|
||||
|
||||
void restart(const Transform* instance) {
|
||||
m_subject = instance;
|
||||
m_last_frame = -1;
|
||||
m_started_at = Clock::now();
|
||||
m_head = 0;
|
||||
m_count = 0;
|
||||
|
||||
m_times.resize(CAPACITY);
|
||||
m_positions.resize(CAPACITY);
|
||||
}
|
||||
|
||||
/** Keeps one sample per frame, so drawing twice does not double up. */
|
||||
void sample(float time, float position) {
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
if(m_last_frame == frame) {
|
||||
return;
|
||||
}
|
||||
m_last_frame = frame;
|
||||
|
||||
m_times[m_head] = time;
|
||||
m_positions[m_head] = position;
|
||||
|
||||
m_head = (m_head + 1) % CAPACITY;
|
||||
m_count = std::min(m_count + 1, CAPACITY);
|
||||
}
|
||||
|
||||
public:
|
||||
void draw(net::EntityPositionInterpolation* instance) {
|
||||
ImGui::SeparatorText("Entity Interpolation");
|
||||
void draw(Transform* instance) {
|
||||
ImGui::SeparatorText("Drawn Position");
|
||||
|
||||
bool m_is_scrolling = true;
|
||||
static float m_metric_history = 10.0f;
|
||||
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
|
||||
if(m_is_scrolling) {
|
||||
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
|
||||
static int axis = 0;
|
||||
ImGui::Combo("Axis", &axis, "X\0Y\0Z\0");
|
||||
|
||||
if(m_subject != instance) {
|
||||
restart(instance);
|
||||
}
|
||||
|
||||
static int32_t time_buffer_len = 1000;
|
||||
ImGui::SliderInt("Buffer length (ms)", &time_buffer_len, 0, 1000);
|
||||
const float time = std::chrono::duration<float>(Clock::now() - m_started_at).count();
|
||||
const float position = instance->position()[axis];
|
||||
|
||||
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
|
||||
auto [from, to, mix] = instance->get_values_around(now);
|
||||
auto mixed_value = glm::mix(from, to, mix);
|
||||
sample(time, position);
|
||||
|
||||
ImGui::Text("Mixed value: (%f, %f)", mixed_value.x, mixed_value.y);
|
||||
ImGui::Text("%.3f", position);
|
||||
|
||||
if(ImPlot::BeginPlot("Interpolation")) {
|
||||
auto plot_x_from = instance->times().begin()->time_since_epoch().count();
|
||||
std::vector<float> times(instance->times().max_size());
|
||||
for(auto it = instance->times().begin(); it != instance->times().end(); ++it) {
|
||||
times.push_back(it->time_since_epoch().count() - plot_x_from);
|
||||
}
|
||||
if(ImPlot::BeginPlot("Position", ImVec2(-1.0f, 180.0f))) {
|
||||
ImPlot::SetupAxes("seconds", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1,
|
||||
time - WINDOW_IN_SECONDS, time, ImGuiCond_Always);
|
||||
|
||||
float interpolated_time = now.time_since_epoch().count() - plot_x_from;
|
||||
std::vector<float> values(instance->values().max_size());
|
||||
for(auto it = instance->values().begin(); it != instance->values().end(); ++it) {
|
||||
values.push_back(it->x);
|
||||
}
|
||||
values.push_back(mixed_value.x);
|
||||
// The trace wraps around, so it is read from wherever the oldest
|
||||
// sample ended up.
|
||||
const int offset = m_count == CAPACITY ? (int)m_head : 0;
|
||||
ImPlot::PlotLine("Drawn", m_times.data(), m_positions.data(),
|
||||
(int)m_count, ImPlotLineFlags_None, offset);
|
||||
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Says whether the position above stands still because nothing new arrives for
|
||||
* the entity, or because what arrives is the same position over and over.
|
||||
*/
|
||||
template<>
|
||||
class tw::dbg::ComponentGui<tw::net::EntityPositionInterpolation> {
|
||||
private:
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
/** How far back the plot reaches. */
|
||||
static constexpr float WINDOW_IN_SECONDS = 10.0f;
|
||||
|
||||
/** Ten seconds of frames at sixty a second. */
|
||||
static constexpr size_t CAPACITY = 600;
|
||||
|
||||
inline static const net::EntityPositionInterpolation* m_subject = nullptr;
|
||||
|
||||
inline static int m_last_frame = -1;
|
||||
inline static size_t m_head = 0;
|
||||
inline static size_t m_count = 0;
|
||||
|
||||
inline static std::vector<Clock::time_point> m_read_times;
|
||||
inline static std::vector<glm::vec3> m_read_values;
|
||||
|
||||
static int64_t millis_since(Clock::time_point point) {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now() - point).count();
|
||||
}
|
||||
|
||||
void restart(const net::EntityPositionInterpolation* instance) {
|
||||
m_subject = instance;
|
||||
m_last_frame = -1;
|
||||
m_head = 0;
|
||||
m_count = 0;
|
||||
|
||||
m_read_times.resize(CAPACITY);
|
||||
m_read_values.resize(CAPACITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps one reading per frame, so drawing twice does not double up. The
|
||||
* whole value is kept rather than the axis on show, so switching axis
|
||||
* still shows the trace that was already gathered.
|
||||
*/
|
||||
void sample(Clock::time_point time, glm::vec3 value) {
|
||||
const int frame = ImGui::GetFrameCount();
|
||||
if(m_last_frame == frame) {
|
||||
return;
|
||||
}
|
||||
m_last_frame = frame;
|
||||
|
||||
m_read_times[m_head] = time;
|
||||
m_read_values[m_head] = value;
|
||||
|
||||
m_head = (m_head + 1) % CAPACITY;
|
||||
m_count = std::min(m_count + 1, CAPACITY);
|
||||
}
|
||||
|
||||
public:
|
||||
void draw(net::EntityPositionInterpolation* instance) {
|
||||
ImGui::SeparatorText("Received Positions");
|
||||
|
||||
const size_t newest = instance->values().size() - 1;
|
||||
|
||||
ImGui::Text("Newest %.3f %.3f %.3f, %ld ms ago",
|
||||
instance->values()[newest].x,
|
||||
instance->values()[newest].y,
|
||||
instance->values()[newest].z,
|
||||
millis_since(instance->times()[newest]));
|
||||
|
||||
ImGui::Text("Oldest %.3f %.3f %.3f, %ld ms ago",
|
||||
instance->values()[0].x,
|
||||
instance->values()[0].y,
|
||||
instance->values()[0].z,
|
||||
millis_since(instance->times()[0]));
|
||||
|
||||
static int axis = 0;
|
||||
ImGui::Combo("Axis##received", &axis, "X\0Y\0Z\0");
|
||||
|
||||
/** How far behind the present the reader looks; matches the caller. */
|
||||
static int delay_in_millis = 1000;
|
||||
ImGui::SliderInt("Delay (ms)", &delay_in_millis, 0, 2000);
|
||||
|
||||
if(m_subject != instance) {
|
||||
restart(instance);
|
||||
}
|
||||
|
||||
// The buffer wraps, so the index says nothing about when a sample
|
||||
// arrived. Placing each one at its own age puts it where it belongs
|
||||
// without having to know where the ring currently starts. Slots
|
||||
// nothing was pushed into are left out.
|
||||
const auto now = Clock::now();
|
||||
const size_t count = instance->size();
|
||||
|
||||
std::vector<float> ages(count);
|
||||
std::vector<float> positions(count);
|
||||
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
ages[i] = -std::chrono::duration<float>(now - instance->times()[i]).count();
|
||||
positions[i] = instance->values()[i][axis];
|
||||
}
|
||||
|
||||
if(ImPlot::BeginPlot("Buffer", ImVec2(-1.0f, 180.0f))) {
|
||||
ImPlot::SetupAxes("seconds ago", "position", ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1,
|
||||
-WINDOW_IN_SECONDS, 0.0f, ImGuiCond_Always);
|
||||
|
||||
ImPlot::PlotScatter("Received", ages.data(), positions.data(), (int)count);
|
||||
|
||||
// Reading the same way the position is read for drawing shows
|
||||
// which two samples the delay lands between, and how far the
|
||||
// result sits from either of them.
|
||||
const auto point = now - std::chrono::milliseconds(delay_in_millis);
|
||||
const auto [from, to, alpha] = instance->get_values_around(point);
|
||||
|
||||
const float read_at = -std::chrono::duration<float>(now - point).count();
|
||||
const float read_out = glm::mix(from, to, alpha)[axis];
|
||||
|
||||
ImPlot::SetNextMarkerStyle(ImPlotMarker_Circle, 5.0f);
|
||||
ImPlot::PlotScatter("Interpolated", &read_at, &read_out, 1);
|
||||
|
||||
// Each reading is kept at the moment it was read for, so the trace
|
||||
// ends on the marker above and runs back along the same timeline
|
||||
// the received samples sit on.
|
||||
sample(point, glm::mix(from, to, alpha));
|
||||
|
||||
std::vector<float> trace_ages(m_count);
|
||||
std::vector<float> trace_positions(m_count);
|
||||
|
||||
const size_t oldest = m_count == CAPACITY ? m_head : 0;
|
||||
|
||||
for(size_t i = 0; i < m_count; i++) {
|
||||
const size_t index = (oldest + i) % CAPACITY;
|
||||
|
||||
trace_ages[i] = -std::chrono::duration<float>(now - m_read_times[index]).count();
|
||||
trace_positions[i] = m_read_values[index][axis];
|
||||
}
|
||||
|
||||
ImPlot::PlotLine("Interpolated Trace",
|
||||
trace_ages.data(), trace_positions.data(), (int)m_count);
|
||||
|
||||
ImPlot::PlotScatter("Server", times.data(), values.data(), times.size() - 1);
|
||||
ImPlot::PushStyleVar(ImPlotStyleVar_FillAlpha, 0.25f);
|
||||
ImPlot::SetNextMarkerStyle(ImPlotMarker_Square, 6, ImPlot::GetColormapColor(1), IMPLOT_AUTO, ImPlot::GetColormapColor(1));
|
||||
ImPlot::PlotScatter("Client", &interpolated_time, &mixed_value.x, 1);
|
||||
ImPlot::PopStyleVar();
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
if(ImGui::BeginTable("entityInterpolation", 2)) {
|
||||
for(int i = 0; i < instance->values().size(); i++) {
|
||||
ImGui::TableNextRow();
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
auto value = instance->values()[i];
|
||||
auto now = std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(time_buffer_len);
|
||||
if(instance->times()[i] > now && (i == instance->values().size() - 1 || instance->times()[i + 1] <= now)) {
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.7f, 0.3f, 0.3f, 0.65f)));
|
||||
ImGui::Text("%f %f %f", mixed_value.x, mixed_value.y, mixed_value.z);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, 0.0f)));
|
||||
}
|
||||
|
||||
ImGui::Text("%f %f %f", value.x, value.y, value.z);
|
||||
|
||||
auto point = instance->times()[i].time_since_epoch();
|
||||
auto hours = std::chrono::duration_cast<std::chrono::hours>(point);
|
||||
point -= hours;
|
||||
hours %= 24;
|
||||
auto minutes = std::chrono::duration_cast<std::chrono::minutes>(point);
|
||||
point -= minutes;
|
||||
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(point);
|
||||
point -= seconds;
|
||||
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(point);
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
auto fmt = std::format("{}:{}:{}.{}", hours, minutes, seconds, milliseconds);
|
||||
ImGui::Text(fmt.c_str());
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* Per-second history of what the client sends, receives and waits for.
|
||||
*
|
||||
* Traffic arrives as running totals, one reading per tick: sample() keeps the
|
||||
* change since the previous reading, so a bucket sums to the traffic of that
|
||||
* second and its extremes are the quietest and busiest tick within it.
|
||||
* Durations are recorded as they are measured.
|
||||
*/
|
||||
class NetworkMetrics {
|
||||
public:
|
||||
using Interval = std::chrono::seconds;
|
||||
using Series = metrics::MetricSeries<Interval>;
|
||||
|
||||
/** How many seconds of history are kept. */
|
||||
static constexpr size_t DEFAULT_HISTORY = 300;
|
||||
|
||||
/** Running totals as of one tick. */
|
||||
struct Totals {
|
||||
uint64_t bytes_sent = 0;
|
||||
uint64_t bytes_received = 0;
|
||||
uint64_t messages_sent = 0;
|
||||
uint64_t messages_received = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
Series m_bytes_out;
|
||||
Series m_bytes_in;
|
||||
Series m_messages_out;
|
||||
Series m_messages_in;
|
||||
Series m_response_ms;
|
||||
Series m_update_ms;
|
||||
|
||||
Series m_rollbacks;
|
||||
Series m_correction_distance;
|
||||
Series m_ack_lag_frames;
|
||||
Series m_replayed_frames;
|
||||
|
||||
Totals m_previous;
|
||||
bool m_has_previous = false;
|
||||
|
||||
static uint64_t delta(uint64_t current, uint64_t previous) {
|
||||
return current > previous ? current - previous : 0;
|
||||
}
|
||||
|
||||
static double to_millis(std::chrono::nanoseconds elapsed) {
|
||||
return std::chrono::duration<double, std::milli>(elapsed).count();
|
||||
}
|
||||
|
||||
public:
|
||||
explicit NetworkMetrics(size_t history_in_seconds = DEFAULT_HISTORY) :
|
||||
m_bytes_out(history_in_seconds),
|
||||
m_bytes_in(history_in_seconds),
|
||||
m_messages_out(history_in_seconds),
|
||||
m_messages_in(history_in_seconds),
|
||||
m_response_ms(history_in_seconds),
|
||||
m_update_ms(history_in_seconds),
|
||||
m_rollbacks(history_in_seconds),
|
||||
m_correction_distance(history_in_seconds),
|
||||
m_ack_lag_frames(history_in_seconds),
|
||||
m_replayed_frames(history_in_seconds)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Records how much `totals` grew since the previous call. The first call
|
||||
* only remembers where the counters started.
|
||||
*/
|
||||
void sample(const Totals& totals) {
|
||||
if(m_has_previous) {
|
||||
m_bytes_out.push((double)delta(totals.bytes_sent, m_previous.bytes_sent));
|
||||
m_bytes_in.push((double)delta(totals.bytes_received, m_previous.bytes_received));
|
||||
m_messages_out.push((double)delta(totals.messages_sent, m_previous.messages_sent));
|
||||
m_messages_in.push((double)delta(totals.messages_received, m_previous.messages_received));
|
||||
}
|
||||
|
||||
m_previous = totals;
|
||||
m_has_previous = true;
|
||||
}
|
||||
|
||||
/** Time between sending an input and seeing the answer to it. */
|
||||
void record_response_time(std::chrono::nanoseconds elapsed) {
|
||||
m_response_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
/** Time one tick spent moving messages in and out, handlers included. */
|
||||
void record_update_time(std::chrono::nanoseconds elapsed) {
|
||||
m_update_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
const Series& bytes_out() const {
|
||||
return m_bytes_out;
|
||||
}
|
||||
|
||||
const Series& bytes_in() const {
|
||||
return m_bytes_in;
|
||||
}
|
||||
|
||||
const Series& messages_out() const {
|
||||
return m_messages_out;
|
||||
}
|
||||
|
||||
const Series& messages_in() const {
|
||||
return m_messages_in;
|
||||
}
|
||||
|
||||
const Series& response_ms() const {
|
||||
return m_response_ms;
|
||||
}
|
||||
|
||||
const Series& update_ms() const {
|
||||
return m_update_ms;
|
||||
}
|
||||
|
||||
/** Records a rollback event (one sample per rollback). */
|
||||
void record_rollback() {
|
||||
m_rollbacks.push(1.0);
|
||||
}
|
||||
|
||||
/** Records the distance in meters of a position correction. */
|
||||
void record_correction_distance(double meters) {
|
||||
m_correction_distance.push(meters);
|
||||
}
|
||||
|
||||
/** Records how many frames behind the ack is trailing the current frame. */
|
||||
void record_ack_lag(uint32_t frames) {
|
||||
m_ack_lag_frames.push((double)frames);
|
||||
}
|
||||
|
||||
/** Records how many frames were replayed during a rollback. */
|
||||
void record_replayed_frames(uint32_t frames) {
|
||||
m_replayed_frames.push((double)frames);
|
||||
}
|
||||
|
||||
const Series& rollbacks() const {
|
||||
return m_rollbacks;
|
||||
}
|
||||
|
||||
const Series& correction_distance() const {
|
||||
return m_correction_distance;
|
||||
}
|
||||
|
||||
const Series& ack_lag_frames() const {
|
||||
return m_ack_lag_frames;
|
||||
}
|
||||
|
||||
const Series& replayed_frames() const {
|
||||
return m_replayed_frames;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
EntityManagerGui::EntityManagerGui(World* world) :
|
||||
DebugWindow("entity_manager", "Entities", "World"),
|
||||
m_world(world) {
|
||||
|
||||
}
|
||||
@@ -49,7 +50,8 @@ void EntityManagerGui::draw_entity_components() {
|
||||
ImGui::Text("Vertex offset: %i", mesh->vertex_offset());
|
||||
}
|
||||
|
||||
draw_debug_components<tw::CharacterBody, tw::CharacterController, tw::net::EntityPositionInterpolation>();
|
||||
draw_debug_components<tw::Transform, tw::net::EntityPositionInterpolation,
|
||||
tw::CharacterBody, tw::CharacterController>();
|
||||
|
||||
// auto character = m_world->registry().try_get<tw::CharacterBody>(m_selected);
|
||||
// if(character != nullptr) {
|
||||
@@ -64,9 +66,7 @@ void EntityManagerGui::draw_entity_components() {
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void EntityManagerGui::draw() {
|
||||
ImGui::Begin("Transforms");
|
||||
|
||||
void EntityManagerGui::draw_contents() {
|
||||
ImGui::BeginChild("Entities", ImVec2(0, 260), ImGuiChildFlags_Border);
|
||||
|
||||
ImGui::SeparatorText("Entities");
|
||||
@@ -87,8 +87,6 @@ void EntityManagerGui::draw() {
|
||||
if(m_selected_entity.has_value()) {
|
||||
draw_entity_components();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/ComponentGui.hpp"
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class EntityManagerGui {
|
||||
class EntityManagerGui : public tw::dbg::DebugWindow {
|
||||
private:
|
||||
World* m_world;
|
||||
|
||||
@@ -29,14 +30,15 @@ private:
|
||||
...);
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
entt::entity& selected() {
|
||||
return m_selected;
|
||||
}
|
||||
|
||||
EntityManagerGui(World* world);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -2,95 +2,109 @@
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
template<typename T, typename Interval, typename Op = net::SumOp<T>>
|
||||
/**
|
||||
* Plots one series against the seconds behind now, ending at the last second
|
||||
* that has fully elapsed.
|
||||
*
|
||||
* The line follows whichever statistic the series is read with. Reading an
|
||||
* average also shades the quietest and busiest value of each second behind it;
|
||||
* a total has no such range to show, since its buckets already hold every value
|
||||
* of that second added together.
|
||||
*/
|
||||
class MetricWidget {
|
||||
public:
|
||||
using Series = metrics::MetricSeries<std::chrono::seconds>;
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::string m_unit;
|
||||
const Series* m_series;
|
||||
metrics::MetricField m_field;
|
||||
|
||||
net::BucketMetric<T, Interval, Op>& m_metric;
|
||||
/**
|
||||
* The second in progress is left out: it only holds the part of itself
|
||||
* that has elapsed, so drawing it makes the newest point drop and climb
|
||||
* back once a second.
|
||||
*/
|
||||
static constexpr size_t SKIP_IN_PROGRESS = 1;
|
||||
|
||||
using Self = MetricWidget<T, Interval, Op>;
|
||||
std::vector<double> m_ages;
|
||||
std::vector<double> m_values;
|
||||
std::vector<double> m_lows;
|
||||
std::vector<double> m_highs;
|
||||
|
||||
struct {
|
||||
T constraint_from;
|
||||
T constraint_to;
|
||||
bool has_range() const {
|
||||
return m_field == metrics::MetricField::Avg;
|
||||
}
|
||||
|
||||
T from;
|
||||
T to;
|
||||
} y_axis;
|
||||
/** Describes what is drawn, so the numbers always match the line. */
|
||||
void draw_summary() const {
|
||||
if(m_values.empty()) {
|
||||
ImGui::TextUnformatted("no samples yet");
|
||||
return;
|
||||
}
|
||||
|
||||
bool m_is_scrolling = true;
|
||||
auto [low, high] = std::minmax_element(m_values.begin(), m_values.end());
|
||||
|
||||
double total = 0.0;
|
||||
for(double value : m_values) {
|
||||
total += value;
|
||||
}
|
||||
|
||||
ImGui::Text("min %.1f %s avg %.1f %s max %.1f %s",
|
||||
*low, m_unit.c_str(),
|
||||
total / (double)m_values.size(), m_unit.c_str(),
|
||||
*high, m_unit.c_str());
|
||||
}
|
||||
|
||||
public:
|
||||
MetricWidget(
|
||||
const std::string& name,
|
||||
net::BucketMetric<T, Interval, Op>& metric
|
||||
) :
|
||||
m_name(name),
|
||||
m_metric(metric)
|
||||
{
|
||||
y_axis = {
|
||||
.constraint_from = 0,
|
||||
.constraint_to = 500,
|
||||
.from = 0,
|
||||
.to = 250
|
||||
};
|
||||
}
|
||||
MetricWidget(std::string name, std::string unit, const Series& series, metrics::MetricField field) :
|
||||
m_name(std::move(name)),
|
||||
m_unit(std::move(unit)),
|
||||
m_series(&series),
|
||||
m_field(field)
|
||||
{ }
|
||||
|
||||
Self& set_x_axis_limits_contraints(T from, T to) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from, to);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& set_y_axis_limits(double from, double to) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& enable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
Self& disable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
|
||||
void draw() {
|
||||
/** Draws the last `history_in_seconds` seconds of the series. */
|
||||
void draw(size_t history_in_seconds) {
|
||||
ImGui::PushID(m_name.c_str());
|
||||
|
||||
auto head = m_metric.get_head();
|
||||
auto head_timeline = m_metric.get_head_timeline();
|
||||
m_series->linearize(m_ages, m_values, m_field, history_in_seconds, SKIP_IN_PROGRESS);
|
||||
|
||||
auto tail = m_metric.get_tail();
|
||||
auto tail_timeline = m_metric.get_tail_timeline();
|
||||
|
||||
static float m_metric_history = 10.0f;
|
||||
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
|
||||
if(m_is_scrolling) {
|
||||
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
|
||||
if(has_range()) {
|
||||
m_series->linearize(m_ages, m_lows, metrics::MetricField::Min,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
m_series->linearize(m_ages, m_highs, metrics::MetricField::Max,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
}
|
||||
|
||||
ImGui::Text("Min: %i", m_metric.min());
|
||||
ImGui::Text("Max: %i", m_metric.max());
|
||||
draw_summary();
|
||||
|
||||
if(ImPlot::BeginPlot(m_name.c_str())) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
if(ImPlot::BeginPlot(m_name.c_str(), ImVec2(-1.0f, 150.0f))) {
|
||||
ImPlot::SetupAxes("seconds ago", m_unit.c_str(),
|
||||
ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, -(double)history_in_seconds, 0.0, ImGuiCond_Always);
|
||||
|
||||
ImPlot::SetupAxes("Time", m_name.c_str(), ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
if(m_is_scrolling) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - m_metric_history, from, ImGuiCond_Always);
|
||||
const int count = (int)m_values.size();
|
||||
|
||||
if(has_range() && count > 0) {
|
||||
ImPlot::PlotShaded("range", m_ages.data(), m_lows.data(), m_highs.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, m_metric.max() * 2, ImGuiCond_Always);
|
||||
// ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
if(count > 0) {
|
||||
ImPlot::PlotLine(m_name.c_str(), m_ages.data(), m_values.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::PlotLine(m_name.c_str(), head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine(m_name.c_str(), tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/MetricWidget.hpp"
|
||||
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class NetworkStatsGui {
|
||||
private:
|
||||
// MetricWidget<uint32_t, std::chrono::seconds, net::AverageOp<uint32_t>> m_ping_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_outgoing_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_incoming_widget;
|
||||
/**
|
||||
* Panel over everything the client measured about its traffic.
|
||||
*
|
||||
* Traffic is shown as the total of each second, since that is the rate the
|
||||
* connection actually carried. Durations are shown as the average of each
|
||||
* second, with the range behind them.
|
||||
*/
|
||||
class NetworkStatsGui : public tw::dbg::DebugWindow {
|
||||
MetricWidget m_response;
|
||||
MetricWidget m_update;
|
||||
MetricWidget m_bytes_in;
|
||||
MetricWidget m_bytes_out;
|
||||
MetricWidget m_messages_in;
|
||||
MetricWidget m_messages_out;
|
||||
|
||||
MetricWidget m_rollbacks;
|
||||
MetricWidget m_correction_distance;
|
||||
MetricWidget m_ack_lag_frames;
|
||||
MetricWidget m_replayed_frames;
|
||||
|
||||
int m_history_in_seconds = 30;
|
||||
|
||||
protected:
|
||||
void draw_contents() override {
|
||||
ImGui::SliderInt("History", &m_history_in_seconds, 5, 300, "%d s");
|
||||
|
||||
const size_t history = (size_t)m_history_in_seconds;
|
||||
|
||||
m_response.draw(history);
|
||||
m_update.draw(history);
|
||||
m_bytes_in.draw(history);
|
||||
m_bytes_out.draw(history);
|
||||
m_messages_in.draw(history);
|
||||
m_messages_out.draw(history);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Prediction");
|
||||
m_rollbacks.draw(history);
|
||||
m_correction_distance.draw(history);
|
||||
m_ack_lag_frames.draw(history);
|
||||
m_replayed_frames.draw(history);
|
||||
}
|
||||
|
||||
public:
|
||||
// NetworkStatsGui() :
|
||||
// m_ping_widget("Ping", net::NetworkStatsLogger::instance()->ping()),
|
||||
// m_outgoing_widget("Outgoing", net::NetworkStatsLogger::instance()->outgoing()),
|
||||
// m_incoming_widget("Incoming", net::NetworkStatsLogger::instance()->incoming())
|
||||
// {
|
||||
// }
|
||||
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// auto& ping = instance->ping();
|
||||
|
||||
ImGui::Begin("Network Stats");
|
||||
|
||||
/* auto head = ping.get_head();
|
||||
auto head_timeline = ping.get_head_timeline();
|
||||
|
||||
auto tail = ping.get_tail();
|
||||
auto tail_timeline = ping.get_tail_timeline();
|
||||
|
||||
static float ping_history = 10.0f;
|
||||
ImGui::SliderFloat("Ping History", &ping_history,1,30,"%.1f s");
|
||||
|
||||
if(ImPlot::BeginPlot("Ping")) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
|
||||
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - ping_history, from, ImGuiCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 120);
|
||||
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
|
||||
ImPlot::PlotLine("Ping", head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine("Ping", tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
} */
|
||||
|
||||
// m_ping_widget.draw();
|
||||
// m_outgoing_widget.draw();
|
||||
// m_incoming_widget.draw();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
explicit NetworkStatsGui(const NetworkMetrics& metrics) :
|
||||
DebugWindow("network_stats", "Network Stats", "Network"),
|
||||
m_response("Response", "ms", metrics.response_ms(), metrics::MetricField::Avg),
|
||||
m_update("Network update", "ms", metrics.update_ms(), metrics::MetricField::Avg),
|
||||
m_bytes_in("Bytes in", "B/s", metrics.bytes_in(), metrics::MetricField::Sum),
|
||||
m_bytes_out("Bytes out", "B/s", metrics.bytes_out(), metrics::MetricField::Sum),
|
||||
m_messages_in("Messages in", "1/s", metrics.messages_in(), metrics::MetricField::Sum),
|
||||
m_messages_out("Messages out", "1/s", metrics.messages_out(), metrics::MetricField::Sum),
|
||||
m_rollbacks("Rollbacks", "1/s", metrics.rollbacks(), metrics::MetricField::Sum),
|
||||
m_correction_distance("Correction distance", "m", metrics.correction_distance(), metrics::MetricField::Avg),
|
||||
m_ack_lag_frames("Ack lag", "frames", metrics.ack_lag_frames(), metrics::MetricField::Avg),
|
||||
m_replayed_frames("Replayed frames", "frames", metrics.replayed_frames(), metrics::MetricField::Avg)
|
||||
{ }
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PacketBacklogGui {
|
||||
private:
|
||||
std::vector<uint32_t> m_buckets;
|
||||
uint32_t m_last_backlog_idx;
|
||||
|
||||
public:
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// size_t size = instance->get_size();
|
||||
|
||||
// if(ImGui::BeginTable("Network Packets", 5)) {
|
||||
// ImGui::TableSetupColumn("Message Type");
|
||||
// ImGui::TableSetupColumn("Time");
|
||||
// ImGui::TableSetupColumn("Is From Us");
|
||||
// ImGui::TableSetupColumn("Target");
|
||||
// ImGui::TableSetupColumn("Size");
|
||||
|
||||
// for(int32_t i = size-1; i >= 0; i--) {
|
||||
// auto& item = instance->get_item(i);
|
||||
// ImGui::PushID(item.timepoint.time_since_epoch().count());
|
||||
|
||||
// ImGui::TableNextRow();
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%i", item.message_type);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(std::format("{}", item.timepoint.time_since_epoch()).c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Checkbox("is_sent_from_us", &item.is_sent_by_us);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(item.target.to_string().c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%ld", item.buffer.size());
|
||||
|
||||
// ImGui::PopID();
|
||||
// }
|
||||
// ImGui::EndTable();
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "ByteBuffer.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
PerformanceStatsGui::PerformanceStatsGui(LockStep& lock_step) :
|
||||
DebugWindow("performance_stats", "Performance", "General"),
|
||||
m_lockstep(lock_step),
|
||||
fps_history(1000),
|
||||
frame_idxs(1000)
|
||||
{
|
||||
}
|
||||
|
||||
void PerformanceStatsGui::draw() {
|
||||
ImGui::Begin("Stats");
|
||||
void PerformanceStatsGui::draw_contents() {
|
||||
|
||||
ImGui::Text("FPS: %ld", m_lockstep.fps());
|
||||
fps_history[fps_history_idx] = m_lockstep.fps();
|
||||
@@ -34,8 +34,6 @@ void PerformanceStatsGui::draw() {
|
||||
ImPlot::PlotLine("FPS", frame_idxs.data(), fps_history.data(), is_plot_filled ? (int)fps_history.size() : (int)fps_history_idx - 1);
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PerformanceStatsGui {
|
||||
class PerformanceStatsGui : public tw::dbg::DebugWindow {
|
||||
private:
|
||||
LockStep& m_lockstep;
|
||||
|
||||
@@ -15,10 +16,11 @@ private:
|
||||
uint32_t frame_idx = 0;
|
||||
bool is_plot_filled = false;
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
PerformanceStatsGui(LockStep& lock_step);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ Instance create_instance(const std::string& name, const lft::win::Window* window
|
||||
// required_extensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
|
||||
|
||||
std::vector<std::string> required_layers = {
|
||||
#if DEBUG
|
||||
"VK_LAYER_KHRONOS_validation"
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL_keycode.h>
|
||||
#include <SDL_mouse.h>
|
||||
#include <imgui_impl_sdl2.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL3/SDL_keycode.h>
|
||||
#include <imgui_impl_sdl3.h>
|
||||
|
||||
#include <Window.hpp>
|
||||
|
||||
@@ -69,30 +69,30 @@ public:
|
||||
m_motion_y = 0.0f;
|
||||
|
||||
while(m_window->poll_event(&event)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
|
||||
switch(event.type) {
|
||||
case SDL_QUIT:
|
||||
case SDL_EVENT_QUIT:
|
||||
m_is_quit = true;
|
||||
break;
|
||||
case SDL_KEYDOWN:
|
||||
case SDL_KEYUP: {
|
||||
switch(event.key.keysym.sym) {
|
||||
case SDLK_w:
|
||||
m_is_pressed[0] = event.key.state;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP: {
|
||||
switch(event.key.key) {
|
||||
case SDLK_W:
|
||||
m_is_pressed[0] = event.key.down;
|
||||
break;
|
||||
case SDLK_s:
|
||||
m_is_pressed[1] = event.key.state;
|
||||
case SDLK_S:
|
||||
m_is_pressed[1] = event.key.down;
|
||||
break;
|
||||
case SDLK_a:
|
||||
m_is_pressed[2] = event.key.state;
|
||||
case SDLK_A:
|
||||
m_is_pressed[2] = event.key.down;
|
||||
break;
|
||||
case SDLK_d:
|
||||
m_is_pressed[3] = event.key.state;
|
||||
case SDLK_D:
|
||||
m_is_pressed[3] = event.key.down;
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
case SDL_MOUSEMOTION:
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
m_motion_x = event.motion.xrel;
|
||||
m_motion_y = event.motion.yrel;
|
||||
break;
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
/**
|
||||
* Writes entity derivation message
|
||||
*/
|
||||
template<typename TMesg>
|
||||
class EntityDerivationWriter {
|
||||
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "entt/entt.hpp"
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
class EntityIdMap {
|
||||
public:
|
||||
const bool is_server_entity_registered(uint32_t server_id) const {
|
||||
return m_mappings.find(server_id) != m_mappings.end();
|
||||
}
|
||||
|
||||
const std::optional<entt::entity> get_local(uint32_t server_id) const {
|
||||
if(is_server_entity_registered(server_id)) {
|
||||
return m_mappings.find(server_id)->second;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void register_server_to_local(uint32_t server_id, entt::entity local) {
|
||||
m_mappings[server_id] = local;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<uint32_t, entt::entity> m_mappings;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -9,47 +9,44 @@
|
||||
namespace tw::net {
|
||||
|
||||
EntityPositionInterpolator::EntityPositionInterpolator(
|
||||
entt::registry* registry, entt::entity player_entity, size_t bufferingIntervalInMillis
|
||||
entt::registry* registry, size_t bufferingIntervalInMillis
|
||||
) : m_registry(registry),
|
||||
m_player_entity(player_entity),
|
||||
m_bufferingIntervalInMillis(bufferingIntervalInMillis)
|
||||
{
|
||||
|
||||
}
|
||||
{ }
|
||||
|
||||
void EntityPositionInterpolator::register_entity(entt::entity entity) {
|
||||
m_registry->emplace<EntityPositionInterpolation>(entity, glm::vec3());
|
||||
}
|
||||
|
||||
void EntityPositionInterpolator::add_position_for_entity(entt::entity entity, glm::vec3 position) {
|
||||
void EntityPositionInterpolator::set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position) {
|
||||
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
|
||||
if(interpolation == nullptr) {
|
||||
spdlog::warn("Attempt to add position for non-registered entity {}", (uint32_t)entity);
|
||||
return;
|
||||
}
|
||||
|
||||
interpolation->push(Clock::now(), position);
|
||||
interpolation->push(time_point, position);
|
||||
}
|
||||
|
||||
glm::vec3 EntityPositionInterpolator::get_position_for_entity(entt::entity entity) {
|
||||
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
|
||||
auto* interpolation = m_registry->try_get<EntityPositionInterpolation>(entity);
|
||||
if(interpolation == nullptr) {
|
||||
spdlog::warn("Attempt to get position for non-registered entity {}", (uint32_t)entity);
|
||||
return glm::vec3();
|
||||
}
|
||||
|
||||
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
|
||||
|
||||
auto [from, to, value] = interpolation->get_values_around(now);
|
||||
auto [from, to, value] = interpolation->get_values_around(time_point);
|
||||
return glm::mix(from, to, value);
|
||||
}
|
||||
|
||||
void EntityPositionInterpolator::update() {
|
||||
void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::time_point time_point) {
|
||||
m_registry->view<EntityPositionInterpolation, Transform>()
|
||||
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
|
||||
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
|
||||
|
||||
auto [from, to, value] = interpolation.get_values_around(now);
|
||||
auto [from, to, value] = interpolation.get_values_around(time_point);
|
||||
spdlog::info("From: {} {} {} - To: {} {} {} - By: {}",
|
||||
from.x, from.y, from.z,
|
||||
to.x, to.y, to.z,
|
||||
value);
|
||||
ts.set_position(glm::mix(from, to, value));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <entt/entt.hpp>
|
||||
@@ -12,23 +13,20 @@ class EntityPositionInterpolator {
|
||||
|
||||
entt::registry* m_registry;
|
||||
|
||||
entt::entity m_player_entity;
|
||||
|
||||
size_t m_bufferingIntervalInMillis;
|
||||
|
||||
glm::vec3 get_position(Clock::time_point time_point, entt::entity entity);
|
||||
|
||||
public:
|
||||
EntityPositionInterpolator(
|
||||
entt::registry* registry,
|
||||
entt::entity player_entity,
|
||||
size_t bufferingIntervalInMillis);
|
||||
|
||||
void register_entity(entt::entity entity);
|
||||
|
||||
void add_position_for_entity(entt::entity entity, glm::vec3 position);
|
||||
void set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position);
|
||||
|
||||
glm::vec3 get_position_for_entity(entt::entity entity);
|
||||
|
||||
void update();
|
||||
void interpolate_smoothed_entities(Clock::time_point time_point);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -5,15 +5,21 @@
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
template<typename T, int length = 3>
|
||||
template<typename T, int length = 10>
|
||||
class InterpolatedProperty {
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
std::array<T, length> m_buffer;
|
||||
std::array<Clock::time_point, length> m_time_buffer;
|
||||
std::vector<T> m_buffer;
|
||||
std::vector<Clock::time_point> m_time_buffer;
|
||||
|
||||
uint32_t m_head;
|
||||
uint32_t m_size;
|
||||
|
||||
public:
|
||||
InterpolatedProperty(T initial_value) {
|
||||
InterpolatedProperty(T initial_value) :
|
||||
m_buffer(length),
|
||||
m_time_buffer(length),
|
||||
m_size(0), m_head(0) {
|
||||
for(int i = 0; i < length; i++) {
|
||||
m_buffer[i] = initial_value;
|
||||
m_time_buffer[i] = Clock::now();
|
||||
@@ -23,7 +29,21 @@ public:
|
||||
GET_REF(m_buffer, values);
|
||||
GET_REF(m_time_buffer, times);
|
||||
|
||||
/** How many slots hold something that was pushed. */
|
||||
inline uint32_t size() const {
|
||||
return m_size;
|
||||
}
|
||||
|
||||
void push(Clock::time_point point, T value) {
|
||||
// m_buffer[m_head] = std::move(value);
|
||||
// m_time_buffer[m_head] = point;
|
||||
//
|
||||
// m_head = (m_head + 1) % m_buffer.size();
|
||||
//
|
||||
// if (m_size < m_buffer.size())
|
||||
// {
|
||||
// ++m_size;
|
||||
// }
|
||||
if(point < m_time_buffer.at(0)) {
|
||||
return;
|
||||
}
|
||||
@@ -64,6 +84,46 @@ public:
|
||||
}
|
||||
|
||||
inline std::tuple<T, T, float> get_values_around(Clock::time_point point) const {
|
||||
/* const std::size_t capacity = m_buffer.size();
|
||||
const std::size_t oldest = (m_head + capacity - m_size) % capacity;
|
||||
|
||||
if (m_size == 0)
|
||||
{
|
||||
return { T(), T(), 0.0f };
|
||||
}
|
||||
|
||||
// Asking for a time the buffer no longer reaches back to. Standing on
|
||||
// the oldest is wrong by however much was missed, but it is wrong in
|
||||
// the direction the values were heading, unlike the newest.
|
||||
if (point < m_time_buffer[oldest])
|
||||
{
|
||||
return { m_buffer[oldest], m_buffer[oldest], 0.0f };
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i + 1 < m_size; ++i)
|
||||
{
|
||||
const std::size_t idx0 = (oldest + i) % capacity;
|
||||
const std::size_t idx1 = (oldest + i + 1) % capacity;
|
||||
|
||||
const auto t0 = m_time_buffer[idx0];
|
||||
const auto t1 = m_time_buffer[idx1];
|
||||
|
||||
if (point >= t0 && point <= t1)
|
||||
{
|
||||
const auto total =
|
||||
std::chrono::duration<float>(t1 - t0).count();
|
||||
const auto elapsed =
|
||||
std::chrono::duration<float>(point - t0).count();
|
||||
|
||||
const float alpha = total > 0.0f ? elapsed / total : 0.0f;
|
||||
|
||||
return { m_buffer[idx0], m_buffer[idx1], alpha };
|
||||
}
|
||||
}
|
||||
|
||||
// Handle exact last sample (or clamp)
|
||||
const std::size_t last = (oldest + m_size - 1) % capacity;
|
||||
return { m_buffer[last], m_buffer[last], 0.0f }; */
|
||||
T prev = m_buffer.at(0);
|
||||
Clock::time_point prev_point = m_time_buffer.at(0);
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
#include "PlayerRollback.hpp"
|
||||
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/Transform.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtx/norm.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics)
|
||||
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
|
||||
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
|
||||
{
|
||||
for (auto& record : m_records) {
|
||||
record.frame = 0;
|
||||
record.valid = false;
|
||||
record.input = glm::vec3(0.0f);
|
||||
record.predicted_position = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::set_input(uint32_t frame, glm::vec3 input) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
m_records[idx].frame = frame;
|
||||
m_records[idx].valid = true;
|
||||
m_records[idx].input = input;
|
||||
}
|
||||
|
||||
void PlayerRollback::record_prediction(uint32_t frame, glm::vec3 position) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
if (m_records[idx].frame == frame && m_records[idx].valid) {
|
||||
m_records[idx].predicted_position = position;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerRollback::apply_correction(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry,
|
||||
uint32_t current_frame)
|
||||
{
|
||||
if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_last_reconciled_ack = ack_frame;
|
||||
m_last_ack_frame = ack_frame;
|
||||
|
||||
Record& record = m_records[ack_frame % RING_SIZE];
|
||||
const bool has_prediction = record.valid && record.frame == ack_frame;
|
||||
|
||||
// With a prediction to compare against, an answer that already matches costs
|
||||
// nothing further. This is the case almost every frame.
|
||||
if (has_prediction) {
|
||||
float distance = glm::distance(record.predicted_position, authoritative_position);
|
||||
m_last_correction_distance = distance;
|
||||
|
||||
if (distance < kPositionEpsilon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restoring the frame the answer describes keeps everything the simulation
|
||||
// derived from it, so only the character has to be moved. Without a stored
|
||||
// frame there is nothing to restore and the answer is taken as it stands.
|
||||
const bool restored = has_prediction && m_physics->rollback(ack_frame);
|
||||
|
||||
place_character(player, registry, authoritative_position, !restored);
|
||||
replay_from(ack_frame, player, registry, current_frame);
|
||||
|
||||
m_last_replayed_frames = current_frame - 1 - ack_frame;
|
||||
m_rollback_count++;
|
||||
|
||||
spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}",
|
||||
ack_frame, m_last_correction_distance, m_last_replayed_frames, restored);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerRollback::place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity) {
|
||||
CharacterBody* body = registry->try_get<CharacterBody>(player);
|
||||
Transform* transform = registry->try_get<Transform>(player);
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
transform->set_position(position);
|
||||
|
||||
if (clear_velocity) {
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame) {
|
||||
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
|
||||
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
Transform* transform = registry->try_get<Transform>(player);
|
||||
if (!transform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Frames the ring never saw still need an entry, or the answer to them
|
||||
// arrives with nothing to compare against and forces another correction.
|
||||
Record& replayed = m_records[f % RING_SIZE];
|
||||
replayed.frame = f;
|
||||
replayed.valid = true;
|
||||
replayed.predicted_position = transform->position();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::reset_at(uint32_t frame) {
|
||||
m_last_reconciled_ack = frame;
|
||||
|
||||
for (auto& record : m_records) {
|
||||
record.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <glm/glm.hpp>
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace tw {
|
||||
class JoltPhysicsWorld;
|
||||
}
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
class PlayerRollback {
|
||||
private:
|
||||
struct Record {
|
||||
uint32_t frame;
|
||||
bool valid;
|
||||
glm::vec3 input;
|
||||
glm::vec3 predicted_position;
|
||||
};
|
||||
|
||||
static constexpr size_t RING_SIZE = 64;
|
||||
static constexpr float kPositionEpsilon = 0.05f;
|
||||
|
||||
tw::JoltPhysicsWorld* m_physics;
|
||||
std::array<Record, RING_SIZE> m_records;
|
||||
|
||||
uint32_t m_last_reconciled_ack = 0;
|
||||
uint64_t m_rollback_count = 0;
|
||||
float m_last_correction_distance = 0.0f;
|
||||
uint32_t m_last_replayed_frames = 0;
|
||||
uint32_t m_last_ack_frame = 0;
|
||||
|
||||
public:
|
||||
PlayerRollback(tw::JoltPhysicsWorld* physics);
|
||||
|
||||
void set_input(uint32_t frame, glm::vec3 input);
|
||||
void record_prediction(uint32_t frame, glm::vec3 position);
|
||||
|
||||
bool apply_correction(
|
||||
uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Re-simulates `from_frame + 1` up to the newest frame, refreshing the stored
|
||||
* prediction for each. The inputs come from the character itself, so this
|
||||
* works even for frames this ring never recorded.
|
||||
*/
|
||||
void replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
/** Places the character at `position` without disturbing the stored frames. */
|
||||
void place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity);
|
||||
|
||||
public:
|
||||
|
||||
void reset_at(uint32_t frame);
|
||||
|
||||
uint64_t rollback_count() const { return m_rollback_count; }
|
||||
float last_correction_distance() const { return m_last_correction_distance; }
|
||||
uint32_t last_replayed_frames() const { return m_last_replayed_frames; }
|
||||
uint32_t last_ack_frame() const { return m_last_ack_frame; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "ReplicatorClient.hpp"
|
||||
|
||||
#include "PlayerMove.pb.h"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "network/EntityIdMap.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/Transform.hpp"
|
||||
#include <chrono>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
ReplicatorClient::ReplicatorClient(
|
||||
ProtobufMessages* messages,
|
||||
EntityIdMap* entity_id_map,
|
||||
dbg::NetworkMetrics *network_metrics,
|
||||
JoltPhysicsWorld *physics_world,
|
||||
World *world,
|
||||
net::ServerConnection* server_connection,
|
||||
EntityPositionInterpolator* entity_writer
|
||||
) :
|
||||
m_messages(messages),
|
||||
m_entity_id_map(entity_id_map),
|
||||
m_network_metrics(network_metrics),
|
||||
m_rollback(physics_world),
|
||||
m_world(world),
|
||||
m_server_connection(server_connection),
|
||||
m_input_send_times(INPUT_SEND_TIME_COUNT),
|
||||
m_entity_interpolator(entity_writer)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
int64_t ReplicatorClient::now_ms() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
Clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
void ReplicatorClient::set_player_entity(entt::entity entity) {
|
||||
m_player_entity = entity;
|
||||
}
|
||||
|
||||
void ReplicatorClient::measure_response_time(uint32_t current_frame_idx, uint32_t snapshot_frame_idx) {
|
||||
// Snapshots carry frame zero until the server has an input to answer, and
|
||||
// repeat the same frame whenever no newer one arrived in between.
|
||||
if(snapshot_frame_idx == 0 || snapshot_frame_idx <= m_last_measured_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything the send times no longer cover, including a frame we never sent,
|
||||
// which underflows into a large distance.
|
||||
if(current_frame_idx - snapshot_frame_idx >= INPUT_SEND_TIME_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_measured_frame = snapshot_frame_idx;
|
||||
|
||||
auto sent_at = m_input_send_times[snapshot_frame_idx % INPUT_SEND_TIME_COUNT];
|
||||
m_network_metrics->record_response_time(Clock::now() - sent_at);
|
||||
}
|
||||
|
||||
void ReplicatorClient::set_input(uint32_t frame_idx, glm::vec3 input) {
|
||||
m_rollback.set_input(frame_idx, input);
|
||||
|
||||
mmo::PlayerMoveMessage player_move_message = {};
|
||||
|
||||
player_move_message.set_frame_idx(frame_idx);
|
||||
mmo::PlayerInput* player_input = new mmo::PlayerInput();
|
||||
player_input->set_x(input.x);
|
||||
player_input->set_y(input.y);
|
||||
player_input->set_z(input.z);
|
||||
|
||||
player_move_message.set_allocated_input(player_input);
|
||||
|
||||
auto send_result = m_messages->send(m_server_connection->server(), player_move_message, false);
|
||||
if(!send_result) {
|
||||
spdlog::error("Failed to send message: {}", send_result.error().message());
|
||||
}
|
||||
|
||||
m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
|
||||
}
|
||||
|
||||
void ReplicatorClient::record_prediction(uint32_t frame_idx) {
|
||||
if(!m_player_entity.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(!transform) {
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 position = transform->position();
|
||||
|
||||
m_rollback.record_prediction(frame_idx, position);
|
||||
}
|
||||
|
||||
void ReplicatorClient::update() {
|
||||
if(!m_player_entity.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// m_entity_interpolator.interpolate_smoothed_entities(std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(100));
|
||||
|
||||
// Transform* transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
// if(!transform) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// transform->set_position(m_render_position);
|
||||
}
|
||||
|
||||
void ReplicatorClient::snap_player_to(uint32_t frame_idx, entt::entity entity, glm::vec3 position) {
|
||||
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
|
||||
if(body) {
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
if(transform) {
|
||||
transform->set_position(position);
|
||||
}
|
||||
|
||||
m_render_position = position;
|
||||
|
||||
// m_rollback.reset_at(frame_idx);
|
||||
}
|
||||
|
||||
void ReplicatorClient::handle_snapshot_entity(
|
||||
uint32_t current_frame_idx,
|
||||
uint32_t record_frame_idx,
|
||||
serial::EntityRecord& record
|
||||
) {
|
||||
std::optional<entt::entity> entity = m_entity_id_map->get_local(record.id);
|
||||
|
||||
if(!entity.has_value()) {
|
||||
spdlog::warn("Received position update for unknown entity {}", record.id);
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 p = {record.position.x, record.position.y, record.position.z};
|
||||
|
||||
if(m_player_entity.has_value() &&
|
||||
entity.value() == m_player_entity.value()
|
||||
) {
|
||||
if(!m_player_position_initialized) {
|
||||
m_player_position_initialized = true;
|
||||
// snap_player_to(current_frame_idx, entity.value(), p);
|
||||
// return;
|
||||
}
|
||||
|
||||
// Applies correction from record_frame_idx to current_frame_idx (if any) on entity. Compares previous position on
|
||||
// frame record_frame_idx with p
|
||||
m_rollback.apply_correction(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||
|
||||
//snap_player_to(current_frame_idx, entity.value(), p);
|
||||
|
||||
// bool reconcile_happened = m_reconciler.reconcile(record_frame_idx, p, entity.value(), &m_world->registry(), current_frame_idx);
|
||||
//
|
||||
// if(reconcile_happened) {
|
||||
// m_network_metrics->record_rollback();
|
||||
// m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
|
||||
// m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
|
||||
// }
|
||||
|
||||
// if(record_frame_idx != 0) {
|
||||
// uint32_t ack_lag = 0;
|
||||
// if(current_frame_idx >= record_frame_idx) {
|
||||
// ack_lag = current_frame_idx - record_frame_idx;
|
||||
// }
|
||||
// m_network_metrics->record_ack_lag(ack_lag);
|
||||
// }
|
||||
} else {
|
||||
m_entity_interpolator->set_position(std::chrono::high_resolution_clock::now(), entity.value(), p);
|
||||
}
|
||||
}
|
||||
|
||||
void ReplicatorClient::handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader) {
|
||||
auto header = reader.read_header();
|
||||
measure_response_time(current_frame_idx, header.frame_idx);
|
||||
|
||||
// Snapshots are sent unreliably, so one can overtake another. Taking a
|
||||
// late one would put entities back where they have already been seen.
|
||||
if(header.tick_idx <= m_latest_tick_idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_latest_tick_idx = header.tick_idx;
|
||||
|
||||
// Arrival is the only time the client can be sure of, and it grows with
|
||||
// every snapshot, which is what reading the buffer back relies on. What it
|
||||
// costs is spacing: the gaps between samples carry the jitter of the way
|
||||
// here rather than the even gaps the server sent them on.
|
||||
const auto taken_at = std::chrono::high_resolution_clock::now();
|
||||
|
||||
while(reader.has_entity()) {
|
||||
auto entity_r = reader.read_entity();
|
||||
|
||||
handle_snapshot_entity(current_frame_idx, header.frame_idx, entity_r);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "WorldStateWriter.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "network/EntityIdMap.hpp"
|
||||
#include "network/PlayerRollback.hpp"
|
||||
#include "network/ServerConnection.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
|
||||
/**
|
||||
* Handles replication messages from server.
|
||||
*/
|
||||
class ReplicatorClient {
|
||||
public:
|
||||
ReplicatorClient(
|
||||
ProtobufMessages* messages,
|
||||
EntityIdMap* entity_id_map,
|
||||
dbg::NetworkMetrics *network_metrics,
|
||||
JoltPhysicsWorld *physics_world,
|
||||
World *world,
|
||||
net::ServerConnection* server_connection,
|
||||
EntityPositionInterpolator* entity_writer
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets player controlled entity. The behaviour for this entity will be
|
||||
* little different.
|
||||
*/
|
||||
void set_player_entity(entt::entity entity);
|
||||
|
||||
/**
|
||||
* Takes the input driving `frame_idx`, keeping it for the answer to that
|
||||
* frame and putting it on the wire.
|
||||
*/
|
||||
void set_input(uint32_t frame_idx, glm::vec3 input);
|
||||
|
||||
/**
|
||||
* Takes where the player ended up on `frame_idx`, which should represent
|
||||
* the prediction taken.
|
||||
*/
|
||||
void record_prediction(uint32_t frame_idx);
|
||||
|
||||
void handle_snapshot(uint32_t current_frame_idx, serial::WorldStateReader& reader);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Records how long the answer to `frame_idx` took to arrive, ignoring
|
||||
* frames that were already measured or are too old to still have a send
|
||||
* time.
|
||||
*/
|
||||
void measure_response_time(uint32_t current_frame_idx, uint32_t snapshot_frame_idx);
|
||||
|
||||
/**
|
||||
* Places the player at an authoritative position outright, clearing the
|
||||
* predicted state that led there. Used for the first position the server
|
||||
* sends, which the local simulation has no history to reconcile against.
|
||||
*/
|
||||
void snap_player_to(uint32_t frame_idx, entt::entity entity, glm::vec3 position);
|
||||
|
||||
void handle_snapshot_entity(
|
||||
uint32_t current_frame_idx,
|
||||
uint32_t record_frame_idx,
|
||||
serial::EntityRecord& record
|
||||
);
|
||||
|
||||
void update();
|
||||
|
||||
glm::vec3 render_position() const {
|
||||
return m_render_position;
|
||||
}
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
static int64_t now_ms();
|
||||
|
||||
ProtobufMessages *m_messages;
|
||||
EntityIdMap* m_entity_id_map;
|
||||
dbg::NetworkMetrics *m_network_metrics;
|
||||
net::PlayerRollback m_rollback;
|
||||
World *m_world;
|
||||
net::ServerConnection* m_server_connection;
|
||||
uint32_t m_latest_tick_idx = 0;
|
||||
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
|
||||
net::EntityPositionInterpolator* m_entity_interpolator;
|
||||
|
||||
/**
|
||||
* Whether the server has placed the player at least once. Entities are
|
||||
* created before their position arrives, so the body starts at the origin
|
||||
* and has to be moved once the first position shows up.
|
||||
*/
|
||||
bool m_player_position_initialized = false;
|
||||
|
||||
glm::vec3 m_render_position{0.0f};
|
||||
|
||||
/**
|
||||
* When each input was sent, indexed by its frame. Holds the most recent
|
||||
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
|
||||
* unmeasured.
|
||||
*/
|
||||
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
|
||||
|
||||
std::vector<Clock::time_point> m_input_send_times;
|
||||
uint32_t m_last_measured_frame = 0;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "ServerConnection.hpp"
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
ServerConnection::ServerConnection(Address address) :
|
||||
m_address(address),
|
||||
m_status(ConnectionStatus::Idle),
|
||||
m_started_at(Clock::now()) {
|
||||
}
|
||||
|
||||
tl::expected<void, msg::MessageError> ServerConnection::start() {
|
||||
auto endpoint_r = msg::MessageEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = endpoint_r.error().message();
|
||||
return tl::make_unexpected(endpoint_r.error());
|
||||
}
|
||||
|
||||
m_endpoint = std::move(endpoint_r.value());
|
||||
|
||||
auto server_r = m_endpoint->connect(m_address.ip_string(), m_address.port());
|
||||
if(!server_r) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = server_r.error().message();
|
||||
return tl::make_unexpected(server_r.error());
|
||||
}
|
||||
|
||||
m_server = server_r.value();
|
||||
m_status = ConnectionStatus::Connecting;
|
||||
m_started_at = Clock::now();
|
||||
spdlog::info("Attempting to connect to server at {}", m_address.to_string());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void ServerConnection::update() {
|
||||
if(!m_endpoint) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_endpoint->update();
|
||||
|
||||
if(m_status == ConnectionStatus::Connecting && m_server) {
|
||||
if(m_server->is_established()) {
|
||||
m_status = ConnectionStatus::Connected;
|
||||
spdlog::info("Connected to server at {}", m_address.to_string());
|
||||
} else {
|
||||
auto elapsed = Clock::now() - m_started_at;
|
||||
if(elapsed >= CONNECT_TIMEOUT) {
|
||||
m_status = ConnectionStatus::Failed;
|
||||
m_error = "No response from " + m_address.to_string();
|
||||
spdlog::error("Connection timeout to {}", m_address.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include "message_protocol/MessageConnection.hpp"
|
||||
#include "message_protocol/MessageError.hpp"
|
||||
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* Status of a server connection attempt or established connection.
|
||||
*/
|
||||
enum class ConnectionStatus { Idle, Connecting, Connected, Failed };
|
||||
|
||||
/**
|
||||
* Encapsulates a connection to a game server.
|
||||
*/
|
||||
class ServerConnection {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server = nullptr;
|
||||
Address m_address;
|
||||
ConnectionStatus m_status = ConnectionStatus::Idle;
|
||||
std::string m_error;
|
||||
Clock::time_point m_started_at;
|
||||
|
||||
static constexpr std::chrono::seconds CONNECT_TIMEOUT{5};
|
||||
|
||||
public:
|
||||
explicit ServerConnection(Address address);
|
||||
|
||||
tl::expected<void, msg::MessageError> start();
|
||||
|
||||
void update();
|
||||
|
||||
ConnectionStatus status() const { return m_status; }
|
||||
|
||||
const std::string& error() const { return m_error; }
|
||||
|
||||
const Address& address() const { return m_address; }
|
||||
|
||||
msg::MessageEndpoint* endpoint() const { return m_endpoint.get(); }
|
||||
|
||||
msg::MessageConnection* server() const { return m_server; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include "app/ClientArgs.hpp"
|
||||
#include "Address.hpp"
|
||||
#include "SDLWindow.h"
|
||||
#include "debug/tools/NetworkStatsGui.hpp"
|
||||
#include "debug/tools/PacketBacklogGui.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "debug/tools/EntityManagerGui.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "draw/MeshData.hpp"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_impl_vulkan.h"
|
||||
#include "imgui_impl_sdl2.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
|
||||
#include "implot.h"
|
||||
#include "implot_internal.h"
|
||||
#include "world/Transform.hpp"
|
||||
|
||||
#include <SDL_events.h>
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <memory>
|
||||
@@ -32,17 +31,23 @@ std::unique_ptr<lft::win::Window> create_window(const std::string& name, VkExten
|
||||
});
|
||||
}
|
||||
|
||||
int get_port_from_args(int argc, char** argv) {
|
||||
try {
|
||||
if(argc > 1) {
|
||||
return atoi(argv[1]);
|
||||
} else {
|
||||
return 8080;
|
||||
/**
|
||||
* The address to connect to without asking, if one was given on the command
|
||||
* line. A malformed one is reported and dropped, leaving the user in the lobby.
|
||||
*/
|
||||
static std::optional<net::Address> auto_connect_address(int argc, char** argv) {
|
||||
auto argument = app::server_arg(argc, argv);
|
||||
if(!argument) {
|
||||
return {};
|
||||
}
|
||||
} catch(std::exception& e) {
|
||||
std::println("Could not parse port from arguments, using default.");
|
||||
return 8080;
|
||||
|
||||
auto parsed = app::parse_address(*argument);
|
||||
if(!parsed) {
|
||||
spdlog::error("Failed to parse server address: {}", parsed.error());
|
||||
return {};
|
||||
}
|
||||
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
Runtime::Runtime(int argc, char** argv) :
|
||||
@@ -52,52 +57,44 @@ Runtime::Runtime(int argc, char** argv) :
|
||||
m_physics_world(&m_world),
|
||||
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
|
||||
m_input_manager(m_window.get()),
|
||||
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }),
|
||||
m_lockstep(60)
|
||||
m_network_metrics(),
|
||||
m_lockstep(60),
|
||||
m_debug_ui(),
|
||||
m_perf_stats(m_lockstep),
|
||||
m_state(std::in_place_type<app::LobbyState>, auto_connect_address(argc, argv))
|
||||
{
|
||||
m_debug_ui.windows().add(&m_perf_stats);
|
||||
}
|
||||
|
||||
// bool Runtime::world_state_packet_handler(uint32_t* p_frame_idx, WorldSnapshotMessage* mesg) {
|
||||
// uint32_t frame_idx = *p_frame_idx;
|
||||
Runtime::~Runtime() {
|
||||
m_debug_ui.windows().remove(&m_perf_stats);
|
||||
}
|
||||
|
||||
// if(mesg->frame_idx < frame_idx) {
|
||||
// return false;
|
||||
// }
|
||||
app::GameContext Runtime::context() {
|
||||
return app::GameContext{
|
||||
&m_world,
|
||||
&m_physics_world,
|
||||
&m_world_renderer,
|
||||
&m_input_manager,
|
||||
&m_network_metrics,
|
||||
&m_debug_ui.windows(),
|
||||
};
|
||||
}
|
||||
|
||||
void Runtime::update_state(double delta_time) {
|
||||
if(auto* lobby = std::get_if<app::LobbyState>(&m_state)) {
|
||||
lobby->update(delta_time);
|
||||
|
||||
// for(int i = 0; i < mesg->player_states.size(); i++) {
|
||||
// if(!m_players.contains(mesg->player_states[i].id)) {
|
||||
// auto mesh = m_world_renderer.add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
|
||||
// const auto entity = m_world.registry().create();
|
||||
// m_players.insert({mesg->player_states[i].id, entity});
|
||||
if(auto result = lobby->take_result()) {
|
||||
m_state.emplace<app::GameState>(context(), std::move(result->connection));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// m_world.registry().emplace<Transform>(entity, Transform(mesg->player_states[i].position));
|
||||
// m_world.registry().emplace<PlayerInfoComponent>(entity,
|
||||
// PlayerInfoComponent(
|
||||
// mesg->player_states[i].id,
|
||||
// mesg->player_states[i].name));
|
||||
// m_world.registry().emplace<drw::Mesh>(entity, mesh);
|
||||
|
||||
// } else {
|
||||
// auto entity = (entt::entity)mesg->player_states[i].id;
|
||||
// auto entity_ts = m_world.registry()
|
||||
// .try_get<Transform>(entity);
|
||||
|
||||
// if(entity_ts) {
|
||||
// entity_ts->transform = glm::translate(glm::mat4(1.0f), mesg->player_states[i].position);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return true;
|
||||
// }
|
||||
std::get<app::GameState>(m_state).update(delta_time);
|
||||
}
|
||||
|
||||
void Runtime::run() {
|
||||
dbg::tools::EntityManagerGui entity_manager(&m_world);
|
||||
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
|
||||
dbg::tools::PacketBacklogGui packet_backlog;
|
||||
dbg::tools::NetworkStatsGui network_stats;
|
||||
|
||||
ImPlot::CreateContext();
|
||||
m_is_running = true;
|
||||
|
||||
@@ -107,51 +104,21 @@ void Runtime::run() {
|
||||
}
|
||||
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
// ImGui::DockSpaceOverViewport();
|
||||
|
||||
entity_manager.draw();
|
||||
|
||||
ImGui::Begin("History");
|
||||
|
||||
if(ImGui::BeginTable("historyTable", 2)) {
|
||||
// for(auto key : m_world_controller.position_history().keys()) {
|
||||
// ImGui::TableNextRow();
|
||||
//
|
||||
// // auto v = *m_world_controller.player_history().get(key).value();
|
||||
// // auto input = std::format("{} {} {}", v.x, v.y, v.z);
|
||||
// auto p = *m_world_controller.position_history().get(key).value();
|
||||
// auto position = std::format("{} {} {}", p.x, p.y, p.z);
|
||||
//
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%u", key);
|
||||
// // ImGui::TableNextColumn();
|
||||
// // ImGui::Text(input.c_str());
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(position.c_str());
|
||||
// }
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
bool change_imgui = false;
|
||||
|
||||
m_input_manager.update();
|
||||
if(m_input_manager.is_quit()) {
|
||||
m_is_running = false;
|
||||
}
|
||||
m_world_controller.update(m_lockstep.delta_time());
|
||||
|
||||
perf_stats.draw();
|
||||
packet_backlog.draw();
|
||||
network_stats.draw();
|
||||
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
|
||||
// Anything that docks needs the dock space to already be there, so the
|
||||
// host goes up before the state draws.
|
||||
m_debug_ui.draw_dockspace();
|
||||
|
||||
m_world.step(m_lockstep.delta_time());
|
||||
update_state(m_lockstep.delta_time());
|
||||
|
||||
m_debug_ui.draw_windows();
|
||||
|
||||
m_world_renderer.render();
|
||||
FrameMark;
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include "app/GameContext.hpp"
|
||||
#include "app/GameState.hpp"
|
||||
#include "app/LobbyState.hpp"
|
||||
#include "debug/DebugUI.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
#include "world/ClientWorldController.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
@@ -17,33 +24,38 @@ namespace tw {
|
||||
class Runtime {
|
||||
private:
|
||||
io::Files m_files;
|
||||
|
||||
std::unique_ptr<lft::win::Window> m_window;
|
||||
|
||||
tw::World m_world;
|
||||
|
||||
JoltPhysicsWorld m_physics_world;
|
||||
|
||||
tw::drw::WorldRenderer m_world_renderer;
|
||||
|
||||
tw::io::InputManager m_input_manager;
|
||||
|
||||
tw::ClientWorldController m_world_controller;
|
||||
|
||||
tw::dbg::NetworkMetrics m_network_metrics;
|
||||
tw::LockStep m_lockstep;
|
||||
|
||||
bool m_is_running;
|
||||
/**
|
||||
* Declared before the state so panels the state owns can still be taken out
|
||||
* of the menu while the state is being torn down.
|
||||
*/
|
||||
tw::dbg::DebugUI m_debug_ui;
|
||||
|
||||
tw::dbg::tools::PerformanceStatsGui m_perf_stats;
|
||||
std::variant<tw::app::LobbyState, tw::app::GameState> m_state;
|
||||
bool m_is_running;
|
||||
std::unordered_map<uint32_t, entt::entity> m_players;
|
||||
|
||||
void send_player_positions();
|
||||
|
||||
app::GameContext context();
|
||||
|
||||
void update_state(double delta_time);
|
||||
|
||||
public:
|
||||
const bool is_running() const {
|
||||
return m_is_running;
|
||||
}
|
||||
|
||||
Runtime(int argc, char** argv);
|
||||
~Runtime();
|
||||
|
||||
void run();
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@ struct CameraData {
|
||||
CameraData(glm::mat4 projection, Transform view) :
|
||||
projection(projection),
|
||||
view(view)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
};
|
||||
|
||||
class Camera {
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
#include "ClientWorldController.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <chrono>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "Address.hpp"
|
||||
|
||||
#include "Entity.pb.h"
|
||||
#include "Login.pb.h"
|
||||
#include "WorldState.pb.h"
|
||||
#include "PlayerMove.pb.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "messenger/Messenger.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "messages/PlayerMoveMessage.hpp"
|
||||
#include "metrics/HistoryBuffer.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/CharacterController.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/WorldEntity.hpp"
|
||||
#include "tw/serial/WorldStateWriter.hpp"
|
||||
|
||||
namespace tw {
|
||||
|
||||
typedef HistoryBuffer<long, glm::vec3> EntityPositionHistory;
|
||||
|
||||
entt::entity create_player_entity(World* world, JoltPhysicsWorld* physics_world, drw::WorldRenderer* renderer) {
|
||||
// entt::entity entity = world->registry().create();
|
||||
|
||||
// drw::Mesh mesh = renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
|
||||
|
||||
// world->registry()
|
||||
// .emplace<Transform>(entity,
|
||||
// Transform(glm::vec3(0.0f, 10.0f, 0.0f)));
|
||||
|
||||
// world->registry()
|
||||
// .emplace<EntityPositionHistory>(entity, 0, glm::vec3(0.0f, 10.0f, 0.0f), 1000);
|
||||
|
||||
// world->registry()
|
||||
// .emplace<tw::WorldEntity>(entity,
|
||||
// tw::WorldEntity(std::string("player"), (uint32_t)entity));
|
||||
|
||||
// world->registry()
|
||||
// .emplace<tw::drw::Mesh>(entity, mesh);
|
||||
|
||||
// world->registry()
|
||||
// .emplace<tw::CharacterController>(entity, 20.0f);
|
||||
// auto* body = &world->registry()
|
||||
// .emplace<CharacterBody>(entity, physics_world->create_character(
|
||||
// new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
|
||||
// glm::vec3(0.0f, 10.0f, 0.0f)));
|
||||
|
||||
|
||||
// return entity;
|
||||
return entt::entity(0);
|
||||
}
|
||||
|
||||
|
||||
entt::entity
|
||||
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
|
||||
const auto entity = m_world->registry().create();
|
||||
|
||||
if(!m_mesh.has_value()) {
|
||||
m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
|
||||
}
|
||||
spdlog::info("Creating entity {}", name);
|
||||
|
||||
m_world->registry()
|
||||
.emplace<Transform>(entity,
|
||||
Transform(position));
|
||||
|
||||
m_world->registry()
|
||||
.emplace<tw::WorldEntity>(entity,
|
||||
tw::WorldEntity(name, (uint32_t)entity));
|
||||
|
||||
m_world->registry()
|
||||
.emplace<tw::drw::Mesh>(entity,
|
||||
m_mesh.value());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
net::TcpStream create_stream(tw::net::Address address) {
|
||||
auto stream = net::TcpStream::connect(address);
|
||||
if(!stream.has_value()) {
|
||||
spdlog::error("Failed to connect to server");
|
||||
throw std::runtime_error("Failed to connect to server");
|
||||
}
|
||||
|
||||
stream.value().set_non_blocking();
|
||||
|
||||
return std::move(stream.value());
|
||||
}
|
||||
|
||||
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
|
||||
if(m_entity_mapping.contains(id)) {
|
||||
return m_entity_mapping[id];
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void ClientWorldController::map_server_entity(int server_id, entt::entity local_id) {
|
||||
m_entity_mapping[server_id] = local_id;
|
||||
}
|
||||
|
||||
void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
|
||||
for(int i = 0; i < count; i++) {
|
||||
const mmo::EntityPosition* const position = positions[i];
|
||||
std::optional<entt::entity> entity = map_from_server_entity(position->id());
|
||||
|
||||
if(!entity.has_value()) {
|
||||
spdlog::warn("Received position update for unknown entity {}", position->id());
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 p = {position->x(), position->y(), position->z()};
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
|
||||
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
|
||||
|
||||
if(history != nullptr) {
|
||||
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
|
||||
history->set(millis, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClientWorldController::ClientWorldController(
|
||||
const io::InputManager* inputs,
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
) :
|
||||
m_input_manager(inputs),
|
||||
m_world(world),
|
||||
m_physics_world(physics_world),
|
||||
m_world_renderer(world_renderer),
|
||||
m_player_entity(create_player_entity(world, physics_world, world_renderer)),
|
||||
m_player_controller(&world_renderer->camera(), glm::vec3()),
|
||||
m_messenger{address},
|
||||
m_tick_step(20),
|
||||
m_is_connected(false),
|
||||
m_position_history_exporter("/home/martin/output.csv"),
|
||||
m_entity_interpolator(&m_world->registry(), m_player_entity, 300)
|
||||
{
|
||||
m_messenger->set_handler<mmo::LoginResponse>(
|
||||
[&](mmo::LoginResponse* mesg) {
|
||||
if(!m_is_connected) {
|
||||
spdlog::info("Joined the game!");
|
||||
}
|
||||
});
|
||||
|
||||
m_messenger->set_raw_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[&](std::span<std::byte> data) -> tl::expected<void, net::NetworkError> {
|
||||
serial::WorldStateReader reader(data);
|
||||
|
||||
auto header = reader.read_header();
|
||||
|
||||
while(reader.has_spawn()) {
|
||||
auto spawn = reader.read_spawn();
|
||||
auto entity = create_entity("test", glm::vec3());
|
||||
spdlog::info("Spawning entity {}", spawn);
|
||||
|
||||
map_server_entity(spawn, entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
|
||||
while(reader.has_entity()) {
|
||||
auto entity_r = reader.read_entity();
|
||||
|
||||
std::optional<entt::entity> entity = map_from_server_entity(entity_r.id);
|
||||
|
||||
if(!entity.has_value()) {
|
||||
spdlog::warn("Received position update for unknown entity {}", entity_r.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
|
||||
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
|
||||
|
||||
if(history != nullptr) {
|
||||
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
|
||||
history->set(millis, p);
|
||||
}
|
||||
}
|
||||
|
||||
// apply_entity_positions();
|
||||
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
m_messenger->set_handler<mmo::EntitySpawnMessage>(
|
||||
[&](mmo::EntitySpawnMessage* mesg) {
|
||||
auto entity = create_entity(mesg->name(), glm::vec3());
|
||||
|
||||
map_server_entity(mesg->entity_id(), entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
});
|
||||
}
|
||||
|
||||
ClientWorldController::~ClientWorldController() {
|
||||
}
|
||||
|
||||
void ClientWorldController::export_entity_history() {
|
||||
}
|
||||
|
||||
void ClientWorldController::update(double delta_time) {
|
||||
m_player_controller.update(m_input_manager, delta_time);
|
||||
|
||||
ImGui::Begin("Player Controller");
|
||||
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity);
|
||||
ImGui::Text("Player count: %ld", m_entity_mapping.size());
|
||||
for(auto mapping : m_entity_mapping) {
|
||||
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
if(m_tick_step.update()) {
|
||||
m_messenger->update();
|
||||
|
||||
if(!m_is_connected && false) {
|
||||
return;
|
||||
} else {
|
||||
|
||||
// CharacterController& character = m_world->registry().get<CharacterController>(m_player_entity);
|
||||
// character.set_input(m_frame_idx, m_player_controller.input());
|
||||
|
||||
mmo::PlayerMoveMessage player_move_message = {};
|
||||
|
||||
player_move_message.set_frame_idx(m_frame_idx);
|
||||
mmo::PlayerInput* player_input = new mmo::PlayerInput();
|
||||
player_input->set_x(m_player_controller.input().x);
|
||||
player_input->set_y(m_player_controller.input().y);
|
||||
player_input->set_z(m_player_controller.input().z);
|
||||
|
||||
player_move_message.set_allocated_input(player_input);
|
||||
auto r = m_messenger->send(player_move_message);
|
||||
|
||||
// CharacterBody& ts = m_world->registry().get<CharacterBody>(m_player_entity);
|
||||
// auto position = ts.m_character->GetPosition();
|
||||
// character.position_history().set(m_frame_idx, glm::vec3(position[0], position[1], position[2]));
|
||||
// EntityInterpolation& interpolation = m_world->registry().get<EntityInterpolation>(m_player_entity);
|
||||
// interpolation.push(std::chrono::steady_clock::now(), glm::vec3(position[0], position[1], position[2]));
|
||||
|
||||
// m_player_controller.set_target(glm::vec3(position[0], position[1], position[2]));
|
||||
//
|
||||
export_entity_history();
|
||||
|
||||
m_frame_idx++;
|
||||
|
||||
m_world->registry().view<Transform>()
|
||||
.each([&](const auto e, Transform& t) {
|
||||
m_position_history_exporter.write((uint32_t)e,
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, delta_time);
|
||||
|
||||
m_entity_interpolator.update();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,7 +50,12 @@ void ThirdPersonPlayerController::update(const tw::io::InputManager* input, doub
|
||||
m_camera_rotation = yaw * m_camera_rotation * pitch;
|
||||
|
||||
glm::vec3 offset = m_camera_rotation * glm::vec3(0, 0, m_camera_zoom);
|
||||
m_camera->view().look_at(get_target_position() + offset, get_target_position());
|
||||
|
||||
glm::vec3 from = m_camera->view().position();
|
||||
glm::vec3 to = get_target_position() + offset;
|
||||
glm::vec3 interpolated = glm::mix(from, to, 0.8f);
|
||||
|
||||
m_camera->view().look_at(interpolated, get_target_position());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
#include "ClientWorldController.hpp"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <chrono>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <span>
|
||||
|
||||
#include "network/ServerConnection.hpp"
|
||||
|
||||
#include "Entity.pb.h"
|
||||
#include "Login.pb.h"
|
||||
#include "WorldState.pb.h"
|
||||
#include "PlayerMove.pb.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "messages/PlayerMoveMessage.hpp"
|
||||
#include "metrics/HistoryBuffer.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/CharacterController.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/WorldEntity.hpp"
|
||||
#include "tw/serial/WorldStateWriter.hpp"
|
||||
#include "network/EntityInterpolation.hpp"
|
||||
|
||||
namespace tw {
|
||||
|
||||
entt::entity
|
||||
ClientWorldController::create_entity(const std::string& name, glm::vec3 position) {
|
||||
const auto entity = m_world->registry().create();
|
||||
|
||||
if(!m_mesh.has_value()) {
|
||||
m_mesh = m_world_renderer->add_mesh(drw::MeshData::cube(glm::vec3(1.0f)));
|
||||
}
|
||||
spdlog::info("Creating entity {}", name);
|
||||
|
||||
m_world->registry()
|
||||
.emplace<Transform>(entity,
|
||||
Transform(position));
|
||||
|
||||
m_world->registry()
|
||||
.emplace<tw::WorldEntity>(entity,
|
||||
tw::WorldEntity(name, (uint32_t)entity));
|
||||
|
||||
m_world->registry()
|
||||
.emplace<tw::drw::Mesh>(entity,
|
||||
m_mesh.value());
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
// void ClientWorldController::apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count) {
|
||||
// for(int i = 0; i < count; i++) {
|
||||
// const mmo::EntityPosition* const position = positions[i];
|
||||
// std::optional<entt::entity> entity = m_entity_id_map.get_local(position->id());
|
||||
//
|
||||
// if(!entity.has_value()) {
|
||||
// spdlog::warn("Received position update for unknown entity {}", position->id());
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// glm::vec3 p = {position->x(), position->y(), position->z()};
|
||||
// m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
//
|
||||
// EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
|
||||
//
|
||||
// if(history != nullptr) {
|
||||
// auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count();
|
||||
// history->set(millis, p);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
void ClientWorldController::spawn_entity(const std::string& name, uint32_t server_id) {
|
||||
auto entity = create_entity(name, glm::vec3());
|
||||
spdlog::info("Spawning entity {}", server_id);
|
||||
|
||||
m_entity_id_map.register_server_to_local(server_id, entity);
|
||||
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == server_id) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_interpolator.register_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
ClientWorldController::ClientWorldController(
|
||||
const io::InputManager* inputs,
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
net::ServerConnection* connection,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
) :
|
||||
m_input_manager(inputs),
|
||||
m_world(world),
|
||||
m_physics_world(physics_world),
|
||||
m_world_renderer(world_renderer),
|
||||
m_player_controller(&world_renderer->camera(), glm::vec3()),
|
||||
m_connection(connection),
|
||||
m_messages(connection->endpoint()),
|
||||
m_network_metrics(network_metrics),
|
||||
m_input_update_tick_step(20),
|
||||
m_interpolator(&m_world->registry(), 300),
|
||||
m_replicator_client(&m_messages, &m_entity_id_map, m_network_metrics, m_physics_world, m_world, m_connection, &m_interpolator)
|
||||
{
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
spdlog::info("Logged in!");
|
||||
});
|
||||
|
||||
m_messages.set_handler<mmo::SetControlledEntity>(
|
||||
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
|
||||
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
|
||||
m_controlled_server_id = mesg.entity_id();
|
||||
try_bind_player_entity();
|
||||
});
|
||||
|
||||
m_connection->endpoint()->set_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[this](msg::PeerId, std::span<const std::byte> data) {
|
||||
|
||||
serial::WorldStateReader reader(data);
|
||||
|
||||
m_replicator_client.handle_snapshot(m_network_frame_idx, reader);
|
||||
});
|
||||
|
||||
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
|
||||
for(auto& spawn : mesg.spawns()) {
|
||||
spawn_entity(spawn.name(), spawn.entity_id());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientWorldController::~ClientWorldController() { }
|
||||
|
||||
void ClientWorldController::try_bind_player_entity() {
|
||||
if(!m_controlled_server_id.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto local_entity = m_entity_id_map.get_local(m_controlled_server_id.value());
|
||||
if(!local_entity.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entt::entity entity = local_entity.value();
|
||||
spdlog::info("Binding player entity");
|
||||
|
||||
m_player_entity = entity;
|
||||
m_replicator_client.set_player_entity(entity);
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
|
||||
|
||||
m_world->registry().emplace<CharacterController>(entity, 20.0f);
|
||||
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
|
||||
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
|
||||
position
|
||||
));
|
||||
|
||||
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void ClientWorldController::update_network() {
|
||||
auto network_start = Clock::now();
|
||||
// collects incoming messages
|
||||
m_connection->update();
|
||||
m_network_metrics->record_update_time(Clock::now() - network_start);
|
||||
|
||||
m_network_metrics->sample({
|
||||
.bytes_sent = m_connection->endpoint()->bytes_sent(),
|
||||
.bytes_received = m_connection->endpoint()->bytes_received(),
|
||||
.messages_sent = m_connection->endpoint()->messages_sent(),
|
||||
.messages_received = m_connection->endpoint()->messages_received()
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// m_world->registry().view<Transform>()
|
||||
// .each([&](const auto e, Transform& t) {
|
||||
// m_position_history_exporter.write((uint32_t)e,
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(Clock::now().time_since_epoch()).count(), t.position());
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
void ClientWorldController::update(double delta_time) {
|
||||
// collects messages from network for processing
|
||||
update_network();
|
||||
|
||||
m_player_controller.update(m_input_manager, delta_time);
|
||||
|
||||
// collect input every tick of the input_update_tick_step
|
||||
if(m_input_update_tick_step.has_ticked()) {
|
||||
// TODO: might be more like `calc_velocity_vector`.
|
||||
// 1. it is not exactly *input* but input mapped to normalized velocity vector
|
||||
// 2. I hate calling methods `update`
|
||||
glm::vec3 input = m_player_controller.input();
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
|
||||
if(controller) {
|
||||
// controller->set_input(m_network_frame_idx, input);
|
||||
m_replicator_client.set_input(m_network_frame_idx, input);
|
||||
m_replicator_client.record_prediction(m_network_frame_idx);
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_network_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
m_network_frame_idx++;
|
||||
}
|
||||
|
||||
m_interpolator.interpolate_smoothed_entities(std::chrono::high_resolution_clock::now() - std::chrono::milliseconds(500));
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
// after interpolation happen, follow the target
|
||||
// TODO: The third person controller could pull
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
// m_player_controller.set_target(player_transform->position());
|
||||
}
|
||||
}
|
||||
|
||||
m_frame_idx++;
|
||||
|
||||
// m_replicator_client.update();
|
||||
}
|
||||
|
||||
}
|
||||
+40
-30
@@ -3,12 +3,13 @@
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/gtx/io.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "metrics/HistoryBufferExporter.hpp"
|
||||
#include "network/EntityIdMap.hpp"
|
||||
#include "network/ReplicatorClient.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
@@ -16,6 +17,9 @@
|
||||
#include "world/ThirdPersonPlayerController.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
class ServerConnection;
|
||||
}
|
||||
|
||||
namespace tw {
|
||||
|
||||
@@ -35,50 +39,40 @@ class ClientWorldController {
|
||||
drw::WorldRenderer* m_world_renderer;
|
||||
JoltPhysicsWorld* m_physics_world;
|
||||
|
||||
entt::entity m_player_entity;
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
std::optional<uint32_t> m_controlled_server_id;
|
||||
ThirdPersonPlayerController m_player_controller;
|
||||
|
||||
std::optional<tw::net::MessageHandler> m_messenger;
|
||||
net::ServerConnection* m_connection;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
LockStep m_tick_step;
|
||||
dbg::NetworkMetrics* m_network_metrics;
|
||||
|
||||
LockStep m_input_update_tick_step;
|
||||
|
||||
uint32_t m_frame_idx = 1;
|
||||
uint32_t m_network_frame_idx = 1;
|
||||
entt::entity m_entity_id;
|
||||
|
||||
glm::vec3 m_input;
|
||||
|
||||
bool m_is_connected;
|
||||
|
||||
std::optional<drw::Mesh> m_mesh;
|
||||
|
||||
net::EntityPositionInterpolator m_interpolator;
|
||||
|
||||
net::ReplicatorClient m_replicator_client;
|
||||
|
||||
net::EntityIdMap m_entity_id_map;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
|
||||
|
||||
net::EntityPositionInterpolator m_entity_interpolator;
|
||||
|
||||
/**
|
||||
* Mapping from the server entity_id to local entity_id
|
||||
* Server might have the same entity under different name
|
||||
* TODO: Figure out if entt supports custUntitledom IDs to sync them
|
||||
*/
|
||||
std::unordered_map<uint32_t, entt::entity> m_entity_mapping;
|
||||
void try_bind_player_entity();
|
||||
|
||||
entt::entity create_entity(const std::string& name, glm::vec3 position);
|
||||
|
||||
net::MessageHandler create_messenger();
|
||||
|
||||
std::optional<entt::entity> map_from_server_entity(int id);
|
||||
|
||||
void map_server_entity(int server_id, entt::entity local_id);
|
||||
|
||||
void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Exports entity history to CSV file for analysis
|
||||
*/
|
||||
void export_entity_history();
|
||||
void update_network();
|
||||
|
||||
public:
|
||||
ClientWorldController(
|
||||
@@ -86,11 +80,27 @@ public:
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
net::ServerConnection* connection,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
);
|
||||
|
||||
~ClientWorldController();
|
||||
|
||||
|
||||
constexpr uint32_t frame_idx() const {
|
||||
return m_frame_idx;
|
||||
}
|
||||
|
||||
constexpr std::optional<entt::entity> player_entity() const {
|
||||
if(!m_controlled_server_id.has_value()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return m_entity_id_map.get_local(m_controlled_server_id.value());
|
||||
}
|
||||
|
||||
void spawn_entity(const std::string& name, uint32_t server_id);
|
||||
|
||||
void update(double delta_time);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include "imgui.h"
|
||||
#include "backends/imgui_impl_vulkan.h"
|
||||
#include "backends/imgui_impl_sdl2.h"
|
||||
#include "backends/imgui_impl_sdl3.h"
|
||||
|
||||
namespace tw::drw::gui {
|
||||
|
||||
@@ -73,7 +73,7 @@ lft::rg::RenderTaskBuilder ImGuiRenderPass::create_render_task() {
|
||||
const std::string font_path = "/home/martin/projects/mmo/external/imgui/misc/fonts/ProggyClean.ttf";
|
||||
io.Fonts->AddFontFromFileTTF(font_path.c_str(), 13.0f);
|
||||
auto init_info = create_imgui_init_info(info.gpu(), info.renderpass());
|
||||
ImGui_ImplSDL2_InitForVulkan(context->m_window->get_handle());
|
||||
ImGui_ImplSDL3_InitForVulkan(context->m_window->get_handle());
|
||||
ImGui_ImplVulkan_Init(&init_info);
|
||||
|
||||
context->is_initialized = true;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
project(tw_io)
|
||||
|
||||
file(GLOB HEADERS
|
||||
include/*.hpp
|
||||
include/io/*.hpp
|
||||
include/bytebuffer/*.hpp
|
||||
include/exception/*.hpp
|
||||
)
|
||||
|
||||
add_library(${PROJECT_NAME} INTERFACE)
|
||||
add_library(tw::io ALIAS ${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME}
|
||||
INTERFACE FILE_SET HEADERS
|
||||
BASE_DIRS include
|
||||
FILES ${HEADERS})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
${PROJECT_SOURCE_DIR}/include/
|
||||
)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
INTERFACE
|
||||
spdlog::spdlog
|
||||
tl::expected
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
# io
|
||||
|
||||
Transport-agnostic I/O primitives shared by the networking modules.
|
||||
|
||||
Header-only (`tw::io`), namespace `tw::net`.
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| `io/Read.hpp`, `io/Write.hpp` | `Read<T>` / `Write<T>` interfaces every stream implements (`read_into`, `write`, `flush`, plus `read_exact` helpers) |
|
||||
| `io/BufferReader.hpp`, `io/BufferWriter.hpp` | Buffering decorators that wrap another `Read<T>` / `Write<T>` |
|
||||
| `bytebuffer/ByteBuffer.hpp` | `RingByteBuffer` — circular buffer over a caller-owned `std::span` |
|
||||
| `bytebuffer/ByteBufferReader.hpp`, `ByteBufferWriter.hpp` | Linear cursor read/write over a `std::span` |
|
||||
| `bytebuffer/ByteBufferCodec.hpp`, `ByteBufferEncoder.hpp`, `ByteBufferDecoder.hpp` | Typed push/pop on top of `RingByteBuffer`, specialize `ByteBufferCodec<T>` for custom types |
|
||||
| `NetworkError.hpp` | errno-backed error type returned by `Read` / `Write` |
|
||||
|
||||
This module owns no sockets and links no transport — it sits below `tw::network`
|
||||
and `tw::quicr` so both can share buffers and stream interfaces without depending
|
||||
on each other.
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <netdb.h>
|
||||
#include <string>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* A failed host lookup.
|
||||
*
|
||||
* Kept apart from NetworkError because getaddrinfo reports EAI_ codes, which
|
||||
* are their own mostly-negative space: sharing one enum would map a lookup
|
||||
* failure onto whichever errno happened to carry the same number.
|
||||
*/
|
||||
struct ResolutionError {
|
||||
int m_code;
|
||||
int m_errno;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Only EAI_SYSTEM defers to errno, and errno will not have survived by the
|
||||
* time message() runs, so it is captured here.
|
||||
*/
|
||||
static ResolutionError from_gai(int code) {
|
||||
return { code, errno };
|
||||
}
|
||||
|
||||
std::string message() const {
|
||||
switch (m_code) {
|
||||
case EAI_NONAME:
|
||||
return "The host name is not known.";
|
||||
case EAI_AGAIN:
|
||||
return "The name server is unreachable or busy; the lookup may succeed later.";
|
||||
case EAI_FAIL:
|
||||
return "The name server returned a permanent failure.";
|
||||
case EAI_FAMILY:
|
||||
return "The requested address family is not supported.";
|
||||
case EAI_SERVICE:
|
||||
return "The requested port is not available for this socket type.";
|
||||
case EAI_MEMORY:
|
||||
return "Insufficient memory was available to complete the lookup.";
|
||||
case EAI_SYSTEM:
|
||||
return std::string(strerror(m_errno));
|
||||
default:
|
||||
return std::string(gai_strerror(m_code));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user