DOTA 互通 作弊 原理

Return Bug+GameCache的应用

一、排除泄露

在讲解完Return Bug的原理以及跟GameCache的简单应用后,我并没有进一步讲解更复杂的数据绑定,而是穿插讲了一篇中级教程《JASS动态注册事件》,因为在用Return Bug+GameCache写一些复杂的系统时,经常会用到动态注册事件。

但在讲解动态注册事件时,一个很重要的内容没有被提到,那就是排除在销毁触发器时由触发器条件和动作引起的泄露。不过在排泄时必须用到Return Bug+GameCache,因此我把排泄留到这章讲,当作Return Bug+GameCache的一个应用实例。

  好吧,让我们先回顾下动态注册事件,依旧用Tiveone的经典教程

 

jass: Copy code
//当触发注册的事件时调用的函数
function OnUnitAmort takes nothing returns nothing
//设置触发单位为无敌
call SetUnitInvulnerable( GetTriggerUnit(), true )
//破坏这个触发器,即下面的RegisterUnitAmortEvent所创建的触发器
//这样就不会导致该触发被多次调用,并且还能清理该触发所占用的内存
call DestroyTrigger(GetTriggeringTrigger())
endfunction

//注册模板
function RegisterUnitAmortEvent takes unit witchUnit returns nothing
//创建一个触发器
local trigger rTrg = CreateTrigger()
//为创建的触发器注册事件:当单位witchUnit的生命值小于或等于50的时触发事件
call TriggerRegisterUnitLifeEvent( rTrg , witchUnit, LESS_THAN_OR_EQUAL, 50 )
//注册触发器事件响应后所调用的函数
call TriggerAddAction( rTrg, function OnUnitAmort )
endfunction

这样的写法不是很好,它会造成内存泄露。我们来看下TriggerAddAction这个函数
native TriggerAddAction takes trigger whichTrigger, code actionFunc returns triggeraction

这里先生成了一个triggeraction,然后把它添加给了触发器;而当我们删除触发器的时候,这个triggeraction却并没有被删除。

那么,为了维护这世界的爱与正义,我们来排除上面动态注册事件的泄露吧~

首先,准备好下列Return Bug常用函数

jass: Copy code
function H2I takes handle h returns integer
return h
return 0
endfunction

function I2TG takes integer i returns trigger
return i
return null
endfunction

function I2TC takes integer i returns triggercondition
return i
return null
endfunction

function I2TA takes integer i returns triggeraction
return i
return null
endfunction

然后,上面的动态注册事件可以这样写

jass: Copy code
function RegisterUnitAmortAction takes nothing returns nothing
local trigger trg = GetTriggeringTrigger()
local triggeraction Act=I2TA(GetStoredInteger(udg_GC,I2S(H2I(trg)),"Triggeraction"))
call SetUnitInvulnerable(GetTriggerUnit(), true)
call TriggerRemoveAction(trg,Act)
call DestroyTrigger(trg)
call FlushStoredMission(udg_GC,I2S(H2I(trg)))
set trg = null
set Act=null
endfunction

function RegisterUnitAmortEvent takes unit witchUnit returns nothing
local trigger trg = CreateTrigger()
local triggeraction Act
call TriggerRegisterUnitStateEvent(trg, witchUnit, UNIT_STATE_LIFE, LESS_THAN_OR_EQUAL, 50)
set Act=TriggerAddAction(trg,function RegisterUnitAmortAction)
call StoreInteger(udg_GC,I2S(H2I(trg)),"Triggeraction ",H2I(Act))
set trg = null
set Act=null
endfunction

在这里,我们定义了一个触发器动作变量Act,用它来保存生成的触发器,然后把它删除;大家是不是觉得这种方法似曾相识呢——以前我们排除点泄露的时候,也是用个点变量来保存生存的点,之后再清除这个变量保存的点。在删除点时,我们使用 RemoveLocation( )函数;而删除触发器条件或动作,我们使用TriggerRemoveCondition()和TriggerAddAction()这两个函数。(至于TriggerClearActions()和TriggerClearConditions()这两个函数,老狼说是废函数,“相当于把垃圾扫床底下”,那我们就不去理会它们吧。)

触发器条件和动作的清除的确比较麻烦,不过大家可以采用老狼写的两个删除触发器自定义函数:

jass: Copy code
function DestroyTriggerAllById takes integer t returns nothing
call TriggerRemoveCondition(I2TG(t),I2TC(GetStoredInteger (udg_GC,I2S(t),"TriggerCondition")))
call DestroyTrigger(I2TG(t))
call FlushStoredMission(udg_GC,I2S(t))
endfunction

function DestroyTriggerAll takes trigger trg returns nothing
call TriggerRemoveCondition(trg,I2TC(GetStoredInteger(udg_GC,I2S(H2I(trg)),"TriggerCondition")))
call DestroyTrigger(trg)
call FlushStoredMission(udg_GC,I2S(H2I(trg)))
endfunction
//========================================================
function RegisterUnitAmortCond takes nothing returns nothing
call SetUnitInvulnerable(GetTriggerUnit(), true)
call DestroyTriggerAll(GetTriggeringTrigger())
endfunction

function RegisterUnitAmortEvent takes unit witchUnit returns nothing
local trigger trg = CreateTrigger()
call TriggerRegisterUnitStateEvent(trg, witchUnit, UNIT_STATE_LIFE, LESS_THAN_OR_EQUAL, 50)
call StoreInteger(udg_GC,I2S(H2I(trg)),"TriggerCondition",H2I (TriggerAddCondition(trg,Condition(function RegisterUnitAmortCond))))
set trg = null
endfunction

老狼把触发器动作里的内容写到了条件里,为什么这样写大家可以去搜索教程,这里就不赘述了。这里DestroyTriggerAll()和DestroyTriggerAllById()两个函数区别仅仅在于一个传递触发器类型参数,一个传递整数类型参数(这里的整数参数是通过H2I(trg)得到的) ,大家按自己写JASS的习惯,选择其中一个来使用就可以了。此外,为了详细讲解步骤,我定义了Act这个触发器动作变量,但实际写代码时,可以像老狼那样省略这个中间变量,直接把数据存入缓存。
大家不妨以老狼这段代码,作为动态注册事件的模板。

二、缓存的使用

在之前讲解缓存时,我曾说Return Bug改变了GameCache的命运,发掘了GameCache的潜力;那么GameCache究竟强大在哪里。此外,当被问到什么是GameCache,高手们往往会回答,“一个数据库”、“一个仓库”。GameCache是如何担当存储数据的重任呢,我们不妨看下GameCache与全局变量的对比。

1.单个变量

我们要储存一个单位,比如everguo,

数据
GameCache
全局变量

Everguo(单位)
StoreInteger(udg_GC,”Handsome”,”Everguo”,H2I(Everuo))
Set Handsome=Everguo

(Handsome为单位型全局变量)

2.数组

数据
GameCache
全局变量

1 Everguo

2 汤姆.汗克斯

3 莱昂纳多

StoreInteger(udg_GC,”Handsome”,”1”,H2I(Everuo))

StoreInteger(udg_GC,”Handsome”,”2”,H2I(汤姆.汗克斯))

StoreInteger(udg_GC,”Handsome”,”3”,H2I(莱昂纳多))

Set Handsome[1]=Everguo

Set Handsome[2]=汤姆.汗克斯

Set Handsome[3]=莱昂纳多

(Handsome为单位型全局变量数组)

3.多个数组

数据
GameCache
全局变量

1
2
StoreInteger(udg_GC,”Handsome”,”1”,H2I(Everuo))

StoreInteger(udg_GC,”Handsome”,”2”,H2I(汤姆.汗克斯))

StoreInteger(udg_GC,”Handsome”,”3”,H2I(莱昂纳多))

StoreInteger(udg_GC,”SeLang”,”1”,H2I(Red_Wolf))

StoreInteger(udg_GC,”SeLang”,”2”,H2I(幽灵狼))

