SCHEDULE I : Broken casino slot machine
Context

Schedule I is an open world crime simulator video game where players build a drug trafficking operation in a fictional city : It combines elements of economic simulation, tactical strategy, and dark comedy, allowing players to grow and sell various drugs.
It is possible to make money in this game by selling drugs or playing at the casino.
By playing the slot machine several times, I realized that it was very easy to win the jackpot. So I asked myself: “But how does the slot machine work ?”.
To answer this question, I had to reverse engineer the game.
Prices of the slot machine
While playing, you can come across a lemon, a grape, a cherry, a watermelon, a bell and a seven.
Here are the prices :

| Bonus | Symbol |
|---|---|
| Initial bet multiplied by 2 | 3 fruits of any kind |
| Initial bet multiplied by 10 | Same fruit 3 times |
| Initial bet multiplied by 25 | 3 bells |
| Initial bet multiplied by 100 | 3 seven |
It would be VERY interesting to land on 3 seven to hit the jackpot (x100 bonus).
But what is the probability of landing on a seven ? Let’s do some reverse engineering !
Il2CppDumper and Ghidra
Schedule I was created with the Unity game engine, that uses IL2CPP.

IL2CPP stands for “Intermediate Language to C++” : this is a scripting backend used in Unity that converts C# code into C++ code for better performance and compatibility across different platforms. It is possible to extract metadata and restore .NET DLLs from IL2CPP binaries using Il2CppDumper tool.


Then, reverse engineering becomes significantly easier in Ghidra : IL2CPP provides Python scripts that load the dumped metadata directly into the decompiled C++ native code.
The ghidra_with_struct.py script will apply function signatures and inject IL2CPP structures from generated script.json.
Dump.cs
I did a simple string search in dump.cs for “casino” and slot machine and I found these precious informations inside ScheduleOne.Casino::SlotMachine class :
193903
2 [ServerRpc(RequireOwnership = False, RunLocally = True)]
3 // RVA: 0x86DAA0 Offset: 0x86C2A0 VA: 0x18086DAA0
4 public void SendStartSpin(NetworkConnection spinner, int betAmount) { }
5
6 [ObserversRpc(RunLocally = True)]
7 // RVA: 0x86E240 Offset: 0x86CA40 VA: 0x18086E240
8 public void StartSpin(NetworkConnection spinner, SlotMachine.ESymbol[] symbols, int betAmount) { }
9
10 // RVA: 0x86C200 Offset: 0x86AA00 VA: 0x18086C200
11 private SlotMachine.EOutcome EvaluateOutcome(SlotMachine.ESymbol[] outcome) { }
12
13 // RVA: 0x86C3E0 Offset: 0x86ABE0 VA: 0x18086C3E0
14 private int GetWinAmount(SlotMachine.EOutcome outcome, int betAmount) { }
15
16 // RVA: 0x86BF20 Offset: 0x86A720 VA: 0x18086BF20
17 private void DisplayOutcome(SlotMachine.EOutcome outcome, int winAmount) { }
18
19 // RVA: 0x86C2F0 Offset: 0x86AAF0 VA: 0x18086C2F0
20 public static SlotMachine.ESymbol GetRandomSymbol() { }
21
22 // RVA: 0x86C880 Offset: 0x86B080 VA: 0x18086C880
23 private bool IsFruit(SlotMachine.ESymbol symbol) { }It seems that public static SlotMachine.ESymbol GetRandomSymbol() is what I’m looking for. I looked at decompiled code at 0x18086C2F0 virtual address in Ghidra :

