Reverse Engineering Therion's Steal Success Rate in Octopath Traveler

So I was playing Octopath Traveler (wonderful game, btw) but I had a suspicion that the game was BS-ing me about the steal rates.
So I wanted to know exactly how the game decides whether Therion gets to steal an item.
This is about the field Steal Path Action (stealing from town NPCs), not the Thief job’s battle steal skills.
Disclaimer
This is for understanding the game.
I don’t redistribute the disassembled code or assets.
Table of Contents
TL;DR
- The roll is a single call:
RandomBoolWithWeight(min(percent * 0.01, 1)). - The percent comes from a 7-bucket lookup based on
effective field-command level − ProperSteal. - Therion’s effective field-command level is his character level plus the Steal bonus from Thieving Tips & Tricks, if he has it, capped at 99.
ProperStealis the steal difficulty assigned to the item for that specific NPC, rather than to the item itself.- The seven success rates are defined in
Content/GameParam/GameParamDefineTable:3, 8, 15, 55, 65, 80, 100.
| Difference | Chance |
|---|---|
| 11 or more | 100% |
| 4 to 10 | 80% |
| 2 to 3 | 65% |
| 0 to 1 | 55% |
| −1 to −3 | 15% |
| −4 to −10 | 8% |
| −11 or less | 3% |
What exactly is the Steal Path Action?
Steal is Therion ’s Path Action .
Basically, you walk up to an NPC, and the game lists the items they’re carrying, along with the success rate for stealing each one.
It usually draws from the same item pool as Tressa ’s Purchase , with a few exceptions: some items can’t be stolen at all, while others are flagged as not for sale, making them exclusive to Steal.
If you fail an attempt, your reputation in that town takes a hit.
Methodology
This is a static analysis of the game’s decompiled Blueprint logic and data tables.
The game is Steam App ID 921570, build 5272616 (published 2020-07-22), running ++UE4+Release-4.18 as a cooked -WindowsNoEditor Shipping package.
The Blueprint bytecode is stored inside a roughly 2 GiB .pak file.
The .pak index is unencrypted (version 4, zlib, 64 KiB blocks), and
FModel
exports a clean .uasset + .uexp pair.
FModel can export the assets, but it doesn’t decompile Blueprint bytecode. For that, I used
KismetKompiler
0.4.0-alpha.
The steal formula
FieldCommandPurchase_C.StealItemExec is where the decision is made.
Decompiled to pseudocode:
1diff = Subtract_IntInt(this.FieldCommandLevel, PurchaseItemInfoData.ProperSteal);
2
3// Both getters fill four out-params from one DataTable row: (min, max, init, param)
4
5// STEAL_PROBABILITY_LESS_THAN 3, 8, 15, 55 -> the low buckets
6// STEAL_PROBABILITY_OVER 65, 80, 100, 0 -> the high buckets
7// STEAL_PROBABILITY_LEVEL_OVER 2, 4, 11, 0 -> thresholds, high side
8// STEAL_PROBABILITY_LEVEL_LESS_THAN -11, -4, -1, 0 -> thresholds, low side
9
10if (diff == 0) {
11 GetGameParamToFloat("STEAL_PROBABILITY_LESS_THAN", this, out LessMin, out LessMax, out LessInit, out LessParam);
12 weight = LessParam; // 55
13}
14else if (diff > 0) {
15 GetGameParamToInt ("STEAL_PROBABILITY_LEVEL_OVER", this, out LvMin, out LvMax, out LvInit, out LvParam);
16 GetGameParamToFloat("STEAL_PROBABILITY_OVER", this, out OverMin, out OverMax, out OverInit, out OverParam);
17 if (diff >= LvInit) weight = OverInit; // 100 (LvInit = 11)
18 else if (diff >= LvMax) weight = OverMax; // 80 (LvMax = 4)
19 else if (diff >= LvMin) weight = OverMin; // 65 (LvMin = 2)
20 else {
21 GetGameParamToFloat("STEAL_PROBABILITY_LESS_THAN", this, out LessMin, out LessMax, out LessInit, out LessParam);
22 weight = LessParam; // 55 (diff == 1: no high bucket matched)
23 }
24}
25else {
26 GetGameParamToInt ("STEAL_PROBABILITY_LEVEL_LESS_THAN", this, out LvMin, out LvMax, out LvInit, out LvParam);
27 GetGameParamToFloat("STEAL_PROBABILITY_LESS_THAN", this, out LessMin, out LessMax, out LessInit, out LessParam);
28 if (diff <= LvMin) weight = LessMin; // 3 (LvMin = -11)
29 else if (diff <= LvMax) weight = LessMax; // 8 (LvMax = -4)
30 else if (diff <= LvInit) weight = LessInit; // 15 (LvInit = -1)
31 // else: unreachable — every diff < 0 already satisfied diff <= -1
32}
33
34// and finally, the roll:
35weight01 = FMin(weight * 0.01f, 1f);
36this.SuccessSteal = RandomBoolWithWeight(weight01);
FieldCommandLevel
FieldCommandLevel is not Therion’s raw level:
1// FieldCommandPurchase_C.Open(NPCLabel, StealMode)
2if (StealMode) {
3 LibFieldCommand_C.GetFieldCommandLevelFromType(2, true, this, out this.FieldCommandLevel); // (byte)2 = eSteal
4} else {
5 LibFieldCommand_C.GetFieldCommandLevelFromType(1, true, this, out this.FieldCommandLevel); // (byte)1 = ePurchase
6}
The field-command level for command type 2 is KSFIeldCommandType::eSteal.
The CheckFCItem=True argument is what enables the Thieving Tips & Tricks bonus lookup.
The decompiled function finds the traveler whose PlayableCharacterDB row matches the command type, reads that character’s level from the save, then adds the bonus and clamps the result to 99.
99 is the number of rows in CharacterGrowData, which has one row per level, named 1 through 99.
1static public void GetFieldCommandLevelFromType(byte CommandType, bool CheckFCItem, out int FieldCommandLevel, Object __WorldContext) {
2 int CharacterLevel = 0;
3 int TmpFieldCommandLevel;
4
5 // which traveler owns this command? Therion for eSteal
6 Array<Name> RowNames;
7 GetDataTableRowNames(PlayableCharacterDB, out RowNames);
8 for (int i = 0; i < RowNames.Num(); i++) {
9 PlayableCharacterData Row;
10 if (!GetDataTableRowFromName(PlayableCharacterDB, RowNames[i], out Row)) continue;
11 if (Row.FieldCommandType != CommandType) continue;
12
13 // their level, straight out of the save
14 KSSaveGameBP_C SaveGame;
15 GetSaveData(__WorldContext, out SaveGame);
16 SaveCharacterData CharacterData;
17 SaveGame.GetCharacterData(Row.ID, out CharacterData);
18 CharacterLevel = CharacterData.Level;
19 break;
20 }
21
22 TmpFieldCommandLevel = CharacterLevel;
23
24 if (CheckFCItem && CommandType != 8 && CommandType != 7 && CommandType != 4) { // not scrutinize / inquire / provoke
25 bool FindItem;
26 int Value;
27 CheckHaveFieldCommandItem(CommandType, __WorldContext, out FindItem, out Value);
28 if (FindItem) {
29 Array<Name> GrowRowNames;
30 GetDataTableRowNames(CharacterGrowData, out GrowRowNames);
31 TmpFieldCommandLevel = Min(TmpFieldCommandLevel + Value, GrowRowNames.Num());
32 }
33 }
34
35 FieldCommandLevel = TmpFieldCommandLevel;
36}
CheckHaveFieldCommandItem
CheckHaveFieldCommandItem checks whether the field-command bonus applies in the current town.
In gameplay terms, this is the Thieving Tips & Tricks bonus. You obtain it by using Cyrus’s Inquire or Alfyn’s Scrutinize on certain NPCs. It improves Therion’s stealing success rate, but only in the town where you obtained the information.
The game implements this by adding a town-specific bonus to Therion’s effective field-command level. The bonus changes the level difference directly, which determines which of the seven probability buckets applies.
8 of the 28 towns have a Steal entry, each specifying the item associated with its bonus:
| town | item | bonus |
|---|---|---|
T_Snow_M | INFO_SNO_TMA_003 | +10 |
T_Plain_M | INFO_PLA_TMA_001 | +10 |
T_Sea_L | INFO_SEA_TLA_001 | +30 |
T_Mount_M | INFO_MOU_TMC_002 | +10 |
T_Desert_S | INFO_DES_TS_002 | +10 |
T_Desert_L | INFO_DES_TLA_004 | +10 |
T_River_M | INFO_RIV_TMB_004 | +10 |
T_Cliff_L | INFO_CLI_TLA_003 | +10 |
ProperSteal
The item definitions (Content/Item/Database/ItemDB, struct ItemData) contain fields such as ItemID, BuyPrice, SellPrice, category and attributes, but no steal-difficulty field.
For example, here’s the relevant part of the ItemData record for ITM_061 (Magic Nut (M)):
| field | value |
|---|---|
ItemID | 73 |
ItemNameID | TX_NA_ITM_061 |
DetailTextID | TX_CO_ITM_061 |
BuyPrice | 2,400 |
SellPrice | 240 |
I’ve omitted the other fields, which cover things like item categories, targeting, attributes, resistances and icons.
Instead, steal difficulty (ProperSteal) is defined per entry in Content/Shop/Database/PurchaseItemTable.
Its row struct, PurchaseItemInfoData, is the same struct used by the steal code above.
The clearest example I found is the Magic Nut (M)
. Both rows are from PurchaseItemTable:
| row | ItemLabel | Town* | NPC* | FCPrice | ProperSteal | Success rate* |
|---|---|---|---|---|---|---|
ForestL_SHOP_FC_18_3 | ITM_061 | Victors Hollow | Townsperson near the entrance | 2,280 | 20 | 100% |
DesertL_SHOP_FC_25_2 | ITM_061 | Marsalim | Know-it-all’s Father | 2,880 | 44 | 3% |
The Town, NPC and Success rate columns are my annotations for convenience; the original data identifies these entries by their row names. ForestL and DesertL correspond to
Victors Hollow
and
Marsalim
, respectively.
Same item ID, different price and steal difficulty. With my Therion at level 32, the two ProperSteal values produce success rates of 100% and 3%, respectively.