StoreInteger(udg_GC,”SeLang”,”3”,H2I(田伯光))

Set Handsome[1]=Everguo

Set Handsome[2]=汤姆.汗克斯

Set Handsome[3]=莱昂纳多

Set SeLang[1]= Red_Wolf

Set SeLang[2]= 幽灵狼

Set SeLang[3]= 田伯光

(Handsome、SeLang均为单位型全局变量数组)

1 Everguo

2 汤姆.汗克斯

3 莱昂纳多

1 Red_Wolf

2 幽灵狼

3 田伯光

以上是GameCache和全局变量在储存数据上的对比,看上去GameCache储存数据比全局变量繁琐得多;不知大家注意到没有,GameCache跟全局变量相比有个最大的优点——能动态地生成数据库。

比如,在游戏开始后,我们想再储存如下人妖排行榜:

1 东方不败

2 李宇春

3 看帖不回的

用GameCache能轻易做到

jass: Copy code
StoreInteger(udg_GC,”RY”,”1”,H2I(东方不败))

StoreInteger(udg_GC,”RY”,”2”,H2I(李宇春))

StoreInteger(udg_GC,”RY”,”3”,H2I(看帖不回的))

但用全局变量却做不到,除非我们一开始就定义了“RY”这个单位型全局变量数组。

回想起来,之前我们为什么说JASS不能用T替代,因为Return Bug+GameCache在T里无法做到。比如,在上文排除触发器泄露中,call StoreInteger(udg_GC,I2S(H2I(trg)),"Triggeraction ",H2I(Act)),如果把这条语句用全局变量来写,那么我们必须事先定义好一个触发器动作型全局变量数组;如果我们想用全局变量绑定数据,那得预先定义海量的全局变量数组。

此外,GameCache的灵活性也是无可替代,以下是上次教程讲解Return Bug+GameCache绑定数据时提到例子

jass: Copy code
function ManData takes unit man,integer stature,integer avoirdupois returns nothing
call StoreInteger(udg_GC,I2S(H2I(man)),"stature",stature)
call StoreInterger(udg_GC,I2S(H2I(man)),"avoirdupois",avoirdupois)
endfunction

然后

jass: Copy code
call ManData(Lucy,165,45)
call ManData(Jack,172,60)

将数据输入。可以看出,只需预先写个系统(比如上文的ManData函数) ,然后再向缓存写入数据就非常方便而且一目了然。

那么,用全局变量的话,如何做到呢

set stature[1]=165
set stature[2]=172
set avoirdupois[1]=45
set avoirdupois[2]=65

首先定义了stature和avoirdupois这两个全局变量数组后,我们还必须有惊人的记忆力,记住数组方括号内的数字对应的单位——这里1对应Lucy,2对应Jack,如果单位多了将很难区分;而缓存在记录数据时,是以字符窜为标识,很形象,比如我们称呼一个人,是称呼“9527”呢,还是“华安”。
缓存与全局变量数组的区别就在于此,它们各有优缺点。缓存以字符窜为标识,存储数据,这在我们写代码的时候能一目了然;但这也正是它的缺点,写入和读出数据速度比较慢。全局变量数组虽然不形象,难记忆,但效率高。
在这里还是向大家推荐缓存,虽然它效率不如全局变量高,但在游戏中差别不大;我们也没必要为提高一点点效率而牺牲代码的可读性。

此外,GameCache还可储存二维数组,比如上文的三个排行榜:

排行榜

排名

1
2
3

1
Everguo
Red_Wolf
东方不败

2
汤姆.汗克斯
幽灵狼
李宇春

3
莱昂纳多
田伯光
看帖不回的

把上面数据写入

