Decompile and Modify Unity-Based Game with dnSpy

Decompile and Modify Unity-Based Game with dnSpy

I’ve always been fascinated by the inner workings of games and apps. Recently, I decided to explore how to decompile and modify a Unity-based game using DnSpy. This blog shares my journey, insights, and technical steps.

ℹ️ NOTE:The game used in this blog has been removed from Google Play

However, the IOS is still viable, that means the server is still alive and you are able to download APK file to play this game.

Context and Motivation

Playing mobile games can be a time-consuming and sometimes costly endeavor if you aim to excel. Back when I was in college, I faced the dilemma of either investing countless hours or spending money to achieve items in the game. This motivated me to explore an alternative: modifying the game’s source code to gain an advantage.

The game I’m going to use in this blog is Card Crushers. Card Crushers is strategy clash of card game. Click to the link to know more about it.
A bit about this game. It features two types of currencies: diamonds and goals. Players’ primary objective is to collect cards through daily quests. These cards can then be used in Player vs. Player (PvP) battles or to conquer dungeons. Strengthening one’s character is achieved solely through collecting and upgrading these cards. Cards come in various colors, including white, blue, purple, gold, and legendary.

The Idea

Knowing that the game I was playing was Unity-based, I thought of using dnSpy, a powerful .NET debugger and assembly editor, to decompile the game and tweak its source code to enhance gameplay. The initial goal was to speed up the game and skip some annoying ads. Then it gradually expanded to getting rare items, buffing diamonds, and even achieving the top rank in the game.

Tools and Requirements

Steps

  1. Decompile the Game: Use Apk Easy Tool to decompile the APK file into its component files, including the crucial Assembly-CSharp.dll file.
  2. Edit the Source Code: Locate and modify specific classes and methods to achieve desired game modifications.
  3. Recompile the Game: Save the changes in dnSpy, then use Apk Easy Tool to recompile the modified APK file.

Examples and Code Snippets

Claimable Chest: Check whether the chest is claimable/openable.
Instead of checking the condition, we will always return the result as true. That means I don’t need to worry about key conditions or waiting time.

claimable_origin.cs
public class ChestSlotReward {
  public bool IsClaimable() {
    TimerController.TimerState timerState = this.GetTimerState();
    return timerState == TimerController.TimerState.Expired;
  }
}
mod_claimable.cs
public class ChestSlotReward {
  // Token: 0x06002DCF RID: 11727
  public bool IsClaimable() {
    return true;// alwayys claimable
  }
}

xDiamond: Increases the number of rewards.
Each time you level up, you receive a reward once. But there is a limit of maxDiamondPerRequest, so I changed the number of times the gift is added to my inventory.

xDiamond_origin.cs
public class PlayerLevelUpWindowController: WindowControllerExtended {
  private int xDiamon = 20;

  float delayPerObject = 1 f;
  yield
  return new WaitForSeconds(this.Delay);
  if (this.reward.Length > 0) {
    this.Reward1.SetCurrency(this.reward[0]);
    this.Reward1.gameObject.SetActive(true);
    Currency.Instance.AddCurrency(this.reward[0], this.Reward1.transform.position);
    yield
    return new WaitForSeconds(delayPerObject);
  }
}

Speed Game Up: Speed the game up 12x.
Increase the game’s rendering speed for faster operations.

speed_up.cs
public class MatchManager: MonoBehaviour {
  private void SpeedUp(bool speedUp) {
    if (speedUp) {
      this.SpeedOnSpeedUp = 2f;
      Time.timeScale = this.SpeedOnSpeedUp;
      return;
    }
    Time.timeScale = 1f;
  }
}
mod_speed_up.cs
public class MatchManager: MonoBehaviour {
  private void SpeedUp(bool speedUp) {
    if (speedUp) {
      this.SpeedOnSpeedUp = 12f; // speed-up 12f instead of 6x now
      Time.timeScale = this.SpeedOnSpeedUp;
      return;
    }
    Time.timeScale = 6f;
  }
}

Toast Message: Displays messages as toast for debugging.
Since I just edited the logic here, it is difficult to log the results, so the simplest way is to display the message on the screen to debug.

toast_message.cs
private void _ShowAndroidToastMessage(string message) {
  AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
  AndroidJavaObject unityActivity = unityPlayer.GetStatic <AndroidJavaObject>("currentActivity");
  if (unityActivity != null) {
    AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
    unityActivity.Call("runOnUiThread", new object[] {
      new AndroidJavaRunnable(delegate() {
        toastClass.CallStatic < AndroidJavaObject > ("makeText", new object[] {
          unityActivity,
          message,
          0
        }).Call("show", new object[0]);
      })
    });
  }
}

Always Win in a Battle
After the match results are available, the result variable will be used as a parameter to update data, rewards, and rankings. So as soon as I got the match results, I changed the result to Victory without paying attention to the real result.

auto_win_original.cs
public class MatchManager: MonoBehaviour {
  private void ProceedEndGame(bool playerWon) {
    if (playerWon) {
      //lots of codes when user wins
      //such as set result to victory, add winning reward
    } else {
      //lots of codes when use looses
      //such as set result to defeat, show ads
    }
  }
}
mod_auto_win_original.cs
public class MatchManager: MonoBehaviour {
  private void ProceedEndGame(bool playerWon) {
    playWon = true;// not matter what the result is. User always wins
    if (playerWon) {
      //lots of codes when user wins
      //such as set result to victory, add winning reward
    } else {
      //lots of codes when use looses
      //such as set result to defeat, show ads
    }
  }
}

Skip All Advertisements:
When the advertising function is called, the system will call the API to run the ad. Here we can delete all the code in the advertise function so that the advertising API will never be called.

skip_advertisement.cs
public abstract class AfterLevelBaseController: WindowControllerExtended {
	  protected void ShowAd() {
     //remove all code content to prevent Ads from displaying on the screen
	}
}

After modding this game, within 1 day I was at the top of the rankings as well as get a large amount of golds and diamonds. To get the quantity as shown below normal players need to deposit thousands of dollars and several years of hard work.

preview diamondpreview card

Some Notes About This Game

  • The game was not obfuscated, so I could easily reverse the code and understand the logic to make the changes.
  • Most of the logic is on the client side, and there is no check from the backend. Therefore, cheaters could easily modify the existing logic to buff their currency and in-game items.
  • If I were this game’s developer, I would move the crucial logic such as Currency.Instance.AddCurrency and MatchManager and all of the logics related ot the creation of in-game items to the backend side. This would prevent users from programmatically invoking those functions and cheating the results.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *