经过第3章的学习,我们学会了amxx插件开发的基本知识,那么我们这章节就来聊聊amxx插件开发的一些进阶知识。

Tasks/Timers

Tasks,即定时器,能够让你在指定时间内执行你的回调函数,可以是一次性的,也可以是多次重复或无限循环的。它可以用来做一些有用的事情,例如,等待几秒后,使一些游戏对象销毁,又例如,循环执行一个任务。

task的运行方式是可以通过参数配置的,使用的函数是set_task():

1
set_task(Float:time,const function[],id = 0,parameter[]="",len = 0,flags[]="", repeat = 0)

参数的含义如下:

  • Float:time - 时间间隔(秒),最小0.1秒
  • function[] - public声明的回调函数名称,string字符串类型
  • id - 任务ID号,需要自己指定
  • parameter - 数字数组类型,传递给task的参数数组
  • len - 参数数组的长度
  • flags - “a”表示循环指定次数;”b”表示无限循环;”c”表示地图更换倒计时后执行;”d”表示地图更换倒计时前执行
  • repeat - 如果flags是”a”,那么repeat的值就是循环次数

这里有个task的例子,功能是让指定的玩家失血5次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//the timed function receives the parameter array and its task id
public slapTask(params[], id)
{
new player = params[0]
user_slap(player, 5)
}

public start_slapping(id)
{
new params[1]
params[0] = id
//we don't need a specific id
set_task(1.0, "slapTask", 0, params, 1, "a", 5)
}

需要注意的是,如果set_task中的len参数为0时,那么回调函数不能像上边的例子那样有两个参数,而应该像下边的一样,只有任务id号作为参数

1
public slapTask(id)

Menus

菜单是提供玩家选择的HUD信息。菜单代码的编写比较复杂,但是在cs1.6中是非常有用的,例如投票、管理员菜单等。

菜单Menus必须通过2个要素来注册。一个是keys,来告诉AMXX要注册多少个菜单选项。另一个是一个字符串,来标识唯一的菜单id号,这个此字符串将显示在要触发函数的每个菜单的开头位置。当你要显示一个菜单时,你可以只对某个玩家显示,也可以对多个玩家显示。当玩家通过按键选择对应的菜单选项时,选择的按键信息将会传递给注册菜单时的回调函数。

在下面的例子中,我们将会做一个菜单,选项有AK47、M4A1和AWP,只对某一个玩家进行显示,并且识别它选择了哪个选项。

1
2
3
4
5
6
7
8
9
#include <amxmodx>
#include <amxmisc>
#include <fun>
public plugin_init()
{
register_plugin("Menu Demo", "1.0", "BAILOPAN")
new keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2
register_menucmd(register_menuid("Which Weapon?"), keys, "giveWeapon")
}

我们可以看到,这里有两个函数,register_menucmd和register_menuid。其中,register_menuid将会通过字符串注册一个menu的标题,并返回它的id号,这个id号用于register_menucmd的第一个参数。register_menucmd的第二个参数是keys玩家按键配置,这里我们的菜单有3个选项,所以我们通过或运算来组合各个按键,这里的例子实际的值是7。最后一个参数是一个public的函数,用作按键回调函数使用。

接下来,我们如何显示菜单呢?我们这里写一个客户端控制台命令giveme,来触发菜单显示。

1
2
3
4
5
6
7
public plugin_init()
{
register_plugin("Menu Demo", "1.0", "BAILOPAN")
new keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2
register_menucmd(register_menuid("Which Weapon?"), keys, "giveWeapon")
register_clcmd("giveme", "showWeaponMenu")
}

我们这里使用的register_clcmd和register_concmd类似,前者只需要接受2个参数,用来注册任何一个客户端命令(除了一些特殊的命令,如+attack)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//The clcmd function will just give us the player id
public showWeaponMenu(id)
{
new menu[192]
new keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2

format(menu, 191, "Which Weapon?^n^n1. AK47^n2. M4A1^n3. AWP")
show_menu(id, keys, menu)
return PLUGIN_HANDLED
}

//Our menu function will get the player id and the key they pressed
public giveWeapon(id, key)
{
//key will start at zero
if (key == 0)
{
give_item(id, "weapon_ak47")
} else if (key == 1) {
give_item(id, "weapon_m4a1")
} else if (key == 2) {
give_item(id, "weapon_awp")
}
}

至此,我们的菜单就可以编译和使用了。这里的format,用来格式化菜单的显示。^n表示换行,另外,想cs1.6使用了VGUI2的,还可以使用\w使字体颜色为白色,使用\r使字体颜色为红色,使用\y字体颜色为黄色。当玩家在控制台输入giveme命令时,就会弹出菜单。当玩家按数字键0/1/2选择菜单选项时,就会触发回调函数giveWeapon,将会得到响应的武器。