jass: Copy code
StoreInteger(udg_GC,”1”,”1”,H2I(Everuo))
StoreInteger(udg_GC,”1”,”2”,H2I(汤姆.汗克斯))
StoreInteger(udg_GC,”1”,”3”,H2I(莱昂纳多))
StoreInteger(udg_GC,”2”,”1”,H2I(Red_Wolf))
StoreInteger(udg_GC,”2”,”2”,H2I(幽灵狼))
StoreInteger(udg_GC,”2”,”3”,H2I(田伯光))
StoreInteger(udg_GC,”3”,”1”,H2I(东方不败))
StoreInteger(udg_GC,”3”,”2”,H2I(李宇春))
StoreInteger(udg_GC,”3”,”3”,H2I(看帖不回的))

然后我们写个函数方便查找数据

jass: Copy code
function UnitData takes integer x,integer y returns unit
return I2U(GetStoredInteger(udg_GC,I2S(x),I2S(y)))
endfunction

如果我们想调用这个二维数组(2,2)对应的单位,只需set unit=UnitData(2,2),就把幽灵狼赋值给了unit。

在T中只有一维数组,因此在处理很多问题时极不方便。而在使用GameCache后,我们可以StoreInteger(udg_GC,I2S(H2I(xx)), I2S(H2I(xx)),H2I(xx))或是GetStoredXXXX(udg_GC,I2S(H2I(xx)),I2S(H2I(xx))),要多方便有多方便。

或许此刻你已经在心中呐喊“Return Bug+GameCache”实在是太YD了,的确,我以前就把他们比喻成奸夫淫妇来着~

不得不说,Return Bug+GameCache改变了WEer写代码的思维,灵活地运用它们我们可以写出很多实用的系统。
或许上面关于Return Bug+GameCache替代全局变量存储数据的例子你不是很懂,不要紧,看完下面的演示你将对Return Bug+GameCache的使用有更清晰的认识。

三.Return bug+GameCache+JASS动态注册事件实例

演示永远是最好的教程。那么,在学习了JASS动态注册事件,以及用Return bug+GameCache存储数据后,我们来看个实例。
大多数RPG地图都有这样的T——“当单位进入XX区域,立刻移动单位到XX区域”,比如常见的进洞,当英雄进入洞口,立刻被传送到洞内,再从洞内的出口出来。下面是我写的一个3C地图的进洞函数。

jass: Copy code

function H2I takes handle h returns integer
return h
return 0
endfunction
function I2U takes integer i returns unit
return i
return null
endfunction

function Trig_RectUnitMove_Conditions takes nothing returns boolean
local unit hero = GetTriggerUnit() //获得触发单位
local trigger trig = GetTriggeringTrigger() //获得当前触发
local player P = GetOwningPlayer(hero)
local real x = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX") //从缓存中读出目标区域中心的X坐标
local real y = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY") //从缓存中读出目标区域中心的Y坐标
local integer level = GetStoredInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel") //从缓存中读出英雄等级限制
local integer i=0
if IsUnitType(hero, UNIT_TYPE_HERO) != true or (GetPlayerId(P)>11) then //如果触发单位不是英雄,或触发玩家是中立玩家
set hero=null
set

trig=null
return false //直接返回,不执行下面的动作
endif
set i = GetHeroLevel(hero) //获得英雄等级
if i<level then
call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, "英雄等级未满"+I2S(level)+"级,不能进入")
//英雄等级不够,不能进洞
else
call SetUnitX(hero,x)
call SetUnitY(hero,y) //移动英雄到目标区域中心
call PanCameraToTimedForPlayer(P,x,y,0) //镜头跟随
call IssueImmediateOrderById(hero,OrderId("stop"))
//由于用坐标函数移动单位,单位当前状态不变,因此进洞后会继续移动,所以得让它停下来
endif
set hero=null
set trig=null
return false
endfunction

function RectUnitMoveTrigger takes integer level,rect r1,rect r2 returns nothing
local real x = GetRectCenterX(r2) //获得目标区域中心的X坐标
local real y = GetRectCenterY(r2) //获得目标区域中心的Y坐标
local trigger trig=CreateTrigger()
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX",x) // 储存目标区域中心的X坐标
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y) // 储存目标区域中心的Y坐标
call StoreInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel",level) //存储英雄等级限制
call TriggerRegisterEnterRectSimple( trig, r1 ) //以单位进入指定区域为事件
call TriggerAddCondition(trig,Condition(function Trig_RectUnitMove_Conditions)) //添加条件
set trig = null
endfunction

上面是个很简单的Return bug+GameCache+JASS动态注册事件运用的例子,在学习了前面Return bug原理教程的朋友很容易能看懂,需要说明的是,这里使用Return bug+GameCache绑定数据,没用把目标区域(r2)中心坐标和等级限制绑定在指定区域(r1),而是绑定在了新生成的触发器(trig)上,因为我们可以通过GetTriggeringTrigger()来得到当前的触发,却不能得到当前英雄所在的区域(r1)。此外,我们在绑定数据的时候,除了触发器,一般是跟单位或计时器绑定,然后通过GetTriggerUnit()和GetExpiredTimer()分别获得单位和计时器,然后从单位和计时器上读出预先绑定的数据。
从上文的例子可以看出,我们先把目标区域中心的坐标写到缓存里,保存在“I2S(H2I(trig))”下,然后通过local trigger trig = GetTriggeringTrigger()来获得当前触发,用GetStoredReal(udg_GC,I2S(H2I(trig)),"XXXXXX")来读出数据。(原理我就不复述了,参看前面Return bug原理教程。)
(给十分钟,好好看下这段代码。)
相信你已经明白了,所谓的数据绑定,无非是把数据用Return bug+GameCache把数据存储到缓存中“XXX”的类别名下(这里的类别“XXX”,大都是通过I2S(H2I())将触发器、单位和计时器转换得来的),然后在按上文的方法从缓存读出来。
那么,我们为何要称这方法为数据绑定呢?
在讲解Return bug原理的时候,我曾提了两种存储数据的方法,一是上文的以I2S(H2I())转化的handle类参数为类别名,如

jass: Copy code
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX",x)
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y)
call StoreInteger(udg_GC,I2S(H2I(trig)),"RectUnitMoveLevel",level)

一是以I2S(H2I())转化的handle类参数为缓存项目名

jass: Copy code
call StoreReal(udg_GC,"RectUnitMoveX",I2S(H2I(trig)),x)
call StoreReal(udg_GC,"RectUnitMoveY",I2S(H2I(trig)),y)
call StoreInteger(udg_GC,"RectUnitMoveLevel",I2S(H2I(trig)),level)

我曾说,第二种方法,不合我们的习惯,为何?
比如,我们希望这个触发器在运行后自动销毁,此时我们应当将缓存中的数据也清除掉。
以I2S(H2I())转化的handle类参数为类别名的清除缓存数据

jass: Copy code
call FlushStoredMission(udg_GC,I2S(H2I(trig)))

以I2S(H2I())转化的handle类参数为缓存项目名的清除缓存数据

jass: Copy code
FlushStoredReal(udg_GC,"RectUnitMoveX",I2S(H2I(trig)))
FlushStoredReal(udg_GC,"RectUnitMoveY",I2S(H2I(trig)))
FlushStoredInteger(udg_GC,"RectUnitMoveLevel",I2S(H2I(trig)))

比较上面两种方法,大家应该明白了为何我们要以I2S(H2I())转化的handle类参数为类别名了吧。用这种方法,可以很容易的把数据与handle类参数挂钩,也可很容易地清除,因此我们称呼这种方法为数据绑定。

当我们把数据绑定到一个单位上,比如以前那个仿暗黑佣兵系统,当单位嗝屁后,可以把与它相关的数据清理掉,释放内存;把数据绑定在触发器上,比如那个环绕函数模板,当环绕时间到了或是释放技能的英雄挂了后,要立刻销毁计时器,同时清空缓存;在前面讲解动态注册事件排泄时,我们看到了,在删除触发器后,也有清除缓存数据的语句。
这便是数据绑定,也是JASS强大所在;为了让大家印象深刻,我再举个例子。

如果我们想只让特定类型单位进入区域才触发事件,比如3C里羊进圈选英雄,那么我们只需修改下上面的代码

