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.
- 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
SIGINTandSIGTERMshutdown; - protocol unit tests, network integration tests, sanitizers and fuzzing;
- reproducible Linux and Windows release archives.
- CMake 3.20 or newer;
- a C++17 compiler;
- Git;
- Git LFS when cloning the bundled
lf1.objproduct fixture.
Bullet Physics 3.25 is pinned and fetched automatically during configuration.
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 infoRun 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.
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCOLLISION_USE_SYSTEM_BULLET=ONEach 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.
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 27015The 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.
| 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.
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"}All integers and IEEE-754 doubles use big-endian byte order.
| 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.
| 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.
Standard tests:
ctest --test-dir build --output-on-failureProtocol 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=20000GitHub 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.0The release workflow can also be run manually to verify packaging without publishing a GitHub Release.
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.
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.
This project is available under the MIT License. Bullet Physics is distributed under its own zlib license.