| 欢迎, 游客 |
您必须先 注册 才能在我们的网站上发帖.
|
| 在线用户 |
当前共有 12 位在线用户. » 0 会员 | 12 游客
|
| 最新主题 |
PawnREST - HTTP/S 文件传输与 R...
板块: 插件
最后发表: siwode
06-11-2026, 09:59 PM
» 回复: 0
» 浏览: 41
|
网页版 TextDraws在线编辑
板块: 发布
最后发表: siwode
06-11-2026, 09:39 PM
» 回复: 0
» 浏览: 8
|
西部角色扮演 Wild-West-Roleplay
板块: 游戏模式
最后发表: siwode
06-11-2026, 09:19 PM
» 回复: 0
» 浏览: 17
|
[插件] kook-connect
板块: 插件
最后发表: siwode
06-08-2026, 11:04 AM
» 回复: 4
» 浏览: 191
|
openmp/samp联机服务器插件开发 完全指南
板块: 教程
最后发表: 柚子爱吃包子
04-09-2026, 09:14 PM
» 回复: 2
» 浏览: 281
|
圣安地列斯联机二十年:SA:MP 与 open.m...
板块: 综合讨论
最后发表: siwode
03-26-2026, 12:40 PM
» 回复: 3
» 浏览: 345
|
[考古] openmp 常见问题解答 | 2020...
板块: 综合讨论
最后发表: 小鸟unsigned
03-24-2026, 04:55 PM
» 回复: 0
» 浏览: 74
|
[考古]MTA 团队与 open.mp 团队早期就...
板块: 综合讨论
最后发表: 小鸟unsigned
03-24-2026, 04:53 PM
» 回复: 0
» 浏览: 70
|
服务器开发 精选资源清单
板块: 发布
最后发表: 小鸟unsigned
03-22-2026, 12:24 AM
» 回复: 0
» 浏览: 241
|
[教程] 枚举器 enum 详细讲解 原文作者: ...
板块: 教程
最后发表: 小鸟unsigned
03-22-2026, 12:11 AM
» 回复: 0
» 浏览: 91
|
|
|
| PawnREST - HTTP/S 文件传输与 REST API |
|
发布者: siwode - 06-11-2026, 09:59 PM - 板块: 插件
- 暂无回复
|
 |
PawnREST - HTTP/S 文件传输与 REST API 框架
一个为 SA-MP/open.mp 服务器提供 HTTP/S 文件上传下载功能以及完整 REST API 框架的插件。
下载地址 https://github.com/Fanorisky/PawnREST
Wiki 文档
API 文档与使用指南位于:
Pawn 示例脚本
示例脚本位于:
- 01_server_routes.pwn - 自定义 REST 路由示例
- 02_file_routes_and_ops.pwn - 文件路由与文件操作示例
- 03_json_nodes.pwn - JSON 节点创建与响应示例
- 04_outbound_uploads.pwn - 外部文件上传示例
- 05_outbound_requests.pwn - HTTP/S 请求客户端示例
- 06_websocket_client.pwn - WebSocket 客户端示例
- 07_crc_utils.pwn - CRC32 校验与文件比较工具
- 08_request_input_fallbacks.pwn - 请求参数读取示例
- 09_discord_webhook.pwn - Discord Webhook 集成示例
✨ 功能特性
- 文件上传服务器 - 通过 HTTP POST 接收文件并进行验证
- 文件下载 API - 通过 HTTP GET 提供文件下载
- 外部文件上传 - 上传文件到第三方服务器
- 上传客户端 - 复用基础 URL 与默认请求头
- HTTP 请求客户端 - REST_Request / REST_RequestJSON
- WebSocket 客户端 - 支持 ws:// 与 wss://
- REST API 框架 - 支持 GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS
- 强大的请求访问器 - URL 查询参数与 Header 解析
- 纯节点 JSON API - JSON 构建与解析
- 鉴权系统 - Bearer Token 路由认证
- HTTPS/TLS 支持 - OpenSSL 支持
- 结构化错误回调 - 提供详细错误信息
- CRC32 完整性校验 - 文件校验验证
安装方法
- 下载适用于你平台的最新版本(Windows 为 .dll,Linux 为 .so)
- 放入 open.mp 服务器的 components 目录
- 将 PawnREST.inc 放入 Pawn 编译器 include 目录
- 在脚本中添加:
公共 API 使用以下前缀:
代码: REST_* // HTTP 与 REST 功能
FILE_* // 文件上传下载功能
快速开始
代码: #include <open.mp>
#include <PawnREST>
new g_MapRoute = -1;
new g_ApiPlayers = -1;
public OnGameModeInit()
{
REST_Start(8080);
g_MapRoute = FILE_RegisterRoute(
"/maps",
"scriptfiles/maps/",
".map,.json",
50
);
FILE_AddAuthKey(g_MapRoute, "upload-secret-key");
FILE_AllowList(g_MapRoute, true);
FILE_AllowDownload(g_MapRoute, true);
g_ApiPlayers = REST_RegisterAPIRoute(
HTTP_METHOD_GET,
"/api/players",
"OnGetPlayers"
);
REST_SetRouteAuthKey(g_ApiPlayers, "api-secret-key");
return 1;
}
HTTP 方法常量
| 常量 |
对应方法 |
| HTTP_METHOD_GET |
GET |
| HTTP_METHOD_POST |
POST |
| HTTP_METHOD_PUT |
PUT |
| HTTP_METHOD_PATCH |
PATCH |
| HTTP_METHOD_DELETE |
DELETE |
| HTTP_METHOD_HEAD |
HEAD |
| HTTP_METHOD_OPTIONS |
OPTIONS |
文件上传路由
支持:
- 上传文件
- 文件列表
- 文件下载
- 文件删除
- 文件信息查询
- CRC32 校验
- 上传鉴权
- 冲突处理策略
主要函数:
代码: FILE_RegisterRoute(...)
FILE_AddAuthKey(...)
FILE_AllowList(...)
FILE_AllowDownload(...)
FILE_AllowDelete(...)
FILE_AllowInfo(...)
FILE_Delete(...)
FILE_GetCount(...)
FILE_GetSize(...)
REST API 路由
注册自定义接口:
代码: REST_RegisterAPIRoute(
HTTP_METHOD_GET,
"/api/server",
"OnGetServer"
);
支持:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- OPTIONS
支持 URL 参数:
示例:
请求数据读取
支持:
- 客户端 IP
- HTTP 方法
- 请求路径
- 请求体 Body
- URL 参数
- Query 参数
- HTTP Header
例如:
代码: REST_GetParamInt(requestId, "id");
REST_GetQueryInt(requestId, "page");
REST_GetHeader(requestId, "Authorization");
JSON API
特点:
- 纯 Node 节点系统
- JSON 解析
- JSON 构建
- 对象与数组操作
- 序列化输出
创建对象:
代码: new payload = JsonObject(
"name", JsonString("PawnREST"),
"version", JsonString("1.0")
);
响应函数
代码: Respond(...)
RespondJSON(...)
RespondNode(...)
RespondError(...)
SetResponseHeader(...)
☁️ 外部 HTTP 请求
支持:
- HTTP 请求
- JSON 请求
- 请求取消
- 请求状态查询
- 错误信息获取
主要函数:
代码: REST_CreateRequestClient(...)
REST_Request(...)
REST_RequestJSON(...)
WebSocket 客户端
支持:
- 文本 WebSocket
- JSON WebSocket
- TLS/WSS
- 发送消息
- 关闭连接
主要函数:
代码: REST_WebSocketClient(...)
REST_JsonWebSocketClient(...)
REST_WebSocketSend(...)
REST_JsonWebSocketSend(...)
CRC32 工具
代码: FILE_VerifyCRC32(...)
FILE_GetCRC32(...)
FILE_Compare(...)
用于:
回调列表
上传相关:
代码: OnIncomingUploadCompleted
OnIncomingUploadFailed
OnIncomingUploadProgress
OnOutgoingUploadStarted
OnOutgoingUploadProgress
OnOutgoingUploadCompleted
OnOutgoingUploadFailed
请求相关:
代码: OnRequestFailure
OnWebSocketDisconnect
内置接口
| 接口 |
说明 |
| GET /health |
健康检查 |
| GET /stats |
服务器统计 |
| GET {route}/files |
获取文件列表 |
| GET {route}/files/{name} |
下载文件 |
| GET {route}/files/{name}/info |
获取文件信息 |
| DELETE {route}/files/{name} |
删除文件 |
鸣谢
- yhirose - HTTP 库
- Southclaws - pawn-requests API 参考
- Fanorisky - 项目实现
- SA-MP Team
- open.mp Contributors
|
|
|
|
| [插件] kook-connect |
|
发布者: siwode - 05-01-2026, 01:07 AM - 板块: 插件
- 回复 (4)
|
 |
