Files
sfall/sfall/Modules/Perks.cpp
T
NovaRain 7887bf5e95 Simplified some jumps in ASM code
* Replaced single-use "mov reg, addr; jmp reg" with constant variables.
2025-01-30 19:44:55 +08:00

1435 lines
42 KiB
C++

/*
* sfall
* Copyright (C) 2008-2025 The sfall team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <vector>
#include "..\main.h"
#include "..\FalloutEngine\Fallout2.h"
#include "LoadGameHook.h"
#include "PartyControl.h"
#include "SubModules\EnginePerks.h"
#include "Perks.h"
namespace sfall
{
long Perks::PerkLevelMod = 0;
constexpr int maxNameLen = 64; // don't change size
constexpr int maxDescLen = 512; // don't change size
static const int descLen = 256; // maximum text length for interface
static char perksFile[MAX_PATH] = {0};
static char Name[maxNameLen * fo::Perk::PERK_count] = {0};
static char Desc[descLen * fo::Perk::PERK_count] = {0};
static char tName[maxNameLen * fo::Trait::TRAIT_count] = {0};
static char tDesc[descLen * fo::Trait::TRAIT_count] = {0};
static char PerkBoxTitle[33];
static const int startFakeID = 256;
static DWORD addPerkMode = 2;
static bool perksReInit = false;
static int perksEnable = 0;
static int traitsEnable = 0;
static fo::PerkInfo perks[fo::Perk::PERK_count];
static fo::TraitInfo traits[fo::Trait::TRAIT_count];
struct PerkInfoExt {
short id;
char reserve[6];
char Name[maxNameLen];
char Desc[descLen];
fo::PerkInfo data;
// extra modificators
long stat1;
long stat1Mod;
long stat2;
long stat2Mod;
long skill3;
long skill3Mod;
long skill4;
long skill4Mod;
long skill5;
long skill5Mod;
};
static std::vector<PerkInfoExt> extPerks;
#pragma pack(push, 1)
struct FakePerk {
int Level; // current level (max 100)
int Image;
char Name[maxNameLen];
char Desc[maxDescLen];
char reserve[506]; // empty block
int ownerId; // 0 = the player, or ID number of party member NPC
short id; // perk id (use last bytes of the description under the ID value for compatibility)
FakePerk() {}
FakePerk(const char* _name, int _level, int _image, const char* _desc, int npcId, short _id = -1) : ownerId(npcId), id(_id), Name {0}, Desc {0}, reserve {0} {
Level = _level;
Image = _image;
strncpy_s(this->Name, _name, _TRUNCATE);
strncpy_s(this->Desc, _desc, _TRUNCATE);
}
};
#pragma pack(pop)
std::vector<FakePerk> fakeTraits;
std::vector<FakePerk> fakePerks;
std::vector<FakePerk> fakeSelectablePerks; // available perks for selection in the perk selection list
static long RemoveTraitID = -1;
static std::list<int> RemovePerkID;
static std::list<int> RemoveSelectableID;
static DWORD traitSkillBonuses[fo::Trait::TRAIT_count * 18] = {0};
static DWORD traitStatBonuses[fo::Trait::TRAIT_count * (fo::STAT_max_derived + 1)] = {0};
static bool disableTraits[fo::Trait::TRAIT_count];
static DWORD IgnoringDefaultPerks = 0;
static DWORD PerkFreqOverride = 0;
static const DWORD GainStatPerks[7][2] = {
{0x4AF122, 0xC9}, // Strength // mov ecx, ecx
{0x4AF184, 0xC9}, // Perception
{0x4AF19F, 0x90}, // Endurance // nop
{0x4AF1C0, 0xC9}, // Charisma
{0x4AF217, 0xC9}, // Intelligance
{0x4AF232, 0x90}, // Agility
{0x4AF24D, 0x90}, // Luck
};
// Returns the index for the found ID
int PerkSearchID(int id) {
int left = 0,
right = extPerks.size();
if (!--right) return (extPerks[right].id == id) ? right : -1;
while (left <= right) {
int mid = (left + right) / 2; // middle index of the segment
int value = extPerks[mid].id;
if (value == id) return mid;
if (value > id) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
void __stdcall SetPerkFreq(int i) {
PerkFreqOverride = i;
}
static DWORD __stdcall LevelUp() {
DWORD eachLevel = PerkFreqOverride;
if (!eachLevel) {
if (Perks::DudeHasTrait(fo::Trait::TRAIT_skilled)) { // Check if the player has the skilled trait
eachLevel = 4;
} else {
eachLevel = 3;
}
}
DWORD level = fo::var::Level_pc; // Get player's level
if (!((level + 1) % eachLevel)) fo::var::free_perk++; // Increment the number of perks owed
return level;
}
static __declspec(naked) void LevelUpHack() {
__asm {
push ecx;
call LevelUp;
mov edx, eax; // player level
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkBoxTitleHook() {
__asm {
lea eax, PerkBoxTitle;
retn;
}
}
void __stdcall IgnoreDefaultPerks() {
IgnoringDefaultPerks = 1;
}
void __stdcall RestoreDefaultPerks() {
IgnoringDefaultPerks = 0;
}
void __fastcall Perks::SetPerkboxTitle(const char* name) {
if (name[0] == '\0') {
PerkBoxTitle[0] = 0;
SafeWrite32(0x43C77D, 0x488CB);
} else {
strncpy_s(PerkBoxTitle, name, _TRUNCATE);
HookCall(0x43C77C, GetPerkBoxTitleHook);
}
}
static bool IsOwnedFake(int ownerId) {
return ((!ownerId && !PartyControl::IsNpcControlled()) || ownerId == fo::var::obj_dude->id);
}
static bool IsNotOwnedFake(int ownerId1, int ownerId2) {
return (ownerId1 != ownerId2);
}
void Perks::SetSelectablePerk(const char* name, int active, int image, const char* desc, int npcID) {
if (active < 0) return;
if (active > 1) active = 1;
size_t size = fakeSelectablePerks.size();
if (active == 0) {
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakeSelectablePerks[i].ownerId, npcID)) continue;
if (!strcmp(name, fakeSelectablePerks[i].Name)) {
fakeSelectablePerks.erase(fakeSelectablePerks.begin() + i);
return;
}
}
} else {
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakeSelectablePerks[i].ownerId, npcID)) continue;
if (!strcmp(name, fakeSelectablePerks[i].Name)) {
fakeSelectablePerks[i].Level = active;
fakeSelectablePerks[i].Image = image;
strncpy(fakeSelectablePerks[i].Desc, desc, descLen - 1);
fakeSelectablePerks[i].Desc[descLen - 1] = 0;
return;
}
}
if (size == fakeSelectablePerks.capacity()) fakeSelectablePerks.reserve(size + 10);
fakeSelectablePerks.emplace_back(name, active, image, desc, npcID);
}
}
void Perks::SetFakePerk(const char* name, int level, int image, const char* desc, int npcID) {
if (level < 0) return;
if (level > 100) level = 100;
size_t size = fakePerks.size();
if (level == 0) { // remove perk from fakePerks
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakePerks[i].ownerId, npcID)) continue;
if (!strcmp(name, fakePerks[i].Name)) {
fakePerks.erase(fakePerks.begin() + i);
return;
}
}
} else { // add or change the existing fake perk in fakePerks
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakePerks[i].ownerId, npcID)) continue;
if (!strcmp(name, fakePerks[i].Name)) {
fakePerks[i].Level = level;
fakePerks[i].Image = image;
strncpy(fakePerks[i].Desc, desc, descLen - 1);
fakePerks[i].Desc[descLen - 1] = 0;
return;
}
}
if (size == fakePerks.capacity()) fakePerks.reserve(size + 10);
fakePerks.emplace_back(name, level, image, desc, npcID);
}
}
void Perks::SetFakeTrait(const char* name, int active, int image, const char* desc, int npcID) {
if (active < 0) return;
if (active > 1) active = 1;
size_t size = fakeTraits.size();
if (active == 0) {
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakeTraits[i].ownerId, npcID)) continue;
if (!strcmp(name, fakeTraits[i].Name)) {
fakeTraits.erase(fakeTraits.begin() + i);
return;
}
}
} else {
for (size_t i = 0; i < size; i++) {
if (IsNotOwnedFake(fakeTraits[i].ownerId, npcID)) continue;
if (!strcmp(name, fakeTraits[i].Name)) {
fakeTraits[i].Level = active;
fakeTraits[i].Image = image;
strncpy(fakeTraits[i].Desc, desc, descLen - 1);
fakeTraits[i].Desc[descLen - 1] = 0;
return;
}
}
if (size == fakeTraits.capacity()) fakeTraits.reserve(size + 5);
fakeTraits.emplace_back(name, active, image, desc, npcID);
}
}
static DWORD __stdcall HaveFakePerks() {
return fakePerks.size();
}
static long __fastcall GetFakePerkLevel(int id) {
int i = id - fo::Perk::PERK_count;
return IsOwnedFake(fakePerks[i].ownerId) ? fakePerks[i].Level : 0;
}
static long __fastcall GetFakePerkImage(int id) {
return fakePerks[id - fo::Perk::PERK_count].Image;
}
static FakePerk* __fastcall GetFakePerk(int id) {
return &fakePerks[id - fo::Perk::PERK_count];
}
// Get level of taken perk
static DWORD __fastcall GetFakeSelectPerkLevel(int id) {
if (id < startFakeID) {
if (!PartyControl::IsNpcControlled()) { // extra perks are not available for controlled NPC
for (DWORD i = 0; i < fakePerks.size(); i++) {
if (fakePerks[i].id == id) return fakePerks[i].Level;
}
}
return 0;
}
long n = id - startFakeID;
const char* name = fakeSelectablePerks[n].Name;
int ownerID = fakeSelectablePerks[n].ownerId;
for (DWORD i = 0; i < fakePerks.size(); i++) {
if (ownerID != fakePerks[i].ownerId) continue;
if (!strcmp(name, fakePerks[i].Name)) {
return fakePerks[i].Level;
}
}
return 0;
}
static long __fastcall GetFakeSelectPerkImage(int id) {
if (id < startFakeID) {
int i = PerkSearchID(id);
return (i != -1) ? extPerks[i].data.image : i;
}
return fakeSelectablePerks[id - startFakeID].Image;
}
static FakePerk* __fastcall GetFakeSelectPerk(int id) {
if (id < startFakeID) {
int i = PerkSearchID(id);
if (i < 0) i = 0; // if id is not found
return (FakePerk*)&extPerks[i];
}
return &fakeSelectablePerks[id - startFakeID];
}
static __declspec(naked) void CheckTraitHack() {
__asm {
mov edx, ds:[FO_VAR_temp_trait];
cmp edx, -1;
jnz end;
mov edx, ds:[FO_VAR_temp_trait2];
end:
retn;
}
}
// Print a list of fake traits
static DWORD HandleFakeTraits(int isSelect) {
for (DWORD i = 0; i < fakeTraits.size(); i++) {
if (!IsOwnedFake(fakeTraits[i].ownerId)) continue;
if (fo::func::folder_print_line(fakeTraits[i].Name) && !isSelect) {
isSelect = 1;
fo::var::folder_card_fid = fakeTraits[i].Image;
fo::var::folder_card_title = (DWORD)fakeTraits[i].Name;
fo::var::folder_card_title2 = 0;
fo::var::folder_card_desc = (DWORD)fakeTraits[i].Desc;
}
}
return isSelect;
}
static long __fastcall PlayerHasPerk(int &isSelectPtr) {
isSelectPtr = HandleFakeTraits(isSelectPtr);
for (int i = 0; i < fo::Perk::PERK_count; i++) {
if (fo::func::perk_level(fo::var::obj_dude, i)) return 0x43438A; // print perks
}
return (!fakePerks.empty())
? 0x43438A // print perks
: 0x434446; // skip print perks
}
static DWORD __fastcall HaveFakeTraits(int &isSelectPtr) {
return (fakeTraits.empty()) ? PlayerHasPerk(isSelectPtr) : 0x43425B; // print traits
}
static __declspec(naked) void PlayerHasPerkHack() {
__asm {
push ecx; // isSelect
mov ecx, esp; // ptr to isSelect
call PlayerHasPerk;
pop ecx; // isSelect value from HandleFakeTraits
jmp eax;
}
}
static __declspec(naked) void PlayerHasTraitHook() {
__asm {
push ecx; // isSelect
mov ecx, esp; // ptr to isSelect
call HaveFakeTraits;
pop ecx; // isSelect value from HandleFakeTraits
jmp eax;
}
}
static __declspec(naked) void GetPerkLevelHook() {
using namespace fo;
__asm {
cmp edx, PERK_count;
jge fake;
jmp fo::funcoffs::perk_level_;
fake:
push ecx;
mov ecx, edx;
call GetFakePerkLevel;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkImageHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_skilldex_fid_;
fake:
push ecx;
mov ecx, eax;
call GetFakePerkImage;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkNameHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_name_;
fake:
push ecx;
mov ecx, eax;
call GetFakePerk;
lea eax, ds:[eax + 8];
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkDescHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_description_;
fake:
push ecx;
mov ecx, eax;
call GetFakePerk;
lea eax, ds:[eax + 72];
pop ecx;
retn;
}
}
// Search all available perks for the player to display them in the character screen
static __declspec(naked) void EndPerkLoopHack() {
static const DWORD EndPerkLoopExit = 0x434446;
static const DWORD EndPerkLoopCont = 0x4343A5;
using namespace fo;
__asm {
jl cLoop; // if ebx < 119
push ecx;
call HaveFakePerks; // return perks count (fake + ext)
pop ecx;
add eax, PERK_count; // total = perks count + vanilla count
cmp ebx, eax; // if perkId < total then continue
jl cLoop;
jmp EndPerkLoopExit; // exit loop
cLoop:
jmp EndPerkLoopCont; // continue loop
}
}
// Build a table of perks ID numbers available for selection, data buffer has limited size for 119 perks
static DWORD __stdcall HandleExtraSelectablePerks(DWORD available, DWORD* data) {
size_t count;
if (!PartyControl::IsNpcControlled()) { // extra perks are not available for controlled NPC
count = extPerks.size();
for (size_t i = 0; i < count; i++) {
if (available >= 119) return available; // exit if the buffer is overfull
if (fo::func::perk_can_add(fo::var::obj_dude, extPerks[i].id)) data[available++] = extPerks[i].id;
}
}
count = fakeSelectablePerks.size();
for (size_t i = 0; i < count; i++) {
if (IsOwnedFake(fakeSelectablePerks[i].ownerId)) {
if (available >= 119) break;
// for fake perks, their ID should start from 256
data[available++] = startFakeID + i;
}
}
return available; // total number of perks available for selection
}
static __declspec(naked) void GetAvailablePerksHook() {
__asm {
push ecx;
push edx; // arg data
cmp IgnoringDefaultPerks, 0;
jnz skipDefaults;
call fo::funcoffs::perk_make_list_; // return available count
jmp next;
skipDefaults:
xor eax, eax;
next:
push eax; // arg available
call HandleExtraSelectablePerks;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkSLevelHook() {
using namespace fo;
__asm {
cmp edx, PERK_count;
jge fake;
jmp fo::funcoffs::perk_level_;
fake:
push ecx;
mov ecx, edx;
call GetFakeSelectPerkLevel;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkSImageHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_skilldex_fid_;
fake:
push ecx;
push edx;
mov ecx, eax;
call GetFakeSelectPerkImage;
pop edx;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkSNameHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_name_;
fake:
push ecx;
push edx;
mov ecx, eax;
call GetFakeSelectPerk;
lea eax, ds:[eax + 8]; // Name
pop edx;
pop ecx;
retn;
}
}
static __declspec(naked) void GetPerkSDescHook() {
using namespace fo;
__asm {
cmp eax, PERK_count;
jge fake;
jmp fo::funcoffs::perk_description_;
fake:
push ecx;
push edx;
mov ecx, eax;
call GetFakeSelectPerk;
lea eax, ds:[eax + 72]; // Desc
pop edx;
pop ecx;
retn;
}
}
static void PerkSkillMod(fo::GameObject* critter, long skill, long mod, long type) {
if (mod == 0 || skill >= fo::SKILL_count) return;
if (mod < 0) {
type = !type;
mod = -mod;
}
if (fo::func::skill_is_tagged(skill)) mod /= 2;
do {
if (type) {
fo::func::skill_inc_point_force(critter, skill);
} else {
fo::func::skill_dec_point_force(critter, skill);
}
} while (--mod);
}
static void PerkStatMod(fo::GameObject* critter, long stat, long mod, long type) {
if (mod == 0 || stat > fo::STAT_poison_resist) return;
if (!type) mod = -mod;
int value = fo::func::stat_get_bonus(critter, stat);
fo::func::stat_set_bonus(critter, stat, value + mod);
}
static void ApplyPerkEffect(long index, fo::GameObject* critter, long type) {
int stat = extPerks[index].stat1;
if (stat > -1) PerkStatMod(critter, stat, extPerks[index].stat1Mod, type);
stat = extPerks[index].stat2;
if (stat > -1) PerkStatMod(critter, stat, extPerks[index].stat2Mod, type);
int skill = extPerks[index].skill3;
if (skill > -1) PerkSkillMod(critter, skill, extPerks[index].skill3Mod, type);
skill = extPerks[index].skill4;
if (skill > -1) PerkSkillMod(critter, skill, extPerks[index].skill4Mod, type);
skill = extPerks[index].skill5;
if (skill > -1) PerkSkillMod(critter, skill, extPerks[index].skill5Mod, type);
}
// Adds the selected perk to the player
static long __stdcall AddFakePerk(DWORD perkID) {
size_t count;
bool matched = false;
if (perkID < startFakeID) { // extra perk can only be added to the player
count = fakePerks.size();
for (size_t d = 0; d < count; d++) {
if (fakePerks[d].id == perkID) {
RemovePerkID.push_back(d);
fakePerks[d].Level++;
matched = true;
break;
}
}
if (!matched) { // add to fakePerks
int index = PerkSearchID(perkID);
if (index < 0) return -1;
RemovePerkID.push_back(count); // index of the added perk
fakePerks.emplace_back(extPerks[index].Name, 1, extPerks[index].data.image, extPerks[index].Desc, 0, extPerks[index].id); // id same as perkID
}
fo::func::perk_add_effect(fo::var::obj_dude, perkID);
return 0;
}
// behavior for fake perk/trait
perkID -= startFakeID;
if (addPerkMode & 1) { // add perk to trait
count = fakeTraits.size();
for (size_t d = 0; d < count; d++) {
if (IsNotOwnedFake(fakeTraits[d].ownerId, fakeSelectablePerks[perkID].ownerId)) continue;
if (!strcmp(fakeTraits[d].Name, fakeSelectablePerks[perkID].Name)) {
matched = true;
break;
}
}
if (!matched) {
if (RemoveTraitID == -1) RemoveTraitID = count; // index of the added trait
fakeTraits.push_back(fakeSelectablePerks[perkID]);
}
}
if (addPerkMode & 2) { // default mode
matched = false;
count = fakePerks.size();
for (size_t d = 0; d < count; d++) {
if (IsNotOwnedFake(fakePerks[d].ownerId, fakeSelectablePerks[perkID].ownerId)) continue;
if (!strcmp(fakePerks[d].Name, fakeSelectablePerks[perkID].Name)) {
RemovePerkID.push_back(d);
fakePerks[d].Level++;
matched = true;
break;
}
}
if (!matched) { // add to fakePerks
RemovePerkID.push_back(count); // index of the added perk
fakePerks.push_back(fakeSelectablePerks[perkID]);
}
}
if (addPerkMode & 4) { // delete from selectable perks
RemoveSelectableID.push_back(perkID); //fakeSelectablePerks.remove_at(perkID);
}
return 0;
}
// Adds perk from selection window to player
static __declspec(naked) void AddPerkHook() {
using namespace fo;
__asm {
cmp edx, PERK_count;
jl normalPerk;
push ecx;
push edx;
call AddFakePerk;
pop ecx;
retn;
normalPerk:
push edx;
call fo::funcoffs::perk_add_;
pop edx;
test eax, eax;
jnz end;
// fix gain perks (add to base stats instead of bonus stats)
cmp edx, PERK_gain_strength_perk;
jl end;
cmp edx, PERK_gain_luck_perk;
jg end;
inc ds:[edx * 4 + (FO_VAR_pc_proto + 0x24 - PERK_gain_strength_perk * 4)]; // base_stat_srength
end:
retn;
}
}
// Checks player statistics to add perk in selection listing
static fo::PerkInfo* __fastcall CanAddPerk(DWORD perkID) {
int index = PerkSearchID(perkID);
if (index != -1) {
int ranks = extPerks[index].data.ranks;
if (ranks > 0) {
for (size_t i = 0; i < fakePerks.size(); i++) {
if (fakePerks[i].id == perkID) {
if (fakePerks[i].Level >= ranks) return 0; // not available to add
break;
}
}
return &extPerks[index].data;
}
}
return 0;
}
static __declspec(naked) void perk_can_add_hack() {
static const DWORD perk_can_add_exit = 0x496A03;
static const DWORD perk_can_add_check = 0x496872;
__asm {
test edx, edx;
jz end;
push edx;
mov ecx, edx;
call CanAddPerk;
pop edx;
test eax, eax;
jz end;
mov ecx, eax; // ptr to perk_data
jmp perk_can_add_check;
end:
jmp perk_can_add_exit;
}
}
static fo::PerkInfo* __fastcall PerkData(DWORD perkID, fo::GameObject* critter, long type) {
int index = PerkSearchID(perkID);
if (index != -1) {
ApplyPerkEffect(index, critter, type); // apply ext. perk to critter
return &extPerks[index].data;
}
return 0;
}
static __declspec(naked) void perk_add_effect_hook() {
static const DWORD perk_add_effect_exit = 0x496CD9;
static const DWORD perk_add_effect_continue = 0x496C4A;
__asm {
cmp edx, startFakeID;
jge end;
push ecx;
push 1; // add effect
mov edx, esi; // critter
call PerkData; // ecx - perkID
pop ecx;
test eax, eax;
jz end;
mov edi, eax; // ptr to perk_data
jmp perk_add_effect_continue;
end:
jmp perk_add_effect_exit;
}
}
static __declspec(naked) void perk_remove_effect_hook() {
static const DWORD perk_remove_effect_exit = 0x496D99;
static const DWORD perk_remove_effect_continue = 0x496D2E;
__asm {
cmp edx, startFakeID;
jge end;
push ecx;
push 0; // remove effect
mov edx, esi; // critter
call PerkData; // ecx - perkID
pop ecx;
test eax, eax;
jz end;
mov edi, eax; // ptr to perk_data
jmp perk_remove_effect_continue;
end:
jmp perk_remove_effect_exit;
}
}
static __declspec(naked) void HeaveHoHack() {
using namespace fo;
__asm {
xor edx, edx;
mov eax, ecx;
call fo::funcoffs::stat_level_;
lea ebx, [0 + eax * 4];
sub ebx, eax; // ST * 3
cmp ebx, esi; // ebx = dist (3*ST), esi = max dist weapon
cmovg ebx, esi; // if dist > max then dist = max
mov eax, ecx;
mov edx, PERK_heave_ho;
call fo::funcoffs::perk_level_;
lea ecx, [0 + eax * 8];
sub ecx, eax;
sub ecx, eax;
mov eax, ecx;
add eax, ebx; // distance = dist + (PERK_heave_ho * 6)
push 0x478AFC;
retn;
}
}
bool Perks::perkHeaveHoModTweak = false;
void __stdcall Perks::ApplyHeaveHoFix() { // not really a fix
MakeJump(0x478AC4, HeaveHoHack);
perks[fo::Perk::PERK_heave_ho].strengthMin = 0;
perkHeaveHoModTweak = true;
}
static void PerkEngineInit() {
perk::EnginePerkBonusInit();
// Character screen (list_perks_)
MakeCall(0x434246, CheckTraitHack, 1); // fix for certain cases
HookCall(0x434256, PlayerHasTraitHook); // jz func
MakeJump(0x43436B, PlayerHasPerkHack);
HookCall(0x4343AC, GetPerkLevelHook);
HookCall(0x43440D, GetPerkImageHook);
HookCall(0x434432, GetPerkDescHook);
MakeJump(0x434440, EndPerkLoopHack, 1);
HookCalls(GetPerkNameHook, {0x4343C1, 0x4343DF, 0x43441B});
// GetPlayerAvailablePerks (ListDPerks_)
HookCall(0x43D127, GetAvailablePerksHook);
// ShowPerkBox (perks_dialog_)
HookCall(0x43C952, AddPerkHook);
// PerkboxSwitchPerk (RedrwDPrks_)
// GetPerkS hooks
HookCalls(GetPerkSLevelHook, {
0x43D25E, 0x43D275, // ListDPerks_
0x43C82E, 0x43C85B, // perks_dialog_
0x43C3F1, 0x43C41E // RedrwDPrks_
});
HookCalls(GetPerkSNameHook, {
0x43D17D, // ListDPerks_
0x43C8A6, 0x43C8EF, // perks_dialog_
0x43C469, 0x43C4B2 // RedrwDPrks_
});
HookCalls(GetPerkSDescHook, {
0x43C888, 0x43C8D1, // perks_dialog_
0x43C44B, 0x43C494 // RedrwDPrks_
});
HookCalls(GetPerkSImageHook, {
0x43C90F, // perks_dialog_
0x43C4D2 // RedrwDPrks_
});
// perk_owed hooks
MakeCall(0x4AFB2F, LevelUpHack, 1); // replaces 'mov edx, ds:[PlayerLevel]'
SafeWrite8(0x43C2EC, CodeType::JumpShort); // skip the block of code which checks if the player has gained a perk (now handled in level up code)
}
static void PerkSetup() {
if (!perksReInit) {
// _perk_data
SafeWriteBatch<DWORD>((DWORD)perks, {0x496669, 0x496837, 0x496BAD, 0x496C41, 0x496D25});
SafeWriteBatch<DWORD>((DWORD)&perks[0].description, {0x496696, 0x496BD1});
SafeWrite32(0x496BF5, (DWORD)&perks[0].image);
SafeWrite32(0x496AD4, (DWORD)&perks[0].ranks);
}
std::memcpy(perks, fo::var::perk_data, sizeof(fo::PerkInfo) * fo::Perk::PERK_count); // copy vanilla data
if (perksEnable) {
char num[4];
for (int i = 0; i < fo::Perk::PERK_count; i++) {
_itoa(i, num, 10);
if (IniReader::GetString(num, "Name", "", &Name[i * maxNameLen], maxNameLen - 1, perksFile)) {
perks[i].name = &Name[i * maxNameLen];
}
if (IniReader::GetString(num, "Desc", "", &Desc[i * descLen], descLen - 1, perksFile)) {
perks[i].description = &Desc[i * descLen];
}
int value;
value = IniReader::GetInt(num, "Image", -99999, perksFile);
if (value != -99999) perks[i].image = value;
value = IniReader::GetInt(num, "Ranks", -99999, perksFile);
if (value != -99999) perks[i].ranks = value;
value = IniReader::GetInt(num, "Level", -99999, perksFile);
if (value != -99999) perks[i].levelMin = value;
value = IniReader::GetInt(num, "Stat", -99999, perksFile);
if (value != -99999) perks[i].stat = value;
value = IniReader::GetInt(num, "StatMag", -99999, perksFile);
if (value != -99999) perks[i].statMod = value;
value = IniReader::GetInt(num, "Skill1", -99999, perksFile);
if (value != -99999) perks[i].skill1 = value;
value = IniReader::GetInt(num, "Skill1Mag", -99999, perksFile);
if (value != -99999) perks[i].skill1Min = value;
value = IniReader::GetInt(num, "Type", -99999, perksFile);
if (value != -99999) perks[i].skillOperator = value;
value = IniReader::GetInt(num, "Skill2", -99999, perksFile);
if (value != -99999) perks[i].skill2 = value;
value = IniReader::GetInt(num, "Skill2Mag", -99999, perksFile);
if (value != -99999) perks[i].skill2Min = value;
value = IniReader::GetInt(num, "STR", -99999, perksFile);
if (value != -99999) perks[i].strengthMin = value;
value = IniReader::GetInt(num, "PER", -99999, perksFile);
if (value != -99999) perks[i].perceptionMin = value;
value = IniReader::GetInt(num, "END", -99999, perksFile);
if (value != -99999) perks[i].enduranceMin = value;
value = IniReader::GetInt(num, "CHR", -99999, perksFile);
if (value != -99999) perks[i].charismaMin = value;
value = IniReader::GetInt(num, "INT", -99999, perksFile);
if (value != -99999) perks[i].intelligenceMin = value;
value = IniReader::GetInt(num, "AGL", -99999, perksFile);
if (value != -99999) perks[i].agilityMin = value;
value = IniReader::GetInt(num, "LCK", -99999, perksFile);
if (value != -99999) perks[i].luckMin = value;
}
if (perksReInit) {
perksReInit = false;
return;
}
// adding extra perks with IDs from 119 to 255
extPerks.reserve(startFakeID - fo::Perk::PERK_count);
extPerks.resize(1);
int n = 0;
for (int id = fo::Perk::PERK_count; id < startFakeID; id++) {
_itoa(id, num, 10);
int ranks = IniReader::GetInt(num, "Ranks", -1, perksFile);
if (ranks == -1) continue;
extPerks[n].data.ranks = ranks;
extPerks[n].data.image = IniReader::GetInt(num, "Image", -1, perksFile);
extPerks[n].data.levelMin = IniReader::GetInt(num, "Level", -1, perksFile);
extPerks[n].data.stat = IniReader::GetInt(num, "Stat", -1, perksFile);
extPerks[n].data.statMod = IniReader::GetInt(num, "StatMag", 0, perksFile);
extPerks[n].data.skill1 = IniReader::GetInt(num, "Skill1", -1, perksFile);
extPerks[n].data.skill1Min = IniReader::GetInt(num, "Skill1Mag", 0, perksFile);
extPerks[n].data.skillOperator = IniReader::GetInt(num, "Type", 0, perksFile);
extPerks[n].data.skill2 = IniReader::GetInt(num, "Skill2", -1, perksFile);
extPerks[n].data.skill2Min = IniReader::GetInt(num, "Skill2Mag", 0, perksFile);
extPerks[n].data.strengthMin = IniReader::GetInt(num, "STR", 0, perksFile);
extPerks[n].data.perceptionMin = IniReader::GetInt(num, "PER", 0, perksFile);
extPerks[n].data.enduranceMin = IniReader::GetInt(num, "END", 0, perksFile);
extPerks[n].data.charismaMin = IniReader::GetInt(num, "CHR", 0, perksFile);
extPerks[n].data.intelligenceMin = IniReader::GetInt(num, "INT", 0, perksFile);
extPerks[n].data.agilityMin = IniReader::GetInt(num, "AGL", 0, perksFile);
extPerks[n].data.luckMin = IniReader::GetInt(num, "LCK", 0, perksFile);
IniReader::GetString(num, "Name", "Error", extPerks[n].Name, maxNameLen - 1, perksFile);
extPerks[n].data.name = extPerks[n].Name;
IniReader::GetString(num, "Desc", "Error", extPerks[n].Desc, descLen - 1, perksFile);
extPerks[n].data.description = extPerks[n].Desc;
extPerks[n].stat1 = IniReader::GetInt(num, "Stat1", -1, perksFile);
extPerks[n].stat1Mod = IniReader::GetInt(num, "Stat1Mag", 0, perksFile);
extPerks[n].stat2 = IniReader::GetInt(num, "Stat2", -1, perksFile);
extPerks[n].stat2Mod = IniReader::GetInt(num, "Stat2Mag", 0, perksFile);
extPerks[n].skill3 = IniReader::GetInt(num, "Skill3", -1, perksFile);
extPerks[n].skill3Mod = IniReader::GetInt(num, "Skill3Mod", 0, perksFile);
extPerks[n].skill4 = IniReader::GetInt(num, "Skill4", -1, perksFile);
extPerks[n].skill4Mod = IniReader::GetInt(num, "Skill4Mod", 0, perksFile);
extPerks[n].skill5 = IniReader::GetInt(num, "Skill5", -1, perksFile);
extPerks[n].skill5Mod = IniReader::GetInt(num, "Skill5Mod", 0, perksFile);
extPerks[n].id = id;
++n;
extPerks.resize(n + 1); // add next 'empty' perk
}
extPerks.pop_back();
extPerks.shrink_to_fit();
if (!extPerks.empty()) {
MakeJump(0x496823, perk_can_add_hack);
HookCall(0x496C2D, perk_add_effect_hook); // jge func
HookCall(0x496D11, perk_remove_effect_hook); // jge func
dlog_f("Added extra %d perks\n", DL_INIT, n);
}
}
perksReInit = false;
}
/////////////////////////// TRAIT FUNCTIONS ///////////////////////////////////
int Perks::TraitsModEnable() {
return traitsEnable;
}
bool Perks::IsTraitDisabled(int traitID) {
return disableTraits[traitID];
}
DWORD Perks::GetTraitStatBonus(int statID, int traitIndex) {
return traitStatBonuses[statID * fo::Trait::TRAIT_count + fo::var::pc_trait[traitIndex]];
}
DWORD Perks::GetTraitSkillBonus(int skillID, int traitIndex) {
return traitSkillBonuses[skillID * fo::Trait::TRAIT_count + fo::var::pc_trait[traitIndex]];
}
static __declspec(naked) void BlockedTrait() {
__asm {
xor eax, eax;
retn;
}
}
static void PerkAndTraitSetup() {
PerkSetup();
if (!traitsEnable) return;
std::memcpy(traits, fo::var::trait_data, sizeof(fo::TraitInfo) * fo::Trait::TRAIT_count);
// _trait_data
SafeWriteBatch<DWORD>((DWORD)traits, {0x4B3A81, 0x4B3B80});
SafeWriteBatch<DWORD>((DWORD)&traits[0].description, {0x4B3AAE, 0x4B3BA0});
SafeWrite32(0x4B3BC0, (DWORD)&traits[0].image);
char buf[512], num[5] = {'t'};
char* num2 = &num[1];
for (int i = 0; i < fo::Trait::TRAIT_count; i++) {
_itoa_s(i, num2, 4, 10);
if (IniReader::GetString(num, "Name", "", &tName[i * maxNameLen], maxNameLen - 1, perksFile)) {
traits[i].name = &tName[i * maxNameLen];
}
if (IniReader::GetString(num, "Desc", "", &tDesc[i * descLen], descLen - 1, perksFile)) {
traits[i].description = &tDesc[i * descLen];
}
int value;
value = IniReader::GetInt(num, "Image", -99999, perksFile);
if (value != -99999) traits[i].image = value;
if (IniReader::GetString(num, "StatMod", "", buf, 512, perksFile) > 0) {
char *stat, *mod;
stat = strtok(buf, "|");
mod = strtok(0, "|");
while (stat && mod) {
int _stat = atoi(stat), _mod = atoi(mod);
if (_stat >= 0 && _stat <= fo::STAT_max_derived) traitStatBonuses[_stat * fo::Trait::TRAIT_count + i] = _mod;
stat = strtok(0, "|");
mod = strtok(0, "|");
}
}
if (IniReader::GetString(num, "SkillMod", "", buf, 512, perksFile) > 0) {
char *stat, *mod;
stat = strtok(buf, "|");
mod = strtok(0, "|");
while (stat && mod) {
int _stat = atoi(stat), _mod = atoi(mod);
if (_stat >= 0 && _stat < 18) traitSkillBonuses[_stat * fo::Trait::TRAIT_count + i] = _mod;
stat = strtok(0, "|");
mod = strtok(0, "|");
}
}
if (IniReader::GetInt(num, "NoHardcode", 0, perksFile)) {
disableTraits[i] = true;
switch (i) {
case fo::Trait::TRAIT_one_hander:
HookCall(0x4245E0, BlockedTrait); // determine_to_hit_func_
break;
case fo::Trait::TRAIT_finesse:
HookCall(0x4248F9, BlockedTrait); // compute_damage_
break;
case fo::Trait::TRAIT_fast_shot:
//HookCall(0x478C8A, BlockedTrait); // item_w_mp_cost_ (obsolete)
HookCall(0x478E70, BlockedTrait); // item_w_called_shot_
break;
case fo::Trait::TRAIT_bloody_mess:
HookCall(0x410707, BlockedTrait); // pick_death_
break;
case fo::Trait::TRAIT_jinxed:
HookCall(0x42389F, BlockedTrait); // compute_attack_
break;
case fo::Trait::TRAIT_drug_addict:
HookCall(0x47A0CD, BlockedTrait); // item_d_take_drug_
HookCall(0x47A51A, BlockedTrait); // perform_withdrawal_start_
break;
case fo::Trait::TRAIT_drug_resistant:
HookCall(0x479BE1, BlockedTrait); // insert_drug_effect_
HookCall(0x47A0DD, BlockedTrait); // item_d_take_drug_
break;
case fo::Trait::TRAIT_skilled:
HookCall(0x43C295, BlockedTrait); // UpdateLevel_
HookCall(0x43C2F3, BlockedTrait); // UpdateLevel_
break;
case fo::Trait::TRAIT_gifted:
HookCall(0x43C2A4, BlockedTrait); // UpdateLevel_
break;
}
}
}
}
static __declspec(naked) void game_init_hook() {
__asm {
call fo::funcoffs::trait_init_;
jmp PerkAndTraitSetup;
}
}
static __declspec(naked) void perks_dialog_hook() {
static const DWORD perks_dialog_Ret = 0x43C92F;
__asm {
call fo::funcoffs::ListDPerks_;
test eax, eax;
jz dlgExit;
retn;
dlgExit:
add esp, 4;
jmp perks_dialog_Ret;
}
}
/*
static __declspec(naked) void item_w_mp_cost_hook() {
__asm {
call fo::funcoffs::item_w_range_;
cmp eax, 2;
jge checkType; // is weapon range less than 2?
retn; // yes, skip -1 AP cost (0x478CA2)
checkType:
mov eax, edi; // source
mov edx, ecx; // hit_mode
call fo::funcoffs::item_hit_with_; // get pointer to weapon
mov edx, ecx; // hit_mode
jmp fo::funcoffs::item_w_subtype_; // eax - item
}
}
*/
// Haenlomal's tweak
static __declspec(naked) void item_w_called_shot_hack() {
static const DWORD FastShotTraitFix_End = 0x478E7F;
using namespace fo;
__asm {
mov edx, ecx; // argument for item_hit_with_: hit_mode
mov eax, ebx; // argument for item_hit_with_: pointer to source_obj (always dude_obj due to code path)
call fo::funcoffs::item_hit_with_; // get pointer to weapon
mov edx, ecx;
call fo::funcoffs::item_w_subtype_;
cmp eax, THROWING; // is weapon type RANGED or THROWING?
jge checkRange; // yes
jmp FastShotTraitFix_End; // continue processing called shot attempt
checkRange:
mov edx, ecx; // argument for item_w_range_: hit_mode
mov eax, ebx; // argument for item_w_range_: pointer to source_obj (always dude_obj due to code path)
call fo::funcoffs::item_w_range_; // get weapon's range
cmp eax, 2; // is weapon range greater than or equal to 2 (i.e. ranged attack)?
jge cantUse; // yes, disallow called shot attempt
jmp FastShotTraitFix_End; // continue processing called shot attempt
cantUse:
xor eax, eax; // clean up and exit function item_w_called_shot
pop esi;
pop ecx;
pop ebx;
retn;
}
}
int Perks::fastShotTweak;
static void FastShotTraitFix() {
Perks::fastShotTweak = IniReader::GetConfigInt("Misc", "FastShotFix", 0);
switch (Perks::fastShotTweak) {
case 1:
dlogr("Applying Fast Shot trait patch (Haenlomal's tweak).", DL_INIT);
MakeJump(0x478E79, item_w_called_shot_hack);
goto fix;
case 2:
dlogr("Applying Fast Shot trait patch (Alternative behavior).", DL_INIT);
/* Implemented in sfall item_w_mp_cost function */
//SafeWrite16(0x478C9F, 0x9090); // item_w_mp_cost_
//HookCalls((void*)0x478C7D, {0x478BB8, 0x478BC7, 0x478BD6, 0x478BEA, 0x478BF9, 0x478C08, 0x478C2F}); // jmp 0x478C7D
break;
case 3:
dlogr("Applying Fast Shot trait patch (Fallout 1 behavior).", DL_INIT);
/* Implemented in sfall item_w_mp_cost function */
//HookCall(0x478C97, (void*)fo::funcoffs::item_hit_with_);
//SafeWrite16(0x478C9E, CodeType::JumpZ << 8); // ignore all unarmed attacks (cmp eax, 0; jz)
break;
default:
dlogr("Applying Fast Shot trait fix.", DL_INIT);
fix:
//HookCall(0x478C97, item_w_mp_cost_hook); - Fix implemented in sfall item_w_mp_cost function
break;
}
}
///////////////////////////////////////////////////////////////////////////////
void __fastcall Perks::SetPerkValue(int id, int param, int value) {
if (id < 0 || id >= fo::Perk::PERK_count) return;
*(DWORD*)((DWORD)(&perks[id]) + param) = value;
perksReInit = true;
}
void Perks::SetPerkName(int id, const char* value) {
if (id < 0 || id >= fo::Perk::PERK_count) return;
strncpy_s(&Name[id * maxNameLen], maxNameLen, value, _TRUNCATE);
perks[id].name = &Name[maxNameLen * id];
perksReInit = true;
}
void Perks::SetPerkDesc(int id, const char* value) {
if (id < 0 || id >= fo::Perk::PERK_count) return;
strncpy_s(&Desc[id * descLen], descLen, value, _TRUNCATE);
perks[id].description = &Desc[descLen * id];
perksReInit = true;
}
void PerksReset() {
fakeTraits.clear();
fakePerks.clear();
fakeSelectablePerks.clear();
IgnoringDefaultPerks = 0;
addPerkMode = 2;
PerkFreqOverride = 0;
if (PerkBoxTitle[0] != 0) {
PerkBoxTitle[0] = 0;
SafeWrite32(0x43C77D, 0x488CB);
}
// Reset some settable game values back to the defaults
// Perk level mod
Perks::PerkLevelMod = 0;
// Pyromaniac bonus
SafeWrite8(0x424AB6, 5);
// Swift Learner bonus
SafeWrite32(0x4AFAE2, 100);
// Restore 'Heave Ho' modify fix
if (Perks::perkHeaveHoModTweak) {
SafeWrite8(0x478AC4, 0xBA);
SafeWrite32(0x478AC5, 0x23);
Perks::perkHeaveHoModTweak = false;
}
if (perksReInit) PerkSetup(); // restore perk data
}
void Perks::Save(HANDLE file) {
DWORD unused, count = fakeTraits.size();
WriteFile(file, &count, 4, &unused, 0);
for (DWORD i = 0; i < count; i++) {
WriteFile(file, &fakeTraits[i], sizeof(FakePerk), &unused, 0);
}
count = fakePerks.size();
WriteFile(file, &count, 4, &unused, 0);
for (DWORD i = 0; i < count; i++) {
WriteFile(file, &fakePerks[i], sizeof(FakePerk), &unused, 0);
}
count = fakeSelectablePerks.size();
WriteFile(file, &count, 4, &unused, 0);
for (DWORD i = 0; i < count; i++) {
WriteFile(file, &fakeSelectablePerks[i], sizeof(FakePerk), &unused, 0);
}
}
bool Perks::Load(HANDLE file) {
DWORD count, size;
ReadFile(file, &count, 4, &size, 0);
if (size != 4) return false;
if (count > 2) fakeTraits.reserve(count);
for (DWORD i = 0; i < count; i++) {
FakePerk fp;
ReadFile(file, &fp, sizeof(FakePerk), &size, 0);
fakeTraits.push_back(fp);
}
ReadFile(file, &count, 4, &size, 0);
if (size != 4) return false;
if (count > 2) fakePerks.reserve(count);
for (DWORD i = 0; i < count; i++) {
FakePerk fp;
ReadFile(file, &fp, sizeof(FakePerk), &size, 0);
if (fp.id > 0) { // update name/desc for non-fake perks
int index = PerkSearchID(fp.id);
if (index != -1) {
strncpy_s(fp.Name, extPerks[index].Name, _TRUNCATE);
strncpy_s(fp.Desc, extPerks[index].Desc, _TRUNCATE);
}
}
fakePerks.push_back(fp);
}
ReadFile(file, &count, 4, &size, 0);
if (count > 2) fakeSelectablePerks.reserve(count);
if (size != 4) return false;
for (DWORD i = 0; i < count; i++) {
FakePerk fp;
ReadFile(file, &fp, sizeof(FakePerk), &size, 0);
fakeSelectablePerks.push_back(fp);
}
return true;
}
void __stdcall AddPerkMode(DWORD mode) {
addPerkMode = mode;
}
DWORD Perks::HasFakePerk(const char* name, long perkId) {
if ((perkId < fo::Perk::PERK_count && name[0] == 0) || (perkId && PartyControl::IsNpcControlled())) return 0;
for (DWORD i = 0; i < fakePerks.size(); i++) {
if (perkId) {
if (fakePerks[i].id == perkId) return fakePerks[i].Level; // current perk level
} else if (IsOwnedFake(fakePerks[i].ownerId) && !strcmp(name, fakePerks[i].Name)) {
return fakePerks[i].Level;
}
}
return 0;
}
DWORD Perks::HasFakeTrait(const char* name) {
if (name[0] == 0) return 0;
for (DWORD i = 0; i < fakeTraits.size(); i++) {
if (IsOwnedFake(fakeTraits[i].ownerId) && !strcmp(name, fakeTraits[i].Name)) {
return 1;
}
}
return 0;
}
DWORD Perks::HasFakePerkOwner(const char* name, long objId) {
if (name[0] == 0) return 0;
for (DWORD i = 0; i < fakePerks.size(); i++) {
if (IsNotOwnedFake(fakePerks[i].ownerId, objId)) continue;
if (!strcmp(name, fakePerks[i].Name)) {
return fakePerks[i].Level;
}
}
return 0;
}
DWORD Perks::HasFakeTraitOwner(const char* name, long objId) {
if (name[0] == 0) return 0;
for (DWORD i = 0; i < fakeTraits.size(); i++) {
if (IsNotOwnedFake(fakeTraits[i].ownerId, objId)) continue;
if (!strcmp(name, fakeTraits[i].Name)) {
return 1;
}
}
return 0;
}
void __stdcall ClearSelectablePerks() {
fakeSelectablePerks.clear();
addPerkMode = 2;
IgnoringDefaultPerks = 0;
SafeWrite32(0x43C77D, 0x488CB);
}
void PerksEnterCharScreen() {
RemoveTraitID = -1;
RemovePerkID.clear();
RemoveSelectableID.clear();
}
void PerksCancelCharScreen() {
if (RemoveTraitID != -1) {
fakeTraits.erase(fakeTraits.begin() + RemoveTraitID, fakeTraits.end());
}
if (RemovePerkID.size() > 1) RemovePerkID.sort(); // sorting to correctly remove from the end
while (!RemovePerkID.empty()) {
int index = RemovePerkID.back();
if (!--fakePerks[index].Level) fakePerks.erase(fakePerks.begin() + index);
RemovePerkID.pop_back();
}
}
void PerksAcceptCharScreen() {
if (RemoveSelectableID.size() > 1) {
RemoveSelectableID.sort();
RemoveSelectableID.unique();
}
while (!RemoveSelectableID.empty()) {
fakeSelectablePerks.erase(fakeSelectablePerks.begin() + RemoveSelectableID.back());
RemoveSelectableID.pop_back();
}
}
void Perks::init() {
LoadGameHook::OnGameReset() += PerksReset;
FastShotTraitFix();
// Disable losing unused perks
SafeWrite16(0x43C369, 0x0DFE); // mov ds:[_free_perk], dh > dec ds:[_free_perk]
// If there are unused perks, then call the perk selection window
SafeWrite8(0x43C370, 0xB1); // jmp 0x43C322
// Don't show an empty perk selection window
HookCall(0x43C80B, perks_dialog_hook);
// Disable gain perks for bonus stats
for (int i = fo::Stat::STAT_st; i <= fo::Stat::STAT_lu; i++) {
SafeWrite8(GainStatPerks[i][0], (BYTE)GainStatPerks[i][1]);
}
PerkEngineInit();
// Perk and Trait init
HookCall(0x44272E, game_init_hook);
if (IniReader::GetConfigString("Misc", "PerksFile", "", &perksFile[2], MAX_PATH - 3)) {
perksFile[0] = '.';
perksFile[1] = '\\';
if (GetFileAttributesA(perksFile) == INVALID_FILE_ATTRIBUTES) return;
perksEnable = IniReader::GetInt("Perks", "Enable", 1, perksFile);
traitsEnable = IniReader::GetInt("Traits", "Enable", 1, perksFile);
// Engine perks settings
perk::ReadPerksBonuses(perksFile);
}
}
}