Events/Messages

我们这里将通过一个示例,来扩展我们上边的menu菜单。每当玩家重生时,我们就显示武器选择菜单,让玩家选择武器。

我们这里只是示例,并没有考虑阻断玩家购买武器和移除购买区域这些功能)

Messages消息是Half-Life客户端与服务器通信的一种方式。它是一系列经过格式化的参数列表。例如,DeathMsg消息(消息id为83),有3个参数:Attacker(byte)、Victim(byte)和Weapon(string)。你可以接收这些消息,也可以发送这些消息,我们下边的示例将会实现接收消息和发送消息两个功能。

首先,我们先实现玩家重生后显示武器菜单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public plugin_init()
{
register_plugin("Menu Demo", "1.0", "BAILOPAN")
new keys = MENU_KEY_0|MENU_KEY_1|MENU_KEY_2
register_menucmd(register_menuid("Which Weapon?"), keys, "giveWeapon")
//flags - b means "sent to one target", e means "target is alive"
//this event is sent when a player spawns
register_event("ResetHUD", "hook_hud", "be")
}

public hook_hud(id)
{
//since we specify no parameters to the task,
//the task id will be given to the function
//this is useful because we can reuse our old
//show menu function which takes an id
set_task(0.2, "showWeaponMenu", id)
}

注意了,我们这里使用task设置了一个延时,当我们接收到message消息的时候。这样,我们可以确保玩家有足够的时间完成重生的动作。

register_event能够携带多个参数,来帮助我们限定所获取的消息,确保是我们所需要的,例如,匹配明确的参数。

那接下来,如果我们需要hook玩家的死亡事件,应该如何做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public plugin_init()
{
register_plugin("Message Demo", "1.0", "BAILOPAN")
//this message informs everyone of a death, so we use
// flag "a" - global event
register_event("DeathMsg", "hook_death", "a")
}

public hook_death()
{
new Killer = read_data(1) //get the first message parameter
new Victim = read_data(2) //get the second message parameter
new headshot = read_data(3) //was this a headshot?
new weapon[32]
read_data(4, weapon, 31) //get the weapon name
}

又或者,我们应该如何像玩家发送死亡消息呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
stock make_deathMsg(Killer, Victim, const weapon[])
{
//message_begin starts a message. NEVER start two messages at once.
//MSG_ALL means send the message to everyone
//get_user_msgid returns the id of a message name
//{0,0,0} is the origin vector - not used here
//0 is the target - no specific target here
message_begin(MSG_ALL, get_user_msgid("DeathMsg"), {0,0,0}, 0)
write_byte(Killer)
write_byte(Victim)
write_string(weapon)
message_end()
}

更多有关消息的内容,你可以查看register_message函数,也可以深入学习半条命引擎源码、MetaMod源码和AMXX源码,以及AMXX modules和AMXX scripts源码。

Catching Log Messages

Catching log message,即拦截系统的LOG信息,并触发回调函数,目前已经很少使用了,但也有些特殊的消息,通过LOG信息拦截是非常方便的,如回合重新开始的LOG消息。LOG信息是由HL-MOD来记录的,我们的AMXX能够截获它,并且能够触发回调函数。例如,在回合开始时,给每个玩家16000初始金钱。

LOG信息记录的rounds回合消息是这样的,World triggered "Round_Start"。AMXX会认为World_triggered是第一个参数,Round_Start是第二个参数。所以,拦截回合开始消息的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <amxmodx>

public plugin_init()
{
register_plugin("Log Demo", "1.0", "BAILOPAN")
//this will filter for two parameters
//roundstart is the public function
register_logevent("roundstart", 2, "0=World triggered", "1=Round_Start")
}

public roundstart()
{
//set a small delay to make sure everyone spawns
set_task(1.0, "roundDelay")
}

public roundDelay(taskId)
{
new players[32], num
get_players(players, num)
new i
for (i=0; i<num; i++)
{
cs_set_user_money(players[i], 16000)
}
}

除此之外,你还可以使用read_logdata()读取指定的LOG信息的参数。但注意的是read_logdata()函数只能在forward函数plugin_log()中被调用。

1
2
3
4
5
6
//receives all log messages
public plugin_log()
{
new data[32]
read_logdata(data, 31)
}

Multi-Lingual Support

多语言支持,相对上边的代码是比较复杂的,但是如果你的插件想要推广国际化实用的话,确实比较实用的。

