Skip to content

Repository files navigation

Collision Detection Server

Version C++ Bullet CI License: MIT

A compact, cross-platform TCP service that performs raycasts against large static OBJ scenes. Geometry is loaded into Bullet Physics BVH triangle meshes, and each request returns the names and coordinates of all intersected objects.

The server is intended for game backends, emulators and other services that need line-of-sight or collision queries without embedding a physics engine in every application.

Status: pre-release (0.1.x). The core server, bounded worker pool, protocol validation, graceful shutdown and structured logging are ready. Keep the service on a trusted network until authentication and transport encryption are provided by an upstream proxy.

Features

  • static OBJ scene loading with large-index support;
  • Bullet Physics BVH raycasting;
  • compact, endian-stable binary protocol;
  • bounded connection queue and fixed-size worker pool;
  • configurable bind address, port and I/O timeout;
  • structured JSON Lines logs;
  • graceful SIGINT and SIGTERM shutdown;
  • protocol unit tests, network integration tests, sanitizers and fuzzing;
  • reproducible Linux and Windows release archives.

Requirements

  • CMake 3.20 or newer;
  • a C++17 compiler;
  • Git;
  • Git LFS when cloning the bundled lf1.obj product fixture.

Bullet Physics 3.25 is pinned and fetched automatically during configuration.

Quick start

git lfs install
git clone https://github.com/SyntaxGameCode/aion-obj-bullet-collision-detection.git
cd aion-obj-bullet-collision-detection

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure

./build/Bullet3RaycastServer/collision-detection-server \
  --data data \
  --bind 127.0.0.1 \
  --port 27015 \
  --log-level info

Run collision-detection-server --help to see every option. The defaults are the data scene list, 0.0.0.0:27015, a five-second client I/O timeout, four workers and 64 queued connections.

For local integrations, bind to 127.0.0.1. Binding to 0.0.0.0 exposes the service on every IPv4 interface.

Using a system Bullet installation

cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Release \
  -DCOLLISION_USE_SYSTEM_BULLET=ON

Scene data

Each whitespace-delimited entry in data.ini names an OBJ file under the data directory:

lf1.obj

The OBJ object declaration (o name) becomes the hit name returned by the server. Release archives include lf1.obj as the product test fixture.

Using the server from Python

Start the server from the unpacked release directory. Keep data.ini and the data directory next to bin:

# Linux
./bin/collision-detection-server --bind 127.0.0.1 --port 27015

# Windows (PowerShell)
.\bin\collision-detection-server.exe --bind 127.0.0.1 --port 27015

The client sends a line segment from start to end. The scene value must exactly match a filename listed in data.ini; the bundled scene is lf1.obj. This example uses only the Python standard library:

import socket
import struct


ERRORS = {
    0xFC: "start and end points are identical",
    0xFD: "scene was not found",
    0xFE: "request is malformed",
}


def receive_exact(connection: socket.socket, size: int) -> bytes:
    data = b""
    while len(data) < size:
        chunk = connection.recv(size - len(data))
        if not chunk:
            raise ConnectionError("server closed the connection")
        data += chunk
    return data


def receive_frame(connection: socket.socket) -> bytes:
    header = receive_exact(connection, 3)
    marker, size = struct.unpack("!BH", header)
    if marker != 0xF0 or size < 5:
        raise ValueError("invalid response header")
    return header + receive_exact(connection, size - 3)


def raycast(
    scene: str,
    start: tuple[float, float, float],
    end: tuple[float, float, float],
    host: str = "127.0.0.1",
    port: int = 27015,
) -> list[tuple[str, tuple[float, float, float]]]:
    scene_bytes = scene.encode("utf-8")
    message_size = 54 + len(scene_bytes)
    request = (
        struct.pack("!BHH", 0xF0, message_size, len(scene_bytes))
        + scene_bytes
        + struct.pack("!6d", *start, *end)
        + b"\xFF"
    )

    with socket.create_connection((host, port), timeout=5) as connection:
        connection.sendall(request)
        response = receive_frame(connection)

    if response[-1] != 0xFF:
        raise ValueError("invalid response end marker")

    # Error frames are: F0 00 05 <error code> FF.
    if len(response) == 5:
        code = response[3]
        raise RuntimeError(ERRORS.get(code, f"unknown server error 0x{code:02X}"))

    _, total_size, hit_count = struct.unpack("!BHH", response[:5])
    if total_size != len(response):
        raise ValueError("invalid response size")

    offset = 5
    names = []
    for _ in range(hit_count):
        name_size = struct.unpack_from("!H", response, offset)[0]
        offset += 2
        names.append(response[offset : offset + name_size].decode("utf-8"))
        offset += name_size

    points = []
    for _ in range(hit_count):
        points.append(struct.unpack_from("!3d", response, offset))
        offset += 24

    if offset != len(response) - 1:
        raise ValueError("unexpected data in response")

    return list(zip(names, points))


hits = raycast(
    scene="lf1.obj",
    start=(100.0, 100.0, 100.0),
    end=(100.0, 100.0, -100.0),
)

if not hits:
    print("No collisions")
else:
    for object_name, point in hits:
        print(f"Hit {object_name!r} at x={point[0]:.3f}, y={point[1]:.3f}, z={point[2]:.3f}")

The result is a list of (object_name, (x, y, z)) tuples. An empty list means that the segment did not intersect any object. The example coordinates are only illustrative; use coordinates from the same coordinate system as the exported OBJ scene.

Configuration

Option Default Description
--data <name> data Scene-list filename without the .ini suffix
--bind <address> 0.0.0.0 IPv4 address to listen on
--port <port> 27015 TCP port
--io-timeout-ms <ms> 5000 Per-client socket timeout
--workers <count> 4 Connection worker count
--max-queued-connections <count> 64 Bounded accepted-socket queue
--log-level <level> info error, warn, info or debug

Linux and MSVC builds use the worker pool. MinGW builds using the legacy win32 thread model fall back to single-client processing because that runtime does not provide the required C++ standard threading API.

Logging

Logs are written to stderr as one JSON object per line. Every record includes an UTC timestamp, severity, event name and thread identifier. Debug logging also records connection and request details, response size and processing duration.

{"timestamp":"2026-07-18T12:00:00.000Z","level":"info","event":"server_started","thread":"1","bind":"127.0.0.1","port":"27015","workers":"4","log_level":"info"}

Wire protocol

All integers and IEEE-754 doubles use big-endian byte order.

Request

Field Size
Start marker (0xF0) 1 byte
Total message size 2 bytes
Scene-name size 2 bytes
Scene name (UTF-8 bytes) variable
Start point X, Y, Z 24 bytes
End point X, Y, Z 24 bytes
End marker (0xFF) 1 byte

The maximum scene-name size is 1024 bytes. Coordinates must be finite, and the start and end points must differ.

Successful response

Field Size
Start marker (0xF0) 1 byte
Total message size 2 bytes
Hit count 2 bytes
Repeated name sizes and names variable
Repeated hit X, Y, Z values 24 bytes per hit
End marker (0xFF) 1 byte

Error responses are five bytes:

  • F0 00 05 FC FF — identical points;
  • F0 00 05 FD FF — unknown scene;
  • F0 00 05 FE FF — malformed request.

Testing and fuzzing

Standard tests:

ctest --test-dir build --output-on-failure

Protocol fuzzing with Clang and libFuzzer:

cmake -S . -B build-fuzz -G Ninja \
  -DCMAKE_C_COMPILER=clang \
  -DCMAKE_CXX_COMPILER=clang++ \
  -DBUILD_TESTING=OFF \
  -DCOLLISION_BUILD_FUZZERS=ON
cmake --build build-fuzz --target protocol-fuzzer
./build-fuzz/Bullet3RaycastServer/protocol-fuzzer -runs=20000

Releases

GitHub Actions builds and tests every push and pull request. Pushing a semantic version tag creates a GitHub Release with Linux x86-64 and Windows x86-64 archives:

git tag v0.1.0
git push origin v0.1.0

The release workflow can also be run manually to verify packaging without publishing a GitHub Release.

Versioning

The project follows Semantic Versioning. Release tags use the vMAJOR.MINOR.PATCH form. The current development version is 0.1.0; the public API and wire protocol may change before 1.0.0.

Security

The server intentionally does not implement authentication or TLS. Bind it to a private interface or place it behind a trusted authenticated proxy. Please report security issues privately to the project maintainer instead of opening a public issue.

License

This project is available under the MIT License. Bullet Physics is distributed under its own zlib license.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages