当你在非资料片模式下,在地狱难度最后杀Diablo的时候,如果是8个玩家结盟在Diablo的场景,有人就会要求等级最低的离队。
这看似是一个奇怪的要求,但是如果没人离队,就是说如果是8pp杀Diablo的话,那么每个玩家只能获得1点经验,而不是预期的大量经验。
同样的情形发生在资料片模式地狱难度杀Baal五小队的第二小队的时候,如果没人离队而且8个玩家都在场,那么每个玩家只能获得1点经验。BN上有一些老玩家习惯在杀第二小队的时候,等级最高的离队,杀完第二小队后,再重新组队。
这个问题解决方法只有一个:杀Diablo或者第二小队的时候,最多保持7个玩家在场。
如果你想深究这个原因,你回去查询IMPK的经验计算过程,“Experience计算的详细流程”
http://impk.blizzard.cn/ShowTopic-546719-34.html 你会发现其中并没有说明为何出现这种情况。因此我决定研究一下程序代码,以找到真正的原因。
很容易,可以发现游戏中经验值的计算代码,而问题出在结盟时候经验分配的计算上,代码如下(1.11b的代码,1.10的算法一样)
.text:6FC9E04D loc_6FC9E04D: ; CODE XREF: PersonExpGain2+50j
.text:6FC9E04D mov esi, [esp+70h] ; 引入结盟因素,基础经验值=0x0039FD64
.text:6FC9E051 lea eax, [ecx-1] ; 同一区域内在exp分配范围内结盟玩家数
.text:6FC9E054 imul eax, esi ; 0x0195EDBC=exp*(player-1)
.text:6FC9E057 imul eax, 59h ; 0x8D1FA65C=exp*(player-1)*89
.text:6FC9E05A cdq ; EDX=FFFFFFFF EAX=8D1FA65C
.text:6FC9E05B and edx, 0FFh ; EDX=FF EAX=8D1FA65C
.text:6FC9E061 add eax, edx ; EAX=8D1FA75B=8D1FA65C+FF
.text:6FC9E063 sar eax, 8 ; EAX=EAX/256=FF8D1FA7
.text:6FC9E066 add eax, esi ; EAX=FFC71D0B=FF8D1FA7+0039FD64
.text:6FC9E068 mov [esp+5Ch+arg_8], eax
.text:6FC9E06C xor esi, esi
.text:6FC9E06E test ecx, ecx ; 游戏内玩家数=8
.text:6FC9E070 fild [esp+5Ch+arg_8] ; FFC71D0B
.text:6FC9E074 pushf
.text:6FC9E075 cmp dword_6FD39F7C, 0
.text:6FC9E07C jnz short loc_6FC9E084
.text:6FC9E07E fidiv dword ptr [esp+5Ch] ; 除以0x277==同一场景内所有玩家级别之和
.text:6FC9E082 jmp short loc_6FC9E08D
.text:6FC9E084 ; ---------------------------------------------------------------------------
.text:6FC9E084
.text:6FC9E084 loc_6FC9E084: ; CODE XREF: PersonExpGain2+ACj
.text:6FC9E084 push dword ptr [esp+5Ch]
.text:6FC9E088 call unknown_libname_13 ; Microsoft VisualC 2-8/net runtime
.text:6FC9E08D
.text:6FC9E08D loc_6FC9E08D: ; CODE XREF: PersonExpGain2+B2j
.text:6FC9E08D popf
.text:6FC9E08E fstp dword ptr [esp+70h] ; C5B8A225
.text:6FC9E092 jle short loc_6FC9E0CB
.text:6FC9E094
.text:6FC9E094 loc_6FC9E094: ; CODE XREF: PersonExpGain2+F9j
.text:6FC9E094 mov edi, [esp+esi*4+34h] ; 4B=玩家级别
.text:6FC9E098 mov ebx, [esp+esi*4+14h] ;
.text:6FC9E09C mov [esp+5Ch+arg_8], edi
.text:6FC9E0A0 fild [esp+5Ch+arg_8] ; 4B
.text:6FC9E0A4 fmul dword ptr [esp+70h]
.text:6FC9E0A8 call __ftol2
.text:6FC9E0AD mov ecx, [esp+6Ch] ; exp=5A
.text:6FC9E0B1 push ecx
.text:6FC9E0B2 push ebp ; ptGame
.text:6FC9E0B3 call PersonExpGain
.text:6FC9E0B8 push eax
.text:6FC9E0B9 push edi
.text:6FC9E0BA push ebp
.text:6FC9E0BB mov eax, ebx
.text:6FC9E0BD call sub_6FC9DDB0
.text:6FC9E0C2 mov eax, [esp+54h]
.text:6FC9E0C6 inc esi
.text:6FC9E0C7 cmp esi, eax
.text:6FC9E0C9 jl short loc_6FC9E094
关键的代码段是从6FC9E04D到6FC9E092这一段。代码边上的注释,其游戏场景就是:非资料片,地狱难度,8pp在场KD。
首先,地狱难度的Diablo的基础经验值是exp1=0x0039FD64,计算算法如下:
1.exp1=0x0039FD64
2.exp2=exp1*(8-1)=0x0195EDBC
3.exp3=exp2*89=0x8D1FA65C
4.将32位的exp3符号扩展成64位的值。由于exp3的最高位为1,所以扩展后EDX=0xFFFFFFFF。注意,EDX:EAX已经变成负数了....
5.EDX&0xFF,就是限制EDX不能太大,由于是负数,所以EDX=0x000000FF
6.exp4=exp3+EDX,很奇怪,为什么要加上高半部分呢?!
7.exp5=exp4/256=FF8D1FA7,注意这里用的是算数移位,由于最高位是1,所以补入1
8.使用fild指令,将exp5装入浮点寄存器。注意fild指令认为这是一个32位的有符号数,所以到了浮点寄存器里面,变成负数浮点数了
9.exp6=exp5/同一场景内所有玩家级别之和
下面开始循环了
10.exp7=exp6*当前玩家的等级
11.将exp7放入eax,调用PersonExpGain函数,进一步修正玩家最终获得的经验
12.对获得经验的每个玩家,重复10、11步
PersonExpGain函数执行经验修正,具体算法就如IMPK的资料所示,值得注意的是,一开始有一个判断
.text:6FC9B3E0 push esi
.text:6FC9B3E1 mov esi, eax ; 经验总数
.text:6FC9B3E3 cmp esi, 7FFFFFh
.text:6FC9B3E9 jle short loc_6FC9B3FE
.text:6FC9B3EB mov esi, 7FFFFFh
.text:6FC9B3F0
.text:6FC9B3F0 loc_6FC9B3F0: ; CODE XREF: PersonExpGain+20j
.text:6FC9B3F0 test ebx, ebx ; ebx=ptPlayer
.text:6FC9B3F2 jz short loc_6FC9B40B
.text:6FC9B3F4 cmp dword ptr [ebx], 0
.text:6FC9B3F7 jnz short loc_6FC9B40B
.text:6FC9B3F9 mov eax, [ebx+4] ; ptPlayer->Type PAL=3
.text:6FC9B3FC jmp short loc_6FC9B40D
.text:6FC9B3FE ; ---------------------------------------------------------------------------
.text:6FC9B3FE
.text:6FC9B3FE loc_6FC9B3FE: ; CODE XREF: PersonExpGain+9j
.text:6FC9B3FE test esi, esi
.text:6FC9B400 jg short loc_6FC9B3F0 ; 经验小于等于0,则强制为1
.text:6FC9B402 mov eax, 1
.text:6FC9B407 pop esi
.text:6FC9B408 retn 8
Blizzard的程序员有先见之明,一开始拿获得的经验值与0x7FFFFF比较,取较小者。但是此时的EAX是负数,所以在
6FC9B3E9 jle short loc_6FC9B3FE
处的比较会成功,转到
6FC9B3FE test esi, esi
然而这个test会失败,跳过了所有后续复杂的经验值修正,所以最后获得的经验值就是1,然后直接返回。
从上面的代码可以看出,暴雪的程序员应当试图在按照IMPK给出的公式来进行编程,但是由于没有注意到数值的有效位数,导致很容易就溢出变成负数,从而导致8pp KD奇怪的经验值获得。
如果是7pp KD,那么计算如下:
1.exp1=0x0039FD64
2.exp2=exp1*(7-1)=0x015BF058
3.exp3=exp2*89=0x78F68E98
4.将32位的exp3符号扩展成64位的值。由于exp3的最高位为0,所以扩展后EDX=0x00000000。注意,EDX:EAX还是正数....
5.EDX&0xFF,就是限制EDX不能太大,由于是正数,所以EDX=0x00000000
6.exp4=exp3+EDX=0x78F68E98
7.exp5=exp4/256=0x0078F68E,注意这里用的是算数移位,由于最高位是0,所以补入0
8.使用fild指令,将exp5装入浮点寄存器。注意fild指令认为这是一个32位的有符号数,所以到了浮点寄存器里面,还是正数浮点数了
9.exp6=exp5/同一场景内所有玩家级别之和
下面开始循环了
10.exp7=exp6*当前玩家的等级
11.将exp7放入eax,调用PersonExpGain函数,进一步修正玩家最终获得的经验
12.对获得经验的每个玩家,重复10、11步
可见,7PP KD,可以获得正常的经验值。