首先,第一步我们先明确哪些内容需要实现国际化翻译。例如:

1
2
new score = get_score()
client_print(id, print_chat, "[AMXX] Your score is %d", score)

然后,创建一个.txt文件(最好是以你的插件名称来命名,我们这里命名为myplugin.txt),并保存到addons\amxmodx\data\lang\目录下。对于每种语言,你需要添加入口到myplugin.txt文件中,并为每个需要国际化翻译的字符串设置key和value:

1
2
3
4
5
6
7
8
9
10
11
12
13
// addons\amxmodx\data\lang\myplugin.txt

[en]
SCORE_MSG = "Your score is %d"

[de]
SCORE_MSG = "Ihr Spielergebnis ist %d"

[es]
SCORE_MSG = "Su cuenta es %d"

[fr]
SCORE_MSG = "Votre score est %d"

接着,在plugin_init()中注册我们的多语言国际化字典myplugin.txt。

1
2
3
4
5
6
public plugin_init()
{
...
//assumes placed in amxmodx\data\lang
register_dictionary("myplugin.txt")
}

现在,我们已经做好准备工作了,那么在插件代码中,如何使用我们的多语言国际化字典呢?这里我们就需要使用format代码风格了,因为AMXX的多语言国际化是编译到format代码风格里边的,所以我们需要使用它。

1
client_print(id, print_chat, "[AMXX] %L", modifier, "SCORE_MSG", get_score())

当出现一个%L时,我们至少要使用2个参数:

  • 第一个参数modifier必须是玩家id、LANG_SERVER或者LANG_PLAYER其中的一个,如果是玩家id,将会根据当前玩家id的语言来决定;如果是LANG_SERVER,则使用服务器AMXX配置的语言环境;最后如果是LANG_PLAYER,那么则会根据每个玩家不同的语言环境来分别的发送不同语言的文本消息,这个参数通常在向全部玩家发送消息时使用。
  • 第二个参数就是"SCORE_MSG"使我们在myplugin.txt中定义的key。
  • 最后的参数是多变参数,根据myplugin.txt中定义的value来决定,如%d、%s等。

下边是一个向全部玩家发送HELLO消息的例子,其中HELLO是在myplugin.txt中定义的key:

1
client_print(0, print_chat, "[AMXX] %L", LANG_PLAYER, "HELLO")

SQL Support

通过sql模块,我们可以通过sql或dbi来使用mysql数据库。我们这里的sql模块是单实例的,也就说当我们的sql模块被加载时,就会创建一个全局唯一的sql实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//Create a connection
new Sql:mysql = dbi_connect("localhost", "dvander", "pass", "dbase")

//If the connection is less than 1, it is bad
if (mysql < SQL_OK) {
new err[255]
new errNum = dbi_error(mysql, err, 254)
server_print("error1: %s|%d", err, errNum)
return 1
}

server_print("Connection handle: %d", mysql)
//Run a query
new Result:ret = dbi_query(mysql, "INSERT INTO config (keyname, val) VALUES ('amx', 'yes')")

//If the query is less than 0, it failed
if (ret < RESULT_NONE) {
new err[255]
new errNum = dbi_error(mysql, err, 254)
server_print("error2: %s|%d", err, errNum)
return 1
}

//Do a select query
new Result:res = dbi_query(mysql, "SELECT * FROM config")

//If the query is greater than 0, you got a handle to the result set
if (res <= RESULT_NONE) {
new err[255]
new errNum = dbi_error(mysql, err, 254)
server_print("error3: %s|%d", err, errNum)
return 1
}

server_print("Result handle: %d", res)

//Loop through the result set
while (res && dbi_nextrow(res)>0) {
new qry[32]
//Get the column/field called "keyname" from the result set
dbi_result(res, "keyname", qry, 32)
server_print("result: %s", qry)
}

//Free the result set
dbi_free_result(res)

//Close the connection
ret = dbi_close(mysql)
if (ret <= RESULT_NONE) {
new err[255]
new errNum = dbi_error(mysql, err, 254)
server_print("error4: %s|%d", err, errNum)
return 1
}

Regular Expressions

正则表达式,AMXX使用Perl兼容的正则表达式,AMXX通过regex_amxx模块来提供正则表达式的native函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <amxmodx>
#include <regex>

public plugin_init()
{
register_plugin("Regex", "1.0", "BAILOPAN")
register_srvcmd("amx_regex", "cmdtest")
}

