| 欢迎, 游客 |
您必须先 注册 才能在我们的网站上发帖.
|
| 在线用户 |
当前共有 2 位在线用户. » 0 会员 | 2 游客
|
| 最新主题 |
PawnREST - HTTP/S 文件传输与 R...
板块: 插件
最后发表: siwode
06-11-2026, 09:59 PM
» 回复: 0
» 浏览: 46
|
网页版 TextDraws在线编辑
板块: 发布
最后发表: siwode
06-11-2026, 09:39 PM
» 回复: 0
» 浏览: 17
|
西部角色扮演 Wild-West-Roleplay
板块: 游戏模式
最后发表: siwode
06-11-2026, 09:19 PM
» 回复: 0
» 浏览: 27
|
[插件] kook-connect
板块: 插件
最后发表: siwode
06-08-2026, 11:04 AM
» 回复: 4
» 浏览: 213
|
openmp/samp联机服务器插件开发 完全指南
板块: 教程
最后发表: 柚子爱吃包子
04-09-2026, 09:14 PM
» 回复: 2
» 浏览: 293
|
圣安地列斯联机二十年:SA:MP 与 open.m...
板块: 综合讨论
最后发表: siwode
03-26-2026, 12:40 PM
» 回复: 3
» 浏览: 358
|
[考古] openmp 常见问题解答 | 2020...
板块: 综合讨论
最后发表: 小鸟unsigned
03-24-2026, 04:55 PM
» 回复: 0
» 浏览: 81
|
[考古]MTA 团队与 open.mp 团队早期就...
板块: 综合讨论
最后发表: 小鸟unsigned
03-24-2026, 04:53 PM
» 回复: 0
» 浏览: 76
|
服务器开发 精选资源清单
板块: 发布
最后发表: 小鸟unsigned
03-22-2026, 12:24 AM
» 回复: 0
» 浏览: 244
|
[教程] 枚举器 enum 详细讲解 原文作者: ...
板块: 教程
最后发表: 小鸟unsigned
03-22-2026, 12:11 AM
» 回复: 0
» 浏览: 97
|
|
|
| [修仙者篇] AMX 汇编文档 |
|
发布者: 小鸟unsigned - 03-21-2026, 11:19 PM - 板块: 教程
- 回复 (1)
|
 |
