[CE][IL2CPP][东方夜雀食堂]替换符卡特效(拒绝光污染!)
geleisisisi
编辑于 2023年05月18日 16:26

玩了东方夜雀食堂一段时间后,我实在是受不了了。

虽然这游戏给了一个关闭符卡动画的选项,但是符卡附带的技能特效还是会出现。

这其实也没啥。问题是有些稀客的符卡特效做的实在是一言难尽。乱七八糟、刺眼夺目、大红大紫、土味十足、俗不可耐,关键是还TM强迫观看。就比如黑谷山女的特效:

一晚上放个五次她的符卡,我觉得我的眼睛都要被闪瞎了,制作组不会觉得这种特效很cool吧?

半年前我就跟制作组反映过这个问题,但是直到现在我也没找到关闭特效的选项。

那只能自己动手,丰衣足食了。

前言:

  • 本文的所有地址基于steam 2.4.1d版本

  • 作者已经提前导出了Assembly-CSharp.dll。方法与大侠立志传安装mod的方法一模一样,只是安装时有可能需要梯子来下载必要文件。

  • 使用的CE版本为7.5,且确保了Activate mono features被激活

定位要修改的位置:

首先,需要选择一个切入点。

既然是想要修改符卡特效,必定要先找到这个符卡。

通常来说,符卡释放后会带来一系列的改变。通过这些变动下断,然后反追上去,一般来说就能找到想要的信息。

那么符卡释放后,会有什么变化呢?

比较典型的,一个是符卡的buff时间会增加,另一个是场面上总buff的个数会增加。

这里我选择的切入点是buff总个数,因为符卡的释放有可能会造成储存buff时间的地址变动,所以不太好追。而buff个数是固定的地址,所以更加容易上手一些。

初始数量搜0-10,然后每增加1buff,搜+1,每减少搜-1,如此反复,得到储存buff数量的地址:15516C346E4

查找什么改写了该地址,然后用黑谷释放一次符卡,得到下图:

追过去,下断,然后重新用黑谷释放符卡:

同时我们也得到了return address:

那么接下来就是定位每一个上层函数,查看函数名以及附近调用的函数。

GameAssembly.dll+12C46AF:

看不出来啥,继续往上追。

GameAssembly.dll+A11F2B:

函数头同样看不出来东西,继续往上追。

GameAssembly.dll+669B49:

这里开始有点意思了,继续往上,看看上层函数叫啥。

GameAssembly.dll+66E09E:

这个从函数名来看,很明显就是黑谷的符卡效果,即:关店时客人不会离开。那么必定是先触发符卡,然后调用此函数。也就是说想要得到符卡信息还得往上走。

GameAssembly.dll+4CAF0D:

看这个函数名,有点像委托。

那么就去Assembly-CSharp.dll里搜一下"c__DisplayClass12_0 OnPositiveBuffExecute b__0",得到下图:

如果我们把游戏切换成英文版,就会发现黑谷山女的英文名就叫Yamame。

换言之,这个Spell_Yamame,其实就是黑谷的符卡信息。

那么我们看一下Spell_Yamame究竟是个啥,以及包含了哪些属性:

首先吧,既然是特效,那么它的类型就不可能是int,float,AudioClip。

排除这三个类型,那么只剩下GameObject类型的lightVFX,webVFX1,webVFX2。

再加上我想去除的特效是闪光棒,不是蜘蛛网。那基本上可以确定了,Spell_Yamame.lightVFX就是我想要去除的特效。

如何得到Spell_Yamame:

从Spell_Yamame的信息图里,我们可以看到,Spell_Yamame继承自SpellBase。

于是我在dnSpy里翻了N久的关键词搜索,终于给我找到了个static类型的函数:

可以看到,GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell 这个函数,只需要一个npc的id,然后就会返回该npc的SpellBase,把它强转一下,不就是Spell_Yamame了么?

代码块
JavaScript
自动换行
复制代码
int ID = 2000;//黑谷山女的ID
Spell_Yamame spell_Yamame = (Spell_Yamame)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(ID);
复制成功

替换特效的逻辑:

首先,我尝试的是直接去除特效,即:

代码块
C#
自动换行
复制代码
int ID = 2000;//黑谷山女的ID
Spell_Yamame spell_Yamame = (Spell_Yamame)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(ID);
spell_Yamame.lightVFX = null;
复制成功

但是很遗憾,一旦将特效设置为null,游戏虽然不会崩溃,但是黑谷会直接卡住不动,不吃饭也不点餐。

由于个人水平有限,我暂时没追到为啥VFX为null时人物会直接卡住。但是我转念一想,既然去除做不到,那就替换呗。

即:

代码块
C#
自动换行
复制代码
int ID = 2000;//黑谷山女的ID
Spell_Yamame spell_Yamame = (Spell_Yamame)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(ID);
spell_Yamame.lightVFX = spell_Yamame.webVFX1;
复制成功