samp-kook-connector 插件 Wiki
将 Kook 机器人集成到你的 SA-MP / open.mp 服务器游戏模式中。
下载地址:siwode1/samp-kook-connect: SA:MP 插件用于控制 Kook bot (github.com)
目录
- 安装
- 重要说明
- Natives(原生函数)
- Callbacks(回调函数)
1. 安装
1.1 创建 Kook 机器人
- 访问 https://developer.kookapp.cn/
- 创建一个新的应用 / 机器人
- 输入机器人名称并完成创建
- 机器人连接模式选择 WEBSOCKET
- 从机器人设置页面获取你的 机器人令牌(Bot Token)
- 通过开发者门户或邀请链接,将机器人邀请到你的服务器
1.2 配置令牌
SA-MP(server.cfg):
代码: kook_bot_token YOUR_BOT_TOKEN_HERE
open.mp(config.json):
代码: "kook": {
"bot_token": "token"
}
也可以使用环境变量代替:
代码: SAMP_KOOK_BOT_TOKEN=YOUR_BOT_TOKEN_HERE
引用:注意事项:- 永远不要将机器人令牌分享给任何人
- 确保服务器时钟定期同步,否则速率限制器将无法正常工作,机器人可能会被封禁
- 此插件专为 open.mp (OMP) 服务器设计
2. 重要说明
引用:关于 Kook ID 与内部 ID:
Kook ID 是一个很大的数字,无法放入 PAWN 的 32 位整数中。因此插件采用两套 ID 体系:- Kook ID:Kook 平台使用的真实 ID,必须以字符串形式传递
- 内部 ID:插件内部使用的整数索引,通过 KCC_Find* / KCC_Get* 系列函数获取
你的脚本只需持有内部 ID(索引),插件会在内部维护与真实 ID 的映射关系。
引用:关于发送私信:
向用户发送私信需要两步:
- 调用 KCC_CreatePrivateChannel(user, "回调函数名") 打开私信频道
- 在回调函数中调用 KCC_GetCreatedPrivateChannel() 获取频道 ID
- 使用该频道 ID 调用 KCC_SendChannelMessage 等频道函数发送消息
3. Natives(原生函数)
3.1 频道
KOOK_FindChannelById
代码: KOOK_FindChannelById(const channel_id[])
通过 Kook ID 查找频道,返回带 KCC_Channel 标签的内部 ID。
KCC_GetChannelId
代码: KCC_GetChannelId(KCC_Channel:channel, dest[DCC_ID_SIZE], max_size = sizeof dest)
获取频道的 Kook ID,以文本形式存入 dest。
KCC_GetChannelType
代码: KCC_GetChannelType(KCC_Channel:channel, &KCC_ChannelType:type)
获取频道类型,存入 type。
KCC_GetChannelGuild
代码: KCC_GetChannelGuild(KCC_Channel:channel, &KCC_Guild:guild)
获取频道所属服务器,存入 guild。
KCC_GetChannelName
代码: KCC_GetChannelName(KCC_Channel:channel, dest[], max_size = sizeof dest)
获取频道名称,存入 dest。
KCC_GetChannelTopic
代码: KCC_GetChannelTopic(KCC_Channel:channel, dest[], max_size = sizeof dest)
获取频道主题,存入 dest。
KCC_GetChannelPosition
代码: KCC_GetChannelPosition(KCC_Channel:channel, &position)
获取频道排列位置,存入 position。
KCC_SendChannelMessage
代码: KCC_SendChannelMessage(KCC_Channel:channel, const message[], const callback[] = "", const format[] = "", {Float, _}:...)
向指定频道发送消息。callback(可选)为消息发送确认后调用的公共函数,format 及后续参数为回调参数。
KCC_SetChannelName
代码: KCC_SetChannelName(KCC_Channel:channel, const name[])
修改频道名称。
KCC_SetChannelTopic
代码: KCC_SetChannelTopic(KCC_Channel:channel, const topic[])
修改频道主题。
KCC_DeleteChannel
代码: KCC_DeleteChannel(KCC_Channel:channel)
删除指定频道。
3.2 消息
KCC_GetMessageId
代码: KCC_GetMessageId(KCC_Message:message, dest[KCC_ID_SIZE], max_size = KCC_ID_SIZE)
获取消息的 Kook ID,存入 dest。
KCC_GetMessageChannel
代码: KCC_GetMessageChannel(KCC_Message:message, &KCC_Channel:channel)
获取消息所在频道,存入 channel。
KCC_GetMessageAuthor
代码: KCC_GetMessageAuthor(KCC_Message:message, &KCC_User:author)
获取消息发送者,存入 author。
KCC_GetMessageContent
代码: KCC_GetMessageContent(KCC_Message:message, dest[], max_size = sizeof dest)
获取消息文本内容,存入 dest。
KCC_IsMessageMentioningEveryone
代码: KCC_IsMessageMentioningEveryone(KCC_Message:message, &bool:mentions_everyone)
检查消息是否 @全体成员。
KCC_GetMessageUserMentionCount
代码: KCC_GetMessageUserMentionCount(KCC_Message:message, &mentioned_user_count)
获取消息中提及的用户数量。
KCC_GetMessageUserMention
代码: KCC_GetMessageUserMention(KCC_Message:message, offset, &KCC_User:mentioned_user)
按索引获取消息中提及的某个用户。
KCC_GetMessageRoleMentionCount
代码: KCC_GetMessageRoleMentionCount(KCC_Message:message, &mentioned_role_count)
获取消息中提及的身份组数量。
KCC_GetMessageRoleMention
代码: KCC_GetMessageRoleMention(KCC_Message:message, offset, &KCC_Role:mentioned_role)
按索引获取消息中提及的某个身份组。
KCC_DeleteMessage
代码: KCC_DeleteMessage(KCC_Message:message)
在 Kook 上删除该消息。
KCC_GetCreatedMessage
代码: KCC_GetCreatedMessage()
在 KCC_SendChannelMessage 的回调中使用,返回刚发送的消息的内部 ID(带 KCC_Message 标签)。
KCC_DeleteInternalMessage
代码: KCC_DeleteInternalMessage(KCC_Message:message)
从插件内部存储中移除该消息(不会在 Kook 上删除消息),用于持久化消息的清理。
KCC_EditMessage
代码: KCC_EditMessage(KCC_Message:message, const content[], KCC_Embed:embed = KCC_Embed:0)
编辑消息内容,可选附带嵌入消息。
KCC_SetMessagePersistent
代码: KCC_SetMessagePersistent(KCC_Message:message, bool:persistent)
设置消息是否持久保存在插件内部存储中。
KCC_CacheChannelMessage
代码: KCC_CacheChannelMessage(const channel_id[DCC_ID_SIZE], const message_id[DCC_ID_SIZE], const callback[] = "", const format[] = "", {Float, _}:...)
通过 Kook ID 将指定消息缓存到插件中,缓存完成后调用 callback。
3.3 用户
引用:用户的 discriminator 是附加在 Kook 用户名后的 4 位数字标识。
KCC_FindUserByName
代码: KCC_FindUserByName(const user_name[], const user_discriminator[])
通过用户名 + discriminator 查找用户,返回带 KCC_User 标签的内部 ID。
KCC_FindUserById
代码: KCC_FindUserById(const user_id[])
通过 Kook ID 查找用户,返回带 KCC_User 标签的内部 ID。
KCC_GetUserId
代码: KCC_GetUserId(KCC_User:user, dest[DCC_ID_SIZE], max_size = DCC_ID_SIZE)
获取用户的 Kook ID,存入 dest。
KCC_GetUserName
代码: KCC_GetUserName(KCC_User:user, dest[DCC_USERNAME_SIZE], max_size = sizeof dest)
获取用户名,存入 dest。
KCC_GetUserDiscriminator
代码: KCC_GetUserDiscriminator(KCC_User:user, dest[], max_size = DCC_ID_SIZE)
获取用户的 discriminator,存入 dest。
KCC_IsUserBot
代码: KCC_IsUserBot(KCC_User:user, &bool:is_bot)
检查用户是否为机器人。
KCC_IsUserVerified
代码: KCC_IsUserVerified(KCC_User:user, &bool:is_verified)
检查用户是否已完成验证。
3.4 身份组
引用:dest 字符串大小必须为 DCC_ID_SIZE。
KCC_FindRoleByName
代码: KCC_FindRoleByName(KCC_Guild:guild, const role_name[])
在指定服务器中按名称查找身份组,返回带 KCC_Role 标签的内部 ID。
KCC_FindRoleById
代码: KCC_FindRoleById(const role_id[])
通过 Kook ID 查找身份组,返回带 KCC_Role 标签的内部 ID。
KCC_GetRoleId
代码: KCC_GetRoleId(KCC_Role:role, dest[DCC_ID_SIZE], max_size = sizeof dest)
获取身份组的 Kook ID,存入 dest。
KCC_GetRoleName
代码: KCC_GetRoleName(KCC_Role:role, dest[], max_size = sizeof dest)
获取身份组名称,存入 dest。
KCC_GetRoleColor
代码: KCC_GetRoleColor(KCC_Role:role, &color)
获取身份组颜色值,存入 color。
KCC_GetRolePermissions
代码: KCC_GetRolePermissions(KCC_Role:role, &perm_high, &perm_low)
获取身份组权限(64 位整数分为高低两个 32 位部分)。
KCC_IsRoleHoist
代码: KCC_IsRoleHoist(KCC_Role:role, &bool:is_hoist)
检查身份组是否在成员列表中单独显示(提升显示)。
KCC_GetRolePosition
代码: KCC_GetRolePosition(KCC_Role:role, &position)
获取身份组的排列位置,存入 position。
KCC_IsRoleMentionable
代码: KCC_IsRoleMentionable(KCC_Role:role, &bool:is_mentionable)
检查身份组是否可被 @ 提及。
3.5 服务器
引用:dest 字符串大小必须为 DCC_ID_SIZE。
— 查询 —
KCC_FindGuildByName
代码: KCC_FindGuildByName(const guild_name[])
按名称查找服务器,返回带 KCC_Guild 标签的内部 ID。
KCC_FindGuildById
代码: KCC_FindGuildById(const guild_id[])
通过 Kook ID 查找服务器,返回带 KCC_Guild 标签的内部 ID。
KCC_GetGuildId
代码: KCC_GetGuildId(KCC_Guild:guild, dest[DCC_ID_SIZE], max_size = sizeof dest)
获取服务器的 Kook ID,存入 dest。
KCC_GetGuildName
代码: KCC_GetGuildName(KCC_Guild:guild, dest[], max_size = sizeof dest)
获取服务器名称,存入 dest。
KCC_GetGuildOwnerId
代码: KCC_GetGuildOwnerId(KCC_Guild:guild, dest[DCC_ID_SIZE], max_size = sizeof dest)
获取服务器所有者的 Kook 用户 ID,存入 dest。
KCC_GetAllGuilds
代码: KCC_GetAllGuilds(KCC_Guild:dest[], max_size = sizeof dest)
获取所有服务器的内部 ID,存入 dest 数组。
— 频道 —
KCC_GetGuildChannel
代码: KCC_GetGuildChannel(KCC_Guild:guild, offset, &KCC_Channel:channel)
按索引获取服务器中的某个频道。
KCC_GetGuildChannelCount
代码: KCC_GetGuildChannelCount(KCC_Guild:guild, &count)
获取服务器的频道总数。
KCC_CreateGuildChannel
代码: KCC_CreateGuildChannel(KCC_Guild:guild, const name[], DCC_ChannelType:type, const callback[] = "", const format[] = "", {Float, _}:...)
在服务器中创建新频道,完成后调用 callback。
KCC_GetCreatedGuildChannel
代码: KCC_GetCreatedGuildChannel()
在 KCC_CreateGuildChannel 的回调中使用,返回新创建频道的内部 ID。
— 成员 —
KCC_GetGuildMember
代码: KCC_GetGuildMember(KCC_Guild:guild, offset, &KCC_User:user)
按索引获取服务器中的某个成员。
KCC_GetGuildMemberCount
代码: KCC_GetGuildMemberCount(KCC_Guild:guild, &count)
获取服务器成员总数。
KCC_GetGuildMemberVoiceChannel
代码: KCC_GetGuildMemberVoiceChannel(KCC_Guild:guild, KCC_User:user, &KCC_Channel:channel)
获取成员当前所在的语音频道。
KCC_GetGuildMemberNickname
代码: KCC_GetGuildMemberNickname(KCC_Guild:guild, KCC_User:user, dest[DCC_NICKNAME_SIZE], max_size = sizeof dest)
获取成员在服务器中的昵称。
KCC_GetGuildMemberRole
代码: KCC_GetGuildMemberRole(KCC_Guild:guild, KCC_User:user, offset, &KCC_Role:role)
按索引获取成员拥有的某个身份组。
KCC_GetGuildMemberRoleCount
代码: KCC_GetGuildMemberRoleCount(KCC_Guild:guild, KCC_User:user, &count)
获取成员拥有的身份组数量。
KCC_HasGuildMemberRole
代码: KCC_HasGuildMemberRole(KCC_Guild:guild, KCC_User:user, KCC_Role:role, &bool:has_role)
检查成员是否拥有指定身份组。
KCC_SetGuildMemberNickname
代码: KCC_SetGuildMemberNickname(KCC_Guild:guild, KCC_User:user, const nickname[])
修改成员昵称。
KCC_AddGuildMemberRole
代码: KCC_AddGuildMemberRole(KCC_Guild:guild, KCC_User:user, KCC_Role:role)
为成员添加身份组。
KCC_RemoveGuildMemberRole
代码: KCC_RemoveGuildMemberRole(KCC_Guild:guild, KCC_User:user, KCC_Role:role)
移除成员的某个身份组。
KCC_RemoveGuildMember
代码: KCC_RemoveGuildMember(KCC_Guild:guild, KCC_User:user)
踢出服务器成员。
KCC_CreateGuildMemberBan
代码: KCC_CreateGuildMemberBan(KCC_Guild:guild, KCC_User:user, const reason[] = "")
封禁成员,可附带原因。
KCC_RemoveGuildMemberBan
代码: KCC_RemoveGuildMemberBan(KCC_Guild:guild, KCC_User:user)
解封成员。
— 身份组管理 —
KCC_GetGuildRole
代码: KCC_GetGuildRole(KCC_Guild:guild, offset, &KCC_Role:role)
按索引获取服务器中的某个身份组。
KCC_GetGuildRoleCount
代码: KCC_GetGuildRoleCount(KCC_Guild:guild, &count)
获取服务器的身份组总数。
KCC_CreateGuildRole
代码: KCC_CreateGuildRole(KCC_Guild:guild, const name[], const callback[] = "", const format[] = "", {Float, _}:...)
在服务器中创建新身份组,完成后调用 callback。
KCC_GetCreatedGuildRole
代码: KCC_GetCreatedGuildRole()
在 KCC_CreateGuildRole 的回调中使用,返回新创建身份组的内部 ID。
KCC_DeleteGuildRole
代码: KCC_DeleteGuildRole(KCC_Guild:guild, KCC_Role:role)
删除服务器中的身份组。
KCC_SetGuildRoleName
代码: KCC_SetGuildRoleName(KCC_Guild:guild, KCC_Role:role, const name[])
修改身份组名称。
KCC_SetGuildRolePermissions
代码: KCC_SetGuildRolePermissions(KCC_Guild:guild, KCC_Role:role, perm_high, perm_low)
修改身份组权限(64 位整数分为高低两个 32 位部分)。
KCC_SetGuildRoleColor
代码: KCC_SetGuildRoleColor(KCC_Guild:guild, KCC_Role:role, color)
修改身份组颜色。
KCC_SetGuildRoleHoist
代码: KCC_SetGuildRoleHoist(KCC_Guild:guild, KCC_Role:role, bool:hoist)
设置身份组是否在成员列表中单独显示。
KCC_SetGuildRoleMentionable
代码: KCC_SetGuildRoleMentionable(KCC_Guild:guild, KCC_Role:role, bool:mentionable)
设置身份组是否可被 @ 提及。
3.6 机器人
KCC_CreatePrivateChannel
代码: KCC_CreatePrivateChannel(KCC_User:user, const callback[], const format[] = "", {Float, _}:...)
向指定用户打开私信频道,完成后调用 callback。
KCC_GetCreatedPrivateChannel
代码: KCC_GetCreatedPrivateChannel()
在 KCC_CreatePrivateChannel 的回调中使用,返回私信频道的内部 ID(可直接用于 KCC_SendChannelMessage 等函数)。
3.7 嵌入消息
嵌入消息(Embed)允许向 Kook 频道发送富文本内容消息。
KCC_CreateEmbed
代码: KCC_CreateEmbed(const title[] = "", const description[] = "", const url[] = "", const timestamp[] = "", color = 0, const footer_text[] = "", const footer_icon_url[] = "", const thumbnail_url[] = "", const image_url[] = "")
创建一个嵌入消息,所有参数均为可选,返回带 KCC_Embed 标签的内部 ID。
KCC_DeleteEmbed
代码: KCC_DeleteEmbed(KCC_Embed:embed)
删除嵌入消息。
KCC_SendChannelEmbedMessage
代码: KCC_SendChannelEmbedMessage(KCC_Channel:channel, KCC_Embed:embed, const message[] = "", const callback[] = "", const format[] = "", {Float, _}:...)
向频道发送带嵌入消息的内容,可附带普通文字。
KCC_AddEmbedField
代码: KCC_AddEmbedField(KCC_Embed:embed, const name[], const value[], bool:inline = false)
为嵌入消息添加字段,inline 为 true 时字段并排显示。
KCC_SetEmbedTitle
代码: KCC_SetEmbedTitle(KCC_Embed:embed, const title[])
设置嵌入消息标题。
KCC_SetEmbedDescription
代码: KCC_SetEmbedDescription(KCC_Embed:embed, const description[])
设置嵌入消息描述。
KCC_SetEmbedUrl
代码: KCC_SetEmbedUrl(KCC_Embed:embed, const url[])
设置嵌入消息链接。
KCC_SetEmbedTimestamp
代码: KCC_SetEmbedTimestamp(KCC_Embed:embed, const timestamp[])
设置嵌入消息时间戳。
KCC_SetEmbedColor
代码: KCC_SetEmbedColor(KCC_Embed:embed, color)
设置嵌入消息左侧色条颜色。
KCC_SetEmbedFooter
代码: KCC_SetEmbedFooter(KCC_Embed:embed, const footer_text[], const footer_icon_url[] = "")
设置嵌入消息页脚文本,可附带图标 URL。
KCC_SetEmbedThumbnail
代码: KCC_SetEmbedThumbnail(KCC_Embed:embed, const thumbnail_url[])
设置嵌入消息缩略图。
KCC_SetEmbedImage
代码: KCC_SetEmbedImage(KCC_Embed:embed, const image_url[])
设置嵌入消息主图片。
3.8 表情
引用:dest 字符串大小必须为 DCC_EMOJI_NAME_SIZE。
KCC_CreateEmoji
代码: KCC_CreateEmoji(const name[DCC_EMOJI_NAME_SIZE], const snowflake[DCC_ID_SIZE] = "")
创建一个表情对象,snowflake 为可选的 Kook 雪花 ID,返回带 KCC_Emoji 标签的内部 ID。
KCC_DeleteEmoji
代码: KCC_DeleteEmoji(KCC_Emoji:emoji)
删除表情对象。
KCC_GetEmojiName
代码: KCC_GetEmojiName(KCC_Emoji:emoji, dest[DCC_EMOJI_NAME_SIZE], maxlen = DCC_EMOJI_NAME_SIZE)
获取表情名称,存入 dest。
3.9 反应
KCC_CreateReaction
代码: KCC_CreateReaction(KCC_Message:message, KCC_Emoji:reaction_emoji)
对指定消息添加表情反应。
KCC_DeleteMessageReaction
代码: KCC_DeleteMessageReaction(KCC_Message:message, KCC_Emoji:reaction_emoji = KCC_Emoji:0)
移除消息上的表情反应。reaction_emoji 为 0(默认)时删除该消息上的所有反应。
3.10 其他
KCC_EscapeMarkdown
代码: KCC_EscapeMarkdown(const src[], dest[], max_size = sizeof dest)
对字符串中的 Markdown 特殊字符进行转义,使文本在 Kook 消息中原样显示,不被格式化。
4. Callbacks(回调函数)
4.1 频道回调
KCC_OnChannelCreate
代码: public KCC_OnChannelCreate(KCC_Channel:channel)
触发时机:服务器中有新频道创建时。
KCC_OnChannelUpdate
代码: public KCC_OnChannelUpdate(KCC_Channel:channel)
触发时机:频道信息更新时(名称、权限等)。
KCC_OnChannelDelete
代码: public KCC_OnChannelDelete(KCC_Channel:channel)
触发时机:频道被删除时。
4.2 消息回调
KCC_OnMessageCreate
代码: public KCC_OnMessageCreate(KCC_Message:message)
触发时机:频道中有新消息发送时。
KCC_OnMessageDelete
代码: public KCC_OnMessageDelete(KCC_Message:message)
触发时机:消息被删除时。
KCC_OnMessageReaction
代码: public KCC_OnMessageReaction(KCC_Message:message, KCC_User:reaction_user, KCC_Emoji:emoji, DCC_MessageReactionType:reaction_type)
触发时机:消息添加或移除表情反应时。- message:消息内部 ID
- reaction_user:操作反应的用户内部 ID
- emoji:使用的表情内部 ID
- reaction_type:反应事件类型(添加 / 移除等)
4.3 用户回调
KCC_OnUserUpdate
代码: public KCC_OnUserUpdate(KCC_User:user)
触发时机:用户资料更新时(用户名、头像等)。
4.4 服务器回调
KCC_OnGuildCreate
代码: public KCC_OnGuildCreate(KCC_Guild:guild)
触发时机:服务器创建,或机器人加入新服务器时。
KCC_OnGuildUpdate
代码: public KCC_OnGuildUpdate(KCC_Guild:guild)
触发时机:服务器信息更新时(名称、图标等)。
KCC_OnGuildDelete
代码: public KCC_OnGuildDelete(KCC_Guild:guild)
触发时机:服务器删除,或机器人离开服务器时。
KCC_OnGuildMemberAdd
代码: public KCC_OnGuildMemberAdd(KCC_Guild:guild, KCC_User:user)
触发时机:用户加入服务器时。
KCC_OnGuildMemberUpdate
代码: public KCC_OnGuildMemberUpdate(KCC_Guild:guild, KCC_User:user)
触发时机:成员信息更新时(昵称、身份组等)。
KCC_OnGuildMemberVoiceUpdate
代码: public KCC_OnGuildMemberVoiceUpdate(KCC_Guild:guild, KCC_User:user, KCC_Channel:channel)
触发时机:成员语音状态变化时(加入 / 离开语音频道)。- channel:语音频道内部 ID,用户离开语音时该值可能无效
KCC_OnGuildMemberRemove
代码: public KCC_OnGuildMemberRemove(KCC_Guild:guild, KCC_User:user)
触发时机:用户离开或被踢出服务器时。
KCC_OnGuildRoleCreate
代码: public KCC_OnGuildRoleCreate(KCC_Guild:guild, KCC_Role:role)
触发时机:服务器中创建新身份组时。
KCC_OnGuildRoleUpdate
代码: public KCC_OnGuildRoleUpdate(KCC_Guild:guild, KCC_Role:role)
触发时机:服务器身份组更新时(名称、颜色、权限等)。
KCC_OnGuildRoleDelete
代码: public KCC_OnGuildRoleDelete(KCC_Guild:guild, KCC_Role:role)
触发时机:服务器身份组被删除时。
|
|
|
|
| 圣安地列斯联机二十年:SA:MP 与 open.mp 历史 |
|
发布者: 小鸟unsigned - 03-24-2026, 08:45 PM - 板块: 综合讨论
- 回复 (3)
|
 |
