Jump to content

Rewriting Hidden Potential - Tutorial


Zybertryx

Recommended Posts

Greets,

 

As I've finally learned how to make meaningful changes to game mechanics and functions beyond single byte replacements I'm going to fulfill a promise I made and lay out the process using a real function and a real change in a step-by-step manner. That said, I am a complete novice so there may be ways of doing things much faster or more efficiently that I've yet to make a habit of. It is not glamorous work.

 

Should note that I have absolutely no prior coding experience at all (and this will probably become quite clear as we move forward). Ironically, this is what the pros here have said qualifies me for writing this tutorial! :huh:

 

Six weeks ago I was using Toolboks and Modpatcher. Four weeks ago I was changing integers in the .upks with the help of UE Explorer and HxD. Two weeks ago I was asking questions like "How can I tell where the integer "1" is; I don't see any "2C 01" here?" And today I'm going to completely rewrite the Second Wave option "Hidden Potential".

 

First things first, and the hardest part can't be easily taught; knowing where a particular thing you want to change is handled in Vanilla. I suck at this part, so I fill my time by browsing through function after function in UE Explorer getting familiar with various functions and locations and often stumbling into "Ah Ha!" moments when I intuit that a particular function is handling a particular mechanic I'd like to see changed. That's how this Hidden Potential thing started, I just stumbled across it, stroked my chin and thought, "Yep, I'm doing it".

 

Looking at XGStrategyGame >>> XGStrategySoldier.LevelUpStats in UE Explorer I noticed it seemed to handle the "Hidden Potential" Second Wave option. (Yes, you reading now, ye interested and clueless parties, get UE Explorer, open up a decompressed "XGStrategyGame.upk", browse to the "XGStrategySoldier" function, expand it and click on the "LevelUpStats" section, have looks).

 

I don't use (or like) Hidden Potential because my mod is extremely anti-HP increases (excepting via equipment), very tight on Aim buffs, and very much dependent on the DGC values to make the classes distinct when leveling, but if I could tailor Hidden Potential for my mod then I would certainly use it, oh yes indeed - more variation in toons is always better - so long as it's sane.

 

So, with the suitable and necessary level of insatiable will in place, lets get to it.

 

---- Continues Below ----

Edited by Zybertryx
Link to comment
Share on other sites

Part 1: Familiarising Yourself with the Vanilla Function (or at least the section that is to be modified or replaced)

 

The first thing is to have a good look at the function currently, have a think about what you'd like to change and how it could be changed, and look for anything can be safely deleted or optimised (via rewrite) to make room (byte wise) for any larger replacement code (should your changes require it). Unfortunately, we are constrained by byte totals. Total byte size of the function must remain unchanged. Thankfully, we have a few tricks in our arsenal to aid with this. I'll explain these methods when we come to writing out our replacement Hex in a later update here.

 

