返回管理系统
插件架构
插件以 Windows DLL 运行,由 RCON 客户端主程序动态加载。插件有两种数据来源:
| 数据来源 | 获取方式 | 内容 | 编码 |
| RCON 响应 | plugin_on_response() | 玩家聊天、ListPlayers、命令确认 | GBK |
| 服务器日志 | Logcore 回调 on_log_line() | 击杀、击倒、伤害、换图 | GBK |
RCON 不推送击杀事件!击杀/伤害等游戏事件只在日志文件中。检测这些事件 必须使用 Logcore 回调。
必须导出的函数
C
__declspec(dllexport) void plugin_init(void (*send_command)(const char *cmd));
__declspec(dllexport) void plugin_on_response(const char *response);
可选导出(支持云端配置热重载)
C__declspec(dllexport) void plugin_on_config_change(const char *config_json);
__declspec(dllexport) const char* plugin_get_default_config(void);
__declspec(dllexport) const char* plugin_get_config_schema(void);
编译
BASHgcc -shared -o my_plugin.dll my_plugin.c -lws2_32 -lwininet
编码说明
| 场景 | 编码 | 注意 |
| plugin_on_response | GBK | 主程序已转换 |
| Logcore 回调 | GBK | Logcore 已转换 |
| send_command | GBK | Squad RCON 使用 GBK |
| API 的 reason / player_name | UTF-8 + URL编码 | 必须先 GBK→UTF-8 再 URL编码 |
C - 编码转换
static void gbk_to_utf8(const char*g,char*u,size_t sz){
int w=MultiByteToWideChar(CP_ACP,0,g,-1,NULL,0);
wchar_t*b=(wchar_t*)malloc(w*sizeof(wchar_t));
MultiByteToWideChar(CP_ACP,0,g,-1,b,w);
WideCharToMultiByte(CP_UTF8,0,b,-1,u,(int)sz,NULL,NULL);
free(b);
}
static void url_encode(const char*s,char*d,size_t sz){
static const char*h="0123456789ABCDEF"; size_t p=0;
while(*s&&p<sz-4){
unsigned char c=*s;
if(isalnum(c)||c=='-'||c=='_'||c=='.'||c=='~')d[p++]=*s;
else{d[p++]='%';d[p++]=h[c>>4];d[p++]=h[c&0xF];}
s++;
} d[p]='\0';
}
Logcore 日志回调
Logcore 是独立日志核心插件 (Logcore.dll),读取 Squad 日志文件并通过回调通知你的插件。
工作原理:打开日志 → 定位最后一次 StartNewGame → tail -f 实时读取 → UTF-8 转 GBK → 回调你的函数。
C - 完整注册流程(参考悬赏插件 zxs.c)typedef void (*LogLineCallback)(const char* line);
static void (*g_register_log_callback)(LogLineCallback cb) = NULL;
static void on_log_line(const char* lineGBK) {
if (!lineGBK) return;
if (strstr(lineGBK, "[DedicatedServer]Die():")) { }
if (strstr(lineGBK, "[DedicatedServer]Wound():")) { }
if (strstr(lineGBK, "LogSquad: StartNewGame")) { }
}
__declspec(dllexport) void plugin_init(void (*send_command)(const char*)) {
g_send_command = send_command;
HMODULE h = GetModuleHandleA("Logcore.dll");
if (h) {
g_register_log_callback = (void(*)(LogLineCallback))
GetProcAddress(h, "plugin_register_log_callback");
if (g_register_log_callback) g_register_log_callback(on_log_line);
}
}
BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID l) {
if (r == DLL_PROCESS_DETACH && g_register_log_callback)
g_register_log_callback(NULL);
return TRUE;
}
线程安全 —— RCON 命令队列
Logcore 回调在其读取线程执行,不能直接调用 g_send_command。使用命令队列中转:
C#define MQ 256
#define ML 512
static char g_q[MQ][ML]; static int g_h=0,g_t=0; static CRITICAL_SECTION g_lk;
static void enqueue_rcon(const char*c){
EnterCriticalSection(&g_lk);
int n=(g_t+1)%MQ;
if(n!=g_h){strncpy(g_q[g_t],c,ML-1);g_t=n;}
LeaveCriticalSection(&g_lk);
}
static void flush_rcon_queue(){
char c[ML]; EnterCriticalSection(&g_lk);
while(g_h!=g_t){
strncpy(c,g_q[g_h],ML-1); g_h=(g_h+1)%MQ;
LeaveCriticalSection(&g_lk);
if(g_send_command) g_send_command(c);
EnterCriticalSection(&g_lk);
} LeaveCriticalSection(&g_lk);
}
__declspec(dllexport) void plugin_on_response(const char*r){
flush_rcon_queue();
}
插件 API (client_*) —— license_key 认证
DLL 插件专用 API。license_key 由主程序自动注入,无需额外申请。
主程序加载插件后,会通过 plugin_on_response 发送 LICENSE_KEY:xxxx。保存即可使用所有 client_* 接口。
Cstatic char g_license_key[64] = "";
__declspec(dllexport) void plugin_on_response(const char*r) {
if (strncmp(r,"LICENSE_KEY:",12)==0) { strncpy(g_license_key,r+12,63); return; }
}
所有请求: POST http://plugin.squad.cyou/api.php?action=<action>
Body: action=xxx&license_key=xxx&... Content-Type: application/x-www-form-urlencoded
| 参数 | 类型 | 必填 | 说明 |
| license_key | string | 是 | 服务器激活码 |
| steamid | string | 是 | 玩家SteamID |
响应{ "success":true, "points":1500, "exists":true }
| 参数 | 类型 | 必填 | 说明 |
| license_key | string | 是 | 服务器激活码 |
| steamid | string | 是 | SteamID |
| amount | int | 是 | 数量(正整数) |
| reason | string | 否 | 原因(UTF-8 + URL编码) |
| player_name | string | 否 | 玩家名,不存在自动创建 |
| 参数 | 类型 | 必填 | 说明 |
| license_key | string | 是 | 服务器激活码 |
| steamid | string | 是 | SteamID |
| amount | int | 是 | 数量(正整数) |
| reason | string | 否 | 原因 |
| 参数 | 类型 | 必填 | 说明 |
| license_key | string | 是 | 激活码 |
| steamid | string | 是 | SteamID |
| reward | int | 否 | 奖励,默认10 |
| cooldown | int | 否 | 冷却秒,默认86400 |
| 参数 | 类型 | 必填 | 说明 |
| license_key | string | 是 | 激活码 |
| page | int | 否 | 页码 |
| page_size | int | 否 | 每页数量 |
API Key 申请 & 认证
所有 dev_* 接口需要有效的 API Key。申请后经管理员审批并绑定服务器方可使用。
传递方式(任选其一):
1. 请求头: X-API-KEY: rcon_xxxx
2. GET 参数: ?api_key=rcon_xxxx
3. POST 参数: api_key=rcon_xxxx
注意:dev_* 的 amount 使用 intval() 转换,只接受正整数。传 "0.03" 会被截断为 0 并被拒绝。
服务器 & 玩家
cURLcurl "https://plugin.squad.cyou/api.php?action=dev_get_servers" \
-H "X-API-KEY: rcon_xxxx"
响应{"success":true,"data":[{"server_id":"uuid...","name":"服务器","status":"active"}]}
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| player_name | string | 否 | 名称 |
| points | int | 否 | 初始积分 |
积分操作
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| amount | int | 是 | 数量(正整数) |
| reason | string | 否 | 原因 |
| player_name | string | 否 | 名称 |
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| amount | int | 是 | 数量 |
| reason | string | 否 | 原因 |
余额不足时,如果有跨服共享关系,会自动从共享服务器扣除差额。
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| points | int | 是 | 目标值 |
| reason | string | 否 | 原因 |
签到 & 排行
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| steamid | string | 是 | SteamID |
| reward | int | 否 | 奖励,默认10 |
| cooldown | int | 否 | 冷却秒,默认86400 |
| 参数 | 类型 | 必填 | 说明 |
| server_id | string | 是 | 服务器ID |
| page | int | 否 | 页码 |
| page_size | int | 否 | 每页数量 |
代码示例
Python
PYTHONimport requests
headers = {"X-API-KEY": "rcon_xxxx"}
BASE = "https://plugin.squad.cyou/api.php"
r = requests.get(BASE, headers=headers, params={
"action": "dev_get_points",
"server_id": "uuid", "steamid": "765..."
})
print(r.json())
JavaScript
JSconst r = await fetch(
`https://plugin.squad.cyou/api.php?action=dev_get_servers`,
{ headers: { 'X-API-KEY': 'rcon_xxxx' } }
);
console.log(await r.json());
PHP
PHP$ch = curl_init("https://plugin.squad.cyou/api.php");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-KEY: rcon_xxxx"],
CURLOPT_POSTFIELDS => http_build_query([
'action'=>'dev_add_points', 'server_id'=>'uuid',
'steamid'=>'765...', 'amount'=>100
])
]);
$r = json_decode(curl_exec($ch), true);
错误处理
| 错误 | 原因 | 解决 |
API密钥无效 | Key 不存在 | 检查拼写 |
API密钥待审核 | 未审批 | 联系管理员 |
此API密钥无权访问该服务器 | 未绑定 | 请管理员绑定 |
激活码为空 | 未传 license_key | 确认主程序已注入 |
参数不完整或金额无效 | amount ≤ 0 | 传正整数 |
积分不足 | 余额不够 | 先查余额 |
常见陷阱
| 陷阱 | 后果 | 正确做法 |
Logcore 回调中直接 g_send_command | 跨线程崩溃 | enqueue_rcon() + flush_rcon_queue() |
amount 传小数 "0.03" | intval→0,被拒绝 | 只传正整数 |
| reason 直接传 GBK | 服务端乱码 | GBK→UTF-8→URL编码 |
| 全文 strstr 匹配命令 | 玩家名含关键字误触 | 精确匹配命令前缀 |
| 卸载时不注销 Logcore 回调 | 调用已释放内存 | g_register_log_callback(NULL) |
调用 dev_deduct_points | "未知操作" | 正确名: dev_subtract_points |