Build your first plugin
Build the example plugin, load it on a server and hot reload it.
In development
The plugin API is at version 1.6. New minor versions only add functions, so plugins built for an older 1.x keep working. Details on this page can still change before the first release.
A Ready Up plugin is a Linux shared library (.so) in game/csgo/readyup/plugins/. It talks to the server only through a table of C functions that the core hands it. It never touches the engine itself. When CS2 updates, the core gets fixed and your plugin keeps working without a rebuild.
The match flow, practice, skins, the fleet link and the other in-house plugins are all built this way, and use nothing but the public API. This page walks through hello, the example plugin in the repo. It is plain C and about 200 lines.
Repo layout
| Path | What it is |
|---|---|
core/ | The core: loader, engine surface, hooks, events, plugin host, status endpoint. |
core/include/readyup/plugin_api.h | The plugin API. A plugin includes this file. |
core/include/readyup/*_iface.h | Interfaces that plugins publish to each other and to the core. See API reference. |
plugins/match/ | The match plugin, match.so. |
plugins/essentials/, plugins/practice/, plugins/whitelist/, plugins/midas/ | The essentials, practice, whitelist and Midas plugins. |
plugins/skins/ | The skins plugin, skins.so. |
plugins/fleet/ | The fleet link, fleet.so. |
plugins/hello/ | The example plugin. |
libs/readyup/ | Shared code that does not touch the engine (JSON, HTTP, the JSON file store, the status snapshot), as static libraries. |
gamedata/ | engine-surface.json, and engine-surface.skins.json for skins. |
tools/ | readyup_sigcheck, readyup_hookcheck and the plugin host test. |
scripts/livetest/ | Bot-only live tests against a real test server. |
What you need
- Linux, CMake and GCC or Clang.
- A CS2 test server with Ready Up installed, where you can use the console.
sv_hibernate_when_empty 0on that server. Plugins load and reload in server frames, and a hibernating server runs none.
Build hello
git clone https://github.com/Auto-Tournament/ready-up.git
cd ready-up
./build.sh./build.sh runs CMake and builds everything into build/: the core (build/libserver.so) and the plugins (build/plugins/*.so). The fleet plugin needs a libcurl with WebSocket support. Without one, the build skips fleet.so with a warning.
To build only one plugin:
cmake --build build --target readyup_plugin_helloTo build in the same Debian image the dev server uses:
BUILD_TARGET=readyup_plugin_hello scripts/docker-build.sh # -> build-docker/plugins/hello.soLoad it
Copy the file
Copy hello.so to game/csgo/readyup/plugins/hello.so on the server. The file name must match the name the plugin reports, here hello.
Load it
The core loads every *.so in that folder on the first server frame, in name order. On a running server, load it by hand:
ru plugin load helloCheck it
ru plugin listIt shows each plugin with its version, the API version it was built for, and how many commands, ticks and subscriptions it has.
Type .hello in chat, or hello_status in the console. Every 10 seconds the console shows a tick heartbeat line from the plugin.
What hello shows
| Feature | API |
|---|---|
.hello in chat, hello_status in the console | register_chat_command, register_console_command |
| A heartbeat every 10 seconds | on_tick |
| Map, round and player events in the log | subscribe |
| A load counter that survives a reload | stash_put, stash_get |
A greeting setting from [hello] in readyup.cfg or cfg/ReadyUp/hello.cfg | config_get |
| Kills with the weapon | subscribe_game_event("player_death") |
| A count of server log lines | subscribe_log_line |
ru hello in the console, .ru hello in chat | register_ru_subcommand |
.hellohide, which does not show in chat | register_chat_command_ex with RU_CMD_HIDE |
Watching sv_cheats while the engine still runs it | register_console_command_ex with RU_CMD_OBSERVE |
| A frame counter that also runs when the server does not simulate | on_frame |
hello asks for API 1.0 and checks each newer function with RU_API_HAS, so it also loads on an older core.
Hot reload
Reload swaps a plugin for the file on disk without a server restart:
ru plugin reload helloAdmins can also type .ru plugin reload hello in chat. The command only queues the reload. It runs at the start of the next server frame, when no plugin code is running. The console then shows plugin: reloaded hello, or plugin: reload hello failed: ....
Try it: change HELLO_VERSION in hello.c, build, copy, reload. .hello answers with the new version.
When you copy a new file over a loaded one, do not write into the old file. Copy it next to it and rename it into place (mv). The server still has the old file mapped.
A reloaded plugin starts with no state, unless it kept some with stash_put. If the new file fails to load, the plugin stays unloaded. Put the old file back and reload again. See What survives a reload.
dev-deploy.sh
scripts/dev-deploy.sh --plugin <name> does all of this in one step. It builds only that plugin in Docker, copies it to the server, renames it into place, keeps the old file as <name>.so.prev, types ru plugin reload <name> into the server's tmux console, and waits up to 15 seconds for the result.
scripts/dev-deploy.sh --plugin helloIt is written for the Ready Up test box. Point it at your own server with these options (or the environment variables in brackets):
| Option | Default | What it is |
|---|---|---|
--target DIR (RU_TARGET) | /home/cs2servermanager/readyup-test | Server folder, with game/ in it. |
--ssh USER@HOST (RU_SSH) | cs2servermanager@localhost | The user that owns the server files. |
--session NAME (RU_SESSION) | ru-test | The tmux session that runs the server. |
--no-build | Deploy the last build. |
The script reads the result from console.log in the server folder. Changes to the core still need a full deploy and a restart: scripts/dev-deploy.sh --restart. For skins, it also copies engine-surface.skins.json. The core only reads that file at start, so add --restart the first time.
Turning plugins off
ru plugin unload <name>unloads a plugin until the next restart.- Rename the file, for example to
hello.so.off, to stop it from loading at start. READYUP_PLUGINS=0loads no plugins at all.
Tests
Offline tests run without a server:
(cd build && ctest --output-on-failure)They include the plugin host test (it loads hello.so, sends it commands and events, then reloads a second build and checks that the old code is gone), a test that runs match.so in the real plugin host, the status endpoint, and the fleet link against a mock platform.
scripts/livetest/run.sh plays a short bot-only match on a real test server and checks each step in the console log: selftest, match load, warmup, knife round, side pick, live, halftime, map end, postgame, idle. --scrim does the same for a bots-only scrim, using dev_bots_scrim. No one has to join. See scripts/livetest/README.md in the repo.
Your own plugin
Inside the repo, add a folder under plugins/ and register it in CMake:
readyup_add_plugin(myplugin myplugin.c)Then add add_subdirectory(plugins/myplugin) to the top-level CMakeLists.txt. readyup_add_plugin sets the flags a plugin needs: hidden symbols, --no-undefined, a version script that exports only the three entry points, and -fno-gnu-unique for C++. See Threading and ABI.
A plugin exports three functions:
#include "readyup/plugin_api.h"
READYUP_PLUGIN_EXPORT const ru_plugin_info* readyup_plugin_info(void);
READYUP_PLUGIN_EXPORT int readyup_plugin_load(const ru_api* api, uint32_t core_api_version);
READYUP_PLUGIN_EXPORT void readyup_plugin_unload(void);Start from plugins/hello/hello.c. The API reference lists every function.
A plugin built outside the repo only needs plugin_api.h (and any *_iface.h it uses). Build it with the same flags.