public cmdtest()
{
new str[] = "It's Walky!"
//this pattern will match any string which contains
// two groupings of characters separated by a space
// the two groupings are substrings 1 and 2
new pattern[] = "(.+) (.+)"

new num, error[128]
//str = string
//pattern = pattern to use
//num = special return case code
//error = if there's an error, it will go here
//127 - error's max length
new Regex:re = regex_match(str, pattern, num, error, 127)

server_print("Result=%d, num=%d, error=%s", re, num, error)

//REGEX_OK means there was a match
if (re >= REGEX_OK)
{
new str2[64]
new i
//since it returned REGEX_OK, num has
// the number of substrings matched by the pattern.
//the first substring (0) seems to match the whole string.
for (i=0; i<num; i++)
{
regex_substr(re, i, str2, 63)
server_print("Substring %d: %s", i, str2)
}
//the regular expression matcher uses memory.
//you must free it if you get REGEX_OK
//This will also set re to 0.
regex_free(re)
}

//note the invalid regular expression pattern
//this will return REGEX_PATTERN_FAIL, -1
re = regex_match("Bruno the Bandit", ".+(]", num, error, 127)

server_print("Result=%d, num=%d, error=%s", re, num, error)
}

当你编译上边的插件在服务器运行后,在服务器控制台输入amx_reges,那么你就会在服务器控制台看到如下输入内容:

1
2
3
4
5
Result=1, num=3, error=
Substring 0: It's Walky!
Substring 1: It's
Substring 2: Walky!
Result=-1, num=4, error=missing )

要注意了,regex_match()的第三个参数num,根据不同的返回值,它可以是子字符串的个数,也可以是match error错误码,也可以是pattern error位置索引。

Entities

Entities实体,是HL半条命引擎中,最基础的动态数据结构。Players玩家、Weapons武器、Grenades手雷,以及其他周边的小物体,都是Entities实体。它们都有一个全局唯一的实体id,通过这个实体id,我们可以找到对应的实体并修改它的属性值。

当然,在不知道实体id的情况下,我们通过搜索实体的class/owner也可以找到对应的实体,例如搜索player实体。

在下边的例子中,我们将会通过实体来创建一个假的玩家,并给他配置武器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  //create a basic entity
entid = create_entity("info_target")
//set its classname
entity_set_string(entid, EV_SZ_classname, "some_guy")
//set its model
entity_set_model(entid, "models/w_suit.mdl")
new Float:Vec[3] = {0.0,0.0,0.0}
//set its origin
entity_set_origin(entid, Vec)
//set some basic properties
entity_set_int(entid, EV_INT_solid, 1)
entity_set_int(entid, EV_INT_movetype, 6)
//create the weapon - thanks to pimp_daddy!
entWeapon = create_entity("info_target")
entity_set_string(entWeapon, EV_SZ_classname, weapString)
//set to follow
entity_set_int(entWeapon, EV_INT_movetype, MOVETYPE_FOLLOW)
entity_set_int(entWeapon, EV_INT_solid, SOLID_NOT)
entity_set_edict(entWeapon, EV_ENT_aiment, entid)
entity_set_model(entWeapon, "models/p_m4a1.mdl")

当然,你也可以修改其他一些基础的要素,例如:

1
2
3
4
5
//how set_user_armor() works in the fun module
stock set_armor(player, Float:value)
{
entity_set_int(player, EV_FL_armorvalue, value)
}

FakeMeta

FakeMeta可以说是新一代的脚本编写规范。它可以让你在small脚本中使用metamod的功能。

  • Engine/DLL Calls:在HL引擎的命名空间中,有两种类型的函数,Engine 函数和DLL函数(DLL函数是HL引擎的game/mod的导出函数),两种函数可以在FakeMeta模块中通过engfunc()和dllfunc()这两个natives函数来调用。其参数将会被直接传入MetaMod中(所以要注意了,如果参数有问题,将可能会导致MetaMod崩溃,从而导致整个服务器崩溃)。
  • Engine/DLL Hooks:除了上边的native calls之外,我们还可以通过register_forward来拦截/重写forward函数的代码逻辑。你可以使用fm_return()和return PLUGIN_HANDLED来阻断后续代码执行。
  • 简易的实体修改:FakeMeta提供pev()和set_pev()两个native函数来替换HL引擎的entity__()函数,更简单地对实体的属性和行为进行修改。
  • Private Offset Hacking:Private offsets即实体私有属性偏移表,在HL引擎中为pvPrivateData。在使用时,需要很注意,不同的游戏它的pvPrivateData中的偏移值代表的含义是不同的,例如在cstrike中表示金钱,而在Natural-Selection中则表示资源,如果它的值设置错误了,很有可能会导致游戏崩溃。

参考文献

AMX Mod X Documentation v1.6