I wondered how best to go through this step-by-step without resorting to You Tube videos, embedded screenshots full of MSpaint scrawl or sleep inducing line-by-line cut/paste commentaries (which due to my poor technical grasp I'd be ill suited to do anyway). I may end up using all of the above at different points in the tutorial but for now I'm going to do it like this:

 

XGStrategySoldier.LevelUpStats (Vanilla) - Beginning of function and first class specific level up entries only, for brevity. There are three more near identically formatted entries for the other three classes. My commentary is relevant for all four class sections - Comments Inside.

 

 

function LevelUpStats(optional out string statsString)
{
local int statOffense, statWill, statHealth, statMobility, iBaseWillIncrease, iRandWillIncrease;

local bool bRand;
local int iStatProgression;

bRand = IsOptionEnabled(3); <<<<<<<< This literally means, "Hidden Potential is ticked" "bRand" = "Hidden Potential." Keep this in mind.
iBaseWillIncrease = 2; <<<<<<<<<<< Mirrors the DGC.ini entries doesn't it? This is probably superseded by the DGC and not needed/read at all here. I'm leaving this alone though.
iRandWillIncrease = 6; <<<<<<<<<< As above
// End:0x6F
if(BARRACKS().HasOTSUpgrade(7)) <<<<<<<<< As Above
{
iBaseWillIncrease += 2; <<<<<<<<< As Above
iRandWillIncrease += 4; <<<<<<<<< As Above
}
statWill = iBaseWillIncrease + Rand(iRandWillIncrease); <<<<<<< Probably needed. I'm leaving all this first section alone anyway.
switch(m_kSoldier.kClass.eType) <<<<<<<< Notice this is prefaced with the word "switch". So the code from this point down will be interpreting a following case call as a Soldier's Class. Switch to Class.
{
// End:0x1F7
case 4: <<<<<<<<<<<<< Assault Class ("1" Is Sniper, "2" is Heavy, "3" is Support and "4" is Assault)
switch(m_kSoldier.iRank) <<<<< Switch case from Class to Rank but nested under the initial Class switch. So, the logic opens up saying "Switch to Assault Class context, then switch to the Rank within that context".
{
// End:0x106 <<<<<< these are jump token references, I'll get to them in more detail later, they determine where to go if the immediately following conditional is false or irrelevant. They are vitally important and highly irritating to deal with. But I've nailed them! Oh yes, the loathsome things.
case 1: <<<< Rank 1 (Squaddie). Remember the case was switched firstly to the Assault class, and then to the Solider's rank within that class. The next 7 cases are the 7 ranks (Squaddie to Colonel) as well as the Vanilla stat boosts Assault Class soldiers gain at each rank for games in which "Hidden Potential" is not ticked. This is dormant/leftover code, (due to the DGC) and is not actually read here.
statOffense = 2;
// End:0x1A2
break;
// End:0x125
case 2: <<<<< Rank 2
statOffense = 3;
statHealth = 1;
// End:0x1A2
break;
// End:0x139
case 3: <<<<< Rank 3
statOffense = 3;
// End:0x1A2
break;
// End:0x158
case 4: <<<<< Rank 4
statOffense = 3;
statHealth = 1; THESE RANK CASES ARE SUPERSEDED BY THE DGC ENTRIES AND

CAN AND WILL BE DELETED. I NEED THE BYTES TO ADD MY CUSTOM HIDDEN POTENTIAL CODE
// End:0x1A2
break;
// End:0x16C
case 5: <<<<< Rank 5
statOffense = 3;
// End:0x1A2
break;
// End:0x18B
case 6: <<<<< Rank 6
statOffense = 2;
statHealth = 1;
// End:0x1A2
break;
// End:0x19F
case 7: <<<<< Rank 7 (Colonel)
statOffense = 4;
// End:0x1A2
break; <<<<<<< Something important that I know very little about :smile:
// End:0xFFFF
default: <<<<<<< Something important that I know very little about :smile:
// End:0x1F4
if(bRand) <<<<<<<<< If(HiddenPotential) remember? This is the beginning of the Assault Class specific stat-boosts per rank when Second Wave "Hidden Potential" is enabled. For the Assault Class, "Hidden Potential" starts here.
{
statOffense = 1 + Rand(5); <<<<< Vanilla Hidden Potential Assaults get 1 + "up to 5" Aim per level with a . . . .
// End:0x1DA
if(Roll(50)) <<<<<<<<<<< 50% Chance of receiving. . . .
{
statHealth = 1; <<<<< +1 Hit Point, each level. Vanilla is bad, very bad. >.>
}
// End:0x1F4 <<<<< Important jump this one, as each preceding jump jumped to the next conditional in the "nest" but this one Jumps out of the nest as the Assault section ends after the "if(Roll(20))" immediately following. I'll get to this sort of thing as I go through the whole procedure of writing, splicing in my new code and fixing the jumps and header in following updates. A "conceptual road map" on how the code is read and why the jumps are so important will likely fill the plate of the next installment.
if(Roll(20)) <<<<< Also, each level, Assaults have a 20% chance. . . .
{
statMobility = 1; <<<<< to receive +1 mobility each level. . . . Vanilla; so very bad.
}
}
// End:0x598 <<<<< Ends Assault Specific Hidden Potential Rank Boosts.
break;

}
// End:0x327
case 1: <<<<<< Next 'parent' case (this is a CLASS case [it is nested under the initial " switch(m_kSoldier.kClass.eType)" at the top of the function and its own Ranks follow after the next switch]. "Case (class) 1" is "Sniper". The format largely mirrors the Assault class stuff above. It is followed similar Class cases (with their own switch-to-rank entries) for Heavy and Support (Case 2 and Case 3 respectively). All of this, for all three classes, is going to be deleted and rewritten, on a per class basis, for Hidden Potential alone. Non Hidden Potential values are handled in the DGC.ini. However, I will always have a copy of an unmodified .upk on hand as it will become vital for correcting our jumps and for ensuring we preserve particular conventions as we construct our code, even if we don't quite understand them.

 

 

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 2a: Fleshing out a Conceptual Replacement or So What Do You Actually Want It To Do?

 

For me, Vanilla Hidden Potential basically breaks the careful and thorough rebalance of the rest of my mod. My DGC entries for non Hidden Potential campaigns are as follows.

 

 

SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Squaddie, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Corporal, iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Sergeant, iHP=0, iAim=2, iWill=10, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Lieutenant,iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=1 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Captain, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Major, iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Support, eRank=eRank_Colonel, iHP=1, iAim=2, iWill=10, iDefense=0, iMobility=0 )

SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Squaddie, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=-1 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Corporal, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Sergeant, iHP=0, iAim=2, iWill=10, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Lieutenant,iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=1 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Captain, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Major, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_HeavyWeapons, eRank=eRank_Colonel, iHP=1, iAim=2, iWill=10, iDefense=0, iMobility=0 )

SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Squaddie, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Corporal, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Sergeant, iHP=0, iAim=2, iWill=10, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Lieutenant,iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=1 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Captain, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Major, iHP=0, iAim=2, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Assault, eRank=eRank_Colonel, iHP=1, iAim=2, iWill=10, iDefense=0, iMobility=0 )

SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Squaddie, iHP=0, iAim=3,iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Corporal, iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Sergeant, iHP=0, iAim=3, iWill=10, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Lieutenant,iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=1 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Captain, iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Major, iHP=0, iAim=3, iWill=3, iDefense=0, iMobility=0 )
SoldierStatProgression=(eClass=eSC_Sniper, eRank=eRank_Colonel, iHP=1, iAim=3,iWill=10, iDefense=0, iMobility=0 )

 

 

