使用Ghidra为GameCube反向工程超级猴子球

2021-03-02 23:18:10

游戏于2021年2月28日,星期日在第1部分中,我们使用了Dolphin的调试器在RAM中查找播放器1的乐谱的位置。在本文中,我们的目标是破解分数计算例程,以使奖励目标不会奖励那么多积分。

超级猴子球中的关卡最多可以具有三个单独的出口:正常的蓝色出口,绿色的出口和红色的出口。如果您设法到达绿色或红色出口,则游戏将让您跳过许多即将到来的关卡(例如,跳过接下来的3个关卡)。此外,它会将完成级别的得分乘以跳过级别的数量(例如,您获得的分数是正常退出时的3倍)。这是一个非常疯狂的点数,特别是对于您仅需几秒钟即可完成的关卡,因此还可以获得时间奖励乘数。在下面的屏幕截图中,正常的蓝色出口将获得3386点。相反,该游戏奖励130,158点,因为我在红色出口处完成了关卡!风险/报酬计算中有一些乐趣,但是当它变得如此偏斜时却没有!

这确实激励您跳过关卡,这意味着您玩的游戏少了,这太糟了。如果我们完全消除级别跳过奖金,那么它将成为更有趣的折衷方案。您可以跳过一些级别,这样就不会失去生命,但分数损失不大,或者您可以选择冒险完成这些级别,并从那些本该跳过的级别中获得所有分数。与总是选择Red退出以获取大量分数相比,这是一个有趣的机制。

因此,让我们从找到在完成关卡时会改变分数的代码开始。与第1部分相同,我们在玩家1的得分上设置了一个内存监视断点,然后完成关卡。正如分数显示的那样,海豚休息了,我们有了例行程序。

在此屏幕快照中,有几件有趣的事情要注意。首先,您可以看到当前的指令正在将更新的分数存储到玩家的结构中,这正是我们期望/希望找到的。在此之前,它将新分数添加到现有分数中。您可以在r4和r5中分别看到要添加的新得分值和玩家的当前得分。因此,这确实看起来像我们想要找到的例程。最后,注意LR值,这是我们跳入该例程的起点。让我们转到该代码,看看什么叫做该例程。更具体地说,我们想知道该例程的开始位置,因为我们已经在例程中间的某个位置暂停了执行。

这告诉我们分数计算例程实际上从何处开始。我们来看一下此功能开始时发生的情况。

好吧……肯定是一些PowerPC代码。如果需要的话,我可以阅读汇编,但这看起来像是一件难以理解的琐事。请注意,例程的开始距离实际存储发生的地方大约500个字节。我不想尝试了解那么多的汇编。

Warning: Can only detect less than 5000 characters

iVar2 = DAT_802f1f18; iVar4 = DAT_801eec4c; if((param_1< 4)&&(1< param_1)){score_multiplier =(int)DAT_801f3a7a; // [4] iVar5 = *(int *)(*(int *)(DAT_802f1f30 + 0xc)+ 0x40); time_score =(DAT_801f3a74 * 100)/ 0x3c +(DAT_801f3a74 * 100> 0x1f); time_score = time_score-(time_score>> 0x1f); out_flags = 0; exit_data = iVar5; if((((DAT_801f3a64._0_2_!= 0)&&(exit_data = iVar5 + 0x14,DAT_801f3a64._0_2_!= 1))&&(exit_data = iVar5 + 0x28,DAT_801f3a64._0_2_!= 2)) exit_data = iVar5 + 0x3c; } if(*(char *)(exit_data + 0x12)==' G'){out_flags = 4; time_score = time_score + 10000; // [1]} else {if(*(char *)(exit_data + 0x12)==' R'){out_flags = 8; time_score = time_score + 20000; // [1]}} if(score_multiplier!= 1){out_flags = out_flags | 2; } if(DAT_801f3a5e>> 1< DAT_801f3a5c){// [2] out_flags = out_flags | 1; score_multiplier = score_multiplier<< 1; // [2]} score_accum = time_score * score_multiplier;如果(param_1 == 3){FUN_8007f368(time_score,score_accum,out_flags); // [3]}

现在,它已经很可读了。一件事确实让我惊讶[1]。我不知道游戏会为实现绿色和红色目标提供额外的得分奖励(预乘数)。在过去的20年中,我已经玩了数百次游戏,却完全不知道发生了什么。您甚至可以在本文顶部的屏幕截图中看到它!分数本来应该是1693,但是是21693,然后乘以6,在已经疯狂的巨额奖金之上又增加了120,000分! (也许这是问题的真正根源……?)

再远一点,在[2]中,您可以看到票面时间奖金的计算。如果您在不到指定时间的一半内击败了该级别,它将获得2倍的乘数。在那里很清楚地阐明了这种逻辑(向左或向右移位一位等于乘以或除以2)。最后一件有趣的事情是在[3]。被调用的函数的确是分数显示例程,它是唯一使用out_flags的地方。这告诉我们,在消除这些奖金时,我们还需要避免设置这些标志,因此在计分时我们不会渲染它们。

现在我们了解了此功能的工作原理,我们可以找出如何破解它以完成我们想要的事情。我们的目标是消除绿色和红色出口的得分奖励和乘数。我不是程序集黑客方面的专家,因此我想进行尽可能小的更改。查看带注释的C代码,我们可以通过将硬编码的10000和20000值分别更改为0来摆脱红利。对于乘数,请注意,初始乘数值是从[4]处的某个全局变量加载的,大概包含要跳过的级别数。不用这样做,我们可以将其硬编码为1,就像普通的Blue出口那样。

Ghidra中的另一个非常酷的功能是,您可以在右窗格中突出显示C代码行,并且它将向您显示与这些代码行相对应的程序集。使用它,我们可以轻松地找到这些硬编码值在游戏代码中的位置。

顺便说一句,我发现PPC骇客比我所做的一点点6502和x86骇客要愉快得多。 PPC是big-endian,因此可以轻松读取和写入原始字节。另外,每条指令正好是4个字节长,因此您不必为添加NOP或在指令中间开始解码而烦恼。超级好用。

无论如何,我将举一个例子说明如何破解分数乘法器,因为这是最复杂的任务。这是原始代码,与上面[4]处的score_multiplier分配行相对应。

而不是从内存中加载值,我们只想将硬编码的值1加载到r7中。我研究了一些PPC文档,发现这是我们想要的:

其他更改都更加简单,只是更改了硬编码的值。总的来说,这是补丁,可应用我们想要的所有更改,并在原始ISO文件中添加了偏移量:

消除阶段跳过分数奖金:offs |原为|现在|目的00060734 | a8e6 0022 | 38e0 0001 |消除阶段跳过奖金60a5 0004 | 60a5 0000 |不要显示绿色退出奖金乘数3884 2710 | 3884 0000 |消除绿色出口奖励积分60a5 0008 | 60a5 0000 |不要显示红色退出奖金乘数3884 4e20 | 3884 0000 |消除红色退出奖励积分

完美的。级别跳跃仍然起作用,时间奖金乘数也起作用,但是红色出口乘数和奖金消失了。即使我和我的朋友认为这不是解决此问题的正确方法,仅了解此例程的工作方式对于查找所需的解决方案来说是巨大的一步。

我们还有其他一些想法,可以进行更多增强:设置每个级别的自定义标准时间(当前标准时间有些不平衡);在可查看的表格中保存完成时间和/或每个级别的高分;级别跳过按钮组合;奖励额外积分,以使游戏以额外的生命完成。但这比在一个简单的例程中摆弄几位要复杂得多,所以我们将看到。