Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6dd954ded |
+4
-1
@@ -12,5 +12,8 @@ cmake-build-debug/
|
||||
compile_commands.json
|
||||
*.md
|
||||
|
||||
# cloned by FetchContent into the build tree at configure time
|
||||
# cloned by FetchContent at configure time
|
||||
external/glm/
|
||||
external/entt/
|
||||
external/jolt/
|
||||
external/tracy/
|
||||
|
||||
@@ -2,7 +2,6 @@ build/
|
||||
Debug/
|
||||
Release/
|
||||
Testing/
|
||||
dist/
|
||||
.cache
|
||||
imgui.ini
|
||||
|
||||
|
||||
+8
-4
@@ -1,9 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.26)
|
||||
project(tw_common)
|
||||
project(towards)
|
||||
|
||||
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)
|
||||
@@ -42,12 +40,18 @@ target_include_directories(${PROJECT_NAME}
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
spdlog::spdlog
|
||||
tw::network
|
||||
tw::protocol
|
||||
loft::common
|
||||
loft::base
|
||||
loft::render_graph
|
||||
glm::glm
|
||||
EnTT::EnTT
|
||||
Jolt
|
||||
tl::expected
|
||||
Tracy::TracyClient
|
||||
pqxx
|
||||
pq
|
||||
)
|
||||
|
||||
include(cmake/Modules.cmake)
|
||||
|
||||
@@ -4,7 +4,6 @@ Game engine built as a modular monolith.
|
||||
|
||||
## Building
|
||||
|
||||
### Locally
|
||||
Dependencies:
|
||||
- Conan 2 (`pipx install conan`, or `pip install conan`)
|
||||
- Vulkan SDK
|
||||
@@ -28,8 +27,8 @@ $ 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
|
||||
$ cmake --preset conan-debug
|
||||
$ cmake --build --preset conan-debug
|
||||
```
|
||||
|
||||
For a release build, repeat both steps with `-s build_type=Release` and the
|
||||
@@ -42,43 +41,3 @@ Server is at `build/modules/client/mmo_server`
|
||||
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,9 +4,8 @@ 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(SDL3 CONFIG REQUIRED)
|
||||
find_library(LIBDECOR_LIB decor-0)
|
||||
find_package(Protobuf CONFIG REQUIRED VERSION 3.75)
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(Protobuf CONFIG REQUIRED)
|
||||
|
||||
include(FetchContent)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;Distribution")
|
||||
|
||||
+7
-22
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
timescaledb:
|
||||
image: timescale/timescaledb:latest-pg15
|
||||
image: timescale/timescaledb:latest-pg16
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: mmo
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- timescaledb_data:/var/lib/towards_db/data
|
||||
- timescaledb_data:/var/lib/postgresql/data
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
@@ -24,28 +24,13 @@ services:
|
||||
depends_on:
|
||||
- timescaledb
|
||||
|
||||
chat-server:
|
||||
profiles: [e2e]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: chat-server
|
||||
zone-server:
|
||||
build: .
|
||||
ports:
|
||||
- "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"
|
||||
- "8101:8101/udp"
|
||||
- "8102:8102/udp"
|
||||
depends_on:
|
||||
- chat-server
|
||||
command: ["--channel", "1"]
|
||||
- timescaledb
|
||||
|
||||
volumes:
|
||||
timescaledb_data:
|
||||
|
||||
+3
-20
@@ -1,31 +1,14 @@
|
||||
[requires]
|
||||
protobuf/7.35.0
|
||||
sdl/3.4.8
|
||||
libpqxx/8.0.2
|
||||
sdl/2.32.10
|
||||
|
||||
[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
|
||||
sdl/*:pulse=False
|
||||
libffi/*:shared=True
|
||||
|
||||
[generators]
|
||||
CMakeDeps
|
||||
CMakeToolchain
|
||||
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# 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"]
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[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
+4
-2
@@ -3,9 +3,10 @@ project(imgui)
|
||||
file(GLOB FILES
|
||||
./*.cpp
|
||||
./backends/imgui_impl_vulkan.cpp
|
||||
./backends/imgui_impl_sdl3.cpp
|
||||
./backends/imgui_impl_sdl2.cpp
|
||||
)
|
||||
|
||||
find_package(SDL2 QUIET)
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
add_library(${PROJECT_NAME} OBJECT ${FILES})
|
||||
@@ -20,10 +21,11 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
volk::volk
|
||||
sdl::sdl
|
||||
${SDL2_LIBRARIES}
|
||||
)
|
||||
|
||||
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
|
||||
sdl::sdl
|
||||
${SDL2_LIBRARIES}
|
||||
imgui::imgui
|
||||
)
|
||||
#
|
||||
|
||||
Vendored
+3
-2
@@ -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 VK_USE_PLATFORM_WAYLAND_KHR)
|
||||
set(VOLK_STATIC_DEFINES VK_USE_PLATFORM_XCB_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,6 +51,7 @@ 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
@@ -0,0 +1,140 @@
|
||||
# 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
|
||||
sdl::sdl
|
||||
${SDL2_LIBRARIES}
|
||||
)
|
||||
|
||||
get_property(dirs DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY INCLUDE_DIRECTORIES)
|
||||
|
||||
@@ -17,8 +17,7 @@ target_include_directories(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
volk::volk
|
||||
sdl::sdl
|
||||
)
|
||||
${SDL2_LIBRARIES})
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
|
||||
Vendored
+560
@@ -0,0 +1,560 @@
|
||||
# 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,6 +9,7 @@ set(LIBS
|
||||
loft_base
|
||||
loft_common
|
||||
loft_window
|
||||
${SDL2_LIBRARIES}
|
||||
volk)
|
||||
|
||||
set(FILES
|
||||
|
||||
+2
-3
@@ -4,7 +4,7 @@ file(GLOB FILES src/*.c src/*.cpp)
|
||||
|
||||
add_library(${PROJECT_NAME} ${FILES})
|
||||
|
||||
find_package(SDL3 REQUIRED)
|
||||
find_package(SDL2 REQUIRED)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/include/"
|
||||
@@ -13,8 +13,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
loft_base
|
||||
sdl::sdl
|
||||
${LIBDECOR_LIB}
|
||||
${SDL2_LIBRARIES}
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
#define LOFT_SDLWINDOW_H
|
||||
|
||||
#include "Window.hpp"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL2/SDL.h>
|
||||
#include <utility>
|
||||
#include <volk.h>
|
||||
#include <string>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <volk.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
#include <SDL2/SDL_events.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
+19
-23
@@ -1,12 +1,10 @@
|
||||
#include "SDLWindow.h"
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <SDL2/SDL_error.h>
|
||||
#include <SDL_video.h>
|
||||
#include <volk.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "SDLWindow.h"
|
||||
#include "LoftException.hpp"
|
||||
#include <SDL3/SDL_vulkan.h>
|
||||
#include <SDL2/SDL_vulkan.h>
|
||||
#include <vector>
|
||||
#include <vulkan/vulkan_core.h>
|
||||
|
||||
@@ -29,16 +27,16 @@ VkExtent2D SDLWindow::get_size() const {
|
||||
SDLWindow::SDLWindow(std::string name, VkRect2D rect) :
|
||||
m_extent{rect.extent}
|
||||
{
|
||||
if(!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVERYTHING) < 0) {
|
||||
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_HIGH_PIXEL_DENSITY |
|
||||
SDL_WINDOW_VULKAN | SDL_WINDOW_ALLOW_HIGHDPI |
|
||||
SDL_WINDOW_RESIZABLE);
|
||||
|
||||
if(m_pWindow == nullptr) {
|
||||
@@ -48,7 +46,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(), NULL, &surface);
|
||||
SDL_Vulkan_CreateSurface(m_pWindow, instance->instance(), &surface);
|
||||
return Surface(instance, surface);
|
||||
}
|
||||
|
||||
@@ -62,21 +60,19 @@ int32_t SDLWindow::poll_event(SDL_Event *pOutEvent) const {
|
||||
|
||||
std::vector<std::string> SDLWindow::get_required_extensions() const {
|
||||
uint32_t count = 0;
|
||||
const char* const *instance_extensions = SDL_Vulkan_GetInstanceExtensions(&count);
|
||||
SDL_Vulkan_GetInstanceExtensions(m_pWindow, &count, nullptr);
|
||||
|
||||
std::vector<std::string> extensions(count);
|
||||
for(int i = 0; i < count; i++) {
|
||||
extensions[i] = std::string(instance_extensions[i]);
|
||||
}
|
||||
std::vector<const char*> extensions(count);
|
||||
SDL_Vulkan_GetInstanceExtensions(m_pWindow, &count, extensions.data());
|
||||
|
||||
// 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 extensions;
|
||||
return extension_strings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
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());
|
||||
std::println("[ch:{}] <{}> {}", bcast.channel_id(), bcast.sender_id(), bcast.message());
|
||||
});
|
||||
|
||||
mmo::chat::JoinChannelRequest join;
|
||||
@@ -98,10 +98,10 @@ int main(int argc, char* argv[]) {
|
||||
[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;
|
||||
@@ -121,7 +121,7 @@ int main(int argc, char* argv[]) {
|
||||
[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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ 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
|
||||
@@ -21,13 +19,12 @@ add_dependencies(tw_shaders imgui::plot imgui::plot)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw_common
|
||||
towards
|
||||
tw::network
|
||||
tw::metrics
|
||||
tw::quicr
|
||||
loft::common
|
||||
loft::base
|
||||
${LIBDECOR_LIB}
|
||||
loft_window
|
||||
loft::render_graph
|
||||
tw::protocol
|
||||
@@ -41,6 +38,8 @@ target_link_libraries(${PROJECT_NAME}
|
||||
Jolt
|
||||
Tracy::TracyClient
|
||||
TracyClient
|
||||
pqxx
|
||||
pq
|
||||
)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
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,7 +2,6 @@
|
||||
|
||||
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;
|
||||
@@ -22,6 +21,5 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
#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
|
||||
@@ -1,33 +0,0 @@
|
||||
#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
|
||||
@@ -1,31 +0,0 @@
|
||||
#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
|
||||
@@ -1,33 +0,0 @@
|
||||
#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
|
||||
@@ -1,110 +0,0 @@
|
||||
#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
|
||||
@@ -1,32 +0,0 @@
|
||||
#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
|
||||
@@ -1,24 +0,0 @@
|
||||
#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
|
||||
@@ -1,34 +0,0 @@
|
||||
#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
|
||||
@@ -1,43 +0,0 @@
|
||||
#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
|
||||
@@ -1,182 +0,0 @@
|
||||
#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
|
||||
@@ -1,78 +0,0 @@
|
||||
#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
|
||||
@@ -1,31 +0,0 @@
|
||||
#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
|
||||
@@ -1,32 +0,0 @@
|
||||
#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
|
||||
@@ -1,42 +0,0 @@
|
||||
#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,5 +10,34 @@ 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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#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();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#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();
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#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,238 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include <format>
|
||||
#include <ratio>
|
||||
|
||||
#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::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(Transform* instance) {
|
||||
ImGui::SeparatorText("Drawn Position");
|
||||
|
||||
static int axis = 0;
|
||||
ImGui::Combo("Axis", &axis, "X\0Y\0Z\0");
|
||||
|
||||
if(m_subject != instance) {
|
||||
restart(instance);
|
||||
}
|
||||
|
||||
const float time = std::chrono::duration<float>(Clock::now() - m_started_at).count();
|
||||
const float position = instance->position()[axis];
|
||||
|
||||
sample(time, position);
|
||||
|
||||
ImGui::Text("%.3f", position);
|
||||
|
||||
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);
|
||||
|
||||
// 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");
|
||||
ImGui::SeparatorText("Entity Interpolation");
|
||||
|
||||
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);
|
||||
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");
|
||||
}
|
||||
|
||||
// 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();
|
||||
static int32_t time_buffer_len = 1000;
|
||||
ImGui::SliderInt("Buffer length (ms)", &time_buffer_len, 0, 1000);
|
||||
|
||||
std::vector<float> ages(count);
|
||||
std::vector<float> positions(count);
|
||||
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);
|
||||
|
||||
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];
|
||||
ImGui::Text("Mixed value: (%f, %f)", mixed_value.x, mixed_value.y);
|
||||
|
||||
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("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];
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
EntityManagerGui::EntityManagerGui(World* world) :
|
||||
DebugWindow("entity_manager", "Entities", "World"),
|
||||
m_world(world) {
|
||||
|
||||
}
|
||||
@@ -50,8 +49,7 @@ void EntityManagerGui::draw_entity_components() {
|
||||
ImGui::Text("Vertex offset: %i", mesh->vertex_offset());
|
||||
}
|
||||
|
||||
draw_debug_components<tw::Transform, tw::net::EntityPositionInterpolation,
|
||||
tw::CharacterBody, tw::CharacterController>();
|
||||
draw_debug_components<tw::CharacterBody, tw::CharacterController, tw::net::EntityPositionInterpolation>();
|
||||
|
||||
// auto character = m_world->registry().try_get<tw::CharacterBody>(m_selected);
|
||||
// if(character != nullptr) {
|
||||
@@ -66,7 +64,9 @@ void EntityManagerGui::draw_entity_components() {
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void EntityManagerGui::draw_contents() {
|
||||
void EntityManagerGui::draw() {
|
||||
ImGui::Begin("Transforms");
|
||||
|
||||
ImGui::BeginChild("Entities", ImVec2(0, 260), ImGuiChildFlags_Border);
|
||||
|
||||
ImGui::SeparatorText("Entities");
|
||||
@@ -87,6 +87,8 @@ void EntityManagerGui::draw_contents() {
|
||||
if(m_selected_entity.has_value()) {
|
||||
draw_entity_components();
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/ComponentGui.hpp"
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "world/World.hpp"
|
||||
|
||||
@@ -9,7 +8,7 @@
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class EntityManagerGui : public tw::dbg::DebugWindow {
|
||||
class EntityManagerGui {
|
||||
private:
|
||||
World* m_world;
|
||||
|
||||
@@ -30,15 +29,14 @@ private:
|
||||
...);
|
||||
}
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
entt::entity& selected() {
|
||||
return m_selected;
|
||||
}
|
||||
|
||||
EntityManagerGui(World* world);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/MetricWidget.hpp"
|
||||
|
||||
@@ -15,7 +14,7 @@ namespace tw::dbg::tools {
|
||||
* connection actually carried. Durations are shown as the average of each
|
||||
* second, with the range behind them.
|
||||
*/
|
||||
class NetworkStatsGui : public tw::dbg::DebugWindow {
|
||||
class NetworkStatsGui {
|
||||
MetricWidget m_response;
|
||||
MetricWidget m_update;
|
||||
MetricWidget m_bytes_in;
|
||||
@@ -30,8 +29,23 @@ class NetworkStatsGui : public tw::dbg::DebugWindow {
|
||||
|
||||
int m_history_in_seconds = 30;
|
||||
|
||||
protected:
|
||||
void draw_contents() override {
|
||||
public:
|
||||
explicit NetworkStatsGui(const NetworkMetrics& metrics) :
|
||||
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)
|
||||
{ }
|
||||
|
||||
void draw() {
|
||||
ImGui::Begin("Network Stats");
|
||||
|
||||
ImGui::SliderInt("History", &m_history_in_seconds, 5, 300, "%d s");
|
||||
|
||||
const size_t history = (size_t)m_history_in_seconds;
|
||||
@@ -49,22 +63,9 @@ protected:
|
||||
m_correction_distance.draw(history);
|
||||
m_ack_lag_frames.draw(history);
|
||||
m_replayed_frames.draw(history);
|
||||
}
|
||||
|
||||
public:
|
||||
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)
|
||||
{ }
|
||||
ImGui::End();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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_contents() {
|
||||
void PerformanceStatsGui::draw() {
|
||||
ImGui::Begin("Stats");
|
||||
|
||||
ImGui::Text("FPS: %ld", m_lockstep.fps());
|
||||
fps_history[fps_history_idx] = m_lockstep.fps();
|
||||
@@ -34,6 +34,8 @@ void PerformanceStatsGui::draw_contents() {
|
||||
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,11 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/DebugWindow.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PerformanceStatsGui : public tw::dbg::DebugWindow {
|
||||
class PerformanceStatsGui {
|
||||
private:
|
||||
LockStep& m_lockstep;
|
||||
|
||||
@@ -16,11 +15,10 @@ private:
|
||||
uint32_t frame_idx = 0;
|
||||
bool is_plot_filled = false;
|
||||
|
||||
protected:
|
||||
void draw_contents() override;
|
||||
|
||||
public:
|
||||
PerformanceStatsGui(LockStep& lock_step);
|
||||
|
||||
void draw();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -49,9 +49,7 @@ 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
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
|
||||
/**
|
||||
* Writes entity derivation message
|
||||
*/
|
||||
template<typename TMesg>
|
||||
class EntityDerivationWriter {
|
||||
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
#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,40 +9,47 @@
|
||||
namespace tw::net {
|
||||
|
||||
EntityPositionInterpolator::EntityPositionInterpolator(
|
||||
entt::registry* registry, size_t bufferingIntervalInMillis
|
||||
entt::registry* registry, entt::entity player_entity, 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::set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position) {
|
||||
void EntityPositionInterpolator::add_position_for_entity(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(time_point, position);
|
||||
interpolation->push(Clock::now(), position);
|
||||
}
|
||||
|
||||
glm::vec3 EntityPositionInterpolator::get_position(Clock::time_point time_point, entt::entity entity) {
|
||||
glm::vec3 EntityPositionInterpolator::get_position_for_entity(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 [from, to, value] = interpolation->get_values_around(time_point);
|
||||
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
|
||||
|
||||
auto [from, to, value] = interpolation->get_values_around(now);
|
||||
return glm::mix(from, to, value);
|
||||
}
|
||||
|
||||
void EntityPositionInterpolator::interpolate_smoothed_entities(Clock::time_point time_point) {
|
||||
void EntityPositionInterpolator::update() {
|
||||
m_registry->view<EntityPositionInterpolation, Transform>()
|
||||
.each([&](const auto entity, const EntityPositionInterpolation& interpolation, Transform& ts) {
|
||||
auto [from, to, value] = interpolation.get_values_around(time_point);
|
||||
auto now = Clock::now() - std::chrono::milliseconds(m_bufferingIntervalInMillis);
|
||||
|
||||
auto [from, to, value] = interpolation.get_values_around(now);
|
||||
ts.set_position(glm::mix(from, to, value));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <entt/entt.hpp>
|
||||
@@ -13,20 +12,23 @@ class EntityPositionInterpolator {
|
||||
|
||||
entt::registry* m_registry;
|
||||
|
||||
size_t m_bufferingIntervalInMillis;
|
||||
entt::entity m_player_entity;
|
||||
|
||||
glm::vec3 get_position(Clock::time_point time_point, entt::entity entity);
|
||||
size_t m_bufferingIntervalInMillis;
|
||||
|
||||
public:
|
||||
EntityPositionInterpolator(
|
||||
entt::registry* registry,
|
||||
entt::entity player_entity,
|
||||
size_t bufferingIntervalInMillis);
|
||||
|
||||
void register_entity(entt::entity entity);
|
||||
|
||||
void set_position(Clock::time_point time_point, entt::entity entity, glm::vec3 position);
|
||||
void add_position_for_entity(entt::entity entity, glm::vec3 position);
|
||||
|
||||
void interpolate_smoothed_entities(Clock::time_point time_point);
|
||||
glm::vec3 get_position_for_entity(entt::entity entity);
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -5,21 +5,15 @@
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
template<typename T, int length = 10>
|
||||
template<typename T, int length = 3>
|
||||
class InterpolatedProperty {
|
||||
using Clock = std::chrono::high_resolution_clock;
|
||||
|
||||
std::vector<T> m_buffer;
|
||||
std::vector<Clock::time_point> m_time_buffer;
|
||||
|
||||
uint32_t m_head;
|
||||
uint32_t m_size;
|
||||
std::array<T, length> m_buffer;
|
||||
std::array<Clock::time_point, length> m_time_buffer;
|
||||
|
||||
public:
|
||||
InterpolatedProperty(T initial_value) :
|
||||
m_buffer(length),
|
||||
m_time_buffer(length),
|
||||
m_size(0), m_head(0) {
|
||||
InterpolatedProperty(T initial_value) {
|
||||
for(int i = 0; i < length; i++) {
|
||||
m_buffer[i] = initial_value;
|
||||
m_time_buffer[i] = Clock::now();
|
||||
@@ -29,21 +23,7 @@ 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;
|
||||
}
|
||||
@@ -84,46 +64,6 @@ 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);
|
||||
|
||||
|
||||
+8
-10
@@ -1,4 +1,4 @@
|
||||
#include "PlayerRollback.hpp"
|
||||
#include "PlayerReconciler.hpp"
|
||||
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics)
|
||||
PlayerReconciler::PlayerReconciler(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)
|
||||
{
|
||||
@@ -21,21 +21,21 @@ PlayerRollback::PlayerRollback(JoltPhysicsWorld* physics)
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::set_input(uint32_t frame, glm::vec3 input) {
|
||||
void PlayerReconciler::record_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) {
|
||||
void PlayerReconciler::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,
|
||||
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry,
|
||||
uint32_t current_frame)
|
||||
{
|
||||
@@ -77,16 +77,14 @@ bool PlayerRollback::apply_correction(uint32_t ack_frame, glm::vec3 authoritativ
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerRollback::place_character(entt::entity player, entt::registry* registry,
|
||||
void PlayerReconciler::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());
|
||||
@@ -94,7 +92,7 @@ void PlayerRollback::place_character(entt::entity player, entt::registry* regist
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::replay_from(uint32_t from_frame, entt::entity player,
|
||||
void PlayerReconciler::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);
|
||||
@@ -113,7 +111,7 @@ void PlayerRollback::replay_from(uint32_t from_frame, entt::entity player,
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerRollback::reset_at(uint32_t frame) {
|
||||
void PlayerReconciler::reset_at(uint32_t frame) {
|
||||
m_last_reconciled_ack = frame;
|
||||
|
||||
for (auto& record : m_records) {
|
||||
+9
-5
@@ -11,7 +11,7 @@ class JoltPhysicsWorld;
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
class PlayerRollback {
|
||||
class PlayerReconciler {
|
||||
private:
|
||||
struct Record {
|
||||
uint32_t frame;
|
||||
@@ -33,13 +33,12 @@ private:
|
||||
uint32_t m_last_ack_frame = 0;
|
||||
|
||||
public:
|
||||
PlayerRollback(tw::JoltPhysicsWorld* physics);
|
||||
PlayerReconciler(tw::JoltPhysicsWorld* physics);
|
||||
|
||||
void set_input(uint32_t frame, glm::vec3 input);
|
||||
void record_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,
|
||||
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
private:
|
||||
@@ -57,6 +56,11 @@ private:
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Drops every stored frame and treats `frame` as already answered. Used when
|
||||
* the player is placed outright, where nothing recorded before the placement
|
||||
* describes where it now is.
|
||||
*/
|
||||
void reset_at(uint32_t frame);
|
||||
|
||||
uint64_t rollback_count() const { return m_rollback_count; }
|
||||
@@ -1,205 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#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,23 +1,23 @@
|
||||
#include "runtime.hpp"
|
||||
|
||||
#include "app/ClientArgs.hpp"
|
||||
#include "Address.hpp"
|
||||
#include "SDLWindow.h"
|
||||
#include "debug/tools/PerformanceStatsGui.hpp"
|
||||
#include "debug/tools/NetworkStatsGui.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_sdl3.h"
|
||||
#include "imgui_impl_sdl2.h"
|
||||
|
||||
#include "implot.h"
|
||||
#include "implot_internal.h"
|
||||
#include "world/Transform.hpp"
|
||||
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
#include <SDL_events.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <tracy/Tracy.hpp>
|
||||
#include <memory>
|
||||
@@ -31,23 +31,17 @@ std::unique_ptr<lft::win::Window> create_window(const std::string& name, VkExten
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {};
|
||||
int get_port_from_args(int argc, char** argv) {
|
||||
try {
|
||||
if(argc > 1) {
|
||||
return atoi(argv[1]);
|
||||
} else {
|
||||
return 8080;
|
||||
}
|
||||
|
||||
auto parsed = app::parse_address(*argument);
|
||||
if(!parsed) {
|
||||
spdlog::error("Failed to parse server address: {}", parsed.error());
|
||||
return {};
|
||||
} catch(std::exception& e) {
|
||||
std::println("Could not parse port from arguments, using default.");
|
||||
return 8080;
|
||||
}
|
||||
|
||||
return *parsed;
|
||||
}
|
||||
|
||||
Runtime::Runtime(int argc, char** argv) :
|
||||
@@ -58,43 +52,51 @@ Runtime::Runtime(int argc, char** argv) :
|
||||
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
|
||||
m_input_manager(m_window.get()),
|
||||
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_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }, &m_network_metrics),
|
||||
m_lockstep(60)
|
||||
{
|
||||
m_debug_ui.windows().add(&m_perf_stats);
|
||||
}
|
||||
|
||||
Runtime::~Runtime() {
|
||||
m_debug_ui.windows().remove(&m_perf_stats);
|
||||
}
|
||||
// bool Runtime::world_state_packet_handler(uint32_t* p_frame_idx, WorldSnapshotMessage* mesg) {
|
||||
// uint32_t frame_idx = *p_frame_idx;
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
// if(mesg->frame_idx < frame_idx) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
void Runtime::update_state(double delta_time) {
|
||||
if(auto* lobby = std::get_if<app::LobbyState>(&m_state)) {
|
||||
lobby->update(delta_time);
|
||||
|
||||
if(auto result = lobby->take_result()) {
|
||||
m_state.emplace<app::GameState>(context(), std::move(result->connection));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 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});
|
||||
|
||||
std::get<app::GameState>(m_state).update(delta_time);
|
||||
}
|
||||
// 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;
|
||||
// }
|
||||
|
||||
void Runtime::run() {
|
||||
dbg::tools::EntityManagerGui entity_manager(&m_world);
|
||||
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
|
||||
dbg::tools::NetworkStatsGui network_stats(m_network_metrics);
|
||||
|
||||
ImPlot::CreateContext();
|
||||
m_is_running = true;
|
||||
|
||||
@@ -104,21 +106,50 @@ void Runtime::run() {
|
||||
}
|
||||
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui_ImplSDL2_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());
|
||||
|
||||
// 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();
|
||||
perf_stats.draw();
|
||||
network_stats.draw();
|
||||
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
|
||||
|
||||
update_state(m_lockstep.delta_time());
|
||||
|
||||
m_debug_ui.draw_windows();
|
||||
m_world.step(m_lockstep.delta_time());
|
||||
|
||||
m_world_renderer.render();
|
||||
FrameMark;
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
#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"
|
||||
|
||||
@@ -24,38 +18,35 @@ 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::dbg::NetworkMetrics m_network_metrics;
|
||||
|
||||
tw::ClientWorldController m_world_controller;
|
||||
|
||||
tw::LockStep m_lockstep;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
#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 "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 {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static std::unique_ptr<msg::MessageEndpoint> create_endpoint() {
|
||||
auto endpoint_r = msg::MessageEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
spdlog::error("Failed to create the endpoint: {}", endpoint_r.error().message());
|
||||
throw std::runtime_error("Failed to create the endpoint");
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
static msg::MessageConnection* connect_to_server(msg::MessageEndpoint* endpoint, net::Address address) {
|
||||
auto server_r = endpoint->connect(address.ip_string(), address.port());
|
||||
if(!server_r) {
|
||||
spdlog::error("Failed to connect to server: {}", server_r.error().message());
|
||||
throw std::runtime_error("Failed to connect to server");
|
||||
}
|
||||
|
||||
spdlog::info("Connected to server at {}", address.to_string());
|
||||
|
||||
return server_r.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,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
) :
|
||||
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_endpoint(create_endpoint()),
|
||||
m_server(connect_to_server(m_endpoint.get(), address)),
|
||||
m_messages(m_endpoint.get()),
|
||||
m_network_metrics(network_metrics),
|
||||
m_tick_step(20),
|
||||
m_is_connected(false),
|
||||
m_input_send_times(INPUT_SEND_TIME_COUNT),
|
||||
m_position_history_exporter("/home/martin/output.csv"),
|
||||
m_entity_interpolator(&m_world->registry(), (entt::entity)0, 300),
|
||||
m_reconciler(physics_world)
|
||||
{
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
if(!m_is_connected) {
|
||||
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_endpoint->set_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[this](msg::PeerId, std::span<const std::byte> data) {
|
||||
|
||||
serial::WorldStateReader reader(data);
|
||||
|
||||
auto header = reader.read_header();
|
||||
|
||||
measure_response_time(header.frame_idx);
|
||||
|
||||
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);
|
||||
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == spawn) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
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};
|
||||
|
||||
if(m_player_entity.has_value() && entity.value() == m_player_entity.value()) {
|
||||
// The entity was created before its position was known, so the
|
||||
// body sits at the origin until the server places it. There is
|
||||
// no predicted history to reconcile against yet.
|
||||
if(!m_player_position_initialized) {
|
||||
m_player_position_initialized = true;
|
||||
snap_player_to(entity.value(), p);
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 position_before = glm::vec3(0.0f);
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(entity.value());
|
||||
if(player_transform) {
|
||||
position_before = player_transform->position();
|
||||
}
|
||||
|
||||
bool reconcile_happened = m_reconciler.reconcile(header.frame_idx, p, entity.value(), &m_world->registry(), m_frame_idx);
|
||||
|
||||
if(reconcile_happened) {
|
||||
// Where the replay actually ended up, which is ahead of the
|
||||
// acked position by the frames that were re-simulated.
|
||||
glm::vec3 position_after = player_transform
|
||||
? player_transform->position()
|
||||
: p;
|
||||
glm::vec3 correction_delta = position_before - position_after;
|
||||
float correction_magnitude = glm::length(correction_delta);
|
||||
if(correction_magnitude > 5.0f) {
|
||||
correction_delta = glm::normalize(correction_delta) * 5.0f;
|
||||
}
|
||||
m_visual_error += correction_delta;
|
||||
|
||||
m_render_curr_position = position_after;
|
||||
m_render_prev_position = position_after;
|
||||
m_tick_accumulator = 0.0;
|
||||
|
||||
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(header.frame_idx != 0) {
|
||||
uint32_t ack_lag = 0;
|
||||
if(m_frame_idx >= header.frame_idx) {
|
||||
ack_lag = m_frame_idx - header.frame_idx;
|
||||
}
|
||||
m_network_metrics->record_ack_lag(ack_lag);
|
||||
}
|
||||
} else {
|
||||
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();
|
||||
});
|
||||
|
||||
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
|
||||
auto entity = create_entity(mesg.name(), glm::vec3());
|
||||
|
||||
map_server_entity(mesg.entity_id(), entity);
|
||||
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == mesg.entity_id()) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientWorldController::~ClientWorldController() {
|
||||
}
|
||||
|
||||
void ClientWorldController::try_bind_player_entity() {
|
||||
if(!m_controlled_server_id.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto local_entity = map_from_server_entity(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;
|
||||
|
||||
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::snap_player_to(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_prev_position = position;
|
||||
m_render_curr_position = position;
|
||||
m_tick_accumulator = 0.0;
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
|
||||
// Frames simulated before the player was placed describe a position it never
|
||||
// actually had, so answers to them must not be reconciled against.
|
||||
m_reconciler.reset_at(m_frame_idx);
|
||||
}
|
||||
|
||||
void ClientWorldController::export_entity_history() {
|
||||
}
|
||||
|
||||
void ClientWorldController::measure_response_time(uint32_t 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(frame_idx == 0 || 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(m_frame_idx - frame_idx >= INPUT_SEND_TIME_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_measured_frame = frame_idx;
|
||||
|
||||
auto sent_at = m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT];
|
||||
m_network_metrics->record_response_time(Clock::now() - sent_at);
|
||||
}
|
||||
|
||||
void ClientWorldController::update(double delta_time) {
|
||||
m_player_controller.update(m_input_manager, delta_time);
|
||||
|
||||
ImGui::Begin("Player Controller");
|
||||
if(m_player_entity.has_value()) {
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity.value());
|
||||
}
|
||||
|
||||
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()) {
|
||||
auto network_start = Clock::now();
|
||||
m_endpoint->update();
|
||||
m_network_metrics->record_update_time(Clock::now() - network_start);
|
||||
|
||||
m_network_metrics->sample({
|
||||
.bytes_sent = m_endpoint->bytes_sent(),
|
||||
.bytes_received = m_endpoint->bytes_received(),
|
||||
.messages_sent = m_endpoint->messages_sent(),
|
||||
.messages_received = m_endpoint->messages_received()
|
||||
});
|
||||
|
||||
if(!m_is_connected && false) {
|
||||
return;
|
||||
} else {
|
||||
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_frame_idx, input);
|
||||
m_reconciler.record_input(m_frame_idx, input);
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
glm::vec3 true_position = player_transform->position();
|
||||
m_reconciler.record_prediction(m_frame_idx, true_position);
|
||||
|
||||
m_render_prev_position = m_render_curr_position;
|
||||
m_render_curr_position = true_position;
|
||||
m_tick_accumulator = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
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(input.x);
|
||||
player_input->set_y(input.y);
|
||||
player_input->set_z(input.z);
|
||||
|
||||
player_move_message.set_allocated_input(player_input);
|
||||
auto r = m_messages.send(m_server, player_move_message, false);
|
||||
|
||||
m_input_send_times[m_frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
|
||||
|
||||
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_entity_interpolator.update();
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
m_visual_error *= std::exp(-delta_time * kVisualErrorDecayRate);
|
||||
if(glm::length(m_visual_error) < 0.001f) {
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
}
|
||||
|
||||
m_tick_accumulator += delta_time;
|
||||
float alpha = glm::clamp(
|
||||
static_cast<float>(m_tick_accumulator / JoltPhysicsWorld::FIXED_DELTA_TIME),
|
||||
0.0f, 1.0f
|
||||
);
|
||||
glm::vec3 smoothed_position = glm::mix(m_render_prev_position, m_render_curr_position, alpha) + m_visual_error;
|
||||
player_transform->set_position(smoothed_position);
|
||||
|
||||
m_player_controller.set_target(smoothed_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/gtx/io.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "metrics/HistoryBufferExporter.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/World.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "world/ThirdPersonPlayerController.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
#include "network/PlayerReconciler.hpp"
|
||||
|
||||
|
||||
namespace tw {
|
||||
|
||||
/**
|
||||
* Multiplayer player controller. Requires address of a server to work.
|
||||
*/
|
||||
class ClientWorldController {
|
||||
/**
|
||||
* Inputs
|
||||
*/
|
||||
const io::InputManager* m_input_manager;
|
||||
|
||||
/**
|
||||
* Pointers to the world
|
||||
*/
|
||||
World* m_world;
|
||||
drw::WorldRenderer* m_world_renderer;
|
||||
JoltPhysicsWorld* m_physics_world;
|
||||
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
std::optional<uint32_t> m_controlled_server_id;
|
||||
ThirdPersonPlayerController m_player_controller;
|
||||
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
dbg::NetworkMetrics* m_network_metrics;
|
||||
|
||||
LockStep m_tick_step;
|
||||
|
||||
uint32_t m_frame_idx = 1;
|
||||
entt::entity m_entity_id;
|
||||
|
||||
glm::vec3 m_input;
|
||||
|
||||
bool m_is_connected;
|
||||
|
||||
std::optional<drw::Mesh> m_mesh;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void try_bind_player_entity();
|
||||
|
||||
/**
|
||||
* 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(entt::entity entity, glm::vec3 position);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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 frame_idx);
|
||||
|
||||
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
|
||||
|
||||
net::EntityPositionInterpolator m_entity_interpolator;
|
||||
|
||||
net::PlayerReconciler m_reconciler;
|
||||
|
||||
/**
|
||||
* Visual smoothing for render-rate interpolation between 20 Hz ticks.
|
||||
*/
|
||||
glm::vec3 m_render_prev_position{0.0f};
|
||||
glm::vec3 m_render_curr_position{0.0f};
|
||||
double m_tick_accumulator = 0.0;
|
||||
|
||||
/**
|
||||
* Visual error from reconciliation corrections, decays over time.
|
||||
*/
|
||||
glm::vec3 m_visual_error{0.0f};
|
||||
static constexpr double kVisualErrorDecayRate = 12.0;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
entt::entity create_entity(const std::string& name, glm::vec3 position);
|
||||
|
||||
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();
|
||||
|
||||
public:
|
||||
ClientWorldController(
|
||||
const io::InputManager* inputs,
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
);
|
||||
|
||||
~ClientWorldController();
|
||||
|
||||
void update(double delta_time);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -50,12 +50,7 @@ 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);
|
||||
|
||||
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());
|
||||
m_camera->view().look_at(get_target_position() + offset, get_target_position());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/gtx/io.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"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "world/ThirdPersonPlayerController.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
|
||||
namespace tw::net {
|
||||
class ServerConnection;
|
||||
}
|
||||
|
||||
namespace tw {
|
||||
|
||||
/**
|
||||
* Multiplayer player controller. Requires address of a server to work.
|
||||
*/
|
||||
class ClientWorldController {
|
||||
/**
|
||||
* Inputs
|
||||
*/
|
||||
const io::InputManager* m_input_manager;
|
||||
|
||||
/**
|
||||
* Pointers to the world
|
||||
*/
|
||||
World* m_world;
|
||||
drw::WorldRenderer* m_world_renderer;
|
||||
JoltPhysicsWorld* m_physics_world;
|
||||
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
std::optional<uint32_t> m_controlled_server_id;
|
||||
ThirdPersonPlayerController m_player_controller;
|
||||
|
||||
net::ServerConnection* m_connection;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
void try_bind_player_entity();
|
||||
|
||||
entt::entity create_entity(const std::string& name, glm::vec3 position);
|
||||
|
||||
void apply_entity_positions(const mmo::EntityPosition* const* positions, size_t count);
|
||||
|
||||
void update_network();
|
||||
|
||||
public:
|
||||
ClientWorldController(
|
||||
const io::InputManager* inputs,
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
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_sdl3.h"
|
||||
#include "backends/imgui_impl_sdl2.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_ImplSDL3_InitForVulkan(context->m_window->get_handle());
|
||||
ImGui_ImplSDL2_InitForVulkan(context->m_window->get_handle());
|
||||
ImGui_ImplVulkan_Init(&init_info);
|
||||
|
||||
context->is_initialized = true;
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
#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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ResolutionError.hpp"
|
||||
#include "tl/expected.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <netdb.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* Turns a host name or an address literal into a socket address.
|
||||
*
|
||||
* `family` is the family of the socket the result will be given to. AF_INET6
|
||||
* asks for IPv4-only names as ::ffff: mapped addresses, so that one dual-stack
|
||||
* socket reaches both; AF_UNSPEC takes the name as it comes and suits addresses
|
||||
* that are only being validated, displayed or stored.
|
||||
*
|
||||
* Blocks for the length of a DNS round trip when the name is not already known,
|
||||
* so it belongs at connect time rather than anywhere periodic.
|
||||
*/
|
||||
inline tl::expected<sockaddr_storage, ResolutionError>
|
||||
resolve_host(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
|
||||
addrinfo hints {};
|
||||
hints.ai_family = family;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
|
||||
// AI_ADDRCONFIG is deliberately absent. Together with AF_INET6 it discards
|
||||
// every result on a host that carries no global IPv6 address, which is the
|
||||
// default state of a container on a bridge network.
|
||||
if(family == AF_INET6) {
|
||||
hints.ai_flags = AI_V4MAPPED | AI_ALL;
|
||||
}
|
||||
|
||||
// Passing the port as the service spares us setting sin_port or sin6_port
|
||||
// by hand once the family of the answer is known.
|
||||
const std::string service = std::to_string(port);
|
||||
|
||||
addrinfo* results = nullptr;
|
||||
const int rc = ::getaddrinfo(host.c_str(), service.c_str(), &hints, &results);
|
||||
if(rc != 0) {
|
||||
return tl::make_unexpected(ResolutionError::from_gai(rc));
|
||||
}
|
||||
|
||||
// The list arrives ordered by RFC 6724, so the head is the address the
|
||||
// system would have picked for itself.
|
||||
sockaddr_storage storage {};
|
||||
std::memcpy(&storage, results->ai_addr, results->ai_addrlen);
|
||||
|
||||
::freeaddrinfo(results);
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -70,13 +70,7 @@ MessageConnection* MessageEndpoint::add_peer(net::quicr::QuicrConnection* connec
|
||||
}
|
||||
|
||||
tl::expected<MessageConnection*, MessageError> MessageEndpoint::connect(const std::string& host, int port) {
|
||||
auto address_r = net::quicr::QuicrAddress::resolve(host, port, m_endpoint->family());
|
||||
if(!address_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::ConnectFailed, address_r.error().message()));
|
||||
}
|
||||
|
||||
auto connection_r = m_endpoint->connect(address_r.value());
|
||||
auto connection_r = m_endpoint->connect(net::quicr::QuicrAddress(host, port));
|
||||
if(!connection_r) {
|
||||
return tl::make_unexpected(
|
||||
MessageError(MessageErrorType::ConnectFailed, connection_r.error().message()));
|
||||
|
||||
@@ -8,7 +8,7 @@ add_executable(${PROJECT_NAME} ${FILES})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
tw_common
|
||||
towards
|
||||
tw::network
|
||||
tw::quicr
|
||||
tw::protocol
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "Address.hpp"
|
||||
#include "Entity.pb.h"
|
||||
#include "Login.pb.h"
|
||||
#include "PlayerMove.pb.h"
|
||||
#include "ProtobufMessages.hpp"
|
||||
@@ -64,9 +63,6 @@ public:
|
||||
m_messages.set_handler<mmo::WorldStateMessage>(
|
||||
[this](tw::msg::PeerId, const mmo::WorldStateMessage& mesg) { });
|
||||
|
||||
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||
[this](tw::msg::PeerId, const mmo::EntitySpawnMessage& mesg) { });
|
||||
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](tw::msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
if(!m_is_connected) {
|
||||
@@ -107,30 +103,13 @@ public:
|
||||
player_input->set_z(m_velocity.z);
|
||||
player_move_mesg.set_allocated_input(player_input);
|
||||
auto r = m_messages.send(m_server, player_move_mesg, false);
|
||||
m_frame_idx++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if(argc > 4) {
|
||||
spdlog::error("Usage: {} [client_count] [host] [port]", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uint32_t NUM_CLIENTS = argc > 1 ? std::strtoul(argv[1], nullptr, 10) : 300;
|
||||
const std::string host = argc > 2 ? argv[2] : "127.0.0.1";
|
||||
const int port = argc > 3 ? std::atoi(argv[3]) : 8101;
|
||||
|
||||
auto address_r = tw::net::Address::resolve(host, port);
|
||||
if(!address_r) {
|
||||
spdlog::error("Failed to resolve {}:{}: {}", host, port, address_r.error().message());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const tw::net::Address address = address_r.value();
|
||||
|
||||
spdlog::info("Starting {} clients against {}", NUM_CLIENTS, address.to_string());
|
||||
int main() {
|
||||
const int NUM_CLIENTS = 600;
|
||||
tw::net::Address address = {"127.0.0.1", 8101};
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
for(uint32_t i = 0; i < NUM_CLIENTS; i++) {
|
||||
@@ -145,7 +124,7 @@ int main(int argc, char** argv) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
for(uint32_t i = 0; i < threads.size(); i++) {
|
||||
for(uint32_t i = 0; i < NUM_CLIENTS; i++) {
|
||||
threads[i].join();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "io/HostResolver.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string>
|
||||
@@ -9,7 +7,6 @@
|
||||
#include <optional>
|
||||
#include <sys/socket.h>
|
||||
#include <format>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
@@ -41,24 +38,6 @@ public:
|
||||
: m_storage(storage)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Look a host up, accepting a name where the constructor above takes only
|
||||
* an address literal.
|
||||
*
|
||||
* `family` should be the family of the socket the address will be used
|
||||
* with. The default suits an address that is only being validated or
|
||||
* displayed, and takes whatever the name resolves to.
|
||||
*/
|
||||
static tl::expected<Address, ResolutionError>
|
||||
resolve(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
|
||||
auto storage_r = resolve_host(host, port, family);
|
||||
if(!storage_r) {
|
||||
return tl::make_unexpected(storage_r.error());
|
||||
}
|
||||
|
||||
return Address(std::move(storage_r.value()));
|
||||
}
|
||||
|
||||
/** Return a const pointer suitable for connect / sendto / bind. */
|
||||
const struct sockaddr* sockaddr() const {
|
||||
return reinterpret_cast<const struct sockaddr*>(&m_storage);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <spdlog/fmt/fmt.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
// #include <barrier>
|
||||
// #include <catch2/catch_test_macros.hpp>
|
||||
// #include <span>
|
||||
// #include <iostream>
|
||||
//
|
||||
// #include "UdpStream.hpp"
|
||||
// #include "Address.hpp"
|
||||
// #include "quicr/QuicrConnection.hpp"
|
||||
// #include "quicr/QuicrConnectionListener.hpp"
|
||||
// #include "quicr/QuicrEndpoint.hpp"
|
||||
//
|
||||
// TEST_CASE("Start two sockets and send message", "[udp]") {
|
||||
// std::barrier create_sync_point(2);
|
||||
// std::barrier send_sync_point(2);
|
||||
//
|
||||
// const std::string message = "Hello, server!";
|
||||
//
|
||||
// std::thread server_thread([&]() {
|
||||
// auto r = tw::net::UdpStream::bind(tw::net::Address {"127.0.0.1", 6969});
|
||||
// if(!r.has_value()) {
|
||||
// std::cout << ("Failed to bind UDP stream: {}") << r.error().message() << std::endl;
|
||||
// }
|
||||
//
|
||||
// auto flag_result = r->set_non_blocking();
|
||||
//
|
||||
// auto server = std::move(r.value());
|
||||
//
|
||||
// create_sync_point.arrive_and_wait();
|
||||
// send_sync_point.arrive_and_wait();
|
||||
//
|
||||
// std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
//
|
||||
// std::vector<std::byte> buffer(1024);
|
||||
// tw::net::Address from {{}, 0};
|
||||
// auto read_result = server.read_into(std::span{buffer.data(), buffer.size()}, &from);
|
||||
//
|
||||
// if(!read_result.has_value()) {
|
||||
// spdlog::error("Failed to read from UDP stream: {}", read_result.error().message());
|
||||
// }
|
||||
//
|
||||
// // convert the buffer to a string
|
||||
// std::string mesg(buffer.data(), buffer.data() + read_result.value());
|
||||
// REQUIRE(mesg == message);
|
||||
// });
|
||||
//
|
||||
// std::thread client_thread([&]() {
|
||||
// create_sync_point.arrive_and_wait();
|
||||
//
|
||||
// auto connect_result = tw::net::UdpStream::to(tw::net::Address{"127.0.0.1", 6969});
|
||||
// if(!connect_result.has_value()) {
|
||||
// spdlog::error("Failed to bind UDP stream: {}", connect_result.error().mesg());
|
||||
// }
|
||||
//
|
||||
// auto client = std::move(connect_result.value());
|
||||
//
|
||||
// std::string mesg = message;
|
||||
// auto write_result = client.write(std::as_writable_bytes(std::span(mesg.begin(), mesg.end())));
|
||||
//
|
||||
// if(!write_result.has_value() && write_result.value() == mesg.length()) {
|
||||
// spdlog::error("Failed to write to UDP stream: {}", write_result.error().message());
|
||||
// }
|
||||
//
|
||||
// send_sync_point.arrive_and_wait();
|
||||
// });
|
||||
//
|
||||
// server_thread.join();
|
||||
// client_thread.join();
|
||||
// }
|
||||
//
|
||||
// using namespace tw::net;
|
||||
// using namespace tw::net::quicr;
|
||||
#include <barrier>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <span>
|
||||
#include <iostream>
|
||||
|
||||
#include "UdpStream.hpp"
|
||||
#include "Address.hpp"
|
||||
#include "quicr/QuicrConnection.hpp"
|
||||
#include "quicr/QuicrConnectionListener.hpp"
|
||||
#include "quicr/QuicrEndpoint.hpp"
|
||||
|
||||
TEST_CASE("Start two sockets and send message", "[udp]") {
|
||||
std::barrier create_sync_point(2);
|
||||
std::barrier send_sync_point(2);
|
||||
|
||||
const std::string message = "Hello, server!";
|
||||
|
||||
std::thread server_thread([&]() {
|
||||
auto r = tw::net::UdpStream::bind(tw::net::Address {"127.0.0.1", 6969});
|
||||
if(!r.has_value()) {
|
||||
std::cout << ("Failed to bind UDP stream: {}") << r.error().message() << std::endl;
|
||||
}
|
||||
|
||||
auto flag_result = r->set_non_blocking();
|
||||
|
||||
auto server = std::move(r.value());
|
||||
|
||||
create_sync_point.arrive_and_wait();
|
||||
send_sync_point.arrive_and_wait();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
std::vector<std::byte> buffer(1024);
|
||||
tw::net::Address from {{}, 0};
|
||||
auto read_result = server.read_into(std::span{buffer.data(), buffer.size()}, &from);
|
||||
|
||||
if(!read_result.has_value()) {
|
||||
spdlog::error("Failed to read from UDP stream: {}", read_result.error().message());
|
||||
}
|
||||
|
||||
// convert the buffer to a string
|
||||
std::string mesg(buffer.data(), buffer.data() + read_result.value());
|
||||
REQUIRE(mesg == message);
|
||||
});
|
||||
|
||||
std::thread client_thread([&]() {
|
||||
create_sync_point.arrive_and_wait();
|
||||
|
||||
auto connect_result = tw::net::UdpStream::to(tw::net::Address{"127.0.0.1", 6969});
|
||||
if(!connect_result.has_value()) {
|
||||
spdlog::error("Failed to bind UDP stream: {}", connect_result.error().mesg());
|
||||
}
|
||||
|
||||
auto client = std::move(connect_result.value());
|
||||
|
||||
std::string mesg = message;
|
||||
auto write_result = client.write(std::as_writable_bytes(std::span(mesg.begin(), mesg.end())));
|
||||
|
||||
if(!write_result.has_value() && write_result.value() == mesg.length()) {
|
||||
spdlog::error("Failed to write to UDP stream: {}", write_result.error().message());
|
||||
}
|
||||
|
||||
send_sync_point.arrive_and_wait();
|
||||
});
|
||||
|
||||
server_thread.join();
|
||||
client_thread.join();
|
||||
}
|
||||
|
||||
using namespace tw::net;
|
||||
using namespace tw::net::quicr;
|
||||
|
||||
@@ -22,7 +22,7 @@ target_include_directories(tw_peer_to_peer_lib
|
||||
|
||||
target_link_libraries(tw_peer_to_peer_lib
|
||||
PUBLIC
|
||||
tw_common
|
||||
towards
|
||||
tw::network
|
||||
tw::quicr
|
||||
tw::protocol
|
||||
@@ -32,6 +32,8 @@ target_link_libraries(tw_peer_to_peer_lib
|
||||
spdlog::spdlog
|
||||
)
|
||||
|
||||
# ── standalone executable ─────────────────────────────────────────────────────
|
||||
|
||||
add_executable(${PROJECT_NAME} src/PeerToPeer.cpp)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
@@ -39,4 +41,6 @@ target_link_libraries(${PROJECT_NAME}
|
||||
tw_peer_to_peer_lib
|
||||
)
|
||||
|
||||
# ── tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
add_subdirectory(tests)
|
||||
|
||||
@@ -36,6 +36,8 @@ PeerWorldController::PeerWorldController(uint32_t self_id, uint16_t port,
|
||||
});
|
||||
}
|
||||
|
||||
// ── registry & connection ─────────────────────────────────────────────────────
|
||||
|
||||
void PeerWorldController::register_self() {
|
||||
PeerRegistry reg(m_registry_path);
|
||||
reg.register_self(m_self_id, "127.0.0.1:" + std::to_string(m_port));
|
||||
@@ -84,6 +86,8 @@ void PeerWorldController::wait_for_connections(uint32_t expected_count,
|
||||
m_self_id, m_peer_ids.size(), expected_count);
|
||||
}
|
||||
|
||||
// ── tick ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
bool PeerWorldController::tick() {
|
||||
m_link->poll();
|
||||
|
||||
@@ -118,6 +122,8 @@ bool PeerWorldController::tick() {
|
||||
return has_advanced;
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
mmo::peer::PeerAction PeerWorldController::build_local_action(uint32_t frame) const {
|
||||
float t = static_cast<float>(frame) *
|
||||
static_cast<float>(WorldStepProcessor::FIXED_DELTA_S);
|
||||
|
||||
@@ -56,9 +56,6 @@ public:
|
||||
|
||||
const std::vector<uint32_t>& peer_ids() const { return m_peer_ids; }
|
||||
|
||||
uint32_t get_peer_latest_snapshot_frame(uint32_t peer_id) {
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t m_self_id;
|
||||
uint16_t m_port;
|
||||
|
||||
@@ -15,18 +15,7 @@ QuicrPeerLink::QuicrPeerLink(uint32_t self_id, uint16_t port)
|
||||
{}
|
||||
|
||||
void QuicrPeerLink::connect_to(uint32_t peer_id, const tw::net::Address& addr) {
|
||||
// The address is rebuilt from its text, which for a mapped or IPv6 peer is
|
||||
// more than the literal constructor can parse, so it goes back through the
|
||||
// resolver — into the endpoint's family, since that is what will send it.
|
||||
auto address_r = net::quicr::QuicrAddress::resolve(addr.ip_string(), addr.port(),
|
||||
m_endpoint->family());
|
||||
if (!address_r) {
|
||||
spdlog::warn("QuicrPeerLink[{}]: address of peer {} failed to resolve: {}",
|
||||
m_self_id, peer_id, address_r.error().message());
|
||||
return;
|
||||
}
|
||||
|
||||
auto r = m_endpoint->connect(address_r.value());
|
||||
auto r = m_endpoint->connect(net::quicr::QuicrAddress(addr.ip_string(), addr.port()));
|
||||
if (!r) {
|
||||
spdlog::warn("QuicrPeerLink[{}]: connect to peer {} failed", m_self_id, peer_id);
|
||||
return;
|
||||
|
||||
@@ -2,16 +2,12 @@ syntax = "proto3";
|
||||
|
||||
package mmo;
|
||||
|
||||
message EntitySpawn {
|
||||
message EntitySpawnMessage {
|
||||
uint32 entity_id = 1;
|
||||
bool is_player = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
message EntitySpawnMessage {
|
||||
repeated EntitySpawn spawns = 1;
|
||||
}
|
||||
|
||||
message EntityDespawnMessage {
|
||||
uint32 entity_id = 1;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "Serialization.hpp"
|
||||
#include "Serializers.hpp"
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "io/HostResolver.hpp"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <string>
|
||||
@@ -9,7 +7,6 @@
|
||||
#include <optional>
|
||||
#include <sys/socket.h>
|
||||
#include <format>
|
||||
#include <tl/expected.hpp>
|
||||
|
||||
namespace tw::net::quicr {
|
||||
|
||||
@@ -41,24 +38,6 @@ public:
|
||||
: m_storage(storage)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Look a host up, accepting a name where the constructor above takes only
|
||||
* an address literal.
|
||||
*
|
||||
* `family` should be the family of the endpoint socket the address will be
|
||||
* sent from, so that a dual-stack socket is handed a mapped address rather
|
||||
* than a bare IPv4 one.
|
||||
*/
|
||||
static tl::expected<QuicrAddress, ResolutionError>
|
||||
resolve(const std::string& host, int port, sa_family_t family = AF_UNSPEC) {
|
||||
auto storage_r = resolve_host(host, port, family);
|
||||
if(!storage_r) {
|
||||
return tl::make_unexpected(storage_r.error());
|
||||
}
|
||||
|
||||
return QuicrAddress(std::move(storage_r.value()));
|
||||
}
|
||||
|
||||
/** Return a const pointer suitable for connect / sendto / bind. */
|
||||
const struct sockaddr* sockaddr() const {
|
||||
return reinterpret_cast<const struct sockaddr*>(&m_storage);
|
||||
|
||||
@@ -18,7 +18,6 @@ class QuicrConnectionListener;
|
||||
|
||||
class QuicrEndpoint {
|
||||
int32_t m_socket_fd;
|
||||
sa_family_t m_family;
|
||||
std::unordered_map<uint64_t, std::shared_ptr<QuicrConnection>> m_connections;
|
||||
|
||||
std::vector<std::byte> m_inbound_buffer;
|
||||
@@ -27,7 +26,7 @@ class QuicrEndpoint {
|
||||
|
||||
void process_datagram(std::span<std::byte> datagram, QuicrAddress from);
|
||||
|
||||
QuicrEndpoint(int socket_fd, sa_family_t family);
|
||||
QuicrEndpoint(int socket_fd);
|
||||
|
||||
public:
|
||||
QuicrEndpoint(const QuicrEndpoint&) = delete;
|
||||
@@ -48,12 +47,6 @@ public:
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The family the socket was opened with. Addresses have to be resolved
|
||||
* into it before they can be sent to.
|
||||
*/
|
||||
sa_family_t family() const { return m_family; }
|
||||
|
||||
static tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> create();
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
namespace tw::net::quicr {
|
||||
|
||||
QuicrEndpoint::QuicrEndpoint(int socket_fd, sa_family_t family)
|
||||
: m_inbound_buffer(64 * 1024), m_socket_fd(socket_fd), m_family(family),
|
||||
QuicrEndpoint::QuicrEndpoint(int socket_fd)
|
||||
: m_inbound_buffer(64 * 1024), m_socket_fd(socket_fd),
|
||||
m_new_connection_handler(nullptr) {
|
||||
|
||||
}
|
||||
@@ -31,57 +31,29 @@ tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> QuicrEndpoint::create_a
|
||||
}
|
||||
|
||||
tl::expected<std::unique_ptr<QuicrEndpoint>, QuicrError> QuicrEndpoint::create() {
|
||||
// An IPv6 socket with IPV6_V6ONLY cleared also carries IPv4 peers, which
|
||||
// arrive as ::ffff: mapped addresses. A host with IPv6 switched off answers
|
||||
// EAFNOSUPPORT instead, and there the endpoint stays IPv4 as it was.
|
||||
sa_family_t domain = AF_INET6;
|
||||
const int domain = AF_INET;
|
||||
int socket_fd = socket(domain, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if(socket_fd < 0 && errno == EAFNOSUPPORT) {
|
||||
domain = AF_INET;
|
||||
socket_fd = socket(domain, SOCK_DGRAM, IPPROTO_UDP);
|
||||
}
|
||||
|
||||
if(socket_fd < 0) {
|
||||
spdlog::error("Failed to create socket: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
if(domain == AF_INET6) {
|
||||
const int v6_only = 0;
|
||||
if(setsockopt(socket_fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6_only, sizeof(v6_only)) < 0) {
|
||||
spdlog::error("Failed to accept IPv4 peers on the socket: {}", strerror(errno));
|
||||
::close(socket_fd);
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
}
|
||||
|
||||
if(fcntl(socket_fd, F_SETFL, fcntl(socket_fd, F_GETFL, 0) | O_NONBLOCK, 1) == -1) {
|
||||
spdlog::error("Failed to set non-blocking mode: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
return std::unique_ptr<QuicrEndpoint>(new QuicrEndpoint(socket_fd, domain));
|
||||
return std::unique_ptr<QuicrEndpoint>(new QuicrEndpoint(socket_fd));
|
||||
}
|
||||
|
||||
tl::expected<void, QuicrError> QuicrEndpoint::bind(int port) {
|
||||
sockaddr_storage storage = {};
|
||||
socklen_t length;
|
||||
|
||||
if(m_family == AF_INET6) {
|
||||
auto& addr = reinterpret_cast<sockaddr_in6&>(storage);
|
||||
addr.sin6_family = AF_INET6;
|
||||
addr.sin6_port = htons(port);
|
||||
addr.sin6_addr = in6addr_any;
|
||||
length = sizeof(sockaddr_in6);
|
||||
} else {
|
||||
auto& addr = reinterpret_cast<sockaddr_in&>(storage);
|
||||
addr.sin_family = AF_INET;
|
||||
const int domain = AF_INET;
|
||||
struct sockaddr_in addr = {};
|
||||
addr.sin_family = domain;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
length = sizeof(sockaddr_in);
|
||||
}
|
||||
|
||||
if(::bind(m_socket_fd, reinterpret_cast<struct sockaddr*>(&storage), length) < 0) {
|
||||
if(::bind(m_socket_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
spdlog::error("Failed to bind socket: {}", strerror(errno));
|
||||
return tl::make_unexpected(QuicrError::from_errno(errno));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ target_link_libraries(tw_serialization
|
||||
EnTT::EnTT
|
||||
)
|
||||
|
||||
# Require C++20 for concepts / span
|
||||
target_compile_features(tw_serialization INTERFACE cxx_std_20)
|
||||
|
||||
# add_subdirectory(tests)
|
||||
# ── Tests / benchmarks ───────────────────────────────────────────────────────
|
||||
add_subdirectory(tests)
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* WorldStateWriter / WorldStateReader
|
||||
* =====================================
|
||||
* Project-specific serialisation for the per-frame world-state snapshot
|
||||
* sent from the server to each connected client.
|
||||
*
|
||||
* Wire format (all values little-endian):
|
||||
*
|
||||
* ┌──────────────────────────────────────────────────────────┐
|
||||
* │ Header (16 bytes) │
|
||||
* │ message_type : uint32 (supplied by the caller) │
|
||||
* │ sequence : uint32 (always 0, no reply expected) │
|
||||
* │ frame_idx : uint32 │
|
||||
* │ entity_count : uint32 (number of position records) │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Spawns section │
|
||||
* │ spawn_count : uint32 │
|
||||
* │ spawn[i].id : uint32 × spawn_count │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Despawns section │
|
||||
* │ despawn_count : uint32 │
|
||||
* │ despawn[i].id : uint32 × despawn_count │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Entity positions (hot path — tightly packed) │
|
||||
* │ [ id:uint32, x:float, y:float, z:float ] × entity_count│
|
||||
* └──────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Total minimum size : 20 bytes (header + empty spawns + empty despawns)
|
||||
* Per entity : 16 bytes
|
||||
* 300 entities : 20 + 300×16 = 4820 bytes (well under MTU for segmented)
|
||||
*
|
||||
* Usage (server side, called once per client per frame):
|
||||
*
|
||||
* tw::serial::WorldStateWriter w(buffer); // buffer is a BinaryBuffer
|
||||
* w.begin(frame_idx);
|
||||
* w.write_spawns(interest.spawns());
|
||||
* w.write_despawns(interest.despawns());
|
||||
* w.begin_entities(num_entities); // writes entity_count slot
|
||||
* for each entity in interest.entities():
|
||||
* w.write_entity(entity_id, position);
|
||||
* w.end(); // patches entity_count
|
||||
* // buffer.view() is ready to send
|
||||
*
|
||||
* Usage (client side):
|
||||
*
|
||||
* tw::serial::WorldStateReader r(payload_span);
|
||||
* auto header = r.read_header(); // frame_idx + counts
|
||||
* for (auto id : r.read_spawns()) { ... }
|
||||
* for (auto id : r.read_despawns()) { ... }
|
||||
* while (r.has_entity()) {
|
||||
* auto [id, pos] = r.read_entity();
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
|
||||
#include "Codec.hpp"
|
||||
#include "GlmCodec.hpp"
|
||||
@@ -13,6 +67,10 @@
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WorldStateWriter
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class WorldStateWriter {
|
||||
BinaryWriter m_w;
|
||||
|
||||
@@ -23,7 +81,14 @@ class WorldStateWriter {
|
||||
public:
|
||||
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
||||
|
||||
void begin(uint32_t frame_idx, uint32_t tick_idx, uint32_t message_type) noexcept {
|
||||
/**
|
||||
* Write the header. Call once per frame, before everything else.
|
||||
* entity_count is patched in end().
|
||||
*
|
||||
* message_type is supplied by the caller so that this module stays free of
|
||||
* the address the message is delivered to.
|
||||
*/
|
||||
void begin(uint32_t frame_idx, uint32_t message_type) noexcept {
|
||||
m_entity_count = 0;
|
||||
|
||||
// message_type — lets the receiver dispatch without peeking further
|
||||
@@ -35,14 +100,15 @@ public:
|
||||
// frame_idx
|
||||
m_w.encode<uint32_t>(frame_idx);
|
||||
|
||||
// tick_idx — counts the states that went out, so the receiver can tell
|
||||
// a late one from a new one. Says nothing about the frame answered.
|
||||
m_w.encode<uint32_t>(tick_idx);
|
||||
|
||||
// entity_count placeholder — patched when end() is called
|
||||
m_entity_count_offset = m_w.reserve_u32();
|
||||
}
|
||||
|
||||
// ── Spawns ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write the spawn list. Pass any range of entt::entity.
|
||||
*/
|
||||
template<typename Range>
|
||||
void write_spawns(const Range& spawns) noexcept {
|
||||
auto count = static_cast<uint32_t>(std::size(spawns));
|
||||
@@ -52,6 +118,8 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Despawns ──────────────────────────────────────────────────────────
|
||||
|
||||
template<typename Range>
|
||||
void write_despawns(const Range& despawns) noexcept {
|
||||
auto count = static_cast<uint32_t>(std::size(despawns));
|
||||
@@ -61,6 +129,16 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entity positions (the hot path) ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write a single entity position record.
|
||||
* id : raw uint32 of the entity handle
|
||||
* pos : world-space position (x, y, z)
|
||||
*
|
||||
* This is the innermost loop of the replicator — every byte matters.
|
||||
* The compiler will inline both calls down to two contiguous memcpys.
|
||||
*/
|
||||
void write_entity(uint32_t id, const glm::vec3& pos) noexcept {
|
||||
m_w.write(id);
|
||||
// Write x, y, z as 3 contiguous floats
|
||||
@@ -68,14 +146,20 @@ public:
|
||||
++m_entity_count;
|
||||
}
|
||||
|
||||
// Convenience overload accepting an entt::entity handle directly
|
||||
void write_entity(entt::entity entity, const glm::vec3& pos) noexcept {
|
||||
write_entity(static_cast<uint32_t>(entity), pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the entity_count field written in begin() and finalise the
|
||||
* buffer. Must be called exactly once after all write_entity() calls.
|
||||
*/
|
||||
void end() noexcept {
|
||||
m_w.patch_u32(m_entity_count_offset, m_entity_count);
|
||||
}
|
||||
|
||||
/** Expose the underlying buffer view (e.g. to pass to send_message). */
|
||||
std::span<const std::byte> view() noexcept {
|
||||
return m_w.buffer().view();
|
||||
}
|
||||
@@ -86,10 +170,13 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WorldStateReader (client-side / test use)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct WorldStateHeader {
|
||||
uint32_t packet_type;
|
||||
uint32_t frame_idx;
|
||||
uint32_t tick_idx;
|
||||
uint32_t entity_count;
|
||||
};
|
||||
|
||||
@@ -114,11 +201,10 @@ public:
|
||||
explicit WorldStateReader(std::span<const std::byte> data) noexcept
|
||||
: m_r(data) {}
|
||||
|
||||
/** Read the header. Must be called first. */
|
||||
/** Read the 12-byte header. Must be called first. */
|
||||
WorldStateHeader read_header() noexcept {
|
||||
// m_header.packet_type = m_r.decode<uint32_t>();
|
||||
m_header.frame_idx = m_r.decode<uint32_t>();
|
||||
m_header.tick_idx = m_r.decode<uint32_t>();
|
||||
m_header.entity_count = m_r.decode<uint32_t>();
|
||||
|
||||
// spawn count follows immediately
|
||||
|
||||
@@ -17,7 +17,7 @@ file(GLOB FILES
|
||||
# )
|
||||
|
||||
# add_executable(${PROJECT_NAME})
|
||||
# add_executable(SerializationBenchmarks ./SerializationBenchmarks.cpp)
|
||||
add_executable(SerializationBenchmarks ./SerializationBenchmarks.cpp)
|
||||
|
||||
target_compile_definitions(SerializationBenchmarks PRIVATE TRACY_ON_DEMAND=1)
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ file(GLOB FILES
|
||||
src/network/*.cpp
|
||||
)
|
||||
|
||||
find_package(libpqxx CONFIG REQUIRED)
|
||||
add_library(tw_server_lib STATIC ${FILES})
|
||||
|
||||
target_include_directories(tw_server_lib
|
||||
@@ -24,7 +23,7 @@ target_include_directories(tw_server_lib
|
||||
|
||||
target_link_libraries(tw_server_lib
|
||||
PUBLIC
|
||||
tw_common
|
||||
towards
|
||||
tw::io
|
||||
tw::network
|
||||
tw::protocol
|
||||
@@ -37,7 +36,8 @@ target_link_libraries(tw_server_lib
|
||||
protobuf::libprotobuf
|
||||
${Boost_LIBRARIES}
|
||||
Tracy::TracyClient
|
||||
libpqxx::pqxx
|
||||
pqxx
|
||||
pq
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME} src/server.cpp)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# Build the toolchain stage on its own and pass it back in to skip the apt step:
|
||||
# docker build -f modules/server/Dockerfile --target toolchain -t tw_toolchain .
|
||||
# docker build -f modules/server/Dockerfile --build-arg TOOLCHAIN_IMAGE=tw_toolchain .
|
||||
ARG TOOLCHAIN_IMAGE=toolchain
|
||||
|
||||
# ── toolchain stage ──────────────────────────────────────────────────────────
|
||||
FROM ubuntu:24.04 AS toolchain
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
clang \
|
||||
libstdc++-14-dev \
|
||||
cmake \
|
||||
ninja-build \
|
||||
mold \
|
||||
git \
|
||||
ca-certificates \
|
||||
pkg-config \
|
||||
glslang-tools \
|
||||
libvulkan-dev \
|
||||
libsdl2-dev \
|
||||
libprotobuf-dev protobuf-compiler \
|
||||
libpq-dev \
|
||||
libpqxx-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV CC=clang
|
||||
ENV CXX=clang++
|
||||
|
||||
# ── build stage ──────────────────────────────────────────────────────────────
|
||||
FROM ${TOOLCHAIN_IMAGE} AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# glm, entt, Jolt, spdlog, expected, Catch2 and tracy are cloned at configure time
|
||||
RUN cmake -B /build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
&& cmake --build /build --target tw_server
|
||||
|
||||
# Stage the shared libraries the binary was linked against; the runtime image has
|
||||
# no package manager. glibc and its loader stay behind, they come with that image.
|
||||
RUN mkdir -p /rootfs/usr/lib/x86_64-linux-gnu \
|
||||
&& ldd /build/modules/server/tw_server \
|
||||
| awk '/=> \//{ print $3 }' \
|
||||
| grep -vE '/(libc|libm|libdl|libpthread|librt|libresolv|libanl)\.so' \
|
||||
| xargs -I{} cp -L {} /rootfs/usr/lib/x86_64-linux-gnu/
|
||||
|
||||
# ── runtime stage ─────────────────────────────────────────────────────────────
|
||||
FROM gcr.io/distroless/cc-debian13:nonroot
|
||||
|
||||
COPY --from=builder /rootfs/ /
|
||||
COPY --from=builder /build/modules/server/tw_server /usr/local/bin/tw_server
|
||||
|
||||
# player connections, zone-server peering
|
||||
EXPOSE 8101/udp 8102/udp
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/tw_server"]
|
||||
@@ -8,22 +8,19 @@
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
/**
|
||||
* Session with the player's client.
|
||||
*/
|
||||
struct PlayerSession {
|
||||
public:
|
||||
SessionId session_id;
|
||||
|
||||
msg::MessageConnection* connection;
|
||||
|
||||
uint32_t last_received_frame;
|
||||
uint32_t last_frame;
|
||||
uint32_t acked_frame;
|
||||
|
||||
PlayerSession(SessionId session_id, msg::MessageConnection* connection) :
|
||||
session_id(session_id),
|
||||
connection(connection),
|
||||
last_received_frame(0),
|
||||
last_frame(0),
|
||||
acked_frame(0)
|
||||
{ }
|
||||
};
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
// Listens on a dedicated port for incoming zone-server peer connections and
|
||||
// opens outgoing connections to known peers.
|
||||
//
|
||||
// Incoming and outgoing peers are held together so that broadcast messages
|
||||
// (ZoneHello, ZoneBye) reach every peer uniformly.
|
||||
class ZoneClusterLink {
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
ProtobufMessages m_messages;
|
||||
@@ -19,10 +24,13 @@ class ZoneClusterLink {
|
||||
public:
|
||||
explicit ZoneClusterLink(int port);
|
||||
|
||||
// Poll for messages and accept any pending peer connections.
|
||||
void update();
|
||||
|
||||
// Connect to a remote zone peer and register it in the peer list.
|
||||
msg::MessageConnection* connect_to_peer(const std::string& host, int port);
|
||||
|
||||
// Send a protobuf message to a single peer.
|
||||
template<typename T>
|
||||
void send_mesg(msg::MessageConnection* peer, const T& msg) {
|
||||
auto send_r = m_messages.send(peer, msg, false);
|
||||
|
||||
@@ -19,6 +19,8 @@ ZoneManager::ZoneManager(im::AreaBounds zone_bounds)
|
||||
))
|
||||
{}
|
||||
|
||||
// ── ZoneProxy interface ───────────────────────────────────────────────────────
|
||||
|
||||
im::AreaBounds ZoneManager::area() const {
|
||||
return {
|
||||
{ (float)m_spatial_backend->world_min_x(), (float)m_spatial_backend->world_min_z() },
|
||||
@@ -31,6 +33,8 @@ void ZoneManager::transfer_entity(EntityInfo&& info) {
|
||||
spawn_entity(std::move(info));
|
||||
}
|
||||
|
||||
// ── Zone management ───────────────────────────────────────────────────────────
|
||||
|
||||
entt::entity ZoneManager::spawn_entity(EntityInfo&& info) {
|
||||
entt::entity entity = m_world->registry().create();
|
||||
|
||||
@@ -100,6 +104,8 @@ uint32_t ZoneManager::acked_input_frame(im::InterestId interest_id) const {
|
||||
return it != m_session_acked_frame.end() ? it->second : 0;
|
||||
}
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
void ZoneManager::check_neighbor_transfers() {
|
||||
ZoneScopedN("ZoneManager::check_neighbor_transfers");
|
||||
|
||||
@@ -155,7 +161,9 @@ void ZoneManager::check_neighbor_transfers() {
|
||||
}
|
||||
}
|
||||
|
||||
void ZoneManager::update(uint32_t frame_idx, float delta_time) {
|
||||
// ── Tick ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
void ZoneManager::tick(uint32_t frame_idx, float delta_time) {
|
||||
ZoneScopedN("ZoneManager::tick");
|
||||
|
||||
FrameMarkStart("Interest System");
|
||||
@@ -168,7 +176,21 @@ void ZoneManager::update(uint32_t frame_idx, float delta_time) {
|
||||
m_world->step(delta_time);
|
||||
FrameMarkEnd("World step");
|
||||
|
||||
// Re-stamp each client entity's newest input onto the server's frame index.
|
||||
// The input ring is keyed by client frame numbers (independent counter),
|
||||
// but physics lookup uses the server's frame counter. By re-stamping onto
|
||||
// the server frame, input(server_frame_idx) resolves by exact match rather
|
||||
// than fallback, ensuring deterministic and correct input consumption.
|
||||
for (const auto& [interest_id, entity] : m_client_entities) {
|
||||
auto* controller = m_world->registry().try_get<CharacterController>(entity);
|
||||
if (!controller) continue;
|
||||
controller->set_input(frame_idx, controller->input());
|
||||
}
|
||||
|
||||
FrameMarkStart("Physics step");
|
||||
// Fixed step, not the measured interval: the simulation has to advance by the
|
||||
// same amount every frame for a replay of the same inputs to land in the same
|
||||
// place.
|
||||
m_physics_world.step(frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME);
|
||||
FrameMarkEnd("Physics step");
|
||||
|
||||
|
||||
@@ -18,28 +18,30 @@
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
// 500-unit cells, 500-unit view radius → 3×3 neighbourhood, no per-entity distance checks.
|
||||
// World bounds are supplied to the constructor at runtime.
|
||||
using SpatialBackendType = im::FixedGrid<500, 500>;
|
||||
|
||||
// How far into this zone a neighbour's interest area is expanded.
|
||||
// Entities within this margin are tracked by the interest system before transfer.
|
||||
static constexpr float kNeighborBorderOverlap = 100.0f;
|
||||
|
||||
/**
|
||||
* Manages one zone: contains state of all entities inside and does logic for them.
|
||||
*/
|
||||
class ZoneManager : public ZoneProxy {
|
||||
struct NeighborEntry {
|
||||
ZoneProxy* proxy;
|
||||
im::InterestId interest_id;
|
||||
};
|
||||
|
||||
// Declaration order == initialisation order.
|
||||
std::unique_ptr<World> m_world;
|
||||
JoltPhysicsWorld m_physics_world;
|
||||
std::unique_ptr<SpatialBackendType> m_spatial_backend;
|
||||
std::unique_ptr<im::InterestSystem<SpatialBackendType>> m_interest_system;
|
||||
|
||||
std::unordered_map<im::InterestId, entt::entity> m_client_entities;
|
||||
std::unordered_map<entt::entity, im::InterestId> m_entity_sessions;
|
||||
std::unordered_map<im::InterestId, uint32_t> m_session_input_frame;
|
||||
std::unordered_map<im::InterestId, uint32_t> m_session_acked_frame;
|
||||
std::unordered_map<entt::entity, im::InterestId> m_entity_sessions; // reverse map
|
||||
std::unordered_map<im::InterestId, uint32_t> m_session_input_frame; // most recent input frame received
|
||||
std::unordered_map<im::InterestId, uint32_t> m_session_acked_frame; // input frame consumed by last tick
|
||||
std::vector<NeighborEntry> m_neighbors;
|
||||
uint32_t m_next_neighbor_id = 0x80000000u;
|
||||
|
||||
@@ -55,23 +57,39 @@ public:
|
||||
|
||||
ZoneManager(im::AreaBounds zone_bounds);
|
||||
|
||||
// ── ZoneProxy interface ───────────────────────────────────────────────────
|
||||
|
||||
// Returns the geographic area this zone owns, derived from the spatial backend bounds.
|
||||
im::AreaBounds area() const override;
|
||||
|
||||
// Receive an entity transferred from a neighbouring zone.
|
||||
// Spawns the entity in this zone's world; wires up input routing if session_id is set.
|
||||
// NOTE: ZoneServer must also update its session→zone routing table after this call.
|
||||
void transfer_entity(EntityInfo&& info) override;
|
||||
|
||||
// ── Zone management ───────────────────────────────────────────────────────
|
||||
|
||||
entt::entity spawn_entity(EntityInfo&& info);
|
||||
|
||||
// Registers an interest for the given entity and begins tracking entities near their position.
|
||||
// Uses session_id as the InterestId; the interest system is unaware of this mapping.
|
||||
// Can fetch the interest using `get_interest(interest_id)`.
|
||||
void add_client(im::InterestId interest_id, entt::entity entity);
|
||||
|
||||
void on_player_move(SessionId session_id, mmo::PlayerMoveMessage&& message);
|
||||
|
||||
// Registers a neighbouring zone. Its geographic area is added to the interest
|
||||
// manager and checked each tick; entities that cross into it are transferred.
|
||||
void register_neighbor_zone(ZoneProxy* neighbor);
|
||||
|
||||
// Returns the current interest state for any registered id, or nullptr.
|
||||
const im::Interest* get_interest(im::InterestId id) const;
|
||||
|
||||
// Returns the input frame that this session's entity was last simulated with, or 0 if unknown.
|
||||
uint32_t acked_input_frame(im::InterestId interest_id) const;
|
||||
|
||||
void update(uint32_t frame_idx, float delta_time);
|
||||
// Runs one tick: interest queries, neighbour transfer checks, world step, physics step.
|
||||
void tick(uint32_t frame_idx, float delta_time);
|
||||
};
|
||||
|
||||
} // namespace tw::net
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user