Modding a Fallout: New Vegas Mod
Table of Contents
I played Fallout: New Vegas (FNV) twice before, but when I was young and simple, and I never thoroughly experience all the content. Recently, I finished the Fallout TV series season 2, my interest for the game was reignited by the slow motion scene where a bullet flying out from a rifle fired by an NCR ranger, which most successfully replicates the scene presented in the opening movie of the original game. In fact, after season 1, I said I would like to play the game again, but I did not. This time, I do not know how, I really started playing FNV and played a lot.
This time I decide to experience the game thoroughly: do every quest, visit every place of interest, collect every notable weapon and armor… So I followed a walk-through guide which helps me navigate through the world, in an easy and reasonable pace, making sure that I do not miss anything interesting.
Other than that, I used a renowned mod list: Viva New Vegas. If you are not familiar with what a mod is, it is some program which can be loaded by the original game but is not officially developed by employees of the developing studio. Those programs modify the behavior of the original game, thus the name mods. Mods can cover a lot of things: fixing bugs, improving performance, adding new content. Generally speaking, they allow people who are interested to expand the game to some extent that they are also developing the game as well. I played Skyrim, Fallout 4, Cyberpunk 2077 and Baldur's Gate 3 before, which also support community mods. I am familiar with mods and enjoy the improvement and new content provided by them, but I have never modified any game before.
Back to Viva New Vegas. It is a collection of mods focusing on fixing bugs, visual improvements, extending some gameplay elements while mostly keeping the style of the original game. Usually, I do not care about visual elements, but I really need those bug fixes, either gameplay or performance ones, since Bethesda's games are infamous for blocking bugs and frequent crashes.
It turns out that I do not want all the bug fixes. I am playing a character with the Logan's Loophole and the Chemist perk. In the vanilla game, although the description of these perks says that they only increase the effect duration of Chemicals, they actually affect all consumables including food, drink, and even magazines. Though strictly speaking, this is a bug, but it is not a bad thing to make your character more powerful. However, it seems that at least one of the mods I installed fixes this problem and those perks now really only works for Chemicals in my game.
Using these mods reminds me of using Emacs packages. Emacs packages are open sourced, so if I want to change certain behavior of certain packages, I just edit its source code and evaluate the updated code. In this case, FNV is the equivalent for Emacs, and mods are the equivalent for packages. However, are mods open sourced so that I can edit them to tweak their behaviors? Well, at least some of them are.
1. Locate the culprit
Anyway, the first step is to locate which mod fixes this bug. Browsing the modlist, I located Yukichigai Unofficial Patch (YUP) first, since it is a collection of bug fixes. In its change logs I found the following:
v12.5
- Removed Chemist/Day Tripper duration fix as it is now better handled by an engine-level fix in JIP LN.
It seems that YUP did fix this problem, but now it removes the fix and let engine level fixes deal with it. Fine. Next stop, JIP LN. Well, this mod is not updated for nearly two years. Not a good signal for any software. A software not maintained is a software dead. Reading its change log, I was happy to find something related:
- Version 56.90
- (From v56.76) Code-level fix for the 'Modify Positive Chem Duration' perk entry point to only effect 'Chems' (instead of all ingestibles) as was intended — improved this fix such that the correct duration is shown in the item effects info.
Also:
- Version 56.76
- Code-level fix for the 'Modify Positive Chem Duration' perk entry point to only effect 'Chems' (instead of all ingestibles) as was intended and as it did in Fallout 3.
Cool. This is exactly what I need. Fortunately, JIP LN is open
sourced. So I download the code. By its change log, I located two
functions. The first, EffectItemGetModifiedDuration, fixes the
displayed duration, introduced in Version 56.90.
1: __declspec(naked) UInt32 __fastcall EffectItemGetModifiedDuration(EffectItem *effItem)
2: {
3: __asm
4: {
5: xor eax, eax
6: mov edx, [ecx+0x14]
7: test byte ptr [edx+0x58], 0x80
8: jnz done
9: mov eax, [ecx+8]
10: test byte ptr [edx+0x58], 1
11: jnz done
12: mov ecx, [ebp]
13: mov ecx, [ecx-0xD4]
14: cmp dword ptr [ecx], 0x1011900
15: jnz done
16: mov dl, [ecx+0x64]
17: sub dl, 0xA
18: cmp dl, 1
19: ja done
20: cvtsi2ss xmm0, eax
21: push ecx
22: movss [esp], xmm0
23: push esp
24: push g_thePlayer
25: push kPerkEntry_ModifyPositiveChemDuration
26: CALL_EAX(0x5E58F0)
27: add esp, 0xC
28: cvttss2si eax, [esp]
29: pop ecx
30: done:
31: retn
32: }
33: }
And the second, ModPositiveChemDurationHook, fixes the actual
effecting duration for ingestibles, introduced in Version 56.76.
__declspec(naked) void ModPositiveChemDurationHook()
{
__asm
{
mov al, [eax+0xA0]
sub al, 0xA
cmp al, 1
seta al
ja done
mov ecx, [ebp+0x10]
mov eax, [ecx+0xC]
mov ecx, [eax+0x14]
mov al, [ecx+0x58]
and al, 1
done:
JMP_EDX(0x82370C)
}
}
2. Deciphering the assemblies, Part 1
Being unfamiliar to mods development, I was really curious why these functions are implemented in assembly, which is more difficult to read and understand than C++, isn't it?
Although I was quite confident that these two functions were what I was looking for, I would feel more comfortable to move along if I could understand what they do so that I could be 100% sure that they were the fixes I wanted to revert.
I started with EffectItemGetModifiedDuration since at least it had
an argument which had a C++ definition, which may help me understand
the code. Then function is marked with __fastcall and since FNV is a
32-bit program, it should follow IA-32 calling convention. From the
Wiki, we can see that fastcall functions implemented by Microsoft
C++ compilers pass its first argument through the ECX
register. Thus, line 6-8 of the assemblies is testing the member at
offset 0x14 of EffectItem, which by definition is a pointer to
EffectSetting.
class EffectItem
{
public:
enum
{
kRange_Self = 0,
kRange_Touch,
kRange_Target,
};
UInt32 magnitude; // 00 used as a float
UInt32 area; // 04
UInt32 duration; // 08
UInt32 range; // 0C
UInt32 actorValueOrOther; // 10
EffectSetting *setting; // 14
float cost; // 18 on autocalc items this seems to be the cost
ConditionList conditions; // 1C
};
Then it tests the member of EffectSetting at offset 0x58 with
0x80, which is a flag set, by the definition of EffectSetting:
class EffectSetting : public TESForm
{
public:
TESModel model; // 18
TESDescription description; // 30
TESFullName fullName; // 38
TESIcon icon; // 44
UInt32 unk50; // 50
UInt32 unk54; // 54
UInt32 effectFlags; // 58
// ...
};
Then, by the definition of EffectSettingFlags, we know that 0x80
is set if the effect has no duration:
enum EffectSettingFlags
{
kEffFlag_Hostile = 1,
kEffFlag_Recover = 2,
kEffFlag_Detrimental = 4,
kEffFlag_Hidden = 8,
kEffFlag_Self = 0x10,
kEffFlag_Touch = 0x20,
kEffFlag_Target = 0x40,
kEffFlag_NoDuration = 0x80,
kEffFlag_NoMagnitude = 0x100,
kEffFlag_NoArea = 0x200,
kEffFlag_FXPersist = 0x400,
kEffFlag_GoryVisuals = 0x1000,
kEffFlag_DisplayNameOnly = 0x2000,
kEffFlag_UseSkill = 0x80000,
kEffFlag_UseAttribute = 0x100000,
kEffFlag_Painless = 0x1000000,
kEffFlag_NoHitEffect = 0x8000000,
kEffFlag_NoDeathDispel = 0x10000000
};
To summarize, line 6-8 is testing if the effect has no duration. If not, then the function simply returns and do nothing. Similarly, line 10 to 11 tests if the effect is hostile, and if so, the function simply returns and do nothing as well.
The following part was mystic for me. Why it is reading ebp? What is
there? Since the function is marked with __declspec(naked) the
compiler would not generate function prologue and epilogue for it,
which means ebp is holding the stack base of some other function
calling this function. It may point to the first argument of that
function, but I have no idea what it is. The constants used in the
following part are also enigmatic to me: 0xD4 and 0x1011900 meant
nothing to me.
I basically could get nothing out from ModPositiveChemDurationHook
as well. It has no parameter, also marked with __declspec(namekd),
which means it is used in some context that I have no idea
about. Guess I could use some help here.
3. Looking for help, Part 1
Obviously, it would be the best if the author of the mod can explain to me how this piece of code worked. However, the mod has not been updated for almost two years. I did not expect to contact the author, but I still tried. Messaging on Nexus received no respond. Joining Discord severs revealed that the author had not been reached by anyone for a long time, and his modding method was different from most of the other mod authors, so I could get little help there as well.
4. Trial-and-error
Fine. No way to be 100% sure about the effect of those functions. I could appreciate some trial-and-error: just did whatever modification I want and saw if it solved my problem. Even if it did not, there would be no harm. It's not rocket science after all.
I commented out two lines of code that installed these two functions:
WriteRelCall(0x7E0C1F, (UInt32)EffectItemGetModifiedDuration);
WriteRelJump(0x8236FD, (UInt32)ModPositiveChemDurationHook);
Then I compiled the project, which produced a .dll file, and
replaced it with the original one. Launching the game, fingers
crossed. Then…
Error! A dialog popped up: Johnny Guitar (JG) complained about that the JIP LN version was incompatible, and its fixes would be disabled. Well, I was not expecting things would work out so easily.
It turns out that JG is another mod that depends on JIP LN. Fortunately, again, JG is open sourced as well so I may be able to modify its source code to accommodate my changes for JIP LN. Or, I could simply bypass its check for JIP LN since I considered my modification should not cause any harm.
JG checks the size and hash of the JIP LN .dll file. I commented out
all the checking code, modified the constant size, compiled and
replaced JG .dll file. Launching the game and fingers crossed,
again… This time, there was even no error. The game just instantly
crashed. Guess the checks were there for a reason.
Now, since JG strongly depends on the specific file, it would be really difficult to modify its source code to accommodate my changes. I did not check the source code thoroughly, but I did not want to recalculate some offsets and hard-coded addresses. Maybe I could use some help, again.
5. Looking for help, Part 2
JG is, fortunately, actively maintained, and from its git log, I could see someone just updated the mod days before. I messaged one of the maintainers, WallSoGB, on Nexus and got his reply really quick. From his explanation, I figured out something about mods development. The answer to the question I asked before was actually obvious: these mod authors use assembly code not because they prefer it, but that is the only way to insert some code into the original game code. After all, the game itself is not open sourced.
Next, although JIP LN is open sourced and JG depends on it, but JG
actually depends on the binary .dll files, rather than the source
code. The interface between these two mods are the binary files rather
than the source code, though reading the source code did help. As a
result, my original plan: modifying JIP LN, compiling a new .dll
file and replacing the old one would never work out. However, WallSoGB
did provide another solution: I could modify JG so that it in turn
modifies JIP LN at runtime. Since no other mods strongly depends on JG
(at least not verifying its size and hash), this should work. What I
need to do is to add the following statement to InitEarlyHooks in
JG:
PatchMemoryNopRange(GetJIPAddress(0x100128C3), GetJIPAddress(0x100128E1));
This overwrites JIP LN memory from 0x100128C3 to 0x100128E1 so
that this piece of memory contains only instruction NOP, an
instruction that actually did nothing. Why between these two
addresses? It turns out that this piece of memory holds the
instructions that install the two fixes:
WriteRelCall(0x7E0C1F, (UInt32)EffectItemGetModifiedDuration);
WriteRelJump(0x8236FD, (UInt32)ModPositiveChemDurationHook);
This can be verified with a disassemble tool (Ghidra, in this case):
patches_game.h:5401 (15)
100128c3 68 10 e7 PUSH EffectItemGetModifiedDuration
00 10
100128c8 68 1f 0c PUSH 0x7e0c1f
7e 00
100128cd e8 5e 41 CALL WriteRelCall void WriteRelCall(ulong param_1,
ff ff
patches_game.h:5402 (15)
100128d2 68 70 e7 PUSH ModPositiveChemDurationHook
00 10
100128d7 68 fd 36 PUSH 0x8236fd
82 00
100128dc e8 ff 40 CALL WriteRelJump void WriteRelJump(ulong param_1,
ff ff
This effectively "comments out" the two statement that installs the two fix functions at runtime. This is why I say these mods interface through binaries rather than source code.
Restoring the .dll file of JIP LN, compiling JG and replacing the
.dll file, launching the game… This time, no error, no
crashes. Open my Pip-Boy and check some food, whose effect duration
was not extended before… Voila! Now its effect duration is correctly
(incorrectly?) extended by perks Chemist and Logan's Loophole, just as
the vanilla game!
6. Deciphering the assemblies, Part 2
Although I successfully reverted the fix, I still was curious about the actual meaning of these two functions. Since I could not find someone who could help me, why not gave LLM a try?
I started a session backed by DeepSeek V3.2 API. First I asked it to
explain to me the JIP LN project. Then I asked it what does the
function ModPositiveChemDurationHook do? It summarized as follows:
**`ModPositiveChemDurationHook`** is an inline assembly hook intended to modify the game’s logic for determining whether a chem (drug) effect is “positive” and should have its duration adjusted by perks such as **Chemist** (which normally doubles the duration of beneficial chems).
Looks quite good, but its explanation for the assembly code did not help too much:
## ?? What the hook does (assembly analysis)
The hook’s assembly code performs the following checks:
```assembly
mov al, [eax+0xA0] ; load a byte from offset 0xA0 of the object pointed by eax
sub al, 0xA ; subtract 10 (0xA)
cmp al, 1 ; compare the result to 1
seta al ; set al = 1 if (byte‑10) > 1
ja done ; jump to “done” if (byte‑10) > 1
; If we get here, the byte is 0xA or 0xB (i.e., a “positive chem” type?)
mov ecx, [ebp+0x10] ; follow a chain of pointers
mov eax, [ecx+0xC]
mov ecx, [eax+0x14]
mov al, [ecx+0x58] ; read a flag byte at offset 0x58
and al, 1 ; isolate bit 0 (likely a “positive effect” flag)
done:
JMP_EDX(0x82370C) ; jump back to the original game code
```
### Interpretation:
1. **Byte at `[eax+0xA0]`** likely identifies the **chem type**.
- If the byte is `0xA` or `0xB` (10 or 11), the hook proceeds; otherwise it jumps to the end.
- These values probably correspond to **positive chems** like Med‑X, Buffout, etc.
2. The second part dereferences a pointer chain to reach an **effect‑item structure** and reads **flag bit 0** at offset `0x58`.
- This bit likely indicates whether the effect is **beneficial** (positive).
3. The final value in `al` is either `0` or `1`, which the original game code uses to decide whether to apply perk‑based duration modifiers.
So I asked it to provide a more detailed explanation to the function, and it generates the following response:
## ?? Step‑by‑step analysis of the assembly
```assembly
mov al, [eax+0xA0] ; 1. load a byte from offset 0xA0 of the object pointed to by EAX
sub al, 0xA ; 2. subtract 10 (0xA) from that byte
cmp al, 1 ; 3. compare the result with 1
seta al ; 4. set AL = 1 if (byte‑0xA) > 1, else AL = 0
ja done ; 5. if (byte‑0xA) > 1, jump to “done” (with AL = 1)
; --- Only reach here if the original byte was 0xA or 0xB (10 or 11) ---
mov ecx, [ebp+0x10] ; 6. load a pointer from the stack (likely a function argument)
mov eax, [ecx+0xC] ; 7. follow offset +0xC to obtain an EffectItem pointer
mov ecx, [eax+0x14] ; 8. follow offset +0x14 to obtain the EffectSetting pointer
mov al, [ecx+0x58] ; 9. read the effectFlags field (32‑bit, but we take only the low byte)
and al, 1 ;10. isolate bit 0 (the “Hostile” flag)
done:
JMP_EDX(0x82370C) ; jump back to the original game code
```
---
## ?? What each part is testing
### **Part 1 – The “chem‑type” filter**
- **`[eax+0xA0]`** reads a byte from an object (likely an **AlchemyItem** or a related structure).
- The byte is compared with **0xA** and **0xB** (decimal 10 and 11).
- **Only if the byte equals 0xA or 0xB** does the hook proceed to Part 2.
- For any other value, `AL` is set to **1** and the hook jumps to the end.
**Interpretation:** The game apparently uses this byte to classify chems. Values 10 and 11 probably correspond to **addiction‑related chems** (note that `MagicItem::kAddiction = 10` in the SDK). The hook restricts the perk‑duration adjustment to *only* these two chem types.
### **Part 2 – The “hostile‑effect” check**
- The hook traverses a pointer chain to reach the **EffectSetting** associated with the chem’s effect.
- At offset **0x58** of the EffectSetting is the `effectFlags` field (see `EffectSetting::effectFlags` in `GameForms.h`).
- **Bit 0** of `effectFlags` is `kEffFlag_Hostile` (value 1).
- The hook reads that bit and returns `AL = 1` if the effect is hostile, `AL = 0` if it is non‑hostile.
**Interpretation:** Even for chem types 10 and 11, the perk should **not** extend the duration of hostile (detrimental) effects. This check ensures that only *non‑hostile* effects of those chem types receive the duration bonus.
This time the result was much better. In fact, it was mostly correct
about the second part. Although its reasoning about the first part was
not fully correct, it brought an important class, AlchemyItem, into
my sight. Reading the definition of this class, I somehow figured out
an explanation myself:
class AlchemyItem : public MagicItemObject
{
public:
TESModelTextureSwap model; // 4C
TESIcon icon; // 6C
BGSMessageIcon messageIcon; // 78
TESScriptableForm scriptable; // 88
TESWeightForm weight; // 94
BGSEquipType equipType; // 9C
BGSDestructibleObjectForm destructible; // A4
BGSPickupPutdownSounds pickupPutdownsounds; // AC
UInt32 value; // B8
UInt8 alchFlags; // BC
UInt8 padBD[3]; // BD
SpellItem *withdrawalEffect; // C0
float addictionChance; // C4
TESSound *consumeSound; // C8
TESIcon iconCC; // CC
bool IsPoison() const;
};
For this piece of code:
mov al, [eax+0xA0]
sub al, 0xA
cmp al, 1
seta al
ja done
Assume eax holds a pointer to an instance of AlchemyItem, then
offset 0xA0 actually points to the last 4 bytes of equipType. The
definition of BGSEquipType shows that our assumption is largely
correct:
class BGSEquipType : public BaseFormComponent
{
public:
enum EquipTypes
{
kEqpType_BigGuns,
kEqpType_EnergyWeapons,
kEqpType_SmallGuns,
kEqpType_MeleeWeapons,
kEqpType_UnarmedWeapons,
kEqpType_ThrowWeapons,
kEqpType_Mine,
kEqpType_BodyWear,
kEqpType_HeadWear,
kEqpType_HandWear,
kEqpType_Chems,
kEqpType_Stimpack,
kEqpType_Food,
kEqpType_Alcohol
};
UInt32 equipType; // 08
};
Its last four bytes store its type, and 0xA is the type for
Chemicals. If the type is larger than 0xB, that is, if it is food or
alcohol, then al is set, and the execution continues outside this
function. This means if the al is set to 1 before execution
continues outside, then duration should not be extended.
The second part looks familiar:
mov ecx, [ebp+0x10]
mov eax, [ecx+0xC]
mov ecx, [eax+0x14]
mov al, [ecx+0x58]
and al, 1
The offsets 0x14 and 0x58 were seen in the function
EffectItemGetModifiedDuration, and as the LLM suggested, it is
checking for the effect flag and making sure if it is a hostile
effect, its duration should not be extended.
Where does the 0xC comes from? Since the 0x14 is offsetting inside
a EffectItem, I searched for it and found that ActiveEffect holds
a reference to EffectItem at offset 0xC. However, I can not find
any reference to ActiveEffect located at offset 0x10. Why the
offset is taken from ebp? This question I can not answer as
well. Obviously, there is lots of information that is known to the
author of the mod but not me. I guess that must be some knowledge of
reverse engineering the original game. However, this journey (and
article) is already long enough. I decide to stop here. After all,
reverse engineering is some topic I have almost zero knowledge about.