Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -699,3 +699,6 @@ FodyWeavers.xsd

# Selected Background
/game/neo/scripts/[Cc]hapter[Bb]ackgrounds.txt

# Developer-local tools (the hot reload sidecar lives in .ide/bin)
/.ide/
52 changes: 52 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,58 @@ It's recommended you fork [the master branch](https://github.com/NeotokyoRebuild

See [README.md](README.md) in this repo for setting up your build environment (currently supporting Windows/Linux).

### Hot reload (Linux)

Edit C++ while the game runs: keep `make watch` running in your build shell and
run the game through Steam. Saving a source file recompiles just that
translation unit and applies it to the live process in a few seconds.

Setup: follow the [ntre-build-hot-reloader Quick start](https://github.com/sunmachine/ntre-build-hot-reloader#quick-start) (checkout next to
this repo or `RELOADER_DIR=`, Rust toolchain with the musl target, sniper image),
then

```bash
make watch-configure # once: the neo-sniper container, the sidecar binary, the hotreload preset
make watch # every session
```

`make watch-configure` needs Podman: it creates a persistent `neo-sniper`
container from the sniper image that mounts your home at the host path and runs
as your uid, or `CONTAINER=` points at an existing Toolbx or distrobox container.
Each step is skipped when already done.

Both Steam launch methods from the README work; the loader finds the build tree
in either layout:

* Source SDK Base 2013 Multiplayer with launch options
`%command% -insecure -dev -game /path/to/your/repo/game/neo`
* the "Neotokyo: Rebuild" sourcemod entry (steamapps/sourcemods symlink or bind
mount of `game/neo`), with `-insecure -dev` as its launch options

`make build` returns to the `linux-debug` preset. See `src/Makefile` (`make help`).
In-game: `sv_neo_hot_reload_status`,
`sv_neo_hot_reload` (apply when `sv_neo_hot_reload_auto` is 0); `cl_` variants
for client.so. The sidecar in `.ide/bin/` and the vendored loader under
`src/game/shared/neo/hotreload/vendor/` (`VENDORED.md` names the revision) come
from the same reloader repo; keep them in step.

To catch a crash with symbols while the game runs, attach the host's gdb by pid
(`ptrace_scope` is 0 here) and launch with `-noassert`, or a pre-existing bot
animation assert opens a modal dialog under a debugger.

What hot reload does not do, by design: change a type's layout or vtable,
apply edits to `BEGIN_DATADESC` / send and receive tables / prediction maps
(they are built once at static init), or reload a header that dirties more
than `--max-tus` units. Each of those is a rebuild. The reloader README lists
the full set under Limits.

Troubleshooting `make watch-configure`:

* `crun: ptsname: Inappropriate ioctl for device`: transient Podman error on the
first exec after boot; re-run the same command.
* `sidecar missing and no reloader checkout`: clone the reloader next to this
repo or set `RELOADER_DIR=`.

### Debugging
To be safe and avoid problems with VAC, it's recommended to add a [-insecure](https://developer.valvesoftware.com/wiki/Command_Line_Options) launch flag before attaching your debugger.

Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Convenience aliases only. Every target here forwards to src/Makefile, and
# src/Makefile in turn only wraps the CMake presets in src/CMakePresets.json.
# Nothing in either Makefile is part of the build itself: the CMake layer is
# the build, and cmake --preset / cmake --build --preset stay the primary,
# supported way to configure and build. Do not add build logic here.

%:
@$(MAKE) -C src $@

.DEFAULT_GOAL := help
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ $ cmake --build --preset PRESET_NAME

Available PRESET_NAME values: `windows-debug`, `windows-release`, `linux-debug`, `linux-release`.

