Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0c3e3ef
Moved changes to public repository.
SaiFi0102 Aug 21, 2015
0875192
Fixed typo in caio readme
SaiFi0102 Aug 21, 2015
8e6e4e3
Merge branch '3.3.5' of https://github.com/TrinityCore/TrinityCore.gi…
SaiFi0102 Aug 21, 2015
89acc9e
Update CAIO_README.md
SaiFi0102 Aug 21, 2015
c1c2709
-Using LuaVal for script key and handler key type
SaiFi0102 Aug 23, 2015
c0cd8c7
Readme typo
SaiFi0102 Aug 23, 2015
f39140a
Fixed a bug with AIOMsg::Add()
SaiFi0102 Aug 24, 2015
dea78cb
Merge TrinityCore/TrinityCore 3.3.5 into CAIO-3.3.5
Rochet2 May 31, 2026
660eeb0
Harden AIO protocol handling and fix reassembly
Rochet2 Jul 29, 2026
57b9728
Update smallfolk_cpp submodule
Rochet2 Jul 29, 2026
f0175de
Add AIO msg cache and block argument limit
Rochet2 Jul 29, 2026
0592ae6
Document AIO server parity
Rochet2 Jul 29, 2026
256051a
Normalize CAIO style to TrinityCore conventions
Rochet2 Jul 29, 2026
a4c201b
Fix GCC CI and compile portability
Rochet2 Jul 29, 2026
51439d4
Modernize CAIO commands and whisper handling
Rochet2 Jul 29, 2026
5f15434
Fix GCC CI visibility and HandleAddonBlock
Rochet2 Jul 29, 2026
123872c
Propagate WITH_CAIO_EXAMPLES for PCH and install
Rochet2 Jul 29, 2026
50ddab2
Harden CAIO review follow-ups and registration
Rochet2 Jul 29, 2026
a0d3e4b
Propagate WITH_CAIO_EXAMPLES and treat nil LuaVal as error
Rochet2 Jul 29, 2026
4c5400a
Merge TrinityCore 3.3.5 into c_aio_new
Rochet2 Jul 29, 2026
c465442
Harden CAIO: ownership, commands, protocol, docs
Rochet2 Jul 29, 2026
50b6b09
Remove TrinityCore GPLv2 banners from CAIO-owned files
Rochet2 Jul 29, 2026
8d6391b
Include Define.h in PlayerAIO.h for TC_GAME_API
Rochet2 Jul 29, 2026
dd35db1
Fix AIOMsg middle-nil packing and WorldSession include
Rochet2 Jul 29, 2026
ce758b9
Port AIO PR #27 framing tests and reassembler helpers
Rochet2 Jul 29, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/linux-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
launcher: ccache
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- name: Set reusable strings
id: strings
shell: bash
Expand Down Expand Up @@ -54,7 +56,7 @@ jobs:
CXX: /usr/bin/${{ matrix.cxx }}
run: >
cmake -GNinja -S ${{ github.workspace }} -B ${{ steps.strings.outputs.build-output-dir }}
-DWITH_WARNINGS=1 -DWITH_WARNINGS_AS_ERRORS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=${{ matrix.pch }} -DUSE_SCRIPTPCH=${{ matrix.pch }} -DTOOLS=1 -DSCRIPTS=dynamic -DSERVERS=1 -DNOJEM=0
-DWITH_WARNINGS=1 -DWITH_WARNINGS_AS_ERRORS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=${{ matrix.pch }} -DUSE_SCRIPTPCH=${{ matrix.pch }} -DTOOLS=1 -DSCRIPTS=dynamic -DSERVERS=1 -DNOJEM=0 -DWITH_CAIO_EXAMPLES=1
-DCMAKE_C_FLAGS_DEBUG="-DNDEBUG -g0" -DCMAKE_CXX_FLAGS_DEBUG="-DNDEBUG -g0"
-DCMAKE_INSTALL_PREFIX=check_install -DBUILD_TESTING=1
- name: Build
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/macos-arm-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ jobs:

steps:
- uses: actions/checkout@v5
with:
submodules: recursive