圣安地列斯联机二十年:SA:MP 与 open.mp 历史
2026 年。GTA6 即将在今年 11 月发售,新的时代正在敲门。
也是在这一年,《侠盗猎车手:圣安地列斯》PC 版上市整整二十年,SA:MP 的第一个可玩版本也已走过二十个年头。
一座从未被设计为联机游戏的城市,有人在这里认识了一辈子的朋友。有人在这里第一次学会了写代码。有人在角色扮演服务器里扮演一个JC或一个黑帮头目。有人在一个无聊的下午打开了游戏,然后就再也没有那个下午了。
SA:MP 从来不是一个精致的产品。它是一群人强行塞进一款单人游戏里的多人世界。它有数不清的 bug,有争议不断的管理者,有过漫长的停滞与分裂——它也有 open.mp,有一批不肯让它消失的开发者,有如今可以在手机上随时上线的客户端,不管你还记不记得服务器的 IP,不管你当年是服主、脚本开发者,还是只是一个普通的玩家——你在这里的时间是真实的。
GTA6 会带来它的世界。但圣安地列斯的街道,还在。服务器还在跑,玩家还在上线,有人还在第一次输入 /register。
前言
引用:本文以客观事实为准绳。所有关键事件、人物声明、原始帖文,均完整收录于正文之中,不以外部链接替代实质内容。链接仅作考证索引,供读者溯源查验,不构成叙述本身的一部分。
凡引用原文处,均以引用块标注,并注明来源与时间。凡转述处,均忠实于原意,不加渲染,不作价值评判。
这是一部关于热爱、控制、背叛与重建的故事,也是一个游戏社区在二十年间走过的记录。
卷首语:一个不该有多人模式的游戏
2004 年 10 月 26 日,《侠盗猎车手:圣安地列斯》(Grand Theft Auto: San Andreas)在 PlayStation 2 平台正式发售。2005 年 6 月 10 日,PC 版本上市。
这款游戏从未被设计为多人联机。Rockstar Games 为它设计了一个宏大的单人故事——主角 CJ 在帮派、JC与腐败之间挣扎求生,横跨三座城市、一片乡野。游戏没有联机大厅,没有服务器浏览器,没有任何供多人游玩的接口。
然而在 GTA 模组社区里,有一批人从不接受"这不可能"这个答案。
他们是一群来自世界各地的年轻人,大多数有着普通的日间生活——学生、程序员、上班族——他们一点一点地撬开这款游戏的外壳,试图将它改造成一个可以容纳数百人同时在线的多人世界。
这便是 SA:MP 的起点。
第一卷:诞生(2004–2006)
第一章 从 Vice City 到 San Andreas
在《圣安地列斯》PC 版尚未发售之前,GTA 模组社区已经有人在为《GTA 罪恶都市》(Vice City)制作多人联机改造,这个项目被命名为 VC:MP(Vice City Multiplayer)。
领导这支早期团队的,是一个网名叫 kyeman 的开发者,真名 Kye Bitossi。他在 GTA 模组社区中颇有声望,是公认的技术核心人物。
2005 年 4 月初,kyeman 在全球最大的 GTA 社区论坛 GTAForums 上发布了一篇帖子,正式宣布 SA:MP 项目的存在。帖子标题是:
引用:"[WIP] SA-MP: Multiplayer for San Andreas PC"
——kyeman,GTAForums,2005年4月3日
消息一出,社区立刻沸腾。预告视频和截图开始在各大 GTA 论坛流传,项目的小型社区论坛在尚未开放任何测试的情况下,已吸引了约 120 名注册用户。
公开 beta 测试期间,服务器承载能力远不及涌入的测试者数量,进入测试服务器需要排队等候,等待时间往往很长。这种盛况,是对整个项目最好的背书。
进入开发团队的成员,来自 GTA 模组社区的各个方向,过去的SA-MP 0.2开发者和测试者包括:kyeman、spookie、Y_Less、mike、jax、Mike、Cam、adamcs、bakasan、Born Acorn、Dalpura、Damian、Delfi、dexx、DrAke$、Drift、ECLiPSE、f3llah1n、him selfe、illspirit、littlewhitey、MrJax、njr1489、Posty、PsYcHoGoD、Shizz、Simon、sockx、squiddy、Static、steve-m、The Azer、Trix、Wacko、XcR、[ULK]Crack,其中许多人既是开发者也是测试员。
他们大多是业余爱好者,利用业余时间共同推进这个无人出资的项目。
第二章 合作模式的争议与团队第一次动荡
然而,SA:MP 从一开始便并非一片和谐。
开发团队内部,关于游戏核心玩法的方向,爆发了一场至今仍被一些老玩家提起的根本性争论:
一部分人主张将 SA:MP 建设为一款合作模式(Co-op)游戏,设想最多 6 名玩家共同游玩,保留完整的 NPC 行为、警察追捕系统与任务流程,让多人体验尽可能接近单人游戏的质感;
另一方,以 kyeman 为首,坚持将其建设为一个能够容纳大量玩家同时在线的大规模死亡竞技(Deathmatch)式多人服务器。
这场分歧并未在内部平息,而是溢出到了 GTANet 社区论坛,引发了玩家的大量公开讨论与批评,负面声音之多,令整个项目团队承受了巨大压力。
2005 年 9 月 18 日,kyeman 在持续的内外压力下,宣布因个人事务无暇继续领导项目,将主导权移交给团队二号核心开发者 spooky。
spooky 接手后,将项目更名为 GTA:Multiplayer(GTA:M),以示风格与管理方式的转变。
然而,约一个月后,kyeman 重新归队。合作模式方案在此期间被彻底搁置,项目名称最终改回 San Andreas Multiplayer(SA:MP),方向确定为大规模多人在线游戏。
关于"Grand Theft Auto"字样是否应出现在项目名称中,团队在命名时也有过考量——使用 Rockstar 的商标可能引发法律风险,最终以缩写"SA:MP"作为正式名称,在一定程度上规避了这一问题。
第三章 2006 年:第一个可玩版本
在经历了那段动荡之后,SA:MP 于 2006 年 发布了第一个向公众开放的可玩版本。
从技术层面看,这是一项相当可观的成就:开发者们在完全没有官方支持的前提下,用逆向工程的方式强行将联机功能"嵌入"进一款从未被设计为多人联机的作品,使其能够支持最多 1000 名玩家同时在线,在同一片虚拟的圣安地列斯地图中驾车、射击、竞速。
服务器使用 Pawn 作为脚本语言,服务器主可以自行编写游戏模式(GameMode)与过滤脚本(FilterScript)。这一设计赋予了整个生态极高的开放性——任何人只要学会 Pawn,就可以在圣安地列斯的地图上建造出属于自己的世界。
0.2.2 版本是这一阶段的代表版本,也是现存有据可查的最早可玩 SA:MP 版本记录之一。
第二卷:黄金时代与暗流(2006–2009)
第四章 版本迭代与社区的兴盛
2006 年至 2014 年,是 SA:MP 最活跃的开发阶段。Kalcor(此时 kyeman 已开始使用这个网名)陆续发布了一系列重要版本:
0.2 系列(2006–2008)期间,SA:MP 奠定了基本架构,确立了 Pawn 脚本语言作为游戏模式开发的标准。这段时间并非全然顺利——一次大规模安全漏洞爆发,险些让整个团队就此放弃。而最终让他们坚持下来的,是一份在线请愿:短短时间内,数以千计的玩家签名支持 SA:MP 继续存在。这份来自社区的温度,让开发者重新燃起了动力。
0.3a(约 2010 年)是 SA:MP 历史上最具里程碑意义的版本之一。单服务器玩家上限从原来的数百人扩展至 500 人(后续版本更高),引入了鼠标驱动的记分板与聊天界面,增加了可编程的 NPC 机器人系统(允许服务器创建可驾驶火车、飞机的脚本机器人),网络同步质量大幅改善。
此后数年,0.3c、0.3d、0.3e、0.3x、0.3z 相继发布,带来了音频流媒体支持、延迟补偿模式、自定义对象渲染距离、新的安全更新等功能。每一个版本的发布,都在 SA:MP 官方论坛的"新闻与更新"板块留下了一条由 Kalcor 亲自发布的公告。
2015 年 5 月 1 日,Kalcor 发布了 SA:MP 0.3.7,这是 SA:MP 有史以来最后一个带有实质性新功能的正式版本。0.3.7 新增了超过 500 个物体 ID(含特技物件与地形物件)、界面字体大小调整选项、若干新版警察皮肤、服务端对车门与车窗的控制权、为未标记警车添加警报灯的能力,以及一个简单的静态 NPC actor 系统。
这个版本在此后数年间成为整个 SA:MP 生态的事实标准,直至今日仍是服务器主最常使用的版本。
与此同时,这个社区在技术之外也在野蛮生长。角色扮演(RP)服务器在这段时期走向成熟,玩家们在 SA:MP 的世界中建构起了拥有经济系统、帮派政治、法律体系的虚拟社会。来自世界各地大型服务器先后崛起。SA:MP 不再只是一个小众模组,它已经成为一个真实的、活跃的、跨越语言与国境的国际社区。
第五章 Y_Less 的双重身份与 YSI 的诞生
在 SA:MP 的所有社区开发者中,有一个人的名字几乎是无可回避的:Y_Less,真名 Alex Cole。
Y_Less 最初以 beta 测试员的身份加入 SA:MP 团队,随后正式成为开发团队成员,主要负责 Pawn 脚本语言相关的功能开发。然而,他与 Kalcor 之间的分歧,从他正式入队的第一天起便已埋下。
用 Y_Less 自己后来的话说:
引用:"加入开发团队之后,我开始开发 Pawn,但我发现自己能做的事被 Kye 严重限制。我们之间存在重大的意见分歧:他认为 90% 的开发精力应该投入同步(sync)工作,Pawn 几乎只是顺手提供一点额外功能的附属品。而我认为,虽然同步很重要,但他已经把同步覆盖得很好了,我更想专注于 Pawn 的功能与函数。然而他是老板,说了算,严重限制了我被允许做的事。此外,他有个坏习惯——每次版本迭代之后,只要在某个细枝末节的测试中有任何不稳定,就要来指责我,尽管测试本来就是我们有 beta 测试团队的原因。不用说,这不是一个好的工作环境,但我仍然喜欢 SA:MP 这个产品本身。"
在这种压抑的工作环境中,YLess 于 2007 年 开始独立开发 YSI(YLess' Server/Script Includes)。
YSI 最初是他此前若干工具的整合与升级,同时也是他在 Kalcor 的管控边界之外,以纯 Pawn 脚本的方式,实现他认为 SA:MP 脚本系统应当具备的扩展能力。他在这个过程中也加入了一些辅助性的函数,例如 player objects 功能,最初是专门为了支撑他的对象流式传输器 e_objects 而写的。
用 Y_Less 自己的话说:
引用:"2007 年,我开始开发 YSI。这部分是对若干早期工具的更新与整合,部分也是在 Kye 的控制之外,以 Pawn 能力范围内的方式带来我认为 Pawn 所需的扩展功能——尽管以纯 Pawn 来做,能实现的事情自然有其上限。YSI 成为我的主要项目这一事实惹恼了 Kye,因为他认为这说明我没有把时间花在 SA:MP 上,尽管我实际上一直都在。"
YSI 涵盖了命令处理(ycommands)、玩家迭代器(yiterate)、钩子机制(yhooks)、INI 文件读写(yini)、多语言支持(y_languages)等数十个子模块,是当时 SA:MP 社区功能最全面、使用最广泛的第三方脚本库。
至于"YSI"到底代表什么,连 Y_Less 本人也说不清:
引用:"最初的想法是'Y[i]Less' Server Includes',但 Server 和 Script 经常被混用,而且现在参与的开发者不止 YLess 一人,所以'Y'就变成了一个递归缩写,代表'YSI'本身。"[/i]
第六章 2008 年底:GTANet 风波与团队大出走
2008 年底,SA:MP 开发团队内部爆发了一场与 GTANet 社区相关的严重矛盾。关于这场风波的具体细节,现有的公开资料已难以完整还原,但其结果是清晰且影响深远的:Mike、aru 和 Peter 等多名核心开发者相继以此为由宣布离队。
Y_Less 在日后的声明中这样描述这段经历:
引用:"2008 年底,GTANet 风波爆发,Mike、aru 和 Peter 因此离开,我也因为团队氛围实在太差,加之大家都在走,便也一同离开了。"
这一波出走之后,SA:MP 的开发工作几乎完全落回 Kalcor 一人肩上,这进一步强化了整个项目对他个人的高度依赖,也使得社区的任何集体意见都更加难以产生实质影响。
然而事情并未就此平息。Y_Less 出走后,出于他自己也说不清的原因,重新回归了团队。他在后来的声明中写道:
引用:"不知为何,真的记不清了,我之后又重新加入了团队。那时候 Kye 想要关闭 SA:MP,我提出愿意接手,他拒绝了。然后我再次退出——不是因为我没接手到,这一点需要说明——于是 Kye 决定封禁我的论坛账号,而且不只是封号,而是彻底抹除——删掉了我所有的帖子。这意味着原版《实用函数》帖没了,旧版 FAQ 帖没了,大量迷你教程没了,《Pawn 关键词》帖也没了——我只有在非常必要的时候才发帖,那些帖子都有其存在的价值,它们的消失被大量用户注意到并提出了强烈抗议。"
这便是 2008 年底那次"第一次封号"事件的始末——相对低调,但已经是一个信号。真正震动整个社区的风波,还在后面。
第七章 2009 年 1 月:YSF 封号事件
从团队再度离开之后,YLess 没有停止为社区创作工具。他开发了 YSF(YLess' Server Fixes)——一个以服务端插件形式存在的 bug 修复集合。
YSF 的设计理念很简单:SA:MP 有很多 Kalcor 长期忽略或拒绝修复的 bug,既然拿不到源代码,那就通过内存操作(memory patching)的方式,在运行时强行修正这些行为。这个方法技术上相当复杂,但效果立竿见影。
Y_Less 事后这样形容 YSF:
引用:"YSF 是我最成功的项目,因为它做到了所有人都想要的事。它本质上是一个更新版的服务端——人们总是渴望新版本带来修复和新功能,功能方面已经有其他插件(包括 YSI 插件)覆盖了,所以我选择了修复这条路(以及一些小功能——显然正是这些小功能引发了麻烦)。"
YSF 在社区中广泛流传,大量服务器安装并使用它。
2009 年 1 月 19 日,事件的直接导火索被点燃。
当天,Kalcor 私信给 Y_Less,表达了对 YSF 的不满。他的顾虑有两项:
第一,他认为 YSF 可能与 SA:MP 未来版本产生脚本兼容性问题;第二,他认为 YSF 代码中存在与 SA:MP 服务端源代码相似的内容,以 functions.cpp 中的 IsNickInvalid 函数为例。
Y_Less 对此逐一作出回应。关于兼容性问题,他指出 YSF 恰恰是在让脚本对未来版本更加兼容,而非相反;关于源代码问题,他坦言:
引用:"这实际上并非盗用,但我承认看起来非常相似,因为它做的事情完全一样,而且也不是什么复杂的函数,我懒得争,就直接把它彻底重写了。"
他还顺带提到,Kalcor 自己曾主导过一些令他反对的修改决策——比如改变服务端的默认运行风格,以及在 0.3 版本中移除对象(最后改为以 IPL 文件形式替代,而非完全移除)——当初他的反对意见被 Kalcor 直接否决。
双方讨论的最终结果,表面上是达成了共识:Kalcor 表示接受了 Y_Less 的解释,YSF 可以继续存在。
然而,Y_Less 第二天登录论坛,发现自己已经被封号。
他在后来的声明中还原了这段过程:
引用:"就在我们讨论同意 YSF 可以继续存在,之后,他发现了 SetServerRule,并意识到它可以用来修改只读控制台变量——比如服务器版本号——从而让人们能够违反 SA:MP 服务条款第(g)条:'你不得人为地增加玩家数量,或向 SA:MP 查询机制提供虚假信息。'"
Kalcor 认为,Y_Less 在 YSF 中提供了这样一个功能,就等同于为违规行为提供了手段,因此必须承担责任。
封号通知的原文如下(Y_Less 在声明中直接引用):
引用:*"抱歉 ssǝן‾ʎ,你已被禁止使用本论坛!
滥用前开发者身份。入侵服务器,并为他人违反服务条款提供手段。"*
封号的同时,Kalcor 再次删除了 Y_Less 在 SA:MP 官方论坛上的全部历史帖子。大量教程、工具发布帖、FAQ 就此永久消失,众多用户注意到这些帖子的蒸发,并提出了强烈抗议。
Y_Less 随即在社区发布了一篇完整声明,以下是他原文的核心段落:
引用:"基本上,我因为 YSF 被 SA:MP 官方论坛封号了!
历史:
200x 年(实在记不清了),在与 Spookie 讨论之后,我以 beta 测试员的身份加入了 SA:MP 团队——那还是第一次公开 beta 之前的事——并开始编写 Pawn 相关的代码。
没过多久,我正式加入了开发团队,专注于 Pawn 的开发,但我很快发现自己受到 Kye 的诸多限制,做不了想做的事。我们之间存在重大分歧:他认为 90% 的开发精力应该用于同步(sync),Pawn 只是顺手提供一点额外功能的附属品;而我认为,虽然同步很重要,但他已经把同步做得很好了,我更想把时间花在 Pawn 的功能扩展上。然而他是老板,说了算,严重限制了我能做的事。更让人受不了的是,他有个习惯——每次版本迭代后,只要在某个犄角旮旯的测试里出了任何小问题,就要来责怪我,哪怕我们有 beta 测试团队本来就是为了发现这些问题的。说实话那不是一个好的工作环境,但我还是喜欢 SA:MP 这个产品本身。
2007 年,我开始开发 YSI。这一方面是对此前一批旧项目的整合升级,另一方面也是想在 Kye 的管控之外,以纯 Pawn 脚本的方式实现我认为 Pawn 所需要的扩展功能——尽管能做到的事情有限,毕竟纯 Pawn 本身有其局限(我确实额外添加了一些辅助函数,比如 player objects 功能完全是为了写我最初的对象流式传输器 e\_objects 而专门实现的)。YSI 成为我的主要项目这件事惹恼了 Kye,因为他觉得我没有把时间花在 SA:MP 上——尽管我实际上一直都在。
2008 年底,GTANet 社区风波爆发,Mike、aru 和 Peter 因此离队。我也因为团队氛围实在太差,加上大家都在走,便也跟着离开了。
不知为何(真的记不清了),我之后又重新回归了团队。那时候 Kye 想要关闭 SA:MP,我主动提出愿意接手,他拒绝了。然后我再次退出——不是因为没接手到,这一点需要说清楚——于是 Kye 决定封禁我的论坛账号,而且不只是封号,而是彻底抹除,删掉了我所有的帖子。这意味着原版《实用函数》帖没了,旧版 FAQ 帖没了,大量迷你教程没了,《Pawn 关键词》帖也没了——我只有在非常必要的时候才会发帖,那些帖子都有其价值,它们的消失被大量用户注意到并提出了抗议。
不管怎样,我还是喜欢 SA:MP 和这个社区本身(至少喜欢其中的一部分),所以我还是留了下来(现在回想起来真不知道当时为什么,深感后悔),并继续努力让 SA:MP 变得更好——尽管 Kye 的存在让这件事很难。我继续开发 YSI、写教程和帮助帖,后来又创作了 YSF。YSF 大概是我最成功的项目了,因为它做到了所有人都想要的事。它本质上就是一个改进版的服务端——人们总是渴望新版本带来修复和新功能,功能方面已经有其他插件(包括 YSI 插件)覆盖了,所以我选择了专注于修复这条路(以及一些小功能——显然正是这些小功能引发了这次麻烦)。
总之,Kye 和我一般都各自行事、互不干扰(至少我是这么以为的),有一段时间相安无事。直到 2009 年 1 月 19 日,我收到了 Kye 的消息,说他不太喜欢我在 YSF 里做的事。经过一番交涉,才明白他的顾虑在于:YSF 可能与未来版本的脚本产生兼容性问题(这话从他嘴里说出来真是讽刺——我当初就反对过他把默认运行方式改掉,以及 0.3 版本里删除对象的决定,但他否决了我;对象最后是改成了 IPL 文件的形式,而非完全删除,但也没有了 per-player objects,至少我最后听到的消息是这样)。此外,他还质疑 YSF 里有 SA:MP 服务端的源代码——他举的例子是 functions.cpp 里的 IsNickInvalid 函数。这个函数其实不是照抄的,但我承认看起来确实很像,毕竟它做的事完全一样,而且也不是什么复杂的函数,我懒得争,就直接把它彻底重写了。在我指出 YSF 并不会造成兼容性问题(恰恰相反,它让脚本对未来版本的兼容性更好),并同意日后不再使用 SA:MP 源代码(尽管我此前一直在刻意避免这样做)之后,他让步了。
第二天我上论坛,发现自己已经被封了……
原来在我们达成共识之后,他又发现了 SetServerRule 这个功能,意识到它可以用来修改只读控制台变量(比如服务器版本号),从而让人能够违反 SA:MP 服务条款第(g)条:'你不得人为地增加玩家数量,或向 SA:MP 查询机制提供虚假信息。'
我解释说,我本人根本没有这样做,这个功能的设计初衷是好的,只是不幸被人滥用了。我甚至想过反问他:他提供论坛和 IRC 频道,同样也给人们提供了违反条款(c)(d)条的方式,按他的逻辑,他也应该被封号——但我懒得说了。封号通知的原文是:
'抱歉 ssǝן‾ʎ,你已被禁止使用本论坛!滥用前开发者身份。入侵服务器,并为他人违反服务条款提供手段。'
所谓'滥用',是指我用它改善了所有人的游戏体验;所谓'入侵服务器',是指我修复了它;所谓'提供手段',意思是人们可以拿我提供的工具做坏事——但这不代表我支持或认可这种行为,这只是一个有用工具所带来的无奈副作用。
Kye 说 YSF 的事已经覆水难收,他只能发布新版本来屏蔽它,伤害已经造成,它已经广泛传播,人们正在用它黑自己的服务器,没有人能阻止,所以我必须被封。我回应说,如果我没被封,我本来可以发布最新版本,在修复拾取物 bug 的同时,加入对规则的只读标志检查——虽然这无法彻底解决问题,但能大幅减少滥用,因为几乎没有人会主动把它改回旧方式,大多数人会为了这个重要修复而升级。说到这里,他就不再回复我了(我觉得他是意识到自己断掉了快速、高效解决这件事的唯一合理途径,然后不想认账了——如果不是这样,我欢迎他来回应)。
总之,我决定把当时正在写的那段代码(YSI 的示例脚本)收个尾,毕竟我还是喜欢使用 YSI 的那些人,上传最终版本,然后就此结束。
所以,截至目前:YSI 1.0、YSF 1.0、YSI2 1.0、YPI 1.0,到此为止,我不干了。
附言:Kye 正在研究一种过滤"修改版本服务端"并将其列入黑名单的机制——我可能会发布一个不自动修改版本号的 YSF 版本,这样用它的人就不会因为我的失误被连坐;请留意这里的动态(我可能也会顺带加入拾取物修复,但不会保留只读变量相关的功能)。
附言二:IRC 才是王道,我还活跃在 GTANet 的大量频道里,包括 #YSI 和 #Y_Less,欢迎来找我。
附言三:不知道 MTA 的源代码长什么样……"
——Y_Less,2009年1月,TMS Forums
这篇声明在社区引发了强烈反响。数日后,经社区压力,Y_Less 得以解封,YSF 在不修改只读变量的前提下被允许继续使用。从表面上看,风波平息了。
但那句"附言三"——"不知道 MTA 的源代码长什么样"——如今读来,已不再只是一句无心之语。
第三卷:停滞、管控与分歧(2014–2019)
第八章 0.3.7 之后的沉寂(含源码泄漏事件)
2015 年 5 月 1 日,SA:MP 0.3.7 发布。此后,Kalcor 在新功能层面的开发近乎陷入停滞。
2018 年前后,Kalcor 发布了 SA-MP 0.3.DL——一个支持服务端向玩家客户端推送自定义模型(Download Content)的版本。这是社区长期以来最渴望的功能之一,玩家们可以在 SA:MP 服务器上看到服务器自定义的车辆、皮肤、物件模型,而不再局限于原版游戏资产。
然而 0.3.DL 的推出并不顺利:安全问题(恶意服务器可能推送损坏的模型文件危害客户端)和与旧版 0.3.7 的兼容性问题,使得相当一部分服务器拒绝迁移。SA:MP 社区自此一分为二,0.3.7 阵营与 0.3.DL 阵营长期并存。
这种分裂令社区感到困惑与沮丧。有人多次在论坛呼吁将两个版本合并,但得不到任何回应。
源码泄漏事件(2010 年起源,2018 年爆发)
在 0.3.DL 引发社区分裂的同一时期,另一条隐患也终于浮出水面。
2018 年 11 月 15 日,SA:MP 官方论坛上出现了一篇由用户 Kshishtof 发起的帖子,标题是《SA-MP 源代码泄漏》。帖子指向一个名为 rw-mp.net 的项目,该项目被认为是基于泄漏的 SA:MP 源代码构建的独立多人联机分支。
帖子引发了社区的激烈讨论。一部分人对 rw-mp 持同情甚至欣赏的态度,认为它至少在做 SA:MP 不再愿意做的事;另一部分人则斥之为对原作的窃取与侮辱。
2018 年 11 月 16 日,Kalcor 亲自在帖子中出现,发表了两段回应。第一段揭示了这批泄漏源码的来历:
引用:"SA:MP 的源代码于 2010 年被一名法国人从我们的服务器上黑掉,目的是为 IV:MP 这个 mod 服务。我记录并上报了这次入侵。
GitHub 上大约有十个副本。我一直没有把它们下架,是因为提交 DMCA 申诉需要将我的真实姓名录入 GitHub 的公开 DMCA 数据库,否则就得花钱请律师。"
第二段话语气更为强硬,是一封事实上的法律警告:
引用:"0.2.5 的源代码是通过计算机入侵从我们的服务器上盗取的。任何基于该代码发布软件的人,都涉嫌与我们作对的刑事共谋。
SA:MP 所使用的 RakNet 版本并非开源,而是通过付费许可证使用的。BASS 音频库同样是付费许可证使用的。整个游戏 mod 社区正在被那些删除原始版权信息、将他人成果冒充自己作品重新发布的蠢货所伤害。
我给他们几天时间关闭并停止任何与 SA:MP 相关的活动。"
——Kalcor,SA:MP 官方论坛,2018 年 11 月 16 日 原帖:https://sampforum.blast.hk/showthread.php?tid=660866
这段声明揭示了几个此前从未公开过的重要事实:
其一,SA:MP 源码泄漏的根源远早于 2018 年——2010 年就已经发生了一次有据可查的服务器入侵事件,是一名法国黑客为了支持 IV:MP(GTA IV 的多人 mod)而实施的。这与 Kalcor 在 2019 年 pawn.wiki 文章中所说的"R* 给 MTA 的代码被用于攻击 SA:MP"形成了完整的脉络——他所遭受的代码安全威胁,来自不止一个方向。
其二,Kalcor 之所以始终不对 GitHub 上的泄漏副本提交 DMCA,并非不知情或不在意,而是出于一个极具讽刺意味的现实顾虑:提交 DMCA 需要将他的真实姓名公开录入数据库,而他不愿意这样做。源码在网上流传的同时,他本人却因为隐私顾虑而放任不管。
这次事件在社区中引发了新一轮关于"Kalcor 应不应该直接将 SA:MP 开源"的讨论。部分社区成员认为,既然源码已经通过非法途径广泛流传,不如主动开源,至少可以让社区以合法的方式参与改进。这个声音 Kalcor 听到了,但他的答案,在两年后的 pawn.wiki 那篇文章里已经说得很清楚。
第九章 Kalcor 眼中的历史:MTA 的旧账、R\* 的源码,与闭源的真实理由
编年史至此已经写了许多关于 Kalcor 的事——他的决策、他的封号、他的沉默——却始终没有给他一个真正完整的出场。他是从哪里来的?他为什么那样做?他拒绝开源的理由究竟是什么?
2019 年 10 月 25 日,就在 Kalcor 于 SA:MP 官方论坛发布某条帖子后不久,俄语 Pawn 开发者社区 pawn.wiki 上出现了一篇由用户 m1n1vv 整理、转述并翻译的长文,标题是《SA-MP 0.3.9 与 Kalcor 的新故事》。这篇文章完整记录了 Kalcor 这篇帖子的核心内容——那是迄今为止他对自己技术履历与闭源立场最完整、最直接的一次亲口陈述。
以下是 Kalcor 在那篇帖子中所说的全部内容,经 m1n1vv 翻译并整理,原文以俄文发布于 pawn.wiki:
引用:那么,请允许我给你们讲一段历史。结论由你们自己得出。
2003 年,在 MTA 的 IRC 频道上待了一周之后,我被邀请加入 MTA:VC(Vice City Multiplayer)的开发。几个月后,我晋升为 MTA 的首席开发者。以下是一张截图,记录了我和 Si|ent 一起尝试让 MTA:VC 中的船只同步正常工作的画面,日期是 2003年12月31日。
我是 MTA:Blue 的少数开发者之一——这是 MTA 针对罪恶都市的新版本。那时候除我之外,大多数人都被现实生活拖住了。我开发了玩家上下文切换同步系统,并引入了 RakNet 库。同样的系统至今仍被 SA-MP 和 MTA:SA 所使用。
这种处境让我感到沮丧——一周又一周过去,只有我一个人在提交代码,而论坛上却有一堆"开发者"在大谈特谈一切将会如何运作。
于是我离开了 MTA 的开发。在我离开期间,另一个叫 eAi 的人加入了项目,开始研究我的代码,试图弄清楚它是怎么工作的。至少有人在做事了——他的方式是去主动联系各方,于是他通过 ICQ 找到了我,还找到了 R(Rockstar Games)开发者的电子邮件地址,并与他们取得了联系。*
那时候我对 GTA 多人联机并不感兴趣。我有一个乐队,还参与了 ipodlinux 等项目,但 eAi 似乎非常热衷于推动 MTA:Blue 取得进展。于是我重新加入了项目,持续了几个月。
以下是 2004年底 MTA:Blue 的公开宣传页面存档。
正是这件事导致了我和 MTA 其他开发者之间的决裂:我想要构建一个服务端系统——就像 VC-MP 的 ini 脚本系统,以及 SA-MP 中基于 Pawn 脚本的服务端系统;而 MTA 的开发者们则想要客户端扩展,就像那个宣传页面上描述的那样。
2005年初,我再次离开了 MTA。就在那段时间,R North 在 PS2 上发布了 GTA:SA,并正在开发 PC 版本。eAi 与 R 的技术总监有联系,还通过 ICQ 向我炫耀说,他拿到了大量 GTA:SA 源代码的头文件。我注意到,MTA 的开发者们似乎打算放弃罪恶都市版的 MTA:Blue,转战 GTA:SA。
不相信 MTA 及其开发能力,2005年中,我开始独立开发自己的多人联机。我发布了 VC-MP,基于玩家上下文切换和 RakNet。
随后,我与 jax、spooky 以及其他一些人一起开发了 SA-MP。它远比之前的 mod 庞大得多。我们与 GTANet/GTAForums——最大的 GTA 非官方粉丝站——有着较为密切的联系,这使我们获得了更大的知名度。
到 2008 年,SA-MP 的玩家数量是 MTA 的 10 到 20 倍。MTA 的开发者们感到在这种竞争环境下已无法与之抗衡,于是他们公开了自己的源代码。MTA 的开发者们从未询问过我或任何前任开发者,是否可以公开这些源代码。他们声称所有东西都已经被重写了,但事实并非完全如此。时至今日,MTA:SA 中仍然有我的代码。我没有费心去追究他们,因为他们看起来太蠢了——但我猜,这里那些呼吁公开 SA-MP 源代码的人,很可能是受到了 MTA 的启发,尽管他们或许不应该这样。
而这就是现在正在发生的事:我为 MTA 开发的"游戏"代码,以及 R\ 给 MTA 的源代码,正在被移植进 mod\so\beit——一款流行的 SA-MP 外挂工具。*
你们知道这意味着什么吗?R\ 给 MTA 的那部分源代码,有相当大一部分被用来——攻击 SA-MP!*
从一小批相当专业的 MTA 开发者起步,这件事已经演变成了极度肮脏的东西。那些想要摧毁 SA-MP 的人,对于是否违法毫不在意。
看起来,任何被公开的源代码都可能被用来攻击 SA-MP——尽管它本可以被用于提供有价值的更新。
公开 SA-MP 源代码这个决定,归根结底取决于:我们是否仍然处于这种充满敌意的环境中——人们在攻击我、想要攻击 SA-MP——还是这一切已经成为过去。我想说,这取决于 mod 的规模以及对外挂工具的需求程度。封闭源代码让我可以在每次发布时重新洗牌所有内部结构,使大量外挂工具失效。SA-MP 的许多"安全"更新,实质上就是我在重新排列数据,以清除现有的外挂工具。
最终,SA-MP 将会走到这样一个时刻:所有人都彼此友善和尊重,外挂与欺诈变得罕见——但届时玩家数量将极为稀少,我们中的大多数人也已不在了。
——Kalcor,2019年10月,SA:MP 官方论坛
(由 m1n1vv 转述并整理,经 Типичный Скриптер 译为俄文,发布于 pawn.wiki,2019年10月25日)
这篇陈述,是理解 Kalcor 其人与其决策的关键文献。
它第一次完整地揭示了他的来历:他不是凭空出现的 SA:MP 创始人,而是一个从 MTA 内部出走的人。2003 年至 2005 年间,他在 MTA 团队中担任首席开发者,亲手构建了玩家同步系统与 RakNet 架构——而他愤而离开,正是因为他坚持的"服务端脚本系统"方向遭到其他成员的反对。SA:MP 的诞生,某种程度上是一次对 MTA 的报复性创业。
它也第一次给出了 Kalcor 拒绝开源的真实逻辑:他不是在藏私,也不是单纯的控制欲作祟——在他看来,闭源是他对抗外挂工具的核心武器。他承认 SA:MP 的许多所谓"安全更新",实质只是重新排列内部数据结构,使依赖固定内存地址的外挂工具在新版本中失效。一旦开源,这条防线便彻底坍塌。
而他与 MTA 之间的积怨,在这篇文章里也首次有了完整呈现:他认为 MTA 在开源自己的代码库时,从未征得他这位最初作者的同意;更令他愤怒的是,他随后发现,R\* 曾向 MTA 提供的部分 GTA:SA 源代码头文件,辗转流入了 SA:MP 外挂工具的制作者手中——成为攻击 SA:MP 的武器。这份偏执与防御,贯穿了他此后对待社区和开源请求的所有态度。
这篇发布于 2019 年 10 月的文章,距 Kalcor 宣告放弃 SA:MP 仅剩不到一个月。这是他在离开之前,最后一次试图解释自己。
|
|
|
|
| [考古] openmp 常见问题解答 | 2020年4月 |
|
发布者: 小鸟unsigned - 03-24-2026, 04:55 PM - 板块: 综合讨论
- 暂无回复
|
 |
GTA SA 和 SA:MP 中的漏洞会被修复吗?
会的!
你也可以向我们反馈我们尚未知晓的 bug:github.com/openmultiplayer/samp-bugs/
或在论坛提交:burgershot.gg/showthread.php?tid=99
open.mp 会支持哪些 SA:MP 版本?包括 0.3DL 吗?
我们不会支持 0.3.7 以下的版本,但你可以自行实现(这正是开源的魅力所在)。
如果大多数服务器都尝试使用 0.3DL,我们会跟进支持。我们也做过调查,多数人支持 0.3DL,所以这件事大概率会做,但不会出现在客户端的前几个版本中。
open.mp 客户端会有固定的更新周期吗?
不会。固定周期需要一支流程成熟的大型团队,而我们目前是一支志愿者队伍。
也许情况将来会有所改变,我们不敢打包票。代码开源之后,你们可以随时关注项目进展。我们会尽一切努力在版本更新之间保持与社区的沟通。
我们可以提交新功能建议吗?
我们非常欢迎社区参与,你可以在这里留下你的建议:burgershot.gg/forumdisplay.php?fid=42&page=6
当然,我们无法保证每个想法都会被采纳和实现。但由于代码是开源的,你随时可以自己动手实现。
会有反作弊系统吗?
会有。
open.mp 会继续使用 Pawn 吗?可以使用其他语言吗?
所有的开发工作、测试和文档均以 Pawn 为基础完成。不过,我们提供了 C API,允许任何人在此之上实现其他语言的支持。
我们也讨论过引入一些半官方的语言模块,但 Pawn 依然是主要且受官方支持的脚本语言。
SA:MP 的插件能在 open.mp 中使用吗?
"插件"这个词目前用来指代旧式插件——即那些为 SA-MP 编写的插件(例如 sscanf、MySQL 等)。open.mp 提供了加载这些现有插件的 API。
我们对新式插件的称呼是"模块"——无论是外部插件还是内部代码,只要使用 open.mp 更新、更强大的 API,统称为模块。
旧式插件仍然可以正常运行,并会持续得到支持,除非它们依赖内存黑客手段(例如 YSF),因为那类插件依赖特定的内存地址和数据结构,而这些在 open.mp 服务端上是完全不同的。不过,大多数老式内存黑客当初存在的意义,恰恰是为了补全一些功能——而这些功能将直接内置于 open.mp,所以那些插件本来也就用不着了。
数据库功能只能用 SQLite,还是也可以用 MySQL?
默认的数据库功能与以前一样,仍然是 SQLite。但如前所述,open.mp 提供了加载现有插件的 API,因此数据库方面无需在内部做任何改动——通过现有插件依然可以实现。
第三方模块在 open.mp 中是完全受支持的"一等公民"——无论是回调还是原生函数,其可用性与内部代码完全一致,因此将功能放入外部模块不存在任何劣势。
HUD、体力值、小游戏(台球、健身房等)、车辆选项、路线指示、SA:MP+ 功能等会有更多自定义选项吗?会支持更灵活的图形界面,而不是像 SA:MP 那样只能用有限的文字绘制吗?CEF 呢?这些内容会如何保障安全?
关于这个话题我们收到了太多提问,请先去泡杯茶——这个回答会有点长。
我们的开发团队里有 Hual(SA:MP+ 的作者),未来会带来什么,谁知道呢??
SA:MP 与 MTA 最本质的区别,其实在名字里就说得很清楚了——SA:MP 就是字面意思上的"圣安地列斯多人游戏",是 GTA 圣安地列斯这款游戏的多人版本;而 MTA 则是一个架设在这个世界观之上、更偏向通用平台的存在。
SA:MP 在加入新功能时,始终紧扣原版游戏的体验感,尽管走出框架并非不可能,但整体风格一直非常保守。
我们之所以长期坚守 SA:MP,正是因为认同这种态度。但我们希望拥有更多一点的自由度(这也是我们另起炉灶的原因),同时又不想走得太远。我们会引入一些新功能,其中有些在原版游戏中并不存在,但我们仍然会审慎地权衡每个功能带来的影响。
让对话框稍微宽一点,算是突破 GTA 正统边界的疯狂改动吗?大概不算。
加入 CEF 是否走得太远了?是的,这不会发生。?♂️
平衡点就在两者之间。
话虽如此,玩家们向来能在有限的支持下迸发出令人惊叹的创造力,我们相信这一点不会改变。我们会始终鼓励玩家和开发者尽情发挥创意——这也是我们选择开源的原因之一,我们甚至会在这方面提供帮助,但这不会是我们的核心任务。
我们最主要的顾虑集中在客户端层面。我们不希望服务器主可以向玩家分发任何具有潜在危害的内容。圣安地列斯本身并非为多人游戏或 mod 而设计,许多文件加载器对损坏或恶意文件缺乏防护——当初根本没有这方面的需求。这正是 SA:MP 在引入自定义模型时遇到的难题,对我们来说同样如此。
每当我们要对游戏的某个部分进行自定义,都必须先确保对应的文件格式和加载器不存在可利用的漏洞。我们可以确认,模型和脚本已经出现过被攻击的案例,其他部分可能也已经被攻击,而一旦我们开始允许服务器上传任意文件,被攻击几乎是必然的。
至于资源保护——坦率地说,这在根本上是无法实现的。要让客户端显示一个自定义模型,就必须把这个模型文件传给它。文件一旦传到客户端,它就在对方的电脑上了,对方拿它怎么做,我们管不了。有些方法可以让盗取变得麻烦一些,但没有哪种方法是绝对可靠的。网络上虽然存在 DRM、安全隔离区等手段,但要使用这些技术,就必须牺牲开源原则。可以参考一下 EME 标准以及 Firefox 中内嵌闭源二进制文件所引发的争议。
NPC 会有更好的支持吗?
这不是我们当前的优先事项,但从客户端角度来看,NPC 本质上与普通玩家没有区别。我们至少希望 open.mp 的 NPC 功能能够对标 FCNPC 插件的水平,这是未来版本中我们可以认真考虑的方向。
你们说 open.mp 移除了所有限制——这具体是怎么实现的?它像 streamer 插件一样吗?
原理类似,但与 streamer 相比集成度更高,因此我们同样可以对载具和玩家进行流式处理,并能以远超插件能力的方式操作本地 ID。
玩家可以被附加到物体上吗?
可以,我们已经实现了这个功能。
open.mp 完成 SA:MP 功能的复现之后,接下来会新增什么?
我们很喜欢聊未来的计划——持续关注我们就好,你们会看到的。
可以用 SA:MP 客户端连接 open.mp 服务器吗?
SA:MP 是通过逆向工程对第三方程序进行交互的 mod,open.mp 也是如此,我们的法律立场与 SA:MP 完全相同。我们没有使用任何 SA:MP 的泄露源代码——这一点已经反复强调,代码开源之后,社区自然会看得清清楚楚。
新版 open.mp 客户端最大的挑战是什么?
把它写出来!
五年后,你们认为 open.mp 会是什么样子?
不必担心这个,我们有很多长远的想法。?
五年后,你们认为 SA-MP 会是什么样子?
那是 SA-MP 团队的事,不是我们该操心的——在这个问题上我们没有立场去猜测。
你们有没有想过放弃 open.mp?
有过,因为我们也是普通人。
最后,也是被问得最多的问题:
我们什么时候才能看到 open.mp?
等它准备好了。???
祝大家接下来的一周愉快!
带着满满的爱,
open.mp 团队 ?
|
|
|
|
| [考古]MTA 团队与 open.mp 团队早期就潜在合作可能性展开的公开对话 | 2019年5月 |
|
发布者: 小鸟unsigned - 03-24-2026, 04:53 PM - 板块: 综合讨论
- 暂无回复
|
 |
Jusonex,MTA 团队代表:
你好,
在正式开始之前,先允许我做个自我介绍。我是 MTA 团队的成员,长期深度参与多人游戏模组的开发工作。本帖内容也已获得团队其他成员的认可。
过去这几天,我们一直在默默关注 SA-MP 的近况,以及你们测试团队的动向。我们刻意等待了一段时间,让局势稍微平息,这也是我们迟迟没有表态的原因。
在我们看来,眼下的局面对 MTA:SA 和 SA-MP 来说,恰恰是一次难得的相互靠拢的机会。我们理解你们对现状的不满,但我们认为,为《GTA 圣安地列斯》这款已有相当历史的游戏再从零搭建一个多人游戏项目,似乎并不是最明智的选择。我们对圣安地列斯的热爱,与你们一样深厚。尽管近来在线玩家数量基本保持稳定,但不可否认的是,吸引新玩家的难度正在与日俱增——尤其考虑到玩家群体结构的变化。
因此,我们诚邀你们加入 MTA 社区。从技术层面来看,我们认为最务实的方案是实现从 SA-MP 到 MTA 的兼容层,这样也能帮助你们更快推进前期更新的发布。
说几个数据:过去三年,我们发布了四个新版本,并持续推出大量迭代更新和反作弊升级;而 SA-MP 的最后一次官方发布,距今已超过四年(依据 sa-mp.com 官方数据)。
除少数例外情况,MTA 在技术上已基本能够"模拟"SA-MP 的全部功能。我们相信,凭借你们作为资深开发者的经验,双方完全可以共同实现客户端兼容。
此外,我们已有一个名为 amx 的资源(https://github.com/multitheftauto/mtasa-resources/tree/master/%5Bgamemodes%5D/%5Bamx%5D),可在 MTA 内部运行 Pawn 虚拟机。该资源目前版本较旧,可能需要重构,但核心功能依然可用。
我们相信这将是一个对双方都有利的合作,也是两个社区难得的机遇——不仅能汇聚各自优秀的开发者和新鲜想法,还能为玩家带来真正丰富的游戏体验,甚至可能吸引新玩家(例如 MTA 的粉丝群体)加入,并催生一批概念全新的脚本功能。
如果你们对潜在问题有任何顾虑,欢迎随时与我们深入探讨。
谨致问候,代表 MTA 团队。
JustMichael,论坛跟进者:
我们需要在团队内部充分讨论之后,才能就该提案给出正式回应。这绝非一个小提案,我们需要时间消化并核实帖子中提及的所有信息。因此,请不要将这条回复理解为拒绝——我们绝对没有在拖延,会尽快给出正式答复。另外,请始终将 J0sh 的名字写作"J0sh……"。
Jusonex,MTA 团队代表:
引用:BloodMaster 写道:「SA-MP 保留了圣安地列斯原汁原味的氛围和外观,而 MTA 则是一个可以高度自定义的引擎——这正是 SA-MP 吸引更多玩家的根本原因。」
这个观点我之前也看到过几次,确实触及了一个重要方面。MTA 确实拥有某种"可自由改造的自定义引擎",但我个人并不认为这是一个负面特质。毕竟,没有人强迫任何人把游戏做成与原版圣安地列斯截然不同的样子——这完全是开发者自己的选择。况且,随着功能的不断叠加,你们迟早也会面临类似的情况。
引用:iReal Worlds 写道:「Lua……认真的吗?」
依我个人之见,Lua 是一门非常出色的多范式语言,同时支持命令式、面向对象乃至部分函数式编程风格。更重要的是,它在设计之初就以"嵌入其他应用程序"为核心目标,这在客户端脚本的安全执行方面具有显著优势,是其他语言难以比拟的。
此外,我还想提一个近期由 MTA 社区打造的有趣项目:https://mta-slipe.com/。它本质上是将 C# 引入到 Lua 环境中。
引用:michael@belgium 写道:「想象一下所有人都切换到 Lua……」
这并不是我的提议。我们谈论的是兼容性方案——让 SA-MP 的模组和插件(用 Pawn 编写)无需任何修改,即可直接在 MTA 中运行。如果 open.mp 的开发者计划支持更多脚本语言,我们也可以将 Pawn 作为 MTA 的附加语言引入(包括对 SA-MP API 脚本的支持)。
MyU,open.mp 开发者:
首先声明:以下内容纯属个人观点,与 open.mp 项目及团队立场无关。
事实上,已经有一些人指出,MTA 和 SA-MP 本质上是两种完全不同的东西——确实如此,差异相当显著。MTA 走的是不断创新的路线,而 SA-MP 则始终"守着老传统";正因为长期没有更新、也不理会社区的功能需求,才会是今天这个局面。
我个人两款都玩过(MTA 是大约两三年前,当时 OOP Lua 刚加入不久,玩了一两个月)。玩了一段时间后,MTA 对我来说变得有些"吃不消"——那时候我的电脑配置比较低,加上遇到了一些 CEF 相关的问题,虽然我相信这些问题现在应该都已经解决了。
SA-MP 相当"轻量":一个压缩包、一个 samp.dll、一个启动器,再加一两个文件,就全了。而 MTA:SA 则捆绑了 CEF 文件、CGUI 文件、本地化文件等一大堆内容。我个人认为,把这两个多人游戏合并在一起是行不通的——它们太不一样了,目标也各不相同。SA-MP 里安装 mod 极其简单,丢进游戏文件夹就能用;MTA:SA 则更像是一个经过精心打磨、功能完整的多人游戏模组,但玩家有自己的偏好,这无可厚非。
简单总结一下:
- SA-MP → 轻量、易上手,但在某些方面存在局限;
- MTA:SA → 体量较重,但功能全面,有时略显复杂,稳定性却强于 SA-MP(玩家们甚至会利用 MTA:SA 的崩溃报告来排查问题——这本身就说明很多问题)。
不过我认为,玩家并不希望社区就此一分为二、各走各的路。竞争本身,以及由此带来的优胜劣汰,对整体发展是有益的。
Jusonex,MTA 团队代表(回复 MyU):
性能问题在绝大多数情况下都源于脚本编写质量低下(尤其是客户端脚本写得糟糕)。"能力越大,责任越大"这句话在这里再合适不过了,虽然这听起来有点无奈。
Sasino97,open.mp 软件开发者:
我认为加入 MTA 最大的好处,是可以直接接触到庞大的现有玩家群体,以及 MTA 客户端本身极为强大的功能上限。至于缺点嘛……他们的论坛实在是太丑了,抱歉直说。
不过,我个人很认可 open.mp 这个项目的理念,所以我觉得两边(MTA 和 open.mp)还是应该作为独立项目,各自继续发展下去。
|
|
|
|
| 服务器开发 精选资源清单 |
|
发布者: 小鸟unsigned - 03-22-2026, 12:24 AM - 板块: 发布
- 暂无回复
|
 |
为 SA-MP 开发精选的实用工具、库、游戏模式、滤镜脚本和插件列表。
工具
库
命令处理器
插件
客户端-服务器插件
加密插件
GDK/SDK
现在,你可以使用 open.mp 在不借助任何插件的情况下,用 Pawn 以外的语言编写脚本。请参阅 这篇博客文章。
游戏模式
滤镜脚本
|
|
|
|
| [教程] 枚举器 enum 详细讲解 原文作者: iPLEOMAX |
|
发布者: 小鸟unsigned - 03-22-2026, 12:11 AM - 板块: 教程
- 暂无回复
|
 |
枚举器
原文作者: iPLEOMAX
关于枚举,有些细节很多脚本作者并不清楚。
很多人都喜欢在脚本里用枚举来存玩家数据、车辆数据、房屋数据之类的,尤其是用户信息这块。
一个很典型的写法是这样的:
代码:
enum E_PLAYER_INFO
{
SCORE,
MONEY,
KILLS,
DEATHS
};
new pInfo[MAX_PLAYERS][E_PLAYER_INFO];
用起来大概是这样:
代码:
public OnPlayerDeath(playerid, killerid, reason)
{
pInfo[playerid][DEATHS]++;
if(IsPlayerConnected(killerid) && killerid != playerid)
pInfo[killerid][KILLS]++;
return 1;
}
这段应该不难理解。
现在我们看另一个例子:
代码:
enum E_PLAYER_INFO
{
SCORE,
MONEY = 9,
KILLS = 5,
DEATHS = 56
};
new pInfo[MAX_PLAYERS][E_PLAYER_INFO];
printf("%i | %i | %i | %i", pInfo[0][SCORE], pInfo[0][MONEY], pInfo[0][KILLS], pInfo[0][DEATHS]);
你可能会觉得输出是 0 | 9 | 5 | 56,对吧?
如果你这么想,那就错了。实际上输出是 0 | 0 | 0 | 0。
你可能会以为枚举是用来存储数据的,但事实并非如此。我在枚举里写了 MONEY = 9,然后在 printf 里用了 pInfo[0][MONEY],但编译器其实把它当成 pInfo[0][9] 来处理,而不是什么 E_PLAYER_INFO:MONEY。也就是说,MONEY 在这里只是作为一个索引来用的。
所以枚举本质上根本不是变量,它其实就是一组常量,只不过帮你的数组索引起了个好记的名字罢了。
好,我们再来深入一点,应该能让你理解得更清楚。
举个例子:
代码:
const e_CAR1 = 0;
const e_CAR2 = 1;
const e_CAR3 = 2;
new MyCars[3];
main()
{
MyCars[e_CAR1] = 520;
MyCars[e_CAR2] = 458;
MyCars[e_CAR3] = 411;
}
现在我们用枚举重写一下:
代码:
enum e_TEST
{
e_CAR1,
e_CAR2,
e_CAR3
};
new MyCars[e_TEST];
main()
{
MyCars[e_CAR1] = 520;
MyCars[e_CAR2] = 458;
MyCars[e_CAR3] = 411;
}
这两段代码都能正常编译,做的事情也完全一样。你看出区别了吗?其实就只是代码写法不同而已。
这个例子告诉我们,枚举干的事情和常量是一样的。不过枚举有些地方比常量或者宏定义(const e_CAR1 = 0; 或者 #define e_CAR1 0)更好用,具体差异我们后面再说。
首先你需要明白枚举的结构:
代码:
enum TEST
{
Abc,
Def,
Ghi,
};
Abc 是一个值为 0 的常量(差不多等于 const Abc = 0; 或 #define Abc 0),Def 是值为 1 的常量,Ghi 是值为 2 的常量。
预编译器会自动给这些常量赋值,从 0 开始一直往上排。而用常量的时候,你得自己手动赋值。枚举则是自动帮你排好的。
再看个例子:
代码:
enum TEST
{
e_ONE, // 自动得到 0
e_TWO, // 得到 1
e_THREE, // 得到 2
e_FOUR = 12,// 这里手动赋了 12,所以不再是 3 了
e_FIVE, // 变成 13,因为上一个值是 12
e_SIX // 变成 14,上一个值是 13
}
看到了吧,如果你手动给某个枚举项赋了值,后面的项就会在此基础上依次加 1。
再来看看更复杂的情况:
代码:
enum DATA
{
INT, // 得到 0
STRING[10], // 得到 1,但因为是数组,一个位置不够,它会占 10 个位置
// 所以实际占用的索引是 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
INT2, // 得到 11,因为 STRING 最后一位是 10
STRING2[10] // 从 12 开始,一直到 22,需要 10 个位置
};
运行下面的代码:
代码:
printf("%i", _:DATA);
结果输出 22。
因为整个枚举的总大小是 22:
INT 占 1 个 + STRING 占 10 个 + INT2 占 1 个 + STRING2 占 10 个 = 22。
你可能会问,为什么我写 INT 是 1 而不是 0?因为这里我数的是占用了多少个位置,不是从哪个位置开始。如果问起始位置,那 INT 确实是 0。
一个大小为 22 的块,索引范围是 0 到 21,总共 22 个位置。
再试一下:
代码:
printf("%i", _:STRING);
输出 1。因为第一个位置(索引 0)被 INT 占了,所以 STRING 从索引 1 开始。
每个枚举项占用的位置是这样的:- INT:索引 0
- STRING:索引 1 到 10
- INT2:索引 11
- STRING2:索引 12 到 21
运行这段:
代码:
new Array[DATA];
printf("%i", sizeof Array);
结果也是 22,因为 DATA 的大小就是 22。
所以,这个数组实际上相当于:
代码:
Array[INT + STRING + INT2 + STRING2];
这行代码是编译不了的,只是帮你想明白结构而已。
也就是说,用枚举的时候,你其实是用这些枚举常量作为数组的索引。
另一个例子
代码:
enum TEST
{
SomeInteger = 124,
SomeString[12],
Float:SomeFloat
};
public OnFilterScriptInit()
{
new var_TEST[TEST];
var_TEST[SomeInteger] = 1337;
printf("1) 储存的值: %i, 常量: %i", var_TEST[SomeInteger], _:SomeInteger);
format(var_TEST[SomeString], 12, "Hey!");
printf("2) 储存的值: %s, 常量: %i", var_TEST[SomeString], _:SomeString);
var_TEST[SomeFloat] = 2054.124;
printf("3) 储存的值: %f, 常量: %i", var_TEST[SomeFloat], _:SomeFloat);
return true;
}
输出结果:
储存的值: 1337, 常量: 124
储存的值: Hey!, 常量: 125
储存的值: 2054.124023, 常量: 137
第一行,SomeInteger 的常量值是 124,我把 1337 存到了索引 124 的位置。
第二行,SomeString 虽然定义了长度为 12,但输出常量值时显示的是 125。因为 125 是这个字符串的起始索引,它占了 125 到 136 这 12 个位置。
第三行,浮点数正常显示,但 SomeFloat 的常量值是 137。你可能会想,它应该在 SomeString(125)后面,应该是 126 才对?但别忘了 SomeString[12] 是个数组,占了 12 个位置,所以 SomeFloat 的起始索引是 125 + 12 = 137。
一些额外的重要信息
枚举也可以作为标签使用:
代码:
enum E_MY_TAG
{
E_FIRST = 4,
E_SECOND = 2
}
然后可以这样写:
代码:
new E_MY_TAG:SomeVar;
或者:
代码:
new E_MY_TAG:MyVariable = E_FIRST; // 不会报错,因为 E_FIRST 本来就是 E_MY_TAG 的一部分
但是:
代码:
new data[E_MY_TAG];
data[E_FIRST] = 7; // 编译错误,索引越界
因为 E_MY_TAG 的大小是 3(枚举里最大索引是 2),而 E_FIRST 的值是 4,已经超出范围了。
匿名枚举:
代码:
enum // 没有名字
{
E_TEST[10] = 32,
E_VAR
};
main()
{
new data[E_TEST]; // E_TEST 的值是 32,不是 10
}
强标签和弱标签:
代码:
enum E_STRONG // 名字以大写字母开头,生成的是强标签
{
E_VAR = 64
};
main()
{
new Test = E_STRONG:E_VAR;
// 会报“标签不匹配”的警告,因为 Test 没有 E_STRONG 标签
}
代码:
enum e_WEAK // 名字以小写字母开头,生成的是弱标签
{
E_VAR = 64
};
main()
{
new Test = e_WEAK:E_VAR;
// 没有警告
}
枚举的大概内容就是这些了,这也是为什么我们更喜欢用枚举而不是直接写常量的原因。
感谢 Y_Less 的讲解。
|
|
|
|
| [教程] Pawn 中的压缩字符串 原文作者: Emmet_ |
|
发布者: 小鸟unsigned - 03-21-2026, 11:52 PM - 板块: 教程
- 暂无回复
|
 |
Pawn 中的压缩字符串
原文作者: Emmet_ 本篇教程仅为翻译搬运
引言
压缩字符串自 SA-MP 和 Pawn 诞生之初就已存在。然而,很多人并不了解压缩字符串以及它能节省多少内存!本教程将教你压缩字符串的基础知识,以及如何正确操作它们。
什么是压缩字符串?
压缩字符串是一种数组,它将数据存储在每个字节中,而不是像普通数组那样存储在每个单元格中。压缩字符串以小端序存储(即低位字节在前),并且只能容纳 0 到 255 的 ASCII 字符,超出这个范围的数值会绕回。
看这段代码:
代码: new string[5];
string[0] = 'a';
string[1] = 'b';
string[2] = 'c';
string[3] = 'd';
string[4] = '\0';
'a' 存储在单元格 0,'b' 存储在单元格 1,依此类推。一个单元格基本上占用 4 个字节,所以算一下,上面的字符串大约占用 20 个字节,每个字符占用 4 个字节的空间。
然而,使用下面的代码:
代码: new string[5 char];
string{0} = 'a';
string{1} = 'b';
string{2} = 'c';
string{3} = 'd';
string{4} = '\0';
'a' 存储在字节 0,'b' 存储在字节 1,依此类推。实际上,上面的字符串只占用 8 个字节,仅包含 2 个单元格!
你可能在想为什么这个字符串不是 5 个字节。使用 char 修饰符会自动将数组大小向上取整到最近的 4 的倍数(例如,1 变成 4,3 变成 4,5 变成 8,23 变成 24,依此类推)。
代码: // 这个数组占用 5 个单元格,共 20 个字节。
new string[5] = "abcd";
// 这个数组占用 2 个单元格,共 8 个字节。
new string[5 char] = !"abcd";
因此,使用压缩字符串可以节省 3 到 4 倍的内存!
适用场景
你可能认为压缩数组没什么用。如果这么想,那你就错了。压缩数组有很多用途,不仅仅是节省内存!
稀疏数组
首先,你可以阅读我关于稀疏数组的教程:
https://sampforum.blast.hk/showthread.php?tid=480439
稀疏数组就是那些大部分数据经常为空的数组。使用压缩数组可以轻松节省内存!
代码: #define MAX_ITEMS (64)
// 8,192 个单元格 = 32,768 字节!
new gData[MAX_ITEMS][128];
// 2,048 个字节 = 8,192 个单元格!
new gData[MAX_ITEMS][128 char];
不常使用的字符串
很多时候,你会把字符串保存到内存中,但很少使用它们(例如几乎不用)。对于这类数组,压缩数组非常适用。
大量数据存储
回到上面的“稀疏数组”部分,如果你需要存储大量数据,最好使用压缩字符串。
内存节省!
压缩数组能节省 4 倍的内存,既然可以用压缩数组,为什么还要用那些多占 4 倍内存的方式存储数据呢?
SA-MP 中的不支持情况
压缩字符串在纯 Pawn 中完美支持。但是,大多数 SA-MP 原生函数不支持压缩字符串,比如 format、GetPlayerName 等。
如果你打算使用压缩字符串,就必须依赖 strpack、strunpack 以及 string.inc 中的其他字符串函数。
格式化字符串
format 和 printf 函数不支持压缩字符串,因此你必须使用 strpack:
代码: new
string[128 char];
strpack(string, "Hello world!");
你也可以这样做:
代码: new
string[128 char],
temp[128]
;
strpack(string, "Emmet");
strunpack(temp, string);
format(temp, sizeof(temp), "%s likes to eat %s.", temp, "Big Macs");
strpack(string, temp);
支持压缩字符串的函数
string.inc 中的所有字符串函数都同时支持压缩数组和非压缩数组。
fread 和 valstr 函数也接受一个可选的 pack 参数来支持压缩数组,因此你不用担心它们是否能与压缩数组一起工作。
访问数据
在 Pawn 中,非压缩数组将数据存储在每个单元格中。而压缩数组将数据存储在每个字节中,这意味着你不能像访问非压缩数组那样使用方括号 [] 来访问和获取压缩数组中的数据。
代码: // 错误。
if (g_PackedString[0] != '\0')
{
g_PackedString[0] = 'h';
g_PackedString[1] = 'i';
}
// 正确!
if (g_PackedString{0} != '\0')
{
g_PackedString{0} = 'h';
g_PackedString{1} = 'i';
}
另外,要设置一个压缩字符串,你需要在字符串前加上感叹号,以表示这是压缩输入。
代码: // 错误。这会尝试将字符串按单元格存储,这不是我们想要的!
g_PackedString = "Hello world.";
// 正确!
g_PackedString = !"Hello world.";
|
|
|
|
|