Last Epoch est un ARPG solo basé sur Unity et C #. Le jeu dispose d'un système d'artisanat - le joueur trouve des modificateurs, qu'il applique ensuite à l'équipement. Avec chaque modificateur, «l'instabilité» s'accumule, ce qui augmente les chances de casser l'objet
J'ai poursuivi deux objectifs:
- Supprimer la "casse" d'un élément suite à l'application de modificateurs
- N'utilisez pas de modificateurs lors de la fabrication
Voici à quoi ressemble la fenêtre de fabrication dans le jeu:
Première partie, où nous éditons le code .NET sans inscription ni SMS
Pour commencer, je décrirai le processus de modification de l'ancienne version du jeu (0.7.8)
C# IL (Intermediate Language) . IL- . Unity IL- <GameFolder>/Managed/Assembly-CSharp.dll
IL- dnSpy — .NET, . dnSpy .NET , IDE.
, dnSpy Assembly-CSharp.dll
. , — , Craft .
— CraftingSlotManager:
Forge() :
// CraftingSlotManager
// Token: 0x06002552 RID: 9554 RVA: 0x0015E958 File Offset: 0x0015CB58
public void Forge()
{
if (!this.forging)
{
this.forging = true;
base.StartCoroutine(this.ForgeBlocker(10));
bool flag = false;
int num = -1;
if (this.main.HasContent())
{
int num2 = 0;
int num3 = 0;
if (this.debugNoFracture)
{
num3 = -10;
}
float num4 = 1f;
int num5 = -1;
bool flag2 = false;
ItemData data = this.main.GetContent()[0].data;
ItemData itemData = null;
if (this.support.HasContent())
{
itemData = this.support.GetContent()[0].data;
num5 = (int)itemData.subType;
if (itemData.subType == 0)
{
num3--;
flag2 = true;
}
else if (itemData.subType == 1)
{
num4 = UnityEngine.Random.Range(0.4f, 1f);
flag2 = true;
}
}
if (this.appliedAffixID >= 0)
{
Debug.Log("applied ID: " + this.appliedAffixID.ToString());
if (this.forgeButtonText.text == "Forge")
{
if (data.AddAffixTier(this.appliedAffixID, Mathf.RoundToInt((float)(5 + num2) * num4), num3))
{
num = this.appliedAffixID;
flag = true;
}
GlobalDataTracker.instance.CheckForShard(this.appliedAffixID);
if (flag2)
{
this.support.Clear();
}
if (!GlobalDataTracker.instance.CheckForShard(this.appliedAffixID))
{
this.DeselectAffixID();
}
}
}
else if (this.modifier.HasContent())
{
Debug.Log("modifier lets go");
ItemData data2 = this.modifier.GetContent()[0].data;
if (data2.itemType == 102)
{
if (data2.subType == 0)
{
Debug.Log("shatter it");
Notifications.CraftingOutcome(data.Shatter());
if (num5 == 0)
{
flag2 = false;
}
this.main.Clear();
flag = true;
this.ResetAffixList();
}
else if (data2.subType == 1)
{
Debug.Log("refine it");
if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))
{
data.ReRollAffixRolls();
}
flag = true;
}
else if (data2.subType == 2 && data.affixes.Count > 0)
{
Debug.Log("remove it");
if (data.AddInstability(Mathf.RoundToInt((float)(2 + num2) * num4), num3, 0))
{
ItemAffix affixToRemove = data.affixes[UnityEngine.Random.Range(0, data.affixes.Count)];
data.RemoveAffix(affixToRemove);
}
flag = true;
}
else if (data2.subType == 3 && data.affixes.Count > 0)
{
Debug.Log("cleanse it");
List<ItemAffix> list = new List<ItemAffix>();
foreach (ItemAffix item in data.affixes)
{
list.Add(item);
}
foreach (ItemAffix affixToRemove2 in list)
{
data.RemoveAffix(affixToRemove2);
}
if (num5 == 0)
{
flag2 = false;
}
data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));
flag = true;
}
else if (data2.subType == 4 && data.sockets == 0)
{
Debug.Log("socket it");
data.AddSocket(1);
data.SetInstability((int)((Mathf.Clamp(UnityEngine.Random.Range(5f, 15f), 0f, (float)data.instability) + (float)num2) * num4));
flag = true;
}
}
}
if (flag)
{
UISounds.playSound(UISounds.UISoundLabel.CraftingSuccess);
if (this.modifier.HasContent())
{
ItemData data3 = this.modifier.GetContent()[0].data;
this.modifier.Clear();
if (num >= 0 && GlobalDataTracker.instance.CheckForShard(num))
{
this.PopShardToModifierSlot(num);
}
else if (data3.itemType == 102)
{
foreach (SingleSubTypeContainer singleSubTypeContainer in ItemContainersManager.instance.materials.Containers)
{
if (singleSubTypeContainer.CanAddItemType((int)data3.itemType) && singleSubTypeContainer.allowedSubID == (int)data3.subType && singleSubTypeContainer.HasContent())
{
singleSubTypeContainer.MoveItemTo(singleSubTypeContainer.GetContent()[0], 1, this.modifier, new IntVector2?(IntVector2.Zero), Context.SILENT);
break;
}
}
}
if (num >= 0 && this.prefixTierVFXObjects.Length != 0 && this.suffixTierVFXObjects.Length != 0)
{
ItemData itemData2 = null;
if (this.main.HasContent())
{
itemData2 = this.main.GetContent()[0].data;
}
if (itemData2 != null && this.main.HasContent())
{
List<ItemAffix> list2 = new List<ItemAffix>();
List<ItemAffix> list3 = new List<ItemAffix>();
foreach (ItemAffix itemAffix in itemData2.affixes)
{
if (itemAffix.affixType == AffixList.AffixType.PREFIX)
{
list2.Add(itemAffix);
}
else
{
list3.Add(itemAffix);
}
}
for (int i = 0; i < list2.Count; i++)
{
if ((int)list2[i].affixId == num && this.prefixTierVFXObjects[i])
{
this.prefixTierVFXObjects[i].SetActive(true);
}
}
for (int j = 0; j < list3.Count; j++)
{
if ((int)list3[j].affixId == num && this.suffixTierVFXObjects[j])
{
this.suffixTierVFXObjects[j].SetActive(true);
}
}
}
}
}
if (!flag2)
{
goto IL_6B3;
}
this.support.Clear();
using (List<SingleSubTypeContainer>.Enumerator enumerator2 = ItemContainersManager.instance.materials.Containers.GetEnumerator())
{
while (enumerator2.MoveNext())
{
SingleSubTypeContainer singleSubTypeContainer2 = enumerator2.Current;
if (singleSubTypeContainer2.CanAddItemType((int)itemData.itemType) && singleSubTypeContainer2.allowedSubID == (int)itemData.subType && singleSubTypeContainer2.HasContent())
{
singleSubTypeContainer2.MoveItemTo(singleSubTypeContainer2.GetContent()[0], 1, this.support, new IntVector2?(IntVector2.Zero), Context.SILENT);
break;
}
}
goto IL_6B3;
}
}
this.modifier.Clear();
this.support.Clear();
}
IL_6B3:
if (!flag)
{
UISounds.playSound(UISounds.UISoundLabel.CraftingFailure);
}
this.slamVFX.SetActive(true);
this.UpdateItemInfo();
this.UpdateFractureChanceDisplay();
this.UpdateForgeButton();
ShardCountText.UpdateAll();
}
}
. , ( num1, num2...). , .
— CraftingSlot
', . CraftingSlotManager
.
: this.modifier
this.support
, .
:
this.modifier.Clear();
this.support.Clear();
, ( , , ) — .
this.modifier.Clear();
this.support.Clear();
dnSpy — .dll — :
Fracture, :
— int num3 = -10;
— .
,
0.7.9 IL2CPP , . IL-, … ?
-, No-CD, OllyDbg 10 . , -
, .dll- GameAssembly.dll
55 . , .
dll- Ghidra' , ( Analyze Address Table
)
, , , — .
IL2CPP Il2CppDumper, - ( <GameFolder>/il2cpp_data/Metadata/global-metadata.dat
). , .
dll :
DummyDll dll- IL-. Assembly-CSharp.dll
dnSpy CraftingSlotManager
:
, , !
Address(RVA = "0x5B9FC0", Offset = "0x5B89C0", VA = "0x1805B9FC0")
VA — ce, :
, .
? , Il2CppDumper , — , ghidra.py
script.json
, . , , .
, this.modifier.Clear();
this.support.Clear();
. . .
— . , CALL NOP
( C, Clear Code Bytes), 90
. !
OneSlotItemContainer$$Clear()
Forge()
( , this.main.Clear();
, ).
int num3 = -10;
. — , ~60 , , . 15 , .
, ( 4 MOVZX AND), . , .
( ) dnSpy , "" AddInstability
public bool AddInstability(int addedInstability, int fractureTierModifier = 0, int affixTier = 0)
{
int num = this.RollFractureTier(fractureTierModifier, affixTier);
if (num > 0)
{
this.Fracture(num); // <-----
return false;
}
this.instability = ((int)this.instability + addedInstability).clampToByte();
this.RebuildID();
return true;
}
:
, CALL ItemData$$RollFractureTier
, TEST EAX
:
, uVar3 < 1
. — ( ) JG(Jump short if greater) JLE(Jump short if less or equal).
— . CALL
XOR EAX, EAX
( ), NOP'.
! , GameAssembly.dll
(- .bin ) .
" ", , .
En réalité, de nombreux langages populaires sont compilés en code intermédiaire, qui est parfaitement interprété et modifié par les décompilateurs de profil. Pour de telles modifications, les compétences de programmation habituelles sont souvent suffisantes.
Et alors que les binaires natifs peuvent être dangereux pour vos yeux et votre cerveau, une connaissance superficielle du fonctionnement des programmes à un niveau proche du matériel est souvent suffisante en conjonction avec des outils open source modernes pour de petites modifications.