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.
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.
Game logic is inside {}
:
Moonlighter's classes
Going around the list, I saw the Bow
class and clicked on it.
The Bow class
Inside, I searched for the string damage
and I got lucky.
Searching for "Damage" in the class
DealDamageToEnemy
sounds interesting. Let's double-click on it. We end up in the Enemy
class.
DealDamageToEnemy
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
.
We don't know the value of damage numbers and the hitpoints of enemies yet. Let's brainstorm a bit:
float.PositiveInfinity
. This might result in an integer underflow. I do not know to be honest but we will definitely try.hitStrength + num
instead. This will definitely increase our damage but will it be enough to kill enemies in one hit?Let's try this one and see what happens.
Right-click on the return
line and select Edit IL Instructions...
.
CalcHitDamage's 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).
12
to select that line.Ctrl+C
to copy13
and Ctrl+V
to paste.Ok
.Modified CalcHitDamage
Save the module, overwrite the original DLL with the modified one and start the game.
No damage
Our evil plan was foiled.
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
.
Changing sub to add Sub changed to 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.
Doing constant damage
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!
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.
Analyzing CalcHitDamage
Two functions look promising:
HeroMerchantProjectile.DealDamage(GameObject)
Weapon.OnMainAttackHit(GameObject)
Let's start with HeroMerchantProjectile.DealDamage
.
HeroMerchantProjectile.DeadlDamage
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.
ObscuredFloat
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?
Will's stats - no intelligence here
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.
Item stats - no intellience here either
In dnSpy, right-click on intelligence
and select Analyze
.
Intelligence analysis
We can see it's set in HeroInventoryPanel.UpdateLabels()
:
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.
Look inside EquipmentStats.AddToHeroMerchant(HeroMerchantStats)
.
EquipmentStats.AddToHeroMerchant
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 ...
.
Line 57 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.
Line 57 modified
And the result in decompiled C# is:
Line 57 modified in C#
Now Will has 90436
strength:
Strong Will
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.
Much strong, very damage, wow
We could easily do the same and modify any other stat.
Back in the analysis result for HeroMerchantStats.Intelligence
we can see it's modified inside HeroMerchantStats.Init()
:
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:
Constants.GetFloat
A little bit further up in the same file, we can see how these constants are obtained.
Constants.ReadFile
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.
TextAssets
We can dump each file. The base stats are inside the constants
dump:
Base stats
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.
We learned:
I saw some hidden features in the decompiled DLL. In the next part, I will try to enable them.