于是那讨人厌、闪瞎眼的闪光棒特效就不见了,取而代之的是相对不起眼的蜘蛛网特效。

我觉得自己实在是太聪明了!!!

接下来无非是去游戏里找个非常不起眼,几乎等同于无的特效。最终我选择的是阿求的特效。

以下是我用来替换一些我不想看到的特效的代码:

代码块
C#
自动换行
复制代码
public static void ReplaceVFX()
{
	Spell_Akyuu spell_Akyuu = (Spell_Akyuu)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(3);
	Unity.GameObject replacingEffect;
	Unity.GameObject replacingEffect2;
	if(spell_Akyuu != null)
	{
		replacingEffect = spell_Akyuu.rewardPic;
		replacingEffect2 = spell_Akyuu.giveItem;
		if(replacingEffect != null && replacingEffect2 != null)
		{
			Spell_Yamame spell_Yamame = (Spell_Yamame)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(2000);
			if(spell_Yamame != null)
			{
				spell_Yamame.lightVFX = replacingEffect;//黑谷山女
			}

			Spell_Parsee spell_Parsee = (Spell_Parsee)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(2001);
			if(spell_Parsee != null)
			{
				spell_Parsee.rewardObject = replacingEffect;//水桥帕露西
			}

			Spell_Marisa spell_Marisa = (Spell_Marisa)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(10);
			if(spell_Marisa != null)
			{
				spell_Marisa.magicArrayEffect = replacingEffect;//雾雨魔理沙
				spell_Marisa.magicArrayExplosion = replacingEffect;
				spell_Marisa.mushroomMilkyWay = replacingEffect2;
			}

			Spell_Utsuho spell_Utsuho = (Spell_Utsuho)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(2005);
			if(spell_Utsuho != null)
			{
				spell_Utsuho.tokamakEffect = replacingEffect;//灵乌路空
			}

			Spell_Patchouli spell_Patchouli = (Spell_Patchouli)GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell(27);
			if(spell_Patchouli != null)
			{
				spell_Patchouli.philosopherStoneEffect = replacingEffect;//帕秋莉
			}
		}
		else
		{
			return;
		}
	}
	else
	{
		return;
	}
}
复制成功

但是!

这游戏好像不支持mod。

每次我使用BepInEx加载游戏到一半时,直接报错"Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object."

如果有大佬可以指点下这个问题,那真的感激不尽。

那么回归正题,既然mod patch不了,那我就直接把C#转成汇编,然后用线程调用汇编写成的函数。毕竟这个函数本质上来说很简单,唯一麻烦点的就是查下偏移。

以下是查找Spell_Yamame.lightVFX的例子。

打开CE里的Net Info,Image选择Assembly-CSharp.dll,class里搜索"Spell_Yamame&#​34;,最后得出下图:

由图可知,lightVFX的偏移是0x28。

同理可以得知其他特效的偏移,最终通过CE,创建线程来执行我们的替换函数:

代码块
HTML
自动换行
复制代码
[ENABLE]
alloc(newmem,512)
createthread(newmem)
label(exit)
label(Yamame)
label(Parsee)
label(Marisa)
label(Utsuho)
label(Patchouli)
newmem:
sub rsp,28

mov rcx,(int)3//稗田阿求
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je exit
mov rsi,[rax+48]//Unity.GameObject replacingEffect = Spell_Akyuu.rewardPic
test rsi,rsi
je exit

mov rdi,[rax+28]//Unity.GameObject replacingEffect2 = Spell_Akyuu.giveItem
test rdi,rdi
je exit

Yamame:
mov rcx,(int)2000//黑谷山女
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je Parsee
mov [rax+28],rsi//Spell_Yamame.lightVFX = replacingEffect


Parsee:
mov rcx,(int)2001//水桥帕露西
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je Marisa
mov [rax+20],rsi//Spell_Parsee.rewardObject = replacingEffect

Marisa:
mov rcx,(int)10//雾雨魔理沙
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je Utsuho
mov [rax+60],rsi//Spell_Marisa.magicArrayEffect = replacingEffect
mov [rax+68],rsi//Spell_Marisa.magicArrayExplosion = replacingEffect
mov [rax+70],rdi//Spell_Marisa.mushroomMilkyWay = replacingEffect2

Utsuho:
mov rcx,(int)2005//灵乌路空
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je Patchouli
mov [rax+38],rsi//Spell_Utsuho.tokamakEffect = replacingEffect

Patchouli:
mov rcx,(int)27//帕秋莉
call GameData.Core.Collections.NightSceneUtility.DataBaseNight.RefSpecialGuestSpell
test rax,rax
je exit
mov [rax+30],rsi//Spell_Patchouli.philosopherStoneEffect = replacingEffect


exit:
add rsp,28
ret
[DISABLE]
dealloc(newmem,512)
复制成功