Initial public release of rem-essentials

This commit is contained in:
2026-08-11 16:48:50 +02:00
commit d611924a5e
161 changed files with 9792 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
EXPORTS
??0INI_Reader@@QAE@XZ
??1INI_Reader@@QAE@XZ
?open@INI_Reader@@QAE_NPBD_N@Z
?read_header@INI_Reader@@QAE_NXZ
?is_header@INI_Reader@@QAE_NPBD@Z
?get_header_ptr@INI_Reader@@QAEPBDXZ
?read_value@INI_Reader@@QAE_NXZ
?is_value@INI_Reader@@QAE_NPBD@Z
?get_value_string@INI_Reader@@QAEPBDI@Z
?get_value_bool@INI_Reader@@QAE_NI@Z
?get_value_float@INI_Reader@@QAEMI@Z
?get_value_int@INI_Reader@@QAEHI@Z
?get_name_ptr@INI_Reader@@QAEPBDXZ
?get_file_name@INI_Reader@@QBEPBDXZ
?close@INI_Reader@@QAEXXZ
?FindFirst@CEquipManager@@QBEPBVCEquip@@I@Z
?get_throttle@CShip@@QBEMXZ
?is_using_tradelane@CShip@@QBE_NXZ
?shiparch@CShip@@QBEPBUShip@Archetype@@XZ
?get_group_name@CShip@@QBEIXZ
?is_enemy@CShip@@QAE_NPAUIObjInspect@@@Z
?get_radius@EngineObject@@QBE?BMXZ
?get_orientation@EngineObject@@QBEABVMatrix@@XZ
?IsTriggered@FuseAction@@UBE_NXZ
?cast@CEEngine@@SAPBV1@PBVCEquip@@@Z
?is_base@CEqObj@@QBE_NXZ
?is_dynamic@CSolar@@QBE_NXZ
?is_waypoint@CSolar@@QBE_NXZ
?solararch@CSolar@@QBEPBUSolar@Archetype@@XZ
?CheckForSync@CRemotePhysicsSimulation@@QAE_NABVVector@@0ABVQuaternion@@@Z
?SinglePlayer@@YA_NXZ
?GetProjectilesPerFire@CELauncher@@QBEIXZ
?get_script_index@Root@Archetype@@QBEHXZ
?get_archetype@CObject@@QBEPAURoot@Archetype@@XZ
?IsMPServer@@YA_NXZ
??0CEquipTraverser@@QAE@H@Z
?Traverse@CEquipManager@@QAEPAVCEquip@@AAVCEquipTraverser@@@Z
?cast@CELightEquip@@SAPAV1@PAVCEquip@@@Z
?GetBehaviorManager@@YAPAVIBehaviorManager@@PAUIObjRW@@@Z
?CreateID@@YAIPBD@Z
?GetInfocard@Vibe@Reputation@@YAHABHAAI@Z
?get_undamaged_collision_group_list@EqObj@Archetype@@QBE_NAAV?$list@UCollisionGroupDesc@@V?$allocator@UCollisionGroupDesc@@@std@@@std@@@Z
?GetShip@Archetype@@YAPAUShip@1@I@Z
?find_by_id@GoodList@@YAPBUGoodInfo@@I@Z
+2
View File
@@ -0,0 +1,2 @@
EXPORTS
?Shutdown@CGunWrapper@@SAXXZ
+3
View File
@@ -0,0 +1,3 @@
EXPORTS
DACOM_GetDllVersion
FDUMP
+386
View File
@@ -0,0 +1,386 @@
#pragma once
#include "fl_math.h"
#include "vftable.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <list>
#include "st6.h"
#define IMPORT __declspec(dllimport)
#define ENGINE_TYPE 0x20000
IMPORT UINT CreateID(LPCSTR str);
class INI_Reader
{
public:
IMPORT INI_Reader();
IMPORT ~INI_Reader();
IMPORT bool open(LPCSTR path, bool throwExceptionOnFail = false);
IMPORT bool read_header();
IMPORT bool is_header(LPCSTR name);
IMPORT LPCSTR get_header_ptr();
IMPORT bool read_value();
IMPORT bool is_value(LPCSTR name);
IMPORT LPCSTR get_value_string(UINT index = 0);
IMPORT bool get_value_bool(UINT index = 0);
IMPORT float get_value_float(UINT index = 0);
IMPORT int get_value_int(UINT index = 0);
IMPORT LPCSTR get_name_ptr();
IMPORT LPCSTR get_file_name() const;
IMPORT void close();
// inline UINT get_value_uint(UINT index = 0)
// {
// return static_cast<UINT>(get_value_float(index));
// }
inline UINT get_value_id(UINT index = 0)
{
return CreateID(get_value_string(index));
}
private:
BYTE data[0x1565];
};
class IMPORT CEquip
{
public:
FILL_VFTABLE(0);
FILL_VFTABLE(1);
FILL_VFTABLE(2);
virtual bool Activate(bool value);
};
class IMPORT CELightEquip : public CEquip
{
public:
static CELightEquip * cast(CEquip * equip);
};
class CELauncher
{
public:
IMPORT UINT GetProjectilesPerFire() const;
UINT GetProjectilesPerFire_Hook() const;
};
class IMPORT CEquipTraverser
{
public:
CEquipTraverser(int equipClass);
private:
BYTE data[0x10];
};
class IMPORT CEquipManager
{
public:
CEquip const * FindFirst(UINT type) const;
CEquip * Traverse(CEquipTraverser& equipTraverser);
private:
BYTE x00[0x20];
};
struct CollisionGroupDesc
{};
namespace Archetype
{
struct Root
{
BYTE data[0x44];
int scriptIndex; // 0x44
IMPORT int get_script_index() const;
};
struct EqObj
{
BYTE x00[0x14];
UINT idsName; // 0x14
UINT idsInfo; // 0x18
// st6::list
IMPORT bool get_undamaged_collision_group_list(std::list<CollisionGroupDesc>& colGroupList) const;
bool get_undamaged_collision_group_list_Hook(std::list<CollisionGroupDesc>& colGroupList) const;
};
struct Ship : public EqObj
{
BYTE x1C[0xEC];
Vector angularDrag; // 0x108
Vector steeringTorque; // 0x114
};
IMPORT Ship* GetShip(UINT shipId);
struct Solar : public EqObj
{
};
struct ShieldGenerator
{
BYTE x00[0x94];
float maxCapacity; // 0x94
BYTE x98[0x8];
float offlineThreshold; // 0xA0
};
}
class IMPORT EngineObject
{
public:
float const get_radius() const;
Matrix const & get_orientation() const;
long engineInstance; // 0x04
BYTE x08[0x44];
private:
FILL_VFTABLE(0);
FILL_VFTABLE(1);
FILL_VFTABLE(2);
FILL_VFTABLE(3);
};
struct IMPORT CObject : public EngineObject
{
Archetype::Root* get_archetype() const;
DWORD classType; // 0x4C
};
struct IMPORT CSimple : CObject
{
BYTE x50[0x60];
UINT nickname; // or simpleId, 0xB0
};
class CAttachedEquip
{
FILL_VFTABLE(0);
FILL_VFTABLE(1);
FILL_VFTABLE(2);
FILL_VFTABLE(3);
FILL_VFTABLE(4);
FILL_VFTABLE(5);
FILL_VFTABLE(6);
FILL_VFTABLE(7);
virtual void Vftable_x80();
public:
virtual long GetRootIndex() const;
CObject* parent; // x04
};
struct IMPORT CEqObj : public CSimple
{
private:
FILL_VFTABLE(4);
FILL_VFTABLE(5);
FILL_VFTABLE(6);
FILL_VFTABLE(7);
virtual void Vftable_x80();
virtual void Vftable_x84();
BYTE xB4[0x30];
public:
CEquipManager equipManager; // 0xE4
BYTE x104[0x5C];
UINT baseId; // 0x160
virtual UINT get_name() const; // 0x88
bool is_base() const;
};
struct IObjInspect;
#define CSHIP_CLASS_TYPE 0x503
struct CShip : public CEqObj
{
BYTE x164[0x50];
DWORD groupId; // 0x1B4
IMPORT float get_throttle() const;
IMPORT Archetype::Ship const * shiparch() const;
IMPORT bool is_using_tradelane() const;
IMPORT UINT get_group_name() const;
IMPORT bool is_enemy(IObjInspect *obj);
UINT get_group_name_Hook() const;
bool is_enemy_Hook(IObjInspect *obj);
};
#define CSOLAR_CLASS_TYPE 0x303
struct CSolar : public CEqObj
{
IMPORT bool is_dynamic() const;
IMPORT bool is_waypoint() const;
IMPORT Archetype::Solar const * solararch() const;
static inline const CSolar* cast(const CObject& obj)
{
if ((obj.classType & CSOLAR_CLASS_TYPE) == CSOLAR_CLASS_TYPE)
return (const CSolar*) &obj;
return nullptr;
}
};
inline bool IsObjectAWaypoint(const CObject& cobject)
{
const CSolar* solar = CSolar::cast(cobject);
if (!solar)
return false;
return solar->is_waypoint();
}
class IMPORT FuseAction
{
public:
virtual void Dealloc(bool unk);
virtual bool IsTriggered() const;
};
class IMPORT CEEngine : public FuseAction
{
public:
static CEEngine const * cast(CEquip const * equip);
};
class CRemotePhysicsSimulation
{
public:
IMPORT bool CheckForSync(Vector const &shipPos, Vector const &shipPos2, Quaternion const &unk);
bool CheckForSync_Hook(const CShip& ship, Vector const &shipPos, Quaternion const &unk);
};
struct IObjRW // : public IObjInspectImpl
{
BYTE x04[0xC];
CObject* cobject; // 0x10
BYTE x14[0x8];
int unk_x1C; // 0x1C
BYTE x20[0x16C];
DWORD flags; // 0x18C
FILL_VFTABLE(0)
FILL_VFTABLE(1)
virtual UINT get_simple_id() const; // 0x20
virtual void Vftable_x24();
virtual void Vftable_x28();
virtual void Vftable_x2C();
FILL_VFTABLE(3)
FILL_VFTABLE(4)
FILL_VFTABLE(5)
virtual void Vftable_x60();
virtual void Vftable_x64();
virtual int get_attitude_towards(float &attitude, IObjRW const *other) const; // 0x68
virtual void Vftable_x6C();
virtual void Vftable_x70();
virtual int get_target(const IObjRW *&target) const; // 0x74
virtual void Vftable_x78();
virtual void Vftable_x7C();
FILL_VFTABLE(8)
FILL_VFTABLE(9)
FILL_VFTABLE(A)
virtual void Vftable_xB0();
virtual void Vftable_xB4();
virtual void Vftable_xB8();
virtual bool is_player() const; // 0xBC
inline bool SentTradeRequest() const
{
#define TRADE_REQUEST_FLAGS 0x4
return (flags & TRADE_REQUEST_FLAGS) != 0;
}
};
struct IObjInspect : public IObjRW
{
};
struct PhysicsInfo
{
BYTE x00[0x2F];
bool autoLevel; // 0x2F
};
class IBehaviorManager
{
public:
BYTE x00[0x08];
PhysicsInfo* physicsInfo; // 0x08
BYTE x0C[0xED];
bool rotationLock; // 0xF9
};
IMPORT IBehaviorManager* GetBehaviorManager(IObjRW *iObjRw);
struct ID_String
{
UINT ids;
};
IMPORT bool SinglePlayer();
IMPORT bool IsMPServer();
namespace Reputation
{
namespace Vibe
{
IMPORT int GetInfocard(int const& id, unsigned int& idsInfo);
}
}
struct EquipDesc
{
DWORD x00;
UINT archId; // 0x4
};
class EquipDescList
{
public:
#ifdef USE_ST6
st6
#else
std
#endif
::list<EquipDesc> list; // 0x0
};
enum GoodType : DWORD
{
Commodity = 0,
Hull = 2,
Ship = 3
};
struct GoodInfo
{
BYTE x00[0x4C];
GoodType type; // 0x4C
BYTE x50[0x4];
UINT shipId; // only if type = Hull, 0x54
BYTE x58[0x38];
UINT shipHullId; // only if type = Ship, 0x90
EquipDescList equipDescLists[3]; // 0x94
};
namespace GoodList
{
IMPORT GoodInfo const * find_by_id(UINT id);
};
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#define IMPORT __declspec(dllimport)
class CGunWrapper
{
public:
IMPORT static void __cdecl Shutdown();
};
class CDPClient
{
};
#define FL_CDP_CLIENT ((CDPClient*) 0x67E7BC)
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define IMPORT __declspec(dllimport)
enum DumpSeverity : DWORD
{
SEV_ERROR = 0x100001,
SEV_WARNING = 0x100002,
SEV_NOTICE = 0x100003
};
typedef int (*FDUMP_HANDLER)(DumpSeverity severity, LPCSTR fmt, ...);
extern "C"
{
#ifndef ASM_FDUMP
IMPORT FDUMP_HANDLER FDUMP;
#endif
IMPORT int DACOM_GetDllVersion(LPCSTR dllPath, UINT32& major, UINT32& minor, UINT32& build);
}
+178
View File
@@ -0,0 +1,178 @@
#pragma once
#include "Common.h"
#define PLAYER_SYSTEM *((PUINT) 0x673354)
#define CHECK_FOR_SYNC_CALL_ADDR 0x541602
#define PUSH_SHIP_POS_SYNC_CHECK_ADDR 0x5415FF
#define POST_INIT_DEALLOC_CALL_ADDR 0x54B8B9
#define OBJ_UPDATE_CALL_ADDR 0x54167C
#define WAYPOINT_CHECK_CALL_ADDR 0x4F4141
#define INIT_NN_ELEMENTS_CALL_ADDR 0x5D4A80
#define TEST_RESOLUTIONS_ADDR 0x4B2440
// The buffer length is denoted in WORDs.
#define FL_BUFFER_1 ((LPWSTR) 0x66DC60)
#define FL_BUFFER_2 ((LPWSTR) 0x66FC60)
#define FL_BUFFER_LEN *((PUINT) 0x6119F8)
#define FL_RESOURCES_HANDLE *((PDWORD) 0x67ECA8)
#define UNKNOWN_OBJECT_IDS 1191
#define WAYPOINT_IDS 1090
#define MISSION_WAYPOINT_IDS 1091
#define KNOW_VISIT_FLAG (1)
#define LAND_VISIT_FLAG (1 << 1)
#define COMMODITY_DEALER_VISIT_FLAG (1 << 2)
#define EQUIPMENT_DEALER_VISIT_FLAG (1 << 3)
#define SHIP_DEALER_VISIT_FLAG (1 << 4)
// Time elapsed since startup in miliseconds
#define FL_TIME_ELAPSED_MS (*(double*) 0x667D38)
// System time in miliseconds
#define TIMING_DELTA_TICK_COUNT (*(PDWORD) 0x667D14)
#define TIME_GET_TIME_VAL (*(PDWORD) 0x667D20)
struct Waypoint
{
Vector pos;
UINT system;
UINT target;
int waypointNumber;
};
struct NavMapObj
{
UINT type;
};
struct NeuroNetNavMap
{
NavMapObj* GetHighlightedObject_Hook(DWORD unk1, DWORD unk2);
NavMapObj* GetHighlightedObject(DWORD unk1, DWORD unk2);
};
struct AudioOption
{
UINT idsName;
UINT idsTooltip;
UINT defaultVolume;
DWORD x0C, x10, x14;
};
#define NN_PREFERENCES_NEW_DATA 0x98C
// 0x330 = current selected width
// 0x8b8 = current active width (int)
// 0x8cc = start of resolution array (10 * 4 * 3 bytes)
// 0x8d4 = start of resolution array + 0x8 (points to the bpp of the first element)
// 0x944 = array of 10 bytes that contains flags of whether the resolution index is supported (1 = supported, 0 = unsupported)
// 0x94e = unallocated word (2 bytes)
// 0x950 = amount of supported resolutions (integer)
// 0x954 = array of 4 * 10 bytes that contains the indices of the resolutions in the selection menu (-1 is unsupported resolution)
struct NN_Preferences
{
BYTE x00[0x528];
AudioOption* audioOptions; // pointer to array of audio info from up to 14 UI scroll elements
BYTE x52C[0x128];
PVOID scrollElements[14]; // 0x654, array of pointers to 14 volume scroll elements (there's more but we only need up to 14)
BYTE x68C[0x2C4];
UINT supportedResAmount;
BYTE x954[0x28];
bool unk_x97C;
BYTE x97D[0x3];
UINT selectedHeight;
UINT activeHeight;
bool* resSupportedArr; // Points to new version of 0x944
BYTE newData;
bool InitElements_Hook(DWORD unk1, DWORD unk2);
bool SetResolution_Active_Hook(UINT width, DWORD unk);
bool SetResolution_Selected_Hook(UINT width, DWORD unk);
void TestResolutions_Hook(DWORD unk);
void VolumeSliderAdjustEnd_Hook(PVOID scrollElement);
bool SetResolution(UINT width, DWORD unk, UINT height);
};
void StopSound(BYTE soundId);
void StartSound(BYTE soundId);
Waypoint* GetWaypoint(int index);
struct WaypointWatcher
{
bool GetCurrentWaypointInfo(bool& isPlayerWaypoint, int& waypointIndex);
};
#define WAYPOINT_WATCHER (*((WaypointWatcher**) 0x674BC8))
IObjRW* GetPlayerIObjRW();
CShip* GetPlayerShip();
CShip* GetPlayerShipSafe();
bool AreIObjRWsInSameGroup(const IObjRW& o1, const IObjRW& o2);
bool AreShipsInSameGroup(const CShip* ship1, const CShip* ship2);
bool IsSimpleUnvisited(const CSimple& simple);
BYTE GetSimpleVisitedValue(const CSimple& simple);
UINT GetIdsForUnvisitedSimple(const CSimple& simple);
UINT GetCShipOrCEqObjName(const CEqObj &eqObj);
UINT GetFlStringFromResources(DWORD resourcesHandle, UINT ids, LPWSTR buffer, UINT bufferLen);
inline UINT GetFlString(UINT ids, LPWSTR buffer, UINT bufferLen)
{
return GetFlStringFromResources(FL_RESOURCES_HANDLE, ids, buffer, bufferLen);
}
class RenderDisplayList
{};
void AppendXmlWsToRdlEx(LPCWSTR ws, UINT wsLen, RenderDisplayList& rdl, DWORD flags);
inline void AppendXmlWsToRdl(LPCWSTR ws, RenderDisplayList& rdl)
{
AppendXmlWsToRdlEx(ws, wcslen(ws), rdl, 0);
}
#define SHIP_TRADER_SHIP_AMOUNT 3
#define SHIP_TRADER_PLAYER_SHIP_INDEX -2
#define SHIP_TRADER_NONE_SELECTED_INDEX -1
// 0x370 = ShipTrader3DShip*
struct NN_ShipTrader
{
BYTE x00[0x3CC];
int shipCount; // 0x3CC
int selectedShipIndex; // 0x3D0
BYTE x3D4[0x24];
float playerReputationWithBaseOwners; // 0x3F8
int shipStatuses[SHIP_TRADER_SHIP_AMOUNT]; // 0x3FC, basically enums for available, rep too low, or level too low
BYTE x408[0x40];
int shipRepPercentages[SHIP_TRADER_SHIP_AMOUNT]; // 0x448
void StoreShipRepRequirement(int shipIndex, float repRequirement);
LPWSTR PrintFmtShipRepRequirement();
PBYTE SwapShipRepPercentages(PBYTE rhsShipStatusAddr);
};
void ExpandNNShipTraderObjMemory();
struct FLCursor
{
float xPos, yPos, distFromZero;
};
struct ServerFilterDialog
{
bool OnFrameUpdate_Hook();
};
double GetDeltaTime();
void UpdateDeltaTime();
void UpdateDeltaTimeAndUpTime();
UINT GetNumOfActiveMissionObjectives();
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "vftable.h"
struct SSPObjUpdateInfo
{
BYTE x00[40];
float throttle; // 0x28
};
class IServerImpl {
public:
FILL_VFTABLE(0)
FILL_VFTABLE(1)
FILL_VFTABLE(2)
FILL_VFTABLE(3)
FILL_VFTABLE(4)
FILL_VFTABLE(5)
FILL_VFTABLE(6)
FILL_VFTABLE(7)
FILL_VFTABLE(8)
FILL_VFTABLE(9)
FILL_VFTABLE(A)
FILL_VFTABLE(B)
FILL_VFTABLE(C)
virtual void SPObjUpdate(SSPObjUpdateInfo &updateInfo, UINT client);
void SPObjUpdate_Hook(const CShip& ship, SSPObjUpdateInfo &updateInfo, UINT client);
};
+15
View File
@@ -0,0 +1,15 @@
#pragma once
struct Alchemy
{
float progress;
void* effect;
};
struct AleLoop
{
int startOffset;
unsigned char maxProgressOffset;
};
void InitAlchemyCrashFix();
+10
View File
@@ -0,0 +1,10 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
struct BaseInfoCat
{
DWORD headerStyleAddr;
DWORD headerNamePrintAddr;
};
void InitBaseInfoSpacingFix();
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitBlankFactionNameFix();
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#ifdef USE_ST6
#include "st6.h"
#else
#include <list>
namespace st6 = std;
#endif
#define FASTCALL __fastcall
struct PlayerData
{
BYTE x00[0x264];
UINT currentShipId; // 0x264
BYTE x268[0xBC];
UINT shipIdOnLand; // 0x324
};
struct BaseGood
{
BYTE x00[0x8];
UINT goodId; // 0x8
float price; // 0xC
int minQuantity; // 0x10
int maxQuantity; // 0x14
DWORD unk_x18; // 0x18
inline bool IsShipCandidate() const
{
return unk_x18 == 0 || unk_x18 == 2;
}
};
struct BaseGoodIt
{
BaseGood* good; // 0x0
void Advance();
};
struct BaseGoodCollection
{
UINT baseName; // 0x0
UINT launchpadName; // 0x4
DWORD unk_x08; // 0x8
float unk_x0C; // 0xC
st6::list<BaseGood> goods; // 0x10
bool HasShipPackageWithGood(UINT goodId);
};
struct MarketGood
{
BYTE x00[0x10];
DWORD type; // 0x10
};
struct BaseMarket
{
UINT baseName; // 0x0
BaseGoodCollection* baseGoods; // 0x4
const MarketGood* GetSoldGood(UINT goodId) const;
};
const MarketGood* FASTCALL GetGoodSoldByBaseOrPartOfShip(const BaseMarket &baseMarket, const PlayerData &playerData, UINT goodId);
void InitShipBuyKickFix();
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "feature_config.h"
void ReadConfig(LPCSTR path, FeatureManager &manager);
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "vftable.h"
#include "Common.h"
#ifdef USE_ST6
#include "st6.h"
#else
#include <vector>
namespace st6 = std;
#endif
struct InputChar
{
WCHAR c;
DWORD flags; // I don't know whether this field actually represents flags; it's just an educated guess.
DWORD unk; // Allocated but never assigned.
};
struct KeyMapInfo
{
BYTE x00[0x8];
DWORD controlCharacterFlags; // 0x8
DWORD x0C;
WCHAR enteredKey; // 0x10
inline bool IsCtrlPressed() const
{
return (controlCharacterFlags & 4) == 4;
}
};
struct InputBoxWindow
{
BYTE x04[0x498];
int pos; // 0x49C
BYTE x4A0[0x24];
st6::vector<InputChar> chars; // 0x4C4
BYTE x4D4[0x3C];
int maxCharsLength; // 0x510
BYTE x514[0x14];
WCHAR forbiddenChar; // 0x528
bool noForbiddenChar; // 0x52A
PDWORD ime; // 0x52C
FILL_VFTABLE(0)
FILL_VFTABLE(1)
FILL_VFTABLE(2)
FILL_VFTABLE(3)
FILL_VFTABLE(4)
FILL_VFTABLE(5)
FILL_VFTABLE(6)
FILL_VFTABLE(7)
FILL_VFTABLE(8)
FILL_VFTABLE(9)
FILL_VFTABLE(A)
FILL_VFTABLE(B)
virtual void Vftable_xC0();
virtual void Vftable_xC4();
virtual bool WriteTypedKey(const KeyMapInfo& kmi);
void HandleCopyPaste(const KeyMapInfo& kmi);
void CopyToClipboard();
void CopyFromClipboard();
void WriteString(LPCWSTR str);
};
void HandleDefaultInputKey_Hook();
void InitCopyPasteFeature();
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Common.h"
// Read from the [Cursor] values in DATA\mouse.ini.
// Constructor: 0x41E550
struct MouseCursor
{
UINT32 nicknameLen; // 0x0
char nickname[32]; // 0x4
PDWORD unk_0x24; // 0x24
UINT32 animNameLen; // 0x28
char animName[24]; // 0x2C
float hotspotX; // 0x44
float hotspotY; // 0x48
DWORD color; // rgba, 0x4C
BYTE x50[0x10];
int animValue1; // 0x60
int animState; // should be preserved when copying, 0x64
BYTE x68[0x28];
};
struct Targetable_Objects
{
BYTE x00[0x3F0];
const CSimple* selectedSimple; // 0x3F0
BYTE x3F4[0x534];
bool isAimLocking; // 0x928
void UpdateTargeting_Hook();
void UpdateTargeting();
};
void InitMoreCursorColors();
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define UI_ELEMENT_VISIBLE 0x3
struct ManeuverFrame
{
BYTE x00[0x6C];
BYTE flags; // 0x6C
};
struct NavBar
{
BYTE x00[0x3D8];
ManeuverFrame* maneuverFrame; // 0x3D8
BYTE x3DC[0x4];
PVOID unkUiElement; // 0x3E0
bool shipDealerMenuOpened; // 0x3E4
void SetHotspot_Hook(PVOID hotspot);
};
struct DealerOpenCamera
{
BYTE x00[0x1338];
bool animationInProgress; // 0x1338
bool StartAnimation(LPCSTR name, PVOID unk, NavBar* navBar, DWORD unk2);
bool StartAnimation_Hook(LPCSTR name, PVOID unk, NavBar* navBar, DWORD unk2);
};
void InitDealerOpenFix();
void InitDealerCrashFix();
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitMissingDllCrashFix();
+6
View File
@@ -0,0 +1,6 @@
#pragma once
void InitPostGameDeadlockFix();
void InitQuitMessageFix();
void CleanupQuitMessageFix();
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <string.h>
#include <map>
struct FlSharpFeature
{
void (*initFunc)(); // feature's init function
void (*cleanupFunc)(); // feature's cleanup function
bool (*applyPredicate)(); // function that determines whether the feature must be applied from a technical perspective
bool enabled; // value determined by the user so they can choose whether they want it to be applied
};
class FeatureManager
{
public:
void RegisterFeature(LPCSTR name, void (*initFunc)(), void (*cleanupFunc)(), bool (*applyPredicate)());
bool SetFeatureEnabled(LPCSTR name, bool enabled);
void InitFeatures();
void CleanupFeatures();
private:
std::map<UINT, FlSharpFeature> features;
};
bool ApplyAlways();
bool ApplyOnlyOnClient();
bool ApplyOnlyOnServer();
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#define NAKED __declspec(naked)
#define NOINLINE __declspec(noinline)
#define FL_FUNC(func, addr) \
NAKED NOINLINE func \
{ \
__asm mov eax, addr \
__asm jmp eax \
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
// NOTE: This only works when loaded via Freelancer.exe, not FLServer.exe.
#define FL_180_OVER_PI (*(float*) 0x5D3D38)
class Vector
{
public:
float x, y, z;
};
class Quaternion
{
public:
float w, x, y, z;
};
class Matrix
{
public:
float data[3][3];
};
float GetRotationDelta(const Quaternion& quat, const Matrix& rot);
Quaternion MatrixToQuaternion(const Matrix& m);
#ifdef _MSC_VER
#if _MSC_VER < 1700
inline float copysign(float x, float y)
{
return (x < 0 && y > 0) || (x > 0 && y < 0) ? -x : x;
}
#endif
#endif
+85
View File
@@ -0,0 +1,85 @@
#pragma once
#include "Common.h"
#include "utils.h"
void InitFlashParticlesFix();
struct EffectInstance
{
virtual void Vftable_x00();
virtual void FreeEngineEffect();
// Dealloc function which the game calls to clean up the effects when e.g. a ship or solar gets destroyed.
inline void EngineDealloc()
{
FreeEngineEffect();
FreeHeapMemory();
}
// Dealloc function which the game calls before creating a new flash effect instance for the same barrel/launcher.
inline void GeneralDealloc()
{
FreeAleEffect();
EngineDealloc();
}
// Dealloc function which the game calls right after quitting the play session.
inline void PostGameDealloc()
{
ResetBaseWatcher();
EngineDealloc();
}
inline void DoFreeHeapMemory()
{
FreeHeapMemory();
}
private:
struct WatcherInfo
{
float data[12];
};
void FreeAleEffect();
int FreeHeapMemory();
void SetBaseWatcher(int unk1, int unk2, const WatcherInfo& watcherInfo);
inline void ResetBaseWatcher()
{
WatcherInfo watcherInfo = { 0 };
watcherInfo.data[0] = watcherInfo.data[4] = watcherInfo.data[8] = 1.0f;
SetBaseWatcher(0, -1, watcherInfo);
}
};
EffectInstance** CreateFlashParticlesArray(UINT barrelAmount);
struct CliLauncher
{
DWORD vftable;
CELauncher* launcher; // 0x04
IObjRW* parent; // 0x08
BYTE x0C[0x1C];
// After playing the flash particle on a launcher, the effect instance is stored in 0x28.
// We need to keep track of more than one effect instance if the launcher has multiple barrels.
// Expanding the struct's memory is not feasible due to there existing many variations of this struct,
// which each have their own unique constructor and object size. Hence we dynamically manage this array at the same offset as currentFlashParticle.
union { // 0x28
EffectInstance* currentFlashParticle;
EffectInstance** flashParticlesArr;
};
void PlayAllFlashParticles(const ID_String& effectName);
// PlayFlashParticleForBarrel must be __cdecl because this code jumps to a vanilla FL function which does ret instead of ret n at the end.
// Therefore, the caller must clean the stack.
void __cdecl PlayFlashParticleForBarrel(const ID_String& effectName, UINT barrelIndex);
void CleanFlashParticlesPostGame_Hook();
void CleanFlashParticlesEngine_Hook();
void CleanFlashParticlesMemory_Hook();
void CleanFlashParticlesArr(void (EffectInstance::*deallocFunc)());
};
+15
View File
@@ -0,0 +1,15 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
enum AttitudeType : int
{
Hostile = -1,
Neutral = 0,
Friendly = 1
};
void InitHostileGroupFormation();
void InitHostileGroupMembersFix();
void InitGroupMemberAttitudeFix();
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <map>
struct InfocardEntry
{
std::map<UINT, UINT>& map;
LPCSTR key;
LPCSTR value;
};
void InitDynamicSolarInfocards();
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
namespace Logger
{
void PrintModuleError(LPCSTR functionName, LPCSTR moduleName);
void PrintFileOpenError(LPCSTR functionName, LPCSTR filePath);
void PrintV10Warning(LPCSTR moduleName);
void PrintInvalidFeatureWarning(LPCSTR functionName, LPCSTR featureName, LPCSTR iniPath);
void PrintInvalidHeaderWarning(LPCSTR functionName, LPCSTR headerName, LPCSTR iniPath);
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "vftable.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#define STDCALL __stdcall
void InitCursorFix();
void InitMouseWarpFix();
// Redefining this because I don't want the project to depend on the DirectX SDK...
struct IDirectInputDevice8
{
FILL_VFTABLE(0)
virtual void Vftable_x10();
virtual void Vftable_x14();
virtual void Vftable_x18();
virtual long STDCALL Acquire(); // 0x1C
virtual long STDCALL Unacquire(); // 0x20
virtual void Vftable_x24();
virtual void Vftable_x28();
virtual void Vftable_x2C();
virtual void Vftable_x30();
virtual long STDCALL SetCooperativeLevel(HWND hwnd, DWORD flags); // 0x34
};
+17
View File
@@ -0,0 +1,17 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
struct StrBuffer
{
LPWSTR str;
size_t capacity;
};
enum NameType : DWORD
{
FactionAndDesignation = 0,
PilotName = 1,
Unk = 2
};
void InitPilotNamesFix();
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Common.h"
void InitProjectilesSoundFix();
void InitProjectilesServerFix();
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
void InitPrintRepRequirements();
struct DealerStack
{
BYTE x00[0x24];
float repRequired;
};
struct NN_Dealer
{
void PrintFmtStrPurchaseInfo_Hook(UINT idsPurchaseInfo, const DealerStack& stack);
void PrintFmtStrPurchaseInfo(UINT idsPurchaseInfo, int fmtValue);
};
+70
View File
@@ -0,0 +1,70 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Freelancer.h"
#define MIN_RES_WIDTH 800
#define MIN_RES_HEIGHT 600
#define NN_PREFERENCES_ALLOC_SIZE_PTR 0x4B296A
#define NN_PREFERENCES_ALLOC_SIZE 0x980
struct WidthHeight
{
UINT width, height;
bool Equals(const WidthHeight &other)
{
return memcmp(this, &other, sizeof(other)) == 0;
}
};
struct ResolutionInfo
{
ResolutionInfo(UINT width, UINT height, UINT bpp)
: width(width), height(height), bpp(bpp)
{}
// bpp = bits per pixel. FL appears to only support 16 and 32
UINT width, height, bpp;
// First sort by bpp, then width, then height, all in ascending order
bool operator < (const ResolutionInfo& other) const
{
if (bpp != other.bpp)
return bpp < other.bpp;
else if (width != other.width)
return width < other.width;
else
return height < other.height;
}
};
struct ResolutionInitInfo
{
BYTE x00[0x8];
ResolutionInfo resolutionInfo;
};
inline bool IsResolutionAllowed(const DEVMODE &dm)
{
return dm.dmPelsWidth >= MIN_RES_WIDTH && dm.dmPelsHeight >= MIN_RES_HEIGHT && (dm.dmBitsPerPel == 16 || dm.dmBitsPerPel == 32);
}
// Returns true if the given resolution is narrower than 4:3.
inline bool IsResolutionNarrow(UINT width, UINT height)
{
#define MIN_4_BY_3_FACTOR (4.0f / 3.0f) - 0.02f
if (height == 0)
return true;
return ((float) width / (float) height) < MIN_4_BY_3_FACTOR;
}
bool ResolutionInit(HWND windowHandle, ResolutionInitInfo& info, DWORD windowFlags);
void InitBetterResolutions();
void CleanupBetterResolutions();
+23
View File
@@ -0,0 +1,23 @@
#pragma once
void CurrentResInfoWrite1();
void CurrentResInfoWrite2();
void CurrentResInfoWrite3();
void CurrentResInfoWrite4();
void CurrentResInfoWrite5();
void CurrentResInfoWrite6();
void CurrentResInfoWrite7();
void CurrentResInfoCheck1();
void CurrentResInfoCheck2();
void CurrentResInfoCheck3();
void CurrentResInfoCheck4();
void CurrentResInfoCheck5();
void CurrentResInfoCheck6();
void CurrentResInfoCheck7();
void DefaultResSet1();
void DefaultResSet2();
void SetMainResWidth(int value);
void SetMainResHeight(int value);
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitSaveCrashFix();
+4
View File
@@ -0,0 +1,4 @@
#pragma once
void InitServerFilterCrashFix();
void InitServerFilterSpeedFix();
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitShieldCapacityFix();
+190
View File
@@ -0,0 +1,190 @@
#pragma once
#include <cstddef>
#include <stdexcept>
#include <iterator>
#ifndef _POINTER_X
#define _POINTER_X(T, A) T*
#endif
#ifndef _REFERENCE_X
#define _REFERENCE_X(T, A) T&
#endif
namespace st6
{
template<class _Ty>
class allocator
{
public:
typedef size_t size_type;
typedef ptrdiff_t difference_type;
typedef _Ty* pointer;
typedef const _Ty* const_pointer;
typedef _Ty& reference;
typedef const _Ty& const_reference;
typedef _Ty value_type;
pointer address(reference _X) const { return (&_X); }
const_pointer address(const_reference _X) const { return (&_X); }
void construct(pointer _P, const _Ty& _V) { _Construct(_P, _V); }
void destroy(pointer _P) { _Destroy(_P); }
size_t max_size() const
{
size_t _N = (size_t)(-1) / sizeof(_Ty);
return (0 < _N ? _N : 1);
}
};
template<class _Ty, class _A = allocator<_Ty>>
class vector
{
public:
typedef vector<_Ty, _A> _Myt;
typedef _A allocator_type;
typedef typename _A::size_type size_type;
typedef typename _A::difference_type difference_type;
typedef typename _A::pointer _Tptr;
typedef typename _A::const_pointer _Ctptr;
typedef typename _A::reference reference;
typedef typename _A::const_reference const_reference;
typedef typename _A::value_type value_type;
typedef _Tptr iterator;
typedef _Ctptr const_iterator;
iterator begin() { return (_First); }
const_iterator begin() const { return ((const_iterator)_First); }
iterator end() { return (_Last); }
const_iterator end() const { return ((const_iterator)_Last); }
size_type size() const { return (_First == 0 ? 0 : _Last - _First); }
bool empty() const { return (size() == 0); }
const_reference operator[](size_type _P) const { return (*(begin() + _P)); }
reference operator[](size_type _P) { return (*(begin() + _P)); }
protected:
_A allocator;
iterator _First, _Last, _End;
};
template <class _Ty, class _A = allocator<_Ty>>
class list
{
protected:
struct _Node;
friend struct _Node;
typedef _POINTER_X(_Node, _A) _Nodeptr;
struct _Node
{
_Nodeptr _Next, _Prev;
_Ty _Value;
};
struct _Acc;
friend struct _Acc;
struct _Acc
{
typedef _REFERENCE_X(_Nodeptr, _A) _Nodepref;
typedef typename _A::reference _Vref;
static _Nodepref _Next(_Nodeptr _P) { return ((_Nodepref)(*_P)._Next); }
static _Nodepref _Prev(_Nodeptr _P) { return ((_Nodepref)(*_P)._Prev); }
static _Vref _Value(_Nodeptr _P) { return ((_Vref)(*_P)._Value); }
};
public:
typedef list<_Ty, _A> _Myt;
typedef _A allocator_type;
typedef typename _A::size_type size_type;
typedef typename _A::difference_type difference_type;
typedef typename _A::pointer _Tptr;
typedef typename _A::const_pointer _Ctptr;
typedef typename _A::reference reference;
typedef typename _A::const_reference const_reference;
typedef typename _A::value_type value_type;
// CLASS const_iterator
class iterator;
class const_iterator;
friend class const_iterator;
class const_iterator
{
public:
const_iterator() {}
const_iterator(_Nodeptr _P) : _Ptr(_P) {}
const_iterator(const iterator& _X) : _Ptr(_X._Ptr) {}
const_reference operator*() const { return (_Acc::_Value(_Ptr)); }
_Ctptr operator->() const { return (&**this); }
const_iterator& operator++()
{
_Ptr = _Acc::_Next(_Ptr);
return (*this);
}
const_iterator operator++(int)
{
const_iterator _Tmp = *this;
++*this;
return (_Tmp);
}
const_iterator& operator--()
{
_Ptr = _Acc::_Prev(_Ptr);
return (*this);
}
const_iterator operator--(int)
{
const_iterator _Tmp = *this;
--*this;
return (_Tmp);
}
bool operator==(const const_iterator& _X) const { return (_Ptr == _X._Ptr); }
bool operator!=(const const_iterator& _X) const { return (!(*this == _X)); }
_Nodeptr _Mynode() const { return (_Ptr); }
protected:
_Nodeptr _Ptr;
};
// CLASS iterator
friend class iterator;
class iterator : public const_iterator
{
public:
iterator() {}
iterator(_Nodeptr _P) : const_iterator(_P) {}
reference operator*() const { return (_Acc::_Value(this->_Ptr)); }
_Tptr operator->() const { return (&**this); }
iterator& operator++()
{
this->_Ptr = _Acc::_Next(this->_Ptr);
return (*this);
}
iterator operator++(int)
{
iterator _Tmp = *this;
++*this;
return (_Tmp);
}
iterator& operator--()
{
this->_Ptr = _Acc::_Prev(this->_Ptr);
return (*this);
}
iterator operator--(int)
{
iterator _Tmp = *this;
--*this;
return (_Tmp);
}
bool operator==(const iterator& _X) const { return (this->_Ptr == _X._Ptr); }
bool operator!=(const iterator& _X) const { return (!(*this == _X)); }
};
iterator begin() { return (iterator(_Acc::_Next(_Head))); }
const_iterator begin() const { return (const_iterator(_Acc::_Next(_Head))); }
iterator end() { return (iterator(_Head)); }
const_iterator end() const { return (const_iterator(_Head)); }
_A allocator;
_Nodeptr _Head;
size_type _Size;
};
}
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitFlightControlsFix();
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "vftable.h"
#include "Common.h"
struct SoundHandle
{
BYTE data_x04[0x2C];
int unkBytePtr; // I don't know anything about this value (besides it being a pointer to some byte), but it gets nulled when the music stops playing.
inline bool FinishedPlaying()
{
return unkBytePtr == NULL || unkBytePtr == -1;
}
void ForcePause();
void ForceResume();
virtual void Vftable_x00();
virtual void Vftable_x04();
virtual DWORD __stdcall FreeReference();
virtual void Vftable_x0C();
FILL_VFTABLE(1)
FILL_VFTABLE(2)
FILL_VFTABLE(3)
FILL_VFTABLE(4)
FILL_VFTABLE(5)
virtual void Vftable_x60();
virtual void Vftable_x64();
virtual void Pause();
virtual void Resume();
virtual bool IsPaused();
};
struct TestSound
{
UINT idsName;
BYTE soundId;
};
struct FlSound
{
DWORD vftable;
UINT id;
LPCSTR filePath;
int unk_x0C;
float unk_x10;
float unk_x14;
};
FlSound* GetSound(const ID_String& ids);
bool GetBackgroundMusicHandle(SoundHandle **pHandle);
bool GetBackgroundAmbienceHandle(SoundHandle **pHandle);
bool GetBackgroundMusicHandle_Hook(SoundHandle **handle);
void StopMusicTestSound_Hook(BYTE soundId);
void InitTestSounds();
typedef bool (*GetSoundHandleFunc)(SoundHandle **pHandle);
void PauseSound(bool &shouldResume, GetSoundHandleFunc getHandle, bool force = false);
void ResumeSound(bool &shouldResume, GetSoundHandleFunc getHandle, bool force = false);
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Common.h"
struct CETradeLaneEquip
{
DWORD vftable;
CSolar* solar;
};
struct TradeLaneEquipObj
{
DWORD vftable;
CETradeLaneEquip* tradeLaneEquip;
BYTE x08[0x28];
BOOL isDisrupted;
void SetLightsState_Hook();
};
void InitTradeLaneLightsFix();
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "vftable.h"
#include "Common.h"
struct BigImage
{
virtual void Vftable_x00();
virtual void Vftable_x04();
virtual DWORD __stdcall Destroy();
};
struct UITextMsgButton
{
public:
int UpdatePosition_Hook(BYTE unk1, const Vector* newPosOffset, BYTE unk2);
FILL_VFTABLE(0)
FILL_VFTABLE(1)
FILL_VFTABLE(2)
FILL_VFTABLE(3)
FILL_VFTABLE(4)
FILL_VFTABLE(5)
FILL_VFTABLE(6)
FILL_VFTABLE(7)
FILL_VFTABLE(8)
FILL_VFTABLE(9)
virtual void Vftable_xA0();
virtual void Vftable_xA4();
// UpdatePosition is actually Transform and unk1 is the transform type, with 6 = UPDATE_POS.
// Thus the function actually has more purposes than just updating the position.
virtual int UpdatePosition(BYTE unk1, const Vector* newPosOffset, BYTE unk2);
BYTE x04[0x3E8];
BigImage* textImage; // 0x3EC. textImage = nullptr will prevent the text from rendering
BYTE x3F0[0x81];
bool disableHovering; // 0x471
};
void InitSlideUiAnimFix();
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#include "Common.h"
#include "RemoteServer.h"
void ResetTimeSinceLastUpdate();
void InitBetterUpdates();
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <cassert>
#include <initializer_list>
void Patch(DWORD vOffset, const LPVOID mem, UINT len);
template <typename Type>
inline void Patch(DWORD vOffset, Type value)
{
Patch(vOffset, &value, sizeof(Type));
}
void PatchBytes(DWORD vOffset, std::initializer_list<BYTE> bytes);
void Nop(DWORD vOffset, UINT len);
inline void ReadWriteProtect(DWORD location, DWORD size)
{
DWORD _;
VirtualProtect((PVOID) location, size, PAGE_EXECUTE_READWRITE, &_);
}
template <typename Func>
Func SetRelPointer(DWORD location, Func hookFunc)
{
// Set and calculate the relative offset for the hook function
DWORD& relOriginalLocation = GetValue<DWORD>(location);
DWORD originalPointer = location + relOriginalLocation + 4;
DWORD hookFuncLocation = *((PDWORD) &hookFunc);
relOriginalLocation = hookFuncLocation - (location + 4);
return GetFuncDef<Func>(originalPointer);
}
template <typename Func>
void Hook(DWORD location, Func hookFunc, UINT instrLen, bool jmp = false)
{
assert(instrLen >= 5);
// Set the opcode for the call or jmp instruction
Patch<BYTE>(location, jmp ? 0xE9 : 0xE8); // 0xE9 = jmp, 0xE8 = call
// Set the relative address
SetRelPointer(location + 1, hookFunc);
// Nop out excess bytes
if (instrLen > 5)
Nop(location + 5, instrLen - 5);
}
template <typename Func>
Func Trampoline(DWORD location, Func hookFunc, UINT instrLen)
{
// Allocate memory for gateway function.
PBYTE gatewayFunc = (PBYTE) VirtualAlloc(nullptr, instrLen + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// Copy the instruction(s) that will be overwritten by setting the hooks to the gateway code.
ReadWriteProtect(location, instrLen);
memcpy(gatewayFunc, (PVOID) location, instrLen);
// Jmp from location to hook function.
Hook(location, hookFunc, instrLen, true);
// Jmp from gateway to original function.
Hook((DWORD) (gatewayFunc + instrLen), GetFuncDef<Func>(location + instrLen), 5, true);
// Return handle for calling the gateway function which in turn calls the original function.
return GetFuncDef<Func>((DWORD) gatewayFunc);
}
template <typename Func>
void CleanupTrampoline(Func trampolineFunc)
{
VirtualFree((LPVOID) *((PDWORD) &trampolineFunc), 0, MEM_RELEASE);
}
template <typename Func>
Func SetPointer(DWORD location, Func hookFunc)
{
DWORD originalPointer = GetValue<DWORD>(location);
*(Func*) location = hookFunc;
return GetFuncDef<Func>(originalPointer);
}
template <typename Type>
inline Type& GetValue(DWORD location)
{
ReadWriteProtect(location, sizeof(Type));
return *(Type*) location;
}
template <class Func>
inline Func GetFuncDef(DWORD funcAddr)
{
return *(Func*) &funcAddr;
}
DWORD GetUnloadedModuleHandle(LPCTSTR moduleName);
struct NopStr
{
UINT len;
LPCSTR nopSequence;
};
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
UINT32 GetDllProductBuildVersion(LPCSTR dllName);
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#define FILL_VFTABLE(tensPlace) \
virtual void Vftable_x ##tensPlace## 0(); \
virtual void Vftable_x ##tensPlace## 4(); \
virtual void Vftable_x ##tensPlace## 8(); \
virtual void Vftable_x ##tensPlace## C();
+3
View File
@@ -0,0 +1,3 @@
#pragma once
void InitWaypointFixes();
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
struct MissionObjective
{
BYTE fmtStr[0x16]; // 0x0
DWORD flags; // 0x18
};
void InitWaypointNameFixes();
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "Common.h"
#include "vftable.h"
enum EngModelType : DWORD
{
Object = 0,
Virtual = 2,
};
struct EngModel
{
EngModelType type; // 0x00
BYTE x04[0xC];
EngModel* parent; // 0x10
};
struct EngAnimation
{
// The first parameter seems to be a pointer to a stack-struct with the first three DWORDS set to 0 and then a ModelBinary*.
bool SetModel_Hook(PDWORD unk, const EngModel* model);
bool SetModel(PDWORD unk, const EngModel* model);
};
struct IAnimation2
{
FILL_VFTABLE(0);
FILL_VFTABLE(1);
virtual void Vftable_x20();
virtual int __stdcall Open(int scriptIndex, long engineInstance, LPCSTR animationScript, int unk1 = 0, int unk2 = 0);
int Open_Hook(LPCSTR animationScript, int scriptIndex, const CAttachedEquip& equip);
};
void InitWeaponAnimFix();
+77
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+90
View File
@@ -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 methods 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
View File
@@ -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();
}
+38
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+49
View File
@@ -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);
}
+123
View File
@@ -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;
}
+133
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+37
View File
@@ -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);
}
+190
View File
@@ -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);
}
+56
View File
@@ -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
View File
@@ -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;
}
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}