As you can see, compared to Vanilla this is all been ruthlessly squeezed. I also radically lowered the starting will for Rookies so that they were prone to panicking when their squadmates were killed or they themselves were seriously wounded (Game over, man! Game over!!). The +10 Will buff at "Sergeant" and later at "Colonel" have a clear effect on modifying their behaviour compared to the lower ranks.

 

Anyway, needless to say, Hidden Potential with all its ridiculous mobility, hit point and aim bonuses was until now not really an option for my mod.

 

Going back to Hidden Potential in the Vanilla function I linked previously, we can isolate the Vanilla mechanic (for Assaults, as example) like this:

 

 

if(bRand)
{
statOffense = 1 + Rand(5);
// End:0x1DA
if(Roll(50))
{
statHealth = 1;
}
// End:0x1F4
if(Roll(20))
{
statMobility = 1;
}
}
// End:0x598
break;

 

Yup, that's it (I trust you can kinda "Read it" now too).

 

So to make it more conducive for my rebalance, I'd like it to look like this:

 

 

case 4: <<<<< Assaults
if(bRand) <<<<< Hidden Potential
{
statOffense = 1 + Rand(1); <<<<< 1 to 2 Aim per level (Note this is what will happen at every level, it is not prefixed with an "if" token; it's a given), with a . . .
if(Roll(40)) <<<<< 40% Chance of an additional . . .
{
statOffense = 1; <<<<<< +1 Aim, and if they are . . .

}
if(m_kSoldier.iRank == 4) <<<<< Rank 4, then I want them to gain . . .
{
statMobility = 1; <<<<< +1 Mobility (note this is not randomised but 'compulsory' despite "Hidden Potential") =) And . . .
}
if(m_kSoldier.iRank == 7) <<<<< If they are rank 7, then I want them to gain . . .
{
statHealth = 1; <<<<< +1 Hit Point too
}
}
break;

 

 

I think I might have to add Rank specific Will entries to mirror (though more randomly) my non Hidden Potential balance settings.

 

Another sequential conditional like

 

 

 

if(m_kSoldier.iRank == 3)

{
statWill = 5 + Rand(5);
}

 

 

 

Well, that's all well and good Z, and cheers, it's starting to make sense, but how do we actually go about turning the Vanilla into the custom?

 

I'm glad you asked. We use the ludicrously awesome Token view in UE Explorer 1.3 beta or the mind numbingly awkward Buffer view of an earlier version (and then break it all down manually. . . gah).

 

To access these views, find the function in UE Explorer and right click it from the list, select "Object", "Token" or "Buffer (which is HEX)".

 

All three views will be vital for when we really get stuck into it although UE Explorer 1.3's token view pretty much contains all the data of all three. "Object" view is the default view (the decompiled code like these spoilers).

 

Token view is the crucial view for viewing call locations and jumps and Buffer view contains the raw Hex (with some great dynamic mouse over tool tips as well).

 

More on this in the next section.

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

A wondrous creature flew to my window and whispered unto me a great insight.

 

This:

 

statOffense = 1 + Rand(1);

if(Roll(40)) <<<<< 40% Chance of an additional . . .
{
statOffense = 1; <<<<<< +1 Aim, and if they are . . .

}

 

Actually says that there's a 40% chance that there will be only +1 Aim gained (overuling the previous statOffense = 1 + Rand(1);) section. That is to say, that like this there won't be an additional +1 gained if the roll is successful (which is my intention).

 

To achieve that the code would need to swap the "=" for a "+=".

 

The next section of this tutorial will get straight into the grit; Hex, syntax and subtle nuances like this.

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 2b: Peeking Under The Hood or WTF Is All This HEX About?!

 

Following on from Part 2, and specifically addressing how it is we can go about turning our concept into a reality, it is necessary to firstly understand that each line in the decompiled code corresponds exactly to particular lines of hex and that it is the jump tokens alone that account for the correct formatting (the 'tabs', 'nests', 'bracks' and so on). Jump tokens will be handled in a subsequent post.

 

Using UE Explorer 1.2.3 beta's Token View by right-clicking the function "LevelUpStats" and selecting it from the list (Under the parent XGStrategySolder function) results in a treasure trove of information. Those of you using a previous version should seriously consider upgrading as although I actually got this mod happening using 1.2.2 It was somewhat tortuous.

 

UE 1.2.3's token View reveals the entire function (including the decompiled lines, the token/jumps and the HEX) like this:

 

 

(000/000) [0B]
N(1/1)

(001/001) [14 2D 00 23 45 00 00 1B A7 14 00 00 00 00 00 00 24 03 16]
LB(23/19) -> BV(10/6) -> LV(9/5) -> VF(12/12) -> BC(2/2) -> EFP(1/1)
bRand = IsOptionEnabled(3)

(018/014) [0F 00 25 45 00 00 2C 02]
L(12/8) -> LV(9/5) -> ICB(2/2)
iBaseWillIncrease = 2

(024/01C) [0F 00 24 45 00 00 2C 06]
L(12/8) -> LV(9/5) -> ICB(2/2)
iRandWillIncrease = 6

(030/024) [07 6F 00 19 1B F4 02 00 00 00 00 00 00 16 0C 00 54 28 00 00 00 1B 17 11 00 00 00 00 00 00 24 07 16]
JIN(37/33) -> C(34/30) -> VF(10/10) -> EFP(1/1) -> VF(12/12) -> BC(2/2) -> EFP(1/1)
if(BARRACKS().HasOTSUpgrade(7))

(055/045) [A1 00 25 45 00 00 2C 02 16]
NF(13/9) -> LV(9/5) -> ICB(2/2) -> EFP(1/1)
iBaseWillIncrease += 2

(062/04E) [A1 00 24 45 00 00 2C 04 16]
NF(13/9) -> LV(9/5) -> ICB(2/2) -> EFP(1/1)
iRandWillIncrease += 4

(06F/057) [0F 00 28 45 00 00 92 00 25 45 00 00 A7 00 24 45 00 00 16 16]
L(32/20) -> LV(9/5) -> NF(22/14) -> LV(9/5) -> NF(11/7) -> LV(9/5) -> EFP(1/1) -> EFP(1/1)
statWill = iBaseWillIncrease + Rand(iRandWillIncrease)

(08F/06B) [05 30 FF FF FF 00 35 30 FF FF FF 7D FA FF FF 00 00 35 B4 F9 FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(57/33) -> SM(47/27) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.kClass.eType)

(0C8/08C) [0A F7 01 24 04]
C(5/5) -> BC(2/2)
case 4:

(0CD/091) [05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(38/22) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.iRank)

(0F3/0A7) [0A 06 01 26]
C(4/4) -> IO(1/1)
case 1:

(0F7/0AB) [0F 00 29 45 00 00 2C 02]
L(12/8) -> LV(9/5) -> ICB(2/2)
statOffense = 2

(103/0B3) [06 A2 01]
J(3/3)
goto J0x1A2

(106/0B6) [0A 25 01 2C 02]
C(5/5) -> ICB(2/2)
case 2:

(10B/0BB) [0F 00 29 45 00 00 2C 03]
L(12/8) -> LV(9/5) -> ICB(2/2)
statOffense = 3

 

Continues for an entire line by line breakdown of the complete function.

 

*Wipes spittle off lip*

 

 

 

 

You need to really look at this yourself in some detail (I highly recommend cut/pasting it to a fresh .txt) but if it feels like a bit of a bludgeon at the moment, let's start by breaking a couple of small examples down. I'll take a line at random:

 

 

 

 

(10B/0BB) [0F 00 29 45 00 00 2C 03]
L(12/8) -> LV(9/5) -> ICB(2/2)
statOffense = 3

 

 

 

Let's take a closer look at the call/phrase/line (the decompile friendly part) and compare it with the Hex. I'm going to colour code it so that we can more easily get savvy to the syntax.

 

 

 

"statOffense = 3"

 

=

 

"0F 00 29 45 00 00 2C 03"

 

 

 

 

The "statOffense" part is the five byte "00 29 45 00 00" section (the "local variable")

 

"0F" is the "=" and must precede the call (so to write "blahblah =" Sane-side you need to write "=blahblah" Hex-side).

 

The "2C" designates that the next byte is to be an Integer (in hexadecimal). So "2C 03" is "3".

 

You'll find that the composition of other local variables (and their contexts) are similar:

 

 

 

 

"statHealth = 1"

 

=

 

"0F 00 27 45 00 00 26"

 

 

Actually this last one is a great example of something I had trouble with initially.

 

Notice how here the Integer "1" is not defined by the "2C 01" convention but is written as a standalone "26". For arcane reasons (probably due to the compiler, as this way requires one less byte) a "26" like this (when it corresponds to an Integer "1" in the decompiled code) is how you write the Integer "1". The Integer "0" can also be written in one byte by using "25".

 

(Integer) "26" = "1"

 

(Integer) "25" = "0"

 

For modders, values rendered like this are a pain and, if you have the bytes to spare when writing your own mods, you should always preference the "2C ##" convention, as to change single byte integers to values larger than "1" requires an extra byte (to effect the 2C ## convention). For the purposes of my rewrite here I will use the "2C 01" or "2C 00" convention for "1" and "0" (and all other sub 255 integers) for the ease of later moddability should a new kind of balance tickle my fancy; for future proofing. I'll be able to get away with this across the board as I'll be deleting a massive chunk of hex and likely not at all filling it all back up with active code.

 

Indecently, and I'm sure its the same for those of you who are soaking this up, the first thing I was doing Hex-side was sifting through the .Upks looking for juicy "2C ##"s I could edit pretty easily (after first identifying the relevant locations using UE Explorers Object View; Don't go in blind!)

 

After a while I began to view the .Upks as 'extended inis' - and you can get quite a bit done just by revaluing the integers like this.

 

I'm going to continue on assuming the reader is acquainted with some of these fundamentals. I'll mention others as they come up. The next section will deal with working out what to delete from the original function which opened this Tutorial following on with how, using the breakdown via Token (or Manual Buffer) View, we can begin to piece together a new hex chunk which will decompile in accordance with the conceptual outline described in Part 2a.

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 3a: Let's Organise Ourselves.

 

The next step is working out what we're going to remove to make room for our changes and what we need to keep to not break anything.

 

