commit d611924a5eb8b05397b4d91b4b2cbee7c33cdd20 Author: Nekura Date: Tue Aug 11 16:48:50 2026 +0200 Initial public release of rem-essentials diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a229980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +build/ +build-*/ +bin/ +obj/ +release/ +*.dll +*.exe +*.ilk +*.iobj +*.ipdb +*.lib +*.obj +*.pdb +*.res +*.suo +*.user +*.vcxproj +*.vcxproj.filters diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..199ae9a --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,150 @@ +cmake_minimum_required(VERSION 3.20) + +cmake_policy(SET CMP0091 NEW) + +project(rem-essentials LANGUAGES CXX) + +set(FLSHARP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/flsharp") +set(FLSHARP_IMPORT_DIR "${CMAKE_CURRENT_BINARY_DIR}/third_party/flsharp") +set(FLSHARP_COMMON_LIB "${FLSHARP_IMPORT_DIR}/Common.lib") +set(FLSHARP_DALIB_LIB "${FLSHARP_IMPORT_DIR}/DALib.lib") +set(FLSHARP_DACOM_LIB "${FLSHARP_IMPORT_DIR}/Dacom.lib") + +file(MAKE_DIRECTORY "${FLSHARP_IMPORT_DIR}") + +add_custom_command( + OUTPUT "${FLSHARP_COMMON_LIB}" + COMMAND lib /NOLOGO /MACHINE:X86 /DEF:"${FLSHARP_DIR}/def/Common.def" /NAME:COMMON /OUT:"${FLSHARP_COMMON_LIB}" + DEPENDS "${FLSHARP_DIR}/def/Common.def" +) + +add_custom_command( + OUTPUT "${FLSHARP_DALIB_LIB}" + COMMAND lib /NOLOGO /MACHINE:X86 /DEF:"${FLSHARP_DIR}/def/DALib.def" /NAME:DALIB /OUT:"${FLSHARP_DALIB_LIB}" + DEPENDS "${FLSHARP_DIR}/def/DALib.def" +) + +add_custom_command( + OUTPUT "${FLSHARP_DACOM_LIB}" + COMMAND lib /NOLOGO /MACHINE:X86 /DEF:"${FLSHARP_DIR}/def/Dacom.def" /NAME:DACOM /OUT:"${FLSHARP_DACOM_LIB}" + DEPENDS "${FLSHARP_DIR}/def/Dacom.def" +) + +add_custom_target(flsharp_imports DEPENDS + "${FLSHARP_COMMON_LIB}" + "${FLSHARP_DALIB_LIB}" + "${FLSHARP_DACOM_LIB}" +) + +set(FLSHARP_SOURCES + third_party/flsharp/src/Freelancer.cpp + third_party/flsharp/src/alchemy_crash.cpp + third_party/flsharp/src/base_info.cpp + third_party/flsharp/src/blank_faction.cpp + third_party/flsharp/src/cheat_detection.cpp + third_party/flsharp/src/copy_paste.cpp + third_party/flsharp/src/cursor_colors.cpp + third_party/flsharp/src/dealer_fixes.cpp + third_party/flsharp/src/dll_crash.cpp + third_party/flsharp/src/exit.cpp + third_party/flsharp/src/flash_particles.cpp + third_party/flsharp/src/fl_math.cpp + third_party/flsharp/src/group_members.cpp + third_party/flsharp/src/infocards.cpp + third_party/flsharp/src/logger.cpp + third_party/flsharp/src/pilot_names.cpp + third_party/flsharp/src/projectiles.cpp + third_party/flsharp/src/rep_requirements.cpp + third_party/flsharp/src/resolutions.cpp + third_party/flsharp/src/resolutions_asm.cpp + third_party/flsharp/src/save_crash.cpp + third_party/flsharp/src/server_filter.cpp + third_party/flsharp/src/shield_capacity.cpp + third_party/flsharp/src/temp_fixes.cpp + third_party/flsharp/src/test_sounds.cpp + third_party/flsharp/src/trade_lane_lights.cpp + third_party/flsharp/src/ui_anim.cpp + third_party/flsharp/src/update.cpp + third_party/flsharp/src/utils.cpp + third_party/flsharp/src/waypoint.cpp + third_party/flsharp/src/waypoint_names.cpp + third_party/flsharp/src/weapon_anim.cpp +) + +set(FLPLUSPLUS_SOURCES + src/features/flplusplus.cpp + src/features/flplusplus_common.cpp + src/features/flplusplus_config.cpp + third_party/flplusplus/src/codec.cpp + third_party/flplusplus/src/consolewindow.cpp + third_party/flplusplus/src/cursor.cpp + third_party/flplusplus/src/fontresource.cpp + third_party/flplusplus/src/Freelancer.cpp + third_party/flplusplus/src/graphics.cpp + third_party/flplusplus/src/log.cpp + third_party/flplusplus/src/patch.cpp + third_party/flplusplus/src/restart.cpp + third_party/flplusplus/src/savegame.cpp + third_party/flplusplus/src/screenshot.cpp + third_party/flplusplus/src/shippreviewscroll.cpp + third_party/flplusplus/src/startlocation.cpp + third_party/flplusplus/src/startup.cpp + third_party/flplusplus/src/thnplayer.cpp + third_party/flplusplus/src/touchpad.cpp +) + +add_library(rem-essentials SHARED + src/main.cpp + src/rem/config.cpp + src/rem/feature_manager.cpp + src/rem/log.cpp + src/rem/patch.cpp + src/rem/runtime.cpp + src/rem/version_guard.cpp + src/features/alchemy_crash.cpp + src/features/jump_waypoint.cpp + src/features/mouse.cpp + src/features/quit_message.cpp + src/features/ship_buy_kick.cpp + ${FLSHARP_SOURCES} + ${FLPLUSPLUS_SOURCES} +) + +add_dependencies(rem-essentials flsharp_imports) + +target_include_directories(rem-essentials PRIVATE "${FLSHARP_DIR}/include" include third_party/flplusplus/src third_party/flplusplus/include) +target_compile_features(rem-essentials PRIVATE cxx_std_17) +target_link_libraries(rem-essentials PRIVATE + user32 + gdi32 + gdiplus + shell32 + shlwapi + version + "${FLSHARP_COMMON_LIB}" + "${FLSHARP_DALIB_LIB}" + "${FLSHARP_DACOM_LIB}" +) + +if(NOT WIN32) + message(FATAL_ERROR "rem-essentials builds a Windows Freelancer DLL.") +endif() + +if(NOT MSVC) + message(FATAL_ERROR "The migrated hooks currently require 32-bit MSVC inline assembly.") +endif() + +if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4) + message(FATAL_ERROR "Freelancer is 32-bit; configure this project with a Win32/x86 generator.") +endif() + +target_compile_options(rem-essentials PRIVATE /W4 /WX /EHsc /O2 /Zc:threadSafeInit-) +set_source_files_properties(${FLSHARP_SOURCES} PROPERTIES COMPILE_OPTIONS "/wd4005;/wd4100;/wd4200;/wd4701;/wd4703") +set_source_files_properties(${FLPLUSPLUS_SOURCES} PROPERTIES COMPILE_OPTIONS "/wd4005;/wd4068;/wd4100;/wd4189;/wd4200;/wd4244;/wd4459;/wd4996") +target_compile_definitions(rem-essentials PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX USE_ST6) +set_property(TARGET rem-essentials PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +set_target_properties(rem-essentials PROPERTIES + OUTPUT_NAME "rem-essentials" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c0131c --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# rem-essentials + +`rem-essentials` is a Freelancer plugin for the REM mod that collects the +client/server fixes REM actually needs. The goal is to keep a single, small, +configurable feature surface that works on native Windows and under +Wine/Proton. + +## Feature surface + +Every feature is individually configurable through `rem-essentials.ini`. +Features cover the mouse/cursor stack, server-side fixes, client stability +patches, navigation, graphics, saves, screenshots, logging, and more. + +The Proton-sensitive mouse stack is split into separate switches: + +- hide the in-game cursor when the real cursor is outside the game window +- prevent Windows cursor flicker at screen borders in borderless windowed mode +- prevent mouse warps to the center of the game window in windowed modes +- no server kick when buying the same ship again +- avoid the quit-message loop hang after the game receives `Quit` +- patch the rare `alchemy.dll` crash around file offset `0x77cb` + +## Documentation + +See [docs/architecture.md](docs/architecture.md) for feature registration rules +and [docs/build.md](docs/build.md) for the current MSVC/x86 build path. +See [docs/deployment.md](docs/deployment.md) for release validation and packaging. +See [docs/configuration.md](docs/configuration.md) for INI precedence, Launcher +overrides, manual installation, and save-directory behavior under Proton. + +## Design rules + +- Keep every fix independently configurable. +- Keep client-only and server-only fixes separate. +- Treat mouse/cursor fixes as Proton-sensitive and test them under Wine/Proton + before enabling them by default. +- Prefer small, named fixes over broad plugin behavior. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..2e28cf1 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,43 @@ +# Architecture + +`rem-essentials` is intentionally feature-first. Every migrated behavior should +live in its own module and be registered as a named feature in `src/main.cpp`. + +## Feature registration + +Each feature has: + +- a stable config key +- an init function +- an optional cleanup function +- a runtime side: `Client`, `Server`, or `Always` +- a code default + +The INI file can override each default. Missing INI entries keep the compiled +default. + +## Current feature keys + +The full key list lives in `rem-essentials.ini`. Feature switches live in +`[rem-essentials]`; typed parameters used by the graphics, saves, and +screenshots behavior live in the `[flplusplus]` section. + +## Design rules + +- Prefer one feature flag per behavior, even when multiple behaviors share one + option. +- Keep client and server hooks in separate files. +- Resolve optional Freelancer imports at runtime when practical. +- Keep adapted third-party code isolated under `third_party/` and wrap it with + thin REM adapters instead of adding a second plugin entry point. + +## Proton-sensitive fixes + +Mouse features stay split because Wine/Proton can handle cursor APIs +differently from native Windows. The current conservative defaults are: + +- `cursor_visibility_fix = true` +- `cursor_border_flicker_fix = false` +- `mouse_warp_fix = false` + +These can be enabled per player in `rem-essentials.ini` after testing. diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..c1cc430 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,37 @@ +# Build + +The migrated hooks currently require 32-bit MSVC because some patches use +`__declspec(naked)` and MSVC inline assembly. + +## Requirements + +- Visual Studio with C++ tools +- CMake 3.20 or newer; the Visual Studio bundled CMake works +- 32-bit/x86 compiler environment + +## Configure and build + +From this workspace, the verified command is: + +```powershell +cmd /s /c '"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars32.bat" && "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" -S . -B build-nmake -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release && "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" --build build-nmake' +``` + +The DLL is written to: + +```text +build-nmake/bin/rem-essentials.dll +``` + +Copy `rem-essentials.dll` and `rem-essentials.ini` to Freelancer's `EXE` +directory and add the DLL to the `[Libraries]` section of `dacom.ini` and, for +server-side features, `dacomsrv.ini`. + +## Current local verification status + +Verified on this machine with Visual Studio 2022 Community's x86 developer +environment. The produced DLL was: + +```text +build-nmake/bin/rem-essentials.dll +``` diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..17b6d7a --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,138 @@ +# Configuration + +`rem-essentials` uses one active runtime configuration and, when the REM +Launcher is used, one persistent user override file. + +## Configuration files + +### Active game configuration + +The DLL reads only this file at game startup: + +```text +Freelancer/EXE/rem-essentials.ini +``` + +The file is shipped with the mod and contains the complete setting schema, +descriptions, metadata, and release defaults. A manual installation only needs +this INI and `rem-essentials.dll` in the `EXE` directory. + +If the active INI is missing, the DLL uses its compiled defaults. The compiled +defaults intentionally match the shipped INI, but distributing the INI is +recommended because it exposes every feature and typed parameter. + +### Launcher user overrides + +The REM Launcher stores player choices separately: + +```text +My Games/REM/rem-essentials.user.ini +``` + +On Wine or Proton, `My Games` is inside the Windows user profile of the active +prefix. Its exact Linux path depends on how the Launcher or game prefix was +created. + +The DLL does not read this user file. The Launcher merges matching values from +the user file into `Freelancer/EXE/rem-essentials.ini`: + +- after a game/mod update +- after saving settings in the Launcher +- immediately before starting Freelancer + +This allows an update to replace the shipped schema while retaining player +choices. New settings from an updated default INI appear automatically. Values +from the user override win when the same section and key exist in both files. + +## Value precedence + +At runtime, the effective value is determined in this order: + +1. The compiled DLL fallback is used when no setting can be read. +2. `Freelancer/EXE/rem-essentials.ini` overrides the compiled fallback. +3. Before launch, the Launcher materializes values from + `rem-essentials.user.ini` into the active INI. + +For a manual installation without the Launcher, only the first two levels +apply. Editing the active INI is therefore sufficient. + +## Sections and value types + +Feature switches live in `[rem-essentials]`. Parameters used by the graphics, +saves, and screenshots behavior live in `[flplusplus]`. + +Boolean values accept: + +```text +true / false +1 / 0 +yes / no +on / off +``` + +Integer, float, and string settings are described by the comments immediately +above each key. Metadata such as `type`, `min`, `max`, `default`, `group`, and +`risk` is also used by the Launcher to build the settings UI. + +## Save directory behavior + +REM keeps the historical installation-local save directory as its default: + +```ini +[rem-essentials] +save_path_feature = true + +[flplusplus] +save_in_directory = true +save_folder_name = Freelancer +``` + +With `save_in_directory = true`, the patched path is calculated relative to +`Freelancer.exe`: + +```text +Freelancer/EXE/../SAVE +``` + +For an installation such as: + +```text +/home//RemLauncher_AppData/current/Freelancer/EXE/Freelancer.exe +``` + +the effective save directory is: + +```text +/home//RemLauncher_AppData/current/Freelancer/SAVE +``` + +With `save_in_directory = false`, the plugin instead uses the Windows +Documents folder exposed by Windows, Wine, or Proton and appends: + +```text +My Games/ +``` + +`save_folder_name` is therefore only relevant when +`save_in_directory = false`. Setting `save_path_feature = false` disables this +hook completely and restores Freelancer's unpatched path behavior. + +Changing the setting does not move existing saves. Move existing files to the +selected directory before launching when switching between layouts. + +## Manual installation + +1. Copy `rem-essentials.dll` and `rem-essentials.ini` into `Freelancer/EXE`. +2. Add `rem-essentials.dll` to the appropriate `[Libraries]` section in + `dacom.ini` and, for FLServer features, `dacomsrv.ini`. +3. Ensure `save_path_feature = true` and `save_in_directory = true` for the REM + installation-local `SAVE` layout. +4. Fully restart Freelancer after changing the INI. Settings are read when the + DLL is loaded. + +## Launcher-managed installation + +Use the Game Settings page to change supported values. Saving creates or +updates `My Games/REM/rem-essentials.user.ini`. Avoid manually changing the +active INI while a conflicting user override exists, because the Launcher will +materialize the user value again before game startup. diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..894b586 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,40 @@ +# Deployment + +## Build and package + +Run the x86 build from the Visual Studio developer environment described in +`docs/build.md`, then package the verified output: + +```powershell +.\tools\package-release.ps1 +``` + +The command verifies that: + +- every registered feature has exactly one INI toggle +- the Launcher and DLL ship the same default INI +- `direct_ips` is not compiled +- the release DLL is x86 and matches the current build +- the Launcher bridge and post-update merge are present + +The generated artifacts are: + +```text +release/rem-essentials.dll +release/rem-essentials-release.zip +``` + +The ZIP can be overlaid on the Freelancer installation. It contains the DLL and +default INI under `EXE/`, plus configuration documentation. + +## Loader configuration + +Add `rem-essentials.dll` to the `[Libraries]` section in client `dacom.ini` and +server `dacomsrv.ini`. + +The Launcher keeps player overrides in +`My Games/REM/rem-essentials.user.ini`. It reapplies them after patching and +immediately before starting Freelancer. + +The complete INI lifecycle, precedence rules, and save-directory mapping are +documented in [configuration.md](configuration.md). diff --git a/include/features/alchemy_crash.h b/include/features/alchemy_crash.h new file mode 100644 index 0000000..710f166 --- /dev/null +++ b/include/features/alchemy_crash.h @@ -0,0 +1,7 @@ +#pragma once + +namespace rem::features +{ + void InitAlchemyCrashFix(); +} + diff --git a/include/features/flplusplus.h b/include/features/flplusplus.h new file mode 100644 index 0000000..6724c79 --- /dev/null +++ b/include/features/flplusplus.h @@ -0,0 +1,21 @@ +#pragma once + +namespace rem::features +{ + void InitFlplusplusTimestampedLogs(); + void InitFlplusplusConsoleLog(); + void InitFlplusplusSavePath(); + void InitFlplusplusRestartRegeneration(); + void InitFlplusplusGraphicsBaseFixes(); + void InitFlplusplusDetailScaling(); + void InitFlplusplusPngScreenshots(); + void InitFlplusplusScreenshotPath(); + void InitFlplusplusWineMp3CodecWarningFix(); + void InitFlplusplusStartLocationWarningFix(); + void InitFlplusplusFontFileLoading(); + void InitFlplusplusThnPlayer(); + void InitFlplusplusShipPreviewScroll(); + void InitFlplusplusFailedSaveDirIds(); + void InitFlplusplusTouchpadSupport(); + void InitFlplusplusCursorConfinement(); +} diff --git a/include/features/flsharp.h b/include/features/flsharp.h new file mode 100644 index 0000000..ae7abab --- /dev/null +++ b/include/features/flsharp.h @@ -0,0 +1,37 @@ +#pragma once + +void InitBetterUpdates(); +void InitWaypointFixes(); +void InitWaypointNameFixes(); +void InitProjectilesSoundFix(); +void InitProjectilesServerFix(); +void InitBetterResolutions(); +void CleanupBetterResolutions(); +void InitTestSounds(); +void InitTradeLaneLightsFix(); +void InitCopyPasteFeature(); +void InitSlideUiAnimFix(); +void InitWeaponAnimFix(); +void InitFlashParticlesFix(); +void InitPrintRepRequirements(); +void InitPostGameDeadlockFix(); +void InitQuitMessageFix(); +void CleanupQuitMessageFix(); +void InitFlightControlsFix(); +void InitDynamicSolarInfocards(); +void InitSaveCrashFix(); +void InitAlchemyCrashFix(); +void InitBlankFactionNameFix(); +void InitServerFilterCrashFix(); +void InitServerFilterSpeedFix(); +void InitMissingDllCrashFix(); +void InitShieldCapacityFix(); +void InitDealerOpenFix(); +void InitDealerCrashFix(); +void InitShipBuyKickFix(); +void InitHostileGroupFormation(); +void InitHostileGroupMembersFix(); +void InitGroupMemberAttitudeFix(); +void InitMoreCursorColors(); +void InitBaseInfoSpacingFix(); +void InitPilotNamesFix(); diff --git a/include/features/jump_waypoint.h b/include/features/jump_waypoint.h new file mode 100644 index 0000000..003d360 --- /dev/null +++ b/include/features/jump_waypoint.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +namespace rem::features +{ + struct Waypoint + { + float pos[3]; + UINT system; + UINT target; + int waypointNumber; + }; + + struct JumpDestinationSolar + { + BYTE x000[0x1B4]; + UINT destinationSystem; + UINT destinationGate; + }; + + void InitJumpDestinationWaypointFix(); +} + diff --git a/include/features/mouse.h b/include/features/mouse.h new file mode 100644 index 0000000..fbf4fff --- /dev/null +++ b/include/features/mouse.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include "rem/vftable.h" + +#define REM_STDCALL __stdcall + +namespace rem::features +{ + void InitCursorVisibilityFix(); + void InitCursorBorderFlickerFix(); + void InitMouseWarpFix(); +} + +struct IDirectInputDevice8_Rem +{ + REM_FILL_VFTABLE(0) + virtual void Vftable_10(); + virtual void Vftable_14(); + virtual void Vftable_18(); + virtual long REM_STDCALL Acquire(); + virtual long REM_STDCALL Unacquire(); + virtual void Vftable_24(); + virtual void Vftable_28(); + virtual void Vftable_2C(); + virtual void Vftable_30(); + virtual long REM_STDCALL SetCooperativeLevel(HWND hwnd, DWORD flags); +}; + diff --git a/include/features/quit_message.h b/include/features/quit_message.h new file mode 100644 index 0000000..6bbb77b --- /dev/null +++ b/include/features/quit_message.h @@ -0,0 +1,8 @@ +#pragma once + +namespace rem::features +{ + void InitQuitMessageFix(); + void CleanupQuitMessageFix(); +} + diff --git a/include/features/ship_buy_kick.h b/include/features/ship_buy_kick.h new file mode 100644 index 0000000..d856bb8 --- /dev/null +++ b/include/features/ship_buy_kick.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "rem/fl_types.h" +#include "rem/st6.h" + +namespace rem::features +{ + struct PlayerData + { + BYTE x00[0x264]; + UINT currentShipId; + BYTE x268[0xBC]; + UINT shipIdOnLand; + }; + + struct BaseGood + { + BYTE x00[0x8]; + UINT goodId; + float price; + int minQuantity; + int maxQuantity; + DWORD unknown18; + + bool IsShipCandidate() const + { + return unknown18 == 0 || unknown18 == 2; + } + }; + + struct BaseGoodCollection + { + UINT baseName; + UINT launchpadName; + DWORD unknown08; + float unknown0C; + st6::list goods; + + bool HasShipPackageWithGood(UINT goodId); + }; + + struct MarketGood + { + BYTE x00[0x10]; + DWORD type; + }; + + struct BaseMarket + { + UINT baseName; + BaseGoodCollection* baseGoods; + }; + + void InitShipBuyKickFix(); +} + diff --git a/include/rem/config.h b/include/rem/config.h new file mode 100644 index 0000000..1f31f5f --- /dev/null +++ b/include/rem/config.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include + +#include "rem/feature_manager.h" + +namespace rem +{ + class Config + { + public: + void Load(HINSTANCE module); + + bool GetBool(const char* section, const char* key, bool fallback) const; + int GetInt(const char* section, const char* key, int fallback) const; + float GetFloat(const char* section, const char* key, float fallback) const; + std::string GetString(const char* section, const char* key, const char* fallback = "") const; + + private: + std::string GetValue(const char* section, const char* key) const; + + std::unordered_map> sections_; + }; + + Config& GetConfig(); + void ReadConfig(HINSTANCE module, FeatureManager& manager); +} diff --git a/include/rem/feature_manager.h b/include/rem/feature_manager.h new file mode 100644 index 0000000..6e3bad9 --- /dev/null +++ b/include/rem/feature_manager.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include + +namespace rem +{ + class Config; + + enum class RuntimeSide + { + Always, + Client, + Server, + }; + + struct Feature + { + std::string name; + void (*init)(); + void (*cleanup)(); + RuntimeSide side; + bool enabled; + bool initialized; + }; + + class FeatureManager + { + public: + void Register(const char* name, void (*init)(), void (*cleanup)(), RuntimeSide side, bool defaultEnabled); + bool SetEnabled(const char* name, bool enabled); + void ApplyConfig(const Config& config); + void InitFeatures(); + void CleanupFeatures(); + + private: + bool Applies(const Feature& feature) const; + + std::vector features_; + }; +} diff --git a/include/rem/fl_types.h b/include/rem/fl_types.h new file mode 100644 index 0000000..ad37239 --- /dev/null +++ b/include/rem/fl_types.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "rem/st6.h" + +namespace rem::fl +{ + struct EquipDesc + { + DWORD unknown; + UINT archId; + }; + + struct EquipDescList + { + st6::list list; + }; + + enum class GoodType : DWORD + { + Commodity = 0, + Hull = 2, + Ship = 3, + }; + + struct GoodInfo + { + BYTE x00[0x4C]; + GoodType type; + BYTE x50[0x4]; + UINT shipId; + BYTE x58[0x38]; + UINT shipHullId; + EquipDescList equipDescLists[3]; + }; +} + diff --git a/include/rem/log.h b/include/rem/log.h new file mode 100644 index 0000000..572228d --- /dev/null +++ b/include/rem/log.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace rem::log +{ + void Info(const char* message); + void Warn(const char* message); + void Error(const char* message); + void ModuleMissing(const char* feature, const char* moduleName); + void ProcMissing(const char* feature, const char* procName); + void UnknownFeature(const char* featureName); + void FeatureInitFailed(const char* featureName, DWORD exceptionCode); +} diff --git a/include/rem/patch.h b/include/rem/patch.h new file mode 100644 index 0000000..b841e43 --- /dev/null +++ b/include/rem/patch.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include +#include +#include + +namespace rem +{ + void Patch(DWORD address, const void* data, UINT length); + void PatchBytes(DWORD address, std::initializer_list bytes); + void Nop(DWORD address, UINT length); + void ReadWriteProtect(DWORD address, DWORD size); + + template + inline void Patch(DWORD address, Type value) + { + Patch(address, &value, sizeof(Type)); + } + + template + inline Type& ValueAt(DWORD address) + { + ReadWriteProtect(address, sizeof(Type)); + return *reinterpret_cast(address); + } + + template + inline Func FunctionAt(DWORD address) + { + return reinterpret_cast(address); + } + + template + Func SetRelPointer(DWORD location, Func hook) + { + DWORD& relative = ValueAt(location); + DWORD original = location + relative + 4; + DWORD hookAddress = *reinterpret_cast(&hook); + relative = hookAddress - (location + 4); + return FunctionAt(original); + } + + template + void Hook(DWORD address, Func hook, UINT instructionLength, bool jump = false) + { + assert(instructionLength >= 5); + Patch(address, jump ? 0xE9 : 0xE8); + SetRelPointer(address + 1, hook); + + if (instructionLength > 5) + Nop(address + 5, instructionLength - 5); + } + + template + Func Trampoline(DWORD address, Func hook, UINT instructionLength) + { + BYTE* gateway = static_cast( + VirtualAlloc(nullptr, instructionLength + 5, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + + ReadWriteProtect(address, instructionLength); + memcpy(gateway, reinterpret_cast(address), instructionLength); + + Hook(address, hook, instructionLength, true); + Hook(reinterpret_cast(gateway + instructionLength), FunctionAt(address + instructionLength), 5, true); + + return FunctionAt(reinterpret_cast(gateway)); + } + + template + void CleanupTrampoline(Func trampoline) + { + VirtualFree(reinterpret_cast(*reinterpret_cast(&trampoline)), 0, MEM_RELEASE); + } +} diff --git a/include/rem/runtime.h b/include/rem/runtime.h new file mode 100644 index 0000000..be10d4a --- /dev/null +++ b/include/rem/runtime.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace rem +{ + bool IsServer(); + bool IsClient(); + bool IsWine(); + HMODULE GetModule(const char* moduleName); +} diff --git a/include/rem/st6.h b/include/rem/st6.h new file mode 100644 index 0000000..eb368a7 --- /dev/null +++ b/include/rem/st6.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +namespace st6 +{ + template + class allocator + { + public: + using size_type = size_t; + using difference_type = ptrdiff_t; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using value_type = T; + }; + + template > + class list + { + struct Node; + using NodePtr = Node*; + + struct Node + { + NodePtr next; + NodePtr prev; + T value; + }; + + public: + class iterator + { + public: + iterator() : ptr_(nullptr) {} + explicit iterator(NodePtr ptr) : ptr_(ptr) {} + + T& operator*() const { return ptr_->value; } + T* operator->() const { return &ptr_->value; } + + iterator& operator++() + { + ptr_ = ptr_->next; + return *this; + } + + bool operator==(const iterator& other) const { return ptr_ == other.ptr_; } + bool operator!=(const iterator& other) const { return ptr_ != other.ptr_; } + + private: + NodePtr ptr_; + }; + + iterator begin() { return iterator(head_->next); } + iterator end() { return iterator(head_); } + + A allocator_; + NodePtr head_; + size_t size_; + }; +} + diff --git a/include/rem/version_guard.h b/include/rem/version_guard.h new file mode 100644 index 0000000..f088fdb --- /dev/null +++ b/include/rem/version_guard.h @@ -0,0 +1,6 @@ +#pragma once + +namespace rem +{ + bool IsSupportedGameVersion(); +} diff --git a/include/rem/vftable.h b/include/rem/vftable.h new file mode 100644 index 0000000..40bafdf --- /dev/null +++ b/include/rem/vftable.h @@ -0,0 +1,8 @@ +#pragma once + +#define REM_FILL_VFTABLE(prefix) \ + virtual void Vftable_##prefix##0(); \ + virtual void Vftable_##prefix##4(); \ + virtual void Vftable_##prefix##8(); \ + virtual void Vftable_##prefix##C(); + diff --git a/rem-essentials.ini b/rem-essentials.ini new file mode 100644 index 0000000..ba1721f --- /dev/null +++ b/rem-essentials.ini @@ -0,0 +1,258 @@ +[rem-essentials] +; type=bool group=Server default=true +; Do not kick players from the server for re-purchasing the same ship. +ship_buy_kick_fix = true + +; type=bool group=Client Stability default=true +; Fix Freelancer sometimes getting stuck in the message loop after the Quit message is retrieved. +quit_message_fix = true + +; type=bool group=Client Stability default=true +; Fix rare alchemy.dll crashes around offsets 0x701b and 0x77cb. +alchemy_crash_fix = true + +; type=bool group=Navigation default=true +; Clear the jump destination waypoint after a successful system jump. +jump_destination_waypoint_fix = true + +; type=bool group=Mouse default=true +; Hide the in-game cursor when the real cursor is outside the game window. +cursor_visibility_fix = true + +; type=bool group=Mouse default=false risk=proton +; Fix Windows cursor flicker at screen borders in borderless windowed mode. +cursor_border_flicker_fix = false + +; type=bool group=Mouse default=false risk=proton +; Prevent mouse warps to the center of the game window in windowed modes. +mouse_warp_fix = false + +; type=bool group=Client Updates default=true +; Smooth client-server updates and fix incorrect engine-state updates. +better_updates = true + +; type=bool group=Navigation default=true +; Keep cross-system waypoints and prevent setting waypoints on the player ship. +waypoint_fixes = true + +; type=bool group=Navigation default=true +; Fix waypoint names and waypoint information in target/current-info views. +waypoint_name_fixes = true + +; type=bool group=Combat default=true +; Play one-shot sounds for multi-barrel launchers. +projectiles_sound_fix = true + +; type=bool group=Combat default=true +; Fix ammo decrementing for multi-barrel launchers on client/server. +projectiles_server_fix = true + +; type=bool group=Display default=false +; Expand selectable resolutions and use the main monitor as default. +better_resolutions = false + +; type=bool group=Audio default=true +; Add interface/ambience test sounds and safer volume slider behavior. +more_test_sounds = true + +; type=bool group=World default=true +; Fix trade-lane lights not turning back on after disruptions. +trade_lane_lights_fix = true + +; type=bool group=UI default=false +; Enable Ctrl+C/Ctrl+V in Freelancer input boxes. +copy_paste_feature = false + +; type=bool group=UI default=true +; Improve slide-out animation for multiplayer menu buttons. +slide_ui_anim_fix = true + +; type=bool group=Equipment default=true +; Enable weapon use_animation scripts and parent animations with leading underscore. +weapon_anim_fix = true + +; type=bool group=Equipment default=true +; Play weapon flash particles on all barrels instead of only the first. +flash_particle_fix = true + +; type=bool group=Dealer default=false +; Print exact reputation requirements in dealer rejection text when resources support it. +print_rep_requirements = false + +; type=bool group=Client Stability default=true +; Move DirectPlay shutdown before process exit to avoid post-game deadlocks. +post_game_deadlock_fix = true + +; type=bool group=Controls default=true +; Reset rotation lock and auto-level defaults when launching to space. +flight_controls_fix = true + +; type=bool group=Infocards default=false +; Show infocards for dynamic solars and archetype fallback infocards. +dynamic_solar_infocards = false + +; type=bool group=Client Stability default=true +; Prevent crashes from malformed save files. +save_crash_fix = true + +; type=bool group=Infocards default=true +; Fix blank faction text for fc_uk_grp ships. +blank_faction_fix = true + +; type=bool group=Client Stability default=true +; Fix crash when closing the Server Filter dialog. +server_filter_crash_fix = true + +; type=bool group=Client Stability default=true +; Fix sudden game-speed increases after opening the Server Filter dialog. +server_filter_speed_fix = true + +; type=bool group=Client Stability default=true +; Prevent Freelancer and FLServer crashing on missing DLL entries in freelancer.ini. +freelancer_dll_crash_fix = true + +; type=bool group=Infocards default=true +; Show shield capacity reduced by offline_threshold. +shield_capacity_fix = true + +; type=bool group=Dealer default=true +; Fix dealer menus not opening when clicking the dealer twice. +dealer_menu_open_fix = true + +; type=bool group=Dealer default=true +; Fix crashes that can occur when opening dealer menus. +dealer_crash_fix = true + +; type=bool group=Groups default=false +; Allow entering formation with hostile group members. +hostile_group_formation = false + +; type=bool group=Groups default=true +; Prevent hostile group members from being shown as hostile. +unhostile_group_members = true + +; type=bool group=Groups default=true +; Show GROUP MEMBER attitude for group members. +group_member_attitude = true + +; type=bool group=Targeting default=false +; Add custom targeting cursor colors for group members and trade request senders. +more_cursor_colors = false + +; type=bool group=Infocards default=true +; Fix first sold-item category entry alignment in Current Information on some resolutions. +base_info_spacing_fix = true + +; type=bool group=Infocards default=true +; Fix long pilot names being truncated in Current Information, comms, and cargo dialogs. +pilot_names_fix = true + +; type=bool group=Logging default=true +; Add timestamps to FLSpew.txt and server log output. +timestamped_logs = true + +; type=bool group=Logging default=false +; Redirect FLSpew output to a console window. +log_to_console = false + +; type=bool group=Saves default=true +; Use the configured save folder path instead of Freelancer's hard-coded default. +save_path_feature = true + +; type=bool group=Wine default=true +; Suppress MP3 codec warning spam on Wine/Proton. +wine_mp3_codec_warning_fix = true + +; type=bool group=Logging default=true +; Remove the Failed to get start location warning. +remove_start_location_warning = true + +; type=bool group=Saves default=true +; Regenerate Restart.fl on startup to avoid malformed restart-file crashes. +always_regenerate_restart_file = true + +; type=bool group=Screenshots default=false +; Replace default screenshots with PNG output. +png_screenshots = false + +; type=bool group=Screenshots default=true +; Use the configured screenshot folder instead of Freelancer's default path. +screenshot_path_feature = true + +; type=bool group=Fonts default=false +; Load private TTF font resources from DATA/FONTS/fonts.ini [FontFiles]. +font_file_loading = false + +; type=bool group=Developer default=false +; Enable THN player behavior when Freelancer is launched with -thn. +thn_player = false + +; type=bool group=Input default=false risk=mouse +; Fix touchpad scrolling but may break normal mouse wheel scrolling. +touchpad_support = false + +; type=bool group=Mouse default=false risk=mouse +; Confine the in-game cursor in windowed mode. +confine_cursor = false + +; type=bool group=Dealer default=false +; Enable mouse-wheel zoom in the ship dealer preview. +ship_preview_scroll = false + +; type=bool group=Graphics default=true +; Disable unsupported video dialog, fix max texture size, and suppress Vibrocentric font override. +graphics_base_fixes = true + +; type=bool group=Graphics default=true +; Apply configurable LOD, pBubble, character-detail, and asteroid-distance scaling. +graphics_detail_scaling = true + +; type=bool group=Saves default=true +; Override the IDS shown when Freelancer cannot initialize the saves directory. +failed_save_dir_ids_override = true + +[flplusplus] +; type=float min=1 max=1000000 group=Graphics default=1.0 +lod_scale = 1.0 + +; type=float min=1 max=1000000 group=Graphics default=1.0 +pbubble_scale = 1.0 + +; type=float min=1 max=1000000 group=Graphics default=1.0 +character_detail_scale = 1.0 + +; type=float min=1 max=10 group=Graphics default=1.0 +asteroid_dist_scale = 1.0 + +; type=string group=Saves default=Freelancer +save_folder_name = Freelancer + +; type=bool group=Saves default=true +save_in_directory = true + +; type=string group=Screenshots default=FreelancerShots +screenshots_folder_name = FreelancerShots + +; type=bool group=Screenshots default=false +screenshots_in_directory = false + +; type=bool group=Screenshots risk=proton default=true +alternative_fullscreen_screenshots_code = true + +; type=bool group=Screenshots risk=proton default=false +alternative_windowed_screenshots_code = false + +; type=int min=0 group=Saves default=1849 +failed_to_init_saves_dir_ids = 1849 + +; type=float min=0 max=50 group=Dealer default=3.0 +ship_preview_scrolling_speed = 3.0 + +; type=bool group=Dealer default=false +ship_preview_scrolling_inverse = false + +; type=float min=0 max=100 group=Dealer default=0.0 +ship_preview_scrolling_min_distance = 0.0 + +; type=float min=0 max=100 group=Dealer default=100.0 +ship_preview_scrolling_max_distance = 100.0 diff --git a/src/features/alchemy_crash.cpp b/src/features/alchemy_crash.cpp new file mode 100644 index 0000000..acddc1a --- /dev/null +++ b/src/features/alchemy_crash.cpp @@ -0,0 +1,67 @@ +#include "features/alchemy_crash.h" + +#include + +#include "rem/log.h" +#include "rem/patch.h" + +#define REM_FASTCALL __fastcall + +namespace +{ + struct Alchemy + { + float progress; + void* effect; + }; + + struct AleLoop + { + int startOffset; + BYTE maxProgressOffset; + }; + + const Alchemy* REM_FASTCALL GetFinishedAle(int maxIndex, const Alchemy* aleArray, float maxProgress) + { + int index = 0; + + for (; index < maxIndex - 1; ++index) + { + if (maxProgress < aleArray[index + 1].progress) + break; + } + + return &aleArray[index]; + } +} + +namespace rem::features +{ + void InitAlchemyCrashFix() + { + constexpr int getFinishedAleStartToEnd = 0x1A; + HMODULE alchemy = GetModuleHandleA("alchemy.dll"); + + if (!alchemy) + { + log::ModuleMissing("alchemy_crash_fix", "alchemy.dll"); + return; + } + + const DWORD base = reinterpret_cast(alchemy); + const AleLoop aleLoops[] = { + { 0x6FDD, 0x10 }, + { 0x778D, 0x10 }, + }; + + for (const AleLoop& loop : aleLoops) + { + const DWORD start = base + loop.startOffset; + PatchBytes(start, { 0x89, 0xF2, 0xFF, 0x74, 0x24 }); + Patch(start + 5, loop.maxProgressOffset); + Hook(start + 6, GetFinishedAle, 20); + Patch(start + getFinishedAleStartToEnd, 0xC689); + } + } +} + diff --git a/src/features/flplusplus.cpp b/src/features/flplusplus.cpp new file mode 100644 index 0000000..3df1dcc --- /dev/null +++ b/src/features/flplusplus.cpp @@ -0,0 +1,175 @@ +#include "features/flplusplus.h" + +#include "codec.h" +#include "config.h" +#include "consolewindow.h" +#include "cursor.h" +#include "fontresource.h" +#include "graphics.h" +#include "log.h" +#include "rem/runtime.h" +#include "rem/log.h" +#include "restart.h" +#include "savegame.h" +#include "screenshot.h" +#include "shippreviewscroll.h" +#include "startlocation.h" +#include "startup.h" +#include "thnplayer.h" +#include "touchpad.h" + +#include + +namespace +{ + char g_dataPath[MAX_PATH]{}; + bool g_pathsInitialized = false; + + void EnsureConfig() + { + config::EnsureInitialized(); + } + + const char* GetDataPath() + { + if (g_pathsInitialized) + return g_dataPath; + + char exePath[MAX_PATH]{}; + GetModuleFileNameA(nullptr, exePath, MAX_PATH); + PathRemoveFileSpecA(exePath); + + strcpy_s(g_dataPath, exePath); + PathRemoveFileSpecA(g_dataPath); + PathAppendA(g_dataPath, "DATA\\"); + + g_pathsInitialized = true; + return g_dataPath; + } + + void ReadFontFiles() + { + char fontsIniPath[MAX_PATH]{}; + strcpy_s(fontsIniPath, GetDataPath()); + PathAppendA(fontsIniPath, "FONTS\\fonts.ini"); + + if (PathFileExistsA(fontsIniPath)) + config::read_font_files(fontsIniPath); + } +} + +namespace rem::features +{ + void InitFlplusplusTimestampedLogs() + { + EnsureConfig(); + if (!logger::enable_fdump(true, false)) + rem::log::ProcMissing("timestamped_logs", "FDUMP"); + if (rem::IsServer()) + if (!logger::patch_serverlogf()) + rem::log::ProcMissing("timestamped_logs", "ServerLogF"); + } + + void InitFlplusplusConsoleLog() + { + EnsureConfig(); + if (config::get_config().logtoconsole) + { + RedirectIOToConsole(); + if (!logger::enable_fdump(false, true)) + rem::log::ProcMissing("log_to_console", "FDUMP"); + } + } + + void InitFlplusplusSavePath() + { + EnsureConfig(); + if (!savegame::init()) + rem::log::ProcMissing("save_path_feature", "?GetUserDataPath@@YA_NQAD@Z"); + } + + void InitFlplusplusRestartRegeneration() + { + EnsureConfig(); + if (!restart::init()) + rem::log::ModuleMissing("always_regenerate_restart_file", "server.dll"); + } + + void InitFlplusplusGraphicsBaseFixes() + { + EnsureConfig(); + if (!graphics::init_base_fixes()) + rem::log::ModuleMissing("graphics_base_fixes", "common.dll"); + } + + void InitFlplusplusDetailScaling() + { + EnsureConfig(); + if (!graphics::init_detail_scaling()) + rem::log::ModuleMissing("graphics_detail_scaling", "common.dll"); + } + + void InitFlplusplusPngScreenshots() + { + EnsureConfig(); + if (!screenshot::init_png()) + rem::log::Error("png_screenshots failed to initialize"); + } + + void InitFlplusplusScreenshotPath() + { + EnsureConfig(); + if (!screenshot::init_path()) + rem::log::ProcMissing("screenshot_path_feature", "?GetScreenShotPath@@YA_NQAD@Z"); + } + + void InitFlplusplusWineMp3CodecWarningFix() + { + EnsureConfig(); + codec::init(); + } + + void InitFlplusplusStartLocationWarningFix() + { + EnsureConfig(); + startlocation::init(); + } + + void InitFlplusplusFontFileLoading() + { + EnsureConfig(); + ReadFontFiles(); + fontresource::init(GetDataPath()); + } + + void InitFlplusplusThnPlayer() + { + EnsureConfig(); + if (!thnplayer::init()) + rem::log::ProcMissing("thn_player", "THN runtime exports"); + } + + void InitFlplusplusShipPreviewScroll() + { + EnsureConfig(); + shippreviewscroll::init(); + } + + void InitFlplusplusFailedSaveDirIds() + { + EnsureConfig(); + startup::init(); + } + + void InitFlplusplusTouchpadSupport() + { + EnsureConfig(); + touchpad::init(); + } + + void InitFlplusplusCursorConfinement() + { + EnsureConfig(); + cursor::init(); + } +} diff --git a/src/features/flplusplus_common.cpp b/src/features/flplusplus_common.cpp new file mode 100644 index 0000000..78b229f --- /dev/null +++ b/src/features/flplusplus_common.cpp @@ -0,0 +1,31 @@ +#include "../../third_party/flplusplus/src/Common.h" + +namespace +{ + template + Func ResolveCommon(const char* name) + { + HMODULE common = GetModuleHandleA("common.dll"); + if (!common) + common = LoadLibraryA("common.dll"); + + return common ? reinterpret_cast(GetProcAddress(common, name)) : nullptr; + } +} + +namespace Universe +{ + IBase* get_base(UINT id) + { + using GetBase = IBase* (__cdecl*)(UINT); + static GetBase function = ResolveCommon("?get_base@Universe@@YAPAUIBase@1@I@Z"); + return function ? function(id) : nullptr; + } + + ISystem* get_system(UINT id) + { + using GetSystem = ISystem* (__cdecl*)(UINT); + static GetSystem function = ResolveCommon("?get_system@Universe@@YAPBUISystem@1@I@Z"); + return function ? function(id) : nullptr; + } +} diff --git a/src/features/flplusplus_config.cpp b/src/features/flplusplus_config.cpp new file mode 100644 index 0000000..80cc9c2 --- /dev/null +++ b/src/features/flplusplus_config.cpp @@ -0,0 +1,117 @@ +#include "config.h" + +#include "Common.h" +#include "rem/config.h" +#include "rem/runtime.h" + +#include + +namespace +{ + config::ConfigData g_config; + bool g_initialized = false; + + bool ReadBool(const char* key, bool fallback) + { + const rem::Config& config = rem::GetConfig(); + return config.GetBool("rem-essentials", key, config.GetBool("flplusplus", key, fallback)); + } +} + +namespace config +{ + ConfigData& get_config() + { + return g_config; + } + + bool is_wine() + { + return rem::IsWine(); + } + + void init_defaults() + { + g_config.lodscale = 1.0f; + g_config.pbubblescale = 1.0f; + g_config.characterdetailscale = 1.0f; + g_config.asteroiddistscale = 1.0f; + g_config.savefoldername = "Freelancer"; + g_config.saveindirectory = true; + g_config.screenshotsfoldername = "REM Screenshots"; + g_config.screenshotsindirectory = false; + g_config.altfullscreenscreenshots = rem::IsWine(); + g_config.altwindowedscreenshots = false; + g_config.removestartlocationwarning = true; + g_config.logtoconsole = false; + g_config.shippreviewscrollingspeed = 3.0f; + g_config.shippreviewscrollinginverse = false; + g_config.shippreviewscrollingmindistance = 0.0f; + g_config.shippreviewscrollingmaxdistance = 100.0f; + g_config.alwaysregeneraterestartfile = true; + g_config.failedtoinitsavesdirids = 1849; + g_config.touchpadsupport = false; + g_config.confinecursor = false; + g_config.fontfiles.clear(); + } + + void init_from_file(const char*) + { + init_defaults(); + const rem::Config& config = rem::GetConfig(); + + g_config.lodscale = config.GetFloat("flplusplus", "lod_scale", g_config.lodscale); + g_config.pbubblescale = config.GetFloat("flplusplus", "pbubble_scale", g_config.pbubblescale); + g_config.characterdetailscale = config.GetFloat("flplusplus", "character_detail_scale", g_config.characterdetailscale); + g_config.asteroiddistscale = config.GetFloat("flplusplus", "asteroid_dist_scale", g_config.asteroiddistscale); + g_config.savefoldername = config.GetString("flplusplus", "save_folder_name", g_config.savefoldername.c_str()); + g_config.saveindirectory = config.GetBool("flplusplus", "save_in_directory", g_config.saveindirectory); + g_config.screenshotsfoldername = config.GetString("flplusplus", "screenshots_folder_name", g_config.screenshotsfoldername.c_str()); + g_config.screenshotsindirectory = config.GetBool("flplusplus", "screenshots_in_directory", g_config.screenshotsindirectory); + g_config.altfullscreenscreenshots = config.GetBool("flplusplus", "alternative_fullscreen_screenshots_code", g_config.altfullscreenscreenshots); + g_config.altwindowedscreenshots = config.GetBool("flplusplus", "alternative_windowed_screenshots_code", g_config.altwindowedscreenshots); + g_config.removestartlocationwarning = ReadBool("remove_start_location_warning", g_config.removestartlocationwarning); + g_config.logtoconsole = ReadBool("log_to_console", g_config.logtoconsole); + g_config.shippreviewscrollingspeed = config.GetFloat("flplusplus", "ship_preview_scrolling_speed", g_config.shippreviewscrollingspeed); + g_config.shippreviewscrollinginverse = config.GetBool("flplusplus", "ship_preview_scrolling_inverse", g_config.shippreviewscrollinginverse); + g_config.shippreviewscrollingmindistance = config.GetFloat("flplusplus", "ship_preview_scrolling_min_distance", g_config.shippreviewscrollingmindistance); + g_config.shippreviewscrollingmaxdistance = config.GetFloat("flplusplus", "ship_preview_scrolling_max_distance", g_config.shippreviewscrollingmaxdistance); + g_config.alwaysregeneraterestartfile = ReadBool("always_regenerate_restart_file", g_config.alwaysregeneraterestartfile); + g_config.failedtoinitsavesdirids = config.GetInt("flplusplus", "failed_to_init_saves_dir_ids", g_config.failedtoinitsavesdirids); + g_config.touchpadsupport = ReadBool("touchpad_support", g_config.touchpadsupport); + g_config.confinecursor = ReadBool("confine_cursor", g_config.confinecursor); + } + + void read_font_files(const char* filename) + { + if (!ReadBool("font_file_loading", false)) + return; + + INI_Reader reader; + if (!reader.open(filename, false)) + return; + + while (reader.read_header()) + { + if (reader.is_header("FontFiles")) + { + while (reader.read_value()) + { + if (reader.is_value("path")) + g_config.fontfiles.emplace_back(reader.get_value_string(0)); + } + } + } + + reader.close(); + } + + void EnsureInitialized() + { + if (g_initialized) + return; + + init_from_file(nullptr); + g_initialized = true; + } +} diff --git a/src/features/jump_waypoint.cpp b/src/features/jump_waypoint.cpp new file mode 100644 index 0000000..1f6aa7c --- /dev/null +++ b/src/features/jump_waypoint.cpp @@ -0,0 +1,66 @@ +#include "features/jump_waypoint.h" + +#include "rem/patch.h" + +#define REM_FASTCALL __fastcall +#define REM_NAKED __declspec(naked) + +namespace +{ + using rem::features::JumpDestinationSolar; + using rem::features::Waypoint; + + using GetWaypointFn = Waypoint* (__cdecl*)(int); + using DeleteWaypointFn = void (__cdecl*)(int); + + constexpr DWORD getWaypointAddress = 0x4C46A0; + constexpr DWORD deleteWaypointAddress = 0x4C46E0; + constexpr DWORD jumpTunnelEntrypointAddress = 0x5037E1; + + Waypoint* GetWaypoint(int index) + { + return rem::FunctionAt(getWaypointAddress)(index); + } + + void DeleteWaypoint(int index) + { + rem::FunctionAt(deleteWaypointAddress)(index); + } + + void REM_FASTCALL ClearWaypointsToJumpDestination(const JumpDestinationSolar& solar) + { + if (!solar.destinationGate) + return; + + for (int index = 0; Waypoint* waypoint = GetWaypoint(index); ++index) + { + if (waypoint->system != solar.destinationSystem || waypoint->target != solar.destinationGate) + continue; + + for (; index >= 0; --index) + DeleteWaypoint(index); + + break; + } + } + + REM_NAKED void ClearWaypointsToJumpDestinationHook() + { + __asm { + pushad + mov ecx, edx + call ClearWaypointsToJumpDestination + popad + ret + } + } +} + +namespace rem::features +{ + void InitJumpDestinationWaypointFix() + { + rem::Hook(jumpTunnelEntrypointAddress, ClearWaypointsToJumpDestinationHook, 5); + } +} + diff --git a/src/features/mouse.cpp b/src/features/mouse.cpp new file mode 100644 index 0000000..434d1fa --- /dev/null +++ b/src/features/mouse.cpp @@ -0,0 +1,119 @@ +#include "features/mouse.h" + +#include "rem/patch.h" + +#define REM_FASTCALL __fastcall + +namespace +{ + int& MouseX() + { + return *reinterpret_cast(0x616840); + } + + int& MouseY() + { + return *reinterpret_cast(0x616844); + } + + int WindowWidth() + { + return *reinterpret_cast(0x679BC8); + } + + int WindowHeight() + { + return *reinterpret_cast(0x679BCC); + } + + bool ShowMouseCursor() + { + return *reinterpret_cast(0x6107DC); + } + + HWND FreelancerWindow() + { + return *reinterpret_cast(0x67ECA0); + } + + bool IsGameFullscreen() + { + constexpr DWORD fullscreenFlag = 1; + return (*reinterpret_cast(0x679BE5) & fullscreenFlag) != 0; + } + + bool ShowMouseCursorHook() + { + if (MouseX() < 0 || MouseY() < 0) + return false; + + if (MouseX() >= WindowWidth() || MouseY() >= WindowHeight()) + return false; + + return ShowMouseCursor(); + } + + void ForceShowWindowsCursor() + { + while (ShowCursor(TRUE) < 1) + { + } + } + + void REM_STDCALL ShowCursorHook() + { + POINT point{}; + + if (GetCursorPos(&point) && ScreenToClient(FreelancerWindow(), &point)) + { + if (point.x < 0 || point.y < 0 || point.x >= WindowWidth() || point.y > WindowHeight()) + ForceShowWindowsCursor(); + + MouseX() = point.x; + MouseY() = point.y; + } + } + + long REM_FASTCALL AcquireHook(IDirectInputDevice8_Rem& mouseDevice) + { + const long result = mouseDevice.Acquire(); + + if (result == S_OK && !IsGameFullscreen()) + { + POINT point{ MouseX(), MouseY() }; + + if (ClientToScreen(FreelancerWindow(), &point)) + SetCursorPos(point.x, point.y); + } + + return result; + } +} + +namespace rem::features +{ + void InitCursorVisibilityFix() + { + constexpr DWORD showMouseCursorCheckAddress = 0x41F30A; + Hook(showMouseCursorCheckAddress, ShowMouseCursorHook, 5); + } + + void InitCursorBorderFlickerFix() + { + constexpr DWORD showWinCursorAddress = 0x420335; + constexpr DWORD cursorBottomBorderCheckAddress = 0x41EA9B; + + Hook(showWinCursorAddress, ShowCursorHook, 19); + Patch(cursorBottomBorderCheckAddress, 0xEB); + } + + void InitMouseWarpFix() + { + constexpr DWORD thisPointerAcquireMouseAddress = 0x41F7D1; + constexpr DWORD acquireMouseAddress = 0x41F7D3; + + Patch(thisPointerAcquireMouseAddress, 0x4E); + Hook(acquireMouseAddress, AcquireHook, 6); + } +} + diff --git a/src/features/quit_message.cpp b/src/features/quit_message.cpp new file mode 100644 index 0000000..e7fa341 --- /dev/null +++ b/src/features/quit_message.cpp @@ -0,0 +1,34 @@ +#include "features/quit_message.h" + +#include + +#include "rem/patch.h" + +namespace +{ + bool noQuitMessageRetrieved = true; + bool (*HandleMessagesOriginal)(WPARAM* msgWParam) = nullptr; + + bool HandleMessagesHook(WPARAM* msgWParam) + { + const bool result = HandleMessagesOriginal(msgWParam); + noQuitMessageRetrieved = noQuitMessageRetrieved && result; + return noQuitMessageRetrieved; + } +} + +namespace rem::features +{ + void InitQuitMessageFix() + { + constexpr DWORD handleMessagesAddress = 0x5B0B60; + HandleMessagesOriginal = Trampoline(handleMessagesAddress, HandleMessagesHook, 5); + } + + void CleanupQuitMessageFix() + { + if (HandleMessagesOriginal) + CleanupTrampoline(HandleMessagesOriginal); + } +} + diff --git a/src/features/ship_buy_kick.cpp b/src/features/ship_buy_kick.cpp new file mode 100644 index 0000000..d74e8e1 --- /dev/null +++ b/src/features/ship_buy_kick.cpp @@ -0,0 +1,128 @@ +#include "features/ship_buy_kick.h" + +#include + +#include "rem/log.h" +#include "rem/patch.h" +#include "rem/runtime.h" + +#define REM_FASTCALL __fastcall +#define REM_NAKED __declspec(naked) + +namespace +{ + using rem::features::BaseGoodCollection; + using rem::features::BaseMarket; + using rem::features::MarketGood; + using rem::features::PlayerData; + + using GetSoldGoodFn = const MarketGood* (__thiscall*)(const BaseMarket*, UINT); + using FindGoodByIdFn = const rem::fl::GoodInfo* (__cdecl*)(UINT); + + GetSoldGoodFn getSoldGood = nullptr; + FindGoodByIdFn findGoodById = nullptr; + + bool ShipPackageContainsGood(const rem::fl::GoodInfo& shipPackage, UINT goodId) + { + for (const rem::fl::EquipDescList& equipDescList : shipPackage.equipDescLists) + { + for (auto equip = const_cast(equipDescList).list.begin(); + equip != const_cast(equipDescList).list.end(); + ++equip) + { + if (equip->archId == goodId) + return true; + } + } + + return false; + } + + const MarketGood* REM_FASTCALL GetGoodSoldByBaseOrPartOfShip(const BaseMarket& baseMarket, const PlayerData& playerData, UINT goodId) + { + const MarketGood* result = getSoldGood(&baseMarket, goodId); + + if (result) + return result; + + if (playerData.currentShipId + && playerData.currentShipId == playerData.shipIdOnLand + && baseMarket.baseGoods + && baseMarket.baseGoods->HasShipPackageWithGood(goodId)) + { + static const MarketGood validMarketGood = {}; + return &validMarketGood; + } + + return nullptr; + } + + REM_NAKED void GetGoodSoldByBaseHook() + { + __asm { + mov edx, esi + jmp GetGoodSoldByBaseOrPartOfShip + } + } +} + +namespace rem::features +{ + bool BaseGoodCollection::HasShipPackageWithGood(UINT goodId) + { + if (!findGoodById) + return false; + + for (auto good = goods.begin(); good != goods.end(); ++good) + { + if (!good->IsShipCandidate()) + continue; + + const rem::fl::GoodInfo* goodInfo = findGoodById(good->goodId); + + if (goodInfo && goodInfo->type == rem::fl::GoodType::Ship && ShipPackageContainsGood(*goodInfo, goodId)) + return true; + } + + return false; + } + + void InitShipBuyKickFix() + { + constexpr DWORD getGoodSoldByBaseCallOffsetServer = 0x6FEEB; + constexpr DWORD getGoodSoldByBaseOffsetServer = 0x33000; + constexpr const char* findByIdSymbol = "?find_by_id@GoodList@@YAPBUGoodInfo@@I@Z"; + + HMODULE server = GetModuleHandleA("server.dll"); + + if (!server) + { + log::ModuleMissing("ship_buy_kick_fix", "server.dll"); + return; + } + + HMODULE common = rem::GetModule("common.dll"); + + if (!common) + { + log::ModuleMissing("ship_buy_kick_fix", "common.dll"); + return; + } + + FARPROC findByIdProc = GetProcAddress(common, findByIdSymbol); + +#pragma warning(suppress: 4191) + findGoodById = reinterpret_cast(findByIdProc); + + if (!findGoodById) + { + log::ProcMissing("ship_buy_kick_fix", findByIdSymbol); + return; + } + + const DWORD serverBase = reinterpret_cast(server); + getSoldGood = reinterpret_cast(serverBase + getGoodSoldByBaseOffsetServer); + + Hook(serverBase + getGoodSoldByBaseCallOffsetServer, GetGoodSoldByBaseHook, 5); + } +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a284911 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,95 @@ +#include + +#include "features/flsharp.h" +#include "features/flplusplus.h" +#include "features/jump_waypoint.h" +#include "features/mouse.h" +#include "rem/config.h" +#include "rem/feature_manager.h" +#include "rem/version_guard.h" + +namespace +{ + rem::FeatureManager manager; + + void Init(HINSTANCE module) + { + if (!rem::IsSupportedGameVersion()) + return; + + manager.Register("better_updates", InitBetterUpdates, nullptr, rem::RuntimeSide::Client, false); + manager.Register("waypoint_fixes", InitWaypointFixes, nullptr, rem::RuntimeSide::Client, true); + manager.Register("waypoint_name_fixes", InitWaypointNameFixes, nullptr, rem::RuntimeSide::Client, true); + manager.Register("projectiles_sound_fix", InitProjectilesSoundFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("projectiles_server_fix", InitProjectilesServerFix, nullptr, rem::RuntimeSide::Always, true); + manager.Register("better_resolutions", InitBetterResolutions, CleanupBetterResolutions, rem::RuntimeSide::Client, false); + manager.Register("more_test_sounds", InitTestSounds, nullptr, rem::RuntimeSide::Client, true); + manager.Register("trade_lane_lights_fix", InitTradeLaneLightsFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("copy_paste_feature", InitCopyPasteFeature, nullptr, rem::RuntimeSide::Client, false); + manager.Register("slide_ui_anim_fix", InitSlideUiAnimFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("weapon_anim_fix", InitWeaponAnimFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("flash_particle_fix", InitFlashParticlesFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("print_rep_requirements", InitPrintRepRequirements, nullptr, rem::RuntimeSide::Client, false); + manager.Register("post_game_deadlock_fix", InitPostGameDeadlockFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("quit_message_fix", InitQuitMessageFix, CleanupQuitMessageFix, rem::RuntimeSide::Client, true); + manager.Register("flight_controls_fix", InitFlightControlsFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("dynamic_solar_infocards", InitDynamicSolarInfocards, nullptr, rem::RuntimeSide::Client, false); + manager.Register("save_crash_fix", InitSaveCrashFix, nullptr, rem::RuntimeSide::Always, true); + manager.Register("alchemy_crash_fix", InitAlchemyCrashFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("blank_faction_fix", InitBlankFactionNameFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("server_filter_crash_fix", InitServerFilterCrashFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("server_filter_speed_fix", InitServerFilterSpeedFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("freelancer_dll_crash_fix", InitMissingDllCrashFix, nullptr, rem::RuntimeSide::Always, true); + manager.Register("shield_capacity_fix", InitShieldCapacityFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("dealer_menu_open_fix", InitDealerOpenFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("dealer_crash_fix", InitDealerCrashFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("ship_buy_kick_fix", InitShipBuyKickFix, nullptr, rem::RuntimeSide::Server, true); + manager.Register("hostile_group_formation", InitHostileGroupFormation, nullptr, rem::RuntimeSide::Client, false); + manager.Register("unhostile_group_members", InitHostileGroupMembersFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("group_member_attitude", InitGroupMemberAttitudeFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("more_cursor_colors", InitMoreCursorColors, nullptr, rem::RuntimeSide::Client, false); + manager.Register("base_info_spacing_fix", InitBaseInfoSpacingFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("pilot_names_fix", InitPilotNamesFix, nullptr, rem::RuntimeSide::Client, true); + + manager.Register("jump_destination_waypoint_fix", rem::features::InitJumpDestinationWaypointFix, nullptr, rem::RuntimeSide::Client, true); + + manager.Register("cursor_visibility_fix", rem::features::InitCursorVisibilityFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("cursor_border_flicker_fix", rem::features::InitCursorBorderFlickerFix, nullptr, rem::RuntimeSide::Client, false); + manager.Register("mouse_warp_fix", rem::features::InitMouseWarpFix, nullptr, rem::RuntimeSide::Client, false); + + manager.Register("timestamped_logs", rem::features::InitFlplusplusTimestampedLogs, nullptr, rem::RuntimeSide::Always, true); + manager.Register("log_to_console", rem::features::InitFlplusplusConsoleLog, nullptr, rem::RuntimeSide::Always, false); + manager.Register("save_path_feature", rem::features::InitFlplusplusSavePath, nullptr, rem::RuntimeSide::Always, true); + manager.Register("always_regenerate_restart_file", rem::features::InitFlplusplusRestartRegeneration, nullptr, rem::RuntimeSide::Client, true); + manager.Register("graphics_base_fixes", rem::features::InitFlplusplusGraphicsBaseFixes, nullptr, rem::RuntimeSide::Client, true); + manager.Register("graphics_detail_scaling", rem::features::InitFlplusplusDetailScaling, nullptr, rem::RuntimeSide::Client, true); + manager.Register("png_screenshots", rem::features::InitFlplusplusPngScreenshots, nullptr, rem::RuntimeSide::Client, false); + manager.Register("screenshot_path_feature", rem::features::InitFlplusplusScreenshotPath, nullptr, rem::RuntimeSide::Client, true); + manager.Register("wine_mp3_codec_warning_fix", rem::features::InitFlplusplusWineMp3CodecWarningFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("remove_start_location_warning", rem::features::InitFlplusplusStartLocationWarningFix, nullptr, rem::RuntimeSide::Client, true); + manager.Register("font_file_loading", rem::features::InitFlplusplusFontFileLoading, nullptr, rem::RuntimeSide::Client, false); + manager.Register("thn_player", rem::features::InitFlplusplusThnPlayer, nullptr, rem::RuntimeSide::Client, false); + manager.Register("ship_preview_scroll", rem::features::InitFlplusplusShipPreviewScroll, nullptr, rem::RuntimeSide::Client, false); + manager.Register("failed_save_dir_ids_override", rem::features::InitFlplusplusFailedSaveDirIds, nullptr, rem::RuntimeSide::Client, true); + manager.Register("touchpad_support", rem::features::InitFlplusplusTouchpadSupport, nullptr, rem::RuntimeSide::Client, false); + manager.Register("confine_cursor", rem::features::InitFlplusplusCursorConfinement, nullptr, rem::RuntimeSide::Client, false); + + rem::ReadConfig(module, manager); + manager.InitFeatures(); + } +} + +BOOL WINAPI DllMain(HINSTANCE module, DWORD reason, LPVOID) +{ + if (reason == DLL_PROCESS_ATTACH) + { + DisableThreadLibraryCalls(module); + Init(module); + } + else if (reason == DLL_PROCESS_DETACH) + { + manager.CleanupFeatures(); + } + + return TRUE; +} diff --git a/src/rem/config.cpp b/src/rem/config.cpp new file mode 100644 index 0000000..1b3f46e --- /dev/null +++ b/src/rem/config.cpp @@ -0,0 +1,172 @@ +#include "rem/config.h" + +#include +#include +#include +#include +#include +#include + +#include "rem/log.h" + +namespace +{ + std::string ConfigPath(HINSTANCE module) + { + std::array path{}; + DWORD length = GetModuleFileNameA(module, path.data(), static_cast(path.size())); + + if (length == 0 || length >= path.size()) + return "rem-essentials.ini"; + + std::string result(path.data(), length); + const size_t slash = result.find_last_of("\\/"); + + if (slash == std::string::npos) + return "rem-essentials.ini"; + + return result.substr(0, slash + 1) + "rem-essentials.ini"; + } + + bool ParseBool(const char* value, bool fallback) + { + if (_stricmp(value, "true") == 0 || _stricmp(value, "1") == 0 || _stricmp(value, "yes") == 0 || _stricmp(value, "on") == 0) + return true; + + if (_stricmp(value, "false") == 0 || _stricmp(value, "0") == 0 || _stricmp(value, "no") == 0 || _stricmp(value, "off") == 0) + return false; + + return fallback; + } + + std::string Trim(const std::string& value) + { + size_t start = 0; + while (start < value.size() && std::isspace(static_cast(value[start]))) + ++start; + + size_t end = value.size(); + while (end > start && std::isspace(static_cast(value[end - 1]))) + --end; + + return value.substr(start, end - start); + } + + std::string ToLower(std::string value) + { + for (char& ch : value) + ch = static_cast(std::tolower(static_cast(ch))); + + return value; + } +} + +namespace rem +{ + Config& GetConfig() + { + static Config config; + return config; + } + + void Config::Load(HINSTANCE module) + { + sections_.clear(); + + std::ifstream file(ConfigPath(module)); + if (!file) + return; + + std::string currentSection; + std::string line; + + while (std::getline(file, line)) + { + const size_t comment = line.find(';'); + if (comment != std::string::npos) + line = line.substr(0, comment); + + line = Trim(line); + if (line.empty()) + continue; + + if (line.front() == '[' && line.back() == ']') + { + currentSection = ToLower(Trim(line.substr(1, line.size() - 2))); + continue; + } + + const size_t equals = line.find('='); + if (equals == std::string::npos || currentSection.empty()) + continue; + + std::string key = ToLower(Trim(line.substr(0, equals))); + std::string value = Trim(line.substr(equals + 1)); + sections_[currentSection][key] = value; + } + } + + bool Config::GetBool(const char* section, const char* key, bool fallback) const + { + const std::string value = GetValue(section, key); + return value.empty() ? fallback : ParseBool(value.c_str(), fallback); + } + + int Config::GetInt(const char* section, const char* key, int fallback) const + { + const std::string value = GetValue(section, key); + if (value.empty()) + return fallback; + + try + { + return std::stoi(value); + } + catch (...) + { + return fallback; + } + } + + float Config::GetFloat(const char* section, const char* key, float fallback) const + { + const std::string value = GetValue(section, key); + if (value.empty()) + return fallback; + + try + { + return std::stof(value); + } + catch (...) + { + return fallback; + } + } + + std::string Config::GetString(const char* section, const char* key, const char* fallback) const + { + const std::string value = GetValue(section, key); + return value.empty() ? fallback : value; + } + + std::string Config::GetValue(const char* section, const char* key) const + { + const auto sectionIt = sections_.find(ToLower(section)); + if (sectionIt == sections_.end()) + return {}; + + const auto keyIt = sectionIt->second.find(ToLower(key)); + if (keyIt == sectionIt->second.end()) + return {}; + + return keyIt->second; + } + + void ReadConfig(HINSTANCE module, FeatureManager& manager) + { + Config& config = GetConfig(); + config.Load(module); + manager.ApplyConfig(config); + } +} diff --git a/src/rem/feature_manager.cpp b/src/rem/feature_manager.cpp new file mode 100644 index 0000000..f8e8817 --- /dev/null +++ b/src/rem/feature_manager.cpp @@ -0,0 +1,92 @@ +#include "rem/feature_manager.h" + +#include +#include + +#include "rem/config.h" +#include "rem/log.h" +#include "rem/runtime.h" + +namespace +{ + bool TryInitializeFeature(void (*init)(), DWORD& exceptionCode) + { + __try + { + init(); + return true; + } + __except (exceptionCode = GetExceptionCode(), EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } +} + +namespace rem +{ + void FeatureManager::Register(const char* name, void (*init)(), void (*cleanup)(), RuntimeSide side, bool defaultEnabled) + { + features_.push_back(Feature{ name, init, cleanup, side, defaultEnabled, false }); + } + + bool FeatureManager::SetEnabled(const char* name, bool enabled) + { + auto feature = std::find_if(features_.begin(), features_.end(), [name](const Feature& candidate) { + return _stricmp(candidate.name.c_str(), name) == 0; + }); + + if (feature == features_.end()) + { + log::UnknownFeature(name); + return false; + } + + feature->enabled = enabled; + return true; + } + + void FeatureManager::ApplyConfig(const Config& config) + { + for (Feature& feature : features_) + feature.enabled = config.GetBool("rem-essentials", feature.name.c_str(), feature.enabled); + } + + void FeatureManager::InitFeatures() + { + for (Feature& feature : features_) + { + if (!feature.enabled || !feature.init || !Applies(feature)) + continue; + + DWORD exceptionCode = 0; + feature.initialized = TryInitializeFeature(feature.init, exceptionCode); + if (!feature.initialized) + log::FeatureInitFailed(feature.name.c_str(), exceptionCode); + } + } + + void FeatureManager::CleanupFeatures() + { + for (auto feature = features_.rbegin(); feature != features_.rend(); ++feature) + { + if (feature->initialized && feature->cleanup) + feature->cleanup(); + } + } + + bool FeatureManager::Applies(const Feature& feature) const + { + switch (feature.side) + { + case RuntimeSide::Always: + return true; + case RuntimeSide::Client: + return IsClient(); + case RuntimeSide::Server: + return IsServer(); + default: + return false; + } + } +} diff --git a/src/rem/log.cpp b/src/rem/log.cpp new file mode 100644 index 0000000..0251675 --- /dev/null +++ b/src/rem/log.cpp @@ -0,0 +1,59 @@ +#include "rem/log.h" + +#include + +namespace +{ + void Write(const char* level, const char* message) + { + char buffer[512]{}; + sprintf_s(buffer, "rem-essentials [%s]: %s\n", level, message); + OutputDebugStringA(buffer); + } +} + +namespace rem::log +{ + void Info(const char* message) + { + Write("info", message); + } + + void Warn(const char* message) + { + Write("warn", message); + } + + void Error(const char* message) + { + Write("error", message); + } + + void ModuleMissing(const char* feature, const char* moduleName) + { + char buffer[256]{}; + sprintf_s(buffer, "%s could not find module %s", feature, moduleName); + Error(buffer); + } + + void ProcMissing(const char* feature, const char* procName) + { + char buffer[256]{}; + sprintf_s(buffer, "%s could not find proc %s", feature, procName); + Error(buffer); + } + + void UnknownFeature(const char* featureName) + { + char buffer[256]{}; + sprintf_s(buffer, "unknown config feature %s", featureName); + Warn(buffer); + } + + void FeatureInitFailed(const char* featureName, DWORD exceptionCode) + { + char buffer[256]{}; + sprintf_s(buffer, "feature %s failed to initialize (exception 0x%08lX)", featureName, exceptionCode); + Error(buffer); + } +} diff --git a/src/rem/patch.cpp b/src/rem/patch.cpp new file mode 100644 index 0000000..727dbe5 --- /dev/null +++ b/src/rem/patch.cpp @@ -0,0 +1,53 @@ +#include "rem/patch.h" + +#include + +namespace rem +{ + void ReadWriteProtect(DWORD address, DWORD size) + { + DWORD oldProtect = 0; + VirtualProtect(reinterpret_cast(address), size, PAGE_EXECUTE_READWRITE, &oldProtect); + } + + void Patch(DWORD address, const void* data, UINT length) + { + ReadWriteProtect(address, length); + memcpy(reinterpret_cast(address), data, length); + } + + void PatchBytes(DWORD address, std::initializer_list bytes) + { + Patch(address, bytes.begin(), static_cast(bytes.size())); + } + + void Nop(DWORD address, UINT length) + { + static const struct + { + UINT length; + const char* bytes; + } nopTable[] = { + { 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& nop : nopTable) + { + while (length >= nop.length) + { + Patch(address, nop.bytes, nop.length); + address += nop.length; + length -= nop.length; + } + } + } +} + diff --git a/src/rem/runtime.cpp b/src/rem/runtime.cpp new file mode 100644 index 0000000..143085c --- /dev/null +++ b/src/rem/runtime.cpp @@ -0,0 +1,41 @@ +#include "rem/runtime.h" + +#include +#include +#include + +namespace rem +{ + bool IsServer() + { + std::array path{}; + DWORD length = GetModuleFileNameA(nullptr, path.data(), static_cast(path.size())); + + if (length == 0 || length >= path.size()) + return false; + + std::string process(path.data(), length); + const size_t slash = process.find_last_of("\\/"); + + if (slash != std::string::npos) + process = process.substr(slash + 1); + + return _stricmp(process.c_str(), "flserver.exe") == 0; + } + + bool IsClient() + { + return !IsServer(); + } + + bool IsWine() + { + HMODULE ntdll = GetModuleHandleA("ntdll.dll"); + return ntdll && GetProcAddress(ntdll, "wine_get_version") != nullptr; + } + + HMODULE GetModule(const char* moduleName) + { + return GetModuleHandleA(moduleName); + } +} diff --git a/src/rem/version_guard.cpp b/src/rem/version_guard.cpp new file mode 100644 index 0000000..ec23421 --- /dev/null +++ b/src/rem/version_guard.cpp @@ -0,0 +1,99 @@ +#include "rem/version_guard.h" + +#include + +#include +#include + +#include "rem/log.h" +#include "rem/runtime.h" + +namespace +{ + constexpr std::uint16_t FreelancerV10Build = 1223; + + bool ReadProductBuild(const char* moduleName, std::uint16_t& build) + { + HMODULE module = GetModuleHandleA(moduleName); + if (!module) + module = LoadLibraryA(moduleName); + + if (!module) + { + rem::log::ModuleMissing("version_guard", moduleName); + return false; + } + + char path[MAX_PATH]{}; + if (GetModuleFileNameA(module, path, MAX_PATH) == 0) + return false; + + DWORD ignored = 0; + const DWORD infoSize = GetFileVersionInfoSizeA(path, &ignored); + if (infoSize == 0) + return false; + + std::vector info(infoSize); + if (!GetFileVersionInfoA(path, 0, infoSize, info.data())) + return false; + + VS_FIXEDFILEINFO* fixedInfo = nullptr; + UINT fixedInfoSize = 0; + if (!VerQueryValueA(info.data(), "\\", reinterpret_cast(&fixedInfo), &fixedInfoSize) + || !fixedInfo + || fixedInfoSize < sizeof(VS_FIXEDFILEINFO) + || fixedInfo->dwSignature != 0xFEEF04BD) + { + return false; + } + + build = HIWORD(fixedInfo->dwProductVersionLS); + return true; + } + + bool CheckModule(const char* moduleName) + { + std::uint16_t build = 0; + if (!ReadProductBuild(moduleName, build)) + { + char message[256]{}; + sprintf_s(message, "could not verify the product version of %s", moduleName); + rem::log::Error(message); + return false; + } + + if (build <= FreelancerV10Build) + { + char message[256]{}; + sprintf_s(message, "%s build %u is unsupported; Freelancer 1.1 is required", moduleName, build); + rem::log::Error(message); + return false; + } + + return true; + } +} + +namespace rem +{ + bool IsSupportedGameVersion() + { + const bool commonSupported = CheckModule("common.dll"); + const bool serverSupported = CheckModule("server.dll"); + if (commonSupported && serverSupported) + return true; + + log::Error("initialization stopped because the Freelancer 1.1 module check failed"); + if (IsClient()) + { + MessageBoxA( + nullptr, + "rem-essentials requires the official Freelancer 1.1 modules.\n" + "No memory patches were applied.", + "REM Essentials", + MB_OK | MB_ICONERROR); + } + + return false; + } +} diff --git a/third_party/flplusplus/include/flplusplus.h b/third_party/flplusplus/include/flplusplus.h new file mode 100644 index 0000000..c906738 --- /dev/null +++ b/third_party/flplusplus/include/flplusplus.h @@ -0,0 +1,15 @@ +#ifndef _FLPLUSPLUS_H_ +#define _FLPLUSPLUS_H_ + +#define FLPEXPORT __declspec(dllexport) +#ifdef __cplusplus +extern "C" { +#endif + +//typedef void (*flplusplus_cblatehook)(void*); +//FLPEXPORT void flplusplus_add_latehook(flplusplus_cblatehook hkfunc, void *userData); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/third_party/flplusplus/src/Common.h b/third_party/flplusplus/src/Common.h new file mode 100644 index 0000000..b39a216 --- /dev/null +++ b/third_party/flplusplus/src/Common.h @@ -0,0 +1,71 @@ +#pragma once +#pragma ms_struct on + +#define WIN32_LEAN_AND_MEAN +#include +#include "offsets.h" + +class INI_Reader +{ +public: + INI_Reader(); + ~INI_Reader(); + + bool open(LPCSTR path, bool throwExceptionOnFail); + bool read_header(); + bool is_header(LPCSTR header); + bool read_value(); + bool is_value(LPCSTR value); + bool get_value_bool(UINT index); + int get_value_int(UINT index); + float get_value_float(UINT index); + LPCSTR get_value_string(UINT index); + void close(); + LPCSTR get_name_ptr(); + +private: + BYTE data[0x1568]; +}; + +namespace Archetype +{ + struct Ship + { + BYTE x00[0x14]; + UINT idsName; + }; + + Ship* GetShip(UINT id); +} + +struct CShip +{ + BYTE x00[0x88]; + Archetype::Ship* shiparch; +}; + +struct IObjInspectImpl +{ + BYTE data[0x10]; + CShip* ship; +}; + +namespace Universe +{ + struct IBase + { + BYTE data[0xC]; + UINT idsName; + }; + + struct ISystem + { + BYTE data[0x68]; + UINT idsName; + }; + + IBase* get_base(UINT id); + ISystem* get_system(UINT id); +} + +bool IsMPServer(); diff --git a/third_party/flplusplus/src/Freelancer.cpp b/third_party/flplusplus/src/Freelancer.cpp new file mode 100644 index 0000000..c7faf0d --- /dev/null +++ b/third_party/flplusplus/src/Freelancer.cpp @@ -0,0 +1,65 @@ +#include "Freelancer.h" +#include "Common.h" +#include + +CShip* GetShip() +{ + typedef IObjInspectImpl* GetPlayerIObjInspectImpl(); + IObjInspectImpl* playerIObjInspect = ((GetPlayerIObjInspectImpl*) OF_GET_PLAYER_INSPECT_IMPL)(); + + return !playerIObjInspect ? nullptr : playerIObjInspect->ship; +} + +std::wstring GetSystemName() +{ + UINT currentSystemId = *((PUINT) OF_CURRENT_SYSTEM_ID); + + if (!currentSystemId) + return {}; + + UINT systemIds = Universe::get_system(currentSystemId)->idsName; + + if (!systemIds) + return {}; + + WCHAR buffer[64] = { 0 }; + GetFlString(systemIds, buffer, _countof(buffer)); + + return std::wstring(buffer); +} + +std::wstring GetBaseName() +{ + UINT currentBaseId = *((PUINT) OF_CURRENT_BASE_ID); + + if (!currentBaseId) + return {}; + + UINT baseIds = Universe::get_base(currentBaseId)->idsName; + + if (!baseIds) + return {}; + + WCHAR buffer[64] = { 0 }; + GetFlString(baseIds, buffer, _countof(buffer)); + + return std::wstring(buffer); +} + +std::wstring GetShipName() +{ + UINT currentShipId = GetShipId(); + + if (!currentShipId) + return {}; + + Archetype::Ship* shiparch = Archetype::GetShip(currentShipId); + + if (!shiparch) + return {}; + + WCHAR buffer[64] = { 0 }; + GetFlString(shiparch->idsName, buffer, _countof(buffer)); + + return std::wstring(buffer); +} diff --git a/third_party/flplusplus/src/Freelancer.h b/third_party/flplusplus/src/Freelancer.h new file mode 100644 index 0000000..5d0d28d --- /dev/null +++ b/third_party/flplusplus/src/Freelancer.h @@ -0,0 +1,24 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "offsets.h" + +std::wstring GetSystemName(); +std::wstring GetBaseName(); +std::wstring GetShipName(); + +inline UINT GetFlString(UINT ids, PWCHAR buffer, UINT bufferSize) +{ + PDWORD resourceHandle = *((PDWORD*) OF_RESOURCES_HANDLE); + + typedef UINT GetFlStringFunc(PDWORD, UINT, PWCHAR, UINT); + return ((GetFlStringFunc*) OF_GET_FL_STRING)(resourceHandle, ids, buffer, bufferSize); +} + +// It is recommended to call this function rather than getting the CURRENT_SHIP_ID directly. +// This is because Console hooks this function to make it so that the player has no ship sometimes. +inline UINT GetShipId() +{ + typedef UINT GetShipIdFunc(); + return ((GetShipIdFunc*) OF_GET_SHIP_ID)(); +} diff --git a/third_party/flplusplus/src/codec.cpp b/third_party/flplusplus/src/codec.cpp new file mode 100644 index 0000000..dbe40a2 --- /dev/null +++ b/third_party/flplusplus/src/codec.cpp @@ -0,0 +1,25 @@ +#include "codec.h" +#include "offsets.h" +#include "patch.h" +#include "config.h" + +#define WIN32_LEAN_AND_MEAN +#include + +void codec::init() +{ + // adoxa created a plugin called MP3 Codec Fix which fixes the "missing MP3 codec" spew warning the right way. + // However, supposedly this fix doesn't work on Wine (probably due to the missing Fraunhofer codec), despite the audio working just fine. + // Hence we apply patches to wipe out the MP3 codec spew warnings on Wine only. + if (!config::is_wine()) + return; + + //Patch out MP3 warnings + auto soundManager = (DWORD) GetModuleHandleA("soundmanager.dll"); + if (soundManager) + patch::patch_uint8(soundManager + F_OF_SOUNDMAN_MP3, 0xC3); + + auto soundStreamer = (DWORD) GetModuleHandleA("soundstreamer.dll"); + if (soundStreamer) + patch::patch_uint8(soundStreamer + F_OF_SOUNDSTR_MP3, 0xC3); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/codec.h b/third_party/flplusplus/src/codec.h new file mode 100644 index 0000000..fab50e1 --- /dev/null +++ b/third_party/flplusplus/src/codec.h @@ -0,0 +1,4 @@ +#pragma once +namespace codec { + void init(); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/config.h b/third_party/flplusplus/src/config.h new file mode 100644 index 0000000..2f2eadf --- /dev/null +++ b/third_party/flplusplus/src/config.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace config { + void init_defaults(); + void init_from_file(const char *filename); + void read_font_files(const char *filename); + void EnsureInitialized(); + class ConfigData + { + public: + float lodscale; + float pbubblescale; + float characterdetailscale; + float asteroiddistscale; + std::string savefoldername; + bool saveindirectory; + std::string screenshotsfoldername; + bool screenshotsindirectory; + bool altfullscreenscreenshots; + bool altwindowedscreenshots; + bool removestartlocationwarning; + bool logtoconsole; + float shippreviewscrollingspeed; + bool shippreviewscrollinginverse; + float shippreviewscrollingmindistance; + float shippreviewscrollingmaxdistance; + bool alwaysregeneraterestartfile; + int failedtoinitsavesdirids; + bool touchpadsupport; + bool confinecursor; + std::vector fontfiles{}; + }; + ConfigData& get_config(); + bool is_wine(); +} diff --git a/third_party/flplusplus/src/consolewindow.cpp b/third_party/flplusplus/src/consolewindow.cpp new file mode 100644 index 0000000..74246bd --- /dev/null +++ b/third_party/flplusplus/src/consolewindow.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include +#include + +using namespace std; + +// maximum mumber of lines the output console should have +static const WORD MAX_CONSOLE_LINES = 500; + + +void RedirectIOToConsole() +{ + int hConHandle; + long lStdHandle; + CONSOLE_SCREEN_BUFFER_INFO coninfo; + FILE *fp; + +// allocate a console for this app + AllocConsole(); + +// set the screen buffer to be big enough to let us scroll text + GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE),&coninfo); + coninfo.dwSize.Y = MAX_CONSOLE_LINES; + SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE),coninfo.dwSize); + +// redirect unbuffered STDOUT to the console + lStdHandle = (long)GetStdHandle(STD_OUTPUT_HANDLE); + hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); + + fp = _fdopen( hConHandle, "w" ); + + freopen_s(&fp, "CONOUT$", "w", stdout); + + setvbuf( stdout, NULL, _IONBF, 0 ); + +// redirect unbuffered STDIN to the console + + lStdHandle = (long)GetStdHandle(STD_INPUT_HANDLE); + hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); + + fp = _fdopen( hConHandle, "r" ); + freopen_s(&fp, "CONIN$", "w", stdin); + setvbuf( stdin, NULL, _IONBF, 0 ); + +// redirect unbuffered STDERR to the console + lStdHandle = (long)GetStdHandle(STD_ERROR_HANDLE); + hConHandle = _open_osfhandle(lStdHandle, _O_TEXT); + + fp = _fdopen( hConHandle, "w" ); + + freopen_s(&fp, "CONOUT$", "w", stderr); + + setvbuf( stderr, NULL, _IONBF, 0 ); + +// make cout, wcout, cin, wcin, wcerr, cerr, wclog and clog +// point to console as well + ios::sync_with_stdio(); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/consolewindow.h b/third_party/flplusplus/src/consolewindow.h new file mode 100644 index 0000000..2c65ef2 --- /dev/null +++ b/third_party/flplusplus/src/consolewindow.h @@ -0,0 +1,2 @@ +#pragma once +void RedirectIOToConsole(); \ No newline at end of file diff --git a/third_party/flplusplus/src/cursor.cpp b/third_party/flplusplus/src/cursor.cpp new file mode 100644 index 0000000..9a4188d --- /dev/null +++ b/third_party/flplusplus/src/cursor.cpp @@ -0,0 +1,43 @@ +#include "cursor.h" +#include "config.h" +#include "offsets.h" +#include "patch.h" + +int __fastcall UpdateMouseX(int mouseX, int increase) +{ + MOUSE_X = mouseX + increase; + if (MOUSE_X < 0) + MOUSE_X = 0; + else if (MOUSE_X > WINDOW_WIDTH - 1) + MOUSE_X = WINDOW_WIDTH - 1; + + return increase; +} + +int __fastcall UpdateMouseY(int mouseY, int increase) +{ + MOUSE_Y = mouseY + increase; + if (MOUSE_Y < 0) + MOUSE_Y = 0; + else if (MOUSE_Y > WINDOW_HEIGHT - 1) + MOUSE_Y = WINDOW_HEIGHT - 1; + + return increase; +} + +void cursor::hook_mouse_func(unsigned int address, void* func) +{ + unsigned char dummy[5]; + patch::patch_uint16(address, 0xC289); // mov edx, eax + patch::detour((unsigned char*) (address + 2), func, dummy, false); + patch::patch_uint8(address + 7, 0x90); +} + +void cursor::init() +{ + if (!config::get_config().confinecursor) + return; + + hook_mouse_func(OF_MOUSE_X_UPDATE, (void*) UpdateMouseX); + hook_mouse_func(OF_MOUSE_Y_UPDATE, (void*) UpdateMouseY); +} diff --git a/third_party/flplusplus/src/cursor.h b/third_party/flplusplus/src/cursor.h new file mode 100644 index 0000000..973b2dd --- /dev/null +++ b/third_party/flplusplus/src/cursor.h @@ -0,0 +1,6 @@ +#pragma once + +namespace cursor { + void hook_mouse_func(unsigned int address, void* func); + void init(); +} diff --git a/third_party/flplusplus/src/fontresource.cpp b/third_party/flplusplus/src/fontresource.cpp new file mode 100644 index 0000000..3ab7a8e --- /dev/null +++ b/third_party/flplusplus/src/fontresource.cpp @@ -0,0 +1,27 @@ +#include "fontresource.h" +#include "config.h" +#include "log.h" + +#include + +void fontresource::init(LPCSTR fontDirectory) +{ + // Add font resources + for (const auto &fontFile : config::get_config().fontfiles) { + char path[MAX_PATH]; + + // Create full path to font file + strcpy_s(path, sizeof(path), fontDirectory); + PathAppendA(path, fontFile.c_str()); + + if (!PathFileExists(path)) { + logger::writeformat("path to font %s does not exist (%s)", fontFile.c_str(), path); + continue; + } + + if (AddFontResourceEx(path, FR_PRIVATE, nullptr)) + logger::writeformat("successfully added font %s", fontFile.c_str()); + else + logger::writeformat("error adding font %s", fontFile.c_str()); + } +} diff --git a/third_party/flplusplus/src/fontresource.h b/third_party/flplusplus/src/fontresource.h new file mode 100644 index 0000000..ffb5d82 --- /dev/null +++ b/third_party/flplusplus/src/fontresource.h @@ -0,0 +1,8 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +namespace fontresource { + void init(LPCSTR fontDirectory); +} diff --git a/third_party/flplusplus/src/graphics.cpp b/third_party/flplusplus/src/graphics.cpp new file mode 100644 index 0000000..a1c2cd7 --- /dev/null +++ b/third_party/flplusplus/src/graphics.cpp @@ -0,0 +1,148 @@ +#include "graphics.h" +#include "patch.h" +#include "offsets.h" +#include "config.h" +#include "Common.h" + +#define WIN32_LEAN_AND_MEAN +#include + +config::ConfigData& cfg = config::get_config(); + +#define MIN_DETAIL_SCALE 1.0f +#define MAX_DETAIL_SCALE 1000000.0f +#define MAX_ASTEROID_DIST_SCALE 10.0f + +float __fastcall multiply_lodranges_float(INI_Reader* reader, PVOID _edx, UINT index) +{ + return reader->get_value_float(index) * cfg.lodscale; +} + +float __fastcall multiply_pbubble_float(INI_Reader* reader, PVOID _edx, UINT index) +{ + return reader->get_value_float(index) * cfg.pbubblescale; +} + +float __fastcall multiply_characterdetail_float(INI_Reader* reader, PVOID _edx, UINT index) +{ + return reader->get_value_float(index) * cfg.characterdetailscale; +} + +float __fastcall multiply_asteroiddist_float(INI_Reader* reader, PVOID _edx, UINT index) +{ + return reader->get_value_float(index) * cfg.asteroiddistscale; +} + +bool patch_lodranges() +{ + if(cfg.lodscale <= MIN_DETAIL_SCALE) + return false; + else if (cfg.lodscale > MAX_DETAIL_SCALE) + cfg.lodscale = MAX_DETAIL_SCALE; + + //distances + patch::set_execute_read_write(OF_REN_DIST0, sizeof(float)); + *((float*)OF_REN_DIST0) *= cfg.lodscale; + + static UINT multiplyLodsPtr = (UINT) &multiply_lodranges_float; + patch::patch_uint32(OF_LODS_GET_VALUE, (UINT) &multiplyLodsPtr); + + return true; +} + +bool patch_pbubble() +{ + if (cfg.pbubblescale <= MIN_DETAIL_SCALE) + return false; + else if (cfg.pbubblescale > MAX_DETAIL_SCALE) + cfg.pbubblescale = MAX_DETAIL_SCALE; + + static UINT multiplyPbubblePtr = (UINT) &multiply_pbubble_float; + + patch::patch_uint32(OF_PBUBBLE_GET_VALUE0, (UINT) &multiplyPbubblePtr); + patch::patch_uint32(OF_PBUBBLE_GET_VALUE1, (UINT) &multiplyPbubblePtr); + + patch::set_execute_read_write(OF_REN_DIST1, sizeof(float)); + float ren_dist1 = *((float*)OF_REN_DIST1); + + if (cfg.pbubblescale > 2.0f) + ren_dist1 += (ren_dist1 / 10.0f) * (cfg.pbubblescale - 2.0f); + + // 40000.0f is considered to be the maximum "safe" value + if (ren_dist1 > 40000.0f) + ren_dist1 = 40000.0f; + + *((float*)OF_REN_DIST1) = ren_dist1; + + return true; +} + +bool patch_characterdetail() +{ + if (cfg.characterdetailscale <= MIN_DETAIL_SCALE) + return false; + else if (cfg.characterdetailscale > MAX_DETAIL_SCALE) + cfg.characterdetailscale = MAX_DETAIL_SCALE; + + auto common = (DWORD) GetModuleHandleA("common.dll"); + if (!common) + common = (DWORD) LoadLibraryA("common.dll"); + if (!common) + return false; + + UINT multiplyCharacterDetailPtr = (UINT) &multiply_characterdetail_float; + UINT detailSwitchAddr = common + F_OF_BODYPART_DETAILSWITCH_GET_VALUE; + + patch::patch_uint32(detailSwitchAddr, multiplyCharacterDetailPtr - detailSwitchAddr - 4); + + return true; +} + +bool patch_asteroiddist() +{ + if (cfg.asteroiddistscale <= MIN_DETAIL_SCALE) + return false; + else if (cfg.asteroiddistscale > MAX_ASTEROID_DIST_SCALE) + cfg.asteroiddistscale = MAX_ASTEROID_DIST_SCALE; + + static UINT multiplyAsteroidDistPtr = (UINT) &multiply_asteroiddist_float; + patch::patch_uint32(OF_ASTEROID_DIST_GET_VALUE, (UINT) &multiplyAsteroidDistPtr); + patch::patch_uint32(OF_AST_BILLBOARD_DIST_GET_VALUE, (UINT)&multiplyAsteroidDistPtr); + + return true; +} + +bool graphics::init_base_fixes() +{ + auto common = (DWORD) GetModuleHandleA("common.dll"); + if (!common) + common = (DWORD) LoadLibraryA("common.dll"); + if (!common) + return false; + + patch::patch_uint8(OF_VIDEODIALOG, 0x33); //disable unsupported video dialog + patch::patch_uint16(OF_MAXTEXSIZE, 0x2000); //texture size bug fix + //replace "Vibrocentric" string + //FL tries to load this font over Agency FB, screws up UI if it finds it + //if you have a font named '\b' you have big problems + const char *garbageFont = "\b\0"; + unsigned int address = common + F_OF_VIBROCENTRICFONT_V11; + patch::patch_bytes(address, (void*)garbageFont, 2); + return true; +} + +bool graphics::init_detail_scaling() +{ + patch_lodranges(); + patch_pbubble(); + if (!patch_characterdetail()) + return false; + patch_asteroiddist(); + return true; +} + +void graphics::init() +{ + init_base_fixes(); + init_detail_scaling(); +} diff --git a/third_party/flplusplus/src/graphics.h b/third_party/flplusplus/src/graphics.h new file mode 100644 index 0000000..8f6a8de --- /dev/null +++ b/third_party/flplusplus/src/graphics.h @@ -0,0 +1,6 @@ +#pragma once +namespace graphics { + void init(); + bool init_base_fixes(); + bool init_detail_scaling(); +} diff --git a/third_party/flplusplus/src/jumptable.h b/third_party/flplusplus/src/jumptable.h new file mode 100644 index 0000000..c89e38a --- /dev/null +++ b/third_party/flplusplus/src/jumptable.h @@ -0,0 +1,29 @@ +#ifndef _JUMPTABLE_H_ +#define _JUMPTABLE_H_ +#define WIN32_LEAN_AND_MEAN +#include +// Macro magic to produce a compact set of functions to +// manually import functions +#ifdef _MSC_VER +#define JUMPTABLE( def, table, index ) __declspec(naked) def { __asm jmp table[index*8+4] } +#else +#define JUMPTABLE( def, table, index ) __attribute__((naked)) def { \ +__asm(".intel_syntax noprefix\n"); \ +__asm__("jmp _" #table "[" #index "*8+4]\n"); \ +__asm__(".att_syntax"); \ +} +#endif +#define FUNC_INITIALIZER(f) \ + static void f(void); \ + struct f##_t_ { f##_t_(void) { f(); } }; static f##_t_ f##_; \ + static void f(void) +#define JUMPTABLE_INIT( dll, table ) \ +FUNC_INITIALIZER( table ## _Load) \ +{ \ + HMODULE library = LoadLibraryA(dll); \ + for(int i = 0; i < (sizeof(table) / sizeof(const char*)); i+=2) \ + { \ + table[i + 1] = (const char*)GetProcAddress(library, table[i]); \ + } \ +} +#endif diff --git a/third_party/flplusplus/src/log.cpp b/third_party/flplusplus/src/log.cpp new file mode 100644 index 0000000..8036179 --- /dev/null +++ b/third_party/flplusplus/src/log.cpp @@ -0,0 +1,137 @@ +#include "log.h" +#include "config.h" +#include "patch.h" +#include "offsets.h" +#define WIN32_LEAN_AND_MEAN +#define _CRT_SECURE_NO_WARNINGS +#include +#include +#include +#include + +static bool linked = false; +static bool fdump_patched = false; +static bool timestamps_enabled = false; +static bool console_enabled = false; + +typedef int (*pFDUMP)(DWORD, const char *, ...); +static pFDUMP *FDUMP = nullptr; +static pFDUMP fdump_original = nullptr; + +static bool do_linking() +{ + if (linked) + return FDUMP != nullptr; + + linked = true; + + HMODULE dacom = GetModuleHandleA("dacom.dll"); + if (!dacom) + dacom = LoadLibraryA("dacom.dll"); + + if (!dacom) + return false; + + FDUMP = (pFDUMP*)GetProcAddress(dacom, "FDUMP"); + return FDUMP != nullptr; +} + +void logger::writeline(const char *line) +{ +#define ERRORCODE_NOTICE 0x100003 + if (do_linking() && *FDUMP) + (*FDUMP)(ERRORCODE_NOTICE, "%s", line); +} + +static DWORD fdump_rem(DWORD errorCode, const char *fmt, ...) +{ + char buffer[4096]; + va_list args; + va_start(args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end(args); + std::time_t rawtime; + std::tm* timeinfo; + char timestamp[100]; + std::time(&rawtime); + timeinfo = std::localtime(&rawtime); + std::strftime(timestamp, 80, "%Y-%m-%d %H:%M:%S", timeinfo); + if(console_enabled) { + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + + auto severity = (BYTE) (errorCode); + + if (severity <= 1) + SetConsoleTextAttribute(hConsole, FOREGROUND_RED); + else if (severity <= 2) + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN); + else + SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE); + + if (timestamps_enabled) + printf("[%s] %s\n", timestamp, buffer); + else + printf("%s\n", buffer); + } + + if (timestamps_enabled) + return fdump_original(errorCode, "[%s] %s", timestamp, buffer); + + return fdump_original(errorCode, "%s", buffer); +} + +bool logger::enable_fdump(bool timestamps, bool console) +{ + timestamps_enabled = timestamps_enabled || timestamps; + console_enabled = console_enabled || console; + + if (fdump_patched) + return true; + + if (!do_linking() || !FDUMP || !*FDUMP) + return false; + + fdump_original = *FDUMP; + *FDUMP = (pFDUMP)fdump_rem; + fdump_patched = true; + return true; +} + +// FLServer has hard coded calls to a function called ServerLogF, +// which prints non-timestamped messages, +// so patch every call to show timestamped messages instead. +bool logger::patch_serverlogf() +{ + if (!enable_fdump(true, false)) + return false; + +#define FLSERVER_BASE (0x400000) + + // File offsets of ServerLogF calls + DWORD serverLogCalls[] = { + 0xB152, 0xB18F, 0xB1CC, 0xB235, 0xB26D, 0xBCE4, + 0xBFD6, 0xCD03, 0x1398D, 0x13B1F, 0x13BA0 + }; + + // Hook all instances where ServerLogF is called + unsigned char originalData[5]; + for (const DWORD serverLogCall : serverLogCalls) { + auto *originalFunc = (unsigned char *)(serverLogCall + FLSERVER_BASE); + patch::detour(originalFunc, (void*) fdump_rem, originalData, false); + } + + // Sets the server log function in remoteclient.dll + // Never seen it being used but overwrite the function just in case + patch::patch_uint32(OF_SERVER_LOG_FUNCTION_REF, (UINT) &fdump_rem); + return true; +} + +void logger::writeformat(const char *fmt, ...) +{ + char buffer[4096]; + va_list args; + va_start (args, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, args); + va_end (args); + logger::writeline(buffer); +} diff --git a/third_party/flplusplus/src/log.h b/third_party/flplusplus/src/log.h new file mode 100644 index 0000000..9b54f7e --- /dev/null +++ b/third_party/flplusplus/src/log.h @@ -0,0 +1,8 @@ +#pragma once + +namespace logger { + void writeline(const char *line); + void writeformat(const char *fmt, ...); + bool enable_fdump(bool timestamps, bool console); + bool patch_serverlogf(); +} diff --git a/third_party/flplusplus/src/offsets.h b/third_party/flplusplus/src/offsets.h new file mode 100644 index 0000000..7abfea0 --- /dev/null +++ b/third_party/flplusplus/src/offsets.h @@ -0,0 +1,73 @@ +#pragma once + +#define OF_REN_DIST0 (0x613EC8) +#define OF_REN_DIST1 (0x5C8910) + +#define OF_MAXTEXSIZE (0x41AD6F) + +#define OF_VIDEODIALOG (0x5B16FC) + +#define OF_PRINTSCREEN (0x425170) + +#define F_OF_SOUNDMAN_MP3 (0x8660) +#define F_OF_SOUNDSTR_MP3 (0x1000) + +#define OF_STARTLOCATION (0x43B348) + +#define F_OF_VIBROCENTRICFONT_V11 (0x143DC0) + +#define OF_LODS_GET_VALUE (0x402385) + +#define OF_PBUBBLE_GET_VALUE0 (0x4FD82C) +#define OF_PBUBBLE_GET_VALUE1 (0x4FD839) + +#define OF_ASTEROID_DIST_GET_VALUE (0x520B26) +#define OF_AST_BILLBOARD_DIST_GET_VALUE (0x54DD32) + +#define F_OF_BODYPART_DETAILSWITCH_GET_VALUE (0x9EFB2) + +#define OF_SHIP_PREVIEW_WINDOW_SCROLL (0x5E4C14) + +#define OF_FREELANCER_HWND (0x67ECA0) +#define OF_FREELANCER_FULLSCREEN_FLAG (0x679BE5) + +#define F_OF_LOAD_SAVE_GAME_CALL (0x69012) +#define F_OF_LOAD_SAVE_GAME (0x68D50) +#define F_OF_RESTART_NAME_PTR (0x68FDC) +#define F_OF_SAVE_FILE_FMT_PTR (0x68FE8) + +#define OF_CHECK_SAVE_GAMES_CALL (0x573BA7) +#define OF_CHECK_SAVE_GAMES (0x5A8840) +#define OF_SAVE_GAME_FAILED_ERROR_IDS (0x5A88C2) + +#define OF_GET_PLAYER_INSPECT_IMPL (0x54BAF0) +#define OF_GET_SHIP_ID (0x4C3E10) + +#define OF_GET_FL_STRING (0x4347E0) +#define OF_RESOURCES_HANDLE (0x4347E0) + +#define OF_CURRENT_SYSTEM_ID (0x673354) +#define OF_CURRENT_BASE_ID (0x673358) +#define OF_CURRENT_SHIP_ID (0x67337C) + +#define OF_GAME_STARTED (0x67A7A4) + +#define OF_INIT_MAIN_MENU (0x5B2BD2) + +#define F_OF_DACOM_VERSION_MS_OFFSET (0x281D + 0xC00) + +#define OF_TOUCHPAD_FIX (0x41FE8F) + +#define CONNECT_TO_SERVER_FUNC (0x5AC710) +#define CONNECT_TO_SERVER_THIS (0x67E7B8) + +#define OF_SERVER_LOG_FUNCTION_REF (0x40BDA2 + 1) + +#define OF_MOUSE_Y_UPDATE (0x41FE78) +#define OF_MOUSE_X_UPDATE (0x41FE14) + +#define MOUSE_X (*(int*) 0x616840) +#define MOUSE_Y (*(int*) 0x616844) + +#define WINDOW_WIDTH (*(int*) 0x679BC8) +#define WINDOW_HEIGHT (*(int*) 0x679BCC) diff --git a/third_party/flplusplus/src/patch.cpp b/third_party/flplusplus/src/patch.cpp new file mode 100644 index 0000000..127d164 --- /dev/null +++ b/third_party/flplusplus/src/patch.cpp @@ -0,0 +1,42 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +namespace patch { + +void detour(unsigned char* pOFunc, void* pHkFunc, unsigned char* originalData, bool jmp) +{ + DWORD dwOldProtection = 0; // Create a DWORD for VirtualProtect calls to allow us to write. + BYTE bPatch[5]; // We need to change 5 bytes and I'm going to use memcpy so this is the simplest way. + bPatch[0] = jmp ? 0xE9 : 0xE8; // Set the first byte of the byte array to the op code for the JMP or CALL instruction. + VirtualProtect((void*)pOFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtection); // Allow us to write to the memory we need to change + DWORD dwRelativeAddress = (DWORD)pHkFunc - (DWORD)pOFunc - 5; // Calculate the relative JMP address. + memcpy(&bPatch[1], &dwRelativeAddress, 4); // Copy the relative address to the byte array. + memcpy(originalData, pOFunc, 5); + memcpy(pOFunc, bPatch, 5); // Change the first 5 bytes to the JMP instruction. + VirtualProtect((void*)pOFunc, 5, dwOldProtection, &dwOldProtection); // Set the protection back to what it was. +} +void undetour(unsigned char* pOFunc, unsigned char* originalData) +{ + DWORD dwOldProtection = 0; // Create a DWORD for VirtualProtect calls to allow us to write. + VirtualProtect((void*)pOFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtection); // Allow us to write to the memory we need to change + memcpy(pOFunc, originalData, 5); + VirtualProtect((void*)pOFunc, 5, dwOldProtection, &dwOldProtection); // Set the protection back to what it was. +} + +void patch_bytes(unsigned int address, void* pData, unsigned int pSize) +{ + DWORD dwOldProtection = 0; + VirtualProtect((void*)address, pSize, PAGE_READWRITE, &dwOldProtection); + memcpy((void*)address, pData, pSize); + VirtualProtect((void*)address, pSize, dwOldProtection, &dwOldProtection); +} + +void set_execute_read_write(unsigned int address, unsigned int size) +{ + DWORD dummy; + VirtualProtect((void*)address, size, PAGE_EXECUTE_READWRITE, &dummy); +} + +} diff --git a/third_party/flplusplus/src/patch.h b/third_party/flplusplus/src/patch.h new file mode 100644 index 0000000..20daa97 --- /dev/null +++ b/third_party/flplusplus/src/patch.h @@ -0,0 +1,32 @@ +#pragma once + +namespace patch { + +void detour(unsigned char* pOFunc, void* pHkFunc, unsigned char* originalData, bool jmp = true); +void undetour(unsigned char* pOFunc, unsigned char* originalData); +void patch_bytes(unsigned int address, void* pData, unsigned int pSize); +void set_execute_read_write(unsigned int address, unsigned int size); + +inline void patch_uint32(unsigned int address, unsigned int data) +{ + patch_bytes(address, (void*)&data, 4); +} +inline void patch_uint16(unsigned int address, unsigned short data) +{ + patch_bytes(address, (void*)&data, 2); +} +inline void patch_uint8(unsigned int address, unsigned char data) +{ + patch_bytes(address, (void*)&data, 1); +} +inline void patch_float(unsigned int address, float data) +{ + patch_bytes(address, (void*)&data, 4); +} +inline void patch_x3(unsigned int address, unsigned char a, unsigned char b, unsigned char c) +{ + unsigned char bytes[] = { a, b, c }; + patch_bytes(address, (void*)bytes, 3); +} +} + diff --git a/third_party/flplusplus/src/restart.cpp b/third_party/flplusplus/src/restart.cpp new file mode 100644 index 0000000..7a25b15 --- /dev/null +++ b/third_party/flplusplus/src/restart.cpp @@ -0,0 +1,48 @@ +#include "restart.h" +#include "offsets.h" +#include "config.h" +#include "patch.h" +#define WIN32_LEAN_AND_MEAN +#include +#include + +DWORD loadSaveGameFuncAddr = 0; +char restartFileName[64]; + +bool __fastcall LoadSaveGame_Hook(PVOID thisptr, PVOID _edx, LPCSTR path, LPCSTR fileName) +{ + if (_stricmp(fileName, restartFileName) == 0) + return false; + + typedef bool __fastcall LoadSaveGame(PVOID, PVOID, LPCSTR, LPCSTR); + return ((LoadSaveGame*) loadSaveGameFuncAddr)(thisptr, _edx, path, fileName); +} + +// Prevents crashes when FL loads a malformed (e.g. from another mod) Restart.fl file. +// This code makes it so that Restart.fl is recreated on every restart. +bool restart::init() +{ + if (!config::get_config().alwaysregeneraterestartfile) + return true; + + auto server = (DWORD) GetModuleHandleA("Server.dll"); + if (!server) + server = (DWORD) LoadLibraryA("Server.dll"); + if (!server) + return false; + + UINT loadSaveGameHookPtr = (UINT) &LoadSaveGame_Hook; + UINT loadSaveGameCallAddr = server + F_OF_LOAD_SAVE_GAME_CALL; + loadSaveGameFuncAddr = server + F_OF_LOAD_SAVE_GAME; + + patch::set_execute_read_write(server + F_OF_RESTART_NAME_PTR, 4); + patch::set_execute_read_write(server + F_OF_SAVE_FILE_FMT_PTR, 4); + + // Dynamically obtain the name of the restart file + LPCSTR restartName = *((LPCSTR*) (server + F_OF_RESTART_NAME_PTR)); + LPCSTR saveFileFmt = *((LPCSTR*) (server + F_OF_SAVE_FILE_FMT_PTR)); + sprintf_s(restartFileName, sizeof(restartFileName), saveFileFmt, restartName); + + patch::patch_uint32(loadSaveGameCallAddr, loadSaveGameHookPtr - loadSaveGameCallAddr - 4); + return true; +} diff --git a/third_party/flplusplus/src/restart.h b/third_party/flplusplus/src/restart.h new file mode 100644 index 0000000..60d9d0a --- /dev/null +++ b/third_party/flplusplus/src/restart.h @@ -0,0 +1,5 @@ +#pragma once + +namespace restart { + bool init(); +} diff --git a/third_party/flplusplus/src/savegame.cpp b/third_party/flplusplus/src/savegame.cpp new file mode 100644 index 0000000..41254dc --- /dev/null +++ b/third_party/flplusplus/src/savegame.cpp @@ -0,0 +1,127 @@ +#include "savegame.h" +#include "config.h" +#include "patch.h" +#include "log.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include + +void HandleUserDataPathFail(char * const outputBuffer, char * failedSavesDirectory) +{ + static bool alreadyPrinted = false; + if (!alreadyPrinted) { + logger::writeformat("flplusplus: failed to access the saves directory for reading and writing (%s). Freelancer may not be able to properly load and store save files.", failedSavesDirectory); + alreadyPrinted = true; + } + + *outputBuffer = '\0'; +} + +void WriteSaveDirSuccessMessage(const char* dir) +{ + static bool alreadyPrinted = false; + if (!alreadyPrinted) { + logger::writeformat("flplusplus: using the following saves directory: \"%s\"", dir); + alreadyPrinted = true; + } +} + +void WriteFallbackMessage() +{ + static bool alreadyPrinted = false; + if (!alreadyPrinted) { + logger::writeline("flplusplus: saveindirectory option not set but trying to access the root SAVE directory regardless (fallback)."); + alreadyPrinted = true; + } +} + +void GetSavesInDirectoryPath(char * path) +{ + GetModuleFileNameA(NULL, path, MAX_PATH); + PathRemoveFileSpecA(path); + PathAppendA(path, "..\\SAVE"); +} + +bool TryGetMyGamesPath(char * path) +{ + if (SHGetFolderPathA(NULL, CSIDL_PERSONAL | CSIDL_FLAG_CREATE, NULL, 0, path) != S_OK) { + return false; + } + + PathAppendA(path, "My Games"); + + if (_access(path, 6) != 0) { + if (_mkdir(path) != 0) { + return false; + } + } + + PathAppendA(path, config::get_config().savefoldername.c_str()); + + if (_access(path, 6) != 0) { + if (_mkdir(path) != 0) { + return false; + } + } + + return true; +} + +bool UserDataPath(char * const outputBuffer) +{ + char path[MAX_PATH]; + + if(config::get_config().saveindirectory) { + GetSavesInDirectoryPath(path); + } else { + if (!TryGetMyGamesPath(path)) { + HandleUserDataPathFail(outputBuffer, path); + + // Fallback + WriteFallbackMessage(); + GetSavesInDirectoryPath(path); + } else { + WriteSaveDirSuccessMessage(path); + strcpy_s(outputBuffer, MAX_PATH, path); + return true; + } + } + + if (_access(path, 6) != 0) { + if (_mkdir(path) != 0) { + HandleUserDataPathFail(outputBuffer, path); + return false; + } + } + + WriteSaveDirSuccessMessage(path); + strcpy_s(outputBuffer, MAX_PATH, path); + return true; +} + +bool savegame::init() +{ + HMODULE common = GetModuleHandleA("common.dll"); + if (!common) + common = LoadLibraryA("common.dll"); + if (!common) + return false; + + auto *origFunc = (unsigned char*)GetProcAddress(common, "?GetUserDataPath@@YA_NQAD@Z"); + if (!origFunc) + return false; + + unsigned char buffer[5]; + patch::detour(origFunc, (void*)UserDataPath, buffer); + return true; +} + +void savegame::get_save_folder(char *buffer) +{ + UserDataPath(buffer); +} diff --git a/third_party/flplusplus/src/savegame.h b/third_party/flplusplus/src/savegame.h new file mode 100644 index 0000000..2993c11 --- /dev/null +++ b/third_party/flplusplus/src/savegame.h @@ -0,0 +1,6 @@ +#pragma once + +namespace savegame { + bool init(); + void get_save_folder(char *buffer); +} diff --git a/third_party/flplusplus/src/screenshot.cpp b/third_party/flplusplus/src/screenshot.cpp new file mode 100644 index 0000000..0ce0fee --- /dev/null +++ b/third_party/flplusplus/src/screenshot.cpp @@ -0,0 +1,290 @@ +//Based off code by Laz + +#include "screenshot.h" +#include "patch.h" +#include "offsets.h" +#include "config.h" +#include "log.h" +#include "Freelancer.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Gdiplus; + +#define MAX_SCREENSHOT_PATH_CHECK_ATTEMPTS 20 +bool altFullscreenScreenshots = false; +bool altWindowedScreenshots = false; + +void HandleScreenShotPathFail(char * const outputBuffer, char * failedScreenshotsDirectory) +{ + logger::writeformat( + "flplusplus: failed to access the screenshots directory for reading and writing (%s). Freelancer may not be able to properly store screenshots.", + failedScreenshotsDirectory); + *outputBuffer = '\0'; +} + +void GetScInDirectoryPath(char * path) +{ + GetModuleFileNameA(NULL, path, MAX_PATH); + PathRemoveFileSpecA(path); + PathAppendA(path, "..\\SCREENSHOTS"); +} + +bool TryGetScreenshotsPath(char * path) +{ + if (SHGetFolderPathA(NULL, CSIDL_MYPICTURES | CSIDL_FLAG_CREATE, NULL, 0, path) != S_OK) { + return false; + } + + PathAppendA(path, config::get_config().screenshotsfoldername.c_str()); + + if (_access(path, 6) != 0) { + if (_mkdir(path) != 0) { + return false; + } + } + + return true; +} + +bool ScreenShotPath(char * const outputBuffer) +{ + char path[MAX_PATH]; + + if (config::get_config().screenshotsindirectory) { + GetScInDirectoryPath(path); + } else { + if (!TryGetScreenshotsPath(path)) { + HandleScreenShotPathFail(outputBuffer, path); + logger::writeline("flplusplus: screenshotsindirectory option not set but trying to access the root SCREENSHOTS directory regardless (fallback)."); + + // Fallback + GetScInDirectoryPath(path); + } else { + strcpy_s(outputBuffer, MAX_PATH, path); + return true; + } + } + + if (_access(path, 6) != 0) { + if (_mkdir(path) != 0) { + HandleScreenShotPathFail(outputBuffer, path); + return false; + } + } + + strcpy_s(outputBuffer, MAX_PATH, path); + return true; +} + +std::wstring stows(const std::string& str) +{ + return std::wstring(str.begin(), str.end()); +} + +int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) +{ + UINT num = 0; // number of image encoders + UINT size = 0; // size of the image encoder array in bytes + + ImageCodecInfo* pImageCodecInfo = nullptr; + + GetImageEncodersSize(&num, &size); + if (size == 0) + return -1; // Failure + + pImageCodecInfo = (ImageCodecInfo*)(malloc(size)); + if (pImageCodecInfo == nullptr) + return -1; // Failure + + GetImageEncoders(num, size, pImageCodecInfo); + + for (UINT j = 0; j < num; ++j) + { + if (wcscmp(pImageCodecInfo[j].MimeType, format) == 0) + { + *pClsid = pImageCodecInfo[j].Clsid; + free(pImageCodecInfo); + return j; // Success + } + } + free(pImageCodecInfo); + return -1; // Failure +} + +void GetWindowSize(HWND flHWND, int& width, int& height) +{ + RECT gameWindow; + GetClientRect(flHWND, &gameWindow); + ClientToScreen(flHWND, (LPPOINT) &gameWindow.left); + ClientToScreen(flHWND, (LPPOINT) &gameWindow.right); + + width = gameWindow.right - gameWindow.left; + height = gameWindow.bottom - gameWindow.top; +} + +std::wstring GetScreenshotOutPath(LPCWSTR directory, const std::wstring &baseFileName, int suffixIndex) +{ + std::wstring fileName = baseFileName; + if (suffixIndex > 0) { + fileName += std::wstring(L"_") + std::to_wstring(suffixIndex); + } + fileName += std::wstring(L".png"); + + WCHAR cleanedFileName[MAX_PATH]; + wcscpy_s(cleanedFileName, _countof(cleanedFileName), fileName.c_str()); + PathCleanupSpec(directory, cleanedFileName); + + return std::wstring(directory) + L'\\' + std::wstring(cleanedFileName); +} + +static DWORD OnScreenshot() +{ + char directoryA[MAX_PATH]; + + if(!ScreenShotPath(directoryA)) + { + return DWORD(-1); + } + + WCHAR directory[MAX_PATH]; + size_t charsConverted; + mbstowcs_s(&charsConverted, directory, _countof(directory), directoryA, _countof(directory) - 1); + + // TODO: Is this check volatile? + // What happens if the user turned fullscreen off via a third party app like Borderless Gaming. + // Will this be reflected in Freelancer's fullscreen flag? + // TODO: Also if fullscreen is true, it's possible that the user moved their window to a secondary monitor and then enabled fullscreen. + // Now the FL window is in fullscreen mode on the secondary monitor. + // When this happens, the code below will take a screenshot of the main monitor's display which won't show anything from the FL window. + // It should actually capture the fullscreen FL window on the secondary monitor. + + // Test if you can move fullscreen window to other monitor and still take a screenshot (windows shift left arrow). + // Replace nullptr with GetActiveWindow, or GetForegroundWindow, or GetDesktopWindow, GetWindowDC. Test with broken DxWrapper version from the old FLSR release. + bool isFullscreen = (*((PBYTE) OF_FREELANCER_FULLSCREEN_FLAG) & 1) == 1; + bool useFullscreenScreenshotCode = isFullscreen; + + if ((isFullscreen && altFullscreenScreenshots) || (!isFullscreen && altWindowedScreenshots)) + useFullscreenScreenshotCode = !useFullscreenScreenshotCode; + + HWND flHWND = useFullscreenScreenshotCode ? nullptr : *(HWND*) OF_FREELANCER_HWND; + + // get the device context of FL's window + HDC hScreenDC = GetDC(flHWND); + + // and a device context to put it in + HDC hMemoryDC = CreateCompatibleDC(hScreenDC); + + int width, height; + + if (useFullscreenScreenshotCode) + { + width = GetDeviceCaps(hScreenDC, HORZRES); + height = GetDeviceCaps(hScreenDC, VERTRES); + } + else + { + GetWindowSize(flHWND, width, height); + } + + // maybe worth checking these are positive values + HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, width, height); + + // get a new bitmap + HBITMAP hOldBitmap = HBITMAP(SelectObject(hMemoryDC, hBitmap)); + + BitBlt(hMemoryDC, 0, 0, width, height, hScreenDC, 0, 0, SRCCOPY); + hBitmap = (HBITMAP)SelectObject(hMemoryDC, hOldBitmap); + + std::time_t rawtime; + std::tm* timeinfo; + WCHAR buffer[100]; + + std::time(&rawtime); + timeinfo = std::localtime(&rawtime); + + std::wcsftime(buffer, 80, L"%Y-%m-%d_%H-%M-%S", timeinfo); + std::wstring fileName = std::wstring(buffer); + + // If the player starts a game and then goes back to the main menu, + // the current system, base, and ship are not always reset. + // Hence, this code may obtain the incorrect names if the player is in the main menu. + // Therefore, only append the names if a game has started. + if (*(bool*) OF_GAME_STARTED) + { + std::wstring names[] = { GetSystemName(), GetBaseName(), GetShipName() }; + for (const auto& name : names) + { + if (!name.empty()) + fileName += L'_' + name; + } + } + + int i = 0; + std::wstring outfile = GetScreenshotOutPath(directory, fileName, i); + + while (PathFileExistsW(outfile.c_str()) && i < MAX_SCREENSHOT_PATH_CHECK_ATTEMPTS) { + i++; + outfile = GetScreenshotOutPath(directory, fileName, i); + } + + if (i < MAX_SCREENSHOT_PATH_CHECK_ATTEMPTS) + { + GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken; + GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr); + Bitmap* image = new Bitmap(hBitmap, nullptr); + + CLSID myClsId; + GetEncoderClsid(L"image/png", &myClsId); + + Status status = image->Save(outfile.c_str(), &myClsId, nullptr); + delete image; + + GdiplusShutdown(gdiplusToken); + } + + // clean up + DeleteDC(hMemoryDC); + ReleaseDC(flHWND, hScreenDC); + + return DWORD(-1); +} + +bool screenshot::init_png() +{ + altFullscreenScreenshots = config::get_config().altfullscreenscreenshots; + altWindowedScreenshots = config::get_config().altwindowedscreenshots; + + unsigned char buffer[5]; + patch::detour((unsigned char*)OF_PRINTSCREEN, (void*)OnScreenshot, buffer); + return true; +} + +bool screenshot::init_path() +{ + HMODULE common = GetModuleHandleA("common.dll"); + if (!common) + common = LoadLibraryA("common.dll"); + if (!common) + return false; + + auto* getScreenShotPath = (unsigned char*)GetProcAddress(common, "?GetScreenShotPath@@YA_NQAD@Z"); + if (!getScreenShotPath) + return false; + + unsigned char buffer[5]; + patch::detour(getScreenShotPath, (void*)ScreenShotPath, buffer); + return true; +} diff --git a/third_party/flplusplus/src/screenshot.h b/third_party/flplusplus/src/screenshot.h new file mode 100644 index 0000000..6c3ab49 --- /dev/null +++ b/third_party/flplusplus/src/screenshot.h @@ -0,0 +1,6 @@ +#pragma once + +namespace screenshot { + bool init_png(); + bool init_path(); +} diff --git a/third_party/flplusplus/src/shippreviewscroll.cpp b/third_party/flplusplus/src/shippreviewscroll.cpp new file mode 100644 index 0000000..28be651 --- /dev/null +++ b/third_party/flplusplus/src/shippreviewscroll.cpp @@ -0,0 +1,66 @@ +#include "shippreviewscroll.h" +#include "offsets.h" +#include "patch.h" +#include "config.h" +#include + +using namespace shippreviewscroll; + +#define MIN_SCROLLING_SPEED 0.0f +#define MAX_SCROLLING_SPEED 50.0f + +#define NN_SHIPTRADER_VFTABLE_ADDR (0x5D593C) + +float scrollingSpeed; +float scrollMinDistance; +float scrollMaxDistance; + +bool __fastcall ShipPreviewWindowScroll(ShipPreviewWindow* window, PVOID _edx, int scrollValue) +{ + // The exact same ship preview element is used in other places as well, + // e.g. the FL beta inventory showed a top-down model of the player ship, but plugins can re-enable it. + // We don't want the zooming to work anywhere else other than in the ship dealer. + if (window->parent->vftable != NN_SHIPTRADER_VFTABLE_ADDR) + return false; + + window->zoomLevel += scrollingSpeed * static_cast(scrollValue); + + // Zoom levels are always negative if you "zoom away" from the ship. + // If you want to zoom "through" the ship, it becomes positive. + window->zoomLevel = std::max(-scrollMaxDistance, std::min(-scrollMinDistance, window->zoomLevel)); + + // The scroll function should just return false + return false; +} + +// TODO: Hook the "on-frame" update function and implement smooth scrolling (see Turret Zoom plugin) +// TODO: Allow the scrolling speed to be scaled based on the ship archetype (ini configurable) +// [ShipPreviewWindow* +0x32C] contains the ship archetype ID +// Alternatively, scale the speed based on the ship class. Array for 3 ships should be in [NN_ShipTrader* + 0x3EC] +// Player ship class: [NN_ShipTrader* + 0x3E8] +// Alternatively, scale by the ship's radius. +// [dalib engine + 0x88] = DALib::Engine::GetRadius(long engineIndex, BYTE unk, float* radius, Vector* vec) +// Engine index: [ShipPreviewWindow* +0x4B8] +void shippreviewscroll::init() +{ + if (config::get_config().shippreviewscrollingspeed < MIN_SCROLLING_SPEED) + scrollingSpeed = MIN_SCROLLING_SPEED; + else if (config::get_config().shippreviewscrollingspeed > MAX_SCROLLING_SPEED) + scrollingSpeed = MAX_SCROLLING_SPEED; + else + scrollingSpeed = config::get_config().shippreviewscrollingspeed; + + if (config::get_config().shippreviewscrollinginverse) + scrollingSpeed = -scrollingSpeed; + + scrollMinDistance = config::get_config().shippreviewscrollingmindistance; + scrollMaxDistance = config::get_config().shippreviewscrollingmaxdistance; + + if (scrollMaxDistance < scrollMinDistance) + scrollMaxDistance = scrollMinDistance; + + // Every window in Freelancer has a virtual "scroll" function + // In the case of the ship preview window, this function does basically nothing + // We replace the pointer to this dummy function in the ship preview window's vftable with our own + patch::patch_uint32(OF_SHIP_PREVIEW_WINDOW_SCROLL, (UINT) ShipPreviewWindowScroll); +} diff --git a/third_party/flplusplus/src/shippreviewscroll.h b/third_party/flplusplus/src/shippreviewscroll.h new file mode 100644 index 0000000..b023bda --- /dev/null +++ b/third_party/flplusplus/src/shippreviewscroll.h @@ -0,0 +1,22 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +namespace shippreviewscroll +{ + struct ShipPreviewParent + { + DWORD vftable; + }; + + struct ShipPreviewWindow + { + DWORD vftable; + ShipPreviewParent* parent; + BYTE x08[0x3E4]; + float zoomLevel; // 0x3EC + }; + + void init(); +} diff --git a/third_party/flplusplus/src/startlocation.cpp b/third_party/flplusplus/src/startlocation.cpp new file mode 100644 index 0000000..64dc54b --- /dev/null +++ b/third_party/flplusplus/src/startlocation.cpp @@ -0,0 +1,12 @@ +#include "startlocation.h" +#include "offsets.h" +#include "patch.h" +#include "config.h" + +void startlocation::init() +{ + if (config::get_config().removestartlocationwarning) { + //Patch out "Failed to get start location" warning + patch::patch_uint8(OF_STARTLOCATION, 0xEB); + } +} diff --git a/third_party/flplusplus/src/startlocation.h b/third_party/flplusplus/src/startlocation.h new file mode 100644 index 0000000..d45560a --- /dev/null +++ b/third_party/flplusplus/src/startlocation.h @@ -0,0 +1,5 @@ +#pragma once + +namespace startlocation { + void init(); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/startup.cpp b/third_party/flplusplus/src/startup.cpp new file mode 100644 index 0000000..2cca969 --- /dev/null +++ b/third_party/flplusplus/src/startup.cpp @@ -0,0 +1,28 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include "startup.h" +#include "patch.h" +#include "offsets.h" +#include "config.h" + +bool startup::check_save_games_hook() +{ + int originalIds = *((int*) OF_SAVE_GAME_FAILED_ERROR_IDS); // Save the original value + *((int*) OF_SAVE_GAME_FAILED_ERROR_IDS) = config::get_config().failedtoinitsavesdirids; // Overwrite the original value + + // Call the original function + typedef bool (check_save_games)(); + bool result = ((check_save_games*) OF_CHECK_SAVE_GAMES)(); + + *((int*) OF_SAVE_GAME_FAILED_ERROR_IDS) = originalIds; // Restore the original value + + return result; +} + +void startup::init() +{ + BYTE originalBytes[5]; + + patch::set_execute_read_write(OF_SAVE_GAME_FAILED_ERROR_IDS, sizeof(UINT)); + patch::detour((unsigned char*) OF_CHECK_SAVE_GAMES_CALL, (void*) startup::check_save_games_hook, originalBytes, false); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/startup.h b/third_party/flplusplus/src/startup.h new file mode 100644 index 0000000..a442f38 --- /dev/null +++ b/third_party/flplusplus/src/startup.h @@ -0,0 +1,4 @@ +namespace startup { + void init(); + bool check_save_games_hook(); +} \ No newline at end of file diff --git a/third_party/flplusplus/src/thnplayer.cpp b/third_party/flplusplus/src/thnplayer.cpp new file mode 100644 index 0000000..56f7755 --- /dev/null +++ b/third_party/flplusplus/src/thnplayer.cpp @@ -0,0 +1,210 @@ +#include "thnplayer.h" +#include "savegame.h" +#include "log.h" +#include "patch.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include + +char *ScriptOverride = NULL; + +static bool ParseArguments(LPWSTR* args, int numArgs, LPWSTR* ret) +{ + for(int i = 0; i < numArgs; i++) { + if(wcscmp(args[i], L"-thn") == 0) { + if(i + 1 < numArgs && PathFileExistsW(args[i + 1])) { + *ret = _wcsdup(args[i + 1]); + } + return true; + } + } + return false; +} + +const unsigned char MenuButtonsRemove[] = { + 0xE8, 0x47, 0xE5, 0xEA, 0xFF, 0x83, 0x7C, 0x24, 0x50, + 0x04, 0x77, 0x11, 0xDB, 0x05, 0x4C, 0x46, 0x57, 0x00, + 0xEB, 0x0D +}; + +typedef bool (__fastcall *pBExit)(void*); +typedef bool (__fastcall *pBEnter)(void*,int,UINT); +typedef void (__cdecl *pUpdateTime)(double delta); +typedef void(__cdecl *pFlushConsole)(int ident, char* data, DWORD len); + +void* FL = (void*)0x668708; +static pBEnter FL_BaseEnter = (pBEnter)0x43b290; +static pBExit FL_BaseExit = (pBExit)0x43b3e0; +static pUpdateTime UpdateTime; +static pFlushConsole FlushConsole = (pFlushConsole)0x46A150; + +static unsigned char thornLoadData[5]; +typedef void *(__cdecl *ScriptLoadPtr)(const char*); +static ScriptLoadPtr _ThornScriptLoad; + +static bool firstLoad = true; + +struct Chat { + UINT Type1; + UINT Len1; + UINT Mask; + UINT Data; + UINT Type2; + UINT Len2; + wchar_t Message[1024]; +}; + +static void PrintText(const wchar_t *text) +{ + Chat chat; + memset(&chat, 0, sizeof(Chat)); + chat.Type1 = 0x1; + chat.Len1 = 0x8; + chat.Mask = 0xFFFFFF00; + chat.Data = 0xFFFFFF00; + chat.Type2 = 0x2; + chat.Len2 = wcslen(text) * 2 + 2; + wcscpy(chat.Message, text); + FlushConsole(0, (char*)&chat, chat.Len2 + 24); +} + + +void * __cdecl OnThornLoad(const char *script) +{ + patch::undetour((unsigned char*)_ThornScriptLoad, thornLoadData); + logger::writeline(ScriptOverride ? (const char*)ScriptOverride : script); + void* retval = _ThornScriptLoad(ScriptOverride ? (const char*)ScriptOverride : script); + patch::detour((unsigned char*)_ThornScriptLoad, (void*)OnThornLoad, thornLoadData); + if(firstLoad) { + PrintText(L"THN Player"); + PrintText(L"F5 - Refresh"); + PrintText(L"F9 - Open"); + PrintText(L"Esc - Exit"); + firstLoad = false; + } + return retval; +} + +static void SceneReload() +{ + FL_BaseExit(FL); + FL_BaseEnter(FL, 0, 0xA3BC3888); //intro1_base +} + +static int reloadFrames = -1; +static void OpenFile() +{ + OPENFILENAME ofn; // common dialog box structure + char szFile[260]; // buffer for file name + + // Initialize OPENFILENAME + ZeroMemory(&ofn, sizeof(ofn)); + ofn.lStructSize = sizeof(ofn); + ofn.hwndOwner = NULL; + ofn.lpstrFile = szFile; + // Set lpstrFile[0] to '\0' so that GetOpenFileName does not + // use the contents of szFile to initialize itself. + ofn.lpstrFile[0] = '\0'; + ofn.nMaxFile = sizeof(szFile); + ofn.lpstrFilter = "THN\0*.THN;*.LUA\0All\0*.*\0"; + ofn.nFilterIndex = 0; + ofn.lpstrFileTitle = NULL; + ofn.nMaxFileTitle = 0; + ofn.lpstrInitialDir = NULL; + ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; + + // Display the Open dialog box. + if (GetOpenFileName(&ofn)==TRUE) { + logger::writeline("Opening"); + logger::writeline(szFile); + ScriptOverride = _strdup(szFile); + reloadFrames = 4; + } +} + +unsigned char updateData[5]; + +static void Update(const double delta) +{ + if((GetAsyncKeyState(VK_F5) & 1)) { + SceneReload(); + } + if((GetAsyncKeyState(VK_F9) & 1)) { + OpenFile(); + } + // Skip hitching + if(reloadFrames > 0) { + reloadFrames--; + } else if(reloadFrames == 0) { + reloadFrames = -1; + SceneReload(); + } + patch::undetour((unsigned char*)UpdateTime, updateData); + UpdateTime(delta); + patch::detour((unsigned char*)UpdateTime, (void*)Update, updateData); +} + +bool thnplayer::init() +{ + int numArgs; + LPWSTR *args = CommandLineToArgvW(GetCommandLineW(), &numArgs); + if (!args) + return false; + + LPWSTR thnscript = NULL; + if(ParseArguments(args, numArgs, &thnscript)) { + HMODULE freelancer = GetModuleHandleA("freelancer.exe"); + HMODULE common = GetModuleHandleA("common.dll"); + if (!common) + common = LoadLibraryA("common.dll"); + + pUpdateTime updateTime = common + ? (pUpdateTime)GetProcAddress(common, "?UpdateGlobalTime@Timing@@YAXN@Z") + : nullptr; + ScriptLoadPtr thornScriptLoad = common + ? (ScriptLoadPtr)GetProcAddress(common, "?ThornScriptLoad@@YAPAUIScriptEngine@@PBD@Z") + : nullptr; + + if (!freelancer || !updateTime || !thornScriptLoad) { + free((void*)thnscript); + LocalFree(args); + return false; + } + + if(thnscript) { + char buffer[500]; + wcstombs(buffer, thnscript, 500); + free((void*)thnscript); + ScriptOverride = _strdup(buffer); + } + DWORD FL = (DWORD)freelancer; + //Remove all menu buttons + patch::patch_bytes(FL + 0x174634, (void*)MenuButtonsRemove, 20); + patch::patch_uint16(FL + 0x1746CA, 0x0); + patch::patch_uint16(FL + 0x174707, 0x0); + patch::patch_uint16(FL + 0x174744, 0x0); + patch::patch_uint16(FL + 0x174781, 0x0); + patch::patch_uint16(FL + 0x1747BE, 0x0); + patch::patch_bytes(FL + 0x1E23DC, (void*)"null", 5); //Disable ui_motion_swish + //Remove version text + patch::patch_uint32(FL + 0x16DDEC, 0x1); + patch::patch_uint32(FL + 0x174890, 0x1); + //Remove logo + patch::patch_uint32(FL + 0x1E266C, 0x0); + //Persistent text + patch::patch_uint32(FL + 0x0691CA, 0x7FFFFFFE); + //Hook update + UpdateTime = updateTime; + patch::detour((unsigned char*)UpdateTime, (void*)Update, updateData); + //Permanent hook load function + _ThornScriptLoad = thornScriptLoad; + patch::detour((unsigned char*)_ThornScriptLoad, (void*)OnThornLoad, thornLoadData); + } + LocalFree(args); + return true; +} diff --git a/third_party/flplusplus/src/thnplayer.h b/third_party/flplusplus/src/thnplayer.h new file mode 100644 index 0000000..5f6dea2 --- /dev/null +++ b/third_party/flplusplus/src/thnplayer.h @@ -0,0 +1,5 @@ +#pragma once +namespace thnplayer { + bool init(); +} + diff --git a/third_party/flplusplus/src/touchpad.cpp b/third_party/flplusplus/src/touchpad.cpp new file mode 100644 index 0000000..2305de7 --- /dev/null +++ b/third_party/flplusplus/src/touchpad.cpp @@ -0,0 +1,18 @@ +#include "touchpad.h" +#include "offsets.h" +#include "patch.h" +#include "config.h" + +// TODO: There's a bug in FL where if you play the game on a system that has a touchpad (laptop for instance), +// then scrolling will not work at all. If you use that touchpad to scroll, then it appears that you scroll infinitely. +// When this happens, toggling engine kill doesn't seem to work anymore either. +// This function applies a patch that makes scrolling with a touchpad behave as you'd expect. +// However, it completely breaks normal mouse wheel scrolling. +// It'd be nice if a solution could be implemented that fixes touchpad scrolling without breaking mouse wheel scrolling. +void touchpad::init() +{ + if (config::get_config().touchpadsupport) { + // Fix touchpad scrolling but break normal mouse wheel scrolling + patch::patch_uint8(OF_TOUCHPAD_FIX, 0x00); + } +} diff --git a/third_party/flplusplus/src/touchpad.h b/third_party/flplusplus/src/touchpad.h new file mode 100644 index 0000000..bef51b5 --- /dev/null +++ b/third_party/flplusplus/src/touchpad.h @@ -0,0 +1,5 @@ +#pragma once + +namespace touchpad { + void init(); +} \ No newline at end of file diff --git a/third_party/flsharp/def/Common.def b/third_party/flsharp/def/Common.def new file mode 100644 index 0000000..31af754 --- /dev/null +++ b/third_party/flsharp/def/Common.def @@ -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 diff --git a/third_party/flsharp/def/DALib.def b/third_party/flsharp/def/DALib.def new file mode 100644 index 0000000..f1bf8c2 --- /dev/null +++ b/third_party/flsharp/def/DALib.def @@ -0,0 +1,2 @@ +EXPORTS +?Shutdown@CGunWrapper@@SAXXZ diff --git a/third_party/flsharp/def/Dacom.def b/third_party/flsharp/def/Dacom.def new file mode 100644 index 0000000..99bbf10 --- /dev/null +++ b/third_party/flsharp/def/Dacom.def @@ -0,0 +1,3 @@ +EXPORTS +DACOM_GetDllVersion +FDUMP diff --git a/third_party/flsharp/include/Common.h b/third_party/flsharp/include/Common.h new file mode 100644 index 0000000..6b57f9f --- /dev/null +++ b/third_party/flsharp/include/Common.h @@ -0,0 +1,386 @@ +#pragma once + +#include "fl_math.h" +#include "vftable.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#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(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& colGroupList) const; + bool get_undamaged_collision_group_list_Hook(std::list& 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 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); +}; + + diff --git a/third_party/flsharp/include/DALib.h b/third_party/flsharp/include/DALib.h new file mode 100644 index 0000000..6fe34a6 --- /dev/null +++ b/third_party/flsharp/include/DALib.h @@ -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) diff --git a/third_party/flsharp/include/Dacom.h b/third_party/flsharp/include/Dacom.h new file mode 100644 index 0000000..b6e7cc3 --- /dev/null +++ b/third_party/flsharp/include/Dacom.h @@ -0,0 +1,23 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#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); +} diff --git a/third_party/flsharp/include/Freelancer.h b/third_party/flsharp/include/Freelancer.h new file mode 100644 index 0000000..6fa3fd4 --- /dev/null +++ b/third_party/flsharp/include/Freelancer.h @@ -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(); diff --git a/third_party/flsharp/include/RemoteServer.h b/third_party/flsharp/include/RemoteServer.h new file mode 100644 index 0000000..48c54f7 --- /dev/null +++ b/third_party/flsharp/include/RemoteServer.h @@ -0,0 +1,31 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#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); +}; diff --git a/third_party/flsharp/include/alchemy_crash.h b/third_party/flsharp/include/alchemy_crash.h new file mode 100644 index 0000000..5223dca --- /dev/null +++ b/third_party/flsharp/include/alchemy_crash.h @@ -0,0 +1,15 @@ +#pragma once + +struct Alchemy +{ + float progress; + void* effect; +}; + +struct AleLoop +{ + int startOffset; + unsigned char maxProgressOffset; +}; + +void InitAlchemyCrashFix(); diff --git a/third_party/flsharp/include/base_info.h b/third_party/flsharp/include/base_info.h new file mode 100644 index 0000000..04e10a2 --- /dev/null +++ b/third_party/flsharp/include/base_info.h @@ -0,0 +1,10 @@ +#define WIN32_LEAN_AND_MEAN +#include + +struct BaseInfoCat +{ + DWORD headerStyleAddr; + DWORD headerNamePrintAddr; +}; + +void InitBaseInfoSpacingFix(); diff --git a/third_party/flsharp/include/blank_faction.h b/third_party/flsharp/include/blank_faction.h new file mode 100644 index 0000000..7443a91 --- /dev/null +++ b/third_party/flsharp/include/blank_faction.h @@ -0,0 +1,3 @@ +#pragma once + +void InitBlankFactionNameFix(); diff --git a/third_party/flsharp/include/cheat_detection.h b/third_party/flsharp/include/cheat_detection.h new file mode 100644 index 0000000..6aba7ea --- /dev/null +++ b/third_party/flsharp/include/cheat_detection.h @@ -0,0 +1,72 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#ifdef USE_ST6 +#include "st6.h" +#else +#include +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 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(); diff --git a/third_party/flsharp/include/config_reader.h b/third_party/flsharp/include/config_reader.h new file mode 100644 index 0000000..8240436 --- /dev/null +++ b/third_party/flsharp/include/config_reader.h @@ -0,0 +1,7 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include "feature_config.h" + +void ReadConfig(LPCSTR path, FeatureManager &manager); diff --git a/third_party/flsharp/include/copy_paste.h b/third_party/flsharp/include/copy_paste.h new file mode 100644 index 0000000..a16fb08 --- /dev/null +++ b/third_party/flsharp/include/copy_paste.h @@ -0,0 +1,72 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include "vftable.h" +#include "Common.h" + +#ifdef USE_ST6 +#include "st6.h" +#else +#include +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 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(); diff --git a/third_party/flsharp/include/cursor_colors.h b/third_party/flsharp/include/cursor_colors.h new file mode 100644 index 0000000..35579e9 --- /dev/null +++ b/third_party/flsharp/include/cursor_colors.h @@ -0,0 +1,36 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#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(); diff --git a/third_party/flsharp/include/dealer_fixes.h b/third_party/flsharp/include/dealer_fixes.h new file mode 100644 index 0000000..d2a291c --- /dev/null +++ b/third_party/flsharp/include/dealer_fixes.h @@ -0,0 +1,36 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#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(); diff --git a/third_party/flsharp/include/dll_crash.h b/third_party/flsharp/include/dll_crash.h new file mode 100644 index 0000000..b8a1148 --- /dev/null +++ b/third_party/flsharp/include/dll_crash.h @@ -0,0 +1,3 @@ +#pragma once + +void InitMissingDllCrashFix(); diff --git a/third_party/flsharp/include/exit.h b/third_party/flsharp/include/exit.h new file mode 100644 index 0000000..062ed9f --- /dev/null +++ b/third_party/flsharp/include/exit.h @@ -0,0 +1,6 @@ +#pragma once + +void InitPostGameDeadlockFix(); + +void InitQuitMessageFix(); +void CleanupQuitMessageFix(); diff --git a/third_party/flsharp/include/feature_config.h b/third_party/flsharp/include/feature_config.h new file mode 100644 index 0000000..19a082c --- /dev/null +++ b/third_party/flsharp/include/feature_config.h @@ -0,0 +1,30 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +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 features; +}; + +bool ApplyAlways(); +bool ApplyOnlyOnClient(); +bool ApplyOnlyOnServer(); diff --git a/third_party/flsharp/include/fl_func.h b/third_party/flsharp/include/fl_func.h new file mode 100644 index 0000000..4699516 --- /dev/null +++ b/third_party/flsharp/include/fl_func.h @@ -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 \ + } diff --git a/third_party/flsharp/include/fl_math.h b/third_party/flsharp/include/fl_math.h new file mode 100644 index 0000000..0c78138 --- /dev/null +++ b/third_party/flsharp/include/fl_math.h @@ -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 diff --git a/third_party/flsharp/include/flash_particles.h b/third_party/flsharp/include/flash_particles.h new file mode 100644 index 0000000..07227ff --- /dev/null +++ b/third_party/flsharp/include/flash_particles.h @@ -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)()); +}; diff --git a/third_party/flsharp/include/group_members.h b/third_party/flsharp/include/group_members.h new file mode 100644 index 0000000..d7a1864 --- /dev/null +++ b/third_party/flsharp/include/group_members.h @@ -0,0 +1,15 @@ +#define WIN32_LEAN_AND_MEAN +#include + +enum AttitudeType : int +{ + Hostile = -1, + Neutral = 0, + Friendly = 1 +}; + +void InitHostileGroupFormation(); + +void InitHostileGroupMembersFix(); + +void InitGroupMemberAttitudeFix(); diff --git a/third_party/flsharp/include/infocards.h b/third_party/flsharp/include/infocards.h new file mode 100644 index 0000000..469ef02 --- /dev/null +++ b/third_party/flsharp/include/infocards.h @@ -0,0 +1,14 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include + +struct InfocardEntry +{ + std::map& map; + LPCSTR key; + LPCSTR value; +}; + +void InitDynamicSolarInfocards(); diff --git a/third_party/flsharp/include/logger.h b/third_party/flsharp/include/logger.h new file mode 100644 index 0000000..14cbf5d --- /dev/null +++ b/third_party/flsharp/include/logger.h @@ -0,0 +1,13 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +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); +} diff --git a/third_party/flsharp/include/mouse.h b/third_party/flsharp/include/mouse.h new file mode 100644 index 0000000..b35f138 --- /dev/null +++ b/third_party/flsharp/include/mouse.h @@ -0,0 +1,28 @@ +#pragma once + +#include "vftable.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#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 +}; diff --git a/third_party/flsharp/include/pilot_names.h b/third_party/flsharp/include/pilot_names.h new file mode 100644 index 0000000..880a834 --- /dev/null +++ b/third_party/flsharp/include/pilot_names.h @@ -0,0 +1,17 @@ +#define WIN32_LEAN_AND_MEAN +#include + +struct StrBuffer +{ + LPWSTR str; + size_t capacity; +}; + +enum NameType : DWORD +{ + FactionAndDesignation = 0, + PilotName = 1, + Unk = 2 +}; + +void InitPilotNamesFix(); diff --git a/third_party/flsharp/include/projectiles.h b/third_party/flsharp/include/projectiles.h new file mode 100644 index 0000000..c7576d7 --- /dev/null +++ b/third_party/flsharp/include/projectiles.h @@ -0,0 +1,9 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#include "Common.h" + +void InitProjectilesSoundFix(); +void InitProjectilesServerFix(); diff --git a/third_party/flsharp/include/rep_requirements.h b/third_party/flsharp/include/rep_requirements.h new file mode 100644 index 0000000..ae02ea0 --- /dev/null +++ b/third_party/flsharp/include/rep_requirements.h @@ -0,0 +1,18 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +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); +}; diff --git a/third_party/flsharp/include/resolutions.h b/third_party/flsharp/include/resolutions.h new file mode 100644 index 0000000..255e3c7 --- /dev/null +++ b/third_party/flsharp/include/resolutions.h @@ -0,0 +1,70 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#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(); diff --git a/third_party/flsharp/include/resolutions_asm.h b/third_party/flsharp/include/resolutions_asm.h new file mode 100644 index 0000000..a628c0e --- /dev/null +++ b/third_party/flsharp/include/resolutions_asm.h @@ -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); diff --git a/third_party/flsharp/include/save_crash.h b/third_party/flsharp/include/save_crash.h new file mode 100644 index 0000000..95e7870 --- /dev/null +++ b/third_party/flsharp/include/save_crash.h @@ -0,0 +1,3 @@ +#pragma once + +void InitSaveCrashFix(); diff --git a/third_party/flsharp/include/server_filter.h b/third_party/flsharp/include/server_filter.h new file mode 100644 index 0000000..ae32973 --- /dev/null +++ b/third_party/flsharp/include/server_filter.h @@ -0,0 +1,4 @@ +#pragma once + +void InitServerFilterCrashFix(); +void InitServerFilterSpeedFix(); diff --git a/third_party/flsharp/include/shield_capacity.h b/third_party/flsharp/include/shield_capacity.h new file mode 100644 index 0000000..88af5ad --- /dev/null +++ b/third_party/flsharp/include/shield_capacity.h @@ -0,0 +1,3 @@ +#pragma once + +void InitShieldCapacityFix(); diff --git a/third_party/flsharp/include/st6.h b/third_party/flsharp/include/st6.h new file mode 100644 index 0000000..be2f296 --- /dev/null +++ b/third_party/flsharp/include/st6.h @@ -0,0 +1,190 @@ +#pragma once + +#include +#include +#include + +#ifndef _POINTER_X + #define _POINTER_X(T, A) T* +#endif +#ifndef _REFERENCE_X + #define _REFERENCE_X(T, A) T& +#endif + +namespace st6 +{ + template + 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 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 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; + }; +} diff --git a/third_party/flsharp/include/temp_fixes.h b/third_party/flsharp/include/temp_fixes.h new file mode 100644 index 0000000..ad6398c --- /dev/null +++ b/third_party/flsharp/include/temp_fixes.h @@ -0,0 +1,3 @@ +#pragma once + +void InitFlightControlsFix(); diff --git a/third_party/flsharp/include/test_sounds.h b/third_party/flsharp/include/test_sounds.h new file mode 100644 index 0000000..351bd57 --- /dev/null +++ b/third_party/flsharp/include/test_sounds.h @@ -0,0 +1,67 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#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); diff --git a/third_party/flsharp/include/trade_lane_lights.h b/third_party/flsharp/include/trade_lane_lights.h new file mode 100644 index 0000000..7c9400d --- /dev/null +++ b/third_party/flsharp/include/trade_lane_lights.h @@ -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(); diff --git a/third_party/flsharp/include/ui_anim.h b/third_party/flsharp/include/ui_anim.h new file mode 100644 index 0000000..14de393 --- /dev/null +++ b/third_party/flsharp/include/ui_anim.h @@ -0,0 +1,42 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#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(); diff --git a/third_party/flsharp/include/update.h b/third_party/flsharp/include/update.h new file mode 100644 index 0000000..c608847 --- /dev/null +++ b/third_party/flsharp/include/update.h @@ -0,0 +1,9 @@ +#pragma once + +#include "Common.h" +#include "RemoteServer.h" + +void ResetTimeSinceLastUpdate(); + +void InitBetterUpdates(); + diff --git a/third_party/flsharp/include/utils.h b/third_party/flsharp/include/utils.h new file mode 100644 index 0000000..dc8cd97 --- /dev/null +++ b/third_party/flsharp/include/utils.h @@ -0,0 +1,108 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include + +void Patch(DWORD vOffset, const LPVOID mem, UINT len); + +template +inline void Patch(DWORD vOffset, Type value) +{ + Patch(vOffset, &value, sizeof(Type)); +} + +void PatchBytes(DWORD vOffset, std::initializer_list bytes); + +void Nop(DWORD vOffset, UINT len); + +inline void ReadWriteProtect(DWORD location, DWORD size) +{ + DWORD _; + VirtualProtect((PVOID) location, size, PAGE_EXECUTE_READWRITE, &_); +} + +template +Func SetRelPointer(DWORD location, Func hookFunc) +{ + // Set and calculate the relative offset for the hook function + DWORD& relOriginalLocation = GetValue(location); + DWORD originalPointer = location + relOriginalLocation + 4; + + DWORD hookFuncLocation = *((PDWORD) &hookFunc); + relOriginalLocation = hookFuncLocation - (location + 4); + + return GetFuncDef(originalPointer); +} + +template +void Hook(DWORD location, Func hookFunc, UINT instrLen, bool jmp = false) +{ + assert(instrLen >= 5); + + // Set the opcode for the call or jmp instruction + Patch(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 +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(location + instrLen), 5, true); + + // Return handle for calling the gateway function which in turn calls the original function. + return GetFuncDef((DWORD) gatewayFunc); +} + +template +void CleanupTrampoline(Func trampolineFunc) +{ + VirtualFree((LPVOID) *((PDWORD) &trampolineFunc), 0, MEM_RELEASE); +} + +template +Func SetPointer(DWORD location, Func hookFunc) +{ + DWORD originalPointer = GetValue(location); + *(Func*) location = hookFunc; + + return GetFuncDef(originalPointer); +} + +template +inline Type& GetValue(DWORD location) +{ + ReadWriteProtect(location, sizeof(Type)); + return *(Type*) location; +} + +template +inline Func GetFuncDef(DWORD funcAddr) +{ + return *(Func*) &funcAddr; +} + +DWORD GetUnloadedModuleHandle(LPCTSTR moduleName); + +struct NopStr +{ + UINT len; + LPCSTR nopSequence; +}; diff --git a/third_party/flsharp/include/version_check.h b/third_party/flsharp/include/version_check.h new file mode 100644 index 0000000..adac927 --- /dev/null +++ b/third_party/flsharp/include/version_check.h @@ -0,0 +1,6 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +UINT32 GetDllProductBuildVersion(LPCSTR dllName); diff --git a/third_party/flsharp/include/vftable.h b/third_party/flsharp/include/vftable.h new file mode 100644 index 0000000..17d760b --- /dev/null +++ b/third_party/flsharp/include/vftable.h @@ -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(); diff --git a/third_party/flsharp/include/waypoint.h b/third_party/flsharp/include/waypoint.h new file mode 100644 index 0000000..26d50a0 --- /dev/null +++ b/third_party/flsharp/include/waypoint.h @@ -0,0 +1,3 @@ +#pragma once + +void InitWaypointFixes(); diff --git a/third_party/flsharp/include/waypoint_names.h b/third_party/flsharp/include/waypoint_names.h new file mode 100644 index 0000000..feec951 --- /dev/null +++ b/third_party/flsharp/include/waypoint_names.h @@ -0,0 +1,12 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +struct MissionObjective +{ + BYTE fmtStr[0x16]; // 0x0 + DWORD flags; // 0x18 +}; + +void InitWaypointNameFixes(); diff --git a/third_party/flsharp/include/weapon_anim.h b/third_party/flsharp/include/weapon_anim.h new file mode 100644 index 0000000..ac2af6d --- /dev/null +++ b/third_party/flsharp/include/weapon_anim.h @@ -0,0 +1,39 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include + +#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(); diff --git a/third_party/flsharp/src/Freelancer.cpp b/third_party/flsharp/src/Freelancer.cpp new file mode 100644 index 0000000..45d7a0f --- /dev/null +++ b/third_party/flsharp/src/Freelancer.cpp @@ -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(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) diff --git a/third_party/flsharp/src/alchemy_crash.cpp b/third_party/flsharp/src/alchemy_crash.cpp new file mode 100644 index 0000000..91b1262 --- /dev/null +++ b/third_party/flsharp/src/alchemy_crash.cpp @@ -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(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(alchemyHandle + aleLoop.startOffset + GET_FINISHED_ALE_START_TO_END, 0xC689); + } +} diff --git a/third_party/flsharp/src/base_info.cpp b/third_party/flsharp/src/base_info.cpp new file mode 100644 index 0000000..50badf4 --- /dev/null +++ b/third_party/flsharp/src/base_info.cpp @@ -0,0 +1,42 @@ +#include "base_info.h" +#include "Freelancer.h" +#include "utils.h" +#include + +// Prints the header name in bold. +void PrintInfoCategoryHeader_Hook(UINT headerIds, RenderDisplayList &rdl) +{ + WCHAR headerName[128]; + GetFlString(headerIds, headerName, _countof(headerName)); + + LPCWSTR rdlBoldTextFmt = L"%s"; + 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(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(baseInfoCat.headerNamePrintAddr + 6, 0x54); // use 5 spaces for the first line + } +} diff --git a/third_party/flsharp/src/blank_faction.cpp b/third_party/flsharp/src/blank_faction.cpp new file mode 100644 index 0000000..b3d37ad --- /dev/null +++ b/third_party/flsharp/src/blank_faction.cpp @@ -0,0 +1,27 @@ +#define WIN32_LEAN_AND_MEAN +#include +#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); +} diff --git a/third_party/flsharp/src/cheat_detection.cpp b/third_party/flsharp/src/cheat_detection.cpp new file mode 100644 index 0000000..f34a8db --- /dev/null +++ b/third_party/flsharp/src/cheat_detection.cpp @@ -0,0 +1,103 @@ +#include "cheat_detection.h" +#include "logger.h" +#include "fl_func.h" +#include "utils.h" +#include "Common.h" +#include + +#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); +} diff --git a/third_party/flsharp/src/config_reader.cpp b/third_party/flsharp/src/config_reader.cpp new file mode 100644 index 0000000..089b42b --- /dev/null +++ b/third_party/flsharp/src/config_reader.cpp @@ -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(); +} diff --git a/third_party/flsharp/src/copy_paste.cpp b/third_party/flsharp/src/copy_paste.cpp new file mode 100644 index 0000000..227165e --- /dev/null +++ b/third_party/flsharp/src/copy_paste.cpp @@ -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(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(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); +} diff --git a/third_party/flsharp/src/cursor_colors.cpp b/third_party/flsharp/src/cursor_colors.cpp new file mode 100644 index 0000000..e2f1394 --- /dev/null +++ b/third_party/flsharp/src/cursor_colors.cpp @@ -0,0 +1,223 @@ +#include "cursor_colors.h" +#include "utils.h" +#include "Freelancer.h" +#include "fl_func.h" + +#include +#include +#include +#include + +#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> groupCursors, tradeRequestCursors; +//std::map> radioCursors; + +std::shared_ptr CreateCustomCursor(const MouseCursor* originalCursor, DWORD color, LPCSTR nicknameSuffix) +{ + auto result = std::make_shared(*originalCursor); + + strcat_s(result->nickname, sizeof(result->nickname), nicknameSuffix); + result->nicknameLen = strlen(result->nickname); + result->color = color; + + return result; +} + +void FillCustomCursorMap(const std::vector &cursorNames, LPCSTR neutralCursorName) +{ + std::vector 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 normalCursorNames = { "friendly", "neutral", "hostile" }; + std::vector 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); +} diff --git a/third_party/flsharp/src/dealer_fixes.cpp b/third_party/flsharp/src/dealer_fixes.cpp new file mode 100644 index 0000000..0424c1a --- /dev/null +++ b/third_party/flsharp/src/dealer_fixes.cpp @@ -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); +} diff --git a/third_party/flsharp/src/dll_crash.cpp b/third_party/flsharp/src/dll_crash.cpp new file mode 100644 index 0000000..e8c10cc --- /dev/null +++ b/third_party/flsharp/src/dll_crash.cpp @@ -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(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); +} diff --git a/third_party/flsharp/src/exit.cpp b/third_party/flsharp/src/exit.cpp new file mode 100644 index 0000000..4b258c7 --- /dev/null +++ b/third_party/flsharp/src/exit.cpp @@ -0,0 +1,90 @@ +#include "exit.h" +#include "DALib.h" +#include "utils.h" +#include "fl_func.h" + +FL_FUNC(void exit_Original(int const status), dword ptr ds:[0x5C713C]) + +void exit_Hook(int const status) +{ + // Call the original function with WaitForSingleObject. + CGunWrapper::Shutdown(); + + // Call the original exit function. + exit_Original(status); +} + +bool noQuitMsgRetrieved = true; +bool (*HandleMessages_Original)(WPARAM *msgWParam); + +bool HandleMessages_Hook(WPARAM *msgWParam) +{ + bool result = HandleMessages_Original(msgWParam); + return noQuitMsgRetrieved &= result; +} + + +// In Freelancer, when you close the server list menu (provided that there were servers listed), +// a thread would be created that closes the DirectPlay connection (takes 15-30 seconds to execute). +// If you quit the game before the DirectPlay was connection closed, a WaitForSingleObject call would be made +// which actually waited indefinitely for the thread to finish. +// Consequently, the Freelancer process would remain open until forcefully closed (via Task Manager for instance). +// Turns out that whenever the thread calls FreeLibrary after FL's main exit function had already been called, +// that FreeLibrary call would never return, and thus the thread would never finish its task. +// I believe this was caused by a deadlock. Yet, I could not explain why this deadlock would occur under these circumstances, +// nor was I able to come up with a "clean fix" for it. So now instead of calling the function with WaitForSingleObject after the exit, +// I call it before the exit. The thread still takes a very long time to close the DirectPlay connection (which I think is a bug too), +// but at least there is no more deadlock and the Freelancer process will eventually close, as it should. +void InitPostGameDeadlockFix() +{ + #define CGUNWRAPPER_SHUTDOWN_CALL_ADDR 0x5B2190 + #define FL_EXE_EXIT_CALL_ADDR 0x5B81C6 + + Nop(CGUNWRAPPER_SHUTDOWN_CALL_ADDR, 5); // nop out the post-game CGunWrapper::Shutdown() call; call it in the exit hook instead + Hook(FL_EXE_EXIT_CALL_ADDR, exit_Hook, 6); +} + +// Freelancer has a message handler function which can be called from multiple places. +// Normally it is called by the "main" function. However, if for example you see the "disconnected" +// dialog, the message handler is actually called from somewhere else. +// This is normally not a problem, unless you exit the game while that dialog is showing. +// The message handler function returns false if the "Quit" message was retrieved. +// When the main caller sees that false was returned, it exits from the loop and shuts down the game. +// This does not happen when the disconnected dialog is showing; it will continue handling messages as normal. +// The "Quit" message is only retrieved once, so when the main handler takes over, it will continue handling messages forever, +// despite the the window being closed by the user. +// This hook fixes the problem by always returning false after the message handler returned false at some point. +void InitQuitMessageFix() +{ + #define HANDLE_MESSAGES_ADDR 0x5B0B60 + + HandleMessages_Original = Trampoline(HANDLE_MESSAGES_ADDR, HandleMessages_Hook, 5); +} + +void CleanupQuitMessageFix() +{ + CleanupTrampoline(HandleMessages_Original); +} + +// TODO: If anyone would like to look into this further: in dpnet.dll there's a function called "DN_Close" (locate it by downloading the debug symbols from Microsoft). +// I believe this function is supposed to represent IDirectPlay8Client::Close. It is this exact function that takes ~40 seconds to return on my end. +// This seems strange since in all online examples I could find that closed some DirectPlay connection, it is always done on the main thread. +// Surely, something must have been done incorrectly in one of the DirectPlay calls. Since DA couldn't figure out what, +// they took the band-aid approach and closed the connection on a separate thread. +// Otherwise the screen freezes for 40 seconds every time the server list menu is closed. + +// TODO Idea: File offset 0x30896 in gundll.dll. This is a call to IDirectPlay8Client::Connect. +// phAsyncHandle +// A DPNHANDLE. When the method returns, phAsyncHandle will point to a handle that you can pass to IDirectPlay8Client::CancelAsyncOperation to cancel the operation. +// This parameter must be set to NULL if you set the DPNCONNECT_SYNC flag in dwFlags. +// What happens if you call IDirectPlay8Client::CancelAsyncOperation before? See dplay.doc in Downloads folder. +// IDirectPlay8Client::CancelAsyncOperation +// Cancels asynchronous requests. Many methods of the IDirectPlay8Client interface run asynchronously by default. Depending on the situation, you might want to cancel requests before they are processed. All the methods of this interface that can be run asynchronously return a hAsyncHandle parameter. +// Specific requests are canceled by passing the hAsyncHandle of the request in this method’s hAsyncHandle parameter. You can cancel all pending asynchronous operations by calling this method, specifying NULL in the hAsyncHandle parameter, and specifying DPNCANCEL_ALL_OPERATIONS in the dwFlags parameter. If a specific handle is provided to this method, no flags should be set. +// DirectPlayClient->CancelAsyncOperation( NULL, DPNCANCEL_ALL_OPERATIONS ); Find where DirectPlayClient is + +// gundll.dll: file offset 0x8376. change xor esi, esi in the function call to mov esi, 1. This fixes the 30 second timer +// Test if this works in Win XP too +// dalib.dll: file offset 0x4C82 = load library call of gundll.dll. Use this to set hooks +// dxcheckOK( DirectPlayClient->Close(DPNCLOSE_IMMEDIATE) ); // WARNING DPNCLOSE_IMMEDIATE is a DP feature from DirectX 9 (released shortly after FL came out) +// SafeRelease( DirectPlayClient ); diff --git a/third_party/flsharp/src/feature_config.cpp b/third_party/flsharp/src/feature_config.cpp new file mode 100644 index 0000000..4873f4a --- /dev/null +++ b/third_party/flsharp/src/feature_config.cpp @@ -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(); +} diff --git a/third_party/flsharp/src/fl_math.cpp b/third_party/flsharp/src/fl_math.cpp new file mode 100644 index 0000000..bb05676 --- /dev/null +++ b/third_party/flsharp/src/fl_math.cpp @@ -0,0 +1,38 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#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)); +} diff --git a/third_party/flsharp/src/flash_particles.cpp b/third_party/flsharp/src/flash_particles.cpp new file mode 100644 index 0000000..54a6938 --- /dev/null +++ b/third_party/flsharp/src/flash_particles.cpp @@ -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(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); + } +} diff --git a/third_party/flsharp/src/group_members.cpp b/third_party/flsharp/src/group_members.cpp new file mode 100644 index 0000000..0a1b74a --- /dev/null +++ b/third_party/flsharp/src/group_members.cpp @@ -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(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(ATTITUDE_CHECK_CURRENT_INFO_ADDR + 5, 0x56EB); // Jump directly to wcscat after our hook executed + GetValue(CLEAN_WCSCAT_STACK_ADDR + 2) -= sizeof(DWORD) * 2; // two params were removed so do not clean them up +} diff --git a/third_party/flsharp/src/infocards.cpp b/third_party/flsharp/src/infocards.cpp new file mode 100644 index 0000000..2cb38bc --- /dev/null +++ b/third_party/flsharp/src/infocards.cpp @@ -0,0 +1,148 @@ +#include "infocards.h" +#include "Common.h" +#include "utils.h" +#include "logger.h" + +std::map msnBaseIdsInfoMap; +std::map msnNicknameIdsInfoMap; + +void ParseEntries(INI_Reader& reader, const std::map& 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 entries = { + { CreateID("MissionCreatedSolar"), { msnBaseIdsInfoMap, "base", "ids_info" } }, + { CreateID("MissionCreatedNonDockableSolar"), { msnNicknameIdsInfoMap, "nickname", "ids_info" } } + }; + + ParseEntries(reader, entries); + reader.close(); +} + +bool FindValueInMap(std::map& 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(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(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(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(GET_INFOCARD_RET_STACK_SIZE) += sizeof(DWORD); +} diff --git a/third_party/flsharp/src/logger.cpp b/third_party/flsharp/src/logger.cpp new file mode 100644 index 0000000..a19aa6f --- /dev/null +++ b/third_party/flsharp/src/logger.cpp @@ -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); +} diff --git a/third_party/flsharp/src/main.cpp b/third_party/flsharp/src/main.cpp new file mode 100644 index 0000000..e8c0425 --- /dev/null +++ b/third_party/flsharp/src/main.cpp @@ -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 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; +} diff --git a/third_party/flsharp/src/mouse.cpp b/third_party/flsharp/src/mouse.cpp new file mode 100644 index 0000000..e07392f --- /dev/null +++ b/third_party/flsharp/src/mouse.cpp @@ -0,0 +1,133 @@ +#include "mouse.h" +#include "utils.h" +#include "fl_func.h" + +#define WIN32_LEAN_AND_MEAN +#include + +#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(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(THIS_PTR_ACQUIRE_MOUSE_ADDR, 0x4E); // mov eax, [esi+0x10] -> mov ecx, [esi+0x10] + Hook(ACQUIRE_MOUSE_ADDR, Acquire_Hook, 6); +} diff --git a/third_party/flsharp/src/pilot_names.cpp b/third_party/flsharp/src/pilot_names.cpp new file mode 100644 index 0000000..5148b22 --- /dev/null +++ b/third_party/flsharp/src/pilot_names.cpp @@ -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); +} diff --git a/third_party/flsharp/src/projectiles.cpp b/third_party/flsharp/src/projectiles.cpp new file mode 100644 index 0000000..f40d895 --- /dev/null +++ b/third_party/flsharp/src/projectiles.cpp @@ -0,0 +1,61 @@ +#include "projectiles.h" +#include "utils.h" +#include "logger.h" +#include + +#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(this->GetProjectilesPerFire(), 1); +} + +void InitProjectilesSoundFix() +{ + #define PROJECTILES_PER_FIRE_CALL_ADDR 0x534D0D + + Patch(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"); + } +} diff --git a/third_party/flsharp/src/rep_requirements.cpp b/third_party/flsharp/src/rep_requirements.cpp new file mode 100644 index 0000000..6eddf6a --- /dev/null +++ b/third_party/flsharp/src/rep_requirements.cpp @@ -0,0 +1,131 @@ +#include "rep_requirements.h" +#include "utils.h" +#include "Freelancer.h" +#include "fl_func.h" +#include + +#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(repValue * 100.0f); +} + +void NN_Dealer::PrintFmtStrPurchaseInfo_Hook(UINT idsPurchaseInfo, const DealerStack& stack) +{ + static BYTE& fmtValIsZeroCheck = GetValue(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(0x4B9011); // 1564 by default + + Hook(REP_REQUIREMENTS_NOT_MET_ADDR + 0x9, &NN_Dealer::PrintFmtStrPurchaseInfo_Hook, 5); + Patch(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); +} diff --git a/third_party/flsharp/src/resolutions.cpp b/third_party/flsharp/src/resolutions.cpp new file mode 100644 index 0000000..23b9a05 --- /dev/null +++ b/third_party/flsharp/src/resolutions.cpp @@ -0,0 +1,300 @@ +#include "resolutions.h" +#include "resolutions_asm.h" +#include "utils.h" +#include "fl_func.h" +#include + +#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 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(DEFAULT_RES_WIDTH_PTR_1, mainMonitorRes.width); + Patch(DEFAULT_RES_WIDTH_PTR_2, mainMonitorRes.width); + Patch(DEFAULT_RES_HEIGHT_PTR_1, mainMonitorRes.height); + Patch(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(ref, resSupportedInfoOffset); + + // weird negated value (note the minus sign) + Patch(0x4B24A5, -resSupportedInfoOffset); + + // +0x954 + const DWORD resIndicesRefs[] = { 0x4B249C, 0x4B17E0, 0x4B0FFA, 0x4ACEF9, 0x4B0764 }; + for (const auto& ref : resIndicesRefs) + Patch(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(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(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(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(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(0x4B1D09) += sizeof(DWORD); + GetValue(0x4B1D14) += sizeof(DWORD); +} + +void CleanupBetterResolutions() +{ + delete[] lastResSupportedArr; + CleanupTrampoline(TestResolutions_Original); +} diff --git a/third_party/flsharp/src/resolutions_asm.cpp b/third_party/flsharp/src/resolutions_asm.cpp new file mode 100644 index 0000000..a2e6fc8 --- /dev/null +++ b/third_party/flsharp/src/resolutions_asm.cpp @@ -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; +} diff --git a/third_party/flsharp/src/save_crash.cpp b/third_party/flsharp/src/save_crash.cpp new file mode 100644 index 0000000..cf3daa3 --- /dev/null +++ b/third_party/flsharp/src/save_crash.cpp @@ -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& 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(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); + } +} diff --git a/third_party/flsharp/src/server_filter.cpp b/third_party/flsharp/src/server_filter.cpp new file mode 100644 index 0000000..e50f9d2 --- /dev/null +++ b/third_party/flsharp/src/server_filter.cpp @@ -0,0 +1,48 @@ +#define WIN32_LEAN_AND_MEAN +#include +#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); +} diff --git a/third_party/flsharp/src/shield_capacity.cpp b/third_party/flsharp/src/shield_capacity.cpp new file mode 100644 index 0000000..7ca0164 --- /dev/null +++ b/third_party/flsharp/src/shield_capacity.cpp @@ -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(GET_MAX_SHIELD_CAPACITY_ADDR, 0x918D); // fld dword [ecx+0x94] -> lea edx, [ecx+0x94] + Hook(MAX_SHIELD_CAPACITY_FTOL_ADDR, GetShieldCapacity_Hook, 5); +} diff --git a/third_party/flsharp/src/temp_fixes.cpp b/third_party/flsharp/src/temp_fixes.cpp new file mode 100644 index 0000000..f98da22 --- /dev/null +++ b/third_party/flsharp/src/temp_fixes.cpp @@ -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(commonHandle + DEFAULT_ROTATION_LOCK_CMN_OFFSET); + defaultAutoLevelValue = GetValue(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); +} diff --git a/third_party/flsharp/src/test_sounds.cpp b/third_party/flsharp/src/test_sounds.cpp new file mode 100644 index 0000000..7c127fd --- /dev/null +++ b/third_party/flsharp/src/test_sounds.cpp @@ -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(INDEPENDENT_INTERFACE_VOLUME_VAL_ADDR) == 0x83) + { + Patch(0x4B1533, 0x00FA); + Patch(0x4B154E, 0xDF); + } + + if (GetValue(INDEPENDENT_AMBIENCE_VOLUME_VAL_ADDR) == 0x84) + { + Patch(0x4B1584, 0xA9); + Patch(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(0x42A3A7); + BYTE jmpNoPauseForBgmOriginal = jmpNoPauseForBgm; + jmpNoPauseForBgm = 0x00; + Pause(); + jmpNoPauseForBgm = jmpNoPauseForBgmOriginal; +} + +void SoundHandle::ForceResume() +{ + static BYTE& jmpNoResumeForBgm = GetValue(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); +} diff --git a/third_party/flsharp/src/trade_lane_lights.cpp b/third_party/flsharp/src/trade_lane_lights.cpp new file mode 100644 index 0000000..a144870 --- /dev/null +++ b/third_party/flsharp/src/trade_lane_lights.cpp @@ -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(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); +} diff --git a/third_party/flsharp/src/ui_anim.cpp b/third_party/flsharp/src/ui_anim.cpp new file mode 100644 index 0000000..805dfe1 --- /dev/null +++ b/third_party/flsharp/src/ui_anim.cpp @@ -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); +} diff --git a/third_party/flsharp/src/update.cpp b/third_party/flsharp/src/update.cpp new file mode 100644 index 0000000..b5fec90 --- /dev/null +++ b/third_party/flsharp/src/update.cpp @@ -0,0 +1,190 @@ +#include "Freelancer.h" +#include "update.h" +#include "utils.h" +#include +#include + +#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(PUSH_SHIP_POS_SYNC_CHECK_ADDR, 0x57); // push eax -> push edi (provide the CShip& to the CheckForSync hook) + + Patch(OBJ_UPDATE_CALL_ADDR, 0x57); // push edi (provide the CShip& to the SPObjUpdate hook) + Hook(OBJ_UPDATE_CALL_ADDR + 1, &IServerImpl::SPObjUpdate_Hook, 5); +} diff --git a/third_party/flsharp/src/utils.cpp b/third_party/flsharp/src/utils.cpp new file mode 100644 index 0000000..93b6d18 --- /dev/null +++ b/third_party/flsharp/src/utils.cpp @@ -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 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; +} diff --git a/third_party/flsharp/src/version_check.cpp b/third_party/flsharp/src/version_check.cpp new file mode 100644 index 0000000..4556291 --- /dev/null +++ b/third_party/flsharp/src/version_check.cpp @@ -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(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; +} diff --git a/third_party/flsharp/src/waypoint.cpp b/third_party/flsharp/src/waypoint.cpp new file mode 100644 index 0000000..d4690d9 --- /dev/null +++ b/third_party/flsharp/src/waypoint.cpp @@ -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); +} diff --git a/third_party/flsharp/src/waypoint_names.cpp b/third_party/flsharp/src/waypoint_names.cpp new file mode 100644 index 0000000..ef6b3a6 --- /dev/null +++ b/third_party/flsharp/src/waypoint_names.cpp @@ -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(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(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); +} diff --git a/third_party/flsharp/src/weapon_anim.cpp b/third_party/flsharp/src/weapon_anim.cpp new file mode 100644 index 0000000..11710d4 --- /dev/null +++ b/third_party/flsharp/src/weapon_anim.cpp @@ -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(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); +} diff --git a/tools/package-release.ps1 b/tools/package-release.ps1 new file mode 100644 index 0000000..f585530 --- /dev/null +++ b/tools/package-release.ps1 @@ -0,0 +1,33 @@ +param( + [string]$LauncherPath = (Join-Path (Split-Path $PSScriptRoot -Parent) '..\Launcher'), + [string]$BuildDirectory = (Join-Path (Split-Path $PSScriptRoot -Parent) 'build-nmake') +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path $PSScriptRoot -Parent +$buildDirectory = [System.IO.Path]::GetFullPath($BuildDirectory) +$releaseDirectory = Join-Path $root 'release' +$stagingDirectory = Join-Path $buildDirectory 'package' +$exeDirectory = Join-Path $stagingDirectory 'EXE' +$archivePath = Join-Path $releaseDirectory 'rem-essentials-release.zip' + +New-Item -ItemType Directory -Force $releaseDirectory | Out-Null +Copy-Item (Join-Path $buildDirectory 'bin\rem-essentials.dll') (Join-Path $releaseDirectory 'rem-essentials.dll') -Force + +if (Test-Path $stagingDirectory) { + Remove-Item $stagingDirectory -Recurse -Force +} + +New-Item -ItemType Directory -Force $exeDirectory | Out-Null +Copy-Item (Join-Path $releaseDirectory 'rem-essentials.dll') (Join-Path $exeDirectory 'rem-essentials.dll') +Copy-Item (Join-Path $root 'rem-essentials.ini') (Join-Path $exeDirectory 'rem-essentials.ini') +Copy-Item (Join-Path $root 'docs\configuration.md') (Join-Path $stagingDirectory 'CONFIGURATION.md') + +if (Test-Path $archivePath) { + Remove-Item $archivePath -Force +} + +Compress-Archive -Path (Join-Path $stagingDirectory '*') -DestinationPath $archivePath -CompressionLevel Optimal +& (Join-Path $PSScriptRoot 'validate-release.ps1') -LauncherPath $LauncherPath -BuildDirectory $buildDirectory + +Write-Host "Release package created: $archivePath" diff --git a/tools/validate-release.ps1 b/tools/validate-release.ps1 new file mode 100644 index 0000000..ac1ff98 --- /dev/null +++ b/tools/validate-release.ps1 @@ -0,0 +1,97 @@ +param( + [string]$LauncherPath = (Join-Path (Split-Path $PSScriptRoot -Parent) '..\Launcher'), + [string]$BuildDirectory = (Join-Path (Split-Path $PSScriptRoot -Parent) 'build-nmake') +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path $PSScriptRoot -Parent +$launcherPath = [System.IO.Path]::GetFullPath($LauncherPath) +$buildDirectory = [System.IO.Path]::GetFullPath($BuildDirectory) + +function Assert-Condition { + param([bool]$Condition, [string]$Message) + if (-not $Condition) { + throw $Message + } +} + +function Read-FeatureKeys { + param([string]$IniPath) + + $section = '' + $keys = [System.Collections.Generic.List[string]]::new() + foreach ($rawLine in Get-Content $IniPath) { + $line = $rawLine.Trim() + if ($line -match '^\[(.+)\]$') { + $section = $Matches[1] + continue + } + + if ($section -ieq 'rem-essentials' -and $line -match '^([^;][^=]+?)\s*=') { + $keys.Add($Matches[1].Trim()) + } + } + + return $keys +} + +function Normalize-IniText { + param([string]$Text) + return (($Text -replace "`r`n", "`n").TrimEnd()) + "`n" +} + +$mainSource = Get-Content (Join-Path $root 'src\main.cpp') -Raw +$registeredKeys = [regex]::Matches($mainSource, 'manager\.Register\("([^"]+)"') | + ForEach-Object { $_.Groups[1].Value } +$iniKeys = Read-FeatureKeys (Join-Path $root 'rem-essentials.ini') + +$missingInIni = @($registeredKeys | Where-Object { $_ -notin $iniKeys }) +$missingInCode = @($iniKeys | Where-Object { $_ -notin $registeredKeys }) +Assert-Condition ($missingInIni.Count -eq 0) "Registered features missing from INI: $($missingInIni -join ', ')" +Assert-Condition ($missingInCode.Count -eq 0) "INI features missing from code: $($missingInCode -join ', ')" +Assert-Condition (($registeredKeys | Select-Object -Unique).Count -eq $registeredKeys.Count) 'Duplicate feature registrations found.' + +$cmake = Get-Content (Join-Path $root 'CMakeLists.txt') -Raw +Assert-Condition ($cmake -notmatch '(?i)direct_?ips') 'direct_ips must not be compiled.' + +$launcherServicePath = Join-Path $launcherPath 'RemLauncher.Core\Services\GameSettingsService.cs' +$launcherMainPath = Join-Path $launcherPath 'RemLauncher\MainWindow.xaml.cs' +$launcherHtmlPath = Join-Path $launcherPath 'web\templates\gamesettings.html' +Assert-Condition (Test-Path $launcherServicePath) "Launcher service not found at $launcherServicePath" + +$launcherService = Get-Content $launcherServicePath -Raw +$rawIniMatch = [regex]::Match( + $launcherService, + 'private const string DefaultRemEssentialsIni = """\r?\n(?.*?)\r?\n\s*""";', + [System.Text.RegularExpressions.RegexOptions]::Singleline) +Assert-Condition $rawIniMatch.Success 'Could not extract the embedded launcher INI.' + +$embeddedLines = $rawIniMatch.Groups['ini'].Value -split '\r?\n' +$embeddedIni = ($embeddedLines | ForEach-Object { + if ($_.Length -ge 8) { $_.Substring(8) } else { '' } +}) -join "`n" +$sourceIni = Get-Content (Join-Path $root 'rem-essentials.ini') -Raw +Assert-Condition ((Normalize-IniText $embeddedIni) -ceq (Normalize-IniText $sourceIni)) 'Launcher and plugin default INIs differ.' + +$launcherMain = Get-Content $launcherMainPath -Raw +$launcherHtml = Get-Content $launcherHtmlPath -Raw +Assert-Condition ($launcherMain -match 'PostMessage\(String\(message\)\);\s*return true;') 'CEF bridge does not report successful messages.' +Assert-Condition ($launcherHtml -match "postMessage\('getGameSettings'\) === false") 'Settings page does not handle bridge failure explicitly.' +Assert-Condition ($launcherMain -match 'ApplyRemEssentialsUserSettings\(\);\s*await _log\.AddLogAsync\("REM Essentials user settings applied after patching') 'Overrides are not applied after patching.' + +$builtDll = Join-Path $buildDirectory 'bin\rem-essentials.dll' +$releaseDll = Join-Path $root 'release\rem-essentials.dll' +Assert-Condition (Test-Path $builtDll) "Built DLL not found at $builtDll" +Assert-Condition (Test-Path $releaseDll) "Release DLL not found at $releaseDll" + +$bytes = [System.IO.File]::ReadAllBytes($builtDll) +Assert-Condition ($bytes.Length -gt 0x40 -and $bytes[0] -eq 0x4D -and $bytes[1] -eq 0x5A) 'Built DLL is not a PE file.' +$peOffset = [BitConverter]::ToInt32($bytes, 0x3C) +$machine = [BitConverter]::ToUInt16($bytes, $peOffset + 4) +Assert-Condition ($machine -eq 0x014C) ('Built DLL is not x86 (machine 0x{0:X4}).' -f $machine) + +$builtHash = (Get-FileHash $builtDll -Algorithm SHA256).Hash +$releaseHash = (Get-FileHash $releaseDll -Algorithm SHA256).Hash +Assert-Condition ($builtHash -eq $releaseHash) 'Release DLL does not match the current build.' + +Write-Host "Release validation passed: $($registeredKeys.Count) features, x86 DLL $builtHash"