jass: Copy code

function Trig_WC_Conditions takes nothing returns boolean
local unit hero = GetTriggerUnit() //获得触发单位
local trigger trig = GetTriggeringTrigger() //获得当前触发
local player P = GetOwningPlayer(hero)
local real x = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveX") //从缓存中读出目标区域中心的X坐标
local real y = GetStoredReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY") //从缓存中读出目标区域中心的Y坐标
local integer WCuid = GetStoredInteger(udg_GC,I2S(H2I(trig)),"WCuid") //从缓存中读出指定单位类型
local integer uid=GetUnitTypeId(hero)
local string s=GetStoredString(udg_GC,I2S(H2I(trig)),"String")
if WCuid!=uid then
call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 10, s) //与指定单位类型不匹配,不能入内
else
call SetUnitX(hero,x)
call SetUnitY(hero,y) //移动单位到目标区域中心
call PanCameraToTimedForPlayer(P,x,y,0) //镜头跟随
call IssueImmediateOrderById(hero,OrderId("stop"))
//由于用坐标函数移动单位,单位当前状态不变,因此进洞后会继续移动,所以得让它停下来
endif
set hero=null
set trig=null
return false
endfunction

function WCMoveTrigger takes integer uid,rect r1,rect r2,string s returns nothing
local real x = GetRectCenterX(r2) //获得目标区域中心的X坐标
local real y = GetRectCenterY(r2) //获得目标区域中心的Y坐标
local trigger trig=CreateTrigger()
call StoreReal(ud

g_GC,I2S(H2I(trig)),"RectUnitMoveX",x) // 储存目标区域中心的X坐标
call StoreReal(udg_GC,I2S(H2I(trig)),"RectUnitMoveY",y) // 储存目标区域中心的Y坐标
call StoreInteger(udg_GC,I2S(H2I(trig)),"WCuid",uid) //存储进入区域单位类型
call StoreString(udg_GC,I2S(H2I(trig)),"string",s) //存储提示文字
call TriggerRegisterEnterRectSimple( trig, r1 ) //以单位进入指定区域为事件
call TriggerAddCondition(trig,Condition(function Trig_WC_Conditions)) //添加条件
set trig = null
endfunction

上面代码使以单位类型判断替换英雄等级判断,只有特定单位才能进入区域;类似地,你也可以加入单位判断,只有XX单位才能进入,比如“只有FRJJ才能进入看帖不回的人的房间”,只需StoreReal(udg_GC,I2S(H2I(trig)),"Lover",FRJJ),然后比较触发单位是不是FRJJ,这里就不把演示写出来了。

相信通过上面的例子,你已经对Return bug+GameCache有一定了解了,但那是不够的,在下次的教程,我将讲解如何用Return bug+GameCache做更复杂的系统和演示。

BCC工具的使用:
1. 运行bcc.bat
2. 然后将你修改过的地图中的J文件和原正常地图中的J文件复制到软件目录, 比如我的修改过的地图名为 war3map.j 原始未修改的J文件为 war3map_o.j.
3. 在命令行敲入 bcc war3map_o.j war3map.j 如果提示" 恭喜, 生成成功"字样时则为成功, 软件目录中会生成一个new_war3map.j文件,将这个new_war3map.j重命名为war3map.j替换正版地图中的J文件即可.

功能:
1. 可以使作弊地图拥有和正规地图相同的HASH值. 即你使用的是作弊图, 而主机使用的是正规图, 你不会因为地图不同而下载地图.

注意: J文件并不是所有东西都可以改, 引用 Sc2DotA.CoM 的 Thewisp (目前的57c作弊版作者) 一段话:

凡是实体数据都不能改,例如触发,timer,等等所有类型为handle的东西

整数,实数,boolean,字符串等都可以,此外缓存也可以操作。

原理是把缓存单方修改后,用sync系列函数把数据扩散出去,因此他就是合法数据了

点击下载

DOTA 互通 作弊 原理》有2个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注