In part 1 we messed a bit with Moonlighter but modifying the save file. In this part, we will modify game logic using dnSpy.
We will modify our damage, player stats and discover a hidden stat.
Why dnSpy?
Moonlighter is built with the Unity game engine (C#). Game logic is usually in Assembly-CSharp.dll
. In my VM, it's at:
C:\Program Files (x86)\Steam\steamapps\common\Moonlighter\Moonlighter_Data\Managed\Assembly-CSharp.dll
I was not successful in debugging the game with dnSpy. But the instructions are here:
This is my first unity game so I might be doing something wrong or it does not work with Steam versions.
Increasing Will's Damage
Game logic is inside {}
:
Going around the list, I saw the Bow
class and clicked on it.
Inside, I searched for the string damage
and I got lucky.
DealDamageToEnemy
DealDamageToEnemy
sounds interesting. Let's double-click on it. We end up in the Enemy
class.
We can analyze this a bit. attackStrength
and enemy defense are used to calculate the damage using CalcHitDamage
:
this.totalDamage = this.CalcHitDamage(this.hitStrength, this.otherDefense);
Then the damage is applied:
this.enemyStats.CurrentHealth -= this.totalDamage;
Note: It doesn't matter if the enemy is invincible (invencible
in the code) or not, the damage is still applied.
CalcHitDamage
Double-click on CalcHitDamage
:
// Token: 0x0600150F RID: 5391 RVA: 0x00082838 File Offset: 0x00080C38
public virtual float CalcHitDamage(float hitStrength, float targetDefense)
{
float num = (float)Mathf.RoundToInt(hitStrength * (targetDefense / 100f));
return Mathf.Clamp(hitStrength - num, 0f, float.PositiveInfinity);
}
This code calculates the target's resistance and deducts it from hitStrength
.
Increasing Will's Damage
We don't know the value of damage numbers and the hitpoints of enemies yet. Let's brainstorm a bit:
- Return
float.PositiveInfinity
. This might result in an integer underflow. I do not know to be honest but we will definitely try. - Return
hitStrength + num
instead. This will definitely increase our damage but will it be enough to kill enemies in one hit? - Multiply the output by a constant.
- Change the lower band of Mathf.Clamp to a large number (e.g. 10000f).
Returning float.PositiveInfinity
Let's try this one and see what happens.
Right-click on the return
line and select Edit IL Instructions...
.
IL is a stack-based language. Values are pushed to the stack before functions or operators are called.
Look at lines 13 and 14. Line 13 calls Math.Clamp
and the next line returns it. In order to return infinity, we need to add another instruction before the return
and copy line 12 to it (pushes infinity to the stack).
- Click on
12
to select that line. Ctrl+C
to copy- Click on
13
andCtrl+V
to paste. - Press
Ok
.
Save the module, overwrite the original DLL with the modified one and start the game.
Our evil plan was foiled.
Return hitStrength + num
Grab a fresh copy and edit IL instructions again. This time we need to change the sub
instruction in line 10 to add
. Click on sub
and dnSpy shows a helpful drop-down menu of all valid instructions. Choose add
.
This is better. We are one-shotting enemies. Our damage is a constant 436
with King Sword
from part 1 regardless of enemy type.
We have accomplished our goal of increasing Will's damage. But you can try the other methods or fiddle with the method in any way you want. Experiment!
Modifying Will's Stats
Player stats are important. They are used to calculate damage. Remember attackStrength
or hitStrength
in the previous section? They should come from somewhere based on our weapon. Let's track them.
Right-click on CalcHitDamage
and select Analyze
. A new window opens up. It shows who calls the target method (Used By
which is similar to x-ref in IDA) and what the target method calls and other information.
Two functions look promising:
HeroMerchantProjectile.DealDamage(GameObject)
Weapon.OnMainAttackHit(GameObject)
HeroMerchantProjectile.DealDamage
Let's start with HeroMerchantProjectile.DealDamage
.
We can see that the intelligence
stat is used to calculate bow damage.
On a side note, clicking on Value
opens an object called ObscuredFloat
in the Stat
class. I vaguely remember reading about this obscured values in Unity on some Cheat Engine forum threads. It's something we might return and look at again when we are dealing with Cheat Engine. Apparently, they are hard to track in memory.
The Case of the Missing Intelligence
There is no intelligence stat in the game. This is a picture from part 1 that show's Will's inventory. There's no intelligence stat. It shows Vitality
, Strength
, Defence
and Speed
. Is the empty green space supposed to be the intelligence?
At first, I thought it's missing in the PC version. I looked at screenshots of the Nintendo Switch version and they looked the same.
Items do not grant intelligence either. This picture shows an item's stats in the blacksmith's UI.
In dnSpy, right-click on intelligence
and select Analyze
.
We can see it's set in HeroInventoryPanel.UpdateLabels()
:
It's updated along with other stats but does not appear in the UI. This is not good because it's an important stat.
Adding Extra Stats
Look inside EquipmentStats.AddToHeroMerchant(HeroMerchantStats)
.
Stats are added to the base stats. We can modify each stat and add any amount. For example, to add 10000
to strength we need to modify line 57: strength.Value += num2;
. Right-click line 57 and select Edit IL Instructions ...
.
See those highlighted lines? Those are IL instructions for line 57 in the source code (coincidentally it also starts from line 57). dnSpy has helpfully highlighted them for us. We must add two instructions before the final add
on line 61. One to load 10000f
and another to add
it to the previous value.
And the result in decompiled C# is:
Now Will has 90436
strength:
Why did Will's strength increase by 90000
? My guess is that each equipped item calls AddToHeroMerchant
individually. We have nine items (remember there were nine items in the willEquippedItems
array in the save file in part 1?). Will does 90436
damage now.
We could easily do the same and modify any other stat.
A Closer Look at Base Stats
Back in the analysis result for HeroMerchantStats.Intelligence
we can see it's modified inside HeroMerchantStats.Init()
:
this.intelligence = new Stat(
Constants.GetFloat("kMaxIntelligence"),
Constants.GetFloat("kMinIntelligence"),
Constants.GetFloat("kBaseIntelligence")
);
This line creates a new character stat named intelligence
. Then sets the maximum, minimum and base values. Let's see where these default values are set. Double-Click on Constants.GetFloat
to go there:
A little bit further up in the same file, we can see how these constants are obtained.
They are read from a JSON file named constants
. If we run a recursive grep for "constants" in the Moonlighter_Data
directory, we find a few files. We need to open resources.assets
. Either use a tool to extract it or open it with a hex editor (e.g. HxD) and search for the string constants
.
I used Unity Assets Bundle Extractor. I needed to install Microsoft Visual C++ 2010 Redistributable Package (x64) before running it.
Sort by Type
and look for files with the TextAsset
type.
We can dump each file. The base stats are inside the constants
dump:
There's more stuff here. For example, item drop probabilities.
Other files here contain other things such as items (we can get a list of all items), recipes, and enemy stats. By editing these files, we can change enemy stats, items stats, recipes, and more.
Lesssons Learned
We learned:
- How to edit game logic for Unity games.
- How to use dnSpy's analysis feature.
- Edit IL instructions to increase Will's damage and stats.
- Discovered a hidden stat called Intelligence that does not appear in the game's UI.
I saw some hidden features in the decompiled DLL. In the next part, I will try to enable them.