It's probably a good idea to go to UE Explorer's "Buffer View" at this point. Select the tab "Edit" from the Buffer View menu and then select the option "Dump Bytes." This copies the Hex for this entire function to your clipboard, where you can easily paste it to a clean .txt (notepad).

 

Switching back to UE Explorer's Token View, at the position 0x08F we can see the first switch to the Class case:

 

 

(08F/06B) [05 30 FF FF FF 00 35 30 FF FF FF 7D FA FF FF 00 00 35 B4 F9 FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(57/33) -> SM(47/27) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.kClass.eType)

 

 

 

The highlighted Aqua (08F) is the position of this line. Paying attention to positions is not all that important at this stage but it will become much more important after we have spliced in our replacement code.

 

Everything above this line we are going to keep unchanged. So highlight and copy the relevant hex for this line and then do a search from the top of your "Dumped Bytes" in your .txt. When you've found it, separate it (and everything preceding it) from everything immediately following it.

 

You should have divided the entire function hex block into two pieces now; a smallish piece which ends with "(continuing to) EC 44 00 00" and a much larger piece which starts with "0A F7 01 24 (continuing on)".

 

Now, that we have 'protected' the upper part of the function, lets isolate the lower part of the function we also wish to preserve.

 

Back into token view, we want to scroll right down to the entry at position 0x598 because that's where all the superfluous Vanilla stuff and the nasty Vanilla Hidden Potential stuff ends.

 

 

(598/434) [07 6A 08 81 2D 00 23 45 00 00 16]
JIN(15/11) -> NF(12/8) -> BV(10/6) -> LV(9/5) -> EFP(1/1)
if(!bRand)

 

 

Note the exclamation mark in "If(!bRand)" is a "NOT" flag/token. I'll explain this in a little more detail later, but for now it will help to know that this exclamation mark is changing the meaning of this 'phrase' to "If(NOTHiddenPotential)then. . "

 

The stuff below that point is the all the stuff that actually is used when Hidden Potential is not enabled (it refers to the DGC.ini). We want to keep "If(!bRand)" itself and everything after it.

 

So, copy the relevant bytes for "If(!bRand)" - 07 6A 08 81 2D 00 23 45 00 00 16

 

Do a "Find" in your txt for it (it is a unique instance in this function - there's only one) and then, once found, separate the beginning of it and everything after it from the rest of the dumped Hex.

 

You should now have separated the "dumped bytes" into three distinct parts.

 

1) A shortish top part which contains the beginning of the function and ends with the first Class call - 05 30 FF FF FF 00 35 30 FF FF FF 7D FA FF FF 00 00 35 B4 F9 FF FF 74 FA FF FF 00 00 01 EC 44 00 00

 

2) The largest chunk which starts with 0A F7 01 24 04 05 73 FB FF FF (continuing until . . .)

 

3) Another largish chunk which starts with 07 6A 08 81 2D 00 23 45 00 00 16 (continuing to the end)

 

The largest chunk is the part of the function concerned with the unreferenced Vanilla DGC values as well as Vanilla's actually-referenced "Hidden Potential" mechanics (which we intend to completely customise).

 

Don't delete it from your .txt though! It's important to hold onto as when using "word wrap" in Notepad it can help us to ensure that our replacement code will be exactly the same size.

 

It might be best to name and save your separated "dumped bytes" .txt now. I'd also name the chunks; "Header" for the smallish first chunk (though it's more than just that), "Remove" for the large middle chunk, and "Keep" for the good sized lower chunk (or whatever's good for you).

 

Next, we'll nail down a working road map so we'll be able to work out the structure our new code should take. Following on from the road map of the next section, we'll lay out how we want our new mechanic to work, hex-line for hex-line, and I'll stress the importance of sticking to Vanilla's conventions as we do so (especially if we barely understand them).

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 3b: Road Mapping.

 

Now that we've separated the original hex into three parts (two we are going to keep and one part we are going to replace), we need to start getting an idea of how our replacement code should look, or how it should 'fit'.

 

For me, based on my DGC.ini rank settings, my plans for Hidden Potential are a little more complex than vanilla. That's fine, though one problem with increased complexity in refactoring is that it compounds that the amount of stitching required after the splice.

 

For now lets march ahead. Based on our Token view of the original function we can see the breakdown of the different calls and we can use these to create a new sequence more tailored to our particular needs.

 

Before we get too carried away though, lets take a good look at how Vanilla tied it all together as this will save us a world of pain later on if we build it into our code now. This is perhaps the most important part of the whole process, understanding how we can make our code work, even if we don't understand much about the particulars.

 

 

(08F/06B) [05 30 FF FF FF 00 35 30 FF FF FF 7D FA FF FF 00 00 35 B4 F9 FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(57/33) -> SM(47/27) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.kClass.eType)

(0C8/08C) [0A F7 01 24 04]
C(5/5) -> BC(2/2)
case 4:

(0CD/091) [05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(38/22) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.iRank)

 

 

 

These three entries offer us a bit of a road map on how to begin our custom code. Note: If you recall, the first line, - switch(m_kSoldier.kClass.eType) - should be the final line of hex in the "Header" section of our saved .txt. which we're keeping.

 

Check out the sequence here, It goes:

 

 

 

Switch (to Class) - we have already preserved this at the rear end of our saved "Header" chunk. This is the parent call for all the classes that follow. Only one of these calls is needed, the other classes and their ranks are all nested under this 'parent' call. I just added this here for clarity.

 

Case (a Class)

 

Switch (to Rank)

 

 

And. . . if we scroll down in our Token views to the end of the seven Vanilla rank cases (case 1 to case 7, which we don't want and won't keep) we see this sequence:

 

 

(19F/12B) [0A FF FF]
C(3/3)
default:

(1A2/12E) [07 F4 01 2D 00 23 45 00 00]
JIN(13/9) -> BV(10/6) -> LV(9/5)
if(bRand)

(1AF/137) [0F 00 29 45 00 00 92 26 A7 2C 05 16 16]
L(17/13) -> LV(9/5) -> NF(7/7) -> IO(1/1) -> NF(4/4) -> ICB(2/2) -> EFP(1/1) -> EFP(1/1)
statOffense = 1 + Rand(5)

 

 

 

Putting these two sequences together gives us:

 

 

 

Case (a specific Class)

 

Switch (to Rank generally)

 

default: (off to the DGC)

 

if(bRand) (Hidden Potential)

 

statOffense = 1 + Rand(5) <<<<< This is the first part of the fun bit - deciding what bonuses each class gains when Hidden Potential (i(bRand) is enabled.

 

We can and will (no doubt) add further tailoring after the statOffense entry for each class, perhaps giving certain class unique bonuses a specific ranks. But we will be preserving this sequence for each class we modify.

 

 

Finally we need to look at how Vanilla 'closes' each specific Class entry, so we scroll down under the last of the level boosts for the first class to position 14F - (1F4/170) - in our Token view, and we see:

 

 

(1F4/170) [06 98 05]
J(3/3)
goto J0x598

(1F7/173) [0A 27 03 24 01]
C(5/5) -> BC(2/2)
case 1:

(1FC/178) [05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(38/22) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.iRank)

 

 

 

Cleaning this up conceptually we see:

 

 

 

Goto (uncondtional jump)

 

Case (the next Class)

 

Switch (to Rank generally (for this class)

 

 

 

And if we scroll down further we will see the beginning of the if(bRand) section of this new class preceded as before with a "default:" entry like this:

 

 

 

 

(2CE/212) [0A FF FF]
C(3/3)
default:

(2D1/215) [07 24 03 2D 00 23 45 00 00]
JIN(13/9) -> BV(10/6) -> LV(9/5)
if(bRand)

(2DE/21E) [0F 00 29 45 00 00 92 2C 03 A7 2C 07 16 16]
L(18/14) -> LV(9/5) -> NF(8/8) -> ICB(2/2) -> NF(4/4) -> ICB(2/2) -> EFP(1/1) -> EFP(1/1)
statOffense = 3 + Rand(7)

 

 

 

 

And if we put all of this together we see:

 

 

 

 

Case (a specific Class) Let's call this Sniper

 

Switch (to Rank generally)

 

default: (off to the DGC)

 

if(bRand) (Hidden Potential)

 

statOffense = 1 + Rand(5) <<<<< This is the first part of the fun bit - deciding what bonuses each class gains when Hidden Potential - if(bRand) - is enabled.

 

stat???? = ?? (this is up to us).

 

stat???? = ?? (this is up to us).

 

Goto (unconditional jump - last entry for each class before the next class)

 

Case (to the next Class) Heavy? (we can decide on the order)

 

Switch (to Rank generally (for this new class)

 

default: (off to the DGC)

 

if(bRand) (Hidden Potential entries for this new class)

 

statOffense = 3 + Rand(7) <<<< Specific Hidden Potential level boosts for this class begin here.

 

stat???? = ?? (this will be up to us).

 

stat???? = ?? (this will be up to us).

 

Goto (unconditional jump - last entry for each class before the next class)

 

Case (to the next Class) Support

 

Switch (to Rank generally (for this new class)

 

default: (off to the DGC)

 

if(bRand) (Hidden Potential entries for this new class)

 

Continuing this pattern. . .

 

 

 

Continues until we have four cases (one for each class) with each ending in an 06 ## ## "goto" unconditional jump.

 

 

Now that we've have a clear 'road map' on how the vanilla code tied the "Remove" part (which we are about to completely replace) into the "Header" and "Keep" parts (which we've separated in our .txt.), we can at last begin construction of our new code in confidence.

 

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 4a: Constructing The Replacement Code

 

At this point we can start constructing the replacement code.

 

Using Token View we can easily isolate and grab the various parts we need, and using some inference, Buffer View, and by generally paying attention to things, we can use these pieces to construct new 'phrases' we'd like too.

 

I've decided I'm going to order the classes as Sniper, Heavy, Support and Assault as each of these corresponds to the case call "1", "2", "3" and "4" respectively.

 

So, based on the road map of the previous section, lets start constructing our new code for the Sniper class.

 

If you recall, the necessary sequence for a class specific Hidden Potential code was:

 

 

 

 

Case (a specific Class) Let's call this Sniper

 

Switch (to Rank generally)

 

default: (off to the DGC)

 

if(bRand) (Hidden Potential)

 

statOffense = 1 + Rand(5) <<<<< This is the first part of the fun bit - deciding what bonuses each class gains when Hidden Potential - if(bRand) - is enabled.

 

Goto (unconditional jump - last entry for each class before the next class)

 

 

 

 

Working from that model, I've grabbed a bunch of entries from the token view in accordance with the above sequence:

 

 

 

 

(0C8/08C) [0A F7 01 24 04]
C(5/5) -> BC(2/2)
case 4:

 

(0CD/091) [05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(38/22) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.iRank)

 

(19F/12B) [0A FF FF]
C(3/3)
default:

 

(1A2/12E) [07 F4 01 2D 00 23 45 00 00]
JIN(13/9) -> BV(10/6) -> LV(9/5)
if(bRand)

(1AF/137) [0F 00 29 45 00 00 92 26 A7 2C 05 16 16]
L(17/13) -> LV(9/5) -> NF(7/7) -> IO(1/1) -> NF(4/4) -> ICB(2/2) -> EFP(1/1) -> EFP(1/1)
statOffense = 1 + Rand(5)

 

(1F4/170) [06 98 05]
J(3/3)
goto J0x598

 

 

 

 

---------------------------------------

 

It's important to note that with crude grabs like this, some of the information taken will be problematic for us when we splice it in. This is par for the course when modifying a large section like this though. In particular the jump references for Vanilla are specific to the precise locations of everything else in the Vanilla function. Our reworking/rewriting of things is going to throw all that way off. That's okay and it can (and must) be fixed up later (though that part is not fun).

 

If you've had a bit of look at Token View in your own time you've probably noticed that all "if" statements begin with an "07" and then two more bytes "F4 01" (an example). It's the 07 # # that constructs the "if". Technically these are "jump-if-not" Tokens. That is to say that, if the following conditional is false then jump away (to the position defined by the subsequent pair of bytes). These bytes are "Little Endian" and you'll need to take that into account when working with them (which we won't be dong in this section but as they're so important and really need understanding I thought I'd write a little primer for them here). What "little Endian" means (at least to me, in practicality) is that you write them in reverse order. So "F4 01" points to the position "1F4".

 

Similarly, "07 3D 04" would point to the position "43D".

 

This is the same for unconditional jumps too, which are all "06 ## ##" in style.

 

So. "07 9C 02" = Jump-to-29C-if-following-is-not-so.

 

And, "06 53 01" = Jump-to-153-unconditionally.

---------------------------------------

 

You can change the 'meaning' of a conditional (turn them into "Is Not" statements, rather than "Is" statements) by adding an "81" immediately following the jump token and prior to actual variable. The "81" token requires its own 'executor' - a "16" - which should be placed at the end of the completed 'hexphrase' (even if for other reasons a "16" is already present).

 

Example:

 

 

 

 

Turning

 

if(bRand)

 

( If(HiddenPotential) )

 

Into

 

if(!bRand)

 

( If(NOTHiddenPotential) )

 

Like so,

if(bRand) = 07 F4 01 2D 00 23 45 00 00

 

if(!bRand) = 07 F4 01 81 2D 00 23 45 00 00 16

 

 

 

 

That was just an example and we won't be molesting our if(bRand) statements at all but we will be modifying a lot of our 'grabs' to suit our intended functionality and values. On that note, let's continue.

 

I grabbed (and pasted into a fresh.txt):

 

 

 

(0C8/08C) [0A F7 01 24 04]
C(5/5) -> BC(2/2)
case 4:

 

(0CD/091) [05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00]
S(38/22) -> SM(28/16) -> IV(9/5)
switch(m_kSoldier.iRank)

 

(19F/12B) [0A FF FF]
C(3/3)
default:

 

(1A2/12E) [07 F4 01 2D 00 23 45 00 00]
JIN(13/9) -> BV(10/6) -> LV(9/5)
if(bRand)

(1AF/137) [0F 00 29 45 00 00 92 26 A7 2C 05 16 16]
L(17/13) -> LV(9/5) -> NF(7/7) -> IO(1/1) -> NF(4/4) -> ICB(2/2) -> EFP(1/1) -> EFP(1/1)
statOffense = 1 + Rand(5)

 

(1F4/170) [06 98 05]
J(3/3)
goto J0x598

 

 

But some of that should be changed right now. Firstly, the "case 4" entry should be changed to "case 1" (from Assault, to Sniper). This is easily accomplished, a simple thing.

 

 

 

case 4:

0A F7 01 24 04

 

to

 

case 1:

0A F7 01 24 01

 

Note, that in this instance the integer defining byte is not a "2C" but a "24" instead. Often either will work but there are some subtleties involved under certain conditions that may result in crashes, so whenever you encounter this style convention its best to stick to Vanilla's lead.

 

 

 

The other part from this initial grab I'd like to change is the "statOffense = 1 + Rand(5)" line. That's far too generous, even for Snipers, for my taste, so I'm going to change it to "statOffense = 1 + Rand(2)"

 

Like so:

 

 


statOffense = 1 + Rand(5)

0F 00 29 45 00 00 92 26 A7 2C 05 16 16

 

to

 

statOffense = 1 + Rand(2)

0F 00 29 45 00 00 92 2C 01 A7 2C 02 16 16

 

Note, I changed the single byte integer "26" (which is "1") to the more easily modifiable "2C 01" convention.

 

 

 

Next up I'm going to grab one of those cool "X% chance of getting statboost X" lines and paste it in my .txt right after the just modified "statOffense = 1 + Rand(2)" line and before the "goto J0x598" jump. Then I'm going to grab another "StatOffense" and marry it to a "+=" to give significance to the roll. I'll clean up all the Token View clutter too so that's its nice and legible (and ready for an eventual stringing together of just the Hex components).

 

 

 

0A F7 01 24 01

case 1:

 

05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00
switch(m_kSoldier.iRank)

0A FF FF
default:

 

07 F4 01 2D 00 23 45 00 00
if(bRand)

0F 00 29 45 00 00 92 2C 01 A7 2C 02 16 16
statOffense = 1 + Rand(2)

 

07 DA 01 1B 37 23 00 00 00 00 00 00 2C 32 16
if(Roll(50))

 

A1 00 29 45 00 00 2C 01 16

statOffense += 1

 

06 98 05
goto J0x598

 

 

 

Now, we do have limits to what we can add for each entry byte-wise, but as I've run through this mod previously I know you can get away with three additional (and simple) sequential entries per class without any trouble. I'm going to go and have a think about what I really want Hidden Potential to do for my classes long term, and map out each class on my .txt., sequentially adding the buffs and values I'm after and in an order which will match the conceptual road map of Part 3. I'll post the results and discuss any lines I had to create that haven't been covered already, in the following installment.

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

Part 4b: The Completed Code

 

So, after a bit of deliberation, consternation and procrastination, I've finally nailed down exactly how I want Hidden Potential to work for each of the four classes. Although this is tailored explicitly for my own balance I'm hoping that this can act as a template that anyone can modify to suit their own tastes. There's also much more content than the original entries contained and that was a slight concern due to the byte limits which constrain us. After careful investigation it appears it will all fit. It's really very fortunate that this function happened to contain so much dormant garbage.

 

First up, the Sniper class. It largely mirrors my non-Hidden Potential DGC settings (offering only a modest amount of randomisation against that measure) but it will suit me fine.

 

By using a combination of Token and Buffer views on the existing Vanilla function I've been able to piece together exactly the kind of 'phrases' I need to produce the intended results.

 

The first part of my working .txt now looks like this:

 

 

 

0A D9 01 24 01
case 1: (Sniper)

05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00
switch(m_kSoldier.iRank)

0A FF FF
default:

07 D6 01 2D 00 23 45 00 00
if(bRand)

0F 00 29 45 00 00 92 2C 03 A7 2C 01 16 16
statOffense = 3 + Rand(1)

07 31 01 1B 37 23 00 00 00 00 00 00 2C 32 16
if(Roll(50))

A1 00 29 45 00 00 2C 01 16
statOffense += 1

07 66 01 9A 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00 2C 03 16
if(m_kSoldier.iRank == 3)

0F 00 28 45 00 00 92 2C 05 A7 2C 0A 16 16
statWill = 5 + Rand(10)

07 95 01 9A 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00 2C 04 16
if(m_kSoldier.iRank == 4)

0F 00 26 45 00 00 2C 01
statMobility = 1

07 D6 01 9A 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00 2C 07 16
if(m_kSoldier.iRank == 7)

0F 00 28 45 00 00 92 2C 05 A7 2C 0A 16 16
statWill = 5 + Rand(10)

 

0F 00 27 45 00 00 2C 01
statHealth = 1

 

06 C0 05
goto J0x5C0

 

0A 1A 03 24 02
case 2: (Heavy)

05 73 FB FF FF 00 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00
switch(m_kSoldier.iRank)

 

 

 

This continues on for the other three classes each with their own flavour and values.

 

For example, I've given the Heavy an additional unique entry, to ensure they are immediately saddled with a -1 Mobility hit. This was done to add a little flavour to the Rocket Launcher.

 

 

 

07 72 02 9A 35 73 FB FF FF 74 FA FF FF 00 00 01 EC 44 00 00 2C 01 16
if(m_kSoldier.iRank == 1)

A2 00 26 45 00 00 2C 01 16
statMobility -= 1

 

 

 

A robust list of various Hex tokens and their functionality can be found on the Nexus Wiki, here:

 

http://wiki.tesnexus.com/index.php/Hex_values_XCOM_Modding

 

Note, that there are sometimes different ways to write the same thing but that some functions or calls prefer one way to the other. If in doubt have a look for the token in the other sub-functions of the parent function in UE Explorer. If you find it used a certain way there, it's probably best to use it that way yourself. Also note that many of these require their own "16" executor. The mouse over tool tips of UE Explorer's Buffer View can really help to determine what kind of token is appropriate and which require their own executors.

 

With the complete line-by-line breakdown of my custom hex finished, it's time to prepare it all for a find/replace splice into the Vanilla function. Before we do that though, we need to ensure that our new code is the same byte size as the section we are replacing. If you're over that limit, then its hopeless; the original byte size of the function cannot be exceeded. But if you're under that limit, and we are, then we can 'puff up' our code to the necessary size with use of Null Operators; dormant "filler" code that will ensure our rewrite contains the same number of bytes as the code it replaces, but which will be totally benign and ignored by the engine.

 

More on this in the next section.

 

--------- Continues Below ---------

Edited by Zybertryx
Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...