AMX 汇编
AMX 版本: 3.0
AMX 文件版本: 8
本文档是关于抽象机执行器(Abstract Machine eXecutor)汇编的非官方文档。本文档的内容可能不准确且可能随时更改。术语部分中定义的部分术语在官方手册中并未出现。它们是为了帮助读者理解而引入的辅助术语。
本文档的目标读者是首次接触汇编语言的程序员。他们应根据需要点击超链接以收集背景信息。高级用户可跳过部分章节或将本文档用作快速参考指南。
目录
先决条件
术语
<a name="term-amx"></a>抽象机执行器 (AMX):AMX 是我们感兴趣的抽象机(或虚拟机)。
<a name="term-amx-assembly"></a>AMX 汇编:AMX 的汇编语言。
<a name="term-amx-binary"></a>AMX 二进制:AMX 二进制文件是编译后生成的可执行文件(通常扩展名为 .amx)。术语 AMX 程序在某些上下文中可能指代 AMX 二进制文件。
<a name="term-amx-instance"></a>AMX 实例:每个加载的 AMX 二进制文件独立存在。加载后的程序称为 AMX 实例。术语 AMX 程序在某些上下文中可能指代 AMX 实例。
<a name="term-host-program"></a>宿主程序:嵌入 AMX 的程序称为宿主程序。以 San Andreas Multiplayer (SA-MP) 或 open.mp 为例,服务器就是宿主程序。
<a name="term-extension-module"></a>扩展模块:扩展模块是一个独立的模块,通常动态链接到宿主程序,提供额外的原生函数。扩展模块通常被称为插件。
基本概念
- ## <a name="concept-cell"></a> 单元格概念
Pawn 是一种无类型语言:没有数据类型。所有数据都作为原始二进制数据存储在一个单元格或一组单元格中。我们将使用有符号二进制补码表示法来传达一个或多个单元格的内容,而不是显式地用二进制写出来。
上述代码创建了两个独立的单元格,分别由 a 和 b 标识。单元格 a 将包含值 5,单元格 b 将包含值 65(字符 'A' 的 ASCII 等效值)。
为了弥补数据类型的缺失,Pawn 提供了为单元格添加标签的能力。这些标签仅仅是编译时的辅助工具,不会直接<sup>1</sup>出现在 AMX 二进制文件中。
代码: new Float:x = 5.0, Float:y = 10.0, Float:z;
z = x + y;
上述代码创建了三个带有标签的单元格,由 x、y 和 z 标识。它们分别被初始化为值 5.0、10.0 和 0.0(默认初始化为二进制零,也是 0.0),其二进制表示符合浮点数的正确表示。编译器会记住遇到的变量的标签,并确保在变量被使用时生成正确的代码 <sup>2</sup>。在上述例子中,编译器在处理第二行时,会生成正确的代码来执行两个浮点数 x 和 y 的加法。请记住,整数加法和浮点数加法的执行方式不同。编译器使用与单元格关联的标签来确定应对操作数执行哪种加法操作。
编译完成后,变量的标签信息会丢失。生成的 AMX 二进制文件将包含对 x 和 y 中存储的数据执行浮点数加法并将结果存储在 z 中的指令。
在运行时,浮点数加法会在不考虑脚本中 x 和 y 的标签原本是否为 Float 的情况下执行。
<a name="bc-cc-note-1"></a> [[1]](#bc-cc-note-1) Pawn 语言提供了 tagof 运算符,它返回与标签关联的编译时 id,该 id 可以被存储。编译器还会在 AMX 二进制文件的标签表中创建一个公开可访问的标签列表。
<a name="bc-cc-note-2"></a> [[2]](#bc-cc-note-2) Pawn 语言允许基于标签的运算符重载。编译器知道所有存在的运算符重载,并根据操作数的标签调用正确的重载。如果给定的操作数没有重载存在,编译器将默认使用无标签单元格的运算符(即整数算术)。
- ## <a name="concept-memory-addresses"></a> 内存地址
AMX 遵循平坦的字节可寻址内存模型,即 AMX 内存可以被认为是一个线性的字节集合
(平坦内存),每个字节都有一个唯一的地址(字节可寻址)。
上述代码创建了一个由五个单元格组成的数组,由 a 标识。这些单元格连续存储,即一个接一个地存储。假设单元格大小为 4 字节,如果存储 a[0] 的位置地址是 1000,那么存储 a[1] 的位置地址将是 1004。
代码: data: a[0] | a[1] | a[2] | a[3] | a[4]
address: 1000 1004 1008 1012 1016
所需知识:基地址,相对地址
所需知识:数组数据结构,元素标识符和寻址公式
另请参阅:字节序
- ## <a name="concept-stack-heap"></a> 栈和堆
每个 AMX 实例都包含一个内部 程序栈(也称为调用栈)和程序堆。这些是 AMX 程序中的内存区域,专用于一组任务。
栈负责:- 存储局部变量
- 存储函数参数
- 存储函数调用信息(例如参数个数)
- 提供临时存储
堆负责:- 为动态分配提供内存
- 存储作为引用/数组传递的默认参数
- 存储不是左值且作为可变参数传递的常量和表达式
###
代码: f(argc, ...) { }
main ()
{
new a, b, c[100];
f(1, 25);
}
在上面的代码片段中,变量 a、b、c 和常量 1 在栈上分配空间。常量 25 在堆上分配空间。引用和可变参数作为地址传递给函数。由于字面量 25 是一个常量且没有地址,因此在堆上分配了一个临时单元格来存储值 25。该单元格的地址被传递给函数。
- ## <a name="concept-instructions"></a> 指令
指令是离散的原子执行单元。大多数指令执行简单的任务,如基本算术。高级结构(如循环和条件语句)可以简化为一系列简单的指令。
例如,将值压入调用栈然后弹出的指令可能是:
汇编器为上述代码片段生成的二进制代码可能是:
push-opcode | 100 | pop-opcode
0x0013 0x0064 0x0014 或者可能是 0000 0000 0000 0000 0000 0000 0001 0011 0000 0000 0000 0000 0000 0000 0110 0100 0000 0000 0000 0000 0000 0000 0001 0100
是的,它只是一系列比特位,CPU(或者在我们这里是抽象机)知道如何解释这些比特位。当为物理硬件上的执行汇编时,这种表示被称为机器码(或原生码)。在为假想 CPU(如 AMX)上的执行汇编时,这种表示被称为 p-code。
另请参阅: 字节码
- ## <a name="concept-spc-vna"></a> 存储程序概念与冯·诺依曼体系结构
代码像数据一样驻留在内存中的想法被称为存储程序概念。这也意味着内存中的每条指令都有一个地址,就像数据一样。代码和数据都驻留在同一内存中的想法是冯·诺依曼体系结构的一个原则。这也意味着所有代码和数据共享一个地址空间。
AMX 使用一个公共内存来存储代码和数据。
- ## <a name="concept-hdc"></a> 头、数据段、代码段
可执行二进制文件及程序的内存结构根据其包含的内容被划分为逻辑上不同的区域。一个典型的二进制可执行文件至少包含一个头、一个代码段和一个数据段。头提供了程序本身的信息,例如程序入口点。
代码: HEADER
-------
CODE
-------
DATA
代码段以二进制形式(机器码/p-code)存储代码,而数据段存储数据,如全局变量和静态局部变量。
程序的内存结构通常不同。除了代码段和数据段外,它通常还包含程序栈和程序堆的区域。
代码: HEADER
-------
CODE
-------
DATA
-------
HEAP
|
STACK
AMX 二进制文件组织为三个部分:前缀、代码和数据(按顺序)。前缀部分包含有关 AMX 二进制文件的信息,例如代码和数据部分在二进制文件中的起始偏移量。代码部分存储 p-code,数据段存储全局变量和静态局部变量。
堆栈区域是在 AMX 二进制文件加载到内存后,使用前缀部分中的信息创建的。
另请参阅:内存分段,数据段,代码段,ELF 格式
- ## <a name="concept-registers"></a> 寄存器
了解处理器和内存位于不同的位置至关重要。
代码: |-------| |------------------|
| CPU | =============== | MEMORY |
|-------| BUS |------------------|
内存存储数据,CPU 对数据进行操作。对于每次操作,处理器都必须将操作数从内存中取出并临时存储在 CPU 内部。这些临时存储位置被称为寄存器。这些寄存器可以容纳少量数据,范围在几个字节(通常为 2、4 或 8 字节)。
假设处理器必须将两个变量 x 和 y 相加,并将结果存储在变量 z 中。一个典型的处理器会执行以下操作:
- 从内存中取出
x 的值(即从 x 的地址读取)并将其存储在一个寄存器中,比如 R1
- 从内存中取出
y 的值(即从 y 的地址读取)并将其存储在一个寄存器中,比如 R2
- 将
R1 和 R2 的内容相加,并将结果存储在某寄存器中,比如 R3
- 将
R3 的内容存储到 z 中(写入 z 的地址)
处理器包含不同类型的寄存器,用于不同目的。一些寄存器专用于特定任务,一些寄存器可用于任何目的。上例中使用的那类寄存器属于通用寄存器。
AMX 的寄存器集包括以下寄存器:- 主寄存器 (PRI):通用寄存器(常用作累加器寄存器)
- 辅助寄存器 (ALT):通用寄存器(常用作地址寄存器)
- 代码段寄存器 (COD):内存中代码段起始的绝对地址
- 数据段寄存器 (DAT):内存中数据段起始的绝对地址
- 当前指令指针 (CIP):下一条要执行指令的地址(相对于 COD 寄存器)
- 栈顶寄存器 (STP):栈顶的地址(相对于 DAT 寄存器)
- 栈指针寄存器 (STK):栈上当前位置的地址(相对于 DAT 寄存器)
- 帧指针寄存器 (FRM):栈中当前函数帧起始的地址(相对于 DAT 寄存器)(稍后解释)
- 堆指针 (HEA):堆顶的地址(相对于 DAT 寄存器)
在编写汇编代码时,地址通常不是绝对的;它们是相对于某个段或帧指针寄存器的。全局变量和字符串的地址相对于 DAT 寄存器。函数、指令和标签的地址相对于 COD 寄存器。这意味着变量或指令的绝对地址分别是相对地址加上 DAT 或 COD 寄存器的值。局部变量的地址相对于帧指针寄存器。这有点复杂,稍后会解释。
从现在开始,当我们讨论代码地址和数据地址时,除非明确说明,否则暗示这些地址分别相对于 COD 寄存器和 DAT 寄存器。
文件与内存布局
所有非调试 AMX 二进制文件按如下方式组织:
代码: START OF BINARY FILE
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
END OF BINARY FILE
- 前缀:包含程序的基本信息
- 代码:包含代码
- 数据:包含数据
注意:根据编译器的选项,前缀部分可能会被填充以使代码和数据部分对齐。
注意:启用调试编译的脚本的二进制映像将在末尾附加符号调试信息。更多详情请参阅 Pawn 实现者指南。
内存中的结构采用与二进制文件类似的结构,但额外包含一个栈和一个堆,这是使用前缀中的信息设置的。
代码: LOW ADDRESS (0)
| ---------------------- |
PREFIX
| ---------------------- |
CODE
| ---------------------- |
DATA
| ---------------------- |
HEAP
| |
| |
FREE SPACE
| |
| |
STACK
| ---------------------- |
HIGH ADDRESS
堆和栈共享一个公共的内存区域。它们从该区域的两端开始,并向相反方向增长。堆向高地址增长,栈向低地址增长。由于它们共享同一内存区域,因此可能会相互覆盖(当堆指针和栈指针碰撞时)。发生这种情况时,AMX 会中止并显示如下错误信息:
代码: Script[gamemodes/TEST.amx]: Run time error 3: "Stack/heap collision (insufficient stack size)"
根据编译器掌握的信息,它会估算并间接地在 AMX 二进制文件的前缀部分设置堆栈大小。然而,程序员可以使用 #pragma dynamic [estimated number of cells] 指令提供自己对所需堆栈区域大小的估计。
前缀部分的字段
| 类型 |
大小 |
描述 |
| size |
4 |
内存映像的大小;不包括栈和堆 |
| magic |
2 |
指示格式和单元格大小 |
| file version |
1 |
格式版本 |
| amx version |
1 |
抽象机所需的最低版本 |
| flags |
2 |
是否存在符号调试信息、紧凑编码等。 |
| defsize |
2 |
表中记录的大小 |
| cod |
4 |
代码部分起始的偏移量 |
| dat |
4 |
数据部分起始的偏移量 |
| hea |
4 |
HEA 寄存器的初始值(标记数据段结束) |
| stp |
4 |
STP 寄存器的初始值(指示总内存需求) |
| cip |
4 |
CIP 寄存器的初始值(main 函数的地址;如果不存在则为 -1) |
| publics |
4 |
公共函数表起始的偏移量 |
| natives |
4 |
原生函数表起始的偏移量 |
| libraries |
4 |
库表起始的偏移量 |
| pubvars |
4 |
公共变量表起始的偏移量 |
| tags |
4 |
标签表起始的偏移量 |
| name table |
4 |
名称表起始的偏移量 |
| overlays |
4 |
覆盖表起始的偏移量 |
| publics |
可变 |
公共函数列表 |
| natives |
可变 |
原生函数列表 |
| libraries |
可变 |
库列表 |
| pubvars |
可变 |
公共变量列表 |
| tags |
可变 |
公共标签列表 |
| overlays |
可变 |
覆盖列表 |
| name table |
可变 |
符号名称列表 |
注意:关于 magic、version 和 flags 字段中信息如何编码的详细信息,请参阅 Pawn 实现者指南。
注意:前缀中的所有多字节字段都以小端格式存储,无论 AMX 二进制文件是在哪个平台上生成或将在哪个平台上执行。
可以看出,前缀由一个固定部分组成,后面跟着一系列表。除了名称表之外,表中的每条记录都由两个字段组成,如下所示:
| 字段 |
大小 |
描述 |
| 变量 |
单元格大小 |
变量 |
| 名称字符串偏移量 |
4 字节 |
从前缀起始到名称表中字符串的偏移量 |
表中每条记录(除了名称表)的大小由前缀中的 defsize 字段给出:defsize = 4 + 单元格大小。
表中的每条记录都被分配了一个索引号。每个表的第一个记录被分配索引 0,后续记录的索引号是前一个记录索引加 1。
名称表:
除了名称表本身之外,其他表中的记录包含一个指向字符串的指针。这些字符串作为以空字符结尾的 C 字符串存储在名称表中。此表中记录的大小是其包含的字符串的大小。
公共函数表:
为脚本中定义的每个公共函数创建一条记录。这些记录包含公共函数的地址(函数第一条指令的地址)和指向相应公共函数名称的偏移量。表中记录的索引就是相应公共函数的索引。
原生函数表:
为脚本中调用的每个原生函数创建一条记录。记录的第一个字段由编译器在二进制文件中设置为 0,但宿主程序在加载二进制文件时会将该字段初始化为函数的地址(位于宿主程序的地址空间中)。第二个字段包含指向原生函数名称的偏移量。表中记录的索引就是相应原生函数的索引。
库表:
Pawn 语言提供了一个 pragma 指令 #pragma directive [library name],用于告知编译器调用的某些原生函数需要扩展模块。对于在脚本中调用了其原生函数的库,会在库表中创建一条记录。目的是告知宿主程序 AMX 程序依赖于一个扩展模块。记录的第一个字段供内部使用,在 AMX 二进制文件中设置为 0。另一个字段包含从前缀起始到名称表中库名称的偏移量。
公共变量表:
为脚本中声明的每个公共变量创建一条记录。该记录包含公共变量的地址和指向公共变量名称的偏移量。表中记录的索引就是公共变量的索引。
标签表:
与 sleep 和 exit 语句一起使用的标签以及与 tagof 运算符一起使用的标签会被导出。为每个这样的标签创建一条记录。第一个字段包含标签 id 号,第二个字段存储指向标签名称的偏移量。
指令集
<a name="pawn-inline-assembly"></a> Pawn 编译器接受的大部分 AMX 汇编指令可以用以下格式表示:
代码: mnemonic[.prefix][.register suffix] operand
SHL.C.pri 3
ZERO.alt
ADD.C 100
LIDX
助记符给出了指令功能的提示。可选的前缀指示指令操作数的类型。可选的尾缀指示指令主要作用于哪个寄存器。
前缀列表:- .C = 常量
- .S = 栈
- .I = 间接寻址
- .B = 无 B 变体的变体<sup>\[需要更好的描述\]</sup>
- .ADR = 地址
- .R = 重复
寄存器尾缀列表:
每条指令的二进制形式需要一个单元格来存储操作码,每个操作数还需要一个额外的单元格。绝大多数指令使用隐含寄存器作为操作数。这减少了所需显式操作数的数量,从而减小了代码段的大小并提高了性能。
<a name="instruction-table"></a> 阅读表格:
- [address] 指的是存储在位置
DAT + address 的值
- 语义列中使用的运算符执行与 Pawn 中相同的操作
| 1 |
LOAD.pri |
address |
PRI = [address] |
| 2 |
LOAD.alt |
address |
ALT = [address] |
| 3 |
LOAD.S.pri |
offset |
PRI = [FRM + offset] |
| 4 |
LOAD.S.alt |
offset |
ALT = [FRM + offset] |
| 5 |
LREF.pri |
address |
PRI = [[address]] |
| 6 |
LREF.alt |
address |
ALT = [[address]] |
| 7 |
LREF.S.pri |
offset |
PRI = [[FRM + offset]] |
| 8 |
LREF.S.alt |
offset |
ALT = [[FRM + offset]] |
| 9 |
LOAD.I |
|
PRI = [PRI] |
| 10 |
LODB.I |
number |
PRI = 'number' of bytes from [PRI] (read 1/2/4 bytes) |
| 11 |
CONST.pri |
value |
PRI = value |
| 12 |
CONST.alt |
value |
ALT = value |
| 13 |
ADDR.pri |
offset |
PRI = FRM + offset |
| 14 |
ADDR.alt |
offset |
ALT = FRM + offset |
| 15 |
STOR.pri |
address |
[address] = PRI |
| 16 |
STOR.alt |
address |
[address] = ALT |
| 17 |
STOR.S.pri |
offset |
[FRM + offset] = PRI |
| 18 |
STOR.S.alt |
offset |
[FRM + offset] = ALT |
| 19 |
SREF.pri |
address |
[[address]] = PRI |
| 20 |
SREF.alt |
address |
[[address]] = ALT |
| 21 |
SREF.S.pri |
offset |
[[FRM + offset]] = PRI |
| 22 |
SREF.S.alt |
offset |
[[FRM + offset]] = ALT |
| 23 |
STOR.I |
|
[ALT] = PRI (full cell) |
| 24 |
STRB.I |
number |
number of bytes at [ALT] = PRI (store 1/2/4 bytes) |
| 25 |
LIDX |
|
PRI = [ALT + (PRI x cell size)] |
| 26 |
LIDX.B |
shift |
PRI = [ALT + (PRI << shift)] |
| 27 |
IDXADDR |
|
PRI = ALT + (PRI x cell size) (calculate indexed address) |
| 28 |
IDXADDR.B |
shift |
PRI = ALT + (PRI << shift) (calculate indexed address) |
| 29 |
ALIGN.pri |
number |
Little Endian: PRI ^= cell size - number |
| 30 |
ALIGN.alt |
number |
Little Endian: ALT ^= cell size - number |
| 31 |
LCTRL |
index |
PRI = value contained in the selected register; 1=COD, 1=DAT, 2=HEA,3=STP, 4=STK, 5=FRM, 6=CIP |
| 32 |
SCTRL |
index |
selected register = PRI; 2=HEA, 4=STK, 5=FRM, 6=CIP |
| 33 |
MOVE.pri |
|
PRI = ALT |
| 34 |
MOVE.alt |
|
ALT = PRI |
| 35 |
XCHG |
|
Exchange contents of PRI and ALT |
| 36 |
PUSH.pri |
|
STK = STK - cell size, [STK] = PRI |
| 37 |
PUSH.alt |
|
STK = STK - cell size, [STK] = ALT |
| 38 |
PUSH.R |
number |
repeat (STK = STK - cell size, [STK] = PRI) 'number' times |
| 39 |
PUSH.C |
value |
STK = STK - cell size, [STK] = value |
| 40 |
PUSH |
address |
STK = STK - cell size, [STK] = [address] |
| 41 |
PUSH.S |
offset |
STK = STK - cell size, [STK] = [FRM + offset] |
| 42 |
POP.pri |
|
PRI = [STK], STK = STK + cell size |
| 43 |
POP.alt |
|
ALT = [STK], STK = STK + cell size |
| 44 |
STACK |
value |
ALT = STK, STK = STK + value |
| 45 |
HEAP |
value |
ALT = HEA, HEA = HEA + value |
| 46 |
PROC |
|
STK = STK - cell size, [STK] = FRM, FRM = STK |
| 47 |
RET |
|
FRM = [STK], STK = STK + cell size, CIP = [STK], STK = STK + cell size |
| 48 |
RETN |
|
FRM = [STK], STK = STK + cell size, CIP = [STK], STK = STK + cell size, STK = STK + [STK] + cell size |
| 49 |
CALL |
offset |
STK = STK − cell size, [STK] = CIP, CIP = offset |
| 50 |
CALL.pri |
|
STK = STK − cell size, [STK] = CIP, CIP = PRI |
| 51 |
JUMP |
offset |
CIP = offset |
| 53 |
JZER |
offset |
if PRI == 0 then CIP = offset |
| 54 |
JNZ |
offset |
if PRI != 0 then CIP = offset |
| 55 |
JEQ |
offset |
if PRI == ALT then CIP = offset |
| 56 |
JNEQ |
offset |
if PRI != ALT then CIP = offset |
| 57 |
JLESS |
offset |
if PRI < ALT (unsigned) then CIP = offset |
| 58 |
JLEQ |
offset |
if PRI <= ALT (unsigned) then CIP = offset |
| 59 |
JGRTR |
offset |
if PRI > ALT (unsigned) then CIP = offset |
| 60 |
JGEQ |
offset |
if PRI >= ALT (unsigned) then CIP = offset |
| 61 |
JSLESS |
offset |
if PRI < ALT (signed) then CIP = offset |
| 62 |
JSLEQ |
offset |
if PRI <= ALT (signed) then CIP = offset |
| 63 |
JSGRTR |
offset |
if PRI > ALT (signed) then CIP = offset |
| 64 |
JSGEQ |
offset |
if PRI >= ALT (signed) then CIP = offset |
| 65 |
SHL |
|
PRI = PRI << ALT |
| 66 |
SHR |
|
PRI = PRI >> ALT (without sign extension) |
| 67 |
SSHR |
|
PRI = PRI >> ALT (with sign extension) |
| 68 |
SHL.C.pri |
value |
PRI = PRI << value |
| 69 |
SHL.C.alt |
value |
ALT = ALT << value |
| 70 |
SHR.C.pri |
value |
PRI = PRI >> value |
| 71 |
SHR.C.alt |
value |
ALT = ALT >> value |
| 72 |
SMUL |
|
PRI = PRI * ALT (signed multiply) |
| 73 |
SDIV |
|
PRI = PRI / ALT (signed divide), ALT = PRI mod ALT |
| 74 |
SDIV.alt |
|
PRI = ALT / PRI (signed divide), ALT = ALT mod PRI |
| 75 |
UMUL |
|
PRI = PRI * ALT (unsigned multiply) |
| 76 |
UDIV |
|
PRI = PRI / ALT (unsigned divide), ALT = PRI mod ALT |
| 77 |
UDIV.alt |
|
PRI = ALT / PRI (unsigned divide), ALT = ALT mod PRI |
| 78 |
ADD |
|
PRI = PRI + ALT |
| 79 |
SUB |
|
PRI = PRI - ALT |
| 80 |
SUB.alt |
|
PRI = ALT - PRI |
| 81 |
AND |
|
PRI = PRI & ALT |
| 82 |
OR |
|
PRI = PRI | ALT |
| 83 |
XOR |
|
PRI = PRI ^ ALT |
| 84 |
NOT |
|
PRI = !PRI |
| 85 |
NEG |
|
PRI = -PRI |
| 86 |
INVERT |
|
PRI = ~PRI |
| 87 |
ADD.C |
value |
PRI = PRI + value |
| 88 |
SMUL.C |
value |
PRI = PRI * value |
| 89 |
ZERO.pri |
|
PRI = 0 |
| 90 |
ZERO.alt |
|
ALT = 0 |
| 91 |
ZERO |
address |
[address] = 0 |
| 92 |
ZERO.S |
offset |
[FRM + offset] = 0 |
| 93 |
SIGN.pri |
|
sign extend the byte in PRI to a cell |
| 94 |
SIGN.alt |
|
sign extend the byte in ALT to a cell |
| 95 |
EQ |
|
PRI = PRI == ALT ? 1 : 0 |
| 96 |
NEQ |
|
PRI = PRI != ALT ? 1 : 0 |
| 97 |
LESS |
|
PRI = PRI < ALT ? 1 : 0 (unsigned) |
| 98 |
LEQ |
|
PRI = PRI <= ALT ? 1 : 0 (unsigned) |
| 99 |
GRTR |
|
PRI = PRI > ALT ? 1 : 0 (unsigned) |
| 100 |
GEQ |
|
PRI = PRI >= ALT ? 1 : 0 (unsigned) |
| 101 |
SLESS |
|
PRI = PRI < ALT ? 1 : 0 (signed) |
| 102 |
SLEQ |
|
PRI = PRI <= ALT ? 1 : 0 (signed) |
| 103 |
SGRTR |
|
PRI = PRI > ALT ? 1 : 0 (signed) |
| 104 |
SGEQ |
|
PRI = PRI >= ALT ? 1 : 0 (signed) |
| 105 |
EQ.C.pri |
value |
PRI = PRI == value ? 1 : 0 |
| 106 |
EQ.C.alt |
value |
PRI = ALT == value ? 1 : 0 |
| 107 |
INC.pri |
|
PRI = PRI + 1 |
| 108 |
INC.alt |
|
ALT = ALT + 1 |
| 109 |
INC |
address |
[address] = [address] + 1 |
| 110 |
INC.S |
offset |
[FRM + offset] = [FRM + offset] + 1 |
| 111 |
INC.I |
|
[PRI] = [PRI] + 1 |
| 112 |
DEC.pri |
|
PRI = PRI - 1 |
| 113 |
DEC.alt |
|
ALT = ALT - 1 |
| 114 |
DEC |
address |
[address] = [address] - 1 |
| 115 |
DEC.S |
offset |
[FRM + offset] = [FRM + offset] - 1 |
| 116 |
DEC.I |
|
[PRI] = [PRI] - 1 |
| 117 |
MOVS |
number |
copy 'number' bytes of non-overlapping memory from [PRI] to [ALT] |
| 118 |
CMPS |
number |
compare 'number' bytes of non-overlapping memory at [PRI] with [ALT] |
| 119 |
FILL |
number |
fill 'number' bytes of memory from [ALT] with value in PRI (number must be multiple of cell size) |
| 120 |
HALT |
0 |
abort execution (exit value in PRI) |
| 121 |
BOUNDS |
value |
abort execution if PRI > value or if PRI < 0 |
| 122 |
SYSREQ.pri |
|
call system service, service number in PRI |
| 123 |
SYSREQ.C |
value |
call system service |
| 128 |
JUMP.pri |
|
CIP = PRI |
| 129 |
SWITCH |
offset |
compare PRI to the values in the case table (whose address is passed in the 'offset' argument) and jump to the associated address in the matching record |
| 130 |
CASETBL |
... |
a variable number of case records follows this opcode, where each record takes two cells |
| 131 |
SWAP.pri |
|
[STK] = PRI, PRI = [STK] |
| 132 |
SWAP.alt |
|
[STK] = ALT, ALT = [STK] |
| 133 |
PUSH.ADR |
offset |
STK = STK - cell size, [STK] = FRM + offset |
| 134 |
NOP |
|
no operation |
加载/存储指令
当使用全局变量作为操作数时,编译器会替换该变量的地址。下面代码中的第一条加载指令,如果 some_global 的地址是 1288,则实际上变成 #emit LOAD.pri 1288。请注意,编译器替换的地址是从数据段起始的偏移量;因此,该地址是相对于 DAT 寄存器的。
代码: new some_global = 10, another_global = 25;
main()
{
#emit LOAD.pri some_global // 将 'some_global' 的值加载到主寄存器
#emit LOAD.alt another_global // 将 'another_global' 的值加载到辅助寄存器
}
当使用局部变量时,编译器会替换从函数帧起始的偏移量。(稍后解释)
代码: main()
{
static s_local = 20; // 静态局部变量存储在数据段中
new some_local = 10, another_local = 25; // 局部变量存储在栈中
#emit LOAD.S.pri some_local // 将 'some_local' 的值加载到主寄存器
#emit LOAD.S.alt another_local // 将 'another_local' 的值加载到辅助寄存器
#emit LOAD.pri s_local // 注意静态局部变量的行为类似于全局变量
}
由于编译器会为全局变量替换地址,因此可以使用 CONST.pri/CONST.alt 来获取这些变量的地址。
代码: new some_global;
main()
{
#emit CONST.pri 10 // 将 10 放入主寄存器
#emit CONST.alt 50 // 将 50 放入辅助寄存器
#emit CONST.pri some_global // 将 'some_global' 的地址存储在主寄存器中
#emit CONST.alt some_global // 将 'some_global' 的地址存储在辅助寄存器中
}
代码: new some_global;
main()
{
new some_local;
#emit ZERO.pri // 将零存储在主寄存器中
#emit STOR.pri some_global // 将主寄存器中存储的值(本例中为零)设置给 'some_global'
#emit CONST.alt 125
#emit STOR.S.alt some_local // 将辅助寄存器中存储的值(125)设置给 'some_local'
}
索引指令
代码: new global_arr[10];
main ()
{
#emit CONST.alt global_arr // 将 'global_arr' 的地址('global_arr' 第一个元素的地址)加载到辅助寄存器中
#emit CONST.pri 2 // 设置我们感兴趣的 'global_arr' 元素的索引
#emit LIDX // 现在主寄存器中存储的是 'global_arr[2]` 处存储的值
#emit CONST.alt global_arr // 将 'global_arr' 的地址(第一个元素的地址)加载到辅助寄存器中
#emit CONST.pri 2 // 设置我们感兴趣的 'global_arr' 元素的索引
#emit IDXADDR // 现在主寄存器中存储的是 'global_arr[2]` 的地址
}
算术指令
代码: main ()
{
#emit CONST.pri 4
#emit CONST.alt 5
#emit SMUL // 现在主寄存器中是 20(SMUL 执行有符号乘法;UMUL 执行无符号乘法)
#emit ADD // 将 5 加到 20(由于上一条指令,主寄存器中存储的是 20)
#emit ADD.C 10 // 将 10 加到主寄存器;现在主寄存器中是 35
#emit SUB.alt // 用 5 减去 35;现在主寄存器中是 -30
#emit SMUL.C 2 // -30 乘以 2;得到 -60
}
逻辑指令
代码: main ()
{
#emit CONST.pri 5 // .. 0000 0101
#emit CONST.alt 3 // ... 0000 0011
#emit AND // 主寄存器现在包含 ... 0000 0001
#emit XOR // 主寄存器现在包含 ... 0000 0110
#emit INVERT // 对主寄存器中存储的值取反码
#emit NEG // 对主寄存器中存储的值取补码(本质上是取负)
}
关系指令
代码: main ()
{
#emit CONST.pri 5
#emit CONST.alt 8
#emit EQ // 如果 5 == 8,则将主寄存器设置为 1,否则为 0
#emit LESS // 如果 0 < 8,则将主寄存器设置为 1,否则为 0
}
栈操作指令
局部变量存储在栈上。详细说明将在后续章节中提供,但现在我们假设使用 CONST.pri some_local 会得到某个偏移量,该偏移量加上帧寄存器中存储的基地址即得数据地址。
代码: main ()
{
new some_local = 25;
#emit ADDR.alt some_local // 计算 'some_local' 的地址
#emit CONST.pri 100
#emit STOR.I // 将 100 存储到 'some_local'
#emit CONST.pri 100
#emit STOR.S.pri some_local // 实现上述代码功能的更好方法
#emit CONST.pri some_local // 神秘地等价于 CONST.pri -4(稍后解释)
}
代码: main ()
{
#emit PUSH.C 100 // 将值 100 压入调用栈
#emit POP.pri // 从栈中弹出一个值并将结果存储在主寄存器中
#emit PUSH.pri // 推送主寄存器的值
#emit PUSH.alt // 推送辅助寄存器的值
#emit POP.pri
#emit POP.alt // 最后 4 条指令有效地交换了主寄存器和辅助寄存器的内容
#emit XCHG // 交换主寄存器和辅助寄存器内容的更好方法
}
局部数组位于栈上。因此,在使用索引指令之前,必须使用 ADDR.alt/ADDR.pri 来获取完整的地址。
代码: main ()
{
new local_array[10];
#emit ADDR.alt local_array // 加载存储在 'local_array' 中的值,即数组的地址
#emit CONST.pri 5
#emit LIDX // 有效地将 'local_array[5]' 的值存储在主寄存器中
}
堆指令
堆指针(HEA 寄存器)指向堆的顶部。将堆指针向前移动 x 字节,我们就能有效地
在堆上预留 x 字节。
代码: main ()
{
#emit HEAP 16 // 为四个单元格预留空间(假设单元格为 4 字节)
// 注意 HEAP 指令也将辅助寄存器设置为我们预留内存的起始地址
// ALT = HEA, HEA += 16
#emit CONST.pri 50
#emit STOR.I // 有效地将值 50 存储在我们预留的堆区域的第一个单元格中
#emit HEAP -16 // 归还预留的内存
}
控制寄存器操作指令
可以使用 LCTRL 和 SCTRL 指令直接读取和修改专用寄存器的内容。
| 助记符 |
操作数 |
描述 |
| LCTRL |
index |
PRI = 所选寄存器中包含的值;0=COD, 1=DAT, 2=HEA, 3=STP, 4=STK, 5=FRM, 6=CIP |
| SCTRL |
index |
所选寄存器 = PRI;2=HEA, 4=STK, 5=FRM, 6=CIP |
代码: main ()
{
new cod, dat;
#emit LCTRL 0 // 将 COD 段寄存器的值存储在主寄存器中
#emit STOR.S.pri cod
#emit LCTRL 1 // 将 DAT 段寄存器的值存储在主寄存器中
#emit STOR.S.pri dat
printf("%d %d", cod, dat);
}
当函数名用作操作数时,编译器会用函数的地址替换它。替换的地址是相对于 COD 寄存器的。
代码: f()
{
print("f() was called.");
}
main ()
{
#emit PUSH.C 0 // 参数占用的字节数(稍后解释)
#emit LCTRL 6 // 获取 CIP 的值,即下一条指令(本例中的 ADD.C)的地址
#emit ADD.C 28 // 计算 'f' 之后要执行的指令的地址(注意每个操作码和操作数都需要一个单元格)
#emit PUSH.pri // 推送返回地址,以便 'f' 知道返回到哪里(稍后解释)
#emit CONST.pri f // 将函数 'f' 的地址存储在 pri 中
#emit SCTRL 6 // 将当前指令指针设置为存储在主寄存器中的值
// 函数 'f' 执行
// 函数 'f' 返回后,下一条指令(本例中的 NOP)将开始执行
#emit NOP // 不执行任何操作的指令
}
控制流指令
代码: main()
{
#emit JUMP check // 跳转到 check 标签
not_equal:
printf("1 is not equal to 2");
return 0;
equal:
print("1 is equal to 2");
return 0;
check:
#emit CONST.pri 1
#emit CONST.alt 2
#emit JEQ equal // 如果主寄存器的值等于辅助寄存器的值,则跳转到 'equal'
#emit JUMP not_equal // 如果执行到这里,意味着两个寄存器的值不相等;因此跳转到 'not_equal'
}
Switch case 指令
switch-case 块使用分支表实现。分支表仅仅是由 case 值及其对应的跳转地址组成的元组列表。对于给定的 case 值,AMX 会搜索分支表并跳转到相应的跳转地址。分支表中的记录按 case 值的升序排列。这允许 AMX 对分支表执行二分搜索,但它也可能执行线性搜索。
SWITCH 指令标记 switch-case 块的开始。它接受一个指向分支表(也位于代码段中)的偏移量作为操作数。分支表正式以 CASETBL 操作码(仅作为标记,功能上未使用)开始,后跟一系列 CASE 记录,每条记录占用两个参数。第一条 case 记录的参数具有特殊含义:第一个参数是分支表中的 case 数量,第二个参数是默认 case 的偏移量。如果没有提供默认 case,则第二个参数包含 switch-case 块之后指令的偏移量。其余的 CASE 记录包含 case 值及其对应的跳转地址。
代码: switch(expression)
{
case 2: {}
case 4: {}
case 3: {}
case 7: {}
case 5: {}
}
编译器会添加指令来计算给定的 expression,其结果存储在主寄存器中。紧接着是一条 SWITCH 指令,它使 AMX 搜索分支表,查找与主寄存器中存储的值匹配的 case 值。如果找到匹配项,则执行跳转到匹配记录指向的地址;否则,跳转到默认地址。
代码: ; 编译器通过在汇编输出中使用标签而不是实际的偏移量/地址,使阅读更容易
; 每个标签都以前缀 "l." 开头
switch 0 ; 注意这里的零是一个标签
l.2 ; 标签 2
jump 1 ; 跳转到标签 1
l.3
jump 1
l.4
jump 1
l.5
jump 1
l.6
jump 1
l.0
casetbl
case 5 1 ; 记录数量,默认跳转地址(本例中为标签 1)
case 2 2 ; 真正的第一条记录
case 3 4 ; case 值: 3, 跳转标签: 4
case 4 3 ; 编译器在实际二进制文件中会用正确的地址替换标签
case 5 6 ;
case 7 5 ; 最后一条记录
l.1
; 其余代码
|
|
|
|
| [wiki系列] openmp/samp 关键字:指令 |
|
发布者: 小鸟unsigned - 03-21-2026, 11:06 PM - 板块: 教程
- 暂无回复
|
 |
[wiki系列] openmp/samp 关键字:指令
来自 SA-MP Wiki
目录
#assert
此指令会检查常量表达式是否为真,如果不为真则停止编译。
代码: #define MOO 10
#assert MOO > 5
以上代码可以正常编译。
代码: #define MOO 1
#assert MOO > 5
以上代码无法编译,会产生致命错误。
这类似于:
代码: #define MOO 1
#if MOO <= 5
#error Moo check failed
#endif
但是 #assert 会给出以下错误信息:
代码: "Assertation failed: 1 > 5"
而第二种写法会给出:
代码: "User error: Moo check failed"
哪种错误信息更实用,则视情况而定。
#define
#define 是一个文本替换指令。只要在代码中找到被定义的符号(宏名),就会将其替换为定义的内容。
代码: #define MOO 7
printf("%d", MOO);
会被替换为:
这也是为什么反编译后的代码中看不到任何 #define,因为所有指令都在预处理器阶段处理完毕。定义的内容不一定是数字:
代码: #define PL new i = 0; i < MAX_PLAYERS; i++) if (IsPlayerConnected(i)
for (PL)
{
printf("%d connected", i);
}
会编译成大家熟悉(或讨厌)的玩家循环。注意括号的使用:一部分来自 for 语句,一部分来自宏本身。
另一个鲜为人知的特性是:定义可以跨行(通过在行尾加上反斜杠 \ 来转义换行)。一般来说换行会结束定义,但下面这样是有效的:
代码: #define PL \
new i = 0; i < MAX_PLAYERS; i++) \
if (IsPlayerConnected(i)
for (PL)
{
printf("%d connected", i);
}
\ 字符表示定义继续到下一行。
#define 的第三个重要特性是支持参数(宏参数)。参数用 %0 到 %9 表示,用法与普通函数参数类似:
代码: #define MOO(%0) \
((%0) * 7)
printf("%d", MOO(6));
输出结果为 42(不是随意选的)。注意宏里多余的括号——这是因为宏是纯文本替换,所以实际编译成:
代码: printf("%d", ((6) * 7));
这样是安全的。但看下面这个例子:
代码: printf("%d", MOO(5 + 6));
你会期望结果是 77((5 + 6) * 7)。加上括号后确实如此;如果去掉括号:
代码: #define MOO(%0) \
%0 * 7
printf("%d", MOO(5 + 6));
实际会变成:
代码: printf("%d", 5 + 6 * 7);
由于运算优先级(BODMAS),结果变为 47(5 + (6 * 7)),这就完全错误了。
关于参数还有一个有趣的事实:如果传入的参数数量超过定义的数量,最后一个参数会包含所有多余的部分。例如:
代码: #define PP(%0,%1) \
printf(%0, %1)
PP("%s %s %s", "hi", "hello", "hi");
实际会打印:
因为 %1 包含了 "hi", "hello", "hi"。你可能还注意到 # 可以把字面量转换成字符串,这是 openmp(samp) 特有的功能,这里只是为了区分参数。
#else
#else 相当于普通 else,但用于 #if 条件。
#elseif
#elseif 相当于 else if,但用于 #if 条件。
代码: #define MOO 10
#if MOO == 9
printf("if");
#elseif MOO == 8
printf("else if");
#else
printf("else");
#endif
#emit
此指令在 pawn-lang.pdf 的表格中并未列出,但实际存在。它相当于内联汇编器,如果你熟悉 AMX 字节码,可以用它直接插入 AMX 操作码。唯一限制是每次只能带一个参数。
语法:#emit <opcode> <argument>
<argument> 可以是小数、整数或(局部/全局)符号(变量、函数、标签)。
操作码列表及其含义可在 Pawn Toolkit ver. 3664 中找到。
#endif
#endif 相当于 #if 的结束括号。#if 不使用大括号,所有条件内容一直持续到对应的 #endif。
#endinput , #endscript
此指令停止当前文件的包含(不再继续读取该文件)。
#error
此指令立即停止编译,并输出自定义错误信息。示例见 #assert。
#if
#if 是预处理器版本的 if。你可以精确控制哪些代码被编译、哪些不被编译。例如:
代码: #define LIMIT 10
if (LIMIT < 10)
{
printf("Limit too low");
}
会编译成:
代码: if (10 < 10)
{
printf("Limit too low");
}
编译器知道这个条件永远为假,因此会给出“常量表达式”警告。但既然永远不会成立,为什么还要保留这段代码呢?你可以直接删掉,但之后别人修改 LIMIT 重新编译时就无法检查了。这就是 #if 的作用。
普通 if 在常量表达式时会警告,而 #if 必须 是常量表达式:
代码: #define LIMIT 10
#if LIMIT < 10
#error Limit too low
#endif
这样会在编译时就检查限制是否过小,如果是则直接报错,而不用运行脚本测试。同时也不会生成多余的代码。注意 #if 不强制使用括号(虽然复杂表达式可能需要)。
另一个例子:
代码: #define LIMIT 10
if (LIMIT < 10)
{
printf("Limit less than 10");
}
else
{
printf("Limit equal to or above 10");
}
同样是常量检查,会产生警告,而且两个 printf 都会被编译(尽管我们知道只会执行一个)。用 #if 改写后:
代码: #define LIMIT 10
#if LIMIT < 10
printf("Limit less than 10");
#else
printf("Limit equal to or above 10");
#endif
这样只有需要的 printf 会被编译,另一个仍保留在源代码中(方便以后改 LIMIT),但不会占用最终编译后的代码空间,也不会每次运行都执行无用的判断。
#include
此指令会把指定文件的所有代码插入到 #include 所在的位置。有两种包含方式:相对路径(用双引号)和系统路径(用尖括号,我自己起的名称,如果你有更好的叫法请告诉我)。
相对包含使用双引号,路径相对于当前文件:
会包含与当前文件同目录下的 me.pwn。
系统包含使用尖括号,从 Pawn 编译器所在目录下的 include 文件夹(或其父目录的 include 文件夹)中查找:
会包含 qawno/include/me.inc(注意没有扩展名;如果文件不是 .p 或 .inc,可以指定扩展名)。
两种方式都支持子目录:
代码: #include "folder/me.pwn"
#include <folder/me>
如果文件不存在,编译会立即失败。
#pragma
说明
这是最复杂的指令之一。它提供了一系列选项来控制脚本的编译行为。例如:
会把转义字符从 \ 改成 $,所以换行符不再是 "\r\n",而是 "$r$n"。
很多选项原本是为嵌入式系统设计的,用来限制 PC 上几乎无限制的资源。这里只列出与 openmp(samp) 相关的选项(完整列表见 pawn-lang.pdf)。
列表
| 名称 |
值 |
说明 |
| codepage |
名称/值 |
设置字符串使用的 Unicode 码页 |
| compress |
1/0 |
openmp(samp) 不支持,请勿使用 |
| deprecated |
符号名称 |
如果使用该符号会产生警告,用于提示有更好的替代版本 |
| dynamic |
值(通常是 2 的幂) |
设置栈和堆内存大小(单位:cells)。出现“excessive memory usage”警告时需要设置 |
| library |
dll 名称 |
在 openmp(samp) 中经常被误用。它指定本文件中的原生函数来自哪个 DLL,并非把文件定义为库 |
| pack |
1/0 |
交换 !"" 和 "" 的含义(详见 pawn-lang.pdf 中的打包字符串说明) |
| tabsize |
值 |
经常被误用。用于设置制表符宽度,避免因空格和制表符混用产生的警告。openmp(samp) 默认设为 4(qawno 编辑器中制表符宽度)。设为 0 会关闭所有缩进警告,但强烈不推荐(会导致代码难以阅读) |
| unused |
符号名称 |
类似 deprecated,用于抑制“symbol is never used”警告。推荐使用 stock,但某些情况(如函数参数)无法使用 stock 时才用此方式 |
示例
已弃用(Deprecated)
代码: new
gOldVariable = 5;
#pragma deprecated gOldVariable
main()
{
printf("%d", gOldVariable);
}
使用 gOldVariable 时会产生警告,提示不应再使用它。主要用于函数更新 API 时保留向下兼容性。
#tryinclude
类似于 #include,但如果文件不存在,编译不会失败。这非常适合根据用户是否安装了特定插件(或插件的 include 文件)来选择性包含功能。
myinc.inc:
代码: #if defined _MY_INC_INC
#endinput
#endif
#define _MY_INC_INC
stock MyIncFunc()
{
printf("Hello");
}
主脚本:
代码: #tryinclude <myinc>
main()
{
#if defined _MY_INC_INC
MyIncFunc();
#endif
}
只有当 myinc.inc 存在并被成功包含时,才会调用 MyIncFunc()。这对 IRC 插件等需要检测插件是否安装的情况非常有用。
#undef
移除之前定义的宏或常量符号。
代码: #define MOO 10
printf("%d", MOO);
#undef MOO
printf("%d", MOO);
第二个 printf 会编译失败,因为 MOO 已被取消定义。
代码: enum
{
e_example = 300
};
printf("%d", e_example);
#undef e_example
printf("%d", e_example); // 致命错误
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| [wiki系列] openmp/samp 关键字:运算符 |
|
发布者: 小鸟unsigned - 03-21-2026, 11:06 PM - 板块: 教程
- 暂无回复
|
 |
[wiki系列] openmp/samp 关键字:运算符
来自 SA-MP Wiki
目录
char
char 返回存放给定数量字符(打包字符串)所需的 cells 数量。
即:存放给定字节数所需的 4 字节 cells 数量。
示例:
返回 1。
返回 1(你不能拥有 3/4 个变量)。
返回 64(256 ÷ 4)。
此运算符通常用于变量声明:
会创建一个10 cells大小的数组。
更多关于打包字符串(packed strings)的细节,请阅读 pawn-lang.pdf。
defined
检查一个符号(symbol)是否存在。通常用于 #if 语句:
代码: new
someVar = 5;
#if defined someVar
printf("%d", someVar);
#else
#error The variable 'someVar' isn't defined
#endif
最常见的用法是检查某个 #define 是否存在,并据此生成不同代码:
代码: #define FILTERSCRIPT
#if defined FILTERSCRIPT
public OnFilterScriptInit()
{
return 1;
}
#else
public OnGameModeInit()
{
return 1;
}
#endif
sizeof
返回数组元素(ELEMENTS)的数量:
代码: new
someVar[10];
printf("%d", sizeof (someVar));
输出:
二维数组示例:
代码: new
someVar[2][10];
printf("%d %d", sizeof (someVar), sizeof (someVar[]));
输出:
state
此关键字与 PAWN 自动机(autonoma)代码相关,因此本文不做介绍。
tagof
tagof 返回一个代表变量标签(tag)的数字:
代码: new
someVar,
Float:someFloat;
printf("%d %d", tagof (someVar), tagof (someFloat));
输出:
代码: -./,)),(-*,( -1073741820
这其实是一个轻微的显示 bug,但本质上代表:
实际应用:检查变量是否为 Float
代码: new Float: fValue = 6.9;
new tag = tagof (fValue);
if (tag == tagof (Float:))
print("float");
else
print("not a float");
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| [wiki系列] openmp/samp 关键字:初始值设定项 |
|
发布者: 小鸟unsigned - 03-21-2026, 11:05 PM - 板块: 教程
- 暂无回复
|
 |
[wiki系列] openmp/samp 关键字:初始值设定项
来自 SA-MP Wiki
目录
const
代码: new const
MY_CONSTANT[] = {1, 2, 3};
const 并不常用,但它用于声明一个无法被代码修改的变量。它的用途包括:让函数的数组参数编译得更高效,或者创建一个类似 #define 但却是数组的常量。const 是一个修饰符,必须与 new 或其他变量声明关键字搭配使用。如果你尝试修改 const 变量,编译器会报错。
enum
枚举(enum)是一个非常实用的系统,用于表示大量数据并快速修改常量。主要用途有三类:- 替代一大堆
#define 语句
- 用符号名称表示数组下标(其实本质相同,只是写法不同)
- 创建新的标签(tag)
最常见的用法是定义数组结构:
代码: enum E_MY_ARRAY
{
E_MY_ARRAY_MONEY,
E_MY_ARRAY_GUN
}
new
gPlayerData[MAX_PLAYERS][E_MY_ARRAY];
public OnPlayerConnect(playerid)
{
gPlayerData[playerid][E_MY_ARRAY_MONEY] = 0;
gPlayerData[playerid][E_MY_ARRAY_GUN] = 5;
}
这会为每个玩家创建一个包含两个槽位的数组。连接时,E_MY_ARRAY_MONEY 槽位设为 0,E_MY_ARRAY_GUN 槽位设为 5。
如果不用 enum,代码会变成这样:
代码: new
gPlayerData[MAX_PLAYERS][2];
public OnPlayerConnect(playerid)
{
gPlayerData[playerid][0] = 0;
gPlayerData[playerid][1] = 5;
}
可读性明显变差——下标 0 和 1 分别代表什么?而且扩展性差:如果要在中间插入一个新槽位,就得手动把所有 1 改成 2,非常容易出错。而使用 enum,只需添加一行:
代码: enum E_MY_ARRAY
{
E_MY_ARRAY_MONEY,
E_MY_ARRAY_AMMO,
E_MY_ARRAY_GUN
}
new
gPlayerData[MAX_PLAYERS][E_MY_ARRAY];
public OnPlayerConnect(playerid)
{
gPlayerData[playerid][E_MY_ARRAY_MONEY] = 0;
gPlayerData[playerid][E_MY_ARRAY_AMMO] = 100;
gPlayerData[playerid][E_MY_ARRAY_GUN] = 5;
}
重新编译后,所有索引自动更新。
enum 的完整格式与工作原理
代码: enum NAME (modifier)
{
NAME_ENTRY_1 = value,
NAME_ENTRY_2 = value,
...
NAME_ENTRY_N = value
}
大部分内容是隐含的。默认 modifier 为 (+= 1),表示每个值都是前一个值 + 1:
代码: enum E_EXAMPLE
{
E_EXAMPLE_0,
E_EXAMPLE_1,
E_EXAMPLE_2
}
结果:E_EXAMPLE_0 = 0,E_EXAMPLE_1 = 1,E_EXAMPLE_2 = 2,E_EXAMPLE = 3(枚举名本身等于最后一个值)。
修改 modifier 的例子:
代码: enum E_EXAMPLE (+= 5)
{
E_EXAMPLE_0,
E_EXAMPLE_1,
E_EXAMPLE_2
}
结果:0, 5, 10, 15。如果声明 new gEnumArray[E_EXAMPLE];,数组大小为 15,但只能通过枚举名访问 0、5、10(仍可使用普通数字访问)。
另一个例子(乘法):
代码: enum E_EXAMPLE (*= 2)
{
E_EXAMPLE_0,
E_EXAMPLE_1,
E_EXAMPLE_2
}
全部变为 0。解决方法是手动指定初始值:
代码: enum E_EXAMPLE (*= 2)
{
E_EXAMPLE_0 = 1,
E_EXAMPLE_1,
E_EXAMPLE_2
}
结果:1, 2, 4, 8。
提示:数组用途时强烈建议只用 += 1,其他 modifier 容易导致混乱。
enum 中可以包含数组
代码: enum E_EXAMPLE
{
E_EXAMPLE_0[10],
E_EXAMPLE_1,
E_EXAMPLE_2
}
结果:E_EXAMPLE_0 = 0,E_EXAMPLE_1 = 10,E_EXAMPLE_2 = 11,E_EXAMPLE = 12(而不是很多人以为的 0、1、2、3)。
enum 项可以带标签
代码: enum E_MY_ARRAY
{
E_MY_ARRAY_MONEY,
E_MY_ARRAY_AMMO,
Float:E_MY_ARRAY_HEALTH,
E_MY_ARRAY_GUN
}
new
gPlayerData[MAX_PLAYERS][E_MY_ARRAY];
public OnPlayerConnect(playerid)
{
gPlayerData[playerid][E_MY_ARRAY_MONEY] = 0;
gPlayerData[playerid][E_MY_ARRAY_AMMO] = 100;
gPlayerData[playerid][E_MY_ARRAY_GUN] = 5;
gPlayerData[playerid][E_MY_ARRAY_HEALTH] = 50.0; // 不会产生标签不匹配警告
}
enum 本身也可以作为标签使用(位标志)
代码: enum E_MY_TAG (<<= 1)
{
E_MY_TAG_NONE,
E_MY_TAG_VAL_1 = 1,
E_MY_TAG_VAL_2,
E_MY_TAG_VAL_3,
E_MY_TAG_VAL_4
}
new
E_MY_TAG:gMyTagVar = E_MY_TAG_VAL_2 | E_MY_TAG_VAL_3;
gMyTagVar 值为 6(4 | 2),并带有自定义标签。直接赋值 gMyTagVar = 7; 会产生标签不匹配警告,但可以用强制转换绕过:
代码: gMyTagVar = E_MY_TAG:7;
进阶用法(掩码 + 组合):
代码: enum E_MY_TAG (<<= 1)
{
E_MY_TAG_NONE,
E_MY_TAG_MASK = 0xFF,
E_MY_TAG_VAL_1 = 0x100,
E_MY_TAG_VAL_2,
E_MY_TAG_VAL_3,
E_MY_TAG_VAL_4
}
new
E_MY_TAG:gMyTagVar = E_MY_TAG_VAL_2 | E_MY_TAG_VAL_3 | (E_MY_TAG:7 & E_MY_TAG_MASK);
结果为 1543(0x0607)。
用无名 enum 替代 #define
传统写法:
代码: #define TEAM_NONE 0
#define TEAM_COP 1
#define TEAM_ROBBER 2
#define TEAM_CIV 3
#define TEAM_CLERK 4
#define TEAM_DRIVER 5
改用 enum 自动赋值:
代码: enum
{
TEAM_NONE,
TEAM_COP,
TEAM_ROBBER,
TEAM_CIV,
TEAM_CLERK,
TEAM_DRIVER
}
值完全相同,使用方式也一样。
更强大的位标志写法(推荐):
代码: enum (<<= 1)
{
TEAM_NONE,
TEAM_COP = 1,
TEAM_ROBBER,
TEAM_CIV,
TEAM_CLERK,
TEAM_DRIVER,
TEAM_ADMIN
}
现在 TEAM_COP = 1(二进制 00000001)、TEAM_ROBBER = 2(00000010)等。一个变量就能同时表示多个团队:
- 添加团队:
gPlayerTeam[playerid] |= TEAM_COP;
- 移除团队:
gPlayerTeam[playerid] &= ~TEAM_COP;
- 检查是否在团队:
if (gPlayerTeam[playerid] & TEAM_COP)
非常简洁且强大。
forward
forward 用于告诉编译器“这个函数稍后会定义”。所有 public 函数都必须 forward,也可以用于其他地方。
语法:
代码: forward MyPublicFunction(playerid, const string[]);
代码: forward MyPublicFunction(playerid, const string[]);
public MyPublicFunction(playerid, const string[])
{
}
除了 public 函数外,forward 还能解决一种罕见的“reparse”警告(当函数返回带标签的值如 Float 时,在声明前被调用):
有警告的写法:
代码: main()
{
new Float:myVar = MyFloatFunction();
}
Float:MyFloatFunction()
{
return 5.0;
}
解决方法 1(函数前置):
代码: Float:MyFloatFunction()
{
return 5.0;
}
main()
{
new Float:myVar = MyFloatFunction();
}
解决方法 2(forward):
代码: forward Float:MyFloatFunction();
main()
{
new Float:myVar = MyFloatFunction();
}
Float:MyFloatFunction()
{
return 5.0;
}
注意:forward 也要带返回值标签。
native
native 函数是由虚拟机(openmp(samp) 服务器或插件)提供的函数,而不是脚本自己定义的。你只能声明已存在于 openmp(samp) 或插件中的 native。
用途 1:让自定义函数出现在 qawno 右侧函数列表中(即使是假的):
代码: /*
native MyFunction(playerid);
*/
注释会被 qawno 识别加入列表,但编译器会忽略。
用途 2:重命名 / 重载函数:
代码: native my_print(const string[]) = print;
现在 print 函数在脚本中已不存在(但服务器内部仍存在)。你可以重新定义它:
代码: print(const string[])
{
my_print("Someone called print()");
my_print(string);
}
以后所有 print() 调用都会先执行你的代码,再调用原函数。
new
new 是变量声明的核心关键字之一。
创建变量并赋值为 5。未赋值时默认为 0:
代码: new myVar;
printf("%d", myVar); // 输出 0
作用域(scope)由大括号 {} 决定,声明在括号内的变量只能在该括号内使用。
代码: if (a == 1)
{
new myVar = 5;
printf("%d", myVar); // 可以
if (myVar == 1)
{
printf("%d", myVar); // 可以
}
}
// 这里无法使用 myVar,会报错
printf("%d", myVar);
全局变量(在函数外声明)从声明位置之后可在整个脚本中使用(包括 #include 的其他文件)。
static 与 new 的区别详见下方 static 部分。
operator
允许为自定义标签重载运算符。
示例(大端序转换):
代码: stock BigEndian:operator=(b)
{
return BigEndian:(((b >>> 24) & 0x000000FF) | ((b >>> 8) & 0x0000FF00) | ((b << 8) & 0x00FF0000) | ((b << 24) & 0xFF000000));
}
main()
{
new BigEndian:a = 7;
printf("%d", _:a); // 输出 117440512(因为小端序读取大端序数据)
}
可重载的运算符:
+, -, *, /, %, ++, --, ==, !=, <, >, <=, >=, !, =
你可以让运算符做任何事(不一定是原本功能):
代码: stock BigEndian:operator+(BigEndian:a, BigEndian:b)
{
return BigEndian:42;
}
a + b 永远返回 42。
public
public 让函数对虚拟机可见,允许 openmp(samp) 服务器从外部直接调用(而非仅在脚本内部调用)。也可用于变量(允许服务器读写),但 openmp(samp) 中极少使用。
public 函数的名称会存储在 .amx 文件中(普通函数只存地址),这也是反编译的难点之一。
通过名称调用 public 函数:
代码: forward MyPublicFunc();
main()
{
CallLocalFunction("MyPublicFunc", "");
}
public MyPublicFunc()
{
printf("Hello");
}
也可使用 @ 前缀:
代码: forward MyPublicFunc();
forward @MyOtherPublicFunc(var);
main()
{
CallLocalFunction("MyPublicFunc", "");
SetTimerEx("@MyOtherPublicFunc", 5000, 0, "i", 7);
}
public MyPublicFunc()
{
printf("Hello");
}
@MyOtherPublicFunc(var)
{
printf("%d", var);
}
所有 openmp(samp) 回调(如 OnPlayerConnect)都是 public,由服务器自动调用。
注意:public 函数也可以像普通函数一样直接调用(速度更快):
代码: forward MyPublicFunc();
main()
{
MyPublicFunc(); // 直接调用,比 CallLocalFunction 快
}
public MyPublicFunc()
{
printf("Hello");
}
static
static 全局变量类似 new,但作用域更小(仅限于声明所在的文件或 #section)。
对比 new 的跨文件可见性,static 仅限本文件:
代码: // file2.pwn 中无法访问 file1.pwn 里的 static 变量
局部 static:作用域与 new 相同(仅大括号内),但值在函数多次调用间保留(不像 new 每次重置)。
代码: MyFunc()
{
new i = 0;
printf("%d", i);
i++;
printf("%d", i);
}
调用 4 次输出:
改为 static 后:
初始化值仅在第一次调用时生效。
static 函数只能在声明所在文件中调用,适合“私有”函数。
stock
stock 用于声明可能不会被使用的变量或函数,避免产生“unused”警告。
代码: new stock
gMayBeUsedVar;
static stock
g_sMayBeUsedVar;
如果被使用则编译保留;如果未使用则完全剔除(不像 #pragma unused 只隐藏警告)。
最常见用途:编写库时。库作者无法预知用户会用哪些函数,使用 stock 可避免大量无用警告。
```pawn
stock Func1()
{
printf("Hello");
}
stock Func2()
{
printf("Hi");
}
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| [wiki系列] openmp/samp 控制结构 |
|
发布者: 小鸟unsigned - 03-21-2026, 11:04 PM - 板块: 教程
- 暂无回复
|
 |
[wiki系列] openmp/samp 控制结构
来自 SA-MP Wiki
目录
条件语句
if
if 语句会检查某个条件是否成立,如果成立则执行对应代码。
代码: new
a = 5;
if (a == 5)
{
print("a is 5");
}
if 后面的括号内是条件,你可以进行多种判断(详见运算符部分)。
条件中不仅可以是变量,也可以是函数:
代码: if (SomeFunction() == 5)
{
print("SomeFunction() is 5");
}
还可以组合多个条件:
代码: new
a = 5,
b = 3;
if (a == 5 && b != 3)
{
print("Won't be printed");
}
上面检查 a == 5 并且 b != 3,但 b 是 3,所以整个条件为假。
使用 ||(或):
代码: new
a = 5,
b = 3;
if (a == 5 || b != 3)
{
print("Will be printed");
}
只要有一个条件为真,整个 if 就成立(即使 b != 3 为假,因为 a == 5 为真)。
还可以链式比较(无需显式写多个 &&):
代码: new
idx = 3;
if (0 < idx < 5)
{
print("idx is greater than 0 and less than 5!");
}
运算符
条件判断中常用的运算符及其含义:
比较运算符
| 运算符 |
含义 |
示例 |
| == |
左边等于右边 |
if (Left == Right) |
| != |
左边不等于右边 |
if (Left != Right) |
| > |
左边大于右边 |
if (Left > Right) |
| >= |
左边大于等于右边 |
if (Left >= Right) |
| < |
左边小于右边 |
if (Left < Right) |
| <= |
左边小于等于右边 |
if (Left <= Right) |
逻辑运算符
| 运算符 |
含义 |
示例 |
| && |
并且(AND) |
if (Left && Right) |
|
|
|
或者(OR) |
if (Left || Right) |
| ! |
非(NOT) |
if (!Variable) |
|
都不(NOR) |
if (!(Left || Right)) |
|
不是都(NAND) |
if (!(Left && Right)) |
|
异或(XOR) |
if (!(Left && Right) && (Left || Right)) |
|
同或(NXOR) |
if ((Left && Right) || !(Left || Right)) |
括号
括号决定运算顺序:
代码: new
a = 3,
b = 3,
c = 1;
if (a == 5 && b == 3 || c == 1)
{
print("Will this be called?");
}
有两种可能的解释:
(a == 5 && b == 3) || c == 1
a == 5 && (b == 3 || c == 1)
第一种:先判断 a==5 && b==3(为假),再判断 c==1(为真),整体为真。
第二种:先判断 b==3 || c==1(为真),再判断 a==5(为假),整体为假。
强烈建议始终使用括号,既能明确逻辑,也能避免歧义。
else
else 在 if 条件不成立时执行:
代码: new
a = 5;
if (a == 3) // 假
{
print("Won't be called");
}
else
{
print("Will be called as the check failed");
}
else if
else if 用于在第一个 if 不成立时继续检查其他条件:
代码: new
a = 5;
if (a == 1)
{
print("Will be called if a is 1");
}
else if (a == 5)
{
print("Will be called if a is 5");
}
else
{
print("All other numbers");
}
你可以连续写多个 else if,但一个 if 组中只能有一个 else。
注意:else if 只在最初的 if 失败时才检查,且变量值以检查开始时的状态为准。
?:(三元运算符)
? 和 : 组合成三元运算符,可将 if-else 写在单行内:
代码: new
a,
b = 3;
if (b == 3)
{
a = 5;
}
else
{
a = 7;
}
可简化为:
代码: new
a,
b = 3;
a = (b == 3) ? 5 : 7;
(条件) ? 真值 : 假值
也可以嵌套(类似多个 else if):
代码: new
a,
b = 3;
a = (b == 1) ? 2 : (b == 2) ? 3 : (b == 3) ? 4 : 5;
循环
while ()
while 循环在条件成立时重复执行代码:
代码: new
a = 9;
while (a < 10)
{
// 循环内的代码
a++;
}
每次到达 } 时会回到条件重新判断。
for ()
for 是压缩版的 while 循环,包含三部分:初始化、条件、递增。
代码: for (new a = 9; a < 10; a++)
{
// 循环内的代码
}
常用遍历玩家示例:
代码: for (new i, a = GetMaxPlayers(); i < a; i++)
{
if (IsPlayerConnected(i))
{
// do something
}
}
三部分均可省略(留空):
代码: new a = 9;
for ( ; a < 10; )
{
a++;
}
do-while
do-while 先执行代码,再判断条件,因此至少执行一次:
代码: new
a = 10;
do
{
// 循环内的代码
a++;
}
while (a < 10); // 注意分号
if-goto
循环本质上就是 if + goto 的结构化形式(不推荐直接使用 goto):
代码: new
a = 9;
loop_start:
if (a < 10)
{
// 循环内的代码
a++;
goto loop_start;
}
OBOE(Off By One Error)
OBOE 是最常见的循环错误之一——循环多跑或少跑一次。
代码: new
a = 0,
b[10];
while (a <= sizeof (b))
{
b[a] = 0;
}
上面会尝试访问 b[10](越界),导致 OOB(Out Of Bounds)错误。
特别注意:do-while 永远至少执行一次,更容易出现 OBOE。
switch
switch
switch 是结构化的 if / else if / else,更清晰:
代码: new
a = 5;
switch (a)
{
case 1:
{
// 不会执行
}
case 2:
{
// 不会执行
}
case 5:
{
// 会执行
}
default:
{
// 不会执行
}
}
switch 只调用一次判断表达式(比连续 else if 更高效)。
case
case 支持单个值、列表或范围:
代码: case 1, 2, 3, 4: // 列表
case 1 .. 4: // 范围(推荐连续数字)
default
相当于 else,所有 case 都不匹配时执行。
单行语句
goto
goto 直接跳转到标签(不推荐使用,会破坏程序流程)。
break
break 立即跳出当前循环:
代码: for (new a = 0; a < 10; a++)
{
if (a == 5) break;
}
continue
continue 跳过本次循环剩余代码,直接进入下一次迭代:
代码: for (new a = 0; a < 3; a++)
{
if (a == 1) continue;
printf("a = %d", a);
}
输出:
注意:在某些循环(尤其是 while)中使用 continue 时要小心,可能导致无限循环。
return
return 立即结束当前函数,返回到调用处:
代码: main()
{
print("1");
MyFunction(1);
print("3");
}
MyFunction(num)
{
if (num == 1)
{
return; // 直接返回,不执行下面的 print
}
print("2");
}
输出:
也可以返回具体值:
注意:如果函数有时返回值,有时不返回值,必须在所有路径都显式 return 一个值。
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| [wiki系列] openmp/samp脚本基础 |
|
发布者: 小鸟unsigned - 03-21-2026, 10:34 PM - 板块: 教程
- 暂无回复
|
 |
[wiki系列] openmp/samp 脚本基础
来自 SAMP Wiki
目录
入门!
下面是一个可能最简单的脚本示例:
代码: #include <open.mp>
main()
{
print("Hello World!");
return 1;
}
我们将逐一讲解各个部分,先从第一行开始。
Include
这一行会把 qawno/includes/open.mp.inc 文件中的所有代码加载到你的脚本中,这样你就可以使用它提供的一切内容。其中它还包含了:
代码: #include <args>
#include <console>
#include <core>
#include <file>
#include <float>
#include <string>
#include <time>
#include <omp_core>
#include <omp_player>
#include <omp_actor>
#include <omp_checkpoint>
#include <omp_class>
#include <omp_database>
#include <omp_dialog>
#include <omp_gangzone>
#include <omp_http>
#include <omp_menu>
#include <omp_network>
#include <omp_object>
#include <omp_pickup>
#include <omp_textdraw>
#include <omp_variable>
#include <omp_vehicle>
#include <omp_textlabel>
#include <omp_npc>
这些文件包含了 open.mp(samp) 的所有核心功能,因此只需这一行,你就拥有了 open.mp(samp) 的全部函数(函数后面会详细说明)。
调用
接下来是函数调用的两部分。main() 是你自己编写代码的函数,由外部调用;print(string[]) 是代码在别处定义、由你调用的函数。目前这个脚本只会加载、打印一行字符串(即在服务器控制台输出 “Hello World!”,不带引号——这是所有脚本语言的传统),然后结束。
这条语句将值 1 返回给调用 main 的地方,告诉它执行结果(具体返回值在这里不重要,但在其他地方很重要)。
你现在已经写出了第一个(非常基础)的脚本。如果你在 qawno 中选择“文件 → 新建”,它会给你一个更大的模板,包含所有回调(见下文),包括 main(严格来说它不是回调,但行为类似)。
语句
print 和 return 行都以 ;(分号)结尾,分号表示一条语句的结束(语句是一组函数和运算符的组合,用于完成某项操作,类似自然语言中的句子)。大多数人把每条语句写在单独一行,但这不是必须的,只是为了更清晰。下面这种写法同样有效:
代码: main() {
print("Hello World!");
return 1;
}
{}(大括号)用于将一组语句组合在一起执行(类似自然语言中的段落)。如果你写成:
代码: main()
print("Hello World!");
return 1;
编译器会报错,因为 return 1; 没有被括起来,不属于 main 函数。大括号将多条语句组合成一条复合语句,而函数体只能包含一条语句。没有大括号时,print 和 return 是两条独立的语句,函数只能有一条,所以第二条语句就游离在函数之外,这是非法的。
不过,你也可以用逗号运算符 , 来扩展复合语句,但不推荐这样做(不是最佳编码习惯)。示例如下:
代码: main()
print("Hello World!"), return 1;
函数
函数是一段完成特定任务的代码,可以从其他地方调用。它还可以将执行结果返回给调用它的地方。
调用
代码: print("Hello World!");
如“入门”部分所述,这会调用 print 函数(定义在 open.mp.inc 中,所以需要 #include),并让它在服务器控制台显示 “Hello World!”。
函数由函数名(如 print)和参数列表(用 () 括起来)组成。参数列表向函数传递额外数据。如果没有参数,你就需要成千上万个函数:
代码: printa();
printaa();
printab();
...
函数可以有任意数量的参数(从 0 开始,至少支持 128 个):
代码: printf("Hello World!", 1, 2, 3, 4, 5, 6);
现在不用关心这个函数具体做什么,只需知道它有 7 个参数,用逗号分隔。
定义
除了调用已有函数,你也可以自己编写并调用函数:
代码: #include <open.mp>
main()
{
return MyFunction();
}
MyFunction()
{
print("Hello World!");
return 1;
}
这段代码功能与最初的示例完全相同,只是结构不同。当模式启动时 main() 被自动调用,它再调用自定义函数 MyFunction()。MyFunction() 在控制台打印消息,然后返回 1 给 main(),main() 再把这个值返回给服务器。
因为 return MyFunction(); 是一条完整语句,你也可以写成:
代码: #include <open.mp>
main() return MyFunction();
MyFunction()
{
print("Hello World!");
return 1;
}
但大多数人为了清晰起见还是分开写。你也可以完全不使用 MyFunction 的返回值:
代码: #include <open.mp>
main()
{
MyFunction();
return 1;
}
MyFunction()
{
print("Hello World!");
return 1;
}
参数
参数是一种特殊的变量,你不需要自己声明,它来自调用函数的地方:
代码: #include <open.mp>
main()
{
return MyFunction("Hello World!");
}
MyFunction(const string[])
{
print(string);
return 1;
}
这段代码功能相同,但现在我们把要显示的内容传给了 MyFunction()。调用时把字符串 “Hello World!” 传递给函数,存储在名为 string 的变量中([] 表示这是一个数组,后面会解释)。然后调用 print,把 string 的内容传给它(没有引号,所以它是一个变量)。
变量
变量本质上是内存中的一块空间,用于存储数据,可以随时读取和修改。变量由一个或多个 cell 组成,一个 cell 是 32 位(4 字节),默认有符号,可存储 -2147483648 到 2147483647(注意 -2147483648 在 PAWN 中定义不完善,显示时可能异常)。由多个 cell 组成的变量叫数组,字符串是一种特殊的数组,每个 cell 存放一个字符(打包字符串每个 cell 可存 4 个字符,但这里不讨论)。
声明
要创建一个新变量,必须先声明它:
这会创建一个名为 myVariable 的变量,初始值为 0。
赋值
声明变量并设置初始值为 7。现在打印它会显示 7。要打印非字符串变量,需要使用前面提到的 printf():
代码: new
myVariable = 7;
printf("%d", myVariable);
现在只需知道这会在服务器控制台输出变量的值(当前是 7)。
代码: new
myVariable = 7;
printf("%d", myVariable);
myVariable = 8;
printf("%d", myVariable);
这段代码会先打印 7,然后把变量改为 8,再打印新的值。
变量操作示例(更多运算符请参考其他章节):
代码: myVariable = myVariable + 4; // 等价于
myVariable += 4; // 增加 4
myVariable -= 4; // 减少 4
myVariable *= 4; // 乘以 4
myVariable /= 4; // 除以 4
数组
声明
数组是一种可以同时存储多个数据、动态访问的变量。数组大小在编译时就固定了,必须提前知道需要多少个槽位。最常见的例子是 MAX_PLAYERS 数组,每个玩家一个槽位,确保数据不会互相干扰。
这会创建一个有 5 个槽位的数组。不能这样做:
代码: new
myVariable = 5,
myArray[myVariable]; // 错误!
因为 PAWN 在编译时就分配内存,数组大小必须是常量。
访问
要给数组某个位置赋值,必须指定下标(index),下标可以是变量:
代码: new
myArray[5];
myArray[2] = 7;
数组现在的内容是:0, 0, 7, 0, 0
注意:下标从 0 开始计数,而不是 1。
如果你尝试访问 myArray[5](超出范围),可能会导致服务器崩溃。
下标可以是数字、变量,甚至是返回值的函数。
代码: new
myArray[5],
myIndex = 2;
myArray[myIndex] = 7;
数组中的元素可以像普通变量一样使用:
代码: myArray[2] = myArray[2] + 1;
myArray[2] += 1;
myArray[2]++;
示例
最常见的数组是 MAX_PLAYERS 数组(MAX_PLAYERS 是一个常量,默认 1000)。下面对比用普通变量和数组处理 4 个玩家数据(假设 MAX_PLAYERS 为 4):
传统写法(4 个变量):
代码: new
gPlayer0, gPlayer1, gPlayer2, gPlayer3;
SetPlayerValue(playerid, value)
{
switch (playerid)
{
case 0: gPlayer0 = value;
case 1: gPlayer1 = value;
case 2: gPlayer2 = value;
case 3: gPlayer3 = value;
}
}
数组写法(推荐):
代码: new
gPlayers[MAX_PLAYERS];
SetPlayerValue(playerid, value)
{
gPlayers[playerid] = value;
}
数组写法无论玩家数量多少,都只需要一行代码,清晰且高效。
字符串
基本用法
字符串是一种特殊的数组,用于存放多个字符组成文本。每个字符默认占用一个 cell。字符串以 NULL 终止(遇到 0 就结束,不是字符 '0')。
代码: new
myString[16] = "Hello World!";
这会创建一个可容纳 15 个字符的字符串,初始值为 “Hello World!”。内部实际存储为:
代码: 104 101 108 108 111 0 x x x x x x x x x x
(0 是终止符,后面的 x 无关紧要)
你可以像操作数组一样修改字符串:
代码: new
myString[16] = "Hello World!";
myString[1] = 97; // 97 是 'a'
结果变为 “hallo”。
更易读的写法:
单引号表示单个字符。
把某个位置设为终止符:
代码: myString[1] = '\0'; // 或 = 0;
字符串会变成 “h”。
转义字符
反斜杠 \ 是特殊字符,用于创建无法直接输入的字符:
代码: new
myString[4] = "\""; // 字符串内容就是一个双引号 "
常用转义序列:
| 转义符 |
含义 |
说明 |
\0 / EOS |
NULL 字符 |
字符串结束 |
\n |
换行 |
Linux 新行(Windows 也可用) |
\r |
回车 |
Windows 新行用 \r\n |
\\ |
反斜杠 |
实际输出 \ |
\' |
单引号 |
在单引号中用 '\' ' |
\" |
双引号 |
在字符串中用 \" |
\xNNN; |
十六进制字符 |
用十六进制值设置字符 |
\NNN; |
十进制字符 |
用十进制值设置字符 |
标签
标签是变量的额外信息,用于定义其用途和可使用范围。标签分为强标签(首字母大写)和弱标签。
示例:
Float 就是标签,表示这是一个浮点数(小数)。
代码: native SetGravity(Float:gravity);
这要求参数必须是 Float 类型:
代码: SetGravity(6.0);
new Float:fGrav = 5.0;
SetGravity(fGrav);
使用错误标签会产生 tag mismatch 警告:
代码: SetGravity(MyTag:7); // 错误
标签区分大小写。
你可以自定义标签:
代码: new myTag: variable = 0,
AppleTag: another = 1;
直接相加时需要用 _: 去除标签,否则会报标签不匹配。
作用域
作用域决定变量在哪里可用。主要有四种:局部、静态局部、全局、全局静态。变量必须先声明后使用。
局部(local)
在函数内用 new 声明:
代码: MyFunc()
{
new var1 = 4;
printf("%d", var1);
{
new var2 = 8;
printf("%d %d", var1, var2);
}
// var2 已失效
}
// var1 已失效
局部变量每次进入函数都会重置:
代码: for (new i = 0; i < 3; i++)
{
new j = 1;
printf("%d", j);
j++;
}
输出:
静态局部(static local)
与局部变量作用域相同,但值会在函数多次调用间保留:
代码: for (new i = 0; i < 3; i++)
{
static j = 1;
printf("%d", j);
j++;
}
输出:
全局(global)
在函数外声明,可在任何函数中使用,且永不重置:
代码: new
gMyVar = 4;
MyFunc()
{
printf("%d", gMyVar);
}
全局静态(global static)
与全局类似,但仅限声明所在文件使用:
File1.pwn
代码: static
gs_MyVar = 4;
MyFunc()
{
printf("%d", gs_MyVar);
}
#include "File2"
File2.pwn
代码: MyFunc2()
{
// gs_MyVar 在这里不存在
printf("%d", gs_MyVar); // 错误
}
static 也可用于函数,实现类似“私有”函数的效果。
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| SAMP 迁移到 openmp 完整指南 + pawn 编译报错全解析 |
|
发布者: 小鸟unsigned - 03-20-2026, 10:36 PM - 板块: 教程
- 暂无回复
|
 |
SAMP 迁移到 openmp 完整指南 + pawn 编译报错全解析
引用:本篇适合:- 已有 SA-MP 服务器,想迁移到 open.mp 的开发者
- 在编译时遇到错误不知道怎么处理的新手
第一部分:迁移步骤
第一步:下载 open.mp 服务端
前往 https://github.com/openmultiplayer/open.mp/releases 下载最新版本。
open.mp-win-x86.zip — Windows 服务器
open.mp-linux-x86.tar.gz — Linux 服务器
第二步:解压
目录结构
代码: omp-server/
├── components/ ← open.mp 原生组件(某些插件放这里而不是 plugins)
├── filterscripts/ ← 过滤脚本(辅助脚本)
├── gamemodes/ ← 游戏模式(主脚本 .amx 放这里)
├── models/ ← 自定义模型文件(.txd .dff)
├── plugins/ ← 旧版插件(.dll 或 .so)
├── qawno/ ← Pawn 编辑器 + include 头文件
│ └── include/ ← 把 .inc 头文件放这里
├── scriptfiles/ ← INI 文件等资料
├── bans.json ← 封禁列表
├── config.json ← 服务器配置(替代 SA-MP 的 server.cfg)
└── omp-server.exe ← 服务器主程序
第三步:放置脚本插件文件
- 把你的游戏模式
.pwn 源文件放入 gamemodes/ 文件夹
- 把所需的
.inc 头文件(如 sscanf2.inc、streamer.inc)放入 qawno/include/
- 把插件文件(如
streamer.dll)放入 plugins/
引用:YSI 用户:需要升级到 YSI-5.x,YSI-4 与 open.mp 不兼容。
引用:FCNPC 用户:FCNPC 插件已被官方 open.mp NPC 组件取代,转换代码即可使用。
引用:YSF 用户:open.mp 已内置了大多数 YSF 的原生函数,不再需要 YSF。
注意:有些插件需要放进 components/ 而不是 plugins/,具体查阅以下清单:
常见插件
| 插件 |
状态 |
备注 |
| Streamer |
✅ 兼容 |
放 plugins/,正常加载 |
| MySQL R41-4 |
✅ 兼容 |
放 plugins/,正常加载 |
| CrashDetect |
✅ 兼容 |
开发调试,放 plugins/ |
| Whirlpool |
✅ 兼容 |
放 plugins/ 但不建议使用 |
| PawnPlus |
✅ 兼容 |
放 plugins/,open.mp 对其有专项优化 |
| pawn-memory |
✅ 兼容 |
放 plugins/ |
| JIT |
✅ 兼容 |
代码稳定后可用来提升性能,放 plugins/ |
| Profiler |
✅ 兼容 |
性能分析工具,放 plugins/ |
| FileManager |
✅ 兼容 |
放 plugins/ |
| sscanf |
? 放 components/ |
使用 sscanf 2.13.8 |
| Pawn.CMD |
? 放 components/ |
使用 3.4.0-omp |
| Pawn.RakNet |
? 放 components/ |
使用 1.6.0-omp |
| sampvoice |
? 放 components/ |
使用 v3.1.5-omp |
| discord-connector |
? 放 components/ |
使用 v0.3.6-pre |
| rustext |
⚠️ 需升级 |
使用 v2.0.11-nomemhack |
| keylistener |
⚠️ 需升级 |
使用 1.1.2-pr |
| BCrypt |
✅ 兼容 |
samp-bcrypt,密码哈希推荐方案 |
| SHA256_PassHash |
? 有替代 |
open.mp 中已标记为废弃,改用 BCrypt |
| nativechecker |
? 有替代 |
open.mp 已内置 native 检查机制,不再需要 |
| YSF |
? 有替代 |
open.mp 已内置大多数 YSF 原生函数,不再需要 |
| SKY |
? 有替代 |
改用 Pawn.RakNet |
| 中文名插件 |
? 有替代 |
open.mp 有内置函数可实现兼容中文昵称 |
| FCNPC |
❌ 不兼容 |
open.mp 官方内置 NPC 组件,性能更好,不再需要它 |
- plugins/ ← 旧版(legacy)插件放这里,在 config.json 的 legacy_plugins 里加载
- components/ ← open.mp 原生组件放这里,服务器启动时自动加载,无需配置
以上资料来源个人经验和 open.mp/docs/server/Installation
第四步:修改 #include
打开你的 .pwn 文件,把第一行:
改为:
然后按 F5 编译。
第五步:配置 config.json
用记事本打开 config.json,按需修改:
加载主图:
代码: "main_scripts": [
"你的脚本文件名 1"
]
加载插件:
代码: "legacy_plugins": [
"mysql",
"sscanf",
"streamer"
]
加载过滤脚本:
代码: "side_scripts": [
"filterscripts/你的过滤脚本名"
]
设置 RCON 密码:
代码: "rcon": {
"allow_teleport": false,
"enable": false,
"password": "你的强密码"
}
引用:如何把旧的 server.cfg 转换为 config.json,参考官方文档:https://www.open.mp/docs/server/config.json
第六步:启动服务器
Windows: 双击 omp-server.exe
Linux:
代码: chmod +x omp-server
./omp-server
第二部分:SA-MP 到 open.mp 的变化
有以下几个变化
变更一:拼写统一为英式英语
open.mp 将所有函数名统一为英式拼写,Color 改为 Colour,例如:
代码: // SA-MP 写法(会报 warning)
TextDrawBoxColor(textid, 0xFF0000FF);
TextDrawColor(textid, 0xFFFFFFFF);
// open.mp 正确写法
TextDrawBoxColour(textid, 0xFF0000FF);
TextDrawColour(textid, 0xFFFFFFFF);
批量替换方法:在你的编辑器里按 Ctrl+H,把所有 .....Color 替换为 .....Colour。
如果不想逐个修改,可以在 include 顶部加上:
代码: #define MIXED_SPELLINGS
#include <open.mp>
变更二:GetPlayerPoolSize 等函数已移除
GetPlayerPoolSize()、GetVehiclePoolSize()、GetActorPoolSize() 这三个函数在 open.mp 中已被移除,因为它们本身就存在 bug(返回的是最高 ID 而不是数量,而且没有玩家时会返回错误数据)。
代码: // 错误:这些函数不存在了
for(new i = 0; i <= GetPlayerPoolSize(); i++) { }
// 正确:直接用 MAX_PLAYERS
for(new i = 0; i < MAX_PLAYERS; i++)
{
if(IsPlayerConnected(i)) { }
}
// 或者用 foreach(需要 YSI)
foreach(new i : Player) { }
同理:GetPlayerPoolSize() → 换成 MAX_PLAYERS
GetVehiclePoolSize() → 换成 MAX_VEHICLES
GetActorPoolSize() → 换成 MAX_ACTORS
变更三:死亡不再自动扣 100 元
SA-MP 中玩家死亡后会自动扣除 100 元(住院费)。open.mp 移除了这个机制,让脚本自己管理金钱。
如果你的脚本之前为了「修复」这个扣钱机制,在 OnPlayerDeath 或 OnPlayerSpawn 里手动加了 GivePlayerMoney(playerid, 100),现在应该删掉这行,否则玩家死亡后反而会多 100 元。
如果你的脚本确实依赖这个「死亡扣钱」功能,在 OnPlayerDeath 里手动加上:
代码: public OnPlayerDeath(playerid, killerid, WEAPON:reason)
{
GivePlayerMoney(playerid, -100); // 手动模拟死亡扣款
return 1;
}
变更四:HideMenuForPlayer 行为修正
SA-MP 中 HideMenuForPlayer 会忽略你传入的菜单 ID,直接关掉玩家当前打开的任意菜单。
open.mp 修正了这个行为,现在它只会关闭你指定的那个菜单。
代码: // SA-MP 的老写法(在 open.mp 里可能不按预期工作)
HideMenuForPlayer(gShopMenu, playerid);
// open.mp 正确写法:先获取玩家当前菜单再关闭
HideMenuForPlayer(GetPlayerMenu(playerid), playerid);
变更五:SetPlayerAttachedObject 不再跨模式保留
SA-MP 中玩家身上的附加物件(attached objects)在游戏模式重启后仍然保留。open.mp 修正了这个行为,重启后附加物件会消失。
如果你需要在模式重启后保留玩家的附加物件,需要在 OnPlayerConnect 里重新为他设置。
变更六:GameText 样式统一(不再渐变)
SA-MP 中有些 GameText 样式会闪烁、有些会忽略时间参数。open.mp 统一用 TextDraw 重新实现了这些样式,外观相同但不再渐变。如果你的玩家反馈 GameText 显示效果有变化,这是正常现象。
自动升级工具
open.mp 提供了一个自动升级工具,可以批量修复旧代码中的 tag 问题、const 问题和拼写问题:
- 工具已包含在
qawno/upgrader/ 文件夹中
- 在线版本:https://github.com/openmultiplayer/upgrade
建议先用工具跑一遍,再手动处理剩余的报错。
第三部分:编译错误与警告完全手册
编译器报的信息分两种:
- error(错误):必须修复,否则无法生成
.amx 文件
- warning(警告):可以运行,但代码存在潜在问题,建议修复
常见 Error(错误)
error 001: expected token
含义:期望某个符号但没找到,通常是缺少分号、括号不匹配。
代码: // 错误:缺少分号
new gold = 100
SetPlayerHealth(playerid, 100.0)
// 正确
new gold = 100;
SetPlayerHealth(playerid, 100.0);
代码: // 错误:括号不匹配
if(gold > 0
{
// ...
}
// 正确
if(gold > 0)
{
// ...
}
error 017: undefined symbol "xxx"
含义:用了一个没有定义过的变量、函数或常量。
最常见的三种原因:
1. 名字拼写错误
代码: // 错误
SenClientMessage(playerid, -1, "hello"); // 少了 d
new scroe = 0; // 字母顺序错了
// 正确
SendClientMessage(playerid, -1, "hello");
new score = 0;
2. SetTimerEx 的目标函数忘记 forward
代码: // 错误:没有 forward,运行时找不到函数
SetTimerEx("MyFunc", 1000, false, "i", playerid);
public MyFunc(playerid) { return 1; }
// 正确:使用前先 forward
forward MyFunc(playerid);
SetTimerEx("MyFunc", 1000, false, "i", playerid);
public MyFunc(playerid) { return 1; }
3. 变量在使用之前没有声明
代码: // 错误:先用再声明
gold += 100;
new gold = 0;
// 正确:先声明再用
new gold = 0;
gold += 100;
error 021: symbol already defined "xxx"
含义:同一个名字被定义了两次。
代码: // 错误:同名变量重复声明
new gold = 0;
new gold = 100; // 重复了
// 错误:同名函数重复定义
MyFunction() { return 1; }
MyFunction() { return 0; } // 重复了
也可能是 .inc 文件被 #include 了两次,导致里面的函数被重复引入。
解决方法:在每个 .inc 文件顶部加防重复引入守卫:
代码: #if defined _MY_INC
#endinput
#endif
#define _MY_INC
error 025: function heading differs from prototype
含义:forward 声明的参数和实际 public 函数的参数不一致,或者参数类型不匹配。
代码: // 错误:forward 和 public 参数不一致
forward MyFunc(playerid);
public MyFunc(playerid, value) { return 1; } // 多了一个参数
open.mp 特别常见的情况是回调参数的 tag 不匹配:
代码: // 错误(SA-MP 写法)
public OnPlayerDeath(playerid, killerid, reason)
// 正确(open.mp 要求 WEAPON: tag)
public OnPlayerDeath(playerid, killerid, WEAPON:reason)
代码: // 错误
public OnPlayerEditAttachedObject(playerid, response, index, modelid, boneid, ...)
// 正确(open.mp 要求 EDIT_RESPONSE: tag)
public OnPlayerEditAttachedObject(playerid, EDIT_RESPONSE:response, index, modelid, boneid, ...)
error 010: invalid function or declaration
含义:函数定义或声明的语法写错了。
代码: // 错误:函数名后面缺括号
MyFunction
{
return 1;
}
// 正确
MyFunction()
{
return 1;
}
error 029: invalid expression, assumed zero
含义:表达式写法不合法,通常是运算符用错或者判断写法有问题。
代码: // 错误:判断用了单等号(赋值)而不是双等号(比较)
if(gold = 100) { } // 这是赋值,不是比较
// 正确
if(gold == 100) { }
error 035: argument type mismatch (argument N)
含义:函数调用时,第 N 个参数的类型与函数期望的类型不符。
代码: // 错误:SetPlayerHealth 需要 Float,传了整数
SetPlayerHealth(playerid, 100);
// 正确:加小数点
SetPlayerHealth(playerid, 100.0);
代码: // 错误:传了字符串给需要整数的参数
SetPlayerSkin(playerid, "86");
// 正确
SetPlayerSkin(playerid, 86);
error 047: array sizes do not match
含义:把一个数组赋值给另一个数组,但两者大小不一样。
代码: new a[10];
new b[20];
a = b; // 错误:大小不同,不能直接赋值
// 正确:用 for 循环逐元素复制,或者用 memcpy
for(new i = 0; i < 10; i++) a[i] = b[i];
error 004: function not implemented
含义:只有 forward 声明,没有对应的 public 函数体。
代码: // 错误:只 forward 了,没有写函数体
forward MyFunc(playerid);
// 下面没有 public MyFunc ...
// 正确:forward 之后要有对应的实现
forward MyFunc(playerid);
public MyFunc(playerid)
{
return 1;
}
常见 Warning(警告)
warning 213: tag mismatch
含义:变量或参数的 tag(类型标签)不匹配。这是迁移到 open.mp 最常见的警告。
open.mp 为很多函数参数增加了 tag,让编译器能检查你传的值是否合理:
代码: // 警告:传了普通整数而不是 bool
TogglePlayerControllable(playerid, 1);
// 正确:用 true/false
TogglePlayerControllable(playerid, true);
代码: // 警告:传了数字而不是枚举值
TextDrawFont(textid, 1);
GivePlayerWeapon(playerid, 4, 1);
// 正确:用枚举常量
TextDrawFont(textid, TEXT_DRAW_FONT_1);
GivePlayerWeapon(playerid, WEAPON_KNIFE, 1);
如果暂时不想处理这些警告,可以在顶部加:
代码: #define NO_TAGS
#include <open.mp>
// 或者只关闭 213 号警告
#pragma warning disable 213
warning 234: function is deprecated
含义:你用的函数已经过时,open.mp 建议换用新的写法。
TextDrawColor → TextDrawColour
代码: TextDrawColor(textid, 0xFFFFFFFF); // 警告
TextDrawColour(textid, 0xFFFFFFFF); // 正确
GetPlayerPoolSize / GetVehiclePoolSize / GetActorPoolSize
代码: GetPlayerPoolSize() // 警告:已移除
MAX_PLAYERS // 正确
SHA256_PassHash(不安全的密码哈希)
代码: SHA256_PassHash(...); // 警告:SHA-256 不安全
// 改用 BCrypt:https://github.com/Sreyas-Sreelal/samp-bcrypt
warning 214 / 239: const 相关警告
含义:把字符串或数组传给没有 const 修饰的参数,或者反过来。
代码: // 警告:参数应该是 const
public MyFunction(string[])
// 正确:加上 const
public MyFunction(const string[])
warning 203: symbol is never used
含义:你声明了一个变量或函数,但从来没有使用过。
代码: // 警告:声明了但没用
new iUnusedVar = 0;
MyUnusedFunction()
{
return 1;
}
解决方法:删掉没用的变量/函数,或者用 stock 修饰函数告诉编译器「不用也不要警告」:
代码: stock MyMaybeUnusedFunction()
{
return 1;
}
warning 204: symbol is assigned a value that is never used
含义:给变量赋值了,但这个值从来没被读取。
代码: new iResult = SomeFunction(); // 警告:iResult 赋了值但后面没用到
通常意味着这行代码没有实际效果,检查是否逻辑有误。
warning 211: possibly unintended assignment
含义:在 if 条件里用了单等号 =,可能是误把赋值写成了比较。
代码: // 警告:这是赋值,不是比较,结果永远为 true
if(gold = 100) { }
// 正确:比较用双等号
if(gold == 100) { }
warning 219: local variable shadows a variable at a higher level
含义:在内层作用域声明了一个和外层同名的变量,内层的「遮住」了外层的,容易引发逻辑混乱。
代码: new gold = 100; // 外层
public OnPlayerConnect(playerid)
{
new gold = 50; // 警告:这个 gold 遮住了外层的 gold
// 在这个函数里,gold 指的是 50 那个
}
解决方法:给内层变量换个名字,或者直接用外层变量。
warning 225: unreachable code
含义:有一段代码永远不会被执行到,通常是 return 后面还有代码。
代码: MyFunction()
{
return 1;
SendClientMessage(playerid, -1, "这行永远不会执行"); // 警告
}
warning 209: function should return a value
含义:函数应该有返回值,但某个分支没有 return。
代码: // 警告:else 分支没有 return
MyFunction(iValue)
{
if(iValue > 0)
{
return 1;
}
// 如果 iValue <= 0,函数执行完没有 return
}
// 正确:所有分支都要有 return
MyFunction(iValue)
{
if(iValue > 0)
{
return 1;
}
return 0;
}
warning 202: number of arguments does not match definition
含义:调用函数时传入的参数数量和函数定义不一致。
代码: // 函数定义需要 2 个参数
MyFunction(playerid, value)
{
return 1;
}
// 警告:只传了 1 个参数
MyFunction(playerid);
// 正确:传够参数
MyFunction(playerid, 100);
运行时报错(服务器控制台)
这些不是编译错误,而是服务器运行时打印在控制台的提示。
Couldn't announce legacy network to open.mp list
代码: [Info] Couldn't announce legacy network to open.mp list.
[Info] [Server Error] Status: 406
含义:服务器无法被 open.mp 的服务器列表访问到。
原因:- 你在本地运行,没有公网 IP
- 防火墙阻止了对应端口
影响:不影响服务器正常运行,玩家仍然可以通过 IP 直连,只是不会显示在公开服务器列表里。
Insufficient specifiers given to format
代码: [Warning] Insufficient specifiers given to `format`: "?" < 1
含义:format 函数里,格式字符串中的占位符数量少于你传入的参数数量。
代码: new str[32];
new name[32] = "Tom";
format(str, sizeof(str), "Hello", name); // 警告:格式字符串里没有 %s,但传了 name
// 正确
format(str, sizeof(str), "Hello %s", name);
有用的资源
延伸阅读
如果您在运行服务器时仍有问题,请加入官方的 open.mp Discord 服务器:https://discord.gg/samp
在 #openmp-support 频道提问。
或者在我们的社区群求助: 673335567
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
|
|
|
|
| 零基础开发openmp/SAMP服务器基础教程 - 第一章 |
|
发布者: 小鸟unsigned - 03-20-2026, 09:38 PM - 板块: 教程
- 暂无回复
|
 |
零基础开发openmp/SAMP服务器基础教程 - 第一章
写给完全没接触过编程的你
零基础开发openmp/SAMP服务器基础教程一共有三个篇章,此为第一篇,按顺序学习阅读即可
引用:这篇文章不会让你马上写出任何代码。
它只做一件事:让你在打开编辑器之前,脑子里先有一张地图。
有了这张地图,后面学什么都会快很多。
你可能正处于这样的处境
你玩过 SAMP 联机,看到别的服务器有各种有趣的玩法,心里想:这个我也想做。
然后你搜了一下教程,打开了一个 .pwn 文件,看到满屏的英文字母和括号符号,看不懂。
然后关掉了。
这不是你的问题。是大多数教程默认你已经有一些基础,直接跳过了最重要的第一步——建立认知。
这篇文章就是来补这一步的。
理解底层逻辑
引用:理解这些尤为重要,是一切的起点。本篇将优先使用新手能理解的大白话进行讲解。
计算机是一个非常听话但非常死板的工具,先忘掉代码,我们来聊聊计算机到底是什么。
比方说地图导航软件
你输入目的地,导航会告诉你:前方 200 米左转,然后直行 500 米,到达目的地。
导航给出的是一步一步的指令,你只需要跟着做。如果你在它说左转的时候右转了,它不会生气,也不会猜你的意思,它只会重新计算,继续给你下一条指令。
计算机就是这样的。
它只会严格地、一条接一条地执行你给它的指令。
它不会猜,不会发挥,不会应该是这个意思吧帮你圆场。你说什么,它做什么。你没说的,它绝对不做。
而编程,就是把你的想法翻译成计算机能看懂的指令。
这件事没有你想的那么神秘
很多人会觉得这是某种神秘的技术,像外星文字一样难以理解。
但你其实每天都在做类似的事情。
假设你要告诉一个什么都不懂的人,怎么帮你冲一杯咖啡,你会怎么说?
你不能说帮我冲杯咖啡,因为他不知道冲咖啡是什么意思。
你需要说:
代码: 1. 走到茶水间
2. 从柜子里取出咖啡粉
3. 打开水壶,确认里面有水
4. 按下水壶开关,等待水烧开
5. 取出一个杯子
6. 在杯子里放入一勺咖啡粉
7. 把烧开的水倒进杯子里
8. 搅拌均匀
你写的这份说明书,其实就是一段程序。
你是在把帮我冲咖啡这个模糊的想法,拆分成一步一步清晰的指令。
编程做的事情,和这个完全一样。
唯一的区别是:你的指令不是写给人看的,是写给计算机看的,所以要用计算机能懂的语法来写。
学编程,本质上是在学把事情说清楚,编程里真正难的不是记住语法,而是把你的想法说得足够清楚、足够准确,清楚到计算机能执行。想法是模糊的。你需要把它变得更具体,这就是代码背后的思维过程。语法是最后才出现的东西,在这之前,你要先把逻辑想清楚。
为什么代码要用奇怪的语法写?
为什么不能直接用中文写?直接告诉电脑给玩家 100 块钱不行吗?
因为计算机本身只认识 0 和 1,它不理解任何人类语言。
所以人们发明了编程语言——它是人和计算机之间的翻译官。
编程语言的语法比中文严格得多,但也简单得多。
中文有几万个汉字,有各种语气、歧义、修辞。编程语言通常只有几十个固定的关键词,每一个词只有一个意思,没有歧义。
比如在 Pawn 里:
代码: if → 如果
else → 否则
new → 新建一个变量(容器)
return → 返回一个结果,并结束
就这些,你能用的词总共就这么多。其他所有千变万化的功能,都是用这些基础词拼出来的。
所以,代码并不是在用一门你不认识的语言说话,它只是在用一套更严格的格式,表达你本来就会表达的逻辑。
不会就查,查了才会
open.mp 有完整的官方文档,里面列出了所有可用的函数和回调,以及每个函数怎么用、参数是什么意思。
地址:https://open.mp/docs
养成看文档的习惯,这是最重要的自学能力。当你查了文档、搜了搜索引擎,还是搞不定,这时候可以去问人
不好的问法:为什么我的代码不行?
好的问法:我想实现玩家加入时显示欢迎消息,我写了这段代码(贴上代码),编译没有报错,但玩家加入时没有收到消息,请问可能是什么原因?
把你的代码、你的预期、你观察到的现象都说清楚,别人才能帮你快速定位问题。你描述问题的清晰程度,决定了你能得到帮助的速度。
报错,是编译器在帮你
报错信息是编译器(帮你把代码翻译成计算机能运行的文件的工具)在帮你找问题。
报错信息通常会告诉你两件事:
就算是工作了十年的程序员,每天也在面对 bug(程序里的错误)。
但是pawn相对简单很多,熟悉了之后,有时都不需要实际测试都可以直接通过看代码预判出运行的效果。
进入正题
Pawn 是什么
Pawn 是一种为游戏服务器设计的轻量脚本语言,不是通用高级语言。
对于想学习编程的人来说,它是一个很好的起点——因为你写的代码可以立刻在游戏里看到效果,这种即时反馈会让你进步很快。
本质上,这是在用C 语言的思维写游戏逻辑。学会 Pawn 的人,以后转 C/C++、Rust 都很快。在国外有不少人通过 SA-MP 开发找到了工作,后来接触到了 C++、PHP、JavaScript 等更复杂的方向。
脚本文件是什么
你写的 Pawn 代码保存在一个 .pwn 文件里,这就是你的脚本源码。
写完后需要经过编译,把 .pwn 变成服务器能运行的 .amx 文件,服务器加载的是 .amx,不是 .pwn。
代码: 你写的代码 编译 服务器运行
myscript.pwn → myscript.amx → open.mp 加载执行
这个过程就像你写了一份中文食谱,翻译成机器能理解的语言,机器才能按步骤操作。
单线程
目前你只需要知道一件事:Pawn 服务器是单线程的。
单线程意味着:整个服务器只有一条流水线,所有逻辑、计算只能排着队一个一个处理,同一时间只能执行一条指令。
举个例子:假设你写了一段代码需要执行 1 秒钟,那在这 1 秒内,服务器上其他所有事情都会停下来等它跑完,包括玩家的移动同步、其他玩家的操作响应。
早期玩过 RPG 服的应该有印象——每到发薪日,服务器会卡顿一下。就是因为给所有玩家结算工资的代码在单线程里跑完之前,所有事件都被暂停了。
结论:写代码要注重性能,避免一段逻辑执行时间过长。单条简单的加减、判断代码几乎是瞬间完成的(微秒级别),这类代码完全不用担心。
理解 Tick(帧)
open.mp 默认每秒运行 30 个 tick(帧),也就是每秒打包、更新、同步 30 次数据。
如果网络不好(玩家或服务器任意一方),就可能出现某次 tick 的数据包没有成功传输,也就是俗称的丢包。
比如连续两秒没收到玩家的位置数据,第三秒突然收到了,就会出现我们常见的瞬移现象。网络好的情况下,每帧数据几乎全部到达,玩家动作就很平滑。
即使你什么逻辑都没写,open.mp 每个 tick 也会自动同步这些基础数据:
- 玩家位置、朝向
- 玩家动作状态
- 武器信息
- 血量和护甲
- 车辆位置、朝向
- 玩家按键状态
从玩家操作到游戏表现,完整流程
代码: 玩家操作
↓
客户端生成数据包,通过 UDP 发送到服务端
↓
服务端接收数据包,自动触发对应的回调函数
↓
回调内的 Pawn 代码执行,修改服务器内存里的数据
↓
Tick 结束,服务器把最新数据同步给所有客户端
↓
玩家在游戏里看到最终表现
不同的玩家操作,会触发不同的回调:
| 玩家操作 |
触发的回调 |
| 移动位置 |
OnPlayerUpdate |
| 开枪 |
OnPlayerWeaponShot |
| 按下按键 |
OnPlayerKeyStateChange |
| 操作对话框 |
OnDialogResponse |
| 死亡 |
OnPlayerDeath |
举个例子:友伤保护
当玩家开枪,触发 OnPlayerWeaponShot。在这个回调里,你可以判断被击中的是不是队友,如果是队友就让伤害为 0:
代码: public OnPlayerWeaponShot(playerid, weaponid, hittype, hitid, Float:fX, Float:fY, Float:fZ)
{
if(IsTeamMate(playerid, hitid))
{
return 0; // 返回 0 = 取消这次子弹伤害
}
return 1; // 返回 1 = 允许子弹正常造成伤害
}
再举个例子:对话框治疗
当玩家操作对话框,触发 OnDialogResponse。判断玩家选了哪个选项,然后执行对应逻辑:
代码: public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
if(listitem == 2) // 第三个选项(listitem 从 0 开始,0=第一个,1=第二个,2=第三个)
{
SetPlayerHealth(playerid, 100.0); // 设置血量为 100
}
return 1;
}
引用:注意:listitem 从 0 开始计数,第一个选项是 0,第二个是 1,第三个是 2,以此类推。这是新手非常容易犯错的地方。
关于作弊
服务器收到的数据,默认是信任客户端发来的。作弊工具就是利用这一点,篡改客户端发出的数据包(比如把血量改成 10000、位置瞬移到某处),服务器收到后会直接把这些数据同步给其他玩家,其他人就会看到无敌血条、飞天瞬移。
GetPlayerMoney 获取的就是客户端发来的金钱数据。如果你没有用服务端自己的变量记录玩家金钱,就相当于完全信任了客户端,没有任何验证。
解决方法:重要的数据(血量、金钱、等级等)永远在服务端自己维护,不信任客户端发来的数值。
什么是回调(Callback)
回调是服务端自动触发的函数,触发时机是服务端提前定好的,你不需要手动调用它。
你能做的是:在这个函数里写你自己的逻辑。
所有服务端自带的回调都以 public On 开头,比如:
代码: public OnPlayerConnect(playerid)
{
// 当有玩家连入服务器时,这里的代码会自动执行
return 1;
}
public OnPlayerDeath(playerid, killerid, reason)
{
// 当有玩家死亡时,这里的代码会自动执行
return 1;
}
public 是什么意思
public 表示这个函数是公开的,可以被服务端直接调用。服务端自带的回调必须加 public,否则服务端找不到它,触发时就不会执行你写的代码。
return 1 是什么意思
函数执行完毕后,需要返回一个结果告诉服务端处理完了。
return 1 = 正常处理,允许默认行为继续
return 0 = 在某些回调里代表拦截/取消这次事件(比如上面的 OnPlayerWeaponShot 返回 0 取消伤害)
具体每个回调返回 0 和 1 各代表什么,查文档就知道。新手阶段统一写 return 1 不会出大问题。
每个回调都是一个独立的房间
回调的范围 = 花括号 {} 包着的所有内容。从 { 到 } 之间的所有代码都属于这个回调。
代码: public OnPlayerConnect(playerid)
{ ← 房间开始
// 这里的代码属于 OnPlayerConnect
// 玩家连入时执行
} ← 房间结束
什么是函数
函数是一个工具,你不需要知道它内部怎么实现的,只需要知道怎么用、给它传什么参数、它会做什么事。
就像微波炉:你不需要知道微波炉的电路原理,只需要把食物放进去、设好时间、按启动,它就帮你加热。
服务端自带大约 1000 个左右的函数,每个函数从名字上就能大致猜到它的作用:
代码: SetPlayerHealth(playerid, Float:health) // 设置玩家血量
SetPlayerSkin(playerid, skinid) // 修改玩家皮肤
SendClientMessage(playerid, color, msg) // 给玩家发送聊天消息
GivePlayerMoney(playerid, money) // 给玩家钱(负数是扣钱)
GetPlayerPos(playerid, Float:x, Float:y, Float:z) // 获取玩家坐标
括号里是参数,告诉函数对谁做做什么。
能不能自己创建函数
可以。当你发现一段代码要重复写很多次,就该把它打包成函数。
代码: // 没有函数:每次都要重复写
GivePlayerMoney(playerid, 500);
SetPlayerHealth(playerid, 100.0);
SendClientMessage(playerid, -1, "欢迎奖励已发放");
// 下一个地方又要写一遍...
GivePlayerMoney(playerid, 500);
SetPlayerHealth(playerid, 100.0);
SendClientMessage(playerid, -1, "欢迎奖励已发放");
代码: // 有了函数:打包一次,随时调用
GiveWelcomeReward(playerid)
{
GivePlayerMoney(playerid, 500);
SetPlayerHealth(playerid, 100.0);
SendClientMessage(playerid, -1, "欢迎奖励已发放");
}
// 需要用的地方只需要一行
GiveWelcomeReward(playerid);
什么时候该建函数? 一段逻辑在两个以上的地方用到,或者逻辑比较复杂、需要给它起个名字方便理解,就建函数。
能不能用别人写好的函数
可以,这就是安装插件和依赖库。
你不需要为了拧一颗螺丝自己炼钢做螺丝刀,直接去五金店买一把就好了。
别人做好的功能,打包成插件,你直接引入使用,不需要知道内部原理。
不过在用之前可以留意几点:- 它会不会消耗大量服务器性能?
- 作者是否持续维护?
- 社区是否有人在用、有没有已知的 bug?
去哪里找? Github 搜索,或者 open.mp 官方推荐列表:https://open.mp/docs/awesome
回调的执行顺序
回调不是按固定顺序排好的,而是由 tick 中收到了什么数据来决定。
玩家没有做某个操作,对应的回调就不会触发。
如果一个玩家在同一个 tick 内同时触发了多个操作(极罕见),服务器会把这些事件排成队列,在单线程里一个接一个处理。
但回调内部的代码,是严格从上到下一行一行执行的。
代码写在哪里
所有实际运行的代码,都必须写在某个回调的花括号里面。
写在外面的只能是:变量声明、函数定义、常量定义。
你自己写的函数,不会自动运行,必须在某个回调里主动调用它,才会被执行:
代码: // 这个函数不会自动运行
MyFunction(playerid)
{
SendClientMessage(playerid, -1, "你好");
}
// 必须在某个回调里调用它,才会执行
public OnPlayerConnect(playerid)
{
MyFunction(playerid); // ← 在这里调用,玩家连入时才会执行
return 1;
}
可以把服务器想象成一台自动售货机:投币(玩家操作)会触发里面固定的机械流程(服务端回调)。你想加功能,只能在原有流程的某个环节里接线(在回调里调用你自己写的函数),你的代码不会凭空自己运行。
决定你高度的关键因素
掌握了上面这些,你已经理解了 open.mp 服务端运行的核心原理。剩下的,本质上都是数学和逻辑问题。
但有一件事只能靠你自己做:查文档。
你能实现什么功能,取决于你知道多少回调和函数的存在。不需要把所有函数都背下来,但你需要通过不断查阅,建立起自己的工具库认知——知道有什么工具可以用,需要的时候去查怎么用。
官方文档:https://open.mp/docs
交流群:673335567
新手最常犯的错误是:遇到不会的功能,直接问别人这个怎么做,但别人告诉你思路了,你又不知道用哪个函数实现。
根本原因是对现有工具了解太少。多看文档,多看别人的开源脚本,这是没有捷径的积累过程。
你知道的越多,能做的就越多。
#GTA# #圣安地列斯# #侠盗猎车手# #圣安地列斯联机# #samp# #gta联机# #gtasa联机# #openmp# #omp# #open.mp# #gtasa#
社区交流群: 673335567
论坛: https://open-mp.cn/
|
|
|
|
| samp/openmp MySQL 数据库教程 |
|
发布者: 小鸟unsigned - 03-20-2026, 08:55 PM - 板块: 教程
- 暂无回复
|
 |
samp/openmp MySQL 数据库教程
数据保存:读、查、改、删
引用:这篇教程只教你用代码操作数据库里的数据。
不涉及登录注册系统,只讲最核心的数据库操作原理和用法。
第一章:数据库?
数据库在哪?
你可以把数据库当成一个 Excel 文件,它就在你电脑硬盘上。
这个 Excel 文件里有很多张表格,每张表格存某一类型的数据,比如玩家数据 载具数据 房屋数据:
代码: 数据库(openmp)
├── players 表 ← 存玩家数据
│ ├── id | name | gold | level | skin
│ ├── 1 | Tom | 5000 | 10 | 287
│ └── 2 | Jane | 2000 | 5 | 86
│
├── houses 表 ← 存房产数据
│ └── ...
│
└── vehicle 表 ← 存载具数据
└── ...
为什么不用 .ini 文件或者 .txt 文件存?
可以,但MySQL数据库的优势:
- 高并发处理能力:不会阻塞主线程,支持非阻塞/异步查询
- 速度快:性能强劲,毫秒级返回结果,不需要历遍查找数据
- 安全:数据安全可靠,也可以设置定时备份数据
- 灵活:可以使用SQL语句执行各种复杂的数据库操作
- 维护管理:可视化工具可直接管理、修改、备份数据
第二章:为什么要连接数据库?
数据库是一个独立运行的软件(MySQL),它和你的 open.mp 服务端是两个程序。
就像你用 QQ 发消息,你要先登录QQ连接腾讯的服务器,才能使用它发消息。
你的 Pawn 脚本要操作数据库,也需要先建立连接,告诉数据库:
建立连接之后,你才能往里面读数据、写数据。
第三章:安装准备
3.1 在本地安装 MySQL
下载 MariaDB Server (80多mb)
下载地址:https://mariadb.org/download/
安装过程有两点需要注意
- 在user settings步骤的时候需要你创建一个密码,这个密码你在后面连接的时候需要用到
MariaDB 安装包自带 HeidiSQL,安装完成之后你的桌面会有一个快捷方式:HeidiSQL,这是一个可视化数据库管理工具,不会操作的新手,网上也有非常多的视频教程更加直观,我就不多说了
3.2 下载 MySQL R41-4 插件
下载地址:https://github.com/pBlueG/SA-MP-MySQL/re.../tag/R41-4
解压后,把文件按下表复制到服务端对应位置:
| 文件 |
复制到 |
plugins/mysql.dll |
服务端 plugins/ 目录 |
libmariadb.dll |
服务端根目录(和 omp-server.exe 同级) |
log-core.dll |
服务端根目录 |
pawno/include/a_mysql.inc |
服务端 qawno/include/ 目录 |
3.3 在 config.json 里加载插件
代码: "pawn": {
"legacy_plugins": [
"mysql"
]
}
启动服务器,控制台看到这行说明插件加载成功:
代码: Loading plugin: mysql
>> plugin.mysql: R41-4 successfully loaded.
第四章:创建数据库并连接数据库
插件装好了,接下来在脚本里建立连接,首先你要先使用 HeidiSQL 创建一个名为 openmp 的数据库。
创建好之后,在pawn写以下代码
代码: #include <open.mp>
#include <a_mysql> // 引入 MySQL 插件头文件
// 全局变量:保存连接句柄
// 句柄相当于钥匙,后续所有操作都要带上它
new MySQL:g_SQLHandle;
public OnGameModeInit()
{
// mysql_connect("数据库地址", "用户名", "密码", "数据库名")
g_SQLHandle = mysql_connect("127.0.0.1", "root", "password", "openmp");
// ↑ 127.0.0.1 = 本机,本地固定写这个
// 检查连接是否成功
if(g_SQLHandle == MYSQL_INVALID_HANDLE || mysql_errno(g_SQLHandle) != 0)
{
print("[MySQL] 连接失败!请检查 MySQL 是否运行。");
// 连不上数据库就关服,避免带着错误运行
// 这也是很多新手拿到开源图打开就闪退的原因
// 因为大部分服务器都是使用MySQL作为数据库
// 你没有正确搭建数据库环境,下面这行代码会自动关闭服务器
SendRconCommand("exit");
return 1;
}
print("[MySQL] 数据库连接成功!");
return 1;
}
第五章:创建数据表
连接成功后,需要在数据库里建一张表来存玩家数据。
什么是 SQL?
SQL 是操作数据库专用的语言,每一条 SQL 语句就是给数据库下一道命令,其实你会发现SQL语句就像是说人话一样,教程的最后会展示一些实用便利的SQL语句示例。
代码: public OnGameModeInit()
{
mysql_query(g_SQLHandle,
"CREATE TABLE IF NOT EXISTS `players` (\
`id` INT NOT NULL AUTO_INCREMENT,\
`name` VARCHAR(24) NOT NULL UNIQUE,\
`gold` INT DEFAULT 0,\
`level` INT DEFAULT 1,\
`skin` INT DEFAULT 86,\
PRIMARY KEY (`id`))",
false
);
// 说明:
// CREATE TABLE = 创建表
// IF NOT EXISTS = 如果表不存在才创建,已存在就跳过
// id - 整数,不能为空,自动递增(每插入一行自动+1,相当于每行的编号)
// name - 最多24字符的字符串,不能为空,且不能重复(UNIQUE)
// gold - 整数,不填时默认值是 0
// level - 整数,不填时默认值是 1
// skin - 整数,不填时默认值是 86
// PRIMARY KEY(id) - 设 id 为主键,数据库用它来精确定位某一行
print("[MySQL] 数据表就绪!");
return 1;
}
执行完这段代码后,你的数据库里就有了一张这样结构的空表:
代码: players 表
┌────┬──────┬──────┬───────┬──────┐
│ id │ name │ gold │ level │ skin │
├────┼──────┼──────┼───────┼──────┤
│ │ │ │ │ │
└────┴──────┴──────┴───────┴──────┘
常用列类型速查
| 类型 |
说明 |
适用场景 |
INT |
整数 |
金币、等级、皮肤 ID |
VARCHAR(n) |
最多 n 个字符的字符串 |
玩家名字、称号 |
FLOAT |
小数 |
坐标、血量 |
TEXT |
长文本(无长度限制) |
个人简介、日志 |
TINYINT(1) |
0 或 1,常用作布尔值 |
是否是管理员、是否封禁 |
常用列约束速查
| 约束 |
说明 |
NOT NULL |
不允许为空,插入时必须填这一列 |
DEFAULT 值 |
不填时使用的默认值 |
UNIQUE |
该列的值在整张表里不能重复 |
AUTO_INCREMENT |
自动递增,配合 INT 主键使用 |
PRIMARY KEY |
主键,每行的唯一标识,一张表只能有一个 |
我需要背这些语句吗?
不需要,没有人会背这些,包括写了很多年代码的开发者,你只需要大概知道每种语句是干什么用的,要用的时候回来翻一下,或者直接百度搜 MySQL 创建表格语法,几秒钟就找到了,又或者直接可视化工具输出具体的SQL语句
第六章:同步 vs 异步(非常重要)
在讲增删改查之前,必须先理解这个概念,否则你会做出卡服务器的代码。
同步查询(mysql_query)—— 阻塞查询
代码: mysql_query(g_SQLHandle, "SELECT * FROM players");
// 同步查询会让服务器等待数据库返回结果,期间其他代码暂停
// OnGameModeInit 是服务器刚启动时执行的,此时没有任何玩家在线
// 有玩家在线时永远不要使用阻塞查询,除非你真的知道你在干什么
就像你在餐厅点餐,服务员站在那里死等厨房做完,期间不接待任何其他桌。
引用:但新手不必害怕这个阻塞查询,阻塞过程可能才千分之一秒,也可能因特殊软件异常或你使用的是远程数据库导致的网络波动会持续更久,但安全意识很重要,只要记住在必要时仅在服务器初始化和启动时同步,其他地方异步即可
异步查询(mysql_tquery)—— 非阻塞查询
代码: mysql_tquery(g_SQLHandle, "SELECT * FROM players", "我的回调函数", "i", playerid);
// 把查询任务交给后台线程,然后立刻执行其他代码
// 等查询完成后,会自动调用 "我的回调函数"
就像你在餐厅点完餐,服务员给你一个号码牌,你的菜好了他会来找你,服务员继续招待其他桌。
第七章:INSERT — 插入(新增一行数据)
场景:玩家第一次进入服务器,在数据库里创建一条新的记录。
SQL 语句解释
代码: INSERT INTO `players` (`name`, `gold`, `skin`) VALUES ('Tom', 100, 86)
INSERT INTO = 向某张表插入数据
players = 表名
(name, gold, skin) = 要填写哪几列信息
VALUES (...) = 信息对应的值
id 不需要填,因为它是 AUTO_INCREMENT,数据库会自动分配。
在 Pawn 里写
代码: InsertPlayer(playerid)
{
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
// 使用 mysql_format 防止玩家名字里有单引号导致 SQL 出错(SQL 注入防护)
// %e 是转义
new query[128];
mysql_format(g_SQLHandle, query, sizeof(query),
"INSERT INTO `players` (`name`, `gold`, `skin`) VALUES ('%e', 50, 86)",
name
);
// 插入完成后调用 OnPlayerInserted,传入 playerid 告诉回调是哪个玩家
mysql_tquery(g_SQLHandle, query, "OnPlayerInserted", "i", playerid);
printf("[MySQL] 玩家 %s 数据正在创建...", name);
return 1;
}
// mysql_tquery 完成后自动调用这里
// mysql 会把sql语句的返回结果传入此回调中
forward OnPlayerInserted(playerid);
public OnPlayerInserted(playerid)
{
if(!IsPlayerConnected(playerid)) return 0;
// cache_insert_id() 返回刚刚插入的那行的 id
new insertID = cache_insert_id();
if(insertID > 0)
printf("[MySQL] 玩家 %d 已创建,数据库 ID:%d", playerid, insertID);
return 1;
}
第八章:SELECT — 查询(读取数据)
场景:玩家进入服务器,从数据库里读取他的金币、等级、皮肤。
SQL 语句解释
代码: SELECT `gold`, `level`, `skin` FROM `players` WHERE `name` = 'Tom'
SELECT = 我要查询
gold, level, skin = 查这几列(用 * 代表查所有列)
FROM players = 从 players 表里查
WHERE name = 'Tom' = 条件:只要名字是 Tom 的那行
在 Pawn 里写
代码: // 存玩家数据的全局数组
new g_Gold[MAX_PLAYERS];
new g_Level[MAX_PLAYERS];
new g_Skin[MAX_PLAYERS];
LoadPlayerData(playerid)
{
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
new query[128];
mysql_format(g_SQLHandle, query, sizeof(query),
"SELECT `gold`, `level`, `skin` FROM `players` WHERE `name` = '%e' LIMIT 1",
name
);
// LIMIT 1 = 表示限制只查1行,名字是唯一的所以这样写更高效
mysql_tquery(g_SQLHandle, query, "OnPlayerDataLoaded", "is", playerid, name);
return 1;
}
forward OnPlayerDataLoaded(playerid, const name[]);
public OnPlayerDataLoaded(playerid, const name[])
{
if(!IsPlayerConnected(playerid)) return 0;
// 查询结果有几行
if(cache_num_rows() == 0)
{
// 没找到这个玩家,说明他是新玩家,去插入一条记录
InsertPlayer(playerid);
return 1;
}
// 从结果的第 0 行(第一行)读取数据,填入全局数组
// cache_get_value_name_int(行号, "列名", 存到哪个变量)
cache_get_value_name_int(0, "gold", g_Gold[playerid]);
cache_get_value_name_int(0, "level", g_Level[playerid]);
cache_get_value_name_int(0, "skin", g_Skin[playerid]);
printf("[MySQL] 玩家 %s 数据加载完毕:金币=%d 等级=%d 皮肤=%d", name, g_Gold[playerid], g_Level[playerid], g_Skin[playerid]);
// 数据读好了,让玩家出生
SpawnPlayer(playerid);
return 1;
}
读取不同类型数据的函数
| 函数 |
用途 |
cache_get_value_name_int(行, "列", 变量) |
读取整数 |
cache_get_value_name_float(行, "列", 变量) |
读取浮点数 |
cache_get_value_name(行, "列", 变量, 长度) |
读取字符串 |
第九章:UPDATE — 修改(更新数据)
场景:玩家下线时,把他当前的金币、等级、皮肤写回数据库。
SQL 语句解释
代码: UPDATE `players` SET `gold` = 500, `level` = 3 WHERE `name` = 'Tom'
UPDATE players = 更新 players 表
SET gold = 500, level = 3 = 把这些列设置成新的值
WHERE name = 'Tom' = 只改名字是 Tom 的那行
引用:WHERE 非常重要:如果不写 WHERE,整张表所有行都会被修改!
在 Pawn 里写
代码: SavePlayerData(playerid)
{
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
new query[256];
mysql_format(g_SQLHandle, query, sizeof(query),
"UPDATE `players` SET `gold` = %d, `level` = %d, `skin` = %d WHERE `name` = '%e'",
g_Gold[playerid],
g_Level[playerid],
g_Skin[playerid],
name
);
// 保存不需要读取回调结果,传空字符串 "" 表示不需要回调
mysql_tquery(g_SQLHandle, query, "");
printf("[MySQL] 玩家 %s 数据已保存", name);
}
// 玩家下线时调用
public OnPlayerDisconnect(playerid, reason)
{
SavePlayerData(playerid);
return 1;
}
第十章:DELETE — 删除(删除一行数据)
场景:把这个玩家的数据从数据库里彻底删除。
SQL 语句解释
代码: DELETE FROM `players` WHERE `name` = 'Tom'
DELETE FROM players = 从 players 表里删除
WHERE name = 'Tom' = 只删名字是 Tom 的那行
引用:同样注意 WHERE:没有 WHERE 条件,整张表的数据全删光!
在 Pawn 里写
代码: DeletePlayerData(playerid)
{
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
new query[128];
mysql_format(g_SQLHandle, query, sizeof(query),
"DELETE FROM `players` WHERE `name` = '%e'",
name
);
mysql_tquery(g_SQLHandle, query, "OnPlayerDataDelete", "s", name);
return 1;
}
forward OnPlayerDataDelete(const name[]);
public OnPlayerDataDelete(const name[])
{
// 如果影响的行数大于0
if(cache_affected_rows() > 0)
printf("[MySQL] 玩家 %s 的数据已删除", name);
return 1;
}
第十一章:查询多行数据
前面的 SELECT 只查了一个玩家(一行),有时候你需要查多行,比如「排行榜」。
场景:查询金币最多的前 5 名玩家。
SQL 语句解释
代码: SELECT `name`, `gold` FROM `players` ORDER BY `gold` DESC LIMIT 5
ORDER BY gold DESC = 按 gold 列降序排列(DESC = 从大到小,ASC = 从小到大)
LIMIT 5 = 只取前 5 行
在 Pawn 里写
代码: ShowTopPlayers(playerid)
{
mysql_tquery(g_SQLHandle,
"SELECT `name`, `gold` FROM `players` ORDER BY `gold` DESC LIMIT 5",
"OnTopPlayerLoaded",
"i", playerid
);
return 1;
}
forward OnTopPlayerLoaded(playerid);
public OnTopPlayerLoaded(playerid)
{
if(!IsPlayerConnected(playerid)) return 0;
new rows = cache_num_rows(); // 获取返回了几行
if(rows == 0)
{
SendClientMessage(playerid, -1, "[排行榜] 暂无数据");
return 1;
}
SendClientMessage(playerid, 0xFFDD44FF, "=== 金币排行榜 ===");
new name[MAX_PLAYER_NAME];
new gold;
// 遍历每一行结果
for(new i = 0; i < rows; i++)
{
cache_get_value_name(i, "name", name, sizeof(name));
cache_get_value_name_int(i, "gold", gold);
SendClientMessage(playerid, 0xFFFFFFFF, "#%d %s — %d 金币", i + 1, name, gold);
}
return 1;
}
第十二章:异步的安全防护措施
这一章讲的问题在实际中极罕见发生,但一旦发生就会造成数据写错玩家,值得了解并防范。
问题是什么?
异步查询的特点是:你发出查询,然后去做别的事,等数据库回来再处理结果。
这中间有一段时间差,通常只有1毫秒(千分之一秒)不到。正常情况下没有任何问题。但考虑这个极端场景:
代码: 1. 玩家 海绵宝宝 连入服务器(ID = 5)
2. 发出 SELECT 非阻塞查询,去数据库读 海绵宝宝 的数据
3. 查询还没回来(1毫秒的空档)
4. 玩家 海绵宝宝 突然断线了,此时ID 5 空出来了
5. 另一个玩家 派大星 立刻连入,空出来 ID 5 就分配给了派大星
6. 数据库查询回来了,把 海绵宝宝 的数据写进了 ID 5 的数组
7. 结果:派大星 拿到了 海绵宝宝 的数据
这个概率极其罕见——需要同一个玩家槽位在毫秒级内被复用,并且又刚好卡在发出查询 -> 查询结果返回来 的这个过程内
但对于认真的服务器来说,防一下是好习惯。
解决思路:给每个玩家槽位标一个「版本号」
每次玩家连入或断开,版本号 +1。
查询发出时记录当前版本号,查询回来时检查版本号是否还一致——不一致说明这个槽位已经换人了,直接丢弃结果。
代码: // 全局:每个槽位的版本号
new g_PlayerRace[MAX_PLAYERS];
public OnPlayerConnect(playerid)
{
g_PlayerRace[playerid]++; // 玩家连入,版本号 +1
LoadPlayerData(playerid);
return 1;
}
public OnPlayerDisconnect(playerid, reason)
{
SavePlayerData(playerid);
g_PlayerRace[playerid]++; // 玩家断开,版本号 +1
return 1;
}
发出查询时,把当前版本号一起传进回调参数:
代码: LoadPlayerData(playerid)
{
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
new query[128];
mysql_format(g_SQLHandle, query, sizeof(query),
"SELECT `gold`,`level`,`skin` FROM `players` WHERE `name`='%e' LIMIT 1",
name
);
// 把 race 一起传入回调,格式 "ii" = 两个整数
mysql_tquery(g_SQLHandle, query, "OnPlayerDataLoaded", "ii", playerid, g_PlayerRace[playerid]);
return 1;
}
回调里第一件事就是核对版本号:
代码: forward OnPlayerDataLoaded(playerid, race);
public OnPlayerDataLoaded(playerid, race)
{
// 核对版本号
// 不一致说明这个 ID 已经换了别的玩家,直接丢弃
if(race != g_PlayerRace[playerid]) return 0;
// 版本号一致,正常处理
if(cache_num_rows() == 0)
{
InsertPlayer(playerid);
return 1;
}
cache_get_value_name_int(0, "gold", g_Gold[playerid]);
cache_get_value_name_int(0, "level", g_Level[playerid]);
cache_get_value_name_int(0, "skin", g_Skin[playerid]);
SpawnPlayer(playerid);
return 1;
}
这几行代码做了什么
代码: 玩家 海绵宝宝 连入(ID=5) → g_PlayerRace[5] = 1,查询携带 race=1 发出
玩家 海绵宝宝 断线 → g_PlayerRace[5] = 2
玩家 派大星 连入(ID=5) → g_PlayerRace[5] = 3
查询回来,race 1 ≠ g_PlayerRace[5] 3 → 丢弃,派大星不受影响
养成习惯,所有玩家异步回调都加上这个检查
附录:MySQL R41-4 完整函数列表
本教程只覆盖了最核心的几个函数。R41-4 还提供了更多功能,全部函数的说明和用法可以在官方 Wiki 查阅:
https://github.com/pBlueG/SA-MP-MySQL/wiki
遇到具体需求时,去 Wiki 查一下,通常几分钟就能找到你需要的。
附录:MySQL 实用便利的查询语句示例
直接在数据库里做加减(不需要先读再写)
代码: UPDATE `players` SET `gold` = `gold` + 100 WHERE `name` = 'Tom'
玩家下线时保存数据,但不确定他的记录存不存在,与其先 SELECT 判断再决定 INSERT 还是 UPDATE,不如一句搞定
代码: INSERT INTO `players` (`name`, `gold`, `level`)
VALUES ('Tom', 100, 1)
ON DUPLICATE KEY UPDATE `gold` = VALUES(`gold`), `level` = VALUES(`level`)
LIMIT + OFFSET(分页)排行榜翻页时很有用,OFFSET 是跳过多少行,LIMIT 是取多少行:
代码: -- 第一页(第 1-10 名)
SELECT `name`, `gold` FROM `players` ORDER BY `gold` DESC LIMIT 10 OFFSET 0
-- 第二页(第 11-20 名)
SELECT `name`, `gold` FROM `players` ORDER BY `gold` DESC LIMIT 10 OFFSET 10
统计服务器总注册人数、某个帮派的成员数等:
代码: SELECT COUNT(*) AS `total` FROM `players`
SELECT COUNT(*) AS `total` FROM `players` WHERE `faction` = 3
SUM / MAX / MIN / AVG(计算相关语句)
代码: -- 所有玩家总金币
SELECT SUM(`gold`) AS `total_gold` FROM `players`
-- 最高等级
SELECT MAX(`level`) AS `max_level` FROM `players`
-- 平均金币
SELECT AVG(`gold`) AS `avg_gold` FROM `players`
模糊搜索,管理员搜索玩家名字时很实用
代码: SELECT `name`, `level` FROM `players` WHERE `name` LIKE '%Tom%'
-- % 是通配符,匹配任意字符
-- 'Tom%' = 以 Tom 开头
-- '%Tom' = 以 Tom 结尾
-- '%Tom%' = 包含 Tom
这里就不一一列举了,总之功能很强大很便利,这是文本保存带来不了的巨大优势
|
|
|
|
| 零基础开发openmp/SAMP服务器基础教程 - 第三章(pawn新手入门终篇) |
|
发布者: 小鸟unsigned - 03-20-2026, 05:12 PM - 板块: 教程
- 暂无回复
|
 |
零基础开发openmp/SAMP服务器基础教程 - 第三章(pawn新手入门终篇)
引用:本篇是新手教程第二章的基础巩固部分,覆盖新手教程第二章中未涉及或一笔带过的语言细节。
建议在读完新手教程第二章、能跑起来第一个脚本之后,再来阅读本篇。
老实说,Pawn 和 openmp/samp 能做的事,远比这新手教程三章展示的多得多。更复杂的数据结构、更多的回调和函数、插件生态、性能优化……这些东西没有讲,也不可能全部讲完。剩下的路,要靠你自己走
方法其实很简单:想做一个功能,就去查它需要什么函数;看到别人的代码,就去读懂它在做什么。 日积月累,你脑子里的「工具库」会越来越大,能做的事情也会越来越多。遇到看不懂的,查文档、搜索、问社区,这是每一个开发者每天都在做的事,你也不例外。
官方文档:https://open.mp/docs
社区交流群:673335567
后续仍然会更新更多教程,但不是pawn新手教程系列
目录
1. 关键字:变量声明修饰符
在 Pawn 里,声明变量和函数时可以在前面加上修饰关键字,控制它们的可见性和行为。
主教程只用了最基础的 new,这里把所有修饰符都介绍清楚:
| 关键字 |
用于 |
作用 |
new |
变量 |
声明一个普通变量(最常用) |
const |
变量 |
声明后不能被修改 |
static |
变量/函数 |
限制作用域 |
stock |
函数/变量 |
未使用时不报警告 |
public |
函数 |
可被服务端直接调用(回调) |
forward |
函数 |
前向声明,让编译器预先知道函数存在 |
native |
函数 |
声明由 C/C++ 实现的内置函数 |
2. const — 不可修改的变量
const 声明的变量在初始化后不能再被修改,编译器会在你试图修改时报错。
代码: const MAX_LEVEL = 50;
// MAX_LEVEL = 60; ← 编译报错!不能修改 const 变量
const 与 #define 的区别
代码: #define MAX_A 50 // 编译时文本替换,不占内存,不带类型
const MAX_B = 50; // 占内存,有类型,编译器会检查类型
在 Pawn 里,绝大多数情况下用 #define 更普遍,const 更常见于:
代码: // 函数参数中的 const 数组:告诉编译器这个数组在函数内部不会被修改
stock MyFunction(const string[])
{
print(string); // 只读,不修改
}
3. static — 静态变量
static 有两种用法:
用法一:局部 static 变量(函数内)
普通局部变量每次调用函数时都会重置为 0,而 static 局部变量只初始化一次,之后保留上次的值:
代码: // 普通局部变量:每次调用都从 0 开始
CountNormal()
{
new i = 0;
i++;
printf("普通:%d", i); // 永远输出 1
}
// static 局部变量:值会在调用之间保留
CountStatic()
{
static iCount = 0; // 只在第一次调用时初始化
iCount++;
printf("静态:%d", iCount); // 第1次=1, 第2次=2, 第3次=3...
}
用法二:全局 static 变量(文件私有)
在函数外部声明的 static 全局变量,只能在本文件内访问,其他文件无法访问,同时还可以避免相同名称的变量冲突:
代码: // 只有本 .pwn 文件才能访问这个变量
static g_Value;
引用:新手建议:入门阶段不用刻意区分,static 局部变量是最常用到的,记住「值会保留」这个特点就够了。
4. forward — 函数前向声明
Pawn 编译器从上到下读取代码。如果函数 A 调用了函数 B,但 B 写在 A 的后面,编译器就会报错说找不到 B。
forward 告诉编译器:「这个函数存在,后面会定义它」。
什么时候必须用 forward?
1. SetTimerEx 的目标函数(最常见)
代码: // 必须在使用前 forward 声明
forward MyTimerFunc(playerid);
// ... 某处调用 ...
SetTimerEx("MyTimerFunc", 3000, false, "i", playerid);
// 函数的实际执行逻辑
public MyTimerFunc(playerid)
{
SendClientMessage(playerid, -1, "3秒到了!");
return 1;
}
2. 相互调用的函数
代码: forward FuncB(); // 先声明 B
FuncA()
{
FuncB(); // 调用 B,编译器已经知道 B 存在了
}
FuncB()
{
// ...
}
引用:规则:只要是 public 函数且通过字符串名称调用的(比如 Timer),都必须 forward。
5. native — 原生函数声明
native 声明的函数是由服务端或插件用 C/C++ 实现的函数,不是在 Pawn 里定义的。
#include <open.mp> 已经帮你声明好了所有内置的 native 函数,所以你平时直接调用 SendClientMessage、CreateVehicle 这些,底层都是 native。
你通常不需要自己写 native 声明,除非你在使用额外的插件,或者想给自定义函数加上 Pawno 的自动提示:
代码: // 声明一个插件提供的函数(例如 sscanf 插件)
native sscanf(const str[], const format[], ...);
// 利用 native 重命名内置函数(高级用法)
native old_print = print;
// 现在 print 不可用,改为用 old_print 调用
引用:新手结论:不需要手写 native,知道它是「C/C++ 实现的函数」就够了。
6. enum — 枚举
枚举是定义一组有名字的整数常量的方式,让代码更有可读性。
基础用法:替代一组 #define
代码: // 不用枚举的写法(容易混乱)
#define CLASS_PEASANT 0
#define CLASS_KNIGHT 1
#define CLASS_MERCHANT 2
#define CLASS_FISHER 3
// 用枚举(更整洁)
enum E_CLASS
{
CLASS_PEASANT, // 自动赋值 0
CLASS_KNIGHT, // 自动赋值 1
CLASS_MERCHANT, // 自动赋值 2
CLASS_FISHER // 自动赋值 3
}
new g_PlayerClass[MAX_PLAYERS];
public OnPlayerSpawn(playerid)
{
g_PlayerClass[playerid] = CLASS_KNIGHT;
return 1;
}
高级用法:用枚举定义结构化数组(最强用法)
这是 SA-MP/open.mp 脚本中最常见的枚举用途,可以把多种数据整合在一个二维数组里:
代码: // 定义玩家数据的"字段"
enum E_PLAYER_DATA
{
PLAYER_GOLD, // 金币
PLAYER_LEVEL, // 等级
PLAYER_SKIN, // 皮肤 ID
PLAYER_KILLS, // 击杀数
PLAYER_DEATHS // 死亡数
}
// 声明二维数组,第二维用枚举大小
new g_PlayerData[MAX_PLAYERS][E_PLAYER_DATA];
// 使用:用字段名代替数字下标,可读性极强
public OnPlayerConnect(playerid)
{
g_PlayerData[playerid][PLAYER_GOLD] = 50;
g_PlayerData[playerid][PLAYER_LEVEL] = 1;
g_PlayerData[playerid][PLAYER_SKIN] = 86;
g_PlayerData[playerid][PLAYER_KILLS] = 0;
g_PlayerData[playerid][PLAYER_DEATHS] = 0;
return 1;
}
// 读取
public OnPlayerDeath(playerid, killerid, reason)
{
g_PlayerData[playerid][PLAYER_DEATHS]++;
if(killerid != INVALID_PLAYER_ID)
{
g_PlayerData[killerid][PLAYER_KILLS]++;
g_PlayerData[killerid][PLAYER_GOLD] += 10; // 击杀奖励 10 金
}
return 1;
}
引用:这种写法比单独维护 g_Gold[]、g_Level[]、g_Skin[] 多个数组更整洁,是进阶脚本的标准写法。
7. Tag(标签)— 类型系统
Pawn 是「弱类型」语言,所有变量本质上都是 32 位整数。
Tag(标签) 是 Pawn 对类型的模拟,让编译器在你混用不同「类型」时发出警告。
常见的内置 Tag
| Tag |
含义 |
示例 |
| (无 tag) |
普通整数 |
new iScore = 0; |
Float: |
浮点数 |
new Float:fX = 0.0; |
bool: |
布尔值 |
new bool:bAlive = true; |
Text: |
TextDraw ID |
new Text:tdLogo; |
PlayerText: |
玩家 TextDraw |
new PlayerText:tdHUD; |
Tag Mismatch 警告
代码: new Float:health;
new score;
score = health; // 警告:tag mismatch!把 Float 赋给整数
修复方法:用类型转换强制去除 tag:
代码: score = _:health; // _: 去除 tag,变成普通整数再赋值(不推荐随意用)
score = floatround(health); // 正确做法:用转换函数
bool 类型
代码: new bool:g_IsAdmin[MAX_PLAYERS];
public OnPlayerConnect(playerid)
{
g_IsAdmin[playerid] = false; // 用 true/false,不用 0/1
return 1;
}
// 判断
if(g_IsAdmin[playerid])
{
SendClientMessage(playerid, -1, "你是管理员!");
}
8. 编译指令 #define
#define 是预处理指令,在代码编译前进行纯文本替换。
基础用法:定义常量
代码: #define MAX_GOLD 999
#define SKIN_KNIGHT 287
带参数的宏(函数式宏)
代码: // 定义一个带参数的宏,%0 是第一个参数,%1 是第二个,以此类推
#define IsValidPlayer(%0) ((%0) >= 0 && (%0) < MAX_PLAYERS && IsPlayerConnected(%0))
// 使用:
if(IsValidPlayer(playerid))
{
SendClientMessage(playerid, -1, "有效的玩家");
}
// 实际展开为:
// if(((playerid) >= 0 && (playerid) < MAX_PLAYERS && IsPlayerConnected(playerid)))
主教程用过的按键宏
代码: // 这两个就是带参数的宏
#define PRESSED(%0) (((newkeys) & (%0)) && !((oldkeys) & (%0)))
#define RELEASED(%0) (!((newkeys) & (%0)) && ((oldkeys) & (%0)))
注意事项
代码: // 宏是纯文本替换,不是函数,没有类型检查,要小心
#define DOUBLE(%0) %0 * 2
new result = DOUBLE(3 + 4);
// 展开为:3 + 4 * 2 = 11,不是 14!
// 正确写法:
#define DOUBLE(%0) ((%0) * 2) // 用括号包裹,避免运算符优先级问题
9. 编译指令 #include
#include 把另一个文件的内容插入到当前文件,就像把那个文件的代码复制过来一样。
两种写法
代码: #include <open.mp> // 尖括号:在系统 include 目录(qawno/include/)查找
#include "myfile" // 引号:在当前目录查找
常见的 include 文件
代码: #include <open.mp> // open.mp 全量头文件(必须)
// 常用插件头文件
#include <sscanf2> // 字符串解析(指令参数处理)
#include <Pawn.CMD> // 指令系统插件
#include <streamer> // 动态对象流送插件
自定义 .inc 文件
当脚本变大时,可以把部分代码拆分到 .inc 文件里:
代码: // shop.inc 文件里写商店相关的函数
// 在主脚本里引入
#include "shop"
注意事项
如果文件格式是.inc可以直接写文件名,如果是其他格式比如 .pwn, .dat, txt 则需要写文件格式
比如: #include "myfile.pwn"
10. 编译指令 #if / #else / #endif
#if 是编译时的条件判断,决定某段代码要不要被编译进去。
注意:这和运行时的 if 不同,#if 在编译阶段就生效。
常见用法:区分 GameMode 和 FilterScript
代码: // 根据是否定义了 FILTERSCRIPT,决定编译哪个初始化函数
#if defined FILTERSCRIPT
public OnFilterScriptInit()
{
print("FilterScript 加载!");
return 1;
}
#else
public OnGameModeInit()
{
print("GameMode 加载!");
return 1;
}
#endif
用 #if 开关调试模式
代码: // 在脚本顶部定义,需要调试时就定义它,发布时删掉
#define DEBUG_MODE
// 在代码中用 #if 检查
#if defined DEBUG_MODE
print("[DEBUG] 服务器已以调试模式启动");
#endif
// 也可以在函数里用(但 #if 在函数内部也是编译时判断)
// 示例中如果没有定义 DEBUG_MODE,则 #if - #endif 内的代码等于不存在,不会有任何开销
public OnPlayerConnect(playerid)
{
#if defined DEBUG_MODE
new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
printf("[DEBUG] 玩家连入: %s (ID: %d)", name, playerid);
#endif
return 1;
}
defined 运算符
代码: // defined 检查某个符号是否被 #define 过
#if defined MY_CONSTANT
// MY_CONSTANT 存在时编译这段
#else
// 不存在时编译这段
#endif
11. 编译指令 #pragma
#pragma 给编译器传递特殊指令,控制编译行为。
最常用的几个
代码: // 关闭 tab 缩进警告
#pragma tabsize 0
// 关闭某个编号的警告
#pragma warning disable 200 // 关闭某个编号的警告
// 标记某个变量为「可能不会被用到,不要报警告」
new value;
#pragma unused value
12. 运算符补充:sizeof
sizeof 返回数组的元素数量(不是字节数),是 Pawn 特有的运算符。
代码: new arr[10];
printf("%d", sizeof(arr)); // 输出 10
new name[MAX_PLAYER_NAME];
printf("%d", sizeof(name)); // 输出 MAX_PLAYER_NAME 的值(24)
为什么要用 sizeof?
避免硬编码数组大小,让代码更安全:
代码: new message[128];
// 不好的写法:手动填 128,万一以后改了数组大小容易忘记改这里
format(message, 128, "你好,%s", name);
// 好的写法:用 sizeof 自动获取
format(message, sizeof(message), "你好,%s", name);
// 以后就算把 128 改成 256,这里也不需要动
在函数参数里传 sizeof
代码: GetPlayerName(playerid, name, sizeof(name));
// ↑ 告诉函数数组有多大,防止越界
13. 运算符补充:三目运算符 ?:
三目运算符是 if/else 的简写,适合简单的赋值判断:
基本语法
代码: // 格式:条件 ? 成立时的值 : 不成立时的值
new result = (mygold > 100) ? 1 : 0;
// 等同于:
// if(mygold > 100) result = 1;
// else result = 0;
实际应用
代码: // 根据玩家是否在线,获取名字
new string[6];
format(string, sizeof(string), "%s", IsPlayerConnected(targetid) ? "在线" : "离线");
// 根据职业决定皮肤
SetPlayerSkin(playerid, ((g_PlayerClass[playerid] == CLASS_KNIGHT) ? 287 : 86));
// 嵌套三目
new levelName[16];
format(levelName, sizeof(levelName), "%s",
level >= 10 ? "高级" :
level >= 5 ? "中级" : "新手"
);
14. 运算符补充:位运算
位运算直接操作数字的二进制位,在 SA-MP/open.mp 中主要用于按键检测和标志位。
基础位运算符
| 运算符 |
名称 |
说明 |
& |
按位与 |
两位都是 1 才得 1 |
\| |
按位或 |
任意一位是 1 就得 1 |
^ |
按位异或 |
两位不同才得 1 |
~ |
按位非 |
取反 |
<< |
左移 |
相当于乘以 2 的 n 次方 |
>> |
右移 |
相当于除以 2 的 n 次方 |
在按键检测中的应用
按键状态是一个整数,每一个「二进制位」代表一个按键是否被按下:
代码: public OnPlayerKeyStateChange(playerid, KEY:newkeys, KEY:oldkeys)
{
// & 运算:检查某个按键位是否为 1(按下状态)
if(newkeys & KEY_YES)
{
// KEY_YES 位是 1,说明 Y 键被按着
}
// PRESSED 宏就是用位运算实现的:
// PRESSED(KEY_YES) 展开为:
// ((newkeys & KEY_YES) && !(oldkeys & KEY_YES))
// 意思是:现在按着 AND 之前没按 = 刚刚按下
return 1;
}
用位运算存储多个布尔标志
代码: // 用一个整数存储多个开关状态(节省内存)
#define FLAG_ADMIN (1 << 0) // 二进制 0001
#define FLAG_VIP (1 << 1) // 二进制 0010
#define FLAG_MUTED (1 << 2) // 二进制 0100
#define FLAG_JAILED (1 << 3) // 二进制 1000
new g_Flags[MAX_PLAYERS];
// 设置标志(用 | 打开某位)
g_Flags[playerid] |= FLAG_ADMIN;
// 清除标志(用 & 和 ~ 关闭某位)
g_Flags[playerid] &= ~FLAG_MUTED;
// 检查标志(用 & 检测某位)
if(g_Flags[playerid] & FLAG_ADMIN)
{
SendClientMessage(playerid, -1, "你是管理员");
}
15. 控制结构补充:do-while
do-while 是 while 的变体,区别是先执行一次,再判断条件。
即使条件一开始就是假的,循环体至少也会执行一次。
代码: // while:先判断,可能一次都不执行
while(false)
{
print("永远不会执行");
}
// do-while:先执行,再判断,至少执行一次
do
{
print("至少执行一次!");
}
while(false); // 注意:while 后面要加分号
实际应用
代码: // 寻找一个空的数据槽位(至少要进去看一次)
new slot = 0;
do
{
if(g_SlotFree[slot])
{
break; // 找到了,跳出
}
slot++;
}
while(slot < MAX_SLOTS);
引用:新手提示:do-while 在脚本里不太常用,了解即可。
16. 控制结构补充:break 与 continue
这两个关键字用于控制循环的流程。
break — 立即跳出循环
代码: // 找到第一个死亡的玩家后停止循环
for(new i = 0; i < MAX_PLAYERS; i++)
{
if(!IsPlayerConnected(i)) continue; // 跳过未连接的(见下)
new Float:health;
GetPlayerHealth(i, health);
if(health <= 0.0)
{
printf("玩家 %d 已死亡", i);
break; // 找到了就不用继续循环了
}
}
continue — 跳过本次,进入下一轮
代码: // 给所有在线玩家发消息,跳过管理员
for(new i = 0; i < MAX_PLAYERS; i++)
{
if(!IsPlayerConnected(i)) continue; // 没连接,跳过
if(g_Flags[i] & FLAG_ADMIN) continue; // 是管理员,跳过
SendClientMessage(i, -1, "非管理员消息");
}
在 switch 中不需要 break
引用:Pawn 的 switch 每个 case 是独立的,不会穿透到下一个 case,这与 C 语言不同,不需要写 break。
代码: // Pawn:不会穿透,不需要 break
switch(value)
{
case 1: print("value 是 1"); // 执行后直接跳出 switch
case 2: print("value 是 2");
case 3: print("value 是 3");
}
17. switch 进阶:范围与列表匹配
Pawn 的 switch 比 C 语言更强大,支持范围和列表匹配。
列表匹配(多个值合并成一个 case)
代码: new level = 3;
switch(level)
{
case 1, 2, 3: // 匹配 1 或 2 或 3
{
SendClientMessage(playerid, -1, "初级段位");
}
case 4, 5, 6: // 匹配 4 或 5 或 6
{
SendClientMessage(playerid, -1, "中级段位");
}
case 7, 8, 9, 10:
{
SendClientMessage(playerid, -1, "高级段位");
}
}
范围匹配(连续区间)
代码: switch(level)
{
case 1 .. 3: // 匹配 1 到 3(含两端)
{
SendClientMessage(playerid, -1, "初级段位");
}
case 4 .. 6: // 匹配 4 到 6
{
SendClientMessage(playerid, -1, "中级段位");
}
case 7 .. 10:
{
SendClientMessage(playerid, -1, "高级段位");
}
default:
{
SendClientMessage(playerid, -1, "段位超出范围");
}
}
18. 字符串函数
Pawn 内置了很多处理字符串的函数,这里列出脚本开发中最常用的几个。
strlen — 获取字符串长度
代码: new name[MAX_PLAYER_NAME];
GetPlayerName(playerid, name, sizeof(name));
new len = strlen(name);
printf("名字长度:%d", len);
// 常用:检查输入是否为空
if(strlen(inputtext) == 0)
{
SendClientMessage(playerid, -1, "输入不能为空!");
return 1;
}
strcmp — 比较两个字符串
代码: // strcmp(str1, str2, ignorecase, length)
// 返回 0 = 相同,非 0 = 不同
new input[32] = "hello";
if(strcmp(input, "hello", true) == 0) // true = 忽略大小写
{
print("字符串匹配!");
}
// 常用简写(检查是否不同)
if(strcmp(input, "admin"))
{
// 不相同时执行(非 0 = true)
}
strcat — 拼接字符串
代码: new result[24] = "Hello, ";
strcat(result, "World!");
// result 现在是 "Hello, World!"
strfind — 查找子字符串
代码: new string[] = "Welcome to the server!";
new pos = strfind(string, "server", true); // 返回位置下标,找不到返回 -1
if(pos != -1)
{
printf("找到了 server,位置:%d", pos); // 输出 15
}
字符串与数字互转
代码: // 字符串 → 整数
new string[] = "123";
new number = strval(string); // number = 123
// 整数 → 字符串(用 format)
new output[16];
format(output, sizeof(output), "%d", number);
19. 玩家指令(Commands)
玩家在聊天框输入 /指令名 时触发 OnPlayerCommandText 回调。
方法一:直接用 strcmp(基础做法)
代码: new g_Gold[MAX_PLAYERS] = {100, ...};
public OnPlayerCommandText(playerid, cmdtext[])
{
// cmdtext 是玩家输入的完整内容,例如 "/heal"
if(strcmp(cmdtext, "/heal", true) == 0)
{
SetPlayerHealth(playerid, 100.0);
SendClientMessage(playerid, -1, "血量已恢复!");
return 1; // ← 一定要 return 1,表示指令已处理
}
if(strcmp(cmdtext, "/gold", true) == 0)
{
SendClientMessage(playerid, -1, "你现在有 %d 枚金币。", g_Gold[playerid]);
return 1;
}
// 没有匹配的指令:return 0 让服务器显示"未知指令"提示
return 0;
}
方法二:用 Pawn.CMD 插件(推荐做法)
Pawn.CMD 是一个流行的指令处理插件,语法更简洁,性能更好:
自行下载安装: https://github.com/katursis/Pawn.CMD
代码: #include <Pawn.CMD> // 需要安装 Pawn.CMD 插件
// Pawn.CMD 的写法:CMD:指令名(playerid, params[])
CMD:heal(playerid, params[])
{
SetPlayerHealth(playerid, 100.0);
SendClientMessage(playerid, -1, "血量已恢复!");
return 1;
}
CMD:gold(playerid, params[])
{
SendClientMessage(playerid, -1, "你现在有 %d 枚金币。", g_Gold[playerid]);
return 1;
}
指令权限检查
代码: CMD:kick(playerid, params[])
{
// 只有管理员才能用
if(!(g_Flags[playerid] & FLAG_ADMIN))
{
SendClientMessage(playerid, -1, "你没有权限使用此指令!");
return 1;
}
// ... 踢人逻辑 ...
return 1;
}
20. sscanf — 解析指令参数
当指令带参数时(例如 /kick 5 或 /give 3 100),需要从字符串里把参数提取出来。
sscanf 插件是处理这类问题的标准工具。
自行下载安装: https://github.com/Y-Less/sscanf
基础用法
代码: // sscanf(输入字符串, 格式, 变量1, 变量2, ...)
// 返回 0 = 解析成功,非 0 = 解析失败(参数不够或格式不对)
CMD:give(playerid, params[])
{
new targetID, amount;
// 格式 "ii" = 两个整数
if(sscanf(params, "ii", targetID, amount))
{
SendClientMessage(playerid, -1, "用法:/give [玩家ID] [金币数]");
return 1;
}
if(!IsPlayerConnected(targetID))
{
SendClientMessage(playerid, -1, "该玩家不在线!");
return 1;
}
g_Gold[targetID] += amount;
SendClientMessage(playerid, -1, "向玩家 %d 赠送了 %d 枚金币。", targetID, amount);
SendClientMessage(targetID, -1, "管理员赠送了 %d 枚金币给你。", amount);
return 1;
}
CMD:skin(playerid, params[])
{
new skinID;
if(sscanf(params, "i", skinID))
{
SendClientMessage(playerid, -1, "用法:/skin [皮肤ID]");
return 1;
}
SetPlayerSkin(playerid, skinID);
g_Skin[playerid] = skinID;
SendClientMessage(playerid, -1, "皮肤已更换为 %d。", skinID);
return 1;
}
sscanf 格式说明符
| 格式符 |
含义 |
i |
整数 |
f |
浮点数 |
s |
字符串(到空格结束) |
s[32] |
最大 32 字符的字符串 |
u |
玩家名字或 ID(自动解析) |
p<分隔符> |
设置分隔符,如 p, 用逗号分隔 |
sscanf 相关文档说明
如何使用: https://github.com/Y-Less/sscanf?tab=readme-ov-file#use
格式说明符: https://github.com/Y-Less/sscanf?tab=rea...specifiers
21. printf 与 print — 控制台输出
这两个函数把内容输出到服务器控制台窗口,玩家看不到,只能在服务器后台终端看到。
常用于调试和记录日志。
print — 输出纯文字
代码: print("服务器启动了!");
print("[DEBUG] 这是一条调试信息");
printf — 带格式化输出
代码: new playerCounts = 0;
for(new i = 0; i < MAX_PLAYERS; i++)
if(IsPlayerConnected(i)) playerCounts++;
printf("[服务器] 当前在线玩家:%d", playerCounts);
printf("[DEBUG] 玩家 %d 的金币:%d", playerid, g_Gold[playerid]);
格式符与 format 相同
代码: printf("整数:%d,浮点:%.2f,字符串:%s", 42, 3.14, "hello");
// 输出:整数:42,浮点:3.14,字符串:hello
22. 代码组织:#include 拆分文件
当你的脚本越来越大,所有代码都写在一个 .pwn 文件里会很难维护。
按功能模块拆分成多个 .inc 文件,再在主文件里引入。
比如
代码: gamemodes/
├── main.pwn ← 主文件(只负责引入和框架)
└── include/
├── shop.inc ← 商店系统
├── npc.inc ← NPC 系统
├── vehicle.inc ← 载具系统
└── commands.inc ← 所有指令
主文件写法
代码: // mian.pwn
#include <open.mp>
// 引入各功能模块
#include "include/shop.inc"
#include "include/npc.inc"
#include "include/vehicle.inc"
#include "include/commands.inc"
// 主文件只写框架回调
public OnGameModeInit()
{
InitShop(); // 调用 shop.inc 里的初始化函数
InitNPCs(); // 调用 npc.inc 里的初始化函数
InitVehicles(); // 调用 vehicle.inc 里的初始化函数
return 1;
}
shop.inc 示例
代码: // 注意:这里不需要再 #include <open.mp>,主文件已经引入了
// 商店相关的变量、函数、指令都写在这里
InitShop()
{
// 创建商店相关的 Pickup 等
return 1;
}
进阶:ALS Hook 系统(让 .inc 自己挂载回调)
Pawn 不允许同一个回调定义两次,也就是说OnGameModeInit OnPlayerDeath 等等这些回调只能有一个
当模块越来越多,每次写新模块都要回去主文件里的各个回调中加入新模块的功能,容易忘记、切不容易维护管理。
让每个 .inc 文件自己管理自己需要使用的回调,执行完自己的逻辑后再把控制权传给下一个模块,形成一条链。
原理
- 我先把
OnGameModeInit 重命名成 shop_OnGameModeInit(用 #define 宏替换)
- 然后我自己写一个新的
OnGameModeInit,在里面先执行我的逻辑,再调用 shop_OnGameModeInit
- 下一个
.inc(比如 npc.inc)接着同样操作,把我写的 OnGameModeInit 再重命名为 npc_OnGameModeInit,再写新的 OnGameModeInit...
- 最终形成一条调用链:
OnGameModeInit → npc_OnGameModeInit → shop_OnGameModeInit → 主文件的 return 1
代码: // shop.inc 文件内部
new g_ShopPickup;
InitShop()
{
g_ShopPickup = CreatePickup(1242, 2, 100.0, 200.0, 10.0, -1);
return 1;
}
public OnGameModeInit()
{
InitShop();
#if defined shop_OnGameModeInit
return shop_OnGameModeInit();
#else
return 1;
#endif
}
#if defined _ALS_OnGameModeInit
#undef OnGameModeInit
#else
#define _ALS_OnGameModeInit
#endif
#define OnGameModeInit shop_OnGameModeInit
#if defined shop_OnGameModeInit
forward shop_OnGameModeInit();
#endif
有了 ALS 之后,主文件什么都不用写
只需要#include进去,干净利落
代码: #include <open.mp>
#include "include/shop.inc"
#include "include/npc.inc"
#include "include/vehicle.inc"
public OnGameModeInit()
{
return 1;
}
|
|
|
|
|