GameParamDefineTable
Content/GameParam/GameParamDefineTable is an Unreal Engine UDataTable with one row per parameter key. The row name is the key the Blueprint looks up, which is why parameter names such as STEAL_PROBABILITY_OVER appear as string literals in the bytecode.
The table contains various game settings, from backpack size and damage caps to encounter rates and UI timings. All entries share the same generic Min, Max, Init, and Param fields, even when a parameter doesn’t need all four.
The four steal-probability rows (86–89) use these fields as separate values:
| Row | Name | Type | Min | Max | Init | Param |
|---|---|---|---|---|---|---|
| 86 | STEAL_PROBABILITY_LESS_THAN | float | 3 | 8 | 15 | 55 |
| 87 | STEAL_PROBABILITY_OVER | float | 65 | 80 | 100 | 0 |
| 88 | STEAL_PROBABILITY_LEVEL_LESS_THAN | int | -11 | -4 | -1 | 0 |
| 89 | STEAL_PROBABILITY_LEVEL_OVER | int | 2 | 4 | 11 | 0 |
For example, STEAL_PROBABILITY_LESS_THAN stores the four lower-side probabilities as 3, 8, 15, 55. The corresponding STEAL_PROBABILITY_LEVEL_LESS_THAN row supplies the thresholds -11, -4, -1.
The code selects a field based on the level difference and the relevant threshold. On the high side, STEAL_PROBABILITY_OVER supplies the three probabilities 65, 80, 100, while STEAL_PROBABILITY_LEVEL_OVER supplies the thresholds 2, 4, 11.
This is why the parameter names alone are misleading: the probability buckets are spread across generic data fields, and the Blueprint determines which values to use.
RandomBoolWithWeight
The final step calls Unreal Engine’s
Random Bool with Weight
Blueprint node, exposed in C++ as UKismetMathLibrary::RandomBoolWithWeight.
1weight01 = FMin(weight * 0.01f, 1f);
2this.SuccessSteal = RandomBoolWithWeight(weight01);
The selected percentage is converted into a weight between 0 and 1. Like, a 55% success rate becomes 0.55. FMin caps the value at 1.0, ensuring it cannot exceed 100%.
The node then performs the weighted boolean roll, and its result is assigned directly to SuccessSteal.
I’m treating Unreal’s built-in random function as an ordinary, unbiased random roll here. I haven’t investigated its implementation, so I can’t guarantee that it has no bias or repeatable patterns. The UE source is available on GitHub, so that would make for a fun exploration, but maybe next time!
Putting it together
With the values shipped in the game:
FieldCommandLevel − ProperSteal | Chance | Slot used |
|---|---|---|
| ≥ 11 | 100% | STEAL_PROBABILITY_OVER → Init |
| ≥ 4 | 80% | STEAL_PROBABILITY_OVER → Max |
| ≥ 2 | 65% | STEAL_PROBABILITY_OVER → Min |
| 0 or 1 | 55% | STEAL_PROBABILITY_LESS_THAN → Param |
| ≤ −1 | 15% | STEAL_PROBABILITY_LESS_THAN → Init |
| ≤ −4 | 8% | STEAL_PROBABILITY_LESS_THAN → Max |
| ≤ −11 | 3% | STEAL_PROBABILITY_LESS_THAN → Min |
A simple example
Let’s say Therion is level 10 and has Thieving Tips & Tricks, which grants a +10 Steal bonus in the current town.
We’re trying to steal an item whose ProperSteal value is 10.
First, the game calculates Therion’s effective field-command level:
1FieldCommandLevel = Min(CharacterLevel + StealBonus, 99); // Min(10 + 10, 99) = 20
Then it subtracts the item’s steal difficulty:
1diff = FieldCommandLevel - ProperSteal; // 20 - 10 = 10
A difference of 10 falls into the diff >= 4 bucket, so the game selects STEAL_PROBABILITY_OVER.Max, which is 80%.
The resulting steal success rate is 80%.
Without Thieving Tips & Tricks, Therion’s effective level would be 10, giving a difference of 10 - 10 = 0. That would falls into the 55% bucket instead.
This might be kinda confusing, but the bonus doesn’t directly add 10 percentage points to the success rate. It increases Therion’s effective level, potentially moving him into a different probability bucket.
Conclusion
Well, that concludes my little investigation.
Looks like the game was being honest after all.
Thanks for reading! Now, if you’ll excuse me, I’m going back to save-scumming my way to OP items. :P