- name: Set reusable strings
id: strings
Expand Down Expand Up @@ -42,7 +44,7 @@ jobs:
CMAKE_BUILD_TYPE: Debug
run: >
cmake -GNinja -B ${{ steps.strings.outputs.build-output-dir }}
-DWITH_WARNINGS=1 -DWITH_WARNINGS_AS_ERRORS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=1 -DUSE_SCRIPTPCH=1 -DTOOLS=1 -DSCRIPTS=static -DSERVERS=1 -DBUILD_TESTING=1
-DWITH_WARNINGS=1 -DWITH_WARNINGS_AS_ERRORS=1 -DWITH_COREDEBUG=0 -DUSE_COREPCH=1 -DUSE_SCRIPTPCH=1 -DTOOLS=1 -DSCRIPTS=static -DSERVERS=1 -DBUILD_TESTING=1 -DWITH_CAIO_EXAMPLES=1
-DCMAKE_C_FLAGS_DEBUG="-DNDEBUG" -DCMAKE_CXX_FLAGS_DEBUG="-DNDEBUG"
-DCMAKE_INSTALL_PREFIX=check_install
-S ${{ github.workspace }}
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/win-x64-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ jobs:
OPENSSL_ROOT_DIR: C:/libs/openssl
steps:
- uses: actions/checkout@v5
with:
submodules: recursive

- name: Set reusable strings
id: strings
Expand Down Expand Up @@ -74,7 +76,7 @@ jobs:
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
run: >
cmake -GNinja -S ${{ github.workspace }} -B ${{ steps.strings.outputs.build-output-dir }}
-DWITH_WARNINGS_AS_ERRORS=ON -DTOOLS=ON
-DWITH_WARNINGS_AS_ERRORS=ON -DTOOLS=ON -DWITH_CAIO_EXAMPLES=ON

- name: Build
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "dep/smallfolk_cpp/smallfolk_cpp"]
path = dep/smallfolk_cpp/smallfolk_cpp
url = https://github.com/Rochet2/smallfolk_cpp.git
674 changes: 674 additions & 0 deletions CAIO LICENSE.txt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CAIO_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CAIO documentation lives in [doc/CAIO_README.md](doc/CAIO_README.md).

Also see [doc/CAIO_MESSAGE_FORMAT.md](doc/CAIO_MESSAGE_FORMAT.md) and [doc/CAIO_SCRIPT_EXAMPLE.md](doc/CAIO_SCRIPT_EXAMPLE.md).
2 changes: 1 addition & 1 deletion cmake/macros/ConfigureScripts.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ endfunction()

# Stores the project name of the given module in the variable
function(GetProjectNameOfScriptModule module variable)
string(TOLOWER "scripts_${SCRIPT_MODULE}" GENERATED_NAME)
string(TOLOWER "scripts_${module}" GENERATED_NAME)
set(${variable} "${GENERATED_NAME}" PARENT_SCOPE)
endfunction()

Expand Down
1 change: 1 addition & 0 deletions cmake/options.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ foreach(SCRIPT_MODULE ${SCRIPT_MODULE_LIST})
set_property(CACHE ${SCRIPT_MODULE_VARIABLE} PROPERTY STRINGS default disabled static dynamic)
endforeach()

option(WITH_CAIO_EXAMPLES "Build optional CAIO ExampleWindow test script" 0)
option(TOOLS "Build map/vmap/mmap extraction/assembler tools" 1)
option(USE_SCRIPTPCH "Use precompiled headers when compiling scripts" 1)
option(USE_COREPCH "Use precompiled headers when compiling servers" 1)
Expand Down
6 changes: 6 additions & 0 deletions cmake/showoptions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ else()
message("* Build with scripts : No")
endif()

if(WITH_CAIO_EXAMPLES)
message("* CAIO ExampleWindow : Yes")
else()
message("* CAIO ExampleWindow : No (default)")
endif()

if(TOOLS)
message("* Build map/vmap tools : Yes (default)")
else()
Expand Down
5 changes: 5 additions & 0 deletions dep/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ if(SERVERS)
add_subdirectory(mysql)
add_subdirectory(readline)
add_subdirectory(gsoap)
set(SMALLFOLK_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(SMALLFOLK_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE)
set(SMALLFOLK_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(SMALLFOLK_ENABLE_CLANG_TIDY OFF CACHE BOOL "" FORCE)
add_subdirectory(smallfolk_cpp/smallfolk_cpp)
endif()

if(SERVERS AND BUILD_EFSW)
Expand Down
1 change: 1 addition & 0 deletions dep/smallfolk_cpp/smallfolk_cpp
Submodule smallfolk_cpp added at 750580
39 changes: 39 additions & 0 deletions doc/CAIO_MESSAGE_FORMAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# CAIO / AIO message format

This matches [Rochet2 AIO](https://github.com/Rochet2/AIO) block layout as used by CAIO and `smallfolk_cpp`.

## Serialized message

One whisper addon payload is a **smallfolk** dump of an array of blocks:

```lua
{
{ n, scriptKey, handlerKey, arg1, arg2, ... },
{ n, scriptKey2, handlerKey2, ... },
}
```

- `n` — number of arguments **including** `handlerKey` (CAIO sets `block[1]` accordingly). Server rejects `n > 15` (stock `AIO_SERVER` limit).
- `scriptKey` — block name (`AIO`, `AIOExample`, …); must be registered with `AIO.RegisterEvent` on the client or `AIOScript` on the server.
- Arguments from index 4 onward are handler parameters (`unpack(data, 3, n+2)` on the Lua side).

Use `AIOMsg` / `AIO.Msg():Add(...)` rather than building tables manually.

## Wire encoding (3.3.5)

On TrinityCore, AIO uses **`CHAT_MSG_WHISPER` with `LANG_ADDON`** (not a separate addon channel packet). The client still sees **`CHAT_MSG_ADDON`**; this matches stock `AIO.lua` server behaviour.

- Prefix: `S` + `AIO.Prefix` + `\t` (server→client) or `C` + prefix + `\t` (client→server).
- Short message: two bytes `\1\1` then the smallfolk string.
- Long message: 2-byte message id + 2-byte part count + 2-byte part id + chunk.
- Each whisper packet must fit in **255 bytes** total (WoW addon limit). Configure `AIO.MsgMaxLen` (default **255**). Long payloads split using `chunkLen = MsgMaxLen - headerBytes` where `headerBytes = 1 + len(prefix) + 1 + 6`.

## Version handshake

On init, client sends `AIO` / `Init` with protocol version **1.75**. Server defines `AIO_VERSION` / `AIO_VERSION_STRING` in `src/server/game/AIO/AIO.h`. Both sides currently require an exact numeric match (stock AIO `~=` / CAIO `|a-b| > 0.01`). Messaging fixes from [AIO PR #27](https://github.com/Rochet2/AIO/pull/27) do **not** change this wire version.

Future work may introduce semver (`MAJOR.MINOR.PATCH`) with handshake on major+minor only; that is deferred until the AIO client supports it.

## Compression / obfuscation

Not implemented on this branch. Addon files are sent with an `U` (uncompressed) prefix only.
195 changes: 195 additions & 0 deletions doc/CAIO_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
## Introduction

CAIO is a server-client communication system for WoW AddOns. It is an extension of [AIO](https://github.com/Rochet2/AIO) to support C++ server side handling.
AIO is designed for sending lua addons and data between players and server.

Currently CAIO only supports TrinityCore 3.3.5. This work lives on [Rochet2/TrinityCore](https://github.com/Rochet2/TrinityCore) (branch `c_aio_new` / PR #183).

## License

+ TrinityCore base: GPLv2 text in `COPYING`; existing TrinityCore sources keep their per-file headers (**GPLv2 or later**).
+ CAIO: GPLv3 text in `CAIO LICENSE.txt`.
+ CAIO-owned units (e.g. `src/server/game/AIO/*`, `AIOMsg.*`, `AIOScript.*`, `cs_caio.cpp`, `src/server/scripts/AIO/*`, and CAIO tests) intentionally have **no** per-file license banner — same as original SaiFi CAIO; their license is via `CAIO LICENSE.txt`.
+ Because TrinityCore headers permit “or later”, a combined TrinityCore+CAIO tree may be distributed under **GPLv3**.

## Supported AIO version

AIO protocol version **1.75** — must match `AIO_VERSION` in your server and client `AIO.lua` files (see [Rochet2/AIO](https://github.com/Rochet2/AIO)). Init still uses exact float equality (`|a-b| > 0.01`), so stock 1.75 clients keep working.

CAIO mirrors messaging hardening from [AIO PR #27](https://github.com/Rochet2/AIO/pull/27) (cache TTL in ms, hole-safe reassembly, buffered-byte accounting, framing helpers/tests) **without** bumping the Init version — those fixes are wire-compatible.

**Future (not in this branch):** semantic versioning (`MAJOR.MINOR.PATCH`) with Init matching major+minor only so patch releases can diverge. That should land in the AIO client first, then CAIO.

## Security notes (untrusted client → server)

+ Treat all client→server AIO payloads as untrusted: validate block `n`, script/handler keys, sizes, and part metadata before dispatch.
+ Production toggles: `AIO.MsgRateLimitMs`, `AIO.MaxIncomingMessageSize`, `AIO.MaxBlocks`, `AIO.MaxParts`, `AIO.MaxBufferSize`, `AIO.MaxParseFailures`, `AIO.MsgCacheTime` / `AIO.MsgCacheDelay`.
+ Addon file paths are restricted via `IsSafeAddonRelativePath` (no `..`, absolute paths, or shell metacharacters).

## PR #27 parity (already in CAIO vs newly covered)

+ Already present: ms message-cache TTL/sweep, hole-safe long-message completion, per-message `BufferedBytes` caps, middle-`nil` `AIOMsg` packing, path safety, rate/parse abuse gates.
+ This work: extracted framing encode/split + reassembler (unit-tested), basename/expiry helpers, `AIO.ForceReloadOnStartup`, expanded Catch2 coverage mirroring AIO’s framing/reassembler/util tests.

## Install

+ Clone this repository/branch or merge with your own TrinityCore 3.3.5 branch
+ `git submodule update --init --recursive` (required for `dep/smallfolk_cpp/smallfolk_cpp` — [smallfolk_cpp](https://github.com/Rochet2/smallfolk_cpp); pin the submodule commit in production, currently `750580db99847f6043fdf952d572f0c40acc3e55`)
+ Build/Install TrinityCore
+ [Install(Add) (C)AIO scripts](#api-reference) — see also `doc/CAIO_SCRIPT_EXAMPLE.md` and `doc/CAIO_MESSAGE_FORMAT.md`
+ Run SQL files from `TrinityCore_Installation_Dir/sql/CAIO` (`Auth.sql` on auth DB, `World.sql` on world DB)
+ Copy `AIO_Client` from your AIO tree to `WoW_Installation_Dir/Interface/AddOns/AIO_Client` (use the same AIO repo/commit as the server expects)
+ Copy server-side client addon sources into `TrinityCore_Installation_Dir/lua_client_scripts` (one folder per addon, e.g. `lua_client_scripts/ExampleWindow/ExampleWindow.lua`)
+ Set `AIO.MsgMaxLen` to **255** in `worldserver.conf` (WoW addon whisper limit; matches client `AIO.lua` when `AIO_SERVER` is false)
+ Ensure `Logger.AIO` is enabled in the logging config if you want AIO diagnostics
+ Optional: build with `-DWITH_CAIO_EXAMPLES=ON` to include the `ExampleWindow` test script (ships `worldserver/lua_client_scripts/ExampleWindow/ExampleWindow.lua` for install; try `/caioexample` in-game)

## Build notes (TrinityCore 3.3.5 + CAIO)

+ OpenSSL **3.x** is detected by upstream `cmake/macros/FindOpenSSL.cmake`.
+ Boost **1.86** with MSVC may still fail on very old 3.3.5 code paths; **Boost 1.81** is a safe choice if you hit `std::_snprintf` errors in Boost headers.
+ CI builds use recursive submodules and `-DWITH_CAIO_EXAMPLES=1` (see `.github/workflows/linux-build.yml`).

## Stock AIO server parity (C++ vs `AIO.lua` with `AIO_SERVER = true`)

+ **Transport:** `LANG_ADDON` whispers with `S`/`C` prefix are required on 3.3.5; the client receives them as `CHAT_MSG_ADDON`.
+ **Init hooks:** C++ `AddInitArgs` appends extra handler blocks to the init reply. `AddOnInit` on `AIOScript` mutates the full outgoing init table before send (**done** — stock `AIO.AddOnInit` parity).
+ **Pre-init gating:** Stock server does **not** queue pre-init blocks (`AIO_INITED` is client-only). CAIO matches that.
+ **Block arg limit:** Server rejects blocks with invalid `n` (non-integer / out of range / `n > 15`).
+ **Message cache:** `AIO.MsgCacheTime` / `AIO.MsgCacheDelay` match `AIO_MSG_CACHE_TIME` / `AIO_MSG_CACHE_DELAY` in `AIO.lua`.

## Todo (out of scope for v1 unless noted)

+ Implement obfuscation (optional, deferred)
+ Implement compression (optional, deferred)
+ Add individual RBAC permissions per `.caio` subcommand (optional; all subcommands use `RBAC_PERM_COMMAND_CAIO` today)

## API reference

### Creating a CAIO script

```cpp
class ExampleCAIOScript : public AIOScript
{
public:
ExampleCAIOScript()
: AIOScript("ExampleScriptName")
{
using namespace std::placeholders;

// Loads addon files; path from AIO.ClientScriptPath in worldserver.conf
AddAddon("ExampleAddon", "example_addon.lua");
AddAddon("AnotherAddon", "example_addon.lua", 192);

AddHandler("Print", std::bind(&ExampleCAIOScript::HandlePrint, this, _1, _2));
AddInitArgs("ExampleScriptName", "Init", std::bind(&ExampleCAIOScript::InitArg, this, _1));
}

void HandlePrint(Player* sender, LuaVal const& args)
{
LuaVal inputVal = args.get(4);
if (!inputVal.isstring())
return;
}

private:
std::string storedString;
};
```

### smallfolk_cpp LuaVal reference

https://github.com/Rochet2/smallfolk_cpp (v2.x on `master` — linked as CMake target `smallfolk_cpp::smallfolk`, include `smallfolk.h`)

Use `LuaVal::nil` (not `LuaVal::nil()`) for default optional arguments. Type tag accessor is `typetag()`.

### CAIO reference and functions

**AIOScript.h** (included from `ScriptMgr.h`)

```cpp
class AIOScript : public ScriptObject
{
protected:
AIOScript(LuaVal const& scriptKey);
void AddHandler(LuaVal const& handlerKey, HandlerFunc function);
void AddInitArgs(LuaVal const& scriptKey, LuaVal const& handlerKey, ...);
void AddOnInit(InitMessageFunc func); // mutates full init reply (AIO.AddOnInit parity)
bool AddAddon(std::string const& addonName, std::string const& addonFile, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION);
};
```

**AIOMsg.h**

```cpp
class AIOMsg
{
public:
AIOMsg();
AIOMsg(LuaVal const& scriptKey, LuaVal const& handlerKey,
LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil,
LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil);
AIOMsg& Add(LuaVal const& scriptKey, LuaVal const& handlerKey,
LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil,
LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil);
AIOMsg& AppendLast(LuaVal const& a1 = LuaVal::nil, LuaVal const& a2 = LuaVal::nil, LuaVal const& a3 = LuaVal::nil,
LuaVal const& a4 = LuaVal::nil, LuaVal const& a5 = LuaVal::nil, LuaVal const& a6 = LuaVal::nil);
std::string dumps() const;
};
```

**Player.h** / **PlayerAIO.h**

```cpp
// Player.h — no LuaVal in the public player API
void Player::SendSimpleAIOMessage(std::string const& message);
void Player::ForceReloadAddons();
void Player::ForceResetAddons();

// PlayerAIO.h — use from CAIO scripts
namespace Trinity::AIO {
void Message(Player* player, AIOMsg& msg);
void Handle(Player* player, LuaVal const& scriptKey, LuaVal const& handlerKey, ...);
void Handle(Player* player, char const* scriptKey, char const* handlerKey, ...);
}
```

**World.h**

```cpp
struct AIOAddon { /* name, file, permission */ };

std::string GetAIOPrefix() const;
std::string GetAIOClientScriptPath() const;
void ForceReloadPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION);
void ForceResetPlayerAddons(uint32 permission = AIO_DEFAULT_ADDON_PERMISSION);
void AIOMessageAll(AIOMsg& msg, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION);
void SendAllSimpleAIOMessage(std::string const& message, uint32 permission = AIO_DEFAULT_ADDON_PERMISSION);
bool ReloadAddons();
bool AddAddon(AIOAddon const& addon);
bool RemoveAddon(std::string const& addonName, uint32* permission = nullptr);
```

## CAIO game commands

+ `.caio version`
+ `.caio addaddon $addonName "$addonFile" [$permission]`
+ `.caio removeaddon $addonName`
+ `.caio reloadaddons`
+ `.caio forcereload $playerName`
+ `.caio forcereset $playerName`
+ `.caio forcereloadall [$permission]`
+ `.caio forceresetall [$permission]`
+ `.caio send $playerName "Message"`
+ `.caio sendall "Message" [$permission]`

`$permission` is an **exact** RBAC permission id (`HasPermission`), not “permission and above”. Default player permission is typically `195` when omitted.

## Reporting issues and submitting fixes

Issues and pull requests: [Rochet2/TrinityCore](https://github.com/Rochet2/TrinityCore).

## Authors, Contributors & Thanks

+ Saif — CAIO
+ Rochet2 — [AIO](https://github.com/Rochet2/AIO), [smallfolk_cpp](https://github.com/Rochet2/smallfolk_cpp)
Loading
Loading