These lines are interesting :
131
2 lVar5 = System.Enum$$GetValues(uVar4,0);
3 if (lVar5 != 0) {
4 uVar3 = System.Array$$get_Length(lVar5,0);
5 uVar4 = UnityEngine.Random$$RandomRangeInt(0,uVar3,0);
6 return uVar4;
7 }The function UnityEngine.Random$$RandomRangeInt() is responsible for choosing a random value inside uVar4 enum object :
122
2uVar4 = ScheduleOne.Casino.SlotMachine.ESymbol_var;which can be found in dump.cs :
193734
2// Namespace:
3public enum SlotMachine.ESymbol // TypeDefIndex: 2440
4{
5 // Fields
6 public int value__; // 0x0
7 public const SlotMachine.ESymbol Cherry = 0;
8 public const SlotMachine.ESymbol Lemon = 1;
9 public const SlotMachine.ESymbol Grape = 2;
10 public const SlotMachine.ESymbol Watermelon = 3;
11 public const SlotMachine.ESymbol Bell = 4;
12 public const SlotMachine.ESymbol Seven = 5;
13}
14
15// Namespace:
16public enum SlotMachine.EOutcome // TypeDefIndex: 2441
17{
18 // Fields
19 public int value__; // 0x0
20 public const SlotMachine.EOutcome Jackpot = 0;
21 public const SlotMachine.EOutcome BigWin = 1;
22 public const SlotMachine.EOutcome SmallWin = 2;
23 public const SlotMachine.EOutcome MiniWin = 3;
24 public const SlotMachine.EOutcome NoWin = 4;
25}Dynamic analysis with WinDBG
Let’s see if the return value of UnityEngine.Random$$RandomRangeInt() impacts the choice of symbol in the slot machine using WinDBG.
I attached the debugger to the PID of Schedule I.exe process and listed all modules to look for base address :

The base address of GameAssembly.dll in memory is 0x7ffcd0a80000.
The relative virtual address of ScheduleOne.Casino::SlotMachine.GetRandomSymbol() is 0x86C2F0
So its virtual address is 0x7ffcd0a80000 + 0x86C2F0 = 0x7ffcd12ec2f0 : this is where i will put my breakpoint.

If my assumptions are correct, according to enum SlotMachine.ESymbol i should land on :
- 3 : Watermelon
- 3 : Watermelon
- 1 : Lemon

Bingo !
Calculation of probabilities
We know that UnityEngine.Random$$RandomRangeInt(0,6) function represents a uniform discrete distribution where the probability of each symbol is 1/6.
Because we need 3 symbols to get an outcome, the total number of all combinations is 6*6*6 = 216.
Here are the calculations :
- 3 fruits of any kind : 4 fruits from 6 symbols, so
4^3 = 64counts possible. But 3 same fruits belongs to x10 multiplier so there are60mixed fruit combos in total - Same fruit 3 times : 4 fruits from 6 symbols, so 4 counts possible.
- 3 bells : Only one possibility to have this result among the 216 totals.
- 3 seven : Only one possibility to have this result among the 216 totals.
- No win : Substract all possible counts from all winning outcomes, so
216 - (60+4+1+1) = 150.
Here is the probability of each outcome :
| Outcome | Probability |
|---|---|
| 3 fruits of any kind | 60/216 ~ 27.78% |
| Same fruit 3 times | 4/216 ~ 1.85% |
| 3 bells | 1/216 ~ 0.46% |
| 3 seven | 1/216 ~ 0.46% |
| No win | 150/216 ~ 69.44% |
The probability of winning is roughly 30.56 % and the probability of loosing is 69.44 %.
To get the RTP (Return To Player), we sum each contribution where the contribution of each outcome is Multiplier x Probability :
| Outcome | Multiplier | Probability | Contribution |
|---|---|---|---|
| 3 fruits of any kind | x2 |
60/216 |
~ 0.5556 |
| Same fruit 3 times | x10 |
4/216 |
~ 0.1852 |
| 3 bells | x25 |
1/216 |
~ 0.1157 |
| 3 seven | x100 |
1/216 |
~ 0.4630 |
| No win | x0 |
150/216 |
0 |
So RTP is 0.5556 + 0.1852 + 0.1157 + 0.4630 + 0 which is… wait … 1.3195 ???
Conclusion
Because of RTP roughly equals to 1.3195, it theoretically means that for every $1 bet you expect to get back $1.32 on average.
All the casino slot machines are surprisingly player-favorable even if the probability of winning is only 30.56 %.
You could theoretically become rich by automating the game on a slot machine via a script !
In reality, a slot machine’s RTP is always less than 1, since a higher value would mean the casino would lose money.