Files
sfall/sfall/Modules/MiscPatches.cpp
T
2021-11-27 06:44:06 +08:00

1020 lines
30 KiB
C++

/*
* sfall
* Copyright (C) 2008-2016 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 "..\main.h"
#include "..\FalloutEngine\Fallout2.h"
#include "..\SimplePatch.h"
#include "..\Translate.h"
#include "LoadGameHook.h"
#include "MainLoopHook.h"
#include "MiscPatches.h"
namespace sfall
{
static char mapName[16] = {};
static char patchName[65] = {};
static char versionString[65] = {};
static int* scriptDialog = nullptr;
static void __declspec(naked) WeaponAnimHook() {
__asm {
cmp edx, 11;
je c11;
cmp edx, 15;
je c15;
jmp fo::funcoffs::art_get_code_;
c11:
mov edx, 16;
jmp fo::funcoffs::art_get_code_;
c15:
mov edx, 17;
jmp fo::funcoffs::art_get_code_;
}
}
static void __declspec(naked) register_object_take_out_hack() {
using namespace fo::Fields;
__asm {
push ecx;
push eax;
mov ecx, edx; // ID1
mov edx, [eax + rotation]; // cur_rot
inc edx;
push edx; // ID3
xor ebx, ebx; // ID2
mov edx, [eax + artFid]; // fid
and edx, 0xFFF; // Index
xor eax, eax;
inc eax; // Obj_Type CRITTER
call fo::funcoffs::art_id_;
mov edx, eax;
xor ebx, ebx;
dec ebx; // delay -1
pop eax; // critter
call fo::funcoffs::register_object_change_fid_;
pop ecx;
xor eax, eax;
retn;
}
}
static void __declspec(naked) gdAddOptionStr_hack() {
__asm {
mov ecx, ds:[FO_VAR_gdNumOptions];
add ecx, '1';
push ecx;
mov ecx, 0x4458FA;
jmp ecx;
}
}
static void __declspec(naked) action_use_skill_on_hook_science() {
using namespace fo;
__asm {
cmp esi, ds:[FO_VAR_obj_dude];
jne end;
mov eax, KILL_TYPE_robot;
retn;
end:
jmp fo::funcoffs::critter_kill_count_type_;
}
}
static void __declspec(naked) intface_item_reload_hook() {
__asm {
push eax;
mov eax, dword ptr ds:[FO_VAR_obj_dude];
call fo::funcoffs::register_clear_;
xor edx, edx; // ANIM_stand
xor ebx, ebx; // delay (unused)
lea eax, [edx + 1]; // RB_UNRESERVED
call fo::funcoffs::register_begin_;
mov eax, dword ptr ds:[FO_VAR_obj_dude];
call fo::funcoffs::register_object_animate_;
call fo::funcoffs::register_end_;
pop eax;
jmp fo::funcoffs::gsound_play_sfx_file_;
}
}
static void __declspec(naked) automap_hack() {
static const DWORD ScannerHookRet = 0x41BC1D;
static const DWORD ScannerHookFail = 0x41BC65;
using fo::PID_MOTION_SENSOR;
__asm {
mov eax, ds:[FO_VAR_obj_dude];
mov edx, PID_MOTION_SENSOR;
call fo::funcoffs::inven_pid_is_carried_ptr_;
test eax, eax;
jz fail;
mov edx, eax;
jmp ScannerHookRet;
fail:
jmp ScannerHookFail;
}
}
static bool __fastcall SeeIsFront(fo::GameObject* source, fo::GameObject* target) {
long dir = source->rotation - fo::func::tile_dir(source->tile, target->tile);
if (dir < 0) dir = -dir;
if (dir == 1 || dir == 5) { // peripheral/side vision, reduce the range for seeing through (3x instead of 5x)
return (fo::func::obj_dist(source, target) <= (fo::func::stat_level(source, fo::STAT_pe) * 3));
}
return (dir == 0); // is directly in front
}
static void __declspec(naked) op_obj_can_see_obj_hook() {
using namespace fo;
using namespace Fields;
__asm {
mov edi, [esp + 4]; // buf **outObject
test ebp, ebp; // check only once
jz checkSee;
xor ebp, ebp; // for only once
push edx;
push eax;
mov edx, [edi - 8]; // target
mov ecx, eax; // source
call SeeIsFront;
xor ecx, ecx;
test al, al;
pop eax;
pop edx;
jnz checkSee; // can see
// vanilla behavior
push 0x10;
push edi;
call fo::funcoffs::make_straight_path_;
retn 8;
checkSee:
push eax; // keep source
push fo::funcoffs::obj_shoot_blocking_at_; // check hex objects func pointer
push 0x20; // flags, 0x20 = check ShootThru
push edi;
call fo::funcoffs::make_straight_path_func_; // overlapping if len(eax) == 0
pop ecx; // source
mov edx, [edi - 8]; // target
mov ebx, [edi]; // blocking object
test ebx, ebx;
jz isSee; // no blocking object
cmp ebx, edx;
jne checkObj; // object is not equal to target
retn 8;
isSee:
mov [edi], edx; // fix for target with ShootThru flag
retn 8;
checkObj:
mov eax, [ebx + protoId];
shr eax, 24;
cmp eax, OBJ_TYPE_CRITTER;
je continue; // see through critter
retn 8;
continue:
mov [edi], ecx; // outObject - ignore source (for cases of overlapping tiles from multihex critters)
mov [edi - 4], ebx; // replace source with blocking object
mov dword ptr [esp], 0x456BAB; // repeat from the blocking object
retn 8;
}
}
static DWORD __fastcall GetWeaponSlotMode(DWORD itemPtr, DWORD mode) {
int slot = (mode > 0) ? 1 : 0;
fo::ItemButtonItem* itemButton = &fo::ptr::itemButtonItems[slot];
if ((DWORD)itemButton->item == itemPtr) {
int slotMode = itemButton->mode;
if (slotMode == 3 || slotMode == 4) {
mode++;
}
}
return mode;
}
static void __declspec(naked) display_stats_hook() {
__asm {
push eax;
push ecx;
mov ecx, ds:[esp + edi + 0xA8 + 0xC]; // get itemPtr
call GetWeaponSlotMode; // ecx - itemPtr, edx - mode;
mov edx, eax;
pop ecx;
pop eax;
jmp fo::funcoffs::item_w_range_;
}
}
static void __fastcall SwapHandSlots(fo::GameObject* item, fo::GameObject* &toSlot) {
if (toSlot && fo::util::GetItemType(item) != fo::item_type_weapon && fo::util::GetItemType(toSlot) != fo::item_type_weapon) {
return;
}
fo::ItemButtonItem* leftSlot = &fo::ptr::itemButtonItems[fo::HandSlot::Left];
fo::ItemButtonItem* rightSlot = &fo::ptr::itemButtonItems[fo::HandSlot::Right];
if (toSlot == nullptr) { // copy to empty slot
fo::ItemButtonItem* dstSlot;
fo::ItemButtonItem item;
if ((int)&toSlot == FO_VAR_i_lhand) {
std::memcpy(&item, rightSlot, 0x14);
item.primaryAttack = fo::AttackType::ATKTYPE_LWEAPON_PRIMARY;
item.secondaryAttack = fo::AttackType::ATKTYPE_LWEAPON_SECONDARY;
dstSlot = leftSlot; // Rslot > Lslot
} else {
std::memcpy(&item, leftSlot, 0x14);
item.primaryAttack = fo::AttackType::ATKTYPE_RWEAPON_PRIMARY;
item.secondaryAttack = fo::AttackType::ATKTYPE_RWEAPON_SECONDARY;
dstSlot = rightSlot; // Lslot > Rslot;
}
std::memcpy(dstSlot, &item, 0x14);
} else { // swap slots
fo::ItemButtonItem hands[2];
std::memcpy(hands, fo::ptr::itemButtonItems, sizeof(fo::ItemButtonItem) * 2);
hands[fo::HandSlot::Left].primaryAttack = fo::AttackType::ATKTYPE_RWEAPON_PRIMARY;
hands[fo::HandSlot::Left].secondaryAttack = fo::AttackType::ATKTYPE_RWEAPON_SECONDARY;
hands[fo::HandSlot::Right].primaryAttack = fo::AttackType::ATKTYPE_LWEAPON_PRIMARY;
hands[fo::HandSlot::Right].secondaryAttack = fo::AttackType::ATKTYPE_LWEAPON_SECONDARY;
std::memcpy(leftSlot, &hands[fo::HandSlot::Right], 0x14); // Rslot > Lslot
std::memcpy(rightSlot, &hands[fo::HandSlot::Left], 0x14); // Lslot > Rslot
}
}
static void __declspec(naked) switch_hand_hack() {
__asm {
pushfd;
test ebx, ebx;
jz skip;
cmp ebx, edx;
jz skip;
push ecx;
mov ecx, eax;
call SwapHandSlots;
pop ecx;
skip:
popfd;
jz end;
retn;
end:
mov dword ptr [esp], 0x4715B7;
retn;
}
}
static long pHitL, sHitL, modeL = -2;
static long pHitR, sHitR, modeR = -2;
static long intface_update_items_hack_begin() {
if (!fo::ptr::itemButtonItems[fo::HandSlot::Left].item && !fo::func::inven_left_hand(*fo::ptr::obj_dude)) {
modeL = fo::ptr::itemButtonItems[fo::HandSlot::Left].mode;
pHitL = fo::ptr::itemButtonItems[fo::HandSlot::Left].primaryAttack;
sHitL = fo::ptr::itemButtonItems[fo::HandSlot::Left].secondaryAttack;
}
if (!fo::ptr::itemButtonItems[fo::HandSlot::Right].item && !fo::func::inven_right_hand(*fo::ptr::obj_dude)) {
modeR = fo::ptr::itemButtonItems[fo::HandSlot::Right].mode;
pHitR = fo::ptr::itemButtonItems[fo::HandSlot::Right].primaryAttack;
sHitR = fo::ptr::itemButtonItems[fo::HandSlot::Right].secondaryAttack;
}
return *fo::ptr::itemCurrentItem;
}
static void intface_update_restore() {
if (modeL != -2 && pHitL == fo::ptr::itemButtonItems[fo::HandSlot::Left].primaryAttack &&
sHitL == fo::ptr::itemButtonItems[fo::HandSlot::Left].secondaryAttack)
{
fo::ptr::itemButtonItems[fo::HandSlot::Left].mode = modeL;
}
if (modeR != -2 && pHitR == fo::ptr::itemButtonItems[fo::HandSlot::Right].primaryAttack &&
sHitR == fo::ptr::itemButtonItems[fo::HandSlot::Right].secondaryAttack)
{
fo::ptr::itemButtonItems[fo::HandSlot::Right].mode = modeR;
}
modeL = -2;
modeR = -2;
}
static void __declspec(naked) intface_update_items_hack_end() {
__asm {
call intface_update_restore;
cmp [esp + 0x1C - 0x18 + 4], 0; // animate
retn;
}
}
static void __declspec(naked) endgame_movie_hook() {
__asm {
cmp [esp + 16], 0x45C563; // call from op_endgame_movie_
je playWalkMovie;
retn;
playWalkMovie:
call fo::funcoffs::stat_level_;
xor edx, edx;
add eax, 10;
mov ecx, eax;
mov eax, 1500;
call fo::funcoffs::pause_for_tocks_;
mov eax, ecx;
jmp fo::funcoffs::gmovie_play_;
}
}
static void __declspec(naked) ListDrvdStats_hook() {
static const DWORD ListDrvdStats_Ret = 0x4354D9;
__asm {
call fo::util::IsRadInfluence;
test eax, eax;
jnz influence;
mov eax, ds:[FO_VAR_obj_dude];
jmp fo::funcoffs::critter_get_rads_;
influence:
xor ecx, ecx;
mov cl, ds:[FO_VAR_RedColor];
cmp dword ptr [esp], 0x4354BE + 5;
jne skip;
mov cl, 131; // color index for selected
skip:
add esp, 4;
jmp ListDrvdStats_Ret;
}
}
static void __declspec(naked) obj_render_outline_hack() {
__asm {
test eax, 0xFF00;
jnz palColor;
mov al, ds:[FO_VAR_GoodColor];
retn;
palColor:
mov al, ah;
retn;
}
}
static void __fastcall RemoveAllFloatTextObjects() {
long textCount = *fo::ptr::text_object_index;
if (textCount > 0) {
for (long i = 0; i < textCount; i++) {
fo::func::mem_free(fo::ptr::text_object_list[i]->unknown10);
fo::func::mem_free(fo::ptr::text_object_list[i]);
}
*fo::ptr::text_object_index = 0;
}
}
static void __declspec(naked) obj_move_to_tile_hook() {
__asm {
push eax;
push edx;
call RemoveAllFloatTextObjects;
pop edx;
pop eax;
jmp fo::funcoffs::map_set_elevation_;
}
}
static void __declspec(naked) map_check_state_hook() {
__asm {
push eax;
call RemoveAllFloatTextObjects;
pop eax;
jmp fo::funcoffs::map_load_idx_;
}
}
static void __declspec(naked) obj_move_to_tile_hook_redraw() {
__asm {
mov MainLoopHook::displayWinUpdateState, 1;
call fo::funcoffs::tile_set_center_;
mov eax, ds:[FO_VAR_display_win];
jmp fo::funcoffs::win_draw_; // update black edges after tile_set_center_
}
}
static void __declspec(naked) map_check_state_hook_redraw() {
__asm {
cmp MainLoopHook::displayWinUpdateState, 0;
je obj_move_to_tile_hook_redraw;
jmp fo::funcoffs::tile_set_center_;
}
}
static void AdditionalWeaponAnimsPatch() {
if (IniReader::GetConfigInt("Misc", "AdditionalWeaponAnims", 0)) {
dlog("Applying additional weapon animations patch.", DL_INIT);
SafeWrite8(0x419320, 18); // art_get_code_
const DWORD weaponAnimAddr[] = {
0x451648, 0x451671, // gsnd_build_character_sfx_name_
0x4194CC // art_get_name_
};
HookCalls(WeaponAnimHook, weaponAnimAddr);
dlogr(" Done", DL_INIT);
}
}
static void SkilldexImagesPatch() {
dlog("Checking for changed skilldex images.", DL_INIT);
long tmp = IniReader::GetConfigInt("Misc", "Lockpick", 293);
if (tmp != 293) SafeWrite32(0x518D54, tmp);
tmp = IniReader::GetConfigInt("Misc", "Steal", 293);
if (tmp != 293) SafeWrite32(0x518D58, tmp);
tmp = IniReader::GetConfigInt("Misc", "Traps", 293);
if (tmp != 293) SafeWrite32(0x518D5C, tmp);
tmp = IniReader::GetConfigInt("Misc", "FirstAid", 293);
if (tmp != 293) SafeWrite32(0x518D4C, tmp);
tmp = IniReader::GetConfigInt("Misc", "Doctor", 293);
if (tmp != 293) SafeWrite32(0x518D50, tmp);
tmp = IniReader::GetConfigInt("Misc", "Science", 293);
if (tmp != 293) SafeWrite32(0x518D60, tmp);
tmp = IniReader::GetConfigInt("Misc", "Repair", 293);
if (tmp != 293) SafeWrite32(0x518D64, tmp);
dlogr(" Done", DL_INIT);
}
static void ScienceOnCrittersPatch() {
switch (IniReader::GetConfigInt("Misc", "ScienceOnCritters", 0)) {
case 1:
HookCall(0x41276E, action_use_skill_on_hook_science);
break;
case 2:
SafeWrite8(0x41276A, CodeType::JumpShort);
break;
}
}
static void BoostScriptDialogLimitPatch() {
const DWORD script_dialog_msgs[] = {
0x4A50C2, 0x4A5169, 0x4A52FA, 0x4A5302, 0x4A6B86, 0x4A6BE0, 0x4A6C37,
};
if (IniReader::GetConfigInt("Misc", "BoostScriptDialogLimit", 0)) {
const int scriptDialogCount = 10000;
dlog("Applying script dialog limit patch.", DL_INIT);
scriptDialog = new int[scriptDialogCount * 2]; // Because the msg structure is 8 bytes, not 4.
SafeWrite32(0x4A50E3, scriptDialogCount); // scr_init
SafeWrite32(0x4A519F, scriptDialogCount); // scr_game_init
SafeWrite32(0x4A534F, scriptDialogCount * 8); // scr_message_free
SafeWriteBatch<DWORD>((DWORD)scriptDialog, script_dialog_msgs); // scr_get_dialog_msg_file
dlogr(" Done", DL_INIT);
}
}
static void NumbersInDialoguePatch() {
if (IniReader::GetConfigInt("Misc", "NumbersInDialogue", 0)) {
dlog("Applying numbers in dialogue patch.", DL_INIT);
SafeWrite32(0x502C32, 0x2000202E);
SafeWrite8(0x446F3B, 0x35);
SafeWrite32(0x5029E2, 0x7325202E);
SafeWrite32(0x446F03, 0x2424448B); // mov eax, [esp+0x24]
SafeWrite8(0x446F07, 0x50); // push eax
SafeWrite32(0x446FE0, 0x2824448B); // mov eax, [esp+0x28]
SafeWrite8(0x446FE4, 0x50); // push eax
MakeJump(0x4458F5, gdAddOptionStr_hack);
dlogr(" Done", DL_INIT);
}
}
static void InstantWeaponEquipPatch() {
const DWORD PutAwayWeapon[] = {
0x411EA2, // action_climb_ladder_
0x412046, // action_use_an_item_on_object_
0x41224A, // action_get_an_object_
0x4606A5, // intface_change_fid_animate_
0x472996, // invenWieldFunc_
};
if (IniReader::GetConfigInt("Misc", "InstantWeaponEquip", 0)) {
//Skip weapon equip/unequip animations
dlog("Applying instant weapon equip patch.", DL_INIT);
SafeWriteBatch<BYTE>(CodeType::JumpShort, PutAwayWeapon); // jmps
BlockCall(0x472AD5); //
BlockCall(0x472AE0); // invenUnwieldFunc_
BlockCall(0x472AF0); //
MakeJump(0x415238, register_object_take_out_hack);
dlogr(" Done", DL_INIT);
}
}
static void DontTurnOffSneakIfYouRunPatch() {
if (IniReader::GetConfigInt("Misc", "DontTurnOffSneakIfYouRun", 0)) {
dlog("Applying DontTurnOffSneakIfYouRun patch.", DL_INIT);
SafeWrite8(0x418135, CodeType::JumpShort);
dlogr(" Done", DL_INIT);
}
}
static void PlayIdleAnimOnReloadPatch() {
if (IniReader::GetConfigInt("Misc", "PlayIdleAnimOnReload", 0)) {
dlog("Applying idle anim on reload patch.", DL_INIT);
HookCall(0x460B8C, intface_item_reload_hook);
dlogr(" Done", DL_INIT);
}
}
static void MotionScannerFlagsPatch() {
if (long flags = IniReader::GetConfigInt("Misc", "MotionScannerFlags", 1)) {
dlog("Applying MotionScannerFlags patch.", DL_INIT);
if (flags & 1) MakeJump(0x41BBE9, automap_hack);
if (flags & 2) {
// automap_
SafeWrite16(0x41BC24, 0x9090);
BlockCall(0x41BC3C);
// item_m_use_charged_item_
SafeWrite8(0x4794B3, 0x5E); // jbe short 0x479512
}
dlogr(" Done", DL_INIT);
}
}
static void __declspec(naked) ResizeEncounterMessagesTable() {
__asm {
add eax, eax; // double the increment
add eax, 3000;
retn;
}
}
static void EncounterTableSizePatch() {
const DWORD EncounterTableSize[] = {
0x4BD1A3, 0x4BD1D9, 0x4BD270, 0x4BD604, 0x4BDA14, 0x4BDA44, 0x4BE707,
0x4C0815, 0x4C0D4A, 0x4C0FD4,
};
int tableSize = IniReader::GetConfigInt("Misc", "EncounterTableSize", 0);
if (tableSize > 40) {
dlog("Applying EncounterTableSize patch.", DL_INIT);
if (tableSize > 50) {
if (tableSize > 100) tableSize = 100;
// Increase the count of message lines from 50 to 100 for the encounter tables in worldmap.msg
const DWORD encounterMsgsTableAddr[] = {0x4C102C, 0x4C0B57};
MakeCalls(ResizeEncounterMessagesTable, encounterMsgsTableAddr);
}
SafeWrite8(0x4BDB17, (BYTE)tableSize);
DWORD nsize = (tableSize + 1) * 180 + 0x50;
SafeWriteBatch<DWORD>(nsize, EncounterTableSize);
dlogr(" Done", DL_INIT);
}
}
static void DisablePipboyAlarmPatch() {
if (IniReader::GetConfigInt("Misc", "DisablePipboyAlarm", 0)) {
dlog("Applying Disable Pip-Boy alarm button patch.", DL_INIT);
SafeWrite8(0x499518, CodeType::Ret);
SafeWrite8(0x443601, 0x0);
dlogr(" Done", DL_INIT);
}
}
static void ObjCanSeeShootThroughPatch() {
if (IniReader::GetConfigInt("Misc", "ObjCanSeeObj_ShootThru_Fix", 0)) {
dlog("Applying obj_can_see_obj fix for seeing through critters and ShootThru objects.", DL_INIT);
HookCall(0x456BC6, op_obj_can_see_obj_hook);
dlogr(" Done", DL_INIT);
}
}
static void OverrideMusicDirPatch() {
static const char* musicOverridePath = "data\\sound\\music\\";
if (long overrideMode = IniReader::GetConfigInt("Sound", "OverrideMusicDir", 0)) {
const DWORD musicOverride1Addr[] = {0x4449C2, 0x4449DB};
SafeWriteBatch<DWORD>((DWORD)musicOverridePath, musicOverride1Addr); // set paths if not present in the cfg
if (overrideMode == 2) {
const DWORD musicOverride2Addr[] = {0x518E78, 0x518E7C};
SafeWriteBatch<DWORD>((DWORD)musicOverridePath, musicOverride2Addr);
SafeWrite16(0x44FCF3, 0x40EB); // jmp 0x44FD35 (skip paths initialization)
}
}
}
static void DialogueFix() {
if (IniReader::GetConfigInt("Misc", "DialogueFix", 1)) {
dlog("Applying dialogue patch.", DL_INIT);
SafeWrite8(0x446848, 0x31);
dlogr(" Done", DL_INIT);
}
}
static void AlwaysReloadMsgs() {
if (IniReader::GetConfigInt("Misc", "AlwaysReloadMsgs", 0)) {
dlog("Applying always reload messages patch.", DL_INIT);
SafeWrite8(0x4A6B8D, 0x0);
dlogr(" Done", DL_INIT);
}
}
static void MusicInDialoguePatch() {
if (IniReader::GetConfigInt("Misc", "EnableMusicInDialogue", 0)) {
dlog("Applying music in dialogue patch.", DL_INIT);
SafeWrite8(0x44525B, 0);
//BlockCall(0x450627);
dlogr(" Done", DL_INIT);
}
}
static void DisplaySecondWeaponRangePatch() {
// Display the range of the secondary attack mode in the inventory when you switch weapon modes in active item slots
//if (IniReader::GetConfigInt("Misc", "DisplaySecondWeaponRange", 1)) {
dlog("Applying display second weapon range patch.", DL_INIT);
HookCall(0x472201, display_stats_hook);
dlogr(" Done", DL_INIT);
//}
}
static void KeepSelectModePatch() {
//if (IniReader::GetConfigInt("Misc", "KeepWeaponSelectMode", 1)) {
dlog("Applying keep selected attack mode patch.", DL_INIT);
MakeCall(0x4714EC, switch_hand_hack, 1);
// Keep unarmed mode
MakeCall(0x45F019, intface_update_items_hack_begin);
MakeCall(0x45F380, intface_update_items_hack_end);
dlogr(" Done", DL_INIT);
//}
}
#pragma pack(push, 1)
struct CodeData {
DWORD dd;
BYTE db;
CodeData() : dd(0x0024548D), db(0x90) {}
} patchData;
#pragma pack(pop)
static void SkipLoadingGameSettingsPatch() {
if (int skipLoading = IniReader::GetConfigInt("Misc", "SkipLoadingGameSettings", 0)) {
dlog("Applying skip loading game settings from a saved game patch.", DL_INIT);
BlockCall(0x493421);
SafeWrite8(0x4935A8, 0x1F);
SafeWrite32(0x4935AB, 0x90901B75);
if (skipLoading == 2) {
const DWORD difficultyAddr[] = {0x49341C, 0x49343B};
SafeWriteBatch<CodeData>(patchData, difficultyAddr);
}
const DWORD settingsAddr[] = {
0x493450, 0x493465, 0x49347A, 0x49348F, 0x4934A4, 0x4934B9, 0x4934CE,
0x4934E3, 0x4934F8, 0x49350D, 0x493522, 0x493547, 0x493558, 0x493569,
0x49357A
};
SafeWriteBatch<CodeData>(patchData, settingsAddr);
dlogr(" Done", DL_INIT);
}
}
static void UseWalkDistancePatch() {
int distance = IniReader::GetConfigInt("Misc", "UseWalkDistance", 3) + 2;
if (distance > 1 && distance < 5) {
dlog("Applying walk distance for using objects patch.", DL_INIT);
const DWORD walkDistanceAddr[] = {0x411FF0, 0x4121C4, 0x412475, 0x412906};
SafeWriteBatch<BYTE>(distance, walkDistanceAddr); // default is 5
dlogr(" Done", DL_INIT);
}
}
static void F1EngineBehaviorPatch() {
if (IniReader::GetConfigInt("Misc", "Fallout1Behavior", 0)) {
dlog("Applying Fallout 1 engine behavior patch.", DL_INIT);
BlockCall(0x4A4343); // disable playing the final movie/credits after the endgame slideshow
SafeWrite8(0x477C71, CodeType::JumpShort); // disable halving the weight for power armor items
HookCall(0x43F872, endgame_movie_hook); // play movie 10 or 11 based on the player's gender before the credits
dlogr(" Done", DL_INIT);
}
}
static long cMusicArea = -1;
static void __declspec(naked) wmMapMusicStart_hook() {
__asm {
push edx;
push eax;
call fo::funcoffs::map_target_load_area_; // returns the area ID of the loaded map
cmp eax, cMusicArea;
mov cMusicArea, eax;
jne default;
mov eax, [esp];
lea edx, ds:[FO_VAR_background_fname_requested];
call fo::funcoffs::stricmp_; // compare music file name
test eax, eax;
jz continuePlay;
default:
pop eax;
pop edx;
jmp fo::funcoffs::gsound_background_play_level_music_;
continuePlay:
pop eax;
pop edx;
xor eax, eax;
retn;
}
}
static void __declspec(naked) map_load_file_hook() {
__asm {
push eax;
call InWorldMap;
test eax, eax;
jnz playWind;
lea eax, LoadGameHook::mapLoadingName;
call fo::funcoffs::wmMapMatchNameToIdx_;
test eax, eax;
js default; // -1
push edx;
sub esp, 4;
mov edx, esp;
call fo::funcoffs::wmMatchAreaContainingMapIdx_;
pop eax;
pop edx;
cmp eax, cMusicArea;
jne playWind;
add esp, 4;
retn;
playWind:
mov eax, -1;
default:
mov cMusicArea, eax;
pop eax;
jmp fo::funcoffs::gsound_background_play_;
}
}
static void __declspec(naked) wmSetMapMusic_hook() {
__asm {
mov ds:[FO_VAR_background_fname_requested], 0;
jmp fo::funcoffs::wmMapMusicStart_;
}
}
// When moving to another map that uses the same music, the music playback will not restart from the beginning
static void PlayingMusicPatch() {
HookCall(0x4C58FB, wmMapMusicStart_hook);
HookCall(0x482BA0, map_load_file_hook);
HookCall(0x4C5999, wmSetMapMusic_hook); // related fix
}
static void __declspec(naked) main_death_scene_hook() {
__asm {
mov eax, 101;
call fo::funcoffs::text_font_;
jmp fo::funcoffs::debug_printf_;
}
}
static void __stdcall SplitPrintMessage(char* message, void* printFunc) {
char* text = message;
while (*text) {
if (text[0] == '\\' && text[1] == 'n') {
*text = 0; // set null terminator
__asm mov eax, message;
__asm call printFunc;
*text = '\\';
text += 2; // position after the 'n' character
message = text;
} else {
text++;
}
}
// print the last line or all the text if there is no line break
if (message != text) {
__asm mov eax, message;
__asm call printFunc;
}
}
static void __declspec(naked) sf_inven_display_msg() {
__asm {
push ecx;
push fo::funcoffs::inven_display_msg_;
push eax; // message
call SplitPrintMessage;
pop ecx;
retn;
}
}
static void __declspec(naked) sf_display_print_alt() {
__asm {
push ecx;
push fo::funcoffs::display_print_;
push eax; // message
call SplitPrintMessage;
pop ecx;
retn;
}
}
static void __declspec(naked) op_display_msg_hook() {
__asm {
cmp dword ptr ds:[FO_VAR_debug_func], 0;
jne debug;
retn;
debug:
jmp fo::funcoffs::config_get_value_;
}
}
static void EngineOptimizationPatches() {
// Speed up display_msg script function
HookCall(0x455404, op_display_msg_hook);
// Remove redundant/duplicate code
BlockCall(0x45EBBF); // intface_redraw_
BlockCall(0x4A4859); // exec_script_proc_
SafeMemSet(0x455189, CodeType::Nop, 11); // op_create_object_sid_
// Improve performance of the data conversion of script interpreter
// mov eax, [edx+eax]; bswap eax; ret;
SafeWrite32(0x4672A4, 0x0F02048B);
SafeWrite16(0x4672A8, 0xC3C8);
// mov eax, [edx+eax]; bswap eax;
SafeWrite32(0x4673E5, 0x0F02048B);
SafeWrite8(0x4673E9, 0xC8);
// mov ax, [eax]; rol ax, 8; ret;
SafeWrite32(0x467292, 0x66008B66);
SafeWrite32(0x467296, 0xC308C0C1);
// Disable unused code for the RandX and RandY window structure fields (these fields can now be used for other purposes)
SafeWrite32(0x4D630C, 0x9090C031); // xor eax, eax
SafeWrite8(0x4D6310, 0x90);
BlockCall(0x4D6319);
// Reduce excessive delays in the save/load game screens
const DWORD lsGameDelayAddr[] = {
0x47D00D, // LoadGame_
0x47C1FD // SaveGame_
};
SafeWriteBatch<BYTE>(16, lsGameDelayAddr); // 41 to 16 ms
// LoadGame_
SafeWrite8(0x47CF0D, 195 + 10); // jz 0x47CFDE
// SaveGame_
SafeWrite8(0x47C135, 140 + 10); // jz 0x47C1CF
}
void MiscPatches::init() {
EngineOptimizationPatches();
if (IniReader::GetConfigString("Misc", "StartingMap", "", mapName, 16)) {
dlog("Applying starting map patch.", DL_INIT);
SafeWrite32(0x480AAA, (DWORD)&mapName);
dlogr(" Done", DL_INIT);
}
if (IniReader::GetConfigString("Misc", "VersionString", "", versionString, 65)) {
dlog("Applying version string patch.", DL_INIT);
SafeWrite32(0x4B4588, (DWORD)&versionString);
dlogr(" Done", DL_INIT);
}
if (IniReader::GetConfigString("Misc", "PatchFile", "", patchName, 65)) {
dlog("Applying patch file patch.", DL_INIT);
SafeWrite32(0x444323, (DWORD)&patchName);
dlogr(" Done", DL_INIT);
}
if (IniReader::GetConfigInt("Misc", "SingleCore", 1)) {
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
if (sysInfo.dwNumberOfProcessors > 1) {
dlog("Applying single core patch.", DL_INIT);
HANDLE process = GetCurrentProcess();
SetProcessAffinityMask(process, 2); // use only CPU 1
CloseHandle(process);
dlogr(" Done", DL_INIT);
}
}
if (IniReader::GetConfigInt("Misc", "OverrideArtCacheSize", 0)) {
dlog("Applying override art cache size patch.", DL_INIT);
SafeWrite32(0x418867, 0x90909090);
SafeWrite32(0x418872, 261); // default for 512 MB system memory by installer
dlogr(" Done", DL_INIT);
}
int time = IniReader::GetConfigInt("Misc", "CorpseDeleteTime", 6); // time in days
if (time != 6) {
dlog("Applying corpse deletion time patch.", DL_INIT);
if (time <= 0) {
time = 12; // hours
} else if (time > 13) {
time = 13 * 24;
} else {
time *= 24;
}
SafeWrite32(0x483348, time);
dlogr(" Done", DL_INIT);
}
// Set idle function
*fo::ptr::idle_func = reinterpret_cast<void*>(Sleep);
SafeWrite16(0x4C9F12, 0x7D6A); // push 125 (ms)
BlockCall(0x4425E6); // Patch out ereg call
SafeWrite8(0x4810AB, CodeType::JumpShort); // Disable selfrun
SimplePatch<DWORD>(0x440C2A, "Misc", "SpecialDeathGVAR", fo::GVAR_MODOC_SHITTY_DEATH);
// Remove hardcoding for maps with IDs 19 and 37
if (IniReader::GetConfigInt("Misc", "DisableSpecialMapIDs", 0)) {
dlog("Applying disable special maps handling patch.", DL_INIT);
const DWORD specialMapIdsAddr[] = {0x4836D6, 0x4836DB};
SafeWriteBatch<BYTE>(0, specialMapIdsAddr);
dlogr(" Done", DL_INIT);
}
// Remove hardcoding for city areas 45 and 46 (AREA_FAKE_VAULT_13)
if (IniReader::GetConfigInt("Misc", "DisableSpecialAreas", 0)) {
dlog("Applying disable special areas handling patch.", DL_INIT);
SafeWrite8(0x4C0576, CodeType::JumpShort);
dlogr(" Done", DL_INIT);
}
// Set the normal font for death screen subtitles
if (IniReader::GetConfigInt("Misc", "DeathScreenFontPatch", 0)) {
dlog("Applying death screen font patch.", DL_INIT);
HookCall(0x4812DF, main_death_scene_hook);
dlogr(" Done", DL_INIT);
}
// Support for the newline control character '\n' in the object description in pro_*.msg files
const DWORD displayPrintAltAddr[] = {0x46ED87, 0x49AD7A}; // setup_inventory_, obj_examine_
SafeWriteBatch<DWORD>((DWORD)&sf_display_print_alt, displayPrintAltAddr);
SafeWrite32(0x472F9A, (DWORD)&sf_inven_display_msg); // inven_obj_examine_func_
// Highlight "Radiated" in red color when the player is under the influence of negative effects of radiation
const DWORD listDrvdStatsAddr[] = {0x43549C, 0x4354BE};
HookCalls(ListDrvdStats_hook, listDrvdStatsAddr);
// Allow setting custom colors from the game palette for object outlines
MakeCall(0x48EE00, obj_render_outline_hack);
// Remove floating text messages after moving to another map or elevation
HookCall(0x48A94B, obj_move_to_tile_hook);
HookCall(0x4836BB, map_check_state_hook);
// Redraw the screen to update black edges of the map (HRP bug)
// https://github.com/phobos2077/sfall/issues/282
HookCall(0x48A954, obj_move_to_tile_hook_redraw);
HookCall(0x483726, map_check_state_hook_redraw);
// Corrects the height of the black background for death screen subtitles
if (!hrpIsEnabled) SafeWrite32(0x48134D, 38 - (640 * 3)); // main_death_scene_ (shift y-offset 2px up, w/o HRP)
if (!hrpIsEnabled || hrpVersionValid) SafeWrite8(0x481345, 4); // main_death_scene_
if (hrpVersionValid) SafeWrite8(HRPAddress(0x10011738), 10);
F1EngineBehaviorPatch();
DialogueFix();
AdditionalWeaponAnimsPatch();
AlwaysReloadMsgs();
PlayIdleAnimOnReloadPatch();
SkilldexImagesPatch();
ScienceOnCrittersPatch();
OverrideMusicDirPatch();
BoostScriptDialogLimitPatch();
MotionScannerFlagsPatch();
EncounterTableSizePatch();
DisablePipboyAlarmPatch();
ObjCanSeeShootThroughPatch();
MusicInDialoguePatch();
DontTurnOffSneakIfYouRunPatch();
InstantWeaponEquipPatch();
NumbersInDialoguePatch();
DisplaySecondWeaponRangePatch();
KeepSelectModePatch();
SkipLoadingGameSettingsPatch();
UseWalkDistancePatch();
PlayingMusicPatch();
}
void MiscPatches::exit() {
if (scriptDialog) delete[] scriptDialog;
}
}