#### Hot reload (Linux)
Edit C++ while the game runs. Setup is the [ntre-build-hot-reloader Quick start](https://github.com/sunmachine/ntre-build-hot-reloader#quick-start):
check out that repo next to this one, install its Rust toolchain, then

```
$ make watch-configure # once
$ make watch # every session
```

and launch the game from Steam with `-insecure -dev`. Needs Podman with the sniper image (see [Linux prerequisite](#linux-prerequisite---steam-runtime-3-sniper-container)). More in [CONTRIBUTING.md](CONTRIBUTING.md#hot-reload-linux).

## Steam mod setup
To make it appear in Steam, the install files have to appear under the sourcemods directory or
be directed to it.
Expand Down
74 changes: 72 additions & 2 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ option(NEO_GENERATE_GAMEDATA "Generate SourceMod gamedata" ${NEO_DEDICATED})
option(NEO_BUILD_LAUNCHER "Build the Steam mod launcher" ON)
option(NEO_UNITY_BUILD_CLIENT_SERVER "Enable unity build for client/server libraries" ON)
option(NEO_UNITY_BUILD_OTHERS "Enable unity build for vgui2, tier1, and mathlib libraries" ON)
option(NEO_ENABLE_LINUX_HOT_RELOAD "Enable the Linux hot reload loader in client and server (Debug, non-unity, Linux only)" OFF)

if(NEO_ENABLE_LINUX_HOT_RELOAD)
if(NOT OS_LINUX OR NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
message(FATAL_ERROR "NEO_ENABLE_LINUX_HOT_RELOAD needs OS_LINUX and CMAKE_BUILD_TYPE Debug")
endif()
if(NEO_UNITY_BUILD_CLIENT_SERVER)
message(FATAL_ERROR "NEO_ENABLE_LINUX_HOT_RELOAD needs NEO_UNITY_BUILD_CLIENT_SERVER=OFF (the sidecar maps saves to per-TU objects)")
endif()
if(NEO_USE_SEPARATE_BUILD_INFO)
message(FATAL_ERROR "NEO_ENABLE_LINUX_HOT_RELOAD needs NEO_USE_SEPARATE_BUILD_INFO=OFF (the loader reads the deployed module's .symtab)")
endif()
if(NEO_DEDICATED)
message(FATAL_ERROR "NEO_ENABLE_LINUX_HOT_RELOAD does not support NEO_DEDICATED (module must be named server.so)")
endif()
# The loader receives the build dir relative to the deployed module's directory,
# so the same binary works from any absolute repo location (host vs dev container).
get_filename_component(NEO_HR_OUTPUT_ABS "${NEO_OUTPUT_LIBRARY_PATH}" ABSOLUTE)
file(RELATIVE_PATH NEO_HOT_RELOAD_BUILD_DIR_REL "${NEO_HR_OUTPUT_ABS}" "${CMAKE_BINARY_DIR}")
message(STATUS "Linux hot reload: ON (mailbox at <module dir>/${NEO_HOT_RELOAD_BUILD_DIR_REL}/.hotreload)")
endif()
set(NEO_MOD_APPID "3172910" CACHE STRING "Steam appid for the mod launcher's steam_appid.txt")

message(STATUS "Treat compile warnings as errors: ${CMAKE_COMPILE_WARNING_AS_ERROR}")
Expand Down Expand Up @@ -295,7 +316,24 @@ if(OS_WINDOWS)
endif()

if(OS_LINUX OR OS_MACOS)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
if(NEO_ENABLE_LINUX_HOT_RELOAD)
# Default visibility so shims can bind the module's definitions by name.
# -falign-functions=16 guarantees room for the entry hook, -fno-gnu-unique
# avoids process-wide STB_GNU_UNIQUE bindings that would pin a shim,
# -fdata-sections gives every static its own named section so the loader
# can share it exactly.
add_compile_options(
-falign-functions=16
-fno-gnu-unique
-fdata-sections
# fmtstr.h pins its classes hidden with a visibility pragma; under
# default visibility every exported class with such a field trips
# -Wattributes, which is noise for this dev-only preset.
-Wno-attributes
)
else()
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
endif()

# Set default optimization option to O2 instead of O3
string(REGEX REPLACE "([\\/\\-]O)3" "\\12" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
Expand Down Expand Up @@ -396,11 +434,21 @@ if(OS_LINUX OR OS_MACOS)

# We should always specify -Wl,--build-id, as documented at:
# http://linux.die.net/man/1/ld and http://fedoraproject.org/wiki/Releases/FeatureBuildId
if(NEO_ENABLE_LINUX_HOT_RELOAD)
# Same script minus the operator* localization (shims resolve the game's
# operator overloads from the module); -Bsymbolic keeps each module bound
# to its own definitions under default visibility.
set(NEO_VERSION_SCRIPT "${CMAKE_SOURCE_DIR}/version_script.linux.hotreload.txt")
add_link_options(-Wl,-Bsymbolic)
else()
set(NEO_VERSION_SCRIPT "${CMAKE_SOURCE_DIR}/version_script.linux.txt")
endif()

add_link_options(
-Wl,--build-id
-static-libgcc
-Wl,--no-undefined
-Wl,--version-script=${CMAKE_SOURCE_DIR}/version_script.linux.txt
-Wl,--version-script=${NEO_VERSION_SCRIPT}
)

# Fix undefined references in static libraries
Expand All @@ -412,6 +460,28 @@ if(OS_LINUX)
set(LIBPUBLIC "${CMAKE_SOURCE_DIR}/${LIBPUBLIC_RELATIVE_PATH}")
set(LIBCOMMON "${CMAKE_SOURCE_DIR}/lib/common/${PLATSUBDIR}")

if(NEO_ENABLE_LINUX_HOT_RELOAD)
# The prebuilt SDK archives were compiled with hidden visibility, so
# their globals (mdlcache, materials, g_pParticleSystemMgr, ...) would
# stay out of the module's dynamic symbol table and a shim rebuilt from
# a translation unit that references one could not link. Link this
# preset against byte-identical copies whose GLOBAL/WEAK symbols are
# promoted to default visibility; st_other is the only changed byte.
set(NEO_HR_LIBPUBLIC "${CMAKE_BINARY_DIR}/libpublic-hotreload")
execute_process(
COMMAND python3 "${CMAKE_SOURCE_DIR}/../tools/hotreload-default-visibility.py"
"${LIBPUBLIC}" "${NEO_HR_LIBPUBLIC}"
RESULT_VARIABLE NEO_HR_LIBPATCH_RESULT
OUTPUT_VARIABLE NEO_HR_LIBPATCH_OUTPUT
ERROR_VARIABLE NEO_HR_LIBPATCH_OUTPUT
)
if(NOT NEO_HR_LIBPATCH_RESULT EQUAL 0)
message(FATAL_ERROR "hot reload library visibility patch failed:\n${NEO_HR_LIBPATCH_OUTPUT}")
endif()
message(STATUS "Linux hot reload: linking against ${NEO_HR_LIBPUBLIC}")
set(LIBPUBLIC "${NEO_HR_LIBPUBLIC}")
endif()

add_compile_definitions(
LINUX
_LINUX
Expand Down
16 changes: 16 additions & 0 deletions src/CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "linux-debug-hotreload",
"displayName": "Linux Debug (hot reload)",
"inherits": "linux-debug",
"cacheVariables": {
"NEO_ENABLE_LINUX_HOT_RELOAD": "ON",
"NEO_UNITY_BUILD_CLIENT_SERVER": "OFF",
"NEO_UNITY_BUILD_OTHERS": "OFF",
"NEO_USE_CCACHE": "OFF"
}
},
{
"name": "linux-release",
"displayName": "Linux Release",
Expand All @@ -88,6 +99,11 @@
"displayName": "Linux Debug",
"configurePreset": "linux-debug"
},
{
"name": "linux-debug-hotreload",
"displayName": "Linux Debug (hot reload)",
"configurePreset": "linux-debug-hotreload"
},
{
"name": "linux-release",
"displayName": "Linux Release",
Expand Down
104 changes: 104 additions & 0 deletions src/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Convenience aliases over the CMake presets in CMakePresets.json. This
# Makefile is not a build system and must not become one: the CMake layer is
# the build, and cmake --preset / cmake --build --preset remain the primary,
# supported way to configure and build. The aliases only add what CMake
# cannot express on its own: running the toolchain inside the build container
# (the host has no cmake or ninja), recording which preset is applied, and
# launching the hot reload sidecar. Anything that affects what gets compiled
# or how belongs in CMakeLists.txt / CMakePresets.json, not here.
#
# make build [PRESET=linux-debug] configure if needed, build, deploy, record the applied preset
# make watch [MODE=save|trigger] ensure the linux-debug-hotreload preset is applied, run the sidecar
# make watch-configure one-time hot reload setup: container, sidecar, preset
# make configure [PRESET=...] configure only
# make status applied preset and sidecar view of the mailbox
# make clean [PRESET=...] remove that preset's build dir
#
# The linux-debug and linux-debug-hotreload presets use separate build dirs and
# both deploy to game/neo/bin/linux64, so switching presets is just make build /
# make watch; .ntre-applied-preset records which one is live.

PRESET ?= linux-debug
WATCH_PRESET ?= linux-debug-hotreload
MODE ?= save
JOBS ?= 12
CONTAINER ?= neo-sniper
SDK_DIR ?= $(HOME)/.steam/steam/steamapps/common/Source SDK Base 2013 Multiplayer

REPO_ROOT := $(abspath ..)
DEPLOY_DIR := $(REPO_ROOT)/game/neo/bin/linux64
STAMP := $(DEPLOY_DIR)/.ntre-applied-preset

# The sidecar binary is not vendored; a static musl build works both on the
# host and inside the container. Override with SIDECAR=... or the
# NTRE_HOTRELOAD_SIDECAR env var.
NTRE_HOTRELOAD_SIDECAR ?= $(REPO_ROOT)/.ide/bin/ntre-hr-sidecar
SIDECAR ?= $(NTRE_HOTRELOAD_SIDECAR)
# Checkout of ntre-build-hot-reloader, used by watch-configure to build the sidecar.
RELOADER_DIR ?= $(REPO_ROOT)/../ntre-build-hot-reloader
SNIPER_IMAGE ?= registry.gitlab.steamos.cloud/steamrt/sniper/sdk

# Everything that needs cmake, ninja, g++ or the sidecar runs inside a
# persistent build container that mounts $HOME at the host path (so the repo
# and the Steam SDK are visible) and runs as the host uid; watch-configure
# creates one with plain podman, and a distrobox or toolbox also qualifies.
IN_CONTAINER = podman start $(CONTAINER) >/dev/null && \
podman exec -i $(shell [ -t 0 ] && echo -t) --user $(shell id -u):$(shell id -g) \
-e HOME=$(HOME) -w $(CURDIR) $(CONTAINER)

.PHONY: build watch watch-configure configure status clean help

# One-time setup for make watch. Each step is skipped when already done.
watch-configure:
@podman container exists $(CONTAINER) 2>/dev/null && echo "container: $(CONTAINER)" || { \
real=$$(realpath "$(HOME)"); extra=""; [ "$$real" = "$(HOME)" ] || extra="-v $$real:$$real"; \
podman create --name $(CONTAINER) --userns=keep-id --security-opt label=disable \
-v "$(HOME):$(HOME)" $$extra -v "$(REPO_ROOT):$(REPO_ROOT)" -w "$(REPO_ROOT)" \
$(SNIPER_IMAGE) sleep infinity >/dev/null && \
echo "container: created $(CONTAINER) from $(SNIPER_IMAGE), home and the repo mounted at host paths"; }
@test -x "$(SIDECAR)" && echo "sidecar: $(SIDECAR)" || { \
test -f "$(RELOADER_DIR)/Makefile" || { echo "sidecar missing and no reloader checkout at $(RELOADER_DIR) (set RELOADER_DIR=...)"; exit 1; }; \
$(MAKE) --no-print-directory -C "$(RELOADER_DIR)" sidecar-musl && \
mkdir -p "$(dir $(SIDECAR))" && \
cp "$(RELOADER_DIR)/sidecar/target/x86_64-unknown-linux-musl/release/ntre-hr-sidecar" "$(SIDECAR)" && \
echo "sidecar: built from $(RELOADER_DIR) into $(SIDECAR)"; }
@$(IN_CONTAINER) sh -c 'test -f build/$(WATCH_PRESET)/build.ninja || cmake --preset $(WATCH_PRESET)' && echo "preset: $(WATCH_PRESET) configured"

build:
@$(IN_CONTAINER) sh -c 'test -f build/$(PRESET)/build.ninja || cmake --preset $(PRESET)'
$(IN_CONTAINER) cmake --build --preset $(PRESET) -j $(JOBS)
@printf '%s\n' '$(PRESET)' > $(STAMP)
@echo "deployed: $(PRESET)"

watch:
@test -x "$(SIDECAR)" || { \
echo "sidecar not found at $(SIDECAR)"; \
echo "get it: make -C <ntre-build-hot-reloader> sidecar-musl,"; \
echo "then copy sidecar/target/x86_64-unknown-linux-musl/release/ntre-hr-sidecar"; \
echo "to $(REPO_ROOT)/.ide/bin/ (or set NTRE_HOTRELOAD_SIDECAR)"; \
exit 1; }
@applied=$$(cat $(STAMP) 2>/dev/null || echo none); \
if [ "$$applied" != "$(WATCH_PRESET)" ]; then \
echo "applied preset is $$applied, building $(WATCH_PRESET) first"; \
$(MAKE) --no-print-directory build PRESET=$(WATCH_PRESET); \
else \
echo "deployed: $$applied"; \
fi
$(IN_CONTAINER) "$(SIDECAR)" --build-dir $(CURDIR)/build/$(WATCH_PRESET) watch \
--mode $(MODE) \
--link-dir $(DEPLOY_DIR) \
--link-dir "$(SDK_DIR)/bin/linux64" \
--never-shim game/shared/neo/hotreload/

configure:
$(IN_CONTAINER) cmake --preset $(PRESET)

status:
@echo "deployed: $$(cat $(STAMP) 2>/dev/null || echo none)"
@test -x "$(SIDECAR)" && $(IN_CONTAINER) "$(SIDECAR)" --build-dir $(CURDIR)/build/$(WATCH_PRESET) status || true

clean:
rm -rf build/$(PRESET)

help:
@sed -n '1,19p' Makefile
25 changes: 25 additions & 0 deletions src/game/client/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1965,3 +1965,28 @@ target_sources_grouped(
${CMAKE_SOURCE_DIR}/game/client/NextBot/C_NextBot.cpp
${CMAKE_SOURCE_DIR}/game/client/NextBot/C_NextBot.h
)

if(NEO_ENABLE_LINUX_HOT_RELOAD)
set(NEO_HR_DIR ${CMAKE_SOURCE_DIR}/game/shared/neo/hotreload)
file(GLOB NEO_HR_VENDOR_SOURCES CONFIGURE_DEPENDS ${NEO_HR_DIR}/vendor/src/*.cpp)

target_sources_grouped(
TARGET client
NAME "Hot Reload"
FILES
${NEO_HR_DIR}/neo_hot_reload.cpp
${NEO_HR_DIR}/neo_hot_reload.h
${NEO_HR_VENDOR_SOURCES}
)

target_include_directories(client PRIVATE ${NEO_HR_DIR}/vendor)

target_compile_definitions(client PRIVATE
NEO_LINUX_HOT_RELOAD
NEO_HOT_RELOAD_BUILD_DIR_REL="${NEO_HOT_RELOAD_BUILD_DIR_REL}"
NEO_HOT_RELOAD_BUILD_DIR_ABS="${CMAKE_BINARY_DIR}"
)

# The vendored loader core is plain C++ that does not include the game PCH.
set_source_files_properties(${NEO_HR_VENDOR_SOURCES} PROPERTIES SKIP_PRECOMPILE_HEADERS ON)
endif()
Loading