Initial public release of rem-essentials
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
#include "Freelancer.h"
|
||||
#include "fl_func.h"
|
||||
#include "utils.h"
|
||||
|
||||
FL_FUNC(void StopSound(BYTE soundId), 0x5646E0)
|
||||
FL_FUNC(void StartSound(BYTE soundId), 0x564650)
|
||||
|
||||
FL_FUNC(UINT GetFlStringFromResources(DWORD resourcesHandle, UINT ids, LPWSTR buffer, UINT bufferLen), 0x4347E0)
|
||||
|
||||
FL_FUNC(void AppendXmlWsToRdlEx(LPCWSTR ws, UINT wsLen, RenderDisplayList& rdl, DWORD flags), 0x57E2C0)
|
||||
|
||||
FL_FUNC(NavMapObj* NeuroNetNavMap::GetHighlightedObject(DWORD unk1, DWORD unk2), 0x496D40)
|
||||
|
||||
FL_FUNC(Waypoint* GetWaypoint(int index), 0x4C46A0)
|
||||
FL_FUNC(bool WaypointWatcher::GetCurrentWaypointInfo(bool& isPlayerWaypoint, int& waypointIndex), 0x4F42A0);
|
||||
|
||||
FL_FUNC(IObjRW* GetPlayerIObjRW(), 0x54BAF0);
|
||||
|
||||
CShip* GetPlayerShip()
|
||||
{
|
||||
IObjRW* playerIObjRW = GetPlayerIObjRW();
|
||||
return !playerIObjRW ? nullptr : (CShip*) playerIObjRW->cobject;
|
||||
}
|
||||
|
||||
CShip* GetPlayerShipSafe()
|
||||
{
|
||||
IObjRW* playerIObjRW = GetPlayerIObjRW();
|
||||
|
||||
if (playerIObjRW && playerIObjRW->cobject)
|
||||
{
|
||||
if ((playerIObjRW->cobject->classType & CSHIP_CLASS_TYPE) == CSHIP_CLASS_TYPE)
|
||||
return (CShip*) playerIObjRW->cobject;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Assumes both the CObjects of IObjRWs are CShips.
|
||||
bool AreIObjRWsInSameGroup(const IObjRW& o1, const IObjRW& o2)
|
||||
{
|
||||
auto* ship1 = (const CShip*) o1.cobject;
|
||||
auto* ship2 = (const CShip*) o2.cobject;
|
||||
|
||||
return AreShipsInSameGroup(ship1, ship2);
|
||||
}
|
||||
|
||||
bool AreShipsInSameGroup(const CShip* ship1, const CShip* ship2)
|
||||
{
|
||||
return ship1->groupId && ship1->groupId == ship2->groupId;
|
||||
}
|
||||
|
||||
FL_FUNC(bool IsSimpleUnvisited(const CSimple& simple), 0x4D4C70);
|
||||
FL_FUNC(BYTE GetSimpleVisitedValue(const CSimple& simple), 0x4D4D00);
|
||||
FL_FUNC(UINT GetIdsForUnvisitedSimple(const CSimple& simple), 0x4D4D50);
|
||||
|
||||
FL_FUNC(UINT GetCShipOrCEqObjName(const CEqObj &eqObj), 0x5472A0);
|
||||
|
||||
FL_FUNC(bool NN_Preferences::SetResolution(UINT width, DWORD unk, UINT height), 0x4B1C00)
|
||||
|
||||
void ExpandNNShipTraderObjMemory()
|
||||
{
|
||||
#define NN_SHIPTRADER_OBJ_SIZE_ADDR 0x4B9739
|
||||
static bool memoryExpanded = false;
|
||||
|
||||
if (!memoryExpanded)
|
||||
{
|
||||
// Expand the size of the NN_ShipTrader object if it hasn't been done yet.
|
||||
GetValue<UINT>(NN_SHIPTRADER_OBJ_SIZE_ADDR) += sizeof(NN_ShipTrader::shipRepPercentages);
|
||||
memoryExpanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
FL_FUNC(double GetDeltaTime(), 0x42D680)
|
||||
FL_FUNC(void UpdateDeltaTime(), 0x42D770)
|
||||
FL_FUNC(void UpdateDeltaTimeAndUpTime(), 0x5B2360)
|
||||
|
||||
FL_FUNC(UINT GetNumOfActiveMissionObjectives(), 0x4C4FB0)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#include "alchemy_crash.h"
|
||||
#include "utils.h"
|
||||
#include "logger.h"
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
|
||||
// This rewrites the original loop present in alchemy.dll.
|
||||
// In principle it would have been possible to just patch one asm instruction to fix the bug,
|
||||
// but rewriting the loop is cooler.
|
||||
const Alchemy* FASTCALL GetFinishedAle(int maxIndex, const Alchemy* aleArr, float maxProgress)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
for (; i < maxIndex - 1; ++i) // original loop condition: "i < maxIndex"
|
||||
{
|
||||
if (maxProgress < aleArr[i + 1].progress)
|
||||
break;
|
||||
}
|
||||
|
||||
return &aleArr[i];
|
||||
}
|
||||
|
||||
// There is code in alchemy.dll that determines how ALE effects should transition to a different effect.
|
||||
// Many times per frame it loops over a set of ALEs and finds which element meets the condition.
|
||||
// However, it assumes that there is at least one element for which this condition holds.
|
||||
// If not, we get that at the end of the loop, i == maxIndex, causing later code to access an out-of-bounds array element and thus crash (offset 0x701b).
|
||||
// This occurs under extremely rare circumstances; you can play the game for 1,000 hours straight and not notice anything,
|
||||
// but one day you start the game and it crashes within 15 minutes. The reason why suddenly no ALE meets this condition is unclear;
|
||||
// the fact that it's so inconsistent and rare makes it impossible to bisect.
|
||||
// This hook code rewrites the loop such that it never loops beyond maxIndex - 1.
|
||||
// If the original problem were to occur, then one or more ALE effects may become invisible, though at least it certainly fixes the crash.
|
||||
// Edit 18/04/26: It seems that even if the crash is fixed, there are other occurrences where it can happen.
|
||||
// For instance, 0x778D has a loop which looks like it was directly copy pasted from 0x6FDD.
|
||||
// Hence, I've looked carefully at the assembly for more similar loops and found two more (but I don't know if they ever get called).
|
||||
// All instances now have the same fix applied. Hopefully, this fixes all variations of this particular crash.
|
||||
void InitAlchemyCrashFix()
|
||||
{
|
||||
#define GET_FINISHED_ALE_START_TO_END 0x1A
|
||||
DWORD alchemyHandle = (DWORD) GetModuleHandle("alchemy.dll");
|
||||
|
||||
if (!alchemyHandle)
|
||||
{
|
||||
Logger::PrintModuleError("InitAlchemyCrashFix", "alchemy.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
static const AleLoop aleLoops[] = {
|
||||
{ 0x6FDD, 0x10 },
|
||||
{ 0x778D, 0x10 },
|
||||
// { 0x7F4C, 0x3C },
|
||||
// { 0x4136D, 0x10 }
|
||||
// I noticed these have the exact same kind of loop as the above two.
|
||||
// AFAICT however, these are never actually called, unlike the above two which are called every frame.
|
||||
// Hence I can't properly test if this hook even works for the latter two instances.
|
||||
};
|
||||
|
||||
for (const auto& aleLoop : aleLoops)
|
||||
{
|
||||
// mov edx, esi followed by push [esp+maxProgressOffset] (passes the needed parameters to our hook)
|
||||
PatchBytes(alchemyHandle + aleLoop.startOffset, { 0x89, 0xF2, 0xFF, 0x74, 0x24 });
|
||||
Patch<BYTE>(alchemyHandle + aleLoop.startOffset + 5, aleLoop.maxProgressOffset);
|
||||
Hook(alchemyHandle + aleLoop.startOffset + 6, GetFinishedAle, 20);
|
||||
|
||||
// mov esi, eax (set the return value so that the rest of the alchemy code can use it)
|
||||
Patch<WORD>(alchemyHandle + aleLoop.startOffset + GET_FINISHED_ALE_START_TO_END, 0xC689);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
#include "base_info.h"
|
||||
#include "Freelancer.h"
|
||||
#include "utils.h"
|
||||
#include <cstdio>
|
||||
|
||||
// Prints the header name in bold.
|
||||
void PrintInfoCategoryHeader_Hook(UINT headerIds, RenderDisplayList &rdl)
|
||||
{
|
||||
WCHAR headerName[128];
|
||||
GetFlString(headerIds, headerName, _countof(headerName));
|
||||
|
||||
LPCWSTR rdlBoldTextFmt = L"<RDL><PUSH/><TRA bold=\"true\"/><TEXT>%s</TEXT><TRA bold=\"false\"/><POP/></RDL>";
|
||||
swprintf_s(FL_BUFFER_2, FL_BUFFER_LEN, rdlBoldTextFmt, headerName);
|
||||
AppendXmlWsToRdl(FL_BUFFER_2, rdl);
|
||||
}
|
||||
|
||||
// If you open the "Current Information" window of a base, it shows which ships,
|
||||
// equipment, and commodities it is selling/buying. Every item displayed under each category
|
||||
// is preceded by a number of spaces. The first entry has 5 spaces and all the others 4.
|
||||
// The different number of spaces was done to fix a misalignment visible on lower 4:3 resolutions.
|
||||
// However, the misalignment still happens on higher resolutions; it is caused by the bold header category text.
|
||||
// This is because the bold closing tag is not added in the correct place (at least I think).
|
||||
// Hence the spaces which are added for the first entry are bold and are thus wider than normal on some resolutions.
|
||||
// This hook fixes it by making sure all entries use 5 spaces and printing the bold header text correctly.
|
||||
void InitBaseInfoSpacingFix()
|
||||
{
|
||||
// Stores for each category, the headerStyleAddr and headerNamePrintAddr, respectively.
|
||||
// The address of the first spacing string is always 6 bytes in front of headerNamePrintAddr.
|
||||
const BaseInfoCat baseInfoCategories[] = {
|
||||
{ 0x476177, 0x476203 }, // Ships For Sale (ids 0x669/1641)
|
||||
{ 0x476388, 0x476414 }, // Commodities Selling (ids 0x668/1640)
|
||||
{ 0x4765F4, 0x476684 }, // Commodities Buying (ids 0x667/1639)
|
||||
{ 0x476939, 0x4769E7 } // Equipment For Sale (ids 0x66A/1642)
|
||||
};
|
||||
|
||||
for (const auto &baseInfoCat : baseInfoCategories)
|
||||
{
|
||||
Patch<WORD>(baseInfoCat.headerStyleAddr + 1, 0x9CA4); // remove the bold style for the category header
|
||||
Hook(baseInfoCat.headerNamePrintAddr, PrintInfoCategoryHeader_Hook, 5); // ensure the header is printed manually, in bold
|
||||
Patch<BYTE>(baseInfoCat.headerNamePrintAddr + 6, 0x54); // use 5 spaces for the first line
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include "Common.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define FC_UK_GRP_IDS_NAME 197510
|
||||
#define NONE_IDS 3022
|
||||
|
||||
UINT CShip::get_group_name_Hook() const
|
||||
{
|
||||
UINT result = this->get_group_name();
|
||||
|
||||
if (result == FC_UK_GRP_IDS_NAME)
|
||||
return NONE_IDS;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// When you open the Current Information window on a factionless ship (fc_uk_grp),
|
||||
// one of the lines will say "Faction:".
|
||||
// This is because the fc_uk_grp faction has no name.
|
||||
// This code replaces the ids_name of fc_uk_grp only in this particular instance with "None" to make it look nicer.
|
||||
void InitBlankFactionNameFix()
|
||||
{
|
||||
#define CURRENT_INFO_GET_GROUP_NAME_INFOCARD_CALL_ADDR 0x475950
|
||||
Hook(CURRENT_INFO_GET_GROUP_NAME_INFOCARD_CALL_ADDR, &CShip::get_group_name_Hook, 6);
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#include "cheat_detection.h"
|
||||
#include "logger.h"
|
||||
#include "fl_func.h"
|
||||
#include "utils.h"
|
||||
#include "Common.h"
|
||||
#include <algorithm>
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
DWORD getGoodSoldByBaseCallAddr = 0;
|
||||
DWORD baseGoodItAdvanceAddr = 0;
|
||||
|
||||
FL_FUNC(const MarketGood* BaseMarket::GetSoldGood(UINT goodId) const, getGoodSoldByBaseCallAddr)
|
||||
FL_FUNC(void BaseGoodIt::Advance(), baseGoodItAdvanceAddr)
|
||||
|
||||
NAKED void GetGoodSoldByBase_Hook()
|
||||
{
|
||||
__asm {
|
||||
mov edx, esi // PlayerData&
|
||||
jmp GetGoodSoldByBaseOrPartOfShip
|
||||
}
|
||||
}
|
||||
|
||||
bool ShipPackageContainsGood(GoodInfo const &shipPackage, UINT goodId)
|
||||
{
|
||||
for (const auto& equipDescList : shipPackage.equipDescLists) {
|
||||
bool containsGoodId = std::any_of(equipDescList.list.begin(), equipDescList.list.end(),
|
||||
[goodId](const EquipDesc &equipDesc) { return equipDesc.archId == goodId; });
|
||||
|
||||
if (containsGoodId)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BaseGoodCollection::HasShipPackageWithGood(UINT goodId)
|
||||
{
|
||||
// Iterate over all the base's sold goods and try to find the ship packages.
|
||||
for (auto goodIt = goods.begin(); goodIt != goods.end(); ((BaseGoodIt*) &goodIt)->Advance())
|
||||
{
|
||||
if (!goodIt->IsShipCandidate())
|
||||
continue;
|
||||
|
||||
GoodInfo const *goodInfo = GoodList::find_by_id(goodIt->goodId);
|
||||
|
||||
// Is it a ship package?
|
||||
if (goodInfo && goodInfo->type == GoodType::Ship)
|
||||
{
|
||||
if (ShipPackageContainsGood(*goodInfo, goodId))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const MarketGood* FASTCALL GetGoodSoldByBaseOrPartOfShip(const BaseMarket &baseMarket, const PlayerData &playerData, UINT goodId)
|
||||
{
|
||||
const MarketGood* result = baseMarket.GetSoldGood(goodId);
|
||||
|
||||
if (result)
|
||||
return result;
|
||||
|
||||
// If the good is not sold by the base directly, maybe it's part of the purchased ship package.
|
||||
// This should only be checked if the player's ship has remained the same while staying on the base.
|
||||
if (playerData.currentShipId
|
||||
&& playerData.currentShipId == playerData.shipIdOnLand
|
||||
&& baseMarket.baseGoods->HasShipPackageWithGood(goodId))
|
||||
{
|
||||
// Return a MarketGood such that FL's return value check passes.
|
||||
static const MarketGood validMarketGood = { 0 };
|
||||
return &validMarketGood;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// In Freelancer there is a bug where if you have a server with players on it
|
||||
// and a player purchases a ship which they already have and then undock, they get kicked from the server.
|
||||
// This is because on undock, FL's anticheat does a check to see if you obtained any equipment which is not sold by the base.
|
||||
// This check only proceeds if your ship hasn't changed since you landed on the base.
|
||||
// If you buy a ship, you usually get some additional equipment as part of the package (e.g. shield).
|
||||
// However, after re-buying the same ship and undocking, you still have the same ship as far as the game is concerned,
|
||||
// and you have a shield which is not sold by the base, and thus you get kicked.
|
||||
// This code fixes it by checking if the "cheated" equipment is part of any of the base's offered ship packages.
|
||||
void InitShipBuyKickFix()
|
||||
{
|
||||
#define GET_GOOD_SOLD_BY_BASE_CALL_OFFSET_SERVER 0x6FEEB
|
||||
|
||||
DWORD serverHandle = (DWORD) GetModuleHandle("server.dll");
|
||||
|
||||
if (!serverHandle)
|
||||
{
|
||||
Logger::PrintModuleError("InitShipBuyKickFix", "server.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
getGoodSoldByBaseCallAddr = serverHandle + 0x33000;
|
||||
baseGoodItAdvanceAddr = serverHandle + 0x35DE0;
|
||||
|
||||
Hook(serverHandle + GET_GOOD_SOLD_BY_BASE_CALL_OFFSET_SERVER, GetGoodSoldByBase_Hook, 5);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#include "config_reader.h"
|
||||
#include "Common.h"
|
||||
#include "feature_config.h"
|
||||
#include "logger.h"
|
||||
|
||||
void ReadConfig(LPCSTR path, FeatureManager &manager)
|
||||
{
|
||||
INI_Reader reader;
|
||||
|
||||
if (!reader.open(path))
|
||||
return;
|
||||
|
||||
while (reader.read_header())
|
||||
{
|
||||
while (reader.read_value())
|
||||
{
|
||||
if (!manager.SetFeatureEnabled(reader.get_name_ptr(), reader.get_value_bool()))
|
||||
{
|
||||
Logger::PrintInvalidFeatureWarning("ReadConfig", reader.get_name_ptr(), reader.get_file_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader.close();
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#include "copy_paste.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
NAKED void HandleDefaultInputKey_Hook()
|
||||
{
|
||||
#define HANDLE_DEFAULT_INPUT_KEY_OG 0x57CDDA
|
||||
|
||||
__asm {
|
||||
mov ecx, esi
|
||||
push edi
|
||||
call InputBoxWindow::HandleCopyPaste
|
||||
mov byte ptr [esp+0x13], 0
|
||||
mov eax, HANDLE_DEFAULT_INPUT_KEY_OG
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
void InputBoxWindow::CopyFromClipboard()
|
||||
{
|
||||
if (!OpenClipboard(nullptr))
|
||||
return;
|
||||
|
||||
HANDLE clipboard = GetClipboardData(CF_UNICODETEXT);
|
||||
|
||||
if (!clipboard)
|
||||
goto _closeClipboard;
|
||||
|
||||
LPCWSTR clipboardStr = static_cast<LPCWSTR>(GlobalLock(clipboard));
|
||||
|
||||
if (!clipboardStr)
|
||||
goto _closeClipboard;
|
||||
|
||||
WriteString(clipboardStr);
|
||||
GlobalUnlock(clipboard);
|
||||
|
||||
_closeClipboard:
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
// There exists a WriteTypedKey function which takes the typedKey variable from a KeyMapInfo object and writes it to the input box.
|
||||
// However, this typedKey variable is the only thing that the function needs from this entire object.
|
||||
// So we just define a dummy KeyMapInfo object where we fill the character we want to enter in every loop iteration.
|
||||
void InputBoxWindow::WriteString(LPCWSTR str)
|
||||
{
|
||||
KeyMapInfo kmi;
|
||||
|
||||
// Stop when the end of the string has been reached, or if the buffer is full.
|
||||
for (size_t i = 0; str[i] != L'\0' && chars.size() < (size_t) maxCharsLength; ++i)
|
||||
{
|
||||
kmi.enteredKey = str[i];
|
||||
this->WriteTypedKey(kmi);
|
||||
}
|
||||
}
|
||||
|
||||
void InputBoxWindow::CopyToClipboard()
|
||||
{
|
||||
size_t inputLength = this->chars.size();
|
||||
|
||||
// If the chars vector is empty, there isn't anything to copy to the clipboard.
|
||||
// If the clipboard won't even open, there's no point in trying either.
|
||||
if (inputLength == 0 || !OpenClipboard(nullptr))
|
||||
return;
|
||||
|
||||
if (!EmptyClipboard())
|
||||
goto _closeClipboard;
|
||||
|
||||
HGLOBAL clipboardData = GlobalAlloc(GMEM_MOVEABLE, sizeof(WCHAR) * (inputLength + 1));
|
||||
|
||||
if (!clipboardData)
|
||||
goto _closeClipboard;
|
||||
|
||||
LPWSTR clipboardStr = static_cast<LPWSTR>(GlobalLock(clipboardData));
|
||||
|
||||
if (!clipboardStr)
|
||||
{
|
||||
GlobalFree(clipboardData);
|
||||
goto _closeClipboard;
|
||||
}
|
||||
|
||||
// Copy every char from the input box buffer to clipboardStr.
|
||||
for (size_t i = 0; i < inputLength; ++i)
|
||||
clipboardStr[i] = this->chars[i].c;
|
||||
|
||||
clipboardStr[inputLength] = L'\0'; // Set the null character at the end.
|
||||
|
||||
GlobalUnlock(clipboardData);
|
||||
|
||||
if (!SetClipboardData(CF_UNICODETEXT, clipboardData))
|
||||
GlobalFree(clipboardData);
|
||||
|
||||
_closeClipboard:
|
||||
CloseClipboard();
|
||||
}
|
||||
|
||||
void InputBoxWindow::HandleCopyPaste(const KeyMapInfo& kmi)
|
||||
{
|
||||
// I saw this check being made in many key handling function, but for this one I don't think it's necessary.
|
||||
// if (this->ime == nullptr)
|
||||
// return;
|
||||
|
||||
if (kmi.IsCtrlPressed())
|
||||
{
|
||||
// Ctrl + V pressed?
|
||||
if (toupper(kmi.enteredKey) == L'V')
|
||||
{
|
||||
CopyFromClipboard();
|
||||
}
|
||||
// Ctrl + C pressed?
|
||||
else if (toupper(kmi.enteredKey) == L'C')
|
||||
{
|
||||
CopyToClipboard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allows for the Ctrl + C and Ctrl + V key combinations to copy and paste the current clipboard from/to the input box.
|
||||
void InitCopyPasteFeature()
|
||||
{
|
||||
#define HANDLE_DEFAULT_INPUT_KEY_ADDR 0x57CE3C
|
||||
SetPointer(HANDLE_DEFAULT_INPUT_KEY_ADDR, HandleDefaultInputKey_Hook);
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
#include "cursor_colors.h"
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
#define CURSOR_LIST ((MouseCursor**) 0x616744)
|
||||
#define CURSOR_LIST_SIZE (*(PUINT) 0x616740)
|
||||
#define CURRENT_CURSOR (*(MouseCursor**) 0x616858)
|
||||
|
||||
#define GROUP_MEMBER_COLOR (*(PDWORD) 0x679B88)
|
||||
#define TRADE_REQUEST_COLOR (*(PDWORD) 0x679B9C)
|
||||
// The yellow color of objects using radio, but it is not used in the contact list.
|
||||
// Presumably because this color is already reserved for the selected target.
|
||||
#define HIGHLIGHT_COLOR (*(PDWORD) 0x679BA4)
|
||||
|
||||
const IObjRW *lastSelectedObj = nullptr;
|
||||
|
||||
FL_FUNC(void Targetable_Objects::UpdateTargeting(), 0x4F2220)
|
||||
|
||||
FL_FUNC(bool IsSimpleUsingRadio(UINT simpleId), 0x4CC880)
|
||||
|
||||
// We want to reset the lastSelectedObj before the targeting is updated
|
||||
// to ensure lastSelectedObj never points to invalid memory.
|
||||
void Targetable_Objects::UpdateTargeting_Hook()
|
||||
{
|
||||
lastSelectedObj = nullptr;
|
||||
UpdateTargeting();
|
||||
}
|
||||
|
||||
FL_FUNC(const IObjRW* FindIObjRW(UINT nickname, DWORD unk), 0x05416C0)
|
||||
|
||||
// Calling FindIObjRW manually every time we want to check the highlighted object is inefficient,
|
||||
// so we intercept the call that FL makes every frame and save the last selected object.
|
||||
const IObjRW* FindCurrentSelectedIObjRW_Hook(UINT nickname, DWORD unk)
|
||||
{
|
||||
const IObjRW* result = FindIObjRW(nickname, unk);
|
||||
if (result)
|
||||
lastSelectedObj = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Gets called when FL checks the attitude of the targeted (aim locked) object
|
||||
NAKED void GetAttitudeOfTarget_Hook()
|
||||
{
|
||||
#define GET_ATTITUDE_OF_TARGET_RET_ADDR 0x4F2465
|
||||
|
||||
__asm {
|
||||
test ebx, ebx
|
||||
je skip
|
||||
mov lastSelectedObj, eax // save the targeted (aim locked) object
|
||||
skip:
|
||||
mov edx, [esp+0x30] // overwritten instructions
|
||||
push eax
|
||||
push edx
|
||||
mov ecx, GET_ATTITUDE_OF_TARGET_RET_ADDR
|
||||
jmp ecx
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::map<MouseCursor*, std::shared_ptr<MouseCursor>> groupCursors, tradeRequestCursors;
|
||||
//std::map<MouseCursor*, std::shared_ptr<MouseCursor>> radioCursors;
|
||||
|
||||
std::shared_ptr<MouseCursor> CreateCustomCursor(const MouseCursor* originalCursor, DWORD color, LPCSTR nicknameSuffix)
|
||||
{
|
||||
auto result = std::make_shared<MouseCursor>(*originalCursor);
|
||||
|
||||
strcat_s(result->nickname, sizeof(result->nickname), nicknameSuffix);
|
||||
result->nicknameLen = strlen(result->nickname);
|
||||
result->color = color;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void FillCustomCursorMap(const std::vector<LPCSTR> &cursorNames, LPCSTR neutralCursorName)
|
||||
{
|
||||
std::vector<MouseCursor*> cursors;
|
||||
|
||||
// Find the relevant cursors.
|
||||
for (UINT i = 0; i < CURSOR_LIST_SIZE; ++i)
|
||||
{
|
||||
for (const auto cursorName : cursorNames)
|
||||
{
|
||||
if (strcmp(CURSOR_LIST[i]->nickname, cursorName) == 0)
|
||||
cursors.push_back(CURSOR_LIST[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the neutral cursor which we want to copy.
|
||||
auto neutralCursorIt = std::find_if(cursors.begin(), cursors.end(),
|
||||
[neutralCursorName](const MouseCursor* cursor) {
|
||||
return strcmp(cursor->nickname, neutralCursorName) == 0;
|
||||
}
|
||||
);
|
||||
|
||||
// Create new cursors based on the copied neutral cursor
|
||||
// and store them by the original friendly, neutral, and hostile version for easy access.
|
||||
if (neutralCursorIt != cursors.end())
|
||||
{
|
||||
auto groupCursor = CreateCustomCursor(*neutralCursorIt, GROUP_MEMBER_COLOR, "_group");
|
||||
auto tradeRequestCursor = CreateCustomCursor(*neutralCursorIt, TRADE_REQUEST_COLOR, "_trade");
|
||||
//auto radioCursor = CreateCustomCursor(*neutralCursorIt, HIGHLIGHT_COLOR, "_radio");
|
||||
|
||||
for (const auto cursor : cursors)
|
||||
{
|
||||
groupCursors.emplace(cursor, groupCursor);
|
||||
tradeRequestCursors.emplace(cursor, tradeRequestCursor);
|
||||
//radioCursors.emplace(cursor, radioCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void (*InitCursors_Original)();
|
||||
|
||||
void InitCursors_Hook()
|
||||
{
|
||||
// This function initializes all the standard cursors.
|
||||
// After it has finished, we want to create our custom-colored cursors by copying the existing neutral cursors.
|
||||
InitCursors_Original();
|
||||
|
||||
std::vector<LPCSTR> normalCursorNames = { "friendly", "neutral", "hostile" };
|
||||
std::vector<LPCSTR> fireCursorNames = { "fire_friendly", "fire_neutral", "fire" };
|
||||
|
||||
FillCustomCursorMap(normalCursorNames, "neutral");
|
||||
FillCustomCursorMap(fireCursorNames, "fire_neutral");
|
||||
}
|
||||
|
||||
FL_FUNC(void SetCurrentCursor(LPCSTR cursorName, bool unk), 0x41DDE0)
|
||||
|
||||
void FASTCALL SetCurrentCustomAimCursor(const Targetable_Objects& to, const IObjRW *highlightedObj, LPCSTR cursorName, bool unk)
|
||||
{
|
||||
// This function updates the CURRENT_CURSOR for targeting (aiming).
|
||||
SetCurrentCursor(cursorName, unk);
|
||||
|
||||
// Check if the player can be obtained.
|
||||
const IObjRW* player = GetPlayerIObjRW();
|
||||
if (!player || player->unk_x1C != 1)
|
||||
return;
|
||||
|
||||
// Try to get the target.
|
||||
const IObjRW *target = nullptr;
|
||||
if (highlightedObj != player && !to.isAimLocking)
|
||||
{
|
||||
target = highlightedObj;
|
||||
}
|
||||
else if (lastSelectedObj)
|
||||
{
|
||||
target = lastSelectedObj;
|
||||
}
|
||||
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
// If the target has been found, check if it is a player who sent a trade request or is a group member.
|
||||
decltype(groupCursors)* customCursorMap = nullptr;
|
||||
|
||||
// if (IsSimpleUsingRadio(target->get_simple_id()))
|
||||
// {
|
||||
// customCursorMap = &radioCursors;
|
||||
// }
|
||||
// else
|
||||
if (target->is_player())
|
||||
{
|
||||
if (target->SentTradeRequest())
|
||||
customCursorMap = &tradeRequestCursors;
|
||||
else if (AreIObjRWsInSameGroup(*target, *player))
|
||||
customCursorMap = &groupCursors;
|
||||
}
|
||||
|
||||
// If we found a better suitable custom cursor, set it as the current cursor.
|
||||
if (customCursorMap)
|
||||
{
|
||||
auto it = customCursorMap->find(CURRENT_CURSOR);
|
||||
|
||||
if (it != customCursorMap->end())
|
||||
{
|
||||
it->second->animState = CURRENT_CURSOR->animState;
|
||||
CURRENT_CURSOR = it->second.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Gets called when FL changes the current aim cursor.
|
||||
NAKED void SetCurrentAimCursor_Hook()
|
||||
{
|
||||
__asm {
|
||||
mov ecx, ebp // Targetable_Objects&
|
||||
mov edx, esi // IObjRW *highlightedObj
|
||||
jmp SetCurrentCustomAimCursor
|
||||
}
|
||||
}
|
||||
|
||||
// In Multiplayer, if you hover over a group member with the mouse, the cursor does not honor the pink group color.
|
||||
// Similarly, if you hover over someone who sent you a trade request, the cursor is not dark purple, either.
|
||||
// This code fixes this by creating custom cursors based on the existing neutral cursors
|
||||
// and showing them if it has been detected that the target is a group member or someone who sent a trade request.
|
||||
void InitMoreCursorColors()
|
||||
{
|
||||
#define INIT_CURSORS_CALL_ADDR 0x59D60B
|
||||
InitCursors_Original = SetRelPointer(INIT_CURSORS_CALL_ADDR + 1, InitCursors_Hook);
|
||||
|
||||
#define UPDATE_TARGETING_CALL_ADDR 0x4EC5EE
|
||||
Hook(UPDATE_TARGETING_CALL_ADDR, &Targetable_Objects::UpdateTargeting_Hook, 5);
|
||||
|
||||
#define FIND_SELECTED_IOBJRW_CALL_ADDR 0x4F22D6
|
||||
Hook(FIND_SELECTED_IOBJRW_CALL_ADDR, FindCurrentSelectedIObjRW_Hook, 5);
|
||||
|
||||
#define GET_ATTITUDE_OF_TARGET_ADDR 0x4F245F
|
||||
Hook(GET_ATTITUDE_OF_TARGET_ADDR, GetAttitudeOfTarget_Hook, 6, true);
|
||||
|
||||
DWORD setCurrentAimCursorCalls[] = { 0x4EC914, 0x4EC953 };
|
||||
for (auto aimCursorCall : setCurrentAimCursorCalls)
|
||||
Hook(aimCursorCall, SetCurrentAimCursor_Hook, 8);
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#include "dealer_fixes.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
|
||||
FL_FUNC(bool DealerOpenCamera::StartAnimation(LPCSTR name, PVOID unk, NavBar* navBar, DWORD unk2), 0x44BA60)
|
||||
|
||||
bool DealerOpenCamera::StartAnimation_Hook(LPCSTR name, PVOID unk, NavBar* navBar, DWORD unk2)
|
||||
{
|
||||
// Return true instead of false if the animation is already in progress. This fixes the bug.
|
||||
if (animationInProgress)
|
||||
return true;
|
||||
|
||||
return StartAnimation(name, unk, navBar, unk2);
|
||||
}
|
||||
|
||||
void FASTCALL SetShipDealerMenuOpened_Hook(PVOID unkUiElement, NavBar& navBar)
|
||||
{
|
||||
navBar.unkUiElement = unkUiElement; // overwritten instruction
|
||||
|
||||
// Don't allow the ship dealer menu to be opened if the room transition hasn't finished yet.
|
||||
// Otherwise it'll crash the game.
|
||||
bool roomTransitionFinished = (navBar.maneuverFrame->flags & UI_ELEMENT_VISIBLE) == UI_ELEMENT_VISIBLE;
|
||||
navBar.shipDealerMenuOpened = roomTransitionFinished;
|
||||
}
|
||||
|
||||
NAKED void GetRoomHotspot_Hook()
|
||||
{
|
||||
__asm {
|
||||
mov ebp, eax // overwritten instruction #1
|
||||
test esi, esi
|
||||
je null
|
||||
mov eax, [esi + 0x1C] // overwritten instruction #2
|
||||
ret
|
||||
null:
|
||||
xor eax, eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
void (NavBar::*SetHotspot_Original)(PVOID hotspot);
|
||||
|
||||
void NavBar::SetHotspot_Hook(PVOID hotspot)
|
||||
{
|
||||
// Yeah, let's not call the function without a valid hotspot.
|
||||
if (hotspot)
|
||||
{
|
||||
(this->*SetHotspot_Original)(hotspot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// In Freelancer there is an infamous bug where if you click the equipment or commodity dealer twice very quickly, the camera goes up but the dealer menu never appears.
|
||||
// Once the bug has been triggered the dealer menus will continue to not show up until you undock and redock, or reload your save file.
|
||||
void InitDealerOpenFix()
|
||||
{
|
||||
#define INIT_CAMERA_TRANSITION_EQUIPMENT_DEALER_ADDR 0x4417E7
|
||||
#define INIT_CAMERA_TRANSITION_COMMODITY_DEALER_ADDR 0x441862
|
||||
|
||||
DWORD initCameraCalls[] = { INIT_CAMERA_TRANSITION_EQUIPMENT_DEALER_ADDR, INIT_CAMERA_TRANSITION_COMMODITY_DEALER_ADDR };
|
||||
for (const auto &call : initCameraCalls)
|
||||
Hook(call, &DealerOpenCamera::StartAnimation_Hook, 5);
|
||||
};
|
||||
|
||||
// There are some rare crashes that can occur when opening the dealer menus.
|
||||
void InitDealerCrashFix()
|
||||
{
|
||||
#define SET_SHIP_DEALER_MENU_OPENED_ADDR 0x441D28
|
||||
#define GET_ROOM_HOTSPOT_ADDR 0x43FFB6
|
||||
#define SET_HOTSPOT_CALL_ADDR 0x43E9CA
|
||||
|
||||
// Fixes a crash when clicking on the ship dealer before the room transition has finished.
|
||||
PatchBytes(SET_SHIP_DEALER_MENU_OPENED_ADDR, { 0x89, 0xDA, 0x89, 0xC5 }); // mov edx, ebx + mov ebp, eax
|
||||
Hook(SET_SHIP_DEALER_MENU_OPENED_ADDR + sizeof(DWORD), SetShipDealerMenuOpened_Hook, 5);
|
||||
PatchBytes(SET_SHIP_DEALER_MENU_OPENED_ADDR + sizeof(DWORD) + 5, { 0x89, 0xE8, 0x66, 0x90 }); // mov eax, ebp + nop
|
||||
|
||||
// Fixes a very rare crash that occurs when randomly clicking on various dealers at a base.
|
||||
Hook(GET_ROOM_HOTSPOT_ADDR, GetRoomHotspot_Hook, 5);
|
||||
SetHotspot_Original = SetRelPointer(SET_HOTSPOT_CALL_ADDR + 1, &NavBar::SetHotspot_Hook);
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
#include "dll_crash.h"
|
||||
#include "utils.h"
|
||||
#include "logger.h"
|
||||
|
||||
#define SKIP_DLL_LOAD_FILE_OFFSET_SERVER 0x63F54
|
||||
#define FDUMP_DLL_INSTANCE_FAILED_1_FILE_OFFSET_SERVER 0x63D50
|
||||
#define CREATE_DLL_INSTANCE_FAILED_1_FILE_OFFSET_SERVER 0x63D6B
|
||||
#define CREATE_DLL_INSTANCE_FAILED_2_FILE_OFFSET_SERVER 0x63E6D
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
DWORD skipDllLoadAddr;
|
||||
|
||||
NAKED void CreateDllInstanceFail_Hook()
|
||||
{
|
||||
__asm {
|
||||
call [edx] // overwritten instruction #1
|
||||
add esp, 0x14 // overwritten instruction #2
|
||||
jmp [skipDllLoadAddr] // skip the DLL loading code
|
||||
}
|
||||
}
|
||||
|
||||
// If you try to load a DLL which doesn't exist via Freelancer.ini (Initial MP DLL or Initial SP DLL),
|
||||
// the game logs an error to the Spew and then crashes. This code fixes the crash to ensure that the game at least still runs.
|
||||
void InitMissingDllCrashFix()
|
||||
{
|
||||
// E.g. console.dll enforces the server library to load without causing any issues, so should be fine
|
||||
DWORD serverHandle = GetUnloadedModuleHandle("server.dll");
|
||||
|
||||
if (!serverHandle)
|
||||
{
|
||||
Logger::PrintModuleError("InitMissingDllCrashFix", "server.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
skipDllLoadAddr = serverHandle + SKIP_DLL_LOAD_FILE_OFFSET_SERVER;
|
||||
|
||||
// mov edx, [FDUMP] <- mov ecx, [FDUMP] to ensure that we can use the same hook for instance 1.
|
||||
Patch<BYTE>(serverHandle + FDUMP_DLL_INSTANCE_FAILED_1_FILE_OFFSET_SERVER, 0x15);
|
||||
|
||||
const DWORD dllInstanceFailedOffsets[] = {
|
||||
CREATE_DLL_INSTANCE_FAILED_1_FILE_OFFSET_SERVER, CREATE_DLL_INSTANCE_FAILED_2_FILE_OFFSET_SERVER, };
|
||||
|
||||
for (const auto& offset : dllInstanceFailedOffsets)
|
||||
Hook(serverHandle + offset, CreateDllInstanceFail_Hook, 5, true);
|
||||
}
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
#include "exit.h"
|
||||
#include "DALib.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
FL_FUNC(void exit_Original(int const status), dword ptr ds:[0x5C713C])
|
||||
|
||||
void exit_Hook(int const status)
|
||||
{
|
||||
// Call the original function with WaitForSingleObject.
|
||||
CGunWrapper::Shutdown();
|
||||
|
||||
// Call the original exit function.
|
||||
exit_Original(status);
|
||||
}
|
||||
|
||||
bool noQuitMsgRetrieved = true;
|
||||
bool (*HandleMessages_Original)(WPARAM *msgWParam);
|
||||
|
||||
bool HandleMessages_Hook(WPARAM *msgWParam)
|
||||
{
|
||||
bool result = HandleMessages_Original(msgWParam);
|
||||
return noQuitMsgRetrieved &= result;
|
||||
}
|
||||
|
||||
|
||||
// In Freelancer, when you close the server list menu (provided that there were servers listed),
|
||||
// a thread would be created that closes the DirectPlay connection (takes 15-30 seconds to execute).
|
||||
// If you quit the game before the DirectPlay was connection closed, a WaitForSingleObject call would be made
|
||||
// which actually waited indefinitely for the thread to finish.
|
||||
// Consequently, the Freelancer process would remain open until forcefully closed (via Task Manager for instance).
|
||||
// Turns out that whenever the thread calls FreeLibrary after FL's main exit function had already been called,
|
||||
// that FreeLibrary call would never return, and thus the thread would never finish its task.
|
||||
// I believe this was caused by a deadlock. Yet, I could not explain why this deadlock would occur under these circumstances,
|
||||
// nor was I able to come up with a "clean fix" for it. So now instead of calling the function with WaitForSingleObject after the exit,
|
||||
// I call it before the exit. The thread still takes a very long time to close the DirectPlay connection (which I think is a bug too),
|
||||
// but at least there is no more deadlock and the Freelancer process will eventually close, as it should.
|
||||
void InitPostGameDeadlockFix()
|
||||
{
|
||||
#define CGUNWRAPPER_SHUTDOWN_CALL_ADDR 0x5B2190
|
||||
#define FL_EXE_EXIT_CALL_ADDR 0x5B81C6
|
||||
|
||||
Nop(CGUNWRAPPER_SHUTDOWN_CALL_ADDR, 5); // nop out the post-game CGunWrapper::Shutdown() call; call it in the exit hook instead
|
||||
Hook(FL_EXE_EXIT_CALL_ADDR, exit_Hook, 6);
|
||||
}
|
||||
|
||||
// Freelancer has a message handler function which can be called from multiple places.
|
||||
// Normally it is called by the "main" function. However, if for example you see the "disconnected"
|
||||
// dialog, the message handler is actually called from somewhere else.
|
||||
// This is normally not a problem, unless you exit the game while that dialog is showing.
|
||||
// The message handler function returns false if the "Quit" message was retrieved.
|
||||
// When the main caller sees that false was returned, it exits from the loop and shuts down the game.
|
||||
// This does not happen when the disconnected dialog is showing; it will continue handling messages as normal.
|
||||
// The "Quit" message is only retrieved once, so when the main handler takes over, it will continue handling messages forever,
|
||||
// despite the the window being closed by the user.
|
||||
// This hook fixes the problem by always returning false after the message handler returned false at some point.
|
||||
void InitQuitMessageFix()
|
||||
{
|
||||
#define HANDLE_MESSAGES_ADDR 0x5B0B60
|
||||
|
||||
HandleMessages_Original = Trampoline(HANDLE_MESSAGES_ADDR, HandleMessages_Hook, 5);
|
||||
}
|
||||
|
||||
void CleanupQuitMessageFix()
|
||||
{
|
||||
CleanupTrampoline(HandleMessages_Original);
|
||||
}
|
||||
|
||||
// TODO: If anyone would like to look into this further: in dpnet.dll there's a function called "DN_Close" (locate it by downloading the debug symbols from Microsoft).
|
||||
// I believe this function is supposed to represent IDirectPlay8Client::Close. It is this exact function that takes ~40 seconds to return on my end.
|
||||
// This seems strange since in all online examples I could find that closed some DirectPlay connection, it is always done on the main thread.
|
||||
// Surely, something must have been done incorrectly in one of the DirectPlay calls. Since DA couldn't figure out what,
|
||||
// they took the band-aid approach and closed the connection on a separate thread.
|
||||
// Otherwise the screen freezes for 40 seconds every time the server list menu is closed.
|
||||
|
||||
// TODO Idea: File offset 0x30896 in gundll.dll. This is a call to IDirectPlay8Client::Connect.
|
||||
// phAsyncHandle
|
||||
// A DPNHANDLE. When the method returns, phAsyncHandle will point to a handle that you can pass to IDirectPlay8Client::CancelAsyncOperation to cancel the operation.
|
||||
// This parameter must be set to NULL if you set the DPNCONNECT_SYNC flag in dwFlags.
|
||||
// What happens if you call IDirectPlay8Client::CancelAsyncOperation before? See dplay.doc in Downloads folder.
|
||||
// IDirectPlay8Client::CancelAsyncOperation
|
||||
// Cancels asynchronous requests. Many methods of the IDirectPlay8Client interface run asynchronously by default. Depending on the situation, you might want to cancel requests before they are processed. All the methods of this interface that can be run asynchronously return a hAsyncHandle parameter.
|
||||
// Specific requests are canceled by passing the hAsyncHandle of the request in this method’s hAsyncHandle parameter. You can cancel all pending asynchronous operations by calling this method, specifying NULL in the hAsyncHandle parameter, and specifying DPNCANCEL_ALL_OPERATIONS in the dwFlags parameter. If a specific handle is provided to this method, no flags should be set.
|
||||
// DirectPlayClient->CancelAsyncOperation( NULL, DPNCANCEL_ALL_OPERATIONS ); Find where DirectPlayClient is
|
||||
|
||||
// gundll.dll: file offset 0x8376. change xor esi, esi in the function call to mov esi, 1. This fixes the 30 second timer
|
||||
// Test if this works in Win XP too
|
||||
// dalib.dll: file offset 0x4C82 = load library call of gundll.dll. Use this to set hooks
|
||||
// dxcheckOK( DirectPlayClient->Close(DPNCLOSE_IMMEDIATE) ); // WARNING DPNCLOSE_IMMEDIATE is a DP feature from DirectX 9 (released shortly after FL came out)
|
||||
// SafeRelease( DirectPlayClient );
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#include "feature_config.h"
|
||||
#include "Common.h"
|
||||
|
||||
void FeatureManager::RegisterFeature(LPCSTR name, void (*initFunc)(), void (*cleanupFunc)(), bool (*applyPredicate)())
|
||||
{
|
||||
// Enable the feature by default.
|
||||
FlSharpFeature feature { initFunc, cleanupFunc, applyPredicate, true };
|
||||
features.emplace(CreateID(name), feature);
|
||||
}
|
||||
|
||||
bool FeatureManager::SetFeatureEnabled(LPCSTR name, bool enabled)
|
||||
{
|
||||
const auto it = features.find(CreateID(name));
|
||||
|
||||
if (it == features.end())
|
||||
return false;
|
||||
|
||||
it->second.enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
void FeatureManager::InitFeatures()
|
||||
{
|
||||
for (const auto& it : features)
|
||||
{
|
||||
const FlSharpFeature& feature = it.second;
|
||||
|
||||
if (feature.enabled && feature.initFunc && feature.applyPredicate())
|
||||
feature.initFunc();
|
||||
}
|
||||
}
|
||||
|
||||
void FeatureManager::CleanupFeatures()
|
||||
{
|
||||
for (const auto& it : features)
|
||||
{
|
||||
const FlSharpFeature& feature = it.second;
|
||||
|
||||
if (feature.enabled && feature.cleanupFunc && feature.applyPredicate())
|
||||
feature.cleanupFunc();
|
||||
}
|
||||
}
|
||||
|
||||
bool ApplyAlways()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApplyOnlyOnClient()
|
||||
{
|
||||
return !IsMPServer();
|
||||
}
|
||||
|
||||
bool ApplyOnlyOnServer()
|
||||
{
|
||||
return IsMPServer();
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include "fl_math.h"
|
||||
|
||||
#define M_PI 3.14159265358979323846f
|
||||
|
||||
Quaternion MatrixToQuaternion(const Matrix& m)
|
||||
{
|
||||
Quaternion result;
|
||||
|
||||
result.w = sqrtf(std::max(0.0f, 1 + m.data[0][0] + m.data[1][1] + m.data[2][2])) / 2;
|
||||
result.x = sqrtf(std::max(0.0f, 1 + m.data[0][0] - m.data[1][1] - m.data[2][2])) / 2;
|
||||
result.y = sqrtf(std::max(0.0f, 1 - m.data[0][0] + m.data[1][1] - m.data[2][2])) / 2;
|
||||
result.z = sqrtf(std::max(0.0f, 1 - m.data[0][0] - m.data[1][1] + m.data[2][2])) / 2;
|
||||
result.x = copysign(result.x, m.data[2][1] - m.data[1][2]);
|
||||
result.y = copysign(result.y, m.data[0][2] - m.data[2][0]);
|
||||
result.z = copysign(result.z, m.data[1][0] - m.data[0][1]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float QuaternionDotProduct(const Quaternion &left, const Quaternion &right)
|
||||
{
|
||||
return left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w;
|
||||
}
|
||||
|
||||
float QuaternionAngleDifference(const Quaternion &left, const Quaternion &right)
|
||||
{
|
||||
float dot = QuaternionDotProduct(left, right);
|
||||
return acosf(fabsf(dot)) * 2 * (180.0f / M_PI);
|
||||
}
|
||||
|
||||
float GetRotationDelta(const Quaternion& quat, const Matrix& rot)
|
||||
{
|
||||
return QuaternionAngleDifference(quat, MatrixToQuaternion(rot));
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
#include "flash_particles.h"
|
||||
#include "Common.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
// This hook gets called when Freelancer wants to play a flash effect animation.
|
||||
// We intercept this call to play the flash effect for every barrel.
|
||||
NAKED void PlayFlashEffect_Hook()
|
||||
{
|
||||
#define PLAY_FLASH_EFFECT_RET_ADDR 0x52D271
|
||||
|
||||
__asm {
|
||||
mov ecx, ebx // CliLauncher*
|
||||
push esi // ID_String&
|
||||
call CliLauncher::PlayAllFlashParticles
|
||||
mov eax, PLAY_FLASH_EFFECT_RET_ADDR
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
// This function has some asm setup code which redirects us to FLs original code
|
||||
// to allow the flash particle to play on a given barrel index.
|
||||
// This is convenient because this way we are reusing FL's own code.
|
||||
NAKED void CliLauncher::PlayFlashParticleForBarrel(const ID_String& effectName, UINT barrelIndex)
|
||||
{
|
||||
#define GET_BARREL_INFO_FOR_FLASH_PROJ_CALL_ADDR 0x52D1DC
|
||||
|
||||
__asm {
|
||||
sub esp, 0x58
|
||||
push ebx
|
||||
push ebp
|
||||
push esi
|
||||
push edi
|
||||
mov ebx, [esp+0x6C] // CliLauncher*
|
||||
mov ecx, [ebx+0x4] // CELauncher*
|
||||
mov esi, [esp+0x70] // ID_String&
|
||||
push [esp+0x74] // barrel index
|
||||
mov eax, GET_BARREL_INFO_FOR_FLASH_PROJ_CALL_ADDR
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
// In this function we play the flash particle effect for every barrel, instead of only the first barrel.
|
||||
// We do this by keeping track of a custom heap-allocated array of size n (n = amount of barrels of the launcher).
|
||||
void CliLauncher::PlayAllFlashParticles(const ID_String& effectName)
|
||||
{
|
||||
UINT barrelAmount = this->launcher->GetProjectilesPerFire();
|
||||
|
||||
// Create the flash particles array if it doesn't exist yet.
|
||||
// TODO: Check for potential memory leaks due to copy constructors, etc.
|
||||
// Can be checked by keeping track of amount of "new" and "delete" calls and verifying whether they are the same.
|
||||
if (!this->flashParticlesArr)
|
||||
this->flashParticlesArr = new EffectInstance*[barrelAmount]();
|
||||
|
||||
for (UINT i = 0; i < barrelAmount; ++i)
|
||||
{
|
||||
// Clean up the previous instance.
|
||||
if (this->flashParticlesArr[i])
|
||||
{
|
||||
this->flashParticlesArr[i]->GeneralDealloc();
|
||||
this->flashParticlesArr[i] = nullptr;
|
||||
}
|
||||
|
||||
// The PlayFlashParticleForBarrel function stores the effect instance in the currentFlashParticle variable (provided creation was successful).
|
||||
// However, this offset also stores our custom array.
|
||||
// So temporarily keep a copy of the original array pointer, and after calling the function,
|
||||
// save the instance in the original array, and then restore the array at the original offset.
|
||||
EffectInstance** ogFlashParticlesArr = this->flashParticlesArr;
|
||||
PlayFlashParticleForBarrel(effectName, i);
|
||||
ogFlashParticlesArr[i] = this->currentFlashParticle;
|
||||
this->flashParticlesArr = ogFlashParticlesArr;
|
||||
}
|
||||
}
|
||||
|
||||
void CliLauncher::CleanFlashParticlesArr(void (EffectInstance::*deallocFunc)())
|
||||
{
|
||||
UINT barrelAmount = this->launcher->GetProjectilesPerFire();
|
||||
|
||||
// Deallocate all active flash particle instances.
|
||||
for (UINT i = 0; i < barrelAmount; ++i)
|
||||
{
|
||||
if (flashParticlesArr[i])
|
||||
(flashParticlesArr[i]->*deallocFunc)();
|
||||
}
|
||||
|
||||
// Destruct the array.
|
||||
delete[] this->flashParticlesArr;
|
||||
}
|
||||
|
||||
// The three hooks below are there to ensure that all flash particles stored in the new array are cleaned.
|
||||
// Hence we hook the instances where FL tries to clean up the individual object, and clean up the whole array instead.
|
||||
// There are three different versions of this hook because for each instance the game calls a different sequence of functions for the cleaning.
|
||||
void CliLauncher::CleanFlashParticlesPostGame_Hook()
|
||||
{
|
||||
CleanFlashParticlesArr(&EffectInstance::PostGameDealloc);
|
||||
}
|
||||
|
||||
void CliLauncher::CleanFlashParticlesEngine_Hook()
|
||||
{
|
||||
CleanFlashParticlesArr(&EffectInstance::EngineDealloc);
|
||||
}
|
||||
|
||||
void CliLauncher::CleanFlashParticlesMemory_Hook()
|
||||
{
|
||||
CleanFlashParticlesArr(&EffectInstance::DoFreeHeapMemory);
|
||||
this->flashParticlesArr = NULL;
|
||||
}
|
||||
|
||||
FL_FUNC(void EffectInstance::FreeAleEffect(), 0x4F8110)
|
||||
FL_FUNC(int EffectInstance::FreeHeapMemory(), 0x4F7A90)
|
||||
FL_FUNC(void EffectInstance::SetBaseWatcher(int unk1, int unk2, const WatcherInfo& watcherInfo), 0x4F7D20)
|
||||
|
||||
// In vanilla Freelancer, if you fire any launcher with a flash particle, the game explicitly plays the particle on barrel index 0 only.
|
||||
// For most launchers this isn't an issue, but if you have a multi-barrel launcher, the flash effect will only play on the first barrel.
|
||||
void InitFlashParticlesFix()
|
||||
{
|
||||
#define PLAY_FLASH_EFFECT_ADDR 0x52D1B4
|
||||
#define CLI_LAUNCHER_POST_GAME_CLEANUP_ADDR 0x52CAF3
|
||||
#define CLI_LAUNCHER_POST_GAME_FREE_HEAP_CALL_ADDR 0x52CB6B
|
||||
#define CLI_LAUNCHER_RELEASE_MEMORY_ADDR 0x52F6B2
|
||||
|
||||
Hook(PLAY_FLASH_EFFECT_ADDR, PlayFlashEffect_Hook, 5, true);
|
||||
|
||||
BYTE ecxPatch[] = { 0x89, 0xF1, 0x90 }; // mov ecx, esi followed by nop
|
||||
|
||||
Patch(CLI_LAUNCHER_POST_GAME_CLEANUP_ADDR, ecxPatch, sizeof(ecxPatch) - 1); // mov ecx, esi
|
||||
Patch<WORD>(CLI_LAUNCHER_POST_GAME_CLEANUP_ADDR + 0x2, 0x74EB); // jmp
|
||||
Hook(CLI_LAUNCHER_POST_GAME_FREE_HEAP_CALL_ADDR, &CliLauncher::CleanFlashParticlesPostGame_Hook, 5);
|
||||
|
||||
Patch(CLI_LAUNCHER_RELEASE_MEMORY_ADDR, ecxPatch, sizeof(ecxPatch));
|
||||
Hook(CLI_LAUNCHER_RELEASE_MEMORY_ADDR + 0x3, &CliLauncher::CleanFlashParticlesMemory_Hook, 5);
|
||||
|
||||
const DWORD engineDeallocCalls[] = { 0x52CD0F, 0x52D68D, 0x52D836, 0x52DBC7 };
|
||||
for (const auto& call : engineDeallocCalls)
|
||||
{
|
||||
Nop(call, 6);
|
||||
Patch(call + 0x6, ecxPatch, sizeof(ecxPatch) - 1); // mov ecx, esi
|
||||
Hook(call + 0x8, &CliLauncher::CleanFlashParticlesEngine_Hook, 5);
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#include "group_members.h"
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
#include "logger.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
#define NEUTRAL_REP (0.0f)
|
||||
|
||||
FL_FUNC(AttitudeType GetAttitudeType(const IObjRW* towards, const IObjRW* from), 0x45A490)
|
||||
|
||||
float hostileRepThreshold = -0.6f;
|
||||
|
||||
int FASTCALL get_attitude_towards_Hook(const IObjRW& target, float& attitude, const IObjRW* player)
|
||||
{
|
||||
int result = target.get_attitude_towards(attitude, player);
|
||||
|
||||
// FL doesn't test the return value so why should I?
|
||||
// Check if the reported attitude is hostile and if the target is a player.
|
||||
// As a sanity check I'm also checking if the "player" actually is the player.
|
||||
if (attitude <= hostileRepThreshold && target.is_player()
|
||||
&& player && player == GetPlayerIObjRW() && player->cobject)
|
||||
{
|
||||
if (AreIObjRWsInSameGroup(*player, target))
|
||||
{
|
||||
// Set the attitude to a value such that FL's return value check thinks the ship is non-hostile.
|
||||
attitude = NEUTRAL_REP;
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
AttitudeType GetAttitudeType_Hook(const IObjRW* towards, const IObjRW* from)
|
||||
{
|
||||
// Call the original function.
|
||||
AttitudeType result = GetAttitudeType(towards, from);
|
||||
|
||||
if (result != AttitudeType::Hostile)
|
||||
return result;
|
||||
|
||||
// If GetAttitudeType returned Attitude::Hostile, that implies towards and from are both non-zero.
|
||||
// Check if the towards object is the player and if "from" is another player.
|
||||
if (from->is_player() && towards == GetPlayerIObjRW() && towards != from && towards->cobject)
|
||||
{
|
||||
// If they're in the same group, treat them as neutral rather than hostile.
|
||||
if (AreIObjRWsInSameGroup(*towards, *from))
|
||||
{
|
||||
return AttitudeType::Neutral;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#define NEUTRAL_ATTITUDE_IDS 1589
|
||||
#define GROUP_MEMBER_IDS 1551
|
||||
|
||||
// Prints "GROUP MEMBER" as the "ATTITUDE" in the current information window if the ship is a group member.
|
||||
void GetAttitudeString_Hook(const IObjRW& towards, const IObjRW* from)
|
||||
{
|
||||
UINT ids;
|
||||
|
||||
// Is the target a group member?
|
||||
if (from && from->is_player() && AreIObjRWsInSameGroup(towards, *from))
|
||||
{
|
||||
// TODO: GROUP_MEMBER_IDS is capitalized, as opposed to the attitude IDS'.
|
||||
// It could be converted to lowercase using towlower but this may not work on localizations that use non-Latin characters.
|
||||
// For now I changed the other attitude IDS' to capitalized versions as well, since the "ATTITUDE: " prefix is spelled in all caps too.
|
||||
ids = GROUP_MEMBER_IDS;
|
||||
}
|
||||
else
|
||||
{
|
||||
AttitudeType attitude = GetAttitudeType(&towards, from);
|
||||
ids = (UINT) ((int) NEUTRAL_ATTITUDE_IDS - attitude);
|
||||
}
|
||||
|
||||
GetFlString(ids, FL_BUFFER_1, FL_BUFFER_LEN);
|
||||
}
|
||||
|
||||
// Called as part of the "Closest Enemy" function.
|
||||
// CShip is the player and obj is the target candidate.
|
||||
// We want to ensure group members cannot be chosen as nearest enemies.
|
||||
bool CShip::is_enemy_Hook(IObjInspect *obj)
|
||||
{
|
||||
bool enemy = is_enemy(obj);
|
||||
|
||||
if (enemy && obj->is_player())
|
||||
{
|
||||
return !AreShipsInSameGroup(this, (CShip*) obj->cobject);
|
||||
}
|
||||
|
||||
return enemy;
|
||||
}
|
||||
|
||||
// In Freelancer, it's not possible to enter formation with group members that are hostile to you.
|
||||
// This code fixes that by checking if the player's selected target is a group member.
|
||||
void InitHostileGroupFormation()
|
||||
{
|
||||
#define GROUP_FORMATION_REP_CHECK_COMMON_OFFSET 0x6C37C
|
||||
#define HOSTILE_REP_THRESHOLD_COMMON_OFFSET 0x13F540
|
||||
|
||||
DWORD commonHandle = (DWORD) GetModuleHandle("common.dll");
|
||||
|
||||
if (commonHandle)
|
||||
{
|
||||
Hook(commonHandle + GROUP_FORMATION_REP_CHECK_COMMON_OFFSET, get_attitude_towards_Hook, 6);
|
||||
hostileRepThreshold = *(float*) (commonHandle + HOSTILE_REP_THRESHOLD_COMMON_OFFSET);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::PrintModuleError("InitHostileGroupFormation", "common.dll");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensures hostile group members are no longer treated as hostile.
|
||||
// For example if you are near a hostile group member, then you will hear the danger/battle music.
|
||||
// For such group members there is also an attack marker displayed.
|
||||
// These things can be quite distracting. The code below ensures they are treated as neutral instead.
|
||||
void InitHostileGroupMembersFix()
|
||||
{
|
||||
// Doing a trampoline hook was inconvenient here, so just manually hook all the call locations,
|
||||
// except for 0x475770 which should be handled by the TODO below.
|
||||
const DWORD getAttitudeTypeCalls[] = {
|
||||
0x48AEAB, 0x4E4950, 0x4EC10E, 0x4EC71A, 0x4EC891, 0x4F1CFF, 0x4F22E4,
|
||||
0x4F2465, 0x53A98C, 0x553290, 0x5532AD, 0x553325, 0x5552A8 };
|
||||
|
||||
for (const auto &call : getAttitudeTypeCalls)
|
||||
SetRelPointer(call + 1, GetAttitudeType_Hook);
|
||||
|
||||
// Fixes enemy group members being selected as "nearest enemies".
|
||||
#define NEAREST_ENEMY_CHECK_ADDR (0x544A8E)
|
||||
Hook(NEAREST_ENEMY_CHECK_ADDR, &CShip::is_enemy_Hook, 6);
|
||||
}
|
||||
|
||||
// If the Current Information window is opened on a group member, this code will make it show "GROUP MEMBER" as the attitude.
|
||||
void InitGroupMemberAttitudeFix()
|
||||
{
|
||||
#define GET_ATTITUDE_TYPE_CURRENT_INFO_ADDR 0x475770
|
||||
#define CLEAN_STACK_GET_ATTITUDE_STRING_ADDR 0x47579F
|
||||
#define ATTITUDE_CHECK_CURRENT_INFO_ADDR 0x4757A2
|
||||
#define CLEAN_WCSCAT_STACK_ADDR 0x47580B
|
||||
|
||||
Nop(GET_ATTITUDE_TYPE_CURRENT_INFO_ADDR, 5); // wipe out GetAttitudeType call
|
||||
GetValue<BYTE>(CLEAN_STACK_GET_ATTITUDE_STRING_ADDR + 2) -= sizeof(DWORD) * 2; // ensure "towards" and "from" remain on the stack
|
||||
Hook(ATTITUDE_CHECK_CURRENT_INFO_ADDR, GetAttitudeString_Hook, 5);
|
||||
Patch<WORD>(ATTITUDE_CHECK_CURRENT_INFO_ADDR + 5, 0x56EB); // Jump directly to wcscat after our hook executed
|
||||
GetValue<BYTE>(CLEAN_WCSCAT_STACK_ADDR + 2) -= sizeof(DWORD) * 2; // two params were removed so do not clean them up
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
#include "infocards.h"
|
||||
#include "Common.h"
|
||||
#include "utils.h"
|
||||
#include "logger.h"
|
||||
|
||||
std::map<UINT, UINT> msnBaseIdsInfoMap;
|
||||
std::map<UINT, UINT> msnNicknameIdsInfoMap;
|
||||
|
||||
void ParseEntries(INI_Reader& reader, const std::map<UINT, InfocardEntry>& entries)
|
||||
{
|
||||
while (reader.read_header())
|
||||
{
|
||||
const auto it = entries.find(CreateID(reader.get_header_ptr()));
|
||||
|
||||
if (it == entries.end())
|
||||
{
|
||||
Logger::PrintInvalidHeaderWarning("ParseEntries", reader.get_header_ptr(), reader.get_file_name());
|
||||
continue;
|
||||
}
|
||||
|
||||
UINT key = 0, value = 0;
|
||||
|
||||
while (reader.read_value())
|
||||
{
|
||||
if (reader.is_value(it->second.key))
|
||||
{
|
||||
key = reader.get_value_id();
|
||||
}
|
||||
else if (reader.is_value(it->second.value))
|
||||
{
|
||||
value = reader.get_value_int();
|
||||
}
|
||||
}
|
||||
|
||||
it->second.map.emplace(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses the MissionCreatedSolars.ini file and for every solar stores its ids_info in a map.
|
||||
void ParseMsnCreatedSolars(LPCSTR iniPath)
|
||||
{
|
||||
INI_Reader reader;
|
||||
|
||||
if (!reader.open(iniPath))
|
||||
{
|
||||
Logger::PrintFileOpenError("ParseMsnCreatedSolars", iniPath);
|
||||
return;
|
||||
}
|
||||
|
||||
std::map<UINT, InfocardEntry> entries = {
|
||||
{ CreateID("MissionCreatedSolar"), { msnBaseIdsInfoMap, "base", "ids_info" } },
|
||||
{ CreateID("MissionCreatedNonDockableSolar"), { msnNicknameIdsInfoMap, "nickname", "ids_info" } }
|
||||
};
|
||||
|
||||
ParseEntries(reader, entries);
|
||||
reader.close();
|
||||
}
|
||||
|
||||
bool FindValueInMap(std::map<UINT, UINT>& map, UINT key, UINT& foundValue)
|
||||
{
|
||||
auto it = map.find(key);
|
||||
if (it != map.end())
|
||||
{
|
||||
foundValue = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void GetAltSolarIdsInfo(const CSolar* solar, UINT &idsInfo)
|
||||
{
|
||||
const Archetype::Solar* solarArch = solar->solararch();
|
||||
|
||||
if (solarArch->idsInfo)
|
||||
idsInfo = solarArch->idsInfo;
|
||||
// Showing the solar's own name as the infocard doesn't really add any value.
|
||||
// else if (UINT solarIdsName = solar->get_name())
|
||||
// idsInfo = solarIdsName;
|
||||
else
|
||||
idsInfo = solarArch->idsName;
|
||||
}
|
||||
|
||||
// Function which Freelancer calls to obtain the ids infocard of the selected object in the Current Info window.
|
||||
int GetInfocard_Hook(const CObject& selectedObj, const int &id, UINT &idsInfo)
|
||||
{
|
||||
// Is the selected object a solar?
|
||||
if (const CSolar* solar = CSolar::cast(selectedObj))
|
||||
{
|
||||
if (solar->is_dynamic())
|
||||
{
|
||||
// Try to find the idsInfo in the base map.
|
||||
if (solar->is_base() && FindValueInMap(msnBaseIdsInfoMap, solar->baseId, idsInfo))
|
||||
return S_OK;
|
||||
|
||||
// Otherwise try the nickname map.
|
||||
if (FindValueInMap(msnNicknameIdsInfoMap, solar->nickname, idsInfo))
|
||||
return S_OK;
|
||||
|
||||
// GetInfocard will never return a correct infocard for dynamic solars, so don't bother calling it.
|
||||
// Try the alternatives as a last resort.
|
||||
GetAltSolarIdsInfo(solar, idsInfo);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
int result = Reputation::Vibe::GetInfocard(id, idsInfo);
|
||||
if (!idsInfo || result != S_OK)
|
||||
{
|
||||
// If a non-dynamic solar doesn't have an infocard, use one of the alternatives.
|
||||
GetAltSolarIdsInfo(solar, idsInfo);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// If the selected object isn't a solar, get the infocard by calling the original function.
|
||||
return Reputation::Vibe::GetInfocard(id, idsInfo);
|
||||
}
|
||||
|
||||
// In Freelancer, when opening the Current Info window on a dynamic solar, it won't display its infocard.
|
||||
// Presumably this happens because they are not stored by the server.
|
||||
// A workaround is to first parse MissionCreatedSolars.ini and store the values.
|
||||
// Then hook the get infocard function for the Current Info window, check if the selected object is a dynamic solar,
|
||||
// if so, return the stored ids_info.
|
||||
void InitDynamicSolarInfocards()
|
||||
{
|
||||
// Get the full path to MissionCreatedSolars.ini dynamically.
|
||||
char fullIniPath[MAX_PATH];
|
||||
strcpy_s(fullIniPath, sizeof(fullIniPath), "..\\DATA\\");
|
||||
LPCSTR relIniPath = GetValue<LPCSTR>(0x476C7A); // Universe\\MissionCreatedSolars.ini
|
||||
strcat_s(fullIniPath, sizeof(fullIniPath), relIniPath);
|
||||
|
||||
ParseMsnCreatedSolars(fullIniPath);
|
||||
|
||||
// Add a "push esi" instruction so we can check out the selected CObject in our hook.
|
||||
#define GET_INFOCARD_CURRENT_INFO_CALL_ADDR 0x475BD8
|
||||
Patch<BYTE>(GET_INFOCARD_CURRENT_INFO_CALL_ADDR, 0x56); // push esi (CObject&)
|
||||
Hook(GET_INFOCARD_CURRENT_INFO_CALL_ADDR + 1, GetInfocard_Hook, 5);
|
||||
|
||||
// Fix the stack offset of the return value (shifted by 4 bytes due to the added parameter).
|
||||
#define GET_INFOCARD_IDS_STACK_OFFSET 0x475BE1
|
||||
GetValue<BYTE>(GET_INFOCARD_IDS_STACK_OFFSET) += sizeof(DWORD);
|
||||
|
||||
// Increase the amount of stack bytes cleaned because the GetInfocard hook takes an additional parameter.
|
||||
#define GET_INFOCARD_RET_STACK_SIZE 0x475BE4
|
||||
GetValue<BYTE>(GET_INFOCARD_RET_STACK_SIZE) += sizeof(DWORD);
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
#include "logger.h"
|
||||
#include "Dacom.h"
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
#ifdef ASM_FDUMP
|
||||
NAKED void FDUMP_Asm(DumpSeverity severity, LPCSTR fmt, ...)
|
||||
{
|
||||
#define FL_FDUMP_IMPORT_ADDR 0x5C6D18
|
||||
|
||||
__asm {
|
||||
mov eax, dword ptr ds:[FL_FDUMP_IMPORT_ADDR]
|
||||
jmp dword ptr ds:[eax]
|
||||
}
|
||||
}
|
||||
|
||||
#define FDUMP_FUNC FDUMP_Asm
|
||||
#else
|
||||
#define FDUMP_FUNC FDUMP
|
||||
#endif
|
||||
|
||||
void Logger::PrintModuleError(LPCSTR functionName, LPCSTR moduleName)
|
||||
{
|
||||
FDUMP_FUNC(DumpSeverity::SEV_ERROR, "FLSharp (%s): Could not get module handle \"%s\".", functionName, moduleName);
|
||||
}
|
||||
|
||||
void Logger::PrintFileOpenError(LPCSTR functionName, LPCSTR filePath)
|
||||
{
|
||||
FDUMP_FUNC(DumpSeverity::SEV_ERROR, "FLSharp (%s): Could not open file \"%s\".", functionName, filePath);
|
||||
}
|
||||
|
||||
void Logger::PrintV10Warning(LPCSTR moduleName)
|
||||
{
|
||||
FDUMP_FUNC(DumpSeverity::SEV_WARNING, "FLSharp: %s may be v1.0 while v1.1 is assumed. "
|
||||
"Please install the official 1.1 patch, or proceed at your own risk.", moduleName);
|
||||
}
|
||||
|
||||
void Logger::PrintInvalidFeatureWarning(LPCSTR functionName, LPCSTR featureName, LPCSTR iniPath)
|
||||
{
|
||||
FDUMP_FUNC(DumpSeverity::SEV_WARNING,
|
||||
"FLSharp (%s): invalid feature name \"%s\" found in file \"%s\". See \"src/main.cpp\" for a full list of supported features.",
|
||||
functionName, featureName, iniPath);
|
||||
}
|
||||
|
||||
void Logger::PrintInvalidHeaderWarning(LPCSTR functionName, LPCSTR headerName, LPCSTR iniPath)
|
||||
{
|
||||
FDUMP_FUNC(DumpSeverity::SEV_WARNING, "FLSharp (%s): invalid header \"%s\" found in file \"%s\".",
|
||||
functionName, headerName, iniPath);
|
||||
}
|
||||
Vendored
+123
@@ -0,0 +1,123 @@
|
||||
#include "feature_config.h"
|
||||
#include "config_reader.h"
|
||||
#include "version_check.h"
|
||||
#include "logger.h"
|
||||
#include "dacom.h"
|
||||
#include "update.h"
|
||||
#include "waypoint.h"
|
||||
#include "waypoint_names.h"
|
||||
#include "projectiles.h"
|
||||
#include "resolutions.h"
|
||||
#include "test_sounds.h"
|
||||
#include "trade_lane_lights.h"
|
||||
#include "copy_paste.h"
|
||||
#include "ui_anim.h"
|
||||
#include "weapon_anim.h"
|
||||
#include "flash_particles.h"
|
||||
#include "rep_requirements.h"
|
||||
#include "exit.h"
|
||||
#include "temp_fixes.h"
|
||||
#include "infocards.h"
|
||||
#include "save_crash.h"
|
||||
#include "alchemy_crash.h"
|
||||
#include "blank_faction.h"
|
||||
#include "server_filter.h"
|
||||
#include "dll_crash.h"
|
||||
#include "shield_capacity.h"
|
||||
#include "dealer_fixes.h"
|
||||
#include "cheat_detection.h"
|
||||
#include "group_members.h"
|
||||
#include "cursor_colors.h"
|
||||
#include "base_info.h"
|
||||
#include "mouse.h"
|
||||
#include "pilot_names.h"
|
||||
|
||||
FeatureManager manager;
|
||||
|
||||
void CheckDllVersions()
|
||||
{
|
||||
// Stores for each DLL its module name and known 1.0 build version.
|
||||
std::pair<LPCSTR, UINT32> dlls[] =
|
||||
{
|
||||
{ "common.dll", 1223 },
|
||||
{ "server.dll", 1223 },
|
||||
};
|
||||
|
||||
// Checks if any of the DLLs are 1.0 instead of 1.1.
|
||||
for (const auto &dll : dlls)
|
||||
{
|
||||
// If the build version is anything higher than the 1.0 build, we'll consider it 1.1.
|
||||
if (GetDllProductBuildVersion(dll.first) <= dll.second)
|
||||
{
|
||||
Logger::PrintV10Warning(dll.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
// All registered features must be able to work independently of each other.
|
||||
// They must not assume a certain load order or that another feature is active/inactive.
|
||||
manager.RegisterFeature("better_updates", InitBetterUpdates, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("waypoint_fixes", InitWaypointFixes, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("waypoint_name_fixes", InitWaypointNameFixes, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("projectiles_sound_fix", InitProjectilesSoundFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("projectiles_server_fix", InitProjectilesServerFix, nullptr, ApplyAlways);
|
||||
manager.RegisterFeature("better_resolutions", InitBetterResolutions, CleanupBetterResolutions, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("more_test_sounds", InitTestSounds, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("trade_lane_lights_fix", InitTradeLaneLightsFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("copy_paste_feature", InitCopyPasteFeature, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("slide_ui_anim_fix", InitSlideUiAnimFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("weapon_anim_fix", InitWeaponAnimFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("flash_particle_fix", InitFlashParticlesFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("print_rep_requirements", InitPrintRepRequirements, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("post_game_deadlock_fix", InitPostGameDeadlockFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("quit_message_fix", InitQuitMessageFix, CleanupQuitMessageFix, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("flight_controls_fix", InitFlightControlsFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("dynamic_solar_infocards", InitDynamicSolarInfocards, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("save_crash_fix", InitSaveCrashFix, nullptr, ApplyAlways);
|
||||
manager.RegisterFeature("alchemy_crash_fix", InitAlchemyCrashFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("blank_faction_fix", InitBlankFactionNameFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("server_filter_crash_fix", InitServerFilterCrashFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("server_filter_speed_fix", InitServerFilterSpeedFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("freelancer_dll_crash_fix", InitMissingDllCrashFix, nullptr, ApplyAlways);
|
||||
manager.RegisterFeature("shield_capacity_fix", InitShieldCapacityFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("dealer_menu_open_fix", InitDealerOpenFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("dealer_crash_fix", InitDealerCrashFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("ship_buy_kick_fix", InitShipBuyKickFix, nullptr, ApplyOnlyOnServer);
|
||||
manager.RegisterFeature("hostile_group_formation", InitHostileGroupFormation, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("unhostile_group_members", InitHostileGroupMembersFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("group_member_attitude", InitGroupMemberAttitudeFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("more_cursor_colors", InitMoreCursorColors, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("base_info_spacing_fix", InitBaseInfoSpacingFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("cursor_fix", InitCursorFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("mouse_warp_fix", InitMouseWarpFix, nullptr, ApplyOnlyOnClient);
|
||||
manager.RegisterFeature("pilot_names_fix", InitPilotNamesFix, nullptr, ApplyOnlyOnClient);
|
||||
|
||||
ReadConfig("FLSharp.ini", manager);
|
||||
|
||||
manager.InitFeatures();
|
||||
}
|
||||
|
||||
void Cleanup()
|
||||
{
|
||||
manager.CleanupFeatures();
|
||||
}
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(lpReserved);
|
||||
|
||||
if (fdwReason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
CheckDllVersions();
|
||||
Init();
|
||||
}
|
||||
else if (fdwReason == DLL_PROCESS_DETACH)
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
Vendored
+133
@@ -0,0 +1,133 @@
|
||||
#include "mouse.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
|
||||
#define MOUSE_X (*(int*) 0x616840)
|
||||
#define MOUSE_Y (*(int*) 0x616844)
|
||||
|
||||
#define WINDOW_WIDTH (*(int*) 0x679BC8)
|
||||
#define WINDOW_HEIGHT (*(int*) 0x679BCC)
|
||||
|
||||
#define SHOW_MOUSE_CURSOR (*(bool*) 0x6107DC)
|
||||
|
||||
#define FL_HWND (*(HWND*) 0x67ECA0)
|
||||
|
||||
#define FL_RES_FLAGS (*(PDWORD) 0x679BE5)
|
||||
#define FULLSCREEN_FLAG (1)
|
||||
|
||||
// Hook that prevents the in-game cursor from being shown when it is outside the game window.
|
||||
bool ShowMouseCursor_Hook()
|
||||
{
|
||||
if (MOUSE_X < 0 || MOUSE_Y < 0)
|
||||
return false;
|
||||
|
||||
if (MOUSE_X >= WINDOW_WIDTH || MOUSE_Y >= WINDOW_HEIGHT)
|
||||
return false;
|
||||
|
||||
return SHOW_MOUSE_CURSOR;
|
||||
}
|
||||
|
||||
bool IsGameFullscreen()
|
||||
{
|
||||
return FL_RES_FLAGS & FULLSCREEN_FLAG;
|
||||
}
|
||||
|
||||
void ForceShowWindowsCursor()
|
||||
{
|
||||
// Same code as FL.
|
||||
while (ShowCursor(TRUE) < 1);
|
||||
}
|
||||
|
||||
#define MOUSE_DEVICE (*(IDirectInputDevice8**) 0x6167C8)
|
||||
|
||||
// Code that FL calls to force show the Windows cursor when it is on the edge of the game window.
|
||||
// FL doesn't properly track mouse when you move the cursor outside the screen.
|
||||
// TODO: Currently in windowed mode, if your mouse is on the bottom edge of the window,
|
||||
// both the in-game and Windows cursor become invisible.
|
||||
// I couldn't get around it because DirectInput threw a wrench.
|
||||
// Every time I wanted to show the Windows cursor right on the bottom edge, it would either start flickering or warp to the center.
|
||||
// I spent way too much time on trying to get it to work so I gave up and just let it be invisible for now.
|
||||
void STDCALL ShowCursor_Hook()
|
||||
{
|
||||
// if (IsGameFullscreen())
|
||||
// {
|
||||
// // Normal routine, although that'll probably never happen because this code is never called in fullscreen mode AFAIK...
|
||||
// ForceShowWindowsCursor();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Get the actual cursor position.
|
||||
POINT p;
|
||||
if (GetCursorPos(&p) && ScreenToClient(FL_HWND, &p))
|
||||
{
|
||||
// TODO: Well, here's an annoying edge case that I can't fix.
|
||||
// If you have the game running in borderless windowed mode on a large primary monitor,
|
||||
// and you move your mouse in the top-left corner towards a smaller secondary monitor on the left,
|
||||
// the mouse cursor is supposed to "teleport" to the top right-corner of the second monitor without issues.
|
||||
// This seems to happen fine if you try it for the first time. However, if you give focus to another window on the second monitor,
|
||||
// and then let the FL window regain focus, the mouse cursor will now get stuck if you retry the above steps.
|
||||
// If you place the ForceShowWindowsCursor call at the very start of the function without the if-statements, this issue does not occur.
|
||||
// However, this results in other things being broken, for instance the flickering of the cursor when the mouse is near the edges.
|
||||
// The exact purpose of this hook is to fix this flickering bug in particular.
|
||||
if (p.x < 0 || p.y < 0 || p.x >= WINDOW_WIDTH || p.y > WINDOW_HEIGHT)
|
||||
{
|
||||
ForceShowWindowsCursor();
|
||||
}
|
||||
|
||||
// Prevent the in-game mouse cursor from going outside the screen.
|
||||
MOUSE_X = p.x;
|
||||
MOUSE_Y = p.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Fixes the mouse snapping to the center of the game in (borderless) windowed mode.
|
||||
long FASTCALL Acquire_Hook(IDirectInputDevice8& mouseDevice)
|
||||
{
|
||||
// Acquire can sometimes warp the mouse to the center of the game window in (borderless) windowed mode.
|
||||
// To fix that, we set the cursor position to the original value after the Acquire call.
|
||||
long result = mouseDevice.Acquire();
|
||||
|
||||
if (result == S_OK && !IsGameFullscreen())
|
||||
{
|
||||
POINT p{ MOUSE_X, MOUSE_Y };
|
||||
|
||||
// TODO: Get the original cursor position by calling GetCursorPos before Acquire instead?
|
||||
if (ClientToScreen(FL_HWND, &p))
|
||||
SetCursorPos(p.x, p.y);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fixes the in-game mouse cursor remaining visible in windowed mode
|
||||
// despite the actual cursor being outside the game window.
|
||||
void InitCursorFix()
|
||||
{
|
||||
#define SHOW_MOUSE_CURSOR_CHECK_ADDR 0x41F30A
|
||||
Hook(SHOW_MOUSE_CURSOR_CHECK_ADDR, ShowMouseCursor_Hook, 5);
|
||||
|
||||
#define SHOW_WIN_CURSOR_ADDR 0x420335
|
||||
Hook(SHOW_WIN_CURSOR_ADDR, ShowCursor_Hook, 19);
|
||||
|
||||
// Freelancer appears to have a pretty hard time figuring out
|
||||
// whether the cursor should be shown when it is at the bottom-edge of the window.
|
||||
// It seems it's putting minimal effort into clamping the cursor's y-position near the bottom-edge.
|
||||
// This patch prevents that from happening.
|
||||
#define CURSOR_BOTTOM_BORDER_CHECK_ADDR 0x41EA9B
|
||||
Patch<BYTE>(CURSOR_BOTTOM_BORDER_CHECK_ADDR, 0xEB);
|
||||
// TODO: Do the same for 0x41ECBF? Doesn't look necessary.
|
||||
}
|
||||
|
||||
void InitMouseWarpFix()
|
||||
{
|
||||
#define THIS_PTR_ACQUIRE_MOUSE_ADDR 0x41F7D1
|
||||
#define ACQUIRE_MOUSE_ADDR 0x41F7D3
|
||||
|
||||
Patch<BYTE>(THIS_PTR_ACQUIRE_MOUSE_ADDR, 0x4E); // mov eax, [esi+0x10] -> mov ecx, [esi+0x10]
|
||||
Hook(ACQUIRE_MOUSE_ADDR, Acquire_Hook, 6);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#include "pilot_names.h"
|
||||
#include "fl_func.h"
|
||||
#include "Freelancer.h"
|
||||
#include "utils.h"
|
||||
|
||||
FL_FUNC(size_t GetCShipPilotName(const CSimple &simple, StrBuffer &buffer), 0x5472D0)
|
||||
|
||||
FL_FUNC(bool GetSimpleName(const CSimple &simple, StrBuffer &buffer, NameType type, bool unk), 0x4E8100)
|
||||
|
||||
// GetCShipPilotName calls CShip::get_pilot_name() which returns a truncated string if the length is large.
|
||||
// GetSimpleName on the other hand returns the full string, so this function is preferred if the entire name is needed.
|
||||
// Only problem is that GetSimpleName writes stuff to FL_BUFFER_1 and FL_BUFFER_2.
|
||||
size_t GetCShipPilotName_Hook(const CSimple &simple, StrBuffer &buffer)
|
||||
{
|
||||
// If it's not a ship, just call GetCShipPilotName, otherwise the next code will think it's a pilot.
|
||||
// The function doesn't actually get a name and returns 0 in this case,
|
||||
// but it does allocate the buffer if it's empty.
|
||||
if ((simple.classType & CSHIP_CLASS_TYPE) != CSHIP_CLASS_TYPE)
|
||||
return GetCShipPilotName(simple, buffer);
|
||||
|
||||
bool success = GetSimpleName(simple, buffer, NameType::PilotName, true);
|
||||
|
||||
// GetSimpleName modifies FL_BUFFER_1 and FL_BUFFER_2, so null both buffers
|
||||
// because the code that comes after this may assume that the buffers are nulled.
|
||||
FL_BUFFER_1[0] = FL_BUFFER_2[0] = '\0';
|
||||
|
||||
// Make the return value compatible with that of GetCShipPilotName.
|
||||
// Hence return the string length if successful, otherwise call the original function.
|
||||
return success && buffer.str ? wcslen(buffer.str) : GetCShipPilotName(simple, buffer);
|
||||
}
|
||||
|
||||
// Many UI elements in FL call GetCShipPilotName which returns a string with a max buffer length of 24.
|
||||
// Hence if the pilot name is longer than 23 characters, it will be truncated.
|
||||
// This doesn't matter ofr player names in MP as those are limited to 23 characters.
|
||||
// However, with some NPCs I did notice the names being truncated, in particular with some Transport and Corsair pilots who often have long names.
|
||||
// This issue is fixed by hooking all the GetCShipPilotName calls where we instead call GetSimpleName; this function always returns the full string.
|
||||
void InitPilotNamesFix()
|
||||
{
|
||||
DWORD getPilotNameCallAddrs[] = {
|
||||
0x4756F8, // Current Information window
|
||||
0x48AB1F, // Hand over your cargo or I'll open fire window
|
||||
0x4CB85F, // Comm text
|
||||
//0x4E4400 // Sub-target name in target window (not needed as FL already calls GetSimpleName before this but if that fails it falls back to GetCShipPilotName.)
|
||||
};
|
||||
|
||||
for (auto getPilotNameCallAddr : getPilotNameCallAddrs)
|
||||
Hook(getPilotNameCallAddr, GetCShipPilotName_Hook, 5);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#include "projectiles.h"
|
||||
#include "utils.h"
|
||||
#include "logger.h"
|
||||
#include <algorithm>
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
// We hook this function in one instance where it gets called
|
||||
// because it uses the return value to play the one_shot_sound of the launchers.
|
||||
// When the function returns 2 (i.e. the launcher has two barrels), the one_shot_sound fails to play.
|
||||
// In this hook we make sure the return value is 1 at most, which fixes the bug.
|
||||
// It is not recommended to modify the GetProjectilesPerFire function directly because it gets called in other instances as well.
|
||||
UINT CELauncher::GetProjectilesPerFire_Hook() const
|
||||
{
|
||||
return std::min<UINT>(this->GetProjectilesPerFire(), 1);
|
||||
}
|
||||
|
||||
void InitProjectilesSoundFix()
|
||||
{
|
||||
#define PROJECTILES_PER_FIRE_CALL_ADDR 0x534D0D
|
||||
|
||||
Patch<WORD>(PROJECTILES_PER_FIRE_CALL_ADDR, 0xBB90);
|
||||
SetPointer(PROJECTILES_PER_FIRE_CALL_ADDR + 0x2, &CELauncher::GetProjectilesPerFire_Hook);
|
||||
}
|
||||
|
||||
DWORD playerLauncherFireRet;
|
||||
|
||||
// Hook function that replaces the hard-coded "1" when decrementing ammo with a GetProjectilesPerFire call.
|
||||
// This fixes a bug that makes the server decrement the wrong amount of ammo when a player fires a multi-barrel launcher.
|
||||
// TODO: Can be rewritten to a non-asm hook.
|
||||
NAKED void HandlePlayerLauncherFire_Hook()
|
||||
{
|
||||
__asm {
|
||||
push 0x3F800000 // overwritten instruction (1.0f)
|
||||
xchg ecx, edi // preserve ecx, while also setting the fired CELauncher as the thisptr
|
||||
mov esi, edx // preserve edx
|
||||
call dword ptr [CELauncher::GetProjectilesPerFire]
|
||||
mov ecx, edi // restore ecx
|
||||
mov edx, esi // restore edx
|
||||
push eax // push projectiles per fire
|
||||
jmp [playerLauncherFireRet]
|
||||
}
|
||||
}
|
||||
|
||||
// This function may be executed on both the client and server-side
|
||||
void InitProjectilesServerFix()
|
||||
{
|
||||
// E.g. console.dll enforces the server library to load without causing any issues, so should be fine
|
||||
DWORD serverHandle = GetUnloadedModuleHandle("server.dll");
|
||||
|
||||
if (serverHandle)
|
||||
{
|
||||
playerLauncherFireRet = serverHandle + 0xD91A;
|
||||
|
||||
Hook(serverHandle + 0xD913, HandlePlayerLauncherFire_Hook, 5, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::PrintModuleError("InitProjectilesServerFix", "server.dll");
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#include "rep_requirements.h"
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
#include "fl_func.h"
|
||||
#include <cstdio>
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
UINT insufficientRepIds = 1564;
|
||||
|
||||
FL_FUNC(void NN_Dealer::PrintFmtStrPurchaseInfo(UINT idsPurchaseInfo, int fmtValue), 0x47FD50)
|
||||
|
||||
// Converts the reputation value to a percentage.
|
||||
int GetRepPercentage(float repValue)
|
||||
{
|
||||
return static_cast<int>(repValue * 100.0f);
|
||||
}
|
||||
|
||||
void NN_Dealer::PrintFmtStrPurchaseInfo_Hook(UINT idsPurchaseInfo, const DealerStack& stack)
|
||||
{
|
||||
static BYTE& fmtValIsZeroCheck = GetValue<BYTE>(0x47FE86);
|
||||
BYTE originalCheckValue = fmtValIsZeroCheck;
|
||||
fmtValIsZeroCheck = 0xEB; // allow the rep percentage to be printed if it's 0
|
||||
PrintFmtStrPurchaseInfo(idsPurchaseInfo, GetRepPercentage(stack.repRequired));
|
||||
fmtValIsZeroCheck = originalCheckValue; // restore the original value to prevent other 0's from being unintentionally printed
|
||||
}
|
||||
|
||||
NAKED void GetShipRepRequirement_Hook()
|
||||
{
|
||||
#define STORE_SHIP_REP_REQUIREMENT_RET_ADDR 0x4B9469
|
||||
|
||||
__asm {
|
||||
mov [esi+0xC], eax // overwritten instruction #1
|
||||
push ecx
|
||||
fst dword ptr [esp] // shipLevelRequirement
|
||||
push ebp // shipIndex
|
||||
mov ecx, ebx // NN_ShipTrader
|
||||
call NN_ShipTrader::StoreShipRepRequirement
|
||||
mov eax, [esp+0x10] // overwritten instruction #2
|
||||
mov ecx, STORE_SHIP_REP_REQUIREMENT_RET_ADDR
|
||||
jmp ecx
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the ship index and stores the rep requirement as a percentage in the right location.
|
||||
void NN_ShipTrader::StoreShipRepRequirement(int shipIndex, float repRequirement)
|
||||
{
|
||||
// This code is run in a loop from 0 to shipCount - 1, so the shipIndex should always be valid.
|
||||
this->shipRepPercentages[shipIndex] = GetRepPercentage(repRequirement);
|
||||
}
|
||||
|
||||
LPWSTR NN_ShipTrader::PrintFmtShipRepRequirement()
|
||||
{
|
||||
GetFlString(insufficientRepIds, FL_BUFFER_1, FL_BUFFER_LEN);
|
||||
|
||||
// The selectedShipIndex is always correctly calculated before this code is called.
|
||||
swprintf_s(FL_BUFFER_2, FL_BUFFER_LEN, FL_BUFFER_1, shipRepPercentages[selectedShipIndex]);
|
||||
return FL_BUFFER_2;
|
||||
}
|
||||
|
||||
NAKED void PrintShipRepRequirement_Hook()
|
||||
{
|
||||
#define PRINT_SHIP_REP_REQUIREMENT_RET_ADDR 0x4B9017
|
||||
|
||||
__asm {
|
||||
mov ecx, esi // NN_ShipTrader*
|
||||
call NN_ShipTrader::PrintFmtShipRepRequirement
|
||||
push eax // buffer
|
||||
push 0x1D // 0x1D means print from buffer, 0x1E means print from IDS
|
||||
mov eax, PRINT_SHIP_REP_REQUIREMENT_RET_ADDR
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
PBYTE NN_ShipTrader::SwapShipRepPercentages(PBYTE rhsShipStatusAddr)
|
||||
{
|
||||
#define SHIP_STATUS_PTR_START (offsetof(NN_ShipTrader, shipStatuses))
|
||||
int rhsShipIndex = (rhsShipStatusAddr - (PBYTE) this - SHIP_STATUS_PTR_START) / sizeof(int);
|
||||
|
||||
// Swap the left-hand side and the right-hand side.
|
||||
std::swap(shipRepPercentages[rhsShipIndex - 1], shipRepPercentages[rhsShipIndex]);
|
||||
|
||||
return rhsShipStatusAddr; // restore eax
|
||||
}
|
||||
|
||||
// Fixes the ship rep percentages being wrong when FL reorders the ships.
|
||||
// FL does a stable sort on the ships based on their availability.
|
||||
// This hook is called every time FL swaps two ships as part of the sorting algorithm.
|
||||
// We swap the ship rep percentages to keep them in sync with FL's ship ordering.
|
||||
NAKED void SwapShips_Hook()
|
||||
{
|
||||
// One could do "push edi" to send the lhsShipIndex directly to the function,
|
||||
// but the rhsShipIndex can be calculated from rhsShipStatusAddr.
|
||||
__asm {
|
||||
mov ecx, ebx // NN_ShipTrader*
|
||||
push eax // rhsShipStatusAddr
|
||||
call NN_ShipTrader::SwapShipRepPercentages
|
||||
mov ecx, [eax+0x14] // overwritten instruction #1
|
||||
xor dl, dl // overwritten instruction #2
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// In FL there exists the string "You must be on friendlier terms to purchase this."
|
||||
// which gets printed in the Dealer menu when you do not meet the requirements to purchase
|
||||
// the selected item. The function that's called to print this supports one additional argument
|
||||
// that can be used to replace a format specifier in the provided IDS.
|
||||
// By default the friendlier terms string gets printed with the integer 0 as a dummy argument.
|
||||
// This code replaces that 0 with the reputation required as a percentage from -100 to 100.
|
||||
// If the "friendlier terms" IDS is modified to have "%d" included, then that percentage will be printed too.
|
||||
// Printing this value for the ships is more involving as it requires the value to be format-printed manually.
|
||||
// Moreover, the required ship reputation values have to be saved somewhere as Freelancer's original code doesn't do this.
|
||||
void InitPrintRepRequirements()
|
||||
{
|
||||
#define REP_REQUIREMENTS_NOT_MET_ADDR 0x480739
|
||||
#define GET_SHIP_REQUIREMENT_ADDR 0x4B9462
|
||||
#define PRINT_SHIP_REQUIREMENT_ADDR 0x4B9010
|
||||
#define SWAP_SHIPS_ADDR 0x4B9545
|
||||
|
||||
insufficientRepIds = GetValue<UINT>(0x4B9011); // 1564 by default
|
||||
|
||||
Hook(REP_REQUIREMENTS_NOT_MET_ADDR + 0x9, &NN_Dealer::PrintFmtStrPurchaseInfo_Hook, 5);
|
||||
Patch<WORD>(REP_REQUIREMENTS_NOT_MET_ADDR, 0x9054); // push esp followed by nop (replaces param 0 with a stack pointer)
|
||||
|
||||
ExpandNNShipTraderObjMemory();
|
||||
|
||||
Hook(GET_SHIP_REQUIREMENT_ADDR, GetShipRepRequirement_Hook, 7, true);
|
||||
Hook(PRINT_SHIP_REQUIREMENT_ADDR, PrintShipRepRequirement_Hook, 7, true);
|
||||
|
||||
Hook(SWAP_SHIPS_ADDR, SwapShips_Hook, 5);
|
||||
}
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
#include "resolutions.h"
|
||||
#include "resolutions_asm.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
#include <set>
|
||||
|
||||
#define DEFAULT_RES_WIDTH_PTR_1 0x56223F
|
||||
#define DEFAULT_RES_HEIGHT_PTR_1 (DEFAULT_RES_WIDTH_PTR_1 + 0x7)
|
||||
|
||||
#define DEFAULT_RES_WIDTH_PTR_2 0x424E9D
|
||||
#define DEFAULT_RES_HEIGHT_PTR_2 (DEFAULT_RES_WIDTH_PTR_2 + 0x5)
|
||||
|
||||
// sizeof(int) + sizeof(BYTE) = for the indices in menu and supported array entry
|
||||
#define INDEX_RES_AND_SUP_ARR_ENTRY_SIZE (sizeof(int) + sizeof(BYTE))
|
||||
|
||||
std::set<ResolutionInfo> resolutions;
|
||||
UINT lastSupportedResAmount = 0;
|
||||
bool lastUnk_x97C = true;
|
||||
BYTE* lastResSupportedArr = nullptr;
|
||||
|
||||
WidthHeight mainMonitorRes;
|
||||
|
||||
WidthHeight GetMainMonitorResolution()
|
||||
{
|
||||
WidthHeight result;
|
||||
|
||||
HDC hdc = GetDC(nullptr);
|
||||
|
||||
if (hdc)
|
||||
{
|
||||
result.width = GetDeviceCaps(hdc, HORZRES);
|
||||
result.height = GetDeviceCaps(hdc, VERTRES);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.width = 1024;
|
||||
result.height = 768;
|
||||
}
|
||||
|
||||
ReleaseDC(nullptr, hdc);
|
||||
return result;
|
||||
}
|
||||
|
||||
void AddFlResolutions()
|
||||
{
|
||||
const WidthHeight defaultResolutions[] =
|
||||
{ { 800, 600 }, { 1024, 768 }, { 1152, 864 }, { 1280, 960 }, { 1600, 1200 } };
|
||||
|
||||
for (const auto& defaultRes : defaultResolutions)
|
||||
{
|
||||
resolutions.emplace(defaultRes.width, defaultRes.height, 16);
|
||||
resolutions.emplace(defaultRes.width, defaultRes.height, 32);
|
||||
}
|
||||
}
|
||||
|
||||
void AddWindowRectResolutions()
|
||||
{
|
||||
RECT desktop;
|
||||
|
||||
if (GetWindowRect(GetDesktopWindow(), &desktop))
|
||||
{
|
||||
resolutions.emplace(desktop.right, desktop.bottom, 16);
|
||||
resolutions.emplace(desktop.right, desktop.bottom, 32);
|
||||
}
|
||||
}
|
||||
|
||||
void AddMainMonitorResolutions()
|
||||
{
|
||||
SetMainResWidth(mainMonitorRes.width);
|
||||
SetMainResHeight(mainMonitorRes.height);
|
||||
|
||||
Patch<int>(DEFAULT_RES_WIDTH_PTR_1, mainMonitorRes.width);
|
||||
Patch<int>(DEFAULT_RES_WIDTH_PTR_2, mainMonitorRes.width);
|
||||
Patch<int>(DEFAULT_RES_HEIGHT_PTR_1, mainMonitorRes.height);
|
||||
Patch<int>(DEFAULT_RES_HEIGHT_PTR_2, mainMonitorRes.height);
|
||||
|
||||
resolutions.emplace(mainMonitorRes.width, mainMonitorRes.height, 16);
|
||||
resolutions.emplace(mainMonitorRes.width, mainMonitorRes.height, 32);
|
||||
}
|
||||
|
||||
void AddDisplaySettingsResolutions()
|
||||
{
|
||||
bool isMainResNarrow = IsResolutionNarrow(mainMonitorRes.width, mainMonitorRes.height);
|
||||
DEVMODE dm = { 0 };
|
||||
dm.dmSize = sizeof(dm);
|
||||
|
||||
for (DWORD iModeNum = 0; EnumDisplaySettings(nullptr, iModeNum, &dm) != FALSE; ++iModeNum)
|
||||
{
|
||||
// Discard resolutions that are not allowed.
|
||||
// Moreover, discard resolutions that are too narrow (e.g. 5:4) since FL doesn't run well with those.
|
||||
// Though if the user's monitor resolution is narrow as well, do allow narrow resolutions because otherwise there won't be much left to choose from.
|
||||
if (!IsResolutionAllowed(dm) || (!isMainResNarrow && IsResolutionNarrow(dm.dmPelsWidth, dm.dmPelsHeight)))
|
||||
continue;
|
||||
|
||||
resolutions.emplace(dm.dmPelsWidth, dm.dmPelsHeight, dm.dmBitsPerPel);
|
||||
}
|
||||
}
|
||||
|
||||
bool (NN_Preferences::*InitElements_Original)(DWORD unk1, DWORD unk2);
|
||||
|
||||
bool NN_Preferences::InitElements_Hook(DWORD unk1, DWORD unk2)
|
||||
{
|
||||
ResolutionInfo* nextInfo;
|
||||
auto it = resolutions.begin();
|
||||
|
||||
// Fill Resolution info
|
||||
for (int i = 0; it != resolutions.end(); ++it)
|
||||
{
|
||||
nextInfo = ((ResolutionInfo*) &this->newData) + (i++);
|
||||
*nextInfo = *it;
|
||||
}
|
||||
|
||||
memset((PBYTE) ++nextInfo, 0x00, resolutions.size());
|
||||
|
||||
PBYTE resIndicesVOffset = (PBYTE) nextInfo + resolutions.size();
|
||||
memset(resIndicesVOffset, 0xFF, resolutions.size() * sizeof(int));
|
||||
|
||||
this->resSupportedArr = (bool*) nextInfo;
|
||||
int resSupportedInfoOffset = ((PBYTE) nextInfo) - ((PBYTE) this);
|
||||
int resIndicesOffset = resIndicesVOffset - ((PBYTE) this);
|
||||
|
||||
// +0x944
|
||||
const DWORD supportedInfoRefs[] = { 0x4B1005, 0x4B24B3, 0x4B1C73, 0x4B0773, 0x4ACEDA };
|
||||
for (const auto& ref : supportedInfoRefs)
|
||||
Patch<int>(ref, resSupportedInfoOffset);
|
||||
|
||||
// weird negated value (note the minus sign)
|
||||
Patch<int>(0x4B24A5, -resSupportedInfoOffset);
|
||||
|
||||
// +0x954
|
||||
const DWORD resIndicesRefs[] = { 0x4B249C, 0x4B17E0, 0x4B0FFA, 0x4ACEF9, 0x4B0764 };
|
||||
for (const auto& ref : resIndicesRefs)
|
||||
Patch<int>(ref, resIndicesOffset);
|
||||
|
||||
// Call original function
|
||||
return (this->*InitElements_Original)(unk1, unk2);
|
||||
}
|
||||
|
||||
// Dirty hack which adds an additional parameter to the game's internal SetResolution function
|
||||
// The purpose of putting the new parameter (height) last is so that it doesn't change the offsets of the other two parameters
|
||||
// There are two variations of this hook, one sets the active height as the height parameter, the other one sets the selected height
|
||||
|
||||
bool NN_Preferences::SetResolution_Active_Hook(UINT width, DWORD unk)
|
||||
{
|
||||
return SetResolution(width, unk, this->activeHeight);
|
||||
}
|
||||
|
||||
bool NN_Preferences::SetResolution_Selected_Hook(UINT width, DWORD unk)
|
||||
{
|
||||
return SetResolution(width, unk, this->selectedHeight);
|
||||
}
|
||||
|
||||
void (NN_Preferences::*TestResolutions_Original)(DWORD unk);
|
||||
|
||||
// Hook that ensures the resolutions are tested only when necessary (optimization)
|
||||
void NN_Preferences::TestResolutions_Hook(DWORD unk)
|
||||
{
|
||||
WidthHeight currentMainRes = GetMainMonitorResolution();
|
||||
|
||||
if (lastSupportedResAmount && currentMainRes.Equals(mainMonitorRes))
|
||||
{
|
||||
// If the monitor settings haven't changed and we know the supported resolution info,
|
||||
// set the info without testing the resolutions
|
||||
memcpy(this->resSupportedArr, lastResSupportedArr, resolutions.size() * INDEX_RES_AND_SUP_ARR_ENTRY_SIZE);
|
||||
this->supportedResAmount = lastSupportedResAmount;
|
||||
this->unk_x97C = lastUnk_x97C;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the monitor settings have changed or the resolutions haven't been tested yet,
|
||||
// test the resolutions
|
||||
(this->*TestResolutions_Original)(unk);
|
||||
|
||||
// Save the supported resolution info for later use
|
||||
memcpy(lastResSupportedArr, this->resSupportedArr, resolutions.size() * INDEX_RES_AND_SUP_ARR_ENTRY_SIZE);
|
||||
lastSupportedResAmount = this->supportedResAmount;
|
||||
lastUnk_x97C = this->unk_x97C;
|
||||
}
|
||||
|
||||
mainMonitorRes = currentMainRes;
|
||||
}
|
||||
|
||||
void DiscardLowestResolutions(size_t newSize)
|
||||
{
|
||||
auto it = resolutions.begin();
|
||||
|
||||
while (resolutions.size() > newSize)
|
||||
{
|
||||
resolutions.erase(it++);
|
||||
}
|
||||
}
|
||||
|
||||
FL_FUNC(bool ResolutionInit(HWND windowHandle, ResolutionInitInfo& info, DWORD windowFlags), 0x424DD0)
|
||||
|
||||
bool ResolutionInit_Hook(HWND windowHandle, ResolutionInitInfo& info, DWORD windowFlags)
|
||||
{
|
||||
// If a resolution has been set in the ini file which is beyond the display's capabilities, the game may still run with it, but it'll make everything look strange.
|
||||
if (info.resolutionInfo.height > mainMonitorRes.height || info.resolutionInfo.width > mainMonitorRes.width)
|
||||
{
|
||||
// Zero the resolution's width, causing FL to use a default resolution.
|
||||
info.resolutionInfo.width = 0;
|
||||
}
|
||||
|
||||
return ResolutionInit(windowHandle, info, windowFlags);
|
||||
}
|
||||
|
||||
// Expands the hard-coded resolutions array of size 10 used in the options menu to allow for up to 127 resolutions instead.
|
||||
// The new resolutions are determined dynamically based on the current user's main monitor resolution.
|
||||
// Also adds an optimization to make the game only verify the resolutions when necessary.
|
||||
// Moreover, FL can now distinguish resolutions that have the same width but a different height.
|
||||
void InitBetterResolutions()
|
||||
{
|
||||
mainMonitorRes = GetMainMonitorResolution();
|
||||
AddDisplaySettingsResolutions();
|
||||
|
||||
// Make sure there can only be 127 resolutions at most after the resolutions below have been added too
|
||||
DiscardLowestResolutions(127 - 14);
|
||||
|
||||
AddFlResolutions();
|
||||
AddWindowRectResolutions();
|
||||
AddMainMonitorResolutions();
|
||||
// Hook the resolution call address to allow for an additional resolution check.
|
||||
Hook(0x5B17AE, ResolutionInit_Hook, 5);
|
||||
|
||||
size_t resolutionAmount = resolutions.size();
|
||||
lastResSupportedArr = new BYTE[resolutionAmount * INDEX_RES_AND_SUP_ARR_ENTRY_SIZE];
|
||||
|
||||
UINT32& nnPreferencesAllocSize = GetValue<UINT32>(NN_PREFERENCES_ALLOC_SIZE_PTR);
|
||||
size_t additionalSize =
|
||||
resolutionAmount * sizeof(ResolutionInfo) // resolution info
|
||||
+ resolutionAmount // supported array
|
||||
+ resolutionAmount * sizeof(int) // indices in menu
|
||||
+ sizeof(UINT32) * 3; // active and selected height + pointer to supported array
|
||||
|
||||
// Expand the allocated heap memory of the NN_Preferences object so that we can store more resolutions
|
||||
nnPreferencesAllocSize += additionalSize;
|
||||
|
||||
// These offsets below are always the same so we can just set them once on startup
|
||||
|
||||
// Patch resolution amount (byte, 0xA)
|
||||
const DWORD resAmountRefs[] = { 0x4B2521, 0x4B1086, 0x4B1CC1, 0x4B17F0, 0x4B07DA, 0x4ACEF1 };
|
||||
// We know resolutions.size() <= 127, so casting it directly to a byte is fine
|
||||
for (const auto& ref : resAmountRefs)
|
||||
Patch<char>(ref, (BYTE) resolutions.size());
|
||||
|
||||
// Patch references to the start of the resolution array such that it points to the new one (0x8CC)
|
||||
const DWORD resStartRefs[] = { 0x4B0FEB, 0x4B17FF, 0x4B1C5C };
|
||||
for (const auto& ref : resStartRefs)
|
||||
Patch<int>(ref, NN_PREFERENCES_NEW_DATA);
|
||||
|
||||
// Patch references to the first bpp in the resolution array (0x8D4)
|
||||
const DWORD firstBppRefs[] = { 0x4B24B9, 0x4ACED3, 0x4B076A };
|
||||
for (const auto& ref : firstBppRefs)
|
||||
Patch<int>(ref, NN_PREFERENCES_NEW_DATA + 0x8);
|
||||
|
||||
// Set hook that copies the resolutions into the right location when called
|
||||
InitElements_Original = SetPointer(INIT_NN_ELEMENTS_CALL_ADDR, &NN_Preferences::InitElements_Hook);
|
||||
|
||||
// Places where the current resolution info is written to (selected and/or active width)
|
||||
Hook(0x4A9AAB, CurrentResInfoWrite1, 6);
|
||||
Hook(0x4B1046, CurrentResInfoWrite2, 6);
|
||||
Hook(0x4B180F, CurrentResInfoWrite3, 6);
|
||||
Hook(0x4B1C20, CurrentResInfoWrite4, 6);
|
||||
Hook(0x4AC264, CurrentResInfoWrite5, 6);
|
||||
Hook(0x4B27A6, CurrentResInfoWrite6, 6);
|
||||
Hook(0x4B10C3, CurrentResInfoWrite7, 6);
|
||||
|
||||
// Places where the current resolution info is checked or compared (selected and/or active width)
|
||||
Hook(0x4B1F67, CurrentResInfoCheck1, 6, true);
|
||||
Hook(0x4B102B, CurrentResInfoCheck2, 5, true);
|
||||
Hook(0x4B257A, CurrentResInfoCheck3, 6, true);
|
||||
Hook(0x4B1C93, CurrentResInfoCheck4, 8, true);
|
||||
Hook(0x4B074E, CurrentResInfoCheck5, 6, true);
|
||||
Hook(0x4B0786, CurrentResInfoCheck6, 7, true);
|
||||
Hook(0x4ACEE2, CurrentResInfoCheck7, 5, true);
|
||||
|
||||
// Places a hook where a function is called which sets the new resolution
|
||||
// This is hooked because we need this function to take an additional parameter (the height)
|
||||
Hook(0x4AC4B0, &NN_Preferences::SetResolution_Active_Hook, 5);
|
||||
Hook(0x4B1E65, &NN_Preferences::SetResolution_Selected_Hook, 5);
|
||||
Hook(0x4B2594, &NN_Preferences::SetResolution_Selected_Hook, 5);
|
||||
Hook(0x4B2781, &NN_Preferences::SetResolution_Active_Hook, 5);
|
||||
|
||||
// Hook test resolutions functions so that we only test the resolutions when it's actually necessary (optimization)
|
||||
TestResolutions_Original = Trampoline(TEST_RESOLUTIONS_ADDR, &NN_Preferences::TestResolutions_Hook, 8);
|
||||
|
||||
// Places that determine the width of the "default" resolution
|
||||
Hook(0x4ACEAB, DefaultResSet1, 5, true);
|
||||
Hook(0x4ACEBB, DefaultResSet2, 7, true);
|
||||
|
||||
// Increase the amount of bytes that are cleaned from the stack when the "set resolution function" returns because an additional parameter has been added
|
||||
GetValue<WORD>(0x4B1D09) += sizeof(DWORD);
|
||||
GetValue<WORD>(0x4B1D14) += sizeof(DWORD);
|
||||
}
|
||||
|
||||
void CleanupBetterResolutions()
|
||||
{
|
||||
delete[] lastResSupportedArr;
|
||||
CleanupTrampoline(TestResolutions_Original);
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
#include "resolutions_asm.h"
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
#define SELECTED_HEIGHT_OF 0x980
|
||||
#define ACTIVE_HEIGHT_OF 0x984
|
||||
|
||||
int mainResWidth, mainResHeight, tempHeight;
|
||||
|
||||
// Inline assembly functions that are used to add additional instructions to the existing game's code
|
||||
|
||||
NAKED void CurrentResInfoWrite1()
|
||||
{
|
||||
__asm {
|
||||
mov [ebp+0x8B8], ebx
|
||||
mov [ebp+SELECTED_HEIGHT_OF], ebx
|
||||
mov [ebp+ACTIVE_HEIGHT_OF], ebx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoWrite2()
|
||||
{
|
||||
__asm {
|
||||
mov [ebx+0x330], eax
|
||||
mov [ebx+SELECTED_HEIGHT_OF], edi
|
||||
mov [ebx+ACTIVE_HEIGHT_OF], edi
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoWrite3()
|
||||
{
|
||||
__asm {
|
||||
mov [edi+0x330], eax
|
||||
mov [edi+SELECTED_HEIGHT_OF], ecx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoWrite4()
|
||||
{
|
||||
__asm {
|
||||
mov [ebp+0x8B8], eax
|
||||
mov eax, [esp+0x20]
|
||||
mov [ebp+ACTIVE_HEIGHT_OF], eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Selected to active
|
||||
NAKED void CurrentResInfoWrite5()
|
||||
{
|
||||
__asm {
|
||||
mov [ebp+0x8B8], eax
|
||||
mov eax, [ebp+SELECTED_HEIGHT_OF]
|
||||
mov [ebp+ACTIVE_HEIGHT_OF], eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Active to selected
|
||||
NAKED void CurrentResInfoWrite6()
|
||||
{
|
||||
__asm {
|
||||
mov [esi+0x330], eax
|
||||
mov eax, [esi+ACTIVE_HEIGHT_OF]
|
||||
mov [esi+ACTIVE_HEIGHT_OF], eax
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoWrite7()
|
||||
{
|
||||
__asm {
|
||||
mov [ebx+0x330], eax
|
||||
mov [ebx+SELECTED_HEIGHT_OF], edx
|
||||
mov [ebx+ACTIVE_HEIGHT_OF], edx
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck1()
|
||||
{
|
||||
__asm {
|
||||
mov eax, [ebp+ACTIVE_HEIGHT_OF]
|
||||
cmp eax, [ebp+SELECTED_HEIGHT_OF]
|
||||
jne notequal
|
||||
mov cl, byte ptr ss:[ebp+0x8BC]
|
||||
push 0x04B1F6D
|
||||
ret
|
||||
notequal:
|
||||
push 0x04B1F75
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck2()
|
||||
{
|
||||
__asm {
|
||||
cmp [esp+0x88], edi
|
||||
jne notequal
|
||||
push edi
|
||||
push eax
|
||||
cmp ecx, 0x20
|
||||
lea edx, [esp+0xAC]
|
||||
sete cl
|
||||
push 0x4B103A
|
||||
ret
|
||||
notequal:
|
||||
push 0x4B1075
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck3()
|
||||
{
|
||||
__asm {
|
||||
mov ecx, [esi+ACTIVE_HEIGHT_OF]
|
||||
cmp ecx, [esi+SELECTED_HEIGHT_OF]
|
||||
jne notequal
|
||||
mov cl, byte ptr ss:[esi+0x8BC]
|
||||
push 0x4B2580
|
||||
ret
|
||||
notequal:
|
||||
push 0x4B2588
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck4()
|
||||
{
|
||||
__asm {
|
||||
cmp edi, [esp+0x48]
|
||||
jne notequal
|
||||
push 0xFFFFFFFF // -1
|
||||
push edi
|
||||
push esi
|
||||
lea ecx, [esp+0x20]
|
||||
push 0x4B1C9B
|
||||
ret
|
||||
notequal:
|
||||
push 0x4B1CB7
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck5()
|
||||
{
|
||||
__asm {
|
||||
mov edi, [ebp+ACTIVE_HEIGHT_OF]
|
||||
cmp edi, [ebp+SELECTED_HEIGHT_OF]
|
||||
jne notequal
|
||||
mov cl, byte ptr ss:[ebp+0x8BC]
|
||||
push 0x4B0754
|
||||
ret
|
||||
notequal:
|
||||
push 0x4B0760
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck6()
|
||||
{
|
||||
__asm {
|
||||
mov eax, [ebp+ACTIVE_HEIGHT_OF]
|
||||
cmp eax, [ebx-0x4]
|
||||
jne notequal
|
||||
mov ecx, [ebx]
|
||||
xor eax, eax
|
||||
cmp ecx, 0x20
|
||||
movzx ecx, byte ptr ss:[ebp+0x8BC]
|
||||
sete al
|
||||
push 0x4B0797
|
||||
ret
|
||||
notequal:
|
||||
push 0x4B07D1
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void CurrentResInfoCheck7()
|
||||
{
|
||||
__asm {
|
||||
cmp [ecx-8], edx
|
||||
jne notequal
|
||||
mov ebx, [ecx-4]
|
||||
cmp ebx, [tempHeight]
|
||||
jne notequal
|
||||
push 0x4ACEE7
|
||||
ret
|
||||
notequal:
|
||||
push 0x4ACEEB
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void DefaultResSet1()
|
||||
{
|
||||
__asm {
|
||||
mov edx, [mainResWidth]
|
||||
mov ebx, [mainResHeight]
|
||||
mov [tempHeight], ebx
|
||||
push 0x4ACEB0
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
NAKED void DefaultResSet2()
|
||||
{
|
||||
__asm {
|
||||
mov edx, 0x320
|
||||
mov [tempHeight], 0x258
|
||||
cmp eax, esi
|
||||
jbe conditionmet
|
||||
push 0x4ACEC4
|
||||
ret
|
||||
conditionmet:
|
||||
push 0x4ACEC9
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
void SetMainResWidth(int value)
|
||||
{
|
||||
mainResWidth = value;
|
||||
}
|
||||
|
||||
void SetMainResHeight(int value)
|
||||
{
|
||||
mainResHeight = value;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#include "save_crash.h"
|
||||
#include "utils.h"
|
||||
#include "Common.h"
|
||||
#include "logger.h"
|
||||
|
||||
#define IDS_UNKNOWN 0
|
||||
|
||||
bool Archetype::EqObj::get_undamaged_collision_group_list_Hook(std::list<CollisionGroupDesc>& colGroupList) const
|
||||
{
|
||||
// Oh hell no.
|
||||
if (this == nullptr)
|
||||
return false;
|
||||
|
||||
return this->get_undamaged_collision_group_list(colGroupList);
|
||||
}
|
||||
|
||||
UINT GetShipIdsName_Hook(UINT shipId)
|
||||
{
|
||||
Archetype::Ship* shipArch = Archetype::GetShip(shipId);
|
||||
return shipArch ? shipArch->idsName : IDS_UNKNOWN;
|
||||
}
|
||||
|
||||
// Fixes a crash that occurs when Freelancer loads all the save files on startup.
|
||||
// If one of the save files is malformed/modded, that may cause the Archetype::GetShip function to return a nullptr.
|
||||
// Freelancer doesn't check the return value, so it potentially calls a class function on a nullptr.
|
||||
// We implement the nullptr check here.
|
||||
// Additionally, there is a crash that occurs when selecting a malformed/modded save file in the F1 Load Game Menu,
|
||||
// (not the one you can access via the main menu). We fix that here too.
|
||||
void InitSaveCrashFix()
|
||||
{
|
||||
#define GET_UNDAMAGED_COL_GROUP_LIST_FILE_OFFSET_SERVER 0x6766E
|
||||
#define GET_SHIP_IDS_NAME_CALL_ADDR 0x487EBF
|
||||
|
||||
// E.g. console.dll enforces the server library to load without causing any issues, so should be fine
|
||||
DWORD serverHandle = GetUnloadedModuleHandle("server.dll");
|
||||
|
||||
if (serverHandle)
|
||||
{
|
||||
Patch<WORD>(serverHandle + GET_UNDAMAGED_COL_GROUP_LIST_FILE_OFFSET_SERVER, 0xBF90);
|
||||
SetPointer(serverHandle + GET_UNDAMAGED_COL_GROUP_LIST_FILE_OFFSET_SERVER + 0x2, &Archetype::EqObj::get_undamaged_collision_group_list_Hook);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::PrintModuleError("InitSaveCrashFix", "server.dll");
|
||||
}
|
||||
|
||||
// Client-only.
|
||||
if (!IsMPServer())
|
||||
{
|
||||
Hook(GET_SHIP_IDS_NAME_CALL_ADDR, GetShipIdsName_Hook, 9);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
|
||||
#define NAKED __declspec(naked)
|
||||
|
||||
#define DISABLE_SERVER_FILTER_HOVERING_ADDR 0x571592
|
||||
#define DISABLE_SERVER_FILTER_HOVERING_SKIP_ADDR 0x571600
|
||||
|
||||
#define SERVER_FILTER_ON_FRAME_UPDATE_VFTABLE_ADDR 0x5E2120
|
||||
|
||||
NAKED void ServerFilterClose_Hook()
|
||||
{
|
||||
__asm {
|
||||
mov ecx, [esi+0xC4] // overwritten instruction
|
||||
test ecx, ecx
|
||||
mov eax, DISABLE_SERVER_FILTER_HOVERING_ADDR + 6
|
||||
mov edx, DISABLE_SERVER_FILTER_HOVERING_SKIP_ADDR
|
||||
cmove eax, edx
|
||||
jmp eax
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes when you close the server filter dialog (MP list menu) while interacting with the GUI elements, the game crashes.
|
||||
// This happens because FL wants to disable the hovering for the GUI elements in the server filter dialog while they no longer exist.
|
||||
// The problem has been fixed by adding a simple null check.
|
||||
void InitServerFilterCrashFix()
|
||||
{
|
||||
Hook(DISABLE_SERVER_FILTER_HOVERING_ADDR, ServerFilterClose_Hook, 6, true);
|
||||
}
|
||||
|
||||
bool (ServerFilterDialog::*OnFrameUpdate_Original)();
|
||||
|
||||
bool ServerFilterDialog::OnFrameUpdate_Hook()
|
||||
{
|
||||
UpdateDeltaTimeAndUpTime();
|
||||
return (this->*OnFrameUpdate_Original)();
|
||||
}
|
||||
|
||||
// While the server filter window is opened (MP list menu), the delta time value is not updated for some reason.
|
||||
// If you open the window while the game is stuttering, the delta value remains very high until the window is closed,
|
||||
// causing the game speed to suddenly become extremely fast.
|
||||
// This bug is fixed by hooking the on-frame update function and updating the delta time manually.
|
||||
void InitServerFilterSpeedFix()
|
||||
{
|
||||
OnFrameUpdate_Original = SetPointer(SERVER_FILTER_ON_FRAME_UPDATE_VFTABLE_ADDR, &ServerFilterDialog::OnFrameUpdate_Hook);
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#include "shield_capacity.h"
|
||||
#include "Common.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define FASTCALL __fastcall
|
||||
|
||||
// Replaces the "ftol" function.
|
||||
long FASTCALL GetShieldCapacity_Hook(const Archetype::ShieldGenerator &shield, const float &maxCapacity)
|
||||
{
|
||||
// The calculation can be more efficient/concise but to avoid possible differences in rounding, I'm using the same code as FL.
|
||||
float shieldCapacity = maxCapacity - (float) ((long) (shield.offlineThreshold * maxCapacity));
|
||||
|
||||
if (shieldCapacity < 0.0f)
|
||||
shieldCapacity = 0.0f;
|
||||
|
||||
return (long) shieldCapacity;
|
||||
}
|
||||
|
||||
// In Freelancer, the actual online shield capacity is reduced by the offline_threshold value.
|
||||
// However, the shield infocards simply show the max_capacity as the shield capacity.
|
||||
// This code ensures the offline_threshold is taken into account when the value is printed.
|
||||
// It's calculated as follows: shield_capacity = max_capacity - offline_threshold * max_capacity
|
||||
void InitShieldCapacityFix()
|
||||
{
|
||||
#define GET_MAX_SHIELD_CAPACITY_ADDR 0x485055
|
||||
#define MAX_SHIELD_CAPACITY_FTOL_ADDR 0x48505B
|
||||
|
||||
Patch<WORD>(GET_MAX_SHIELD_CAPACITY_ADDR, 0x918D); // fld dword [ecx+0x94] -> lea edx, [ecx+0x94]
|
||||
Hook(MAX_SHIELD_CAPACITY_FTOL_ADDR, GetShieldCapacity_Hook, 5);
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#include "temp_fixes.h"
|
||||
#include "Freelancer.h"
|
||||
#include "Common.h"
|
||||
#include "utils.h"
|
||||
#include "logger.h"
|
||||
|
||||
// These mostly keep track of what the current value is (state).
|
||||
#define ROTATION_LOCK *((bool*) (0x678E40 + 0x44))
|
||||
#define AUTO_LEVEL *((bool*) 0x612700)
|
||||
|
||||
// These represent the actual default values of the flight behavior.
|
||||
#define DEFAULT_ROTATION_LOCK_CMN_OFFSET 0x7249A
|
||||
#define DEFAULT_AUTO_LEVEL_CMN_OFFSET 0x86542
|
||||
|
||||
bool defaultRotationLockValue = true;
|
||||
bool defaultAutoLevelValue = true;
|
||||
|
||||
namespace TempFixes
|
||||
{
|
||||
void (*PostInitDealloc_Original)(PVOID obj);
|
||||
|
||||
// Hook for dealloc function that gets called right after initializing the player's ship (undock or load game in space).
|
||||
// This is where we want to make sure rotation lock and auto level are set to their default value.
|
||||
void PostInitDealloc_Hook(PVOID obj)
|
||||
{
|
||||
// Call original function.
|
||||
PostInitDealloc_Original(obj);
|
||||
|
||||
IBehaviorManager* behaviorManager = GetBehaviorManager(GetPlayerIObjRW());
|
||||
|
||||
if (behaviorManager)
|
||||
{
|
||||
ROTATION_LOCK = behaviorManager->rotationLock;
|
||||
AUTO_LEVEL = behaviorManager->physicsInfo->autoLevel;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the behavior manager couldn't be retrieved, set rotation lock and auto level to their intended default value.
|
||||
ROTATION_LOCK = defaultRotationLockValue;
|
||||
AUTO_LEVEL = defaultAutoLevelValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// There is a bug in Freelancer where if you change the rotation lock or auto level from its default option, then load a game,
|
||||
// the in-game behavior manager gets confused about whether or not these controls are turned on (the state differs from the underlying flight behavior value).
|
||||
// To fix this, these controls must be set to their default value when the player's ship is initialized.
|
||||
void InitFlightControlsFix()
|
||||
{
|
||||
DWORD commonHandle = (DWORD) GetModuleHandle("common.dll");
|
||||
|
||||
// Save the intended default values just in case.
|
||||
if (commonHandle)
|
||||
{
|
||||
defaultRotationLockValue = GetValue<bool>(commonHandle + DEFAULT_ROTATION_LOCK_CMN_OFFSET);
|
||||
defaultAutoLevelValue = GetValue<bool>(commonHandle + DEFAULT_AUTO_LEVEL_CMN_OFFSET);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::PrintModuleError("InitFlightControlsFix", "common.dll");
|
||||
}
|
||||
|
||||
TempFixes::PostInitDealloc_Original = SetRelPointer(POST_INIT_DEALLOC_CALL_ADDR + 1, TempFixes::PostInitDealloc_Hook);
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
#include "test_sounds.h"
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
#include "fl_func.h"
|
||||
|
||||
#define INTERFACE_VOLUME_SOUND_ID 0x21
|
||||
#define AMBIENCE_VOLUME_SOUND_ID 0x22
|
||||
|
||||
bool shouldResumeBGM = false, shouldResumeBGA = false;
|
||||
|
||||
FL_FUNC(FlSound* GetSound(const ID_String& ids), 0x42AE40)
|
||||
|
||||
// Checks whether a test sound exists.
|
||||
// This is important, because if it does not exist, then the game should not attempt to play it.
|
||||
// A crash will occur if otherwise.
|
||||
bool IsTestSoundAvailable(LPCSTR nickname)
|
||||
{
|
||||
// Generates a Spew warning if the sound is not defined.
|
||||
// I think it is useful because the warning will only appear if FL wants to plays the test sound
|
||||
// while the slider is being dragged but the sound is not defined.
|
||||
// It's a hint to the modder that something is missing.
|
||||
// Moverover, this function is called at most once for each sound, so it won't spam the Spew.
|
||||
FlSound* sound = GetSound(ID_String{ CreateID(nickname) });
|
||||
return sound != nullptr;
|
||||
}
|
||||
|
||||
bool IsInterfaceTestSoundAvailable()
|
||||
{
|
||||
static bool result = IsTestSoundAvailable("ui_interface_test");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool IsAmbienceTestSoundAvailable()
|
||||
{
|
||||
static bool result = IsTestSoundAvailable("ui_ambiance_test");
|
||||
return result;
|
||||
}
|
||||
|
||||
void EnsureTestSoundsPlay()
|
||||
{
|
||||
#define INDEPENDENT_INTERFACE_VOLUME_VAL_ADDR 0x4B1503
|
||||
#define INDEPENDENT_AMBIENCE_VOLUME_VAL_ADDR 0x4B1554
|
||||
|
||||
// Test if the interface and ambience volume controls are independent from the sound effects and music, respectively.
|
||||
// If these custom edits are applied, then the respective test sounds will never play.
|
||||
// Hence patch Freelancer.exe to make the sounds actually play.
|
||||
if (GetValue<BYTE>(INDEPENDENT_INTERFACE_VOLUME_VAL_ADDR) == 0x83)
|
||||
{
|
||||
Patch<WORD>(0x4B1533, 0x00FA);
|
||||
Patch<BYTE>(0x4B154E, 0xDF);
|
||||
}
|
||||
|
||||
if (GetValue<BYTE>(INDEPENDENT_AMBIENCE_VOLUME_VAL_ADDR) == 0x84)
|
||||
{
|
||||
Patch<BYTE>(0x4B1584, 0xA9);
|
||||
Patch<BYTE>(0x4B159F, 0x8E);
|
||||
}
|
||||
}
|
||||
|
||||
FL_FUNC(bool GetBackgroundMusicHandle(SoundHandle **pHandle), 0x428BA0);
|
||||
FL_FUNC(bool GetBackgroundAmbienceHandle(SoundHandle **pHandle), 0x428BC0);
|
||||
|
||||
// There exists a bug in the game where if for example you are docked at a planet and its music has stopped playing,
|
||||
// you will not hear any test music while adjusting the music volume in the options menu.
|
||||
// FL tests if there currently exists background music, but not if it has actually ever stopped playing.
|
||||
// The hook below makes it so that it only returns the handle if the music is still playing.
|
||||
// As a result, you'll now hear the iconic Tau music when the BGM stopped playing; this way you can more easily fine tune the volume to your liking.
|
||||
bool GetBackgroundMusicHandle_Hook(SoundHandle **pBgm)
|
||||
{
|
||||
if (GetBackgroundMusicHandle(pBgm))
|
||||
{
|
||||
SoundHandle *bgm = *pBgm;
|
||||
|
||||
bool bgmPlaying = !(bgm->FinishedPlaying() || bgm->IsPaused());
|
||||
if (bgmPlaying)
|
||||
{
|
||||
// Handle is freed by the caller.
|
||||
return true;
|
||||
}
|
||||
|
||||
bgm->FreeReference();
|
||||
bgm = nullptr;
|
||||
}
|
||||
|
||||
// Pause the background ambience.
|
||||
PauseSound(shouldResumeBGA, GetBackgroundAmbienceHandle);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hook of code section that stops the test sounds when the user stops adjusting the volume sliders.
|
||||
// The point of hooking and reimplementing this section is to add stops for more test sounds besides the three that already exist.
|
||||
// If you were to force new test sounds to play, then without adding respective StopSound entries below they would play indefinitely.
|
||||
void NN_Preferences::VolumeSliderAdjustEnd_Hook(PVOID adjustedScrollElement)
|
||||
{
|
||||
// For every known test sound, store its IDS name and sound ID.
|
||||
static const TestSound testSounds[] =
|
||||
{
|
||||
{ 1409, 0x1E }, // dialogue
|
||||
{ 1336, 0x1F }, // sfx
|
||||
{ 1337, 0x20 }, // music
|
||||
{ 1411, INTERFACE_VOLUME_SOUND_ID }, // interface
|
||||
{ 1412, AMBIENCE_VOLUME_SOUND_ID } // ambience
|
||||
};
|
||||
|
||||
for (int i = 0; i < _countof(scrollElements); ++i)
|
||||
{
|
||||
if (this->scrollElements[i] != adjustedScrollElement)
|
||||
continue;
|
||||
|
||||
for (const auto& testSound : testSounds)
|
||||
{
|
||||
if (this->audioOptions[i].idsName == testSound.idsName)
|
||||
{
|
||||
StopSound(testSound.soundId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResumeSound(shouldResumeBGM, GetBackgroundMusicHandle, true);
|
||||
ResumeSound(shouldResumeBGA, GetBackgroundAmbienceHandle);
|
||||
}
|
||||
|
||||
// Make sure to stop the new test sounds too.
|
||||
void StopMusicTestSound_Hook(BYTE soundId)
|
||||
{
|
||||
StopSound(soundId); // soundId should always be 0x20 here
|
||||
StopSound(INTERFACE_VOLUME_SOUND_ID);
|
||||
StopSound(AMBIENCE_VOLUME_SOUND_ID);
|
||||
|
||||
ResumeSound(shouldResumeBGM, GetBackgroundMusicHandle, true);
|
||||
ResumeSound(shouldResumeBGA, GetBackgroundAmbienceHandle);
|
||||
}
|
||||
|
||||
// Prevent the interface test sound from starting if it isn't available (prevent crashes)
|
||||
void StartInterfaceTestSound_Hook(BYTE soundId)
|
||||
{
|
||||
if (IsInterfaceTestSoundAvailable())
|
||||
StartSound(soundId); // soundId should always be 0x21 here
|
||||
}
|
||||
|
||||
void StartAmbienceTestSound_Hook(BYTE soundId) // soundId should always be 0x22 here
|
||||
{
|
||||
SoundHandle *bga = nullptr;
|
||||
if (GetBackgroundAmbienceHandle(&bga))
|
||||
{
|
||||
bool bgaPlaying = !(bga->FinishedPlaying() || bga->IsPaused());
|
||||
|
||||
bga->FreeReference();
|
||||
|
||||
if (bgaPlaying)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsAmbienceTestSoundAvailable())
|
||||
return;
|
||||
|
||||
StartSound(soundId);
|
||||
|
||||
// Pause the background music.
|
||||
PauseSound(shouldResumeBGM, GetBackgroundMusicHandle, true);
|
||||
}
|
||||
|
||||
void PauseSound(bool &shouldResume, GetSoundHandleFunc getHandle, bool force)
|
||||
{
|
||||
SoundHandle *handle = nullptr;
|
||||
if (!getHandle(&handle))
|
||||
return;
|
||||
|
||||
if (!handle->IsPaused())
|
||||
{
|
||||
if (force)
|
||||
handle->ForcePause();
|
||||
else
|
||||
handle->Pause();
|
||||
|
||||
shouldResume = true;
|
||||
}
|
||||
|
||||
handle->FreeReference();
|
||||
}
|
||||
|
||||
void ResumeSound(bool &shouldResume, GetSoundHandleFunc getHandle, bool force)
|
||||
{
|
||||
SoundHandle *handle = nullptr;
|
||||
if (!getHandle(&handle))
|
||||
return;
|
||||
|
||||
if (shouldResume && handle->IsPaused())
|
||||
{
|
||||
if (force)
|
||||
handle->ForceResume();
|
||||
else
|
||||
handle->Resume();
|
||||
}
|
||||
|
||||
shouldResume = false;
|
||||
handle->FreeReference();
|
||||
}
|
||||
|
||||
// The Resume and Pause functions have explicit checks that prevent the BGM from being paused and resumed.
|
||||
// However, our code is special, so we are allowed to pause and resume the BGM.
|
||||
void SoundHandle::ForcePause()
|
||||
{
|
||||
static BYTE& jmpNoPauseForBgm = GetValue<BYTE>(0x42A3A7);
|
||||
BYTE jmpNoPauseForBgmOriginal = jmpNoPauseForBgm;
|
||||
jmpNoPauseForBgm = 0x00;
|
||||
Pause();
|
||||
jmpNoPauseForBgm = jmpNoPauseForBgmOriginal;
|
||||
}
|
||||
|
||||
void SoundHandle::ForceResume()
|
||||
{
|
||||
static BYTE& jmpNoResumeForBgm = GetValue<BYTE>(0x42A3EB);
|
||||
BYTE jmpNoResumeForBgmOriginal = jmpNoResumeForBgm;
|
||||
jmpNoResumeForBgm = 0x00;
|
||||
Resume();
|
||||
jmpNoResumeForBgm = jmpNoResumeForBgmOriginal;
|
||||
}
|
||||
|
||||
// Improves the way FL handles test sounds in the options menu.
|
||||
// For instance, provide better support for playing the interface and ambience test sounds.
|
||||
// Mute background music accordingly when adjusting the ambience volume.
|
||||
// Also allow the test background music to play if the current planetscape background music has stopped playing.
|
||||
void InitTestSounds()
|
||||
{
|
||||
#define GET_BGM_INSTANCE_CALL_ADDR 0x4B17A1
|
||||
#define VOLUME_SLIDER_ADJUST_END_CALL 0x4ACBAB
|
||||
#define STOP_MUSIC_TEST_SOUND_1 0x4ADD81
|
||||
#define STOP_MUSIC_TEST_SOUND_2 0x4B0689
|
||||
#define STOP_MUSIC_TEST_SOUND_3 0x4B0903
|
||||
#define START_INTERFACE_TEST_SOUND 0x4B1967
|
||||
#define START_AMBIENCE_TEST_SOUND 0x4B1949
|
||||
|
||||
EnsureTestSoundsPlay();
|
||||
|
||||
// Boilerplate code for setting the volume slider adjust end hook.
|
||||
PatchBytes(VOLUME_SLIDER_ADJUST_END_CALL - 0x70 - 0x2, { 0xEB, 0x70 }); // jmp 0x04ACBAB
|
||||
PatchBytes(VOLUME_SLIDER_ADJUST_END_CALL, { 0x51, 0x89, 0xE9 }); // push ecx + mov ecx, ebp
|
||||
|
||||
Hook(VOLUME_SLIDER_ADJUST_END_CALL + 3, &NN_Preferences::VolumeSliderAdjustEnd_Hook, 5);
|
||||
Hook(GET_BGM_INSTANCE_CALL_ADDR, GetBackgroundMusicHandle_Hook, 5);
|
||||
Hook(STOP_MUSIC_TEST_SOUND_1, StopMusicTestSound_Hook, 5);
|
||||
Hook(STOP_MUSIC_TEST_SOUND_2, StopMusicTestSound_Hook, 5);
|
||||
Hook(STOP_MUSIC_TEST_SOUND_3, StopMusicTestSound_Hook, 5);
|
||||
Hook(START_INTERFACE_TEST_SOUND, StartInterfaceTestSound_Hook, 5);
|
||||
Hook(START_AMBIENCE_TEST_SOUND, StartAmbienceTestSound_Hook, 5);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#include "trade_lane_lights.h"
|
||||
#include "utils.h"
|
||||
|
||||
// Hook that gets called each time a trade lane has been disrupted or restored.
|
||||
// When it is disrupted, we turn off the lights, and when it's restored, we turn on the lights.
|
||||
void TradeLaneEquipObj::SetLightsState_Hook()
|
||||
{
|
||||
#define CELIGHTEQUIP_CLASS_TYPE 1
|
||||
|
||||
bool activateLights = this->isDisrupted == FALSE;
|
||||
CSolar* tradeLaneSolar = this->tradeLaneEquip->solar;
|
||||
|
||||
CEquipTraverser tr = CEquipTraverser(CELIGHTEQUIP_CLASS_TYPE);
|
||||
|
||||
// Loop over all the TLR's light equip objects and turn them on/off.
|
||||
while (CEquip* equip = tradeLaneSolar->equipManager.Traverse(tr))
|
||||
{
|
||||
CELightEquip* lightEquip = CELightEquip::cast(equip);
|
||||
|
||||
if (lightEquip)
|
||||
{
|
||||
lightEquip->Activate(activateLights);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InitTradeLaneLightsFix()
|
||||
{
|
||||
#define IS_TLR_DISRUPTED_CHECK_ADDR 0x516965
|
||||
#define ENABLE_TLR_LIGHTS_CALL_ADDR 0x516978
|
||||
|
||||
Patch<BYTE>(IS_TLR_DISRUPTED_CHECK_ADDR, 0xEB); // Redirect trade lane disrupt calls to the hook below as well.
|
||||
|
||||
// FL has legacy code that deallocates all light objects when a trade lane is disrupted and when it is restored it attempts to re-create all the light objects.
|
||||
// However, the latter doesn't work in the retail version of FL, resulting in the trade lane lights remaining off permanently after one disruption.
|
||||
// The legacy code looked overly complicated for such a simple task and there seemed to be no easy way to just "fix" it.
|
||||
// Hence new code has been written that simply activates/deactivates the lights based on whether or not the trade lane is disrupted.
|
||||
Hook(ENABLE_TLR_LIGHTS_CALL_ADDR, &TradeLaneEquipObj::SetLightsState_Hook, 5);
|
||||
}
|
||||
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
#include "ui_anim.h"
|
||||
#include "utils.h"
|
||||
|
||||
// In many of the MP-related menus there is an animation for all the buttons where they slide out as you close the menu.
|
||||
// I noticed that often the button texts slide out about nine times faster than their respective button background.
|
||||
// Ideally I wanted to make it so that the slide out speeds match, but my attempts proved to be unsuccessful.
|
||||
// Turns out that the buttons that do have matching slide speeds (e.g. in the main menu), use completely different code to achieve this.
|
||||
// Now for all animations with the different slide-out speeds I just hide the text when the slide-out animation is active;
|
||||
// by default this already happens in the slide-in animation.
|
||||
// Now the animations feel a lot more seamless and smooth.
|
||||
int UITextMsgButton::UpdatePosition_Hook(BYTE unk1, const Vector* newPosOffset, BYTE unk2)
|
||||
{
|
||||
if (this->textImage)
|
||||
{
|
||||
// If the textImage is nulled prematurely, then FL will no longer destroy it when it's not needed anymore.
|
||||
// Hence it's destroyed here.
|
||||
this->textImage->Destroy();
|
||||
this->textImage = nullptr;
|
||||
}
|
||||
|
||||
this->disableHovering = true;
|
||||
|
||||
return UpdatePosition(unk1, newPosOffset, unk2);
|
||||
}
|
||||
|
||||
void InitSlideUiAnimFix()
|
||||
{
|
||||
const DWORD slideAnimationCalls[] = {
|
||||
0x56FB13, 0x56FB2A, 0x56FB41, 0x56FB58, // "FREELANCER SERVERS" menu
|
||||
0x56A86F, 0x56A885, 0x56A89B, 0x56A8B1, 0x56A8C7, // "SELECT A CHARACTER" menu
|
||||
0x561A43, 0x561A5A, // "CREATE A NEW CHARACTER" menu
|
||||
0x572F22, 0x572F39 // "Account ID" menu
|
||||
};
|
||||
|
||||
for (const auto& call : slideAnimationCalls)
|
||||
Hook(call, &UITextMsgButton::UpdatePosition_Hook, 6);
|
||||
}
|
||||
Vendored
+190
@@ -0,0 +1,190 @@
|
||||
#include "Freelancer.h"
|
||||
#include "update.h"
|
||||
#include "utils.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#define M_PIF 3.14159265358979323846f
|
||||
#define MIN_SYNC_INTERVAL_SEC (40.0f / 1000.0f)
|
||||
#define MIN_SYNC_INTERVAL_TLR_SEC (750.0f / 1000.0f)
|
||||
#define MAX_SYNC_INTERVAL_SEC (2000.0f / 1000.0f)
|
||||
#define ROTATION_CHECK_INTERVAL_SEC (250.0f / 1000.0f)
|
||||
|
||||
bool sendUpdateAsap = true;
|
||||
bool engineKillEnabledLastTime = false;
|
||||
|
||||
#define DEFAULT_SHIP_TURN_THRESHOLD 30.0f
|
||||
float shipTurnThreshold = DEFAULT_SHIP_TURN_THRESHOLD;
|
||||
|
||||
Quaternion lastOrientation;
|
||||
float secElapsedSinceLastUpdate = 0.0;
|
||||
|
||||
void ResetTimeSinceLastUpdate()
|
||||
{
|
||||
secElapsedSinceLastUpdate = 0.0;
|
||||
sendUpdateAsap = false;
|
||||
}
|
||||
|
||||
void ForceObjUpdate()
|
||||
{
|
||||
secElapsedSinceLastUpdate = MAX_SYNC_INTERVAL_SEC;
|
||||
sendUpdateAsap = true;
|
||||
}
|
||||
|
||||
bool IsEkEnabled(const CShip& ship)
|
||||
{
|
||||
// This seems to be a relatively fast operation; Freelancer calls it numerous times per frame.
|
||||
CEEngine const * engine = CEEngine::cast(ship.equipManager.FindFirst(ENGINE_TYPE));
|
||||
|
||||
if (!engine)
|
||||
return false;
|
||||
|
||||
return !engine->IsTriggered();
|
||||
}
|
||||
|
||||
// Checks if engine kill has been toggled and update the last known value.
|
||||
bool IsEkToggled(const CShip& ship)
|
||||
{
|
||||
bool engineKillEnabled = IsEkEnabled(ship);
|
||||
|
||||
bool result = engineKillEnabledLastTime != engineKillEnabled;
|
||||
engineKillEnabledLastTime = engineKillEnabled;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HasOrientationChanged(const CShip& ship, float secElapsed)
|
||||
{
|
||||
if (secElapsed < ROTATION_CHECK_INTERVAL_SEC)
|
||||
return false;
|
||||
|
||||
float rotationDelta = GetRotationDelta(lastOrientation, ship.get_orientation());
|
||||
return rotationDelta >= shipTurnThreshold;
|
||||
}
|
||||
|
||||
float GetShipTurnThreshold(const CShip& ship)
|
||||
{
|
||||
Archetype::Ship const * shipArch = ship.shiparch();
|
||||
|
||||
// TODO: The angular drag is meant to be calculated dynamically using the CShip::get_angular_drag() function.
|
||||
// However, the angular drag factor is kind of an unused feature in FL and not many mods use it.
|
||||
// Though some do for instance to increase the weight of the ship based on the amount of cargo you have.
|
||||
// This means the turn speed should be continuously recalculated instead of only once on launch.
|
||||
float avgDrag = (shipArch->angularDrag.x + shipArch->angularDrag.y) / 2.0f;
|
||||
float avgTorque = (shipArch->steeringTorque.x + shipArch->steeringTorque.y) / 2.0f;
|
||||
float maxTurnSpeed = (avgTorque / avgDrag) * (180.0f / M_PIF);
|
||||
|
||||
return std::min(DEFAULT_SHIP_TURN_THRESHOLD, 15.0f * sqrtf(maxTurnSpeed) / sqrtf(ship.get_radius()));
|
||||
}
|
||||
|
||||
namespace Update
|
||||
{
|
||||
void (*PostInitDealloc_Original)(PVOID obj);
|
||||
|
||||
// Hook for dealloc function that gets called right after initializing the player's ship (undock or load game in space)
|
||||
// This is where we want to calculate the ship's turn threshold and set some default values
|
||||
void PostInitDealloc_Hook(PVOID obj)
|
||||
{
|
||||
// Call original function
|
||||
PostInitDealloc_Original(obj);
|
||||
|
||||
if (SinglePlayer()) // No need to calculate the turn threshold in SP
|
||||
return;
|
||||
|
||||
engineKillEnabledLastTime = false;
|
||||
// TODO: Check if it is really needed to force an update initially.
|
||||
// Does FL already correctly position the ship after undocking with a default velocity?
|
||||
ForceObjUpdate();
|
||||
|
||||
if (CShip* ship = GetPlayerShip())
|
||||
shipTurnThreshold = GetShipTurnThreshold(*ship);
|
||||
else
|
||||
shipTurnThreshold = DEFAULT_SHIP_TURN_THRESHOLD;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShouldSendUpdate(const CShip& ship, float secElapsed)
|
||||
{
|
||||
// Has it been a while since the last update?
|
||||
// Has the orientation been changed to some extent?
|
||||
return (secElapsed >= MAX_SYNC_INTERVAL_SEC) || HasOrientationChanged(ship, secElapsed);
|
||||
}
|
||||
|
||||
inline float GetShipMinSyncInterval(const CShip& ship)
|
||||
{
|
||||
// Ensure updates are sent less frequently when the player ship is taking a tradelane to prevent jitter
|
||||
return ship.is_using_tradelane() ? MIN_SYNC_INTERVAL_TLR_SEC : MIN_SYNC_INTERVAL_SEC;
|
||||
}
|
||||
|
||||
void (*SendUpdatesToServer_Original)(float deltaTime);
|
||||
|
||||
// Hook that keeps track of the elapsed time.
|
||||
void SendUpdatesToServer_Hook(float deltaTime)
|
||||
{
|
||||
if (!SinglePlayer())
|
||||
{
|
||||
secElapsedSinceLastUpdate += deltaTime;
|
||||
SendUpdatesToServer_Original(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Hook for function that determines whether an update should be sent to the server
|
||||
bool CRemotePhysicsSimulation::CheckForSync_Hook(const CShip& ship, Vector const &shipPos, Quaternion const &unk)
|
||||
{
|
||||
bool isEkToggled = IsEkToggled(ship);
|
||||
bool syncResult = CheckForSync(shipPos, shipPos, unk);
|
||||
|
||||
if (secElapsedSinceLastUpdate < GetShipMinSyncInterval(ship))
|
||||
{
|
||||
// Prevent the client from sending too many updates in a short amount of time
|
||||
// This resolves the jitter issue that occurs when playing on a high framerate
|
||||
|
||||
// TODO: If EK has been toggled twice before the min sync interval has passed, then an asap update should actually not be sent because of this.
|
||||
// But then you'd also have to check if the asap update *should* be sent because CheckForSync or ShouldSendUpdate returned true. Eh, this sounds complicated.
|
||||
if (!sendUpdateAsap)
|
||||
sendUpdateAsap = syncResult || isEkToggled || ShouldSendUpdate(ship, secElapsedSinceLastUpdate);
|
||||
|
||||
return false;
|
||||
}
|
||||
else if (sendUpdateAsap)
|
||||
{
|
||||
// If an update has been missed, send an update as soon as this becomes possible, but do it only once
|
||||
return true;
|
||||
}
|
||||
|
||||
return syncResult || isEkToggled || ShouldSendUpdate(ship, secElapsedSinceLastUpdate);
|
||||
}
|
||||
|
||||
// Hook for function that sends an update to the server
|
||||
void IServerImpl::SPObjUpdate_Hook(const CShip& ship, SSPObjUpdateInfo &updateInfo, UINT client)
|
||||
{
|
||||
// Get throttle from the ship and set it in the update info if engine kill is currently disabled.
|
||||
// If it's enabled we want to set the throttle value to 0.
|
||||
updateInfo.throttle = engineKillEnabledLastTime ? 0.0f : ship.get_throttle();
|
||||
|
||||
// Send update to the server
|
||||
SPObjUpdate(updateInfo, client);
|
||||
|
||||
ResetTimeSinceLastUpdate();
|
||||
lastOrientation = MatrixToQuaternion(ship.get_orientation());
|
||||
}
|
||||
|
||||
// This allows for extra checks to prevent jitters and allow smoother updates from the client to the server.
|
||||
// Also fixes a bug where the client always sends the throttle state as 0.
|
||||
void InitBetterUpdates()
|
||||
{
|
||||
#define SEND_UPDATES_TO_SERVER_CALL_ADDR (0x54B16D)
|
||||
#define SERVER_UPDATE_SP_CHECK_ADDR (0x54158C)
|
||||
|
||||
SendUpdatesToServer_Original = SetRelPointer(SEND_UPDATES_TO_SERVER_CALL_ADDR + 1, SendUpdatesToServer_Hook);
|
||||
// Wipe out the original single player check because it is already checked for in the hook.
|
||||
Nop(SERVER_UPDATE_SP_CHECK_ADDR, 14);
|
||||
|
||||
Update::PostInitDealloc_Original = SetRelPointer(POST_INIT_DEALLOC_CALL_ADDR + 1, Update::PostInitDealloc_Hook);
|
||||
|
||||
Hook(CHECK_FOR_SYNC_CALL_ADDR, &CRemotePhysicsSimulation::CheckForSync_Hook, 5);
|
||||
Patch<BYTE>(PUSH_SHIP_POS_SYNC_CHECK_ADDR, 0x57); // push eax -> push edi (provide the CShip& to the CheckForSync hook)
|
||||
|
||||
Patch<BYTE>(OBJ_UPDATE_CALL_ADDR, 0x57); // push edi (provide the CShip& to the SPObjUpdate hook)
|
||||
Hook(OBJ_UPDATE_CALL_ADDR + 1, &IServerImpl::SPObjUpdate_Hook, 5);
|
||||
}
|
||||
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
#include "utils.h"
|
||||
|
||||
void Patch(DWORD vOffset, const LPVOID mem, UINT len)
|
||||
{
|
||||
ReadWriteProtect(vOffset, len);
|
||||
memcpy((PVOID) vOffset, mem, len);
|
||||
}
|
||||
|
||||
void PatchBytes(DWORD vOffset, std::initializer_list<BYTE> bytes)
|
||||
{
|
||||
Patch(vOffset, (LPVOID) bytes.begin(), bytes.size());
|
||||
}
|
||||
|
||||
void Nop(DWORD vOffset, UINT len)
|
||||
{
|
||||
// Recommended Multi-Byte Sequence of NOP Instruction from the x86 instruction set reference.
|
||||
// Not sure whether the commented entries are safe to use on older CPUs.
|
||||
static const NopStr nopStrTable[] =
|
||||
{
|
||||
//{ 15, "\x66\x66\x66\x66\x66\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
//{ 14, "\x66\x66\x66\x66\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
//{ 13, "\x66\x66\x66\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
//{ 12, "\x66\x66\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
//{ 11, "\x66\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
//{ 10, "\x66\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
{ 9, "\x66\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
{ 8, "\x0F\x1F\x84\x00\x00\x00\x00\x00" },
|
||||
{ 7, "\x0F\x1F\x80\x00\x00\x00\x00" },
|
||||
{ 6, "\x66\x0F\x1F\x44\x00\x00" },
|
||||
{ 5, "\x0F\x1F\x44\x00\x00" },
|
||||
{ 4, "\x0F\x1F\x40\x00" },
|
||||
{ 3, "\x0F\x1F\x00" },
|
||||
{ 2, "\x66\x90" },
|
||||
{ 1, "\x90" },
|
||||
};
|
||||
|
||||
for (const auto &nopStr : nopStrTable)
|
||||
{
|
||||
while (len >= nopStr.len)
|
||||
{
|
||||
Patch(vOffset, (PBYTE) nopStr.nopSequence, nopStr.len);
|
||||
len -= nopStr.len;
|
||||
vOffset += nopStr.len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DWORD GetUnloadedModuleHandle(LPCTSTR moduleName)
|
||||
{
|
||||
DWORD handle = (DWORD) GetModuleHandle(moduleName);
|
||||
|
||||
if (!handle)
|
||||
handle = (DWORD) LoadLibrary(moduleName);
|
||||
|
||||
return handle;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#include "version_check.h"
|
||||
#include "Dacom.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define DACOM_VERSION_MS_FUNC_OFFSET (0x281D - 0x2720)
|
||||
|
||||
// Function that returns the third value of the FILEVERSION/PRODUCTVERSION in a DLL's Version Info.
|
||||
UINT32 GetDllProductBuildVersion(LPCSTR dllName)
|
||||
{
|
||||
if (!GetUnloadedModuleHandle(dllName))
|
||||
return 0;
|
||||
|
||||
// Hack the DACOM_GetDllVersion function such that it returns the value we're after as the "major".
|
||||
// Basically instead of returning the high word of dwProductVersionMS, return the high word of dwProductVersionLS.
|
||||
// This is the only value that can be used to distinguish 1.0 DLLs from 1.1 DLLs.
|
||||
DWORD dacomVersionMsAddr = ((DWORD) DACOM_GetDllVersion) + DACOM_VERSION_MS_FUNC_OFFSET;
|
||||
BYTE& dacomVersionMs = GetValue<BYTE>(dacomVersionMsAddr);
|
||||
dacomVersionMs += sizeof(UINT32);
|
||||
|
||||
UINT32 productBuild = 0, minor, build;
|
||||
if (DACOM_GetDllVersion(dllName, productBuild, minor, build) != S_OK)
|
||||
{
|
||||
productBuild = 0;
|
||||
}
|
||||
|
||||
// Restore the patch.
|
||||
dacomVersionMs -= sizeof(UINT32);
|
||||
|
||||
return productBuild;
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
#include "waypoint.h"
|
||||
#include "utils.h"
|
||||
#include "Freelancer.h"
|
||||
|
||||
#define PLAYERSHIP_NAVMAP_OBJ_TYPE 2
|
||||
#define NAV_MAP_GET_HIGHLIGHTED_OBJ_WAYPOINT_CALL_ADDR 0x493A00
|
||||
#define NAV_MAP_GET_HIGHLIGHTED_OBJ_BESTPATH_CALL_ADDR 0x493B21
|
||||
|
||||
// Hook that prevents waypoints from being cleared when the player is in a different system
|
||||
Waypoint* GetWaypoint_Hook(int index)
|
||||
{
|
||||
Waypoint* waypoint = GetWaypoint(index);
|
||||
|
||||
if (!waypoint)
|
||||
return nullptr;
|
||||
|
||||
// Only return the waypoint if the player is in the same system as the waypoint
|
||||
return PLAYER_SYSTEM == waypoint->system ? waypoint : nullptr;
|
||||
}
|
||||
|
||||
// Hook that prevents waypoints from being set at the player ship's location
|
||||
NavMapObj* NeuroNetNavMap::GetHighlightedObject_Hook(DWORD unk1, DWORD unk2)
|
||||
{
|
||||
NavMapObj* result = GetHighlightedObject(unk1, unk2);
|
||||
|
||||
if (!result)
|
||||
return nullptr;
|
||||
|
||||
// Only return the nav map obj if it isn't the player ship
|
||||
return result->type == PLAYERSHIP_NAVMAP_OBJ_TYPE ? nullptr : result;
|
||||
}
|
||||
|
||||
// Init some waypoint-related fixes.
|
||||
void InitWaypointFixes()
|
||||
{
|
||||
// Prevent waypoints from being cleared when the player is in a different system
|
||||
Hook(WAYPOINT_CHECK_CALL_ADDR, GetWaypoint_Hook, 5);
|
||||
|
||||
// Prevent waypoints from being set at the player ship's location
|
||||
Hook(NAV_MAP_GET_HIGHLIGHTED_OBJ_WAYPOINT_CALL_ADDR, &NeuroNetNavMap::GetHighlightedObject_Hook, 5);
|
||||
Hook(NAV_MAP_GET_HIGHLIGHTED_OBJ_BESTPATH_CALL_ADDR, &NeuroNetNavMap::GetHighlightedObject_Hook, 5);
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#include "waypoint_names.h"
|
||||
#include "Freelancer.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define GET_UNKNOWN_SIMPLE_IDS_FOR_TARGET_LIST_CALL_ADDR 0x4E40AF
|
||||
#define SIMPLE_UNVISITED_CHECK_FOR_TARGET_LIST_CALL_ADDR 0x4E4094
|
||||
#define SIMPLE_VISITED_CHECK_FOR_CURRENT_INFO_LIST_CALL_ADDR 0x4755B8
|
||||
|
||||
// Fixes waypoints being called "Unknown Object" in the target view and Current Information window.
|
||||
// The two hooks below ensure that waypoints aren't treated as "unvisited".
|
||||
bool IsSimpleUnvisited_Hook(const CSimple& simple)
|
||||
{
|
||||
// If the simple is visited, follow the normal routine.
|
||||
if (!IsSimpleUnvisited(simple))
|
||||
return false;
|
||||
|
||||
// Treat waypoints as "visited".
|
||||
return !IsObjectAWaypoint(simple);
|
||||
}
|
||||
|
||||
BYTE GetSimpleVisitedValue_Hook(const CSimple& simple)
|
||||
{
|
||||
BYTE result = GetSimpleVisitedValue(simple);
|
||||
|
||||
// If the simple is unknown and it's a waypoint, set the know visit flag.
|
||||
if ((result & KNOW_VISIT_FLAG) == 0 && IsObjectAWaypoint(simple))
|
||||
result |= KNOW_VISIT_FLAG;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// When you open the Current Information window while selecting a player waypoint,
|
||||
// it always shows "PLAYER WAYPOINT1".
|
||||
// This hook adds a space and ensures the correct number is printed.
|
||||
int swprintf_Hook(int waypointIndex)
|
||||
{
|
||||
// FL's original code for getting the waypoint number (incorrect).
|
||||
int waypointNumber = waypointIndex + 1;
|
||||
|
||||
// If we can obtain the waypoint, use its waypoint number.
|
||||
// If this approach fails, just use the value that FL would originally use.
|
||||
if (Waypoint* waypoint = GetWaypoint(waypointIndex))
|
||||
{
|
||||
waypointNumber = waypoint->waypointNumber;
|
||||
}
|
||||
|
||||
// Printf the waypoint number with an added space.
|
||||
// This gives "PLAYER WAYPOINT n" instead of "PLAYER WAYPOINTn".
|
||||
return swprintf_s(FL_BUFFER_1, FL_BUFFER_LEN, L" %d\n", waypointNumber);
|
||||
}
|
||||
|
||||
// Ensures mission waypoints are called "Mission Waypoint" instead of "Waypoint".
|
||||
UINT GetCShipOrCEqObjName_Hook(const CEqObj &eqObj)
|
||||
{
|
||||
UINT result = GetCShipOrCEqObjName(eqObj);
|
||||
|
||||
// Check to make sure that we're dealing with a waypoint here.
|
||||
if (result == WAYPOINT_IDS && IsObjectAWaypoint(eqObj))
|
||||
{
|
||||
int waypointIndex;
|
||||
bool isPlayerWaypoint;
|
||||
|
||||
// Try to check whether this is a player waypoint.
|
||||
if (WAYPOINT_WATCHER && WAYPOINT_WATCHER->GetCurrentWaypointInfo(isPlayerWaypoint, waypointIndex))
|
||||
{
|
||||
// If it's not a player waypoint, it's a mission waypoint, so return the right IDS.
|
||||
if (!isPlayerWaypoint)
|
||||
return MISSION_WAYPOINT_IDS;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MissionObjective* (*GetMissionObjective_Original)(int index);
|
||||
|
||||
// When you open the Current Information window while having a random mission waypoint selected, nothing is printed.
|
||||
// This is because FL assumes a hard-coded mission objective index of 0 which is only correct for story mission waypoints.
|
||||
// This hook attempts to find the correct index dynamically so that the objective is printed for random mission waypoints, too.
|
||||
MissionObjective* GetMissionObjective_Hook(int index)
|
||||
{
|
||||
#define SPACE_OBJECTIVE 0xA
|
||||
|
||||
for (int i = 0; MissionObjective* missionObjective = GetMissionObjective_Original(i); ++i)
|
||||
{
|
||||
// Try to find the first space objective.
|
||||
if ((missionObjective->flags & 0xF) == SPACE_OBJECTIVE)
|
||||
return missionObjective;
|
||||
}
|
||||
|
||||
// If a space objective couldn't be found, just return return the first objective which FL does by default.
|
||||
// Index should always be 0.
|
||||
return GetMissionObjective_Original(index);
|
||||
}
|
||||
|
||||
// Init some waypoint name and infocard fixes.
|
||||
void InitWaypointNameFixes()
|
||||
{
|
||||
// Fix waypoints being called "Unknown Object" in the target view.
|
||||
Hook(SIMPLE_UNVISITED_CHECK_FOR_TARGET_LIST_CALL_ADDR, IsSimpleUnvisited_Hook, 5); // Target selection
|
||||
Hook(SIMPLE_VISITED_CHECK_FOR_CURRENT_INFO_LIST_CALL_ADDR, GetSimpleVisitedValue_Hook, 5); // Current Information window
|
||||
|
||||
// Fix the player waypoint being printed incorrectly in the Current Information window.
|
||||
#define SWPRINTF_WAYPOINT_PARAMS_ADDR 0x475A6C
|
||||
#define WAYPOINT_INFO_PARAMS_CLEANED_STACK_ADDR 0x475A8C
|
||||
Patch<WORD>(SWPRINTF_WAYPOINT_PARAMS_ADDR, 0x74FF); // push waypoint number onto stack
|
||||
Hook(SWPRINTF_WAYPOINT_PARAMS_ADDR + 4, swprintf_Hook, 5);
|
||||
Nop(SWPRINTF_WAYPOINT_PARAMS_ADDR + 4 + 5, 9); // nop out unneeded param pushes
|
||||
// Decrease the cleaned stack by 8 bytes because we removed two params from our hook call.
|
||||
GetValue<BYTE>(WAYPOINT_INFO_PARAMS_CLEANED_STACK_ADDR) -= sizeof(DWORD) * 2;
|
||||
|
||||
// Ensure player waypoints are called "Waypoint" and mission waypoints "Mission Waypoint".
|
||||
#define GET_OBJ_NAME_CURRENT_INFO_CALL_ADDR 0x475676
|
||||
#define GET_OBJ_NAME_TARGET_SELECTION_CALL_ADDR 0x4E8131
|
||||
Hook(GET_OBJ_NAME_CURRENT_INFO_CALL_ADDR, GetCShipOrCEqObjName_Hook, 5); // Current Information window
|
||||
Hook(GET_OBJ_NAME_TARGET_SELECTION_CALL_ADDR, GetCShipOrCEqObjName_Hook, 5); // Target selection
|
||||
|
||||
// Fix nothing being show in the Current Information window for random mission waypoints.
|
||||
#define GET_MISSION_OBJECTIVE_INFO_WINDOW_CALL_ADDR 0x475A94
|
||||
GetMissionObjective_Original = SetRelPointer(GET_MISSION_OBJECTIVE_INFO_WINDOW_CALL_ADDR + 1, GetMissionObjective_Hook);
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#include "weapon_anim.h"
|
||||
#include "utils.h"
|
||||
#include "fl_func.h"
|
||||
#include "logger.h"
|
||||
|
||||
DWORD setModelCallAddr = 0;
|
||||
|
||||
FL_FUNC(bool EngAnimation::SetModel(PDWORD unk, const EngModel* model), setModelCallAddr);
|
||||
|
||||
// There exist many animations for the weapon models in Freelancer (e.g. barrels rotating or moving back and forth while shooting).
|
||||
// However, despite all weapon animations already being defined correctly in the ini files, there is a bug in engbase.dll that prevents these animations from playing properly.
|
||||
// In vanilla FL the internal weapon animation structs have their model set to their actual gun, but this way they don't work.
|
||||
// The fix is to set the animation structs model to its parent (the ship hull). With this the animations will play correctly.
|
||||
bool EngAnimation::SetModel_Hook(PDWORD unk, const EngModel* model)
|
||||
{
|
||||
const EngModel* currentModel = model;
|
||||
|
||||
// Find the parent (ship hull) of the weapon model.
|
||||
while (currentModel)
|
||||
{
|
||||
// If the parent has been found, ensure this is used as the model for the animation, but keep checking for greater parents.
|
||||
if (currentModel->type == EngModelType::Object)
|
||||
{
|
||||
model = currentModel;
|
||||
}
|
||||
|
||||
currentModel = currentModel->parent;
|
||||
}
|
||||
|
||||
// Call original function.
|
||||
return SetModel(unk, model);
|
||||
}
|
||||
|
||||
// This hook allows for e.g. the Cruiser forward gun animation to work without having to modify the model.
|
||||
// Normally in FL the gun .cmp files have their gun animation included.
|
||||
// However, the cruiser gun model is part of the Cruiser ship model itself, and thus the animation is also in the ship model.
|
||||
// In vanilla FL it's not possible to call ship animations when firing a gun.
|
||||
// This hook allows an animation to be played on the parent of the gun if there is a leading underscore in the animation name (_).
|
||||
int IAnimation2::Open_Hook(LPCSTR animationScript, int scriptIndex, const CAttachedEquip &equip)
|
||||
{
|
||||
// If the animation script has a leading underscore, open the animation on the parent of the equipment.
|
||||
if (animationScript && animationScript[0] == '_')
|
||||
{
|
||||
// Remove the leading underscore.
|
||||
// One could argue removing a leading underscore like this is bad practice
|
||||
// since the address is no longer DWORD-aligned.
|
||||
// However, these string addresses aren't DWORD-aligned out of the box, so it doesn't matter.
|
||||
++animationScript;
|
||||
|
||||
if (CObject* parent = equip.parent)
|
||||
{
|
||||
// Open the animation on the parent.
|
||||
return Open(parent->get_archetype()->scriptIndex, parent->engineInstance, animationScript);
|
||||
}
|
||||
}
|
||||
|
||||
// Open the animation on the attached equipment (normal routine).
|
||||
return Open(scriptIndex, equip.GetRootIndex(), animationScript);
|
||||
}
|
||||
|
||||
// Fixes the weapon animations and allows weapons to play ship animations (e.g. wings).
|
||||
void InitWeaponAnimFix()
|
||||
{
|
||||
#define SET_MODEL_FUNC_FILE_OFFSET_ENGBASE 0xADC0
|
||||
#define SET_MODEL_CALL_FILE_OFFSET_ENGBASE 0xB83F
|
||||
#define GET_ROOT_INDEX_CALL_ADDR 0x52C8AF
|
||||
#define PUSH_ZERO_ADDR 0x52C8BF
|
||||
#define ANIM_OPEN_CALL_ADDR 0x52C8C2
|
||||
|
||||
DWORD engbaseHandle = (DWORD) GetModuleHandle("engbase.dll");
|
||||
|
||||
if (engbaseHandle)
|
||||
{
|
||||
setModelCallAddr = engbaseHandle + SET_MODEL_FUNC_FILE_OFFSET_ENGBASE;
|
||||
Hook(engbaseHandle + SET_MODEL_CALL_FILE_OFFSET_ENGBASE, &EngAnimation::SetModel_Hook, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::PrintModuleError("InitWeaponAnimFix", "engbase.dll");
|
||||
}
|
||||
|
||||
// Setup for IAnimation2::Open hook
|
||||
Patch<WORD>(GET_ROOT_INDEX_CALL_ADDR, 0x5551); // Replace GetRootIndex call with push ecx + push ebx
|
||||
Nop(GET_ROOT_INDEX_CALL_ADDR + 2, 4);
|
||||
Nop(PUSH_ZERO_ADDR, 2); // Nop out two zero pushes
|
||||
Nop(ANIM_OPEN_CALL_ADDR, 1); // Nop another instruction
|
||||
|
||||
Hook(ANIM_OPEN_CALL_ADDR + 1, &IAnimation2::Open_Hook, 5);
|
||||
}
|
||||
Reference in New Issue
Block a user