Initial code commit

This commit is contained in:
izawartka 2025-09-30 15:24:22 +02:00
parent 0b7cd1c6c6
commit b0541f8bd3
80 changed files with 13093 additions and 0 deletions

23
Directory.Build.props Normal file
View File

@ -0,0 +1,23 @@
<Project>
<PropertyGroup>
<VcpkgTriplet>x86-windows</VcpkgTriplet>
<VcpkgRoot>C:\src\vcpkg\</VcpkgRoot>
<VcpkgInstalledDir>$(VcpkgRoot)installed\$(VcpkgTriplet)\</VcpkgInstalledDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(VcpkgInstalledDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<ObjectFileName>$(IntDir)%(RelativeDir)%(Filename).obj</ObjectFileName>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VcpkgInstalledDir)lib;$(VcpkgInstalledDir)debug\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<PostBuildEvent>
if exist "$(VcpkgInstalledDir)bin\" (
xcopy /Y /I "$(VcpkgInstalledDir)bin\*.*" "$(OutDir)"
)
</PostBuildEvent>
</Link>
</ItemDefinitionGroup>
</Project>

7
GTA2.props Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<GTA2_DIR>D:\GTA2</GTA2_DIR>
<GTA2_EXE>gta2-seuko-mod.exe</GTA2_EXE>
</PropertyGroup>
</Project>

25
gta2-seuko-mod.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36301.6
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "gta2-seuko-mod", "gta2-seuko-mod.vcxproj", "{E36BE4A1-BF14-45D3-94FB-73C0BC74A114}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E36BE4A1-BF14-45D3-94FB-73C0BC74A114}.Debug|x86.ActiveCfg = Debug|Win32
{E36BE4A1-BF14-45D3-94FB-73C0BC74A114}.Debug|x86.Build.0 = Debug|Win32
{E36BE4A1-BF14-45D3-94FB-73C0BC74A114}.Release|x86.ActiveCfg = Release|Win32
{E36BE4A1-BF14-45D3-94FB-73C0BC74A114}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2EB2C408-09DD-4DDB-82A3-1507A8F0B787}
EndGlobalSection
EndGlobal

231
gta2-seuko-mod.vcxproj Normal file
View File

@ -0,0 +1,231 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{e36be4a1-bf14-45d3-94fb-73c0bc74a114}</ProjectGuid>
<RootNamespace>gta2seukomod</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>gta2-seuko-mod</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="GTA2.props" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="GTA2.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;GTA2SEUKOMOD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>
</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile />
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)third_party\spdlog\include\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<PostBuildEvent>
<Command>if "$(GTA2_DIR)"=="" (
echo ERROR: GTA2_DIR is not set. Make sure to set in in GTA2.props file.
exit /b 1
)
if "$(GTA2_EXE)"=="" (
echo ERROR: GTA2_EXE is not set. Make sure to set in in GTA2.props file.
exit /b 1
)
if not exist "$(GTA2_DIR)\" (
echo ERROR: GTA2_DIR "$(GTA2_DIR)" specified in GTA2.props does not exist.
exit /b 1
)
if not exist "$(GTA2_DIR)\$(GTA2_EXE)" (
echo ERROR: "$(GTA2_EXE)" not found in "$(GTA2_DIR)". Check GTA2.props file.
exit /b 1
)
copy /Y "$(TargetPath)" "$(GTA2_DIR)\"
if exist "$(TargetDir)$(TargetName).pdb" copy /Y "$(TargetDir)$(TargetName).pdb" "$(GTA2_DIR)\"
exit /b 0
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;GTA2SEUKOMOD_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>
</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile />
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)third_party\spdlog\include\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
</Link>
<PostBuildEvent>
<Command>if "$(GTA2_DIR)"=="" (
echo ERROR: GTA2_DIR is not set. Make sure to set in in GTA2.props file.
exit /b 1
)
if "$(GTA2_EXE)"=="" (
echo ERROR: GTA2_EXE is not set. Make sure to set in in GTA2.props file.
exit /b 1
)
if not exist "$(GTA2_DIR)\" (
echo ERROR: GTA2_DIR "$(GTA2_DIR)" specified in GTA2.props does not exist.
exit /b 1
)
if not exist "$(GTA2_DIR)\$(GTA2_EXE)" (
echo ERROR: "$(GTA2_EXE)" not found in "$(GTA2_DIR)". Check GTA2.props file.
exit /b 1
)
copy /Y "$(TargetPath)" "$(GTA2_DIR)\"
if exist "$(TargetDir)$(TargetName).pdb" copy /Y "$(TargetDir)$(TargetName).pdb" "$(GTA2_DIR)\"
exit /b 0
</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="src\common.h" />
<ClInclude Include="src\core\core.h" />
<ClInclude Include="src\core\listener-base.h" />
<ClInclude Include="src\core\make-resolver.h" />
<ClInclude Include="src\core\types.h" />
<ClInclude Include="src\core\watch-manager.h" />
<ClInclude Include="src\core\watched-base.h" />
<ClInclude Include="src\events\game-tick.h" />
<ClInclude Include="src\events\keyboard.h" />
<ClInclude Include="src\game\common.h" />
<ClInclude Include="src\game\keycode.h" />
<ClInclude Include="src\game\functions.h">
<DeploymentContent>false</DeploymentContent>
</ClInclude>
<ClInclude Include="src\game\game.h" />
<ClInclude Include="src\game\gta2.h" />
<ClInclude Include="src\core\event-manager.h" />
<ClInclude Include="src\core\event-base.h" />
<ClInclude Include="src\core\hook-manager.h" />
<ClInclude Include="src\game\gta2_wrapper.h" />
<ClInclude Include="src\hook-types\function-call-hook.h" />
<ClInclude Include="src\core\hook-base.h" />
<ClInclude Include="src\core\module-manager.h" />
<ClInclude Include="src\core\module-base.h" />
<ClInclude Include="src\game\memory.h" />
<ClInclude Include="src\core\common.h" />
<ClInclude Include="src\events\draw.h" />
<ClInclude Include="src\hook-types\function-multi-call-hook.h" />
<ClInclude Include="src\modules\modmenu\common.h" />
<ClInclude Include="src\modules\modmenu\menu-base.h" />
<ClInclude Include="src\modules\modmenu\menus\main-menu.h" />
<ClInclude Include="src\modules\modmenu\menus\menus.h" />
<ClInclude Include="src\modules\modmenu\menus\player-menu.h" />
<ClInclude Include="src\modules\modmenu\menus\player-pos-menu.h" />
<ClInclude Include="src\modules\modmenu\modmenu.h" />
<ClInclude Include="src\modules\modmenu\root.h" />
<ClInclude Include="src\modules\ui\common.h" />
<ClInclude Include="src\modules\ui\component.h" />
<ClInclude Include="src\modules\ui\components\components.h" />
<ClInclude Include="src\modules\ui\components\horcont.h" />
<ClInclude Include="src\modules\ui\components\margin.h" />
<ClInclude Include="src\modules\ui\components\selectable.h" />
<ClInclude Include="src\modules\ui\components\spacer.h" />
<ClInclude Include="src\modules\ui\components\text.h" />
<ClInclude Include="src\modules\ui\components\vertcont.h" />
<ClInclude Include="src\modules\ui\controller.h" />
<ClInclude Include="src\modules\ui\controllers\controllers.h" />
<ClInclude Include="src\modules\ui\controllers\var-text-editable.h" />
<ClInclude Include="src\modules\ui\controllers\menu.h" />
<ClInclude Include="src\modules\ui\controllers\var-text.h" />
<ClInclude Include="src\modules\ui\rect.h" />
<ClInclude Include="src\modules\ui\root.h" />
<ClInclude Include="src\modules\ui\ui.h" />
<ClInclude Include="src\modules\ui\utils\string-utils.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\console.cpp" />
<ClCompile Include="src\core\core.cpp" />
<ClCompile Include="src\core\event-listener-support.h" />
<ClCompile Include="src\core\event-manager.cpp" />
<ClCompile Include="src\core\hook-manager.cpp" />
<ClCompile Include="src\core\module-manager.cpp" />
<ClCompile Include="src\core\watch-manager.cpp" />
<ClCompile Include="src\dllmain.cpp" />
<ClCompile Include="src\events\draw.cpp" />
<ClCompile Include="src\events\game-tick.cpp" />
<ClCompile Include="src\events\keyboard.cpp" />
<ClCompile Include="src\game\keycode.cpp" />
<ClCompile Include="src\modules\modmenu\menu-base.cpp" />
<ClCompile Include="src\modules\modmenu\menus\main-menu.cpp" />
<ClCompile Include="src\modules\modmenu\menus\player-menu.cpp" />
<ClCompile Include="src\modules\modmenu\menus\player-pos-menu.cpp" />
<ClCompile Include="src\modules\modmenu\root.cpp" />
<ClCompile Include="src\modules\ui\component.cpp" />
<ClCompile Include="src\modules\ui\components\selectable.cpp" />
<ClCompile Include="src\modules\ui\components\text.cpp" />
<ClCompile Include="src\modules\ui\controllers\menu.cpp" />
<ClCompile Include="src\modules\ui\root.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="GTA2.props" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -0,0 +1,246 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\core\common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\gta2.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\memory.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\event-manager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\event-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\core.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\hook-manager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\events\draw.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\hook-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\functions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\module-manager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\module-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\hook-types\function-call-hook.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\modmenu.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\game.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\gta2_wrapper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\watch-manager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\watched-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\make-resolver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\events\game-tick.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\hook-types\function-multi-call-hook.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\types.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\events\keyboard.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\ui.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\game\keycode.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\root.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\component.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\text.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\rect.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\components.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\vertcont.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\horcont.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\spacer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\margin.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\components\selectable.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\controllers\menu.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\controllers\controllers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\controller.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\core\listener-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\common.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\menu-base.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\menus\menus.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\menus\main-menu.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\root.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\menus\player-menu.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\modmenu\menus\player-pos-menu.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\controllers\var-text.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\controllers\var-text-editable.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\modules\ui\utils\string-utils.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\hook-manager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\event-manager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\module-manager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\events\draw.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\core.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\watch-manager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\events\game-tick.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\console.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\events\keyboard.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\ui\root.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\ui\component.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\ui\components\text.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\ui\components\selectable.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\ui\controllers\menu.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\core\event-listener-support.h">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\modmenu\root.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\modmenu\menus\main-menu.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\modmenu\menus\player-menu.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\modmenu\menu-base.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\modules\modmenu\menus\player-pos-menu.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\game\keycode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="GTA2.props" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>true</ShowAllFiles>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LocalDebuggerCommand>$(GTA2_DIR)\$(GTA2_EXE)</LocalDebuggerCommand>
<LocalDebuggerWorkingDirectory>$(GTA2_DIR)</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<LocalDebuggerCommand>$(GTA2_DIR)\$(GTA2_EXE)</LocalDebuggerCommand>
<LocalDebuggerWorkingDirectory>$(GTA2_DIR)</LocalDebuggerWorkingDirectory>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

6
src/common.h Normal file
View File

@ -0,0 +1,6 @@
#pragma once
#include <windows.h>
#include <math.h>
#include <atlstr.h>
#include "spdlog/spdlog.h"

31
src/console.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "common.h"
#if defined(_DEBUG)
#include <spdlog/sinks/stdout_color_sinks.h>
#include <cstdio>
#include <iostream>
#endif
static void InitConsole()
{
#if defined(_DEBUG)
if (!AttachConsole(ATTACH_PARENT_PROCESS))
AllocConsole();
FILE* fDummy;
freopen_s(&fDummy, "CONOUT$", "w", stdout);
freopen_s(&fDummy, "CONOUT$", "w", stderr);
freopen_s(&fDummy, "CONIN$", "r", stdin);
std::ios::sync_with_stdio(true);
auto logger = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(logger);
spdlog::set_level(spdlog::level::debug);
spdlog::flush_on(spdlog::level::debug);
spdlog::info("Debug console ready. DLL initializing...");
#else
spdlog::set_level(spdlog::level::off);
#endif
}

21
src/core/common.h Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <math.h>
#include <atlstr.h>
#include <vector>
#include <functional>
#include <unordered_set>
#include <unordered_map>
#include <typeindex>
#include <memory>
#include <cassert>
#include <optional>
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>
#include "spdlog/spdlog.h"

15
src/core/core.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "core.h"
Core::Core* Core::Core::m_instance = nullptr;
Core::Core::Core()
{
assert(m_instance == nullptr, "Core instance already exists!");
m_instance = this;
spdlog::info("Done initializing Core.");
}
Core::Core::~Core()
{
m_instance = nullptr;
}

31
src/core/core.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include "common.h"
#include "types.h"
#include "hook-base.h"
#include "hook-manager.h"
#include "event-base.h"
#include "event-manager.h"
#include "event-listener-support.h"
#include "make-resolver.h"
#include "watched-base.h"
#include "watch-manager.h"
#include "module-base.h"
#include "module-manager.h"
namespace Core {
class Core
{
public:
Core();
~Core();
Core(const Core&) = delete;
Core& operator=(const Core&) = delete;
private:
static Core* m_instance;
EventManager m_eventManager;
HookManager m_hookManager;
WatchManager m_watchManager;
ModuleManager m_moduleManager;
};
}

11
src/core/event-base.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
namespace Core
{
class EventBase
{
protected:
EventBase() = default;
virtual ~EventBase() = default;
};
}

View File

@ -0,0 +1,63 @@
#pragma once
#include "common.h"
#include "types.h"
#include "event-base.h"
#include "event-manager.h"
namespace Core {
class EventListenerSupport {
protected:
EventListenerSupport() = default;
virtual ~EventListenerSupport() {
RemoveAllEventListeners();
}
template<typename EventT, typename U>
void AddEventListener(EventMethodListener<EventT, U> method) {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
const std::type_index eventTypeIdx = typeid(EventT);
if (m_eventListenerIds.find(eventTypeIdx) != m_eventListenerIds.end()) {
spdlog::error("Event listener for event type {} already added", eventTypeIdx.name());
return;
}
auto* eventManager = EventManager::GetInstance();
EventListenerId listenerId = eventManager->AddListener<EventT, U>(static_cast<U*>(this), method);
m_eventListenerIds[eventTypeIdx] = listenerId;
}
template<typename EventT>
void RemoveEventListener() {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
const std::type_index eventTypeIdx = typeid(EventT);
auto it = m_eventListenerIds.find(eventTypeIdx);
if (it == m_eventListenerIds.end()) {
spdlog::warn("No event listener found for event type {}", eventTypeIdx.name());
return;
}
auto* eventManager = EventManager::GetInstance();
eventManager->RemoveListener(eventTypeIdx, it->second);
m_eventListenerIds.erase(it);
}
void RemoveAllEventListeners() {
auto* eventManager = EventManager::GetInstance();
for (const auto& pair : m_eventListenerIds) {
eventManager->RemoveListener(pair.first, pair.second);
}
m_eventListenerIds.clear();
}
EventListenerSupport(const EventListenerSupport&) = delete;
EventListenerSupport& operator=(const EventListenerSupport&) = delete;
EventListenerSupport(EventListenerSupport&&) = delete;
EventListenerSupport& operator=(EventListenerSupport&&) = delete;
private:
std::unordered_map<std::type_index, EventListenerId> m_eventListenerIds;
};
}

View File

@ -0,0 +1,21 @@
#include "common.h"
#include "event-manager.h"
Core::EventManager* Core::EventManager::m_instance = nullptr;
Core::EventManager::EventManager()
{
assert(m_instance == nullptr, "EventManager instance already exists!");
m_instance = this;
}
Core::EventManager::~EventManager()
{
m_instance = nullptr;
}
Core::EventManager* Core::EventManager::GetInstance()
{
assert(m_instance != nullptr, "Core not initialized!");
return m_instance;
}

156
src/core/event-manager.h Normal file
View File

@ -0,0 +1,156 @@
#pragma once
#include "common.h"
#include "types.h"
#include "event-base.h"
#include <queue>
namespace Core
{
class EventManager
{
public:
static EventManager* GetInstance();
template<typename EventT>
void Dispatch(const EventT& event) {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
auto it = m_listeners.find(typeid(EventT));
if (it == m_listeners.end()) return;
m_dispatching = true;
auto& listeners = it->second;
for (auto& wrapper : listeners) {
wrapper.func(event);
}
m_dispatching = false;
ProcessPendingChanges();
}
template<typename EventT>
EventListenerId AddListener(EventListener<EventT> listener) {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
std::type_index eventTypeIdx = typeid(EventT);
auto it = m_listeners.find(eventTypeIdx);
if (it == m_listeners.end()) {
callInit<EventT>();
}
EventListenerId id = m_nextListenerId++;
PendingChange change = { ChangeType::Add, eventTypeIdx, id, [listener](const EventBase& e) {
listener(static_cast<const EventT&>(e));
} };
if (m_dispatching) {
m_pendingChanges.push(change);
return id;
}
AddListenerEntry(change);
return id;
}
template<typename EventT, typename U>
EventListenerId AddListener(U* instance, EventMethodListener<EventT, U> method) {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
return AddListener<EventT>([instance, method](const EventT& e) {
(instance->*method)(e);
});
}
void RemoveListener(std::type_index eventType, EventListenerId id) {
PendingChange change = { ChangeType::Remove, eventType, id, nullptr };
if (m_dispatching) {
m_pendingChanges.push(change);
return;
}
RemoveListenerEntry(change);
}
template<typename EventT>
void RemoveListener(EventListenerId id) {
static_assert(std::is_base_of<EventBase, EventT>::value, "EventT must derive from Core::EventBase");
RemoveListener(typeid(EventT), id);
}
private:
friend class Core;
EventManager();
~EventManager();
EventManager(const EventManager&) = delete;
EventManager& operator=(const EventManager&) = delete;
static EventManager* m_instance;
struct ListenerWrapper {
EventListenerId id;
std::function<void(const EventBase&)> func;
};
enum class ChangeType { Add, Remove };
struct PendingChange {
ChangeType type;
std::type_index eventType;
EventListenerId id;
std::function<void(const EventBase&)> func; // Only used for Add
};
void AddListenerEntry(PendingChange change) {
spdlog::debug("Adding listener id {} for event type: {}", change.id, change.eventType.name());
auto& listeners = m_listeners[change.eventType];
listeners.push_back({ change.id, change.func });
}
void RemoveListenerEntry(PendingChange change) {
spdlog::debug("Removing listener id {} for event type: {}", change.id, change.eventType.name());
auto it = m_listeners.find(change.eventType);
if (it == m_listeners.end()) {
spdlog::warn("No listeners found for event type: {}", change.eventType.name());
return;
}
auto& listeners = it->second;
auto lit = std::find_if(listeners.begin(), listeners.end(), [id = change.id](const ListenerWrapper& wrapper) {
return wrapper.id == id;
});
if (lit == listeners.end()) {
spdlog::warn("Listener id {} not found for event type: {}", change.id, change.eventType.name());
return;
}
listeners.erase(lit);
// Note: m_listeners entry is not being removed
}
void ProcessPendingChanges() {
while (!m_pendingChanges.empty()) {
auto change = m_pendingChanges.front();
m_pendingChanges.pop();
if (change.type == ChangeType::Add) {
AddListenerEntry(change);
}
if (change.type == ChangeType::Remove) {
RemoveListenerEntry(change);
}
}
}
std::unordered_map<std::type_index, std::vector<ListenerWrapper>> m_listeners;
EventListenerId m_nextListenerId = 1;
bool m_dispatching = false;
std::queue<PendingChange> m_pendingChanges;
template<typename T, typename = void>
struct has_init : std::false_type {};
template<typename T>
struct has_init<T, std::void_t<decltype(T::Init())>> : std::true_type {};
template<typename EventT>
void callInit() {
if constexpr (has_init<EventT>::value) {
spdlog::debug("Calling Init for event type: {}", typeid(EventT).name());
EventT::Init();
}
}
};
}

12
src/core/hook-base.h Normal file
View File

@ -0,0 +1,12 @@
#pragma once
#include "common.h"
namespace Core
{
class HookBase
{
protected:
HookBase() = default;
virtual ~HookBase() = default;
};
}

20
src/core/hook-manager.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "hook-manager.h"
Core::HookManager* Core::HookManager::m_instance = nullptr;
Core::HookManager::HookManager()
{
assert(m_instance == nullptr, "HookManager instance already exists!");
m_instance = this;
}
Core::HookManager::~HookManager()
{
m_instance = nullptr;
}
Core::HookManager* Core::HookManager::GetInstance()
{
assert(m_instance != nullptr, "Core not initialized!");
return m_instance;
}

41
src/core/hook-manager.h Normal file
View File

@ -0,0 +1,41 @@
#pragma once
#include "common.h"
#include "hook-base.h"
namespace Core
{
class HookManager
{
public:
static HookManager* GetInstance();
template<typename HookT>
bool AddHook(const HookT& hookDef) {
static_assert(std::is_base_of<HookBase, HookT>::value, "HookT must derive from Core::HookBase");
spdlog::debug("Adding hook: {}", typeid(HookT).name());
if (m_hooks.find(&hookDef) != m_hooks.end()) {
spdlog::debug("Hook already added: {}", typeid(HookT).name());
return true;
}
if (!HookT::Hook(hookDef)) {
spdlog::error("Failed to add hook: {}", typeid(HookT).name());
return false;
}
m_hooks.insert(&hookDef);
return true;
}
private:
friend class Core;
HookManager();
~HookManager();
HookManager(const HookManager&) = delete;
HookManager& operator=(const HookManager&) = delete;
std::unordered_set<const HookBase*> m_hooks;
static HookManager* m_instance;
};
}

55
src/core/listener-base.h Normal file
View File

@ -0,0 +1,55 @@
#pragma once
#include "common.h"
#include "types.h"
#include "event-base.h"
#include "event-manager.h"
namespace Core {
class ListenerBase {
ListenerBase() = delete;
virtual ~ListenerBase() {
RemoveAllEventListeners();
}
template<typename EventT, typename U>
void AddEventListener(EventMethodListener<EventT, U> method) {
static_assert(std::is_base_of<Event, EventT>::value, "EventT must derive from Core::Event");
const std::type_index eventTypeIdx = typeid(EventT);
if (m_eventListenerIds.find(eventTypeIdx) != m_eventListenerIds.end()) {
spdlog::error("Event listener for event type {} already added", eventTypeIdx.name());
return;
}
auto* eventManager = EventManager::GetInstance();
EventListenerId listenerId = eventManager->AddListener<EventT, U>(static_cast<U*>(this), method);
m_eventListenerIds[eventTypeIdx] = listenerId;
}
template<typename EventT>
void RemoveEventListener() {
static_assert(std::is_base_of<Event, EventT>::value, "EventT must derive from Core::Event");
const std::type_index eventTypeIdx = typeid(EventT);
auto it = m_eventListenerIds.find(eventTypeIdx);
if (it == m_eventListenerIds.end()) {
spdlog::warn("No event listener found for event type {}", eventTypeIdx.name());
return;
}
auto* eventManager = EventManager::GetInstance();
eventManager->RemoveListener(eventTypeIdx, it->second);
m_eventListenerIds.erase(it);
}
void RemoveAllEventListeners() {
auto* eventManager = EventManager::GetInstance();
for (const auto& pair : m_eventListenerIds) {
eventManager->RemoveListener(pair.first, pair.second);
}
m_eventListenerIds.clear();
}
protected:
std::unordered_map<std::type_index, EventListenerId> m_eventListenerIds;
};
}

102
src/core/make-resolver.h Normal file
View File

@ -0,0 +1,102 @@
#pragma once
#include "common.h"
namespace {
template<typename Class, typename Member>
struct MemStep {
Member Class::* mptr;
constexpr MemStep(Member Class::* p) : mptr(p) {}
template<typename Ptr>
auto operator()(Ptr base) const
-> std::conditional_t<std::is_pointer<Member>::value, Member, std::add_pointer_t<Member>>
{
using Ret = std::conditional_t<std::is_pointer<Member>::value, Member, std::add_pointer_t<Member>>;
if (!base) return static_cast<Ret>(nullptr);
if constexpr (std::is_pointer<Member>::value) {
return base->*mptr;
}
else {
return std::addressof(base->*mptr);
}
}
};
template<typename Class, typename Element, std::size_t N>
struct MemStep<Class, Element[N]> {
Element(Class::* mptr)[N];
constexpr MemStep(Element(Class::* p)[N]) : mptr(p) {}
template<typename Ptr>
Element* operator()(Ptr base) const {
if (!base) return nullptr;
return &((base->*mptr)[0]);
}
};
template<typename Class, typename Member>
constexpr auto mem(Member Class::* p) { return MemStep<Class, Member>(p); }
template<typename Class, typename Member>
struct MemAddr {
Member Class::* mptr;
constexpr MemAddr(Member Class::* p) : mptr(p) {}
template<typename Ptr>
auto operator()(Ptr base) const -> std::add_pointer_t<Member> {
using Ret = std::add_pointer_t<Member>;
if (!base) return static_cast<Ret>(nullptr);
return std::addressof(base->*mptr); // always return address of the member
}
};
template<typename Class, typename Member>
constexpr auto mem_addr(Member Class::* p) { return MemAddr<Class, Member>(p); }
struct IdxStep {
std::size_t idx;
constexpr IdxStep(std::size_t i) : idx(i) {}
template<typename ArrPtr>
auto operator()(ArrPtr arr) const {
using Elem = std::remove_pointer_t<std::decay_t<ArrPtr>>;
using Ret = std::conditional_t<std::is_pointer<Elem>::value, Elem, std::add_pointer_t<Elem>>;
if (!arr) return static_cast<Ret>(nullptr);
if constexpr (std::is_pointer<Elem>::value) {
return static_cast<Ret>(arr[idx]);
}
else {
return static_cast<Ret>(std::addressof(arr[idx]));
}
}
};
inline constexpr IdxStep idx(std::size_t i) { return IdxStep(i); }
template<std::size_t I, typename Cur, typename Tuple>
auto ChainStep(Cur cur, const Tuple& t)
{
if constexpr (I >= std::tuple_size_v<Tuple>) {
return cur;
}
else {
auto next = std::get<I>(t)(cur);
return ChainStep<I + 1>(next, t);
}
}
}
namespace Core {
template<typename BaseGetter, typename... Steps>
auto MakeResolver(BaseGetter baseGetter, Steps... steps)
-> std::function< decltype(ChainStep<0>(std::declval<std::invoke_result_t<BaseGetter>>(), std::declval<std::tuple<Steps...>>()))() >
{
auto stepsTuple = std::make_tuple(steps...);
using BasePtr = std::invoke_result_t<BaseGetter>;
using FinalPtr = decltype(ChainStep<0>(std::declval<BasePtr>(), stepsTuple));
return std::function<FinalPtr()>(
[baseGetter = std::move(baseGetter), stepsTuple]() -> FinalPtr {
BasePtr base = baseGetter();
return ChainStep<0>(base, stepsTuple);
});
}
}

18
src/core/module-base.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include "common.h"
#include "event-listener-support.h"
namespace Core
{
class ModuleBase : public EventListenerSupport
{
public:
virtual ~ModuleBase() override = default;
virtual bool Attach() = 0;
virtual void Detach() = 0;
protected:
ModuleBase() = default;
};
}

View File

@ -0,0 +1,22 @@
#include "common.h"
#include "module-manager.h"
Core::ModuleManager* Core::ModuleManager::m_instance = nullptr;
Core::ModuleManager::ModuleManager()
{
assert(m_instance == nullptr, "ModuleManager instance already exists!");
m_instance = this;
}
Core::ModuleManager::~ModuleManager()
{
RemoveAllModules();
m_instance = nullptr;
}
Core::ModuleManager* Core::ModuleManager::GetInstance()
{
assert(m_instance != nullptr, "Core not initialized!");
return m_instance;
}

80
src/core/module-manager.h Normal file
View File

@ -0,0 +1,80 @@
#pragma once
#include "common.h"
#include "module-base.h"
namespace Core
{
class ModuleManager
{
public:
static ModuleManager* GetInstance();
template<typename ModuleT, typename... Args>
ModuleT* AddModule(Args&&... args) {
static_assert(std::is_base_of<ModuleBase, ModuleT>::value, "ModuleT must derive from Core::ModuleBase");
spdlog::debug("Adding module: {}", typeid(ModuleT).name());
std::type_index typeIdx(typeid(ModuleT));
if (m_modules.find(typeIdx) != m_modules.end()) {
spdlog::warn("Module already added: {}", typeid(ModuleT).name());
return static_cast<ModuleT*>(m_modules[typeIdx].get());
}
auto module = std::make_unique<ModuleT>(std::forward<Args>(args)...);
if (!module->Attach()) {
spdlog::error("Failed to attach module: {}", typeid(ModuleT).name());
return nullptr;
}
ModuleT* modulePtr = module.get();
m_modules[typeIdx] = std::move(module);
return modulePtr;
}
template<typename ModuleT>
void RemoveModule() {
static_assert(std::is_base_of<ModuleBase, ModuleT>::value, "ModuleT must derive from Core::ModuleBase");
spdlog::debug("Removing module: {}", typeid(ModuleT).name());
std::type_index typeIdx(typeid(ModuleT));
auto it = m_modules.find(typeIdx);
if(it == m_modules.end()) {
spdlog::warn("Module not found: {}", typeid(ModuleT).name());
return;
}
it->second->Detach();
m_modules.erase(it);
}
template<typename ModuleT>
ModuleT* GetModule() {
static_assert(std::is_base_of<ModuleBase, ModuleT>::value, "ModuleT must derive from Core::ModuleBase");
std::type_index typeIdx(typeid(ModuleT));
auto it = m_modules.find(typeIdx);
if(it == m_modules.end()) {
spdlog::warn("Module not found: {}", typeid(ModuleT).name());
return nullptr;
}
return static_cast<ModuleT*>(it->second.get());
}
void RemoveAllModules() {
while (!m_modules.empty()) {
auto it = std::prev(m_modules.end());
it->second->Detach();
m_modules.erase(it);
}
}
private:
friend class Core;
ModuleManager();
~ModuleManager();
ModuleManager(const ModuleManager&) = delete;
ModuleManager& operator=(const ModuleManager&) = delete;
std::unordered_map<std::type_index, std::unique_ptr<ModuleBase>> m_modules;
static ModuleManager* m_instance;
};
}

26
src/core/types.h Normal file
View File

@ -0,0 +1,26 @@
#pragma once
#include "common.h"
namespace Core
{
/* EventManager */
template<typename EventT>
using EventListener = std::function<void(const EventT&)>;
template<typename EventT, typename U>
using EventMethodListener = void (U::*)(const EventT&);
using EventListenerId = size_t;
/* WatchManager */
template<typename T>
using Resolver = std::function<T* ()>;
template<typename T>
using WatchedListener = std::function<void(std::optional<T>, std::optional<T>)>;
template<typename T, typename U>
using WatchedMethodListener = void (U::*)(std::optional<T>, std::optional<T>);
using WatchedId = size_t;
}

View File

@ -0,0 +1,21 @@
#include "common.h"
#include "watch-manager.h"
Core::WatchManager* Core::WatchManager::m_instance = nullptr;
Core::WatchManager::WatchManager()
{
assert(m_instance == nullptr, "WatchManager instance already exists!");
m_instance = this;
}
Core::WatchManager::~WatchManager()
{
m_instance = nullptr;
}
Core::WatchManager* Core::WatchManager::GetInstance()
{
assert(m_instance != nullptr, "WatchManager not initialized!");
return m_instance;
}

135
src/core/watch-manager.h Normal file
View File

@ -0,0 +1,135 @@
#pragma once
#include "common.h"
#include "types.h"
#include "watched-base.h"
#include "event-base.h"
#include "event-manager.h"
namespace Core
{
class WatchManager {
public:
static WatchManager* GetInstance();
template<typename EventT, typename T>
Watched<T>* Watch(Resolver<T> resolver, WatchedListener<T> listener)
{
static_assert(std::is_copy_constructible<T>::value, "ComponentT must be copy-constructible");
static_assert(std::is_base_of_v<EventBase, EventT> || std::is_same<EventT, void>::value, "EventT must derive from Core::Event or be void");
spdlog::debug("Adding watched: {} (Event: {})", typeid(T).name(), typeid(EventT).name());
auto entry = new Watched<T>(m_nextEntryId++, std::move(resolver), std::move(listener));
WatchedId id = entry->m_id;
EnsureEventType<EventT>();
auto eventTypeIdx = std::type_index(typeid(EventT));
m_entries[eventTypeIdx].push_back(std::unique_ptr<WatchedBase>(entry));
m_idToEventType.push_back({ id, eventTypeIdx });
return entry;
}
template<typename EventT, typename T, typename U>
Watched<T>* Watch(Resolver<T> resolver, U* instance, WatchedMethodListener<T, U> method) {
static_assert(std::is_copy_constructible<T>::value, "ComponentT must be copy-constructible");
static_assert(std::is_base_of_v<EventBase, EventT> || std::is_same<EventT, void>::value, "EventT must derive from Core::Event or be void");
return Watch<EventT, T>(std::move(resolver), [instance, method](std::optional<T> oldValue, std::optional<T> newValue) {
(instance->*method)(oldValue, newValue);
});
}
void Unwatch(WatchedId id) {
spdlog::debug("Removing watched with id: {}", id);
auto it = std::find_if(m_idToEventType.begin(), m_idToEventType.end(),
[id](const auto& pair) { return pair.first == id; });
if (it == m_idToEventType.end()) {
spdlog::warn("No watched found with id: {}", id);
/// TODO
}
auto eventTypeIdx = it->second;
m_idToEventType.erase(it);
auto& vec = m_entries[eventTypeIdx];
vec.erase(std::remove_if(vec.begin(), vec.end(),
[id](const auto& entry) { return entry->m_id == id; }), vec.end());
}
template <typename T>
void Unwatch(Watched<T>* watched) {
Unwatch(watched->m_id);
}
private:
friend class Core;
WatchManager();
~WatchManager();
WatchManager(const WatchManager&) = delete;
WatchManager& operator=(const WatchManager&) = delete;
template<typename EventT>
void EnsureEventType() {
static_assert(std::is_base_of_v<EventBase, EventT>, "EventT must derive from Core::Event or be void");
auto eventTypeIdx = std::type_index(typeid(EventT));
if (m_typeToListenerId.find(eventTypeIdx) != m_typeToListenerId.end()) {
return;
}
m_entries[eventTypeIdx] = std::vector<std::unique_ptr<WatchedBase>>();
spdlog::debug("First watched for event type: {}, adding listener", eventTypeIdx.name());
EventListenerId listenerId = EventManager::GetInstance()->AddListener<EventT>([this](const EventT& event) {
this->Update(std::type_index(typeid(EventT)));
});
m_typeToListenerId[eventTypeIdx] = listenerId;
}
template<>
void EnsureEventType<void>() {
auto eventTypeIdx = std::type_index(typeid(void));
if (m_typeToListenerId.find(eventTypeIdx) != m_typeToListenerId.end()) {
return;
}
m_entries[eventTypeIdx] = std::vector<std::unique_ptr<WatchedBase>>();
spdlog::debug("First watched of type void, no event listener needed");
}
void RemoveEventType(std::type_index eventType) {
// Note: m_entries entry is not being removed
spdlog::debug("No more watched for event type: {}, removing listener", eventType.name());
auto it = m_typeToListenerId.find(eventType);
if (it == m_typeToListenerId.end()) return;
EventManager::GetInstance()->RemoveListener(eventType, it->second);
m_typeToListenerId.erase(it);
}
void Update(std::type_index eventType) {
auto it = m_entries.find(eventType);
if (it == m_entries.end()) return;
if (it->second.empty()) {
RemoveEventType(eventType);
return;
}
for (auto& entry : it->second) {
entry->Update();
}
}
static WatchManager* m_instance;
std::unordered_map<std::type_index, std::vector<std::unique_ptr<WatchedBase>>> m_entries;
std::vector<std::pair<WatchedId, std::type_index>> m_idToEventType;
std::unordered_map<std::type_index, EventListenerId> m_typeToListenerId;
WatchedId m_nextEntryId = 1;
};
}

73
src/core/watched-base.h Normal file
View File

@ -0,0 +1,73 @@
#pragma once
#include "types.h"
namespace Core
{
class WatchedBase
{
public:
virtual ~WatchedBase() = default;
virtual void Update() = 0;
protected:
friend class WatchManager;
WatchedBase() = default;
WatchedId m_id = 0;
};
template<typename T>
class Watched : public WatchedBase {
public:
T* GetPointer() {
return m_resolver();
}
std::optional<T> GetValue() {
T* p = m_resolver();
if (p) return *p;
return std::nullopt;
}
bool SetValue(T value) {
T* p = m_resolver();
if (p) {
*p = value;
return true;
}
return false;
}
void Update() override {
T* p = m_resolver();
bool present = (p != nullptr);
std::optional<T> value = std::nullopt;
if (present) value = *p;
if (m_first || value != m_lastValue) {
m_listener(m_lastValue, value);
m_first = false;
m_lastValue = value;
}
}
WatchedId GetId() const {
return m_id;
}
private:
friend class WatchManager;
Watched(WatchedId id_, std::function<T* ()> r, std::function<void(std::optional<T>, std::optional<T>)> l)
: m_resolver(std::move(r)), m_listener(std::move(l))
{
m_id = id_;
}
Watched(const Watched&) = delete;
Watched& operator=(const Watched&) = delete;
std::function<T* ()> m_resolver;
std::function<void(std::optional<T>, std::optional<T>)> m_listener;
std::optional<T> m_lastValue;
bool m_first = true;
};
}

48
src/dllmain.cpp Normal file
View File

@ -0,0 +1,48 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "common.h"
#include "core/core.h"
#include "modules/ui/ui.h"
#include "modules/modmenu/modmenu.h"
#include "console.cpp"
#include "events/game-tick.h"
Core::Core* coreInstance = nullptr;
static void Init()
{
InitConsole();
coreInstance = new Core::Core();
Core::ModuleManager* moduleManager = Core::ModuleManager::GetInstance();
UiModule::RootModule* rootModule = moduleManager->AddModule<UiModule::RootModule>();
ModMenuModule::RootModule* modMenu = moduleManager->AddModule<ModMenuModule::RootModule>();
modMenu->AddMenu<ModMenuModule::MainMenu>();
}
static void Deinit()
{
Core::ModuleManager* moduleManager = Core::ModuleManager::GetInstance();
moduleManager->RemoveModule<UiModule::RootModule>();
delete coreInstance;
coreInstance = nullptr;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Init();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Deinit();
break;
}
return TRUE;
}

43
src/events/draw.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "../game/game.h"
#include "../core/core.h"
#include "../hook-types/function-call-hook.h"
#include "draw.h"
static void DispatchPreDrawUIEvent()
{
PreDrawUIEvent event;
Core::EventManager::GetInstance()->Dispatch(event);
}
static void DispatchDrawUIEvent()
{
DrawUIEvent event;
Core::EventManager::GetInstance()->Dispatch(event);
}
static __declspec(naked) void DrawUIHookFunction(void)
{
__asm {
push ecx
call DispatchPreDrawUIEvent
pop ecx
call Game::Functions::DrawUI
call DispatchDrawUIEvent
ret
}
}
const FunctionCallHook drawHook = {
0x0045a61f,
(DWORD)&DrawUIHookFunction
};
bool PreDrawUIEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(drawHook);
}
bool DrawUIEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(drawHook);
}

24
src/events/draw.h Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include "../core/core.h"
/*
Dispatched right before the UI is drawn.
Things drawn here will appear on top of the game world and exploding scores but behind all other UI elements.
*/
class PreDrawUIEvent : public Core::EventBase {
public:
static bool Init();
PreDrawUIEvent() {};
virtual ~PreDrawUIEvent() override {};
};
/*
Dispatched right after the UI is drawn.
Things drawn here will appear on top of all other UI elements (excluding mike's test).
*/
class DrawUIEvent : public Core::EventBase {
public:
static bool Init();
DrawUIEvent() {};
virtual ~DrawUIEvent() override {};
};

43
src/events/game-tick.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "../game/game.h"
#include "../core/core.h"
#include "../hook-types/function-multi-call-hook.h"
#include "game-tick.h"
static void DispatchPreGameTickEvent()
{
PreGameTickEvent event;
Core::EventManager::GetInstance()->Dispatch(event);
}
static void DispatchGameTickEvent()
{
GameTickEvent event;
Core::EventManager::GetInstance()->Dispatch(event);
}
static __declspec(naked) void GameTickHookFunction(void)
{
__asm {
push ecx
call DispatchPreGameTickEvent
pop ecx
call Game::Functions::GameTick
call DispatchGameTickEvent
ret
}
}
const FunctionMultiCallHook gameTickHook = {
std::vector<DWORD>{ 0x0045c3f2, 0x0045c3e9 },
(DWORD)&GameTickHookFunction
};
bool PreGameTickEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(gameTickHook);
}
bool GameTickEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(gameTickHook);
}

22
src/events/game-tick.h Normal file
View File

@ -0,0 +1,22 @@
#pragma once
#include "../core/core.h"
/*
Dispatched every game tick, before any game logic is processed. Runs only when the game is unpaused.
*/
class PreGameTickEvent : public Core::EventBase {
public:
static bool Init();
PreGameTickEvent() {};
virtual ~PreGameTickEvent() override {};
};
/*
Dispatched every game tick, after all game logic is processed. Runs only when the game is unpaused.
*/
class GameTickEvent : public Core::EventBase {
public:
static bool Init();
GameTickEvent() {};
virtual ~GameTickEvent() override {};
};

63
src/events/keyboard.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "../game/game.h"
#include "../core/core.h"
#include "../hook-types/function-call-hook.h"
#include "keyboard.h"
static int isShiftPressed = false;
static int isCtrlPressed = false;
static int isAltPressed = false;
static void UpdateModifierKeys(bool down, Game::KeyCode keyCode)
{
if(keyCode == Game::KeyCode::DIK_LSHIFT) {
isShiftPressed = down;
}
else if (keyCode == Game::KeyCode::DIK_LCONTROL) {
isCtrlPressed = down;
}
else if (keyCode == Game::KeyCode::DIK_LMENU) {
isAltPressed = down;
}
}
static void DispatchKeyboardEvent()
{
Game::KeyCode keyCode = *Game::Memory::GetKeyPressCode();
int keyState = *Game::Memory::GetKeyPressState();
if (keyState == 0) {
// Key up event
UpdateModifierKeys(false, keyCode);
KeyUpEvent event(keyCode, isShiftPressed, isCtrlPressed, isAltPressed);
Core::EventManager::GetInstance()->Dispatch(event);
}
else {
// Key down event
UpdateModifierKeys(true, keyCode);
KeyDownEvent event(keyCode, isShiftPressed, isCtrlPressed, isAltPressed);
Core::EventManager::GetInstance()->Dispatch(event);
}
}
static __declspec(naked) void OnKeyUpDownHookFunction(void)
{
__asm {
call DispatchKeyboardEvent
jmp Game::Functions::OnKeyUpDown
}
}
const FunctionCallHook keyboardHook = {
0x0044c48d,
(DWORD)&OnKeyUpDownHookFunction
};
bool KeyDownEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(keyboardHook);
}
bool KeyUpEvent::Init()
{
return Core::HookManager::GetInstance()->AddHook(keyboardHook);
}

49
src/events/keyboard.h Normal file
View File

@ -0,0 +1,49 @@
#pragma once
#include "../core/core.h"
#include "../game/game.h"
/*
Dispatched when a key is pressed down.
*/
class KeyDownEvent : public Core::EventBase {
public:
static bool Init();
KeyDownEvent(Game::KeyCode keyCode, bool shift = false, bool ctrl = false, bool alt = false)
: m_keyCode(keyCode), m_shift(shift), m_ctrl(ctrl), m_alt(alt) {
};
virtual ~KeyDownEvent() override {};
Game::KeyCode GetKeyCode() const { return m_keyCode; }
bool IsShiftPressed() const { return m_shift; }
bool IsCtrlPressed() const { return m_ctrl; }
bool IsAltPressed() const { return m_alt; }
private:
Game::KeyCode m_keyCode;
bool m_shift = false;
bool m_ctrl = false;
bool m_alt = false;
};
/*
Dispatched when a key is released.
*/
class KeyUpEvent : public Core::EventBase {
public:
static bool Init();
KeyUpEvent(Game::KeyCode keyCode, bool shift = false, bool ctrl = false, bool alt = false)
: m_keyCode(keyCode), m_shift(shift), m_ctrl(ctrl), m_alt(alt) {
};
virtual ~KeyUpEvent() override {};
Game::KeyCode GetKeyCode() const { return m_keyCode; }
bool IsShiftPressed() const { return m_shift; }
bool IsCtrlPressed() const { return m_ctrl; }
bool IsAltPressed() const { return m_alt; }
private:
Game::KeyCode m_keyCode;
bool m_shift = false;
bool m_ctrl = false;
bool m_alt = false;
};

3
src/game/common.h Normal file
View File

@ -0,0 +1,3 @@
#pragma once
#include <atlstr.h>

24
src/game/functions.h Normal file
View File

@ -0,0 +1,24 @@
#pragma once
#include "gta2_wrapper.h"
#include "keycode.h"
namespace Game {
class Functions
{
public:
typedef void(__fastcall EachFrameDrawType)(Game* game, DWORD edx);
static constexpr EachFrameDrawType* EachFrameDraw = (EachFrameDrawType*)0x0045a5a0;
typedef void(__fastcall GameTickType)(Game* game, DWORD edx);
static constexpr GameTickType* GameTick = (GameTickType*)0x0045c1f0;
typedef void(__stdcall OnKeyUpDownType)(DWORD keyboard, KeyCode* keyCode);
static constexpr OnKeyUpDownType* OnKeyUpDown = (OnKeyUpDownType*)0x0044bfb0;
typedef void(__fastcall DrawUIType)(S10* s10, DWORD edx);
static constexpr DrawUIType* DrawUI = (DrawUIType*)0x004ca440;
typedef void(__stdcall DrawGTATextType)(WCHAR* str, SCR_f x, SCR_f y, int param_4, SCR_f scale, S4_ENUM1* param_6, int param_7, SPRITE_INVISIBILITY spriteInvisibility, SCR_f param_9);
static constexpr DrawGTATextType* DrawGTAText = (DrawGTATextType*)0x004cc100;
};
}

5
src/game/game.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
#include "common.h"
#include "gta2_wrapper.h"
#include "memory.h"
#include "functions.h"

9229
src/game/gta2.h Normal file

File diff suppressed because it is too large Load Diff

6
src/game/gta2_wrapper.h Normal file
View File

@ -0,0 +1,6 @@
#pragma once
#include "common.h"
namespace Game {
#include "gta2.h"
}

94
src/game/keycode.cpp Normal file
View File

@ -0,0 +1,94 @@
#include "keycode.h"
char Game::GetCharFromKeyCode(KeyCode keyCode, bool shift, bool capsLock)
{
auto letter = [&](char upper)->char {
return (capsLock ^ shift) ? upper : static_cast<char>(upper + ('a' - 'A'));
};
switch (keyCode) {
case KeyCode::DIK_1: return shift ? '!' : '1';
case KeyCode::DIK_2: return shift ? '@' : '2';
case KeyCode::DIK_3: return shift ? '#' : '3';
case KeyCode::DIK_4: return shift ? '$' : '4';
case KeyCode::DIK_5: return shift ? '%' : '5';
case KeyCode::DIK_6: return shift ? '^' : '6';
case KeyCode::DIK_7: return shift ? '&' : '7';
case KeyCode::DIK_8: return shift ? '*' : '8';
case KeyCode::DIK_9: return shift ? '(' : '9';
case KeyCode::DIK_0: return shift ? ')' : '0';
case KeyCode::DIK_MINUS: return shift ? '_' : '-';
case KeyCode::DIK_EQUALS: return shift ? '+' : '=';
case KeyCode::DIK_LBRACKET: return shift ? '{' : '[';
case KeyCode::DIK_RBRACKET: return shift ? '}' : ']';
case KeyCode::DIK_BACKSLASH: return shift ? '|' : '\\';
case KeyCode::DIK_SEMICOLON: return shift ? ':' : ';';
case KeyCode::DIK_APOSTROPHE:return shift ? '"' : '\'';
case KeyCode::DIK_GRAVE: return shift ? '~' : '`';
case KeyCode::DIK_COMMA: return shift ? '<' : ',';
case KeyCode::DIK_PERIOD: return shift ? '>' : '.';
case KeyCode::DIK_SLASH: return shift ? '?' : '/';
case KeyCode::DIK_SPACE: return ' ';
case KeyCode::DIK_TAB: return '\t';
case KeyCode::DIK_RETURN:return '\n';
case KeyCode::DIK_NUMPAD0: return '0';
case KeyCode::DIK_NUMPAD1: return '1';
case KeyCode::DIK_NUMPAD2: return '2';
case KeyCode::DIK_NUMPAD3: return '3';
case KeyCode::DIK_NUMPAD4: return '4';
case KeyCode::DIK_NUMPAD5: return '5';
case KeyCode::DIK_NUMPAD6: return '6';
case KeyCode::DIK_NUMPAD7: return '7';
case KeyCode::DIK_NUMPAD8: return '8';
case KeyCode::DIK_NUMPAD9: return '9';
case KeyCode::DIK_DECIMAL: return '.';
case KeyCode::DIK_ADD: return '+';
case KeyCode::DIK_SUBTRACT: return '-';
case KeyCode::DIK_MULTIPLY: return '*';
case KeyCode::DIK_DIVIDE: return '/';
case KeyCode::DIK_NUMPADCOMMA: return ',';
case KeyCode::DIK_NUMPADENTER: return '\n';
case KeyCode::DIK_NUMPADEQUALS: return '=';
case KeyCode::DIK_A: return letter('A');
case KeyCode::DIK_B: return letter('B');
case KeyCode::DIK_C: return letter('C');
case KeyCode::DIK_D: return letter('D');
case KeyCode::DIK_E: return letter('E');
case KeyCode::DIK_F: return letter('F');
case KeyCode::DIK_G: return letter('G');
case KeyCode::DIK_H: return letter('H');
case KeyCode::DIK_I: return letter('I');
case KeyCode::DIK_J: return letter('J');
case KeyCode::DIK_K: return letter('K');
case KeyCode::DIK_L: return letter('L');
case KeyCode::DIK_M: return letter('M');
case KeyCode::DIK_N: return letter('N');
case KeyCode::DIK_O: return letter('O');
case KeyCode::DIK_P: return letter('P');
case KeyCode::DIK_Q: return letter('Q');
case KeyCode::DIK_R: return letter('R');
case KeyCode::DIK_S: return letter('S');
case KeyCode::DIK_T: return letter('T');
case KeyCode::DIK_U: return letter('U');
case KeyCode::DIK_V: return letter('V');
case KeyCode::DIK_W: return letter('W');
case KeyCode::DIK_X: return letter('X');
case KeyCode::DIK_Y: return letter('Y');
case KeyCode::DIK_Z: return letter('Z');
case KeyCode::DIK_OEM_102: return shift ? '>' : '<'; // common on non-US 102-key
case KeyCode::DIK_ABNT_C1: return shift ? '?' : '/'; // brazilian
case KeyCode::DIK_ABNT_C2: return ','; // brazilian keypad dot
case KeyCode::DIK_YEN: return '\\';
case KeyCode::DIK_AT: return '@';
case KeyCode::DIK_COLON: return ':';
case KeyCode::DIK_UNDERLINE:return '_';
default:
return '\0';
}
}

155
src/game/keycode.h Normal file
View File

@ -0,0 +1,155 @@
#pragma once
#include "common.h"
// Source: https://gist.github.com/tracend/912308
namespace Game {
enum class KeyCode : uint32_t {
DIK_ESCAPE = 0x01,
DIK_1 = 0x02,
DIK_2 = 0x03,
DIK_3 = 0x04,
DIK_4 = 0x05,
DIK_5 = 0x06,
DIK_6 = 0x07,
DIK_7 = 0x08,
DIK_8 = 0x09,
DIK_9 = 0x0A,
DIK_0 = 0x0B,
DIK_MINUS = 0x0C, /* - on main keyboard */
DIK_EQUALS = 0x0D,
DIK_BACK = 0x0E, /* backspace */
DIK_TAB = 0x0F,
DIK_Q = 0x10,
DIK_W = 0x11,
DIK_E = 0x12,
DIK_R = 0x13,
DIK_T = 0x14,
DIK_Y = 0x15,
DIK_U = 0x16,
DIK_I = 0x17,
DIK_O = 0x18,
DIK_P = 0x19,
DIK_LBRACKET = 0x1A,
DIK_RBRACKET = 0x1B,
DIK_RETURN = 0x1C, /* Enter on main keyboard */
DIK_LCONTROL = 0x1D,
DIK_A = 0x1E,
DIK_S = 0x1F,
DIK_D = 0x20,
DIK_F = 0x21,
DIK_G = 0x22,
DIK_H = 0x23,
DIK_J = 0x24,
DIK_K = 0x25,
DIK_L = 0x26,
DIK_SEMICOLON = 0x27,
DIK_APOSTROPHE = 0x28,
DIK_GRAVE = 0x29, /* accent grave */
DIK_LSHIFT = 0x2A,
DIK_BACKSLASH = 0x2B,
DIK_Z = 0x2C,
DIK_X = 0x2D,
DIK_C = 0x2E,
DIK_V = 0x2F,
DIK_B = 0x30,
DIK_N = 0x31,
DIK_M = 0x32,
DIK_COMMA = 0x33,
DIK_PERIOD = 0x34, /* . on main keyboard */
DIK_SLASH = 0x35, /* / on main keyboard */
DIK_RSHIFT = 0x36,
DIK_MULTIPLY = 0x37, /* * on numeric keypad */
DIK_LMENU = 0x38, /* left Alt */
DIK_SPACE = 0x39,
DIK_CAPITAL = 0x3A,
DIK_F1 = 0x3B,
DIK_F2 = 0x3C,
DIK_F3 = 0x3D,
DIK_F4 = 0x3E,
DIK_F5 = 0x3F,
DIK_F6 = 0x40,
DIK_F7 = 0x41,
DIK_F8 = 0x42,
DIK_F9 = 0x43,
DIK_F10 = 0x44,
DIK_NUMLOCK = 0x45,
DIK_SCROLL = 0x46, /* Scroll Lock */
DIK_NUMPAD7 = 0x47,
DIK_NUMPAD8 = 0x48,
DIK_NUMPAD9 = 0x49,
DIK_SUBTRACT = 0x4A, /* - on numeric keypad */
DIK_NUMPAD4 = 0x4B,
DIK_NUMPAD5 = 0x4C,
DIK_NUMPAD6 = 0x4D,
DIK_ADD = 0x4E, /* + on numeric keypad */
DIK_NUMPAD1 = 0x4F,
DIK_NUMPAD2 = 0x50,
DIK_NUMPAD3 = 0x51,
DIK_NUMPAD0 = 0x52,
DIK_DECIMAL = 0x53, /* . on numeric keypad */
DIK_OEM_102 = 0x56, /* <> or \| on RT 102-key keyboard (Non-U.S.) */
DIK_F11 = 0x57,
DIK_F12 = 0x58,
DIK_F13 = 0x64, /* (NEC PC98) */
DIK_F14 = 0x65, /* (NEC PC98) */
DIK_F15 = 0x66, /* (NEC PC98) */
DIK_KANA = 0x70, /* (Japanese keyboard) */
DIK_ABNT_C1 = 0x73, /* /? on Brazilian keyboard */
DIK_CONVERT = 0x79, /* (Japanese keyboard) */
DIK_NOCONVERT = 0x7B, /* (Japanese keyboard) */
DIK_YEN = 0x7D, /* (Japanese keyboard) */
DIK_ABNT_C2 = 0x7E, /* Numpad . on Brazilian keyboard */
DIK_NUMPADEQUALS = 0x8D, /* = on numeric keypad (NEC PC98) */
DIK_PREVTRACK = 0x90, /* Previous Track (DIK_CIRCUMFLEX on Japanese keyboard) */
DIK_AT = 0x91, /* (NEC PC98) */
DIK_COLON = 0x92, /* (NEC PC98) */
DIK_UNDERLINE = 0x93, /* (NEC PC98) */
DIK_KANJI = 0x94, /* (Japanese keyboard) */
DIK_STOP = 0x95, /* (NEC PC98) */
DIK_AX = 0x96, /* (Japan AX) */
DIK_UNLABELED = 0x97, /* (J3100) */
DIK_NEXTTRACK = 0x99, /* Next Track */
DIK_NUMPADENTER = 0x9C, /* Enter on numeric keypad */
DIK_RCONTROL = 0x9D,
DIK_MUTE = 0xA0, /* Mute */
DIK_CALCULATOR = 0xA1, /* Calculator */
DIK_PLAYPAUSE = 0xA2, /* Play / Pause */
DIK_MEDIASTOP = 0xA4, /* Media Stop */
DIK_VOLUMEDOWN = 0xAE, /* Volume - */
DIK_VOLUMEUP = 0xB0, /* Volume + */
DIK_WEBHOME = 0xB2, /* Web home */
DIK_NUMPADCOMMA = 0xB3, /* , on numeric keypad (NEC PC98) */
DIK_DIVIDE = 0xB5, /* / on numeric keypad */
DIK_SYSRQ = 0xB7,
DIK_RMENU = 0xB8, /* right Alt */
DIK_PAUSE = 0xC5, /* Pause */
DIK_HOME = 0xC7, /* Home on arrow keypad */
DIK_UP = 0xC8, /* UpArrow on arrow keypad */
DIK_PRIOR = 0xC9, /* PgUp on arrow keypad */
DIK_LEFT = 0xCB, /* LeftArrow on arrow keypad */
DIK_RIGHT = 0xCD, /* RightArrow on arrow keypad */
DIK_END = 0xCF, /* End on arrow keypad */
DIK_DOWN = 0xD0, /* DownArrow on arrow keypad */
DIK_NEXT = 0xD1, /* PgDn on arrow keypad */
DIK_INSERT = 0xD2, /* Insert on arrow keypad */
DIK_DELETE = 0xD3, /* Delete on arrow keypad */
DIK_LWIN = 0xDB, /* Left Windows key */
DIK_RWIN = 0xDC, /* Right Windows key */
DIK_APPS = 0xDD, /* AppMenu key */
DIK_POWER = 0xDE, /* System Power */
DIK_SLEEP = 0xDF, /* System Sleep */
DIK_WAKE = 0xE3, /* System Wake */
DIK_WEBSEARCH = 0xE5, /* Web Search */
DIK_WEBFAVORITES = 0xE6, /* Web Favorites */
DIK_WEBREFRESH = 0xE7, /* Web Refresh */
DIK_WEBSTOP = 0xE8, /* Web Stop */
DIK_WEBFORWARD = 0xE9, /* Web Forward */
DIK_WEBBACK = 0xEA, /* Web Back */
DIK_MYCOMPUTER = 0xEB, /* My Computer */
DIK_MAIL = 0xEC, /* Mail */
DIK_MEDIASELECT = 0xED /* Media Select */
};
char GetCharFromKeyCode(KeyCode key, bool shift, bool capsLock);
}

13
src/game/memory.h Normal file
View File

@ -0,0 +1,13 @@
#pragma once
#include "gta2_wrapper.h"
#include "keycode.h"
namespace Game {
class Memory
{
public:
static constexpr Game* GetGame() { return (Game*)*(DWORD*)0x005eb4fc; }
static constexpr KeyCode* GetKeyPressCode() { return (KeyCode*)0x005e8f8c; }
static constexpr uint* GetKeyPressState() { return (uint*)0x005e8f90; }
};
}

View File

@ -0,0 +1,33 @@
#pragma once
#include "../core/core.h"
/*
Replaces a function call with a call to a custom function.
@param originalCallAddress The address of the original function call instruction (the CALL opcode).
@param hookFunction The address of the custom function to call instead.
*/
class FunctionCallHook : public Core::HookBase
{
public:
FunctionCallHook(const DWORD originalCallAddress, const DWORD hookFunction)
: originalCallAddress(originalCallAddress), hookFunction(hookFunction) {
}
~FunctionCallHook() = default;
static bool Hook(const FunctionCallHook& hookDef) {
DWORD OldProtection = { 0 };
bool success = VirtualProtectEx(GetCurrentProcess(), (LPVOID)hookDef.originalCallAddress, 5, PAGE_EXECUTE_READWRITE, &OldProtection);
if (!success) {
return false;
}
*(BYTE*)((LPBYTE)hookDef.originalCallAddress) = 0xE8;
DWORD offset = hookDef.hookFunction - hookDef.originalCallAddress - 5;
*(DWORD*)((LPBYTE)hookDef.originalCallAddress + 1) = offset;
return true;
}
const DWORD originalCallAddress;
const DWORD hookFunction;
};

View File

@ -0,0 +1,37 @@
#pragma once
#include "../core/core.h"
/*
Replaces multiple calls of the same function with calls to a custom function.
@param originalCallAddresses The addresses of the original function call instructions (the CALL opcodes).
@param hookFunction The address of the custom function to call instead.
*/
class FunctionMultiCallHook : public Core::HookBase
{
public:
FunctionMultiCallHook(const std::vector<DWORD>& originalCallAddresses, DWORD hookFunction)
: originalCallAddresses(originalCallAddresses), hookFunction(hookFunction) {
}
~FunctionMultiCallHook() = default;
static bool Hook(const FunctionMultiCallHook& hookDef) {
DWORD OldProtection = { 0 };
for (const auto& originalCallAddress : hookDef.originalCallAddresses) {
bool success = VirtualProtectEx(GetCurrentProcess(), (LPVOID)originalCallAddress, 5, PAGE_EXECUTE_READWRITE, &OldProtection);
if (!success) {
return false;
}
}
for (const auto& originalCallAddress : hookDef.originalCallAddresses) {
*(BYTE*)((LPBYTE)originalCallAddress) = 0xE8;
DWORD offset = hookDef.hookFunction - originalCallAddress - 5;
*(DWORD*)((LPBYTE)originalCallAddress + 1) = offset;
}
return true;
}
const std::vector<DWORD> originalCallAddresses;
const DWORD hookFunction;
};

View File

@ -0,0 +1,5 @@
#pragma once
#include "../../game/game.h"
#include "../../core/core.h"
#include "../ui/ui.h"

View File

@ -0,0 +1,70 @@
#include "menu-base.h"
#include "root.h"
bool ModMenuModule::MenuBase::Attach()
{
UiModule::Component* vertCont = nullptr;
CreateMenu(L"", vertCont);
return true;
}
void ModMenuModule::MenuBase::Detach()
{
DestroyMenu();
}
void ModMenuModule::MenuBase::CreateMenu(std::wstring title, UiModule::Component*& vertCont)
{
assert(!m_mainComponent && !m_menuController, "Menu already created");
UiModule::RootModule* uiRoot = UiModule::RootModule::GetInstance();
ModMenuOptions options = RootModule::GetInstance()->GetOptions();
UiModule::Margin* mainCont = uiRoot->AddComponent<UiModule::Margin>(nullptr, options.menuX, options.menuY);
mainCont->SetVisible(false);
m_mainComponent = static_cast<UiModule::Component*>(mainCont);
vertCont = uiRoot->AddComponent<UiModule::VertCont>(mainCont);
UiModule::Margin* titleMargin = uiRoot->AddComponent<UiModule::Margin>(vertCont, options.menuHeaderMarginX, options.menuHeaderMarginY);
UiModule::Text* titleText = uiRoot->AddComponent<UiModule::Text>(titleMargin, title, options.menuHeaderTextSize);
m_menuController = uiRoot->AddController<UiModule::MenuController>(options.menuControllerOptions);
m_menuController->SetOnActionCallback([this](UiModule::Selectable* item, UiModule::MenuItemId id) {
this->OnMenuAction(item, id);
});
}
void ModMenuModule::MenuBase::DestroyMenu()
{
UiModule::RootModule* uiRoot = UiModule::RootModule::GetInstance();
if (m_menuController) {
uiRoot->RemoveController(m_menuController);
m_menuController = nullptr;
}
if (m_mainComponent) {
uiRoot->RemoveComponent(m_mainComponent, true);
m_mainComponent = nullptr;
}
}
void ModMenuModule::MenuBase::SetVisible(bool visible)
{
m_visible = visible;
if (visible) {
OnShow();
}
if (m_menuController) {
m_menuController->SetActive(visible);
}
if (m_mainComponent) {
m_mainComponent->SetVisible(visible);
}
if (!visible) {
OnHide();
}
}

View File

@ -0,0 +1,30 @@
#pragma once
#include "common.h"
namespace ModMenuModule {
class MenuBase {
public:
virtual ~MenuBase() = default;
protected:
friend class RootModule;
MenuBase() = default;
virtual bool Attach();
virtual void Detach();
virtual void CreateMenu(std::wstring title, UiModule::Component*& vertCont) final;
virtual void DestroyMenu() final;
virtual void OnShow() {}
virtual void OnHide() {}
virtual void SetVisible(bool visible) final;
virtual bool IsVisible() const final { return m_visible; }
virtual void OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id) = 0;
UiModule::Component* m_mainComponent = nullptr;
UiModule::MenuController* m_menuController = nullptr;
bool m_visible = false;
};
}

View File

@ -0,0 +1,40 @@
#include "main-menu.h"
#include "../root.h"
#include "player-menu.h"
ModMenuModule::MainMenu::MainMenu()
{
}
ModMenuModule::MainMenu::~MainMenu()
{
}
bool ModMenuModule::MainMenu::Attach()
{
UiModule::Component* vertCont;
CreateMenu(L"#Mod Menu#", vertCont);
ModMenuModule::ModMenuOptions options = ModMenuModule::RootModule::GetInstance()->GetOptions();
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Player", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Vehicles", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Weapons", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Camera", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"World", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Misc", options.textSize);
return true;
}
void ModMenuModule::MainMenu::OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id)
{
switch (id) {
case 0: // Player
ModMenuModule::RootModule::GetInstance()->AddMenu<ModMenuModule::PlayerMenu>();
break;
default:
break;
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "../common.h"
#include "../menu-base.h"
namespace ModMenuModule {
class MainMenu : public MenuBase {
public:
MainMenu();
virtual ~MainMenu();
virtual bool Attach() override;
private:
void OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id) override;
};
}

View File

@ -0,0 +1,5 @@
#pragma once
#include "main-menu.h"
#include "player-menu.h"
#include "player-pos-menu.h"

View File

@ -0,0 +1,39 @@
#include "player-menu.h"
#include "player-pos-menu.h"
#include "../root.h"
ModMenuModule::PlayerMenu::PlayerMenu()
{
}
ModMenuModule::PlayerMenu::~PlayerMenu()
{
}
bool ModMenuModule::PlayerMenu::Attach()
{
UiModule::Component* vertCont;
CreateMenu(L"#Player#", vertCont);
ModMenuModule::ModMenuOptions options = ModMenuModule::RootModule::GetInstance()->GetOptions();
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Go back", options.textSize);
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Position", options.textSize);
return true;
}
void ModMenuModule::PlayerMenu::OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id)
{
switch (id) {
case 0: // Go back
ModMenuModule::RootModule::GetInstance()->RemoveLastMenu();
break;
case 1: // Position
ModMenuModule::RootModule::GetInstance()->AddMenu<ModMenuModule::PlayerPosMenu>();
break;
default:
break;
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "../common.h"
#include "../menu-base.h"
namespace ModMenuModule {
class PlayerMenu : public MenuBase {
public:
PlayerMenu();
virtual ~PlayerMenu();
virtual bool Attach() override;
private:
void OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id) override;
};
}

View File

@ -0,0 +1,109 @@
#include "player-pos-menu.h"
#include "../root.h"
ModMenuModule::PlayerPosMenu::PlayerPosMenu()
{
}
ModMenuModule::PlayerPosMenu::~PlayerPosMenu()
{
}
bool ModMenuModule::PlayerPosMenu::Attach()
{
UiModule::Component* vertCont;
CreateMenu(L"#Player position#", vertCont);
UiModule::RootModule* uiRoot = UiModule::RootModule::GetInstance();
ModMenuModule::ModMenuOptions options = ModMenuModule::RootModule::GetInstance()->GetOptions();
m_menuController->CreateItem<UiModule::Text>(vertCont, L"Go back", options.textSize);
Core::Resolver<Game::Sprite> spriteResolver = Core::MakeResolver(
[]() { return Game::Memory::GetGame(); },
mem(&Game::Game::players), idx(0),
mem(&Game::Player::ped),
mem(&Game::Ped::gameObject),
mem(&Game::GameObject::sprite)
);
auto onEditStop = [this]() {
if(m_visible) m_menuController->SetActive(true);
};
UiModule::Text* xText = m_menuController->CreateItem<UiModule::Text>(vertCont, L"", options.textSize);
m_xController = uiRoot->AddController<UiModule::VarTextEditableController<Game::SCR_f>>(
xText,
Core::MakeResolver(spriteResolver, mem(&Game::Sprite::x)),
UiModule::VarTextEditableControllerOptions{ L"X: #", L"#" }
);
m_xController->SetOnEditStopCallback(onEditStop);
UiModule::Text* yText = m_menuController->CreateItem<UiModule::Text>(vertCont, L"", options.textSize);
m_yController = uiRoot->AddController<UiModule::VarTextEditableController<Game::SCR_f>>(
yText,
Core::MakeResolver(spriteResolver, mem(&Game::Sprite::y)),
UiModule::VarTextEditableControllerOptions{ L"Y: #", L"#" }
);
m_yController->SetOnEditStopCallback(onEditStop);
UiModule::Text* zText = m_menuController->CreateItem<UiModule::Text>(vertCont, L"", options.textSize);
m_zController = uiRoot->AddController<UiModule::VarTextEditableController<Game::SCR_f>>(
zText,
Core::MakeResolver(spriteResolver, mem(&Game::Sprite::z)),
UiModule::VarTextEditableControllerOptions{ L"Z: #", L"#" }
);
m_zController->SetOnEditStopCallback(onEditStop);
return true;
}
void ModMenuModule::PlayerPosMenu::Detach()
{
UiModule::RootModule* uiRoot = UiModule::RootModule::GetInstance();
uiRoot->RemoveController(m_xController);
uiRoot->RemoveController(m_yController);
uiRoot->RemoveController(m_zController);
DestroyMenu();
}
void ModMenuModule::PlayerPosMenu::OnShow()
{
m_xController->SetWatching(true);
m_yController->SetWatching(true);
m_zController->SetWatching(true);
}
void ModMenuModule::PlayerPosMenu::OnHide()
{
m_xController->SetWatching(false);
m_xController->SetEditing(false);
m_yController->SetWatching(false);
m_yController->SetEditing(false);
m_zController->SetWatching(false);
m_zController->SetEditing(false);
}
void ModMenuModule::PlayerPosMenu::OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id)
{
switch (id) {
case 0: // Go back
ModMenuModule::RootModule::GetInstance()->RemoveLastMenu();
break;
case 1: // X
m_menuController->SetActive(false);
m_xController->SetEditing(true);
break;
case 2: // Y
m_menuController->SetActive(false);
m_yController->SetEditing(true);
break;
case 3: // Z
m_menuController->SetActive(false);
m_zController->SetEditing(true);
break;
default:
break;
}
}

View File

@ -0,0 +1,23 @@
#pragma once
#include "../common.h"
#include "../menu-base.h"
namespace ModMenuModule {
class PlayerPosMenu : public MenuBase {
public:
PlayerPosMenu();
virtual ~PlayerPosMenu();
virtual bool Attach() override;
virtual void Detach() override;
virtual void OnShow() override;
virtual void OnHide() override;
private:
void OnMenuAction(UiModule::Selectable* item, UiModule::MenuItemId id) override;
UiModule::VarTextEditableController<Game::SCR_f>* m_xController = nullptr;
UiModule::VarTextEditableController<Game::SCR_f>* m_yController = nullptr;
UiModule::VarTextEditableController<Game::SCR_f>* m_zController = nullptr;
};
}

View File

@ -0,0 +1,6 @@
#pragma once
#include "common.h"
#include "menu-base.h"
#include "menus/menus.h"
#include "root.h"

View File

@ -0,0 +1,82 @@
#include "root.h"
ModMenuModule::RootModule* ModMenuModule::RootModule::m_instance = nullptr;
ModMenuModule::RootModule::RootModule(ModMenuModule::ModMenuOptions options)
: m_options(options)
{
assert(UiModule::RootModule::GetInstance() != nullptr, "ModMenu::RootModule requires Ui::RootModule to be initialized first!");
assert(m_instance == nullptr, "ModMenu::RootModule instance already exists!");
m_instance = this;
spdlog::info("ModMenu::RootModule module initialized.");
}
ModMenuModule::RootModule::~RootModule()
{
m_instance = nullptr;
}
ModMenuModule::RootModule* ModMenuModule::RootModule::GetInstance()
{
return m_instance;
}
bool ModMenuModule::RootModule::Attach()
{
AddEventListener<KeyDownEvent>(&RootModule::OnKeyDown);
spdlog::info("ModMenu::RootModule module attached.");
return true;
}
void ModMenuModule::RootModule::Detach()
{
RemoveAllEventListeners();
spdlog::info("ModMenu::RootModule module detached.");
}
void ModMenuModule::RootModule::RemoveLastMenu()
{
if (m_menus.empty()) {
spdlog::warn("Attempted to remove menu from empty stack");
return;
}
m_menus.back()->Detach();
m_menus.pop_back();
if (m_menus.empty()) {
return;
}
ModMenuModule::MenuBase* menu = m_menus.back().get();
menu->Attach();
menu->SetVisible(m_visible);
}
void ModMenuModule::RootModule::ClearMenus()
{
while (!m_menus.empty()) {
m_menus.back()->Detach();
m_menus.pop_back();
}
}
void ModMenuModule::RootModule::SetVisible(bool visible)
{
if (visible == m_visible) {
return;
}
m_visible = visible;
if (m_menus.empty()) {
return;
}
m_menus.back()->SetVisible(visible);
}
void ModMenuModule::RootModule::OnKeyDown(const KeyDownEvent& event)
{
if (event.GetKeyCode() == m_options.toggleKey) {
SetVisible(!m_visible);
}
}

View File

@ -0,0 +1,61 @@
#pragma once
#include "common.h"
#include "menu-base.h"
#include "../../events/keyboard.h"
namespace ModMenuModule {
struct ModMenuOptions {
Game::KeyCode toggleKey = Game::KeyCode::DIK_F11;
Game::SCR_f menuX = 0x40000;
Game::SCR_f menuY = 0x200000;
Game::SCR_f menuHeaderMarginX = 0x60000;
Game::SCR_f menuHeaderMarginY = 0x30000;
Game::SCR_f menuHeaderTextSize = 0x6000;
Game::SCR_f textSize = 0x6000;
UiModule::MenuControllerOptions menuControllerOptions = {};
};
class RootModule : public Core::ModuleBase {
public:
RootModule(ModMenuOptions options = {});
virtual ~RootModule() override;
static RootModule* GetInstance();
bool Attach();
void Detach();
template<typename MenuT, typename... Args>
MenuT* AddMenu(Args&&... args) {
static_assert(std::is_base_of_v<MenuBase, MenuT>, "MenuT must be derived from MenuBase");
MenuT* newMenu = new MenuT(std::forward<Args>(args)...);
if (!newMenu->Attach()) {
spdlog::error("Failed to attach mod menu {}", typeid(MenuT).name());
delete newMenu;
return nullptr;
}
if (m_visible) {
newMenu->SetVisible(true);
}
if (!m_menus.empty()) {
m_menus.back()->Detach();
}
m_menus.emplace_back(std::unique_ptr<MenuBase>(newMenu));
return newMenu;
}
void RemoveLastMenu();
void ClearMenus();
void SetVisible(bool visible);
ModMenuOptions GetOptions() const { return m_options; }
private:
void OnKeyDown(const KeyDownEvent& event);
static RootModule* m_instance;
ModMenuOptions m_options;
std::vector<std::unique_ptr<MenuBase>> m_menus;
bool m_visible = false;
};
}

8
src/modules/ui/common.h Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include "../../game/game.h"
#include "../../core/core.h"
#include <string>
#include <vector>
#include <cfenv>
#include "spdlog/spdlog.h"

View File

@ -0,0 +1,102 @@
#include "component.h"
UiModule::Component::~Component()
{
while(!m_children.empty()) {
RemoveChild(m_children.back());
}
if(m_parent) {
m_parent->RemoveChild(this);
}
}
void UiModule::Component::DrawIfVisible()
{
if (!m_visibleInherited) {
return;
}
Draw();
}
void UiModule::Component::AddChild(Component* child)
{
if (!child) {
spdlog::error("Attempted to add null child to component");
return;
}
child->m_parent = this;
m_children.push_back(child);
UpdateChildrenPos();
child->UpdateChildrenVisibility();
}
void UiModule::Component::RemoveChild(Component* child)
{
if (!child) {
spdlog::error("Attempted to remove null child from component");
return;
}
auto it = std::find(m_children.begin(), m_children.end(), child);
if (it == m_children.end()) {
spdlog::error("Attempted to remove a child that is not a child of this component");
return;
}
m_children.erase(it);
child->m_parent = nullptr;
child->SetPosition(0, 0);
UpdateChildrenPos();
child->UpdateChildrenVisibility();
}
void UiModule::Component::UpdateChildrenPos()
{
for (auto child : m_children) {
child->SetPosition(m_rect.x, m_rect.y);
}
}
void UiModule::Component::SetPosition(Game::SCR_f x, Game::SCR_f y)
{
m_rect.x = x;
m_rect.y = y;
UpdateChildrenPos();
}
void UiModule::Component::OnChildSizeUpdate(Component* child)
{
Rect prevRect = m_rect;
UpdateChildrenPos();
if (m_parent && (m_rect.width != prevRect.width || m_rect.height != prevRect.height)) {
m_parent->OnChildSizeUpdate(this);
}
}
void UiModule::Component::SetVisible(bool visible)
{
if (m_visible == visible) {
return;
}
m_visible = visible;
UpdateChildrenVisibility();
}
void UiModule::Component::UpdateSize(Game::SCR_f* width, Game::SCR_f* height)
{
Rect prevRect = m_rect;
if(width) m_rect.width = *width;
if(height) m_rect.height = *height;
if (m_parent && (m_rect.width != prevRect.width || m_rect.height != prevRect.height)) {
m_parent->OnChildSizeUpdate(this);
}
}
void UiModule::Component::UpdateChildrenVisibility()
{
bool parentVisible = m_parent ? m_parent->m_visibleInherited : true;
m_visibleInherited = m_visible && parentVisible;
for (auto child : m_children) {
child->UpdateChildrenVisibility();
}
}

View File

@ -0,0 +1,32 @@
#pragma once
#include "common.h"
#include "rect.h"
namespace UiModule {
class Component {
public:
virtual ~Component();
virtual void DrawIfVisible();
virtual void Draw() {}
virtual void AddChild(Component* child);
virtual void RemoveChild(Component* child);
virtual void UpdateChildrenPos();
void SetPosition(Game::SCR_f x, Game::SCR_f y);
Rect GetRect() const { return m_rect; }
Component* GetParent() const { return m_parent; }
std::vector<Component*> GetChildren() const { return m_children; }
void OnChildSizeUpdate(Component* child);
void SetVisible(bool visible);
bool IsVisible() const { return m_visibleInherited; }
protected:
void UpdateSize(Game::SCR_f* width, Game::SCR_f* height);
void UpdateChildrenVisibility();
Component* m_parent = nullptr;
std::vector<Component*> m_children = {};
Rect m_rect = { 0, 0, 0, 0 };
bool m_visible = true;
bool m_visibleInherited = true;
};
}

View File

@ -0,0 +1,8 @@
#pragma once
#include "./text.h"
#include "./vertcont.h"
#include "./horcont.h"
#include "./spacer.h"
#include "./margin.h"
#include "./selectable.h"

View File

@ -0,0 +1,28 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
class HorCont : public Component {
public:
HorCont(Component* parent) {
if (parent) {
parent->AddChild(this);
}
}
virtual void UpdateChildrenPos() override {
Game::SCR_f currentX = m_rect.x;
Game::SCR_f maxHeight = 0;
for (auto child : m_children) {
child->SetPosition(currentX, m_rect.y);
Rect childRect = child->GetRect();
currentX += childRect.width;
maxHeight = max(maxHeight, childRect.height);
}
Game::SCR_f newWidth = currentX - m_rect.x;
UpdateSize(&newWidth, &maxHeight);
}
};
}

View File

@ -0,0 +1,47 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
class Margin : public Component {
public:
Margin(Component* parent, Game::SCR_f marginX, Game::SCR_f marginY) {
m_marginX = marginX;
m_marginY = marginY;
m_rect.width = marginX*2;
m_rect.height = marginY*2;
if (parent) {
parent->AddChild(this);
}
}
virtual void UpdateChildrenPos() override {
Game::SCR_f maxWidth = 0;
Game::SCR_f maxHeight = 0;
for (auto child : m_children) {
child->SetPosition(m_rect.x + m_marginX, m_rect.y + m_marginY);
Rect childRect = child->GetRect();
maxWidth = max(maxWidth, childRect.width);
maxHeight = max(maxHeight, childRect.height);
}
Game::SCR_f newWidth = maxWidth + m_marginX * 2;
Game::SCR_f newHeight = maxHeight + m_marginY * 2;
UpdateSize(&newWidth, &newHeight);
}
void SetMargin(Game::SCR_f marginX, Game::SCR_f marginY) {
m_marginX = marginX;
m_marginY = marginY;
UpdateChildrenPos();
}
Game::SCR_f GetMarginX() const { return m_marginX; }
Game::SCR_f GetMarginY() const { return m_marginY; }
protected:
Game::SCR_f m_marginX = 0;
Game::SCR_f m_marginY = 0;
};
}

View File

@ -0,0 +1,47 @@
#include "selectable.h"
UiModule::Selectable::Selectable(Component* parent, SelectableOptions options)
{
m_options = options;
m_rect.width = options.markerOffsetX;
m_rect.height = options.markerScale * 20;
if (parent) {
parent->AddChild(this);
}
}
void UiModule::Selectable::Draw()
{
if(!m_options.selected) {
return;
}
Game::S4_ENUM1 s4enum2 = (Game::S4_ENUM1)2;
Game::Functions::DrawGTAText(
(WCHAR*)m_options.markerText.c_str(),
m_rect.x,
m_rect.y + m_options.markerAdditionalOffsetY,
1,
m_options.markerScale,
&s4enum2,
0,
Game::SPRITE_INVISIBILITY_VISIBLE,
0
);
}
void UiModule::Selectable::UpdateChildrenPos()
{
Game::SCR_f maxWidth = m_options.markerOffsetX;
Game::SCR_f maxHeight = 0;
for (auto child : m_children) {
child->SetPosition(m_rect.x + m_options.markerOffsetX, m_rect.y);
Rect childRect = child->GetRect();
maxWidth = max(maxWidth, m_options.markerOffsetX + childRect.width);
maxHeight = max(maxHeight, childRect.height);
}
UpdateSize(&maxWidth, &maxHeight);
}

View File

@ -0,0 +1,29 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
struct SelectableOptions {
bool selected = false;
Game::SCR_f markerScale = 0x6000;
Game::SCR_f markerOffsetX = 0x60000;
Game::SCR_f markerAdditionalOffsetY = 0x18000; /* doesn't affect component size */
std::wstring markerText = L"*";
};
class Selectable : public Component {
public:
Selectable(Component* parent, SelectableOptions options = {});
virtual void Draw() override;
virtual void UpdateChildrenPos() override;
void SetSelected(bool selected) {
m_options.selected = selected;
}
bool IsSelected() const { return m_options.selected; }
protected:
SelectableOptions m_options;
};
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
class Spacer : public Component {
public:
Spacer(Component* parent, Game::SCR_f width, Game::SCR_f height) {
m_rect.width = width;
m_rect.height = height;
if (parent) {
parent->AddChild(this);
}
}
void SetSize(Game::SCR_f width, Game::SCR_f height) {
UpdateSize(&width, &height);
}
Game::SCR_f GetWidth() const { return m_rect.width; }
Game::SCR_f GetHeight() const { return m_rect.height; }
};
}

View File

@ -0,0 +1,43 @@
#include "text.h"
UiModule::Text::Text(Component* parent, const std::wstring& text, Game::SCR_f scale)
{
m_text = text;
m_scale = scale;
m_rect.width = 0; /// TODO
m_rect.height = scale * 20;
if (parent) {
parent->AddChild(this);
}
}
void UiModule::Text::Draw()
{
Game::S4_ENUM1 s4enum2 = (Game::S4_ENUM1)2;
Game::Functions::DrawGTAText(
(WCHAR*)m_text.c_str(),
m_rect.x,
m_rect.y,
1,
m_scale,
&s4enum2,
1,
Game::SPRITE_INVISIBILITY_VISIBLE,
1
);
}
void UiModule::Text::SetText(const std::wstring& text)
{
m_text = text;
/// TODO: update width
}
void UiModule::Text::SetScale(Game::SCR_f scale)
{
m_scale = scale;
Game::SCR_f newHeight = scale * 20;
UpdateSize(nullptr, &newHeight);
}

View File

@ -0,0 +1,20 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
class Text : public Component {
public:
Text(Component* parent, const std::wstring& text, Game::SCR_f scale = 0x6000);
virtual void Draw() override;
void SetText(const std::wstring& text);
const std::wstring& GetText() const { return m_text; }
void SetScale(Game::SCR_f scale);
Game::SCR_f GetScale() const { return m_scale; }
protected:
std::wstring m_text;
Game::SCR_f m_scale;
};
}

View File

@ -0,0 +1,28 @@
#pragma once
#include "../common.h"
#include "../component.h"
namespace UiModule {
class VertCont : public Component {
public:
VertCont(Component* parent) {
if (parent) {
parent->AddChild(this);
}
}
virtual void UpdateChildrenPos() override {
Game::SCR_f currentY = m_rect.y;
Game::SCR_f maxWidth = 0;
for (auto child : m_children) {
child->SetPosition(m_rect.x, currentY);
Rect childRect = child->GetRect();
currentY += childRect.height;
maxWidth = max(maxWidth, childRect.width);
}
Game::SCR_f newHeight = currentY - m_rect.y;
UpdateSize(&maxWidth, &newHeight);
}
};
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "common.h"
namespace UiModule
{
class Controller
{
public:
virtual ~Controller() = default;
protected:
Controller() = default;
};
}

View File

@ -0,0 +1,5 @@
#pragma once
#include "menu.h"
#include "var-text.h"
#include "var-text-editable.h"

View File

@ -0,0 +1,131 @@
#include "menu.h"
#include "var-text.h"
UiModule::MenuController::MenuController(MenuControllerOptions options)
{
m_options = options;
}
UiModule::MenuController::~MenuController()
{
}
UiModule::MenuItemId UiModule::MenuController::AddItem(Selectable* item)
{
if (!item) {
spdlog::error("Attempted to add null item to MenuController");
return -1;
}
m_items.push_back(item);
if (m_items.size() == 1) {
SetIndex(0);
}
return m_items.size() - 1;
}
std::vector<UiModule::MenuItemId> UiModule::MenuController::AddItems(const std::vector<Selectable*>& items)
{
std::vector<UiModule::MenuItemId> ids;
for (auto* item : items) {
ids.push_back(AddItem(item));
}
return ids;
}
void UiModule::MenuController::RemoveItem(MenuItemId id)
{
if (id < 0 || id >= m_items.size()) {
spdlog::error("Attempted to remove invalid item from MenuController");
return;
}
m_items.erase(m_items.begin() + id);
MenuItemId index = m_currentIndex;
if (m_currentIndex >= id && m_currentIndex > 0) {
index = m_currentIndex - 1;
}
SetIndex(index);
}
void UiModule::MenuController::SetActive(bool active)
{
if(active == m_active) {
return;
}
m_active = active;
if (active) {
AddEventListener<KeyDownEvent>(&MenuController::OnKeyDown);
} else {
RemoveEventListener<KeyDownEvent>();
}
if (m_options.hideMarkerNonActive) {
SetIndex(m_currentIndex);
}
}
void UiModule::MenuController::SetIndex(MenuItemId index)
{
if (index < 0 || index >= m_items.size()) {
spdlog::error("Attempted to set invalid index in MenuController");
return;
}
m_currentIndex = index;
bool hideIndex = !m_active && m_options.hideMarkerNonActive;
for (size_t i = 0; i < m_items.size(); ++i) {
m_items[i]->SetSelected(i == m_currentIndex && !hideIndex);
}
}
void UiModule::MenuController::Next()
{
if(!m_options.loop && m_currentIndex + 1 >= m_items.size()) {
return;
}
if(m_items.empty()) {
return;
}
SetIndex((m_currentIndex + 1) % m_items.size());
}
void UiModule::MenuController::Previous()
{
if(!m_options.loop && m_currentIndex == 0) {
return;
}
if(m_items.empty()) {
return;
}
SetIndex((m_currentIndex - 1 + m_items.size()) % m_items.size());
}
void UiModule::MenuController::Action()
{
if (m_onAction && m_currentIndex >= 0 && m_currentIndex < m_items.size()) {
m_onAction(m_items[m_currentIndex], m_currentIndex);
}
}
void UiModule::MenuController::OnKeyDown(const KeyDownEvent& event)
{
if (!m_active) {
return;
}
Game::KeyCode key = event.GetKeyCode();
if( key == m_options.keyPrev ) {
Previous();
} else if( key == m_options.keyNext ) {
Next();
} else if( key == m_options.keyAction ) {
Action();
}
}

View File

@ -0,0 +1,60 @@
#pragma once
#include "../common.h"
#include "../controller.h"
#include "../components/selectable.h"
#include "../root.h"
#include "../../../events/keyboard.h"
namespace UiModule {
using MenuItemId = size_t;
using MenuActionCallback = std::function<void(Selectable*, MenuItemId)>;
struct MenuControllerOptions {
bool loop = true;
bool hideMarkerNonActive = false;
Game::KeyCode keyPrev = Game::KeyCode::DIK_LBRACKET;
Game::KeyCode keyNext = Game::KeyCode::DIK_RBRACKET;
Game::KeyCode keyAction = Game::KeyCode::DIK_BACKSLASH;
SelectableOptions createdSelectableOptions = {};
};
class MenuController : public Controller, public Core::EventListenerSupport {
public:
MenuController(MenuControllerOptions options = {});
virtual ~MenuController();
template<typename ComponentT, typename... Args>
ComponentT* CreateItem(Component* parent, Args&&... otherArgs) {
static_assert(std::is_base_of_v<Component, ComponentT>, "ComponentT must be derived from Component");
Selectable* selectable = RootModule::GetInstance()->AddComponent<Selectable>(parent, m_options.createdSelectableOptions);
ComponentT* item = RootModule::GetInstance()->AddComponent<ComponentT>(selectable, std::forward<Args>(otherArgs)...);
AddItem(selectable);
return item;
}
MenuItemId AddItem(Selectable* item);
std::vector<MenuItemId> AddItems(const std::vector<Selectable*>& items);
void RemoveItem(MenuItemId id);
void SetActive(bool active);
bool IsActive() const { return m_active; }
void SetIndex(MenuItemId index);
MenuItemId GetIndex() const { return m_currentIndex; }
void Next();
void Previous();
void Action();
void SetOnActionCallback(MenuActionCallback callback) { m_onAction = callback; }
protected:
void OnKeyDown(const KeyDownEvent& event);
MenuControllerOptions m_options;
std::vector<Selectable*> m_items = {};
MenuItemId m_currentIndex = 0;
bool m_active = false;
MenuActionCallback m_onAction = nullptr;
};
}

View File

@ -0,0 +1,243 @@
#pragma once
#include "../common.h"
#include "../controller.h"
#include "../components/text.h"
#include "../utils/string-utils.h"
#include "../../../events/draw.h"
#include "../../../events/keyboard.h"
namespace UiModule {
using VarTextEditableEditStopCallback = std::function<void()>;
struct VarTextEditableControllerOptions {
std::wstring prefix = L"";
std::wstring suffix = L"";
std::wstring nullText = L"N/A";
std::wstring marker = L"-";
Game::KeyCode keyAction = Game::KeyCode::DIK_BACKSLASH;
bool allowControlChars = false; // whether to allow \t, \n, \r
int blinkInterval = 15; // frames
bool removeTrailingZeros = true; // for floating point types
};
template <typename T>
class VarTextEditableController : public Controller, public Core::EventListenerSupport {
public:
VarTextEditableController(Text* text, Core::Resolver<T> resolver, VarTextEditableControllerOptions options = {}) {
static_assert(std::is_copy_constructible<T>::value, "T must be copy-constructible");
m_textComponent = text;
m_options = options;
m_resolver = resolver;
Update();
}
virtual ~VarTextEditableController() {
SetWatching(false);
}
void SetWatching(bool watching) {
m_watchingBeforeEdit = watching;
if (m_editing && watching) return;
if (m_watching == watching) return;
m_watching = watching;
if (watching) {
m_watched = Core::WatchManager::GetInstance()->Watch<PreDrawUIEvent, T>(
m_resolver,
this,
&VarTextEditableController<T>::OnValueUpdate
);
}
else {
Core::WatchManager::GetInstance()->Unwatch(m_watched);
m_watched = nullptr;
}
if(!m_editing) Update();
}
bool IsWatching() const { return m_watching; }
void SetActive(bool active) {
m_activeBeforeEdit = active;
if (active == m_active) return;
m_active = active;
if (active) {
AddEventListener<KeyDownEvent>(&VarTextEditableController<T>::OnKeyDown);
if (m_editing) SetPreDrawUIListener(true);
}
else {
RemoveEventListener<KeyDownEvent>();
SetPreDrawUIListener(false);
}
}
void SetPreDrawUIListener(bool enable) {
if (enable == m_hasPreDrawListener) return;
m_hasPreDrawListener = enable;
if (enable) {
AddEventListener<PreDrawUIEvent>(&VarTextEditableController<T>::OnPreDrawUI);
m_blinkCounter = 0;
}
else {
RemoveEventListener<PreDrawUIEvent>();
}
}
void SetEditing(bool editing) {
if (m_editing == editing) return;
m_editing = editing;
if (editing) {
if (!Load()) {
m_editing = false;
if (m_onEditStop) m_onEditStop();
return;
}
bool watching = m_watching;
bool active = m_active;
SetWatching(false);
SetActive(true);
m_watchingBeforeEdit = watching;
m_activeBeforeEdit = active;
}
else {
if (m_watchingBeforeEdit) {
SetWatching(true);
}
if (!m_activeBeforeEdit) {
SetActive(false);
}
SetPreDrawUIListener(false);
m_editBuffer = L"";
Update();
if (m_onEditStop) m_onEditStop();
}
}
void SetOnEditStopCallback(VarTextEditableEditStopCallback callback) {
m_onEditStop = callback;
}
bool IsEditing() const { return m_editing; }
void Update() {
T* resolved = m_resolver();
std::optional<T> value = resolved ? std::optional<T>(*resolved) : std::nullopt;
OnValueUpdate(std::nullopt, value);
}
protected:
bool Load() {
if (!m_editing) return false;
T* resolved = m_resolver();
if (resolved == nullptr) {
spdlog::warn("Failed to resolve variable for editing");
return false;
}
m_editBuffer = ToString<T>(*resolved);
if ((std::is_same_v<T, Game::SCR_f> || std::is_floating_point_v<T>) && m_options.removeTrailingZeros) {
RemoveTrailingZeros(m_editBuffer);
}
return true;
}
void Save() {
if (!m_editing) return;
try {
T newValue = FromString<T>(m_editBuffer);
T* resolved = m_resolver();
if (resolved) {
*resolved = newValue;
}
else {
spdlog::warn("Failed to resolve variable for setting new value");
}
}
catch (const std::exception& e) {
spdlog::warn("Failed to parse input '{}': {}", std::string(m_editBuffer.begin(), m_editBuffer.end()), e.what());
}
}
void SetValueText(std::wstring text) {
std::wstring marker = m_editing && (m_blinkCounter < m_options.blinkInterval) ? m_options.marker : L"";
m_textComponent->SetText(m_options.prefix + text + marker + m_options.suffix);
}
void OnValueUpdate(std::optional<T> oldValue, std::optional<T> newValue) {
if (m_editing) {
spdlog::warn("Tried to update VarTextEditableController value while editing");
return;
}
if (newValue.has_value()) {
std::wstring text = ToString<T>(newValue.value());
if ((std::is_same_v<T, Game::SCR_f> || std::is_floating_point_v<T>) && m_options.removeTrailingZeros) {
UiModule::RemoveTrailingZeros(text);
}
SetValueText(text);
}
else {
SetValueText(m_options.nullText);
}
}
void OnKeyDown(const KeyDownEvent& event) {
if (!m_active) return;
Game::KeyCode key = event.GetKeyCode();
bool isShiftPressed = event.IsShiftPressed();
if (!m_editing) {
if (key == m_options.keyAction) {
SetEditing(true);
}
return;
}
if (key == m_options.keyAction) {
Save();
SetEditing(!m_editing);
return;
} else if (key == Game::KeyCode::DIK_BACK) {
if (!m_editBuffer.empty()) {
m_editBuffer.pop_back();
}
SetValueText(m_editBuffer);
} else {
char c = Game::GetCharFromKeyCode(key, isShiftPressed, false); /// TODO handle caps lock
if (c == '\0') return;
if (!IsValidCharForType<T>(c, m_options.allowControlChars)) return;
m_editBuffer += c;
SetValueText(m_editBuffer);
}
}
void OnPreDrawUI(const PreDrawUIEvent& event) {
if (!m_editing) return;
if (m_blinkCounter == 0 || m_blinkCounter == m_options.blinkInterval) {
SetValueText(m_editBuffer);
}
m_blinkCounter = (m_blinkCounter + 1) % (m_options.blinkInterval * 2);
}
VarTextEditableControllerOptions m_options;
Text* m_textComponent = nullptr;
Core::Resolver<T> m_resolver = nullptr;
Core::Watched<T>* m_watched = nullptr;
bool m_watching = false;
bool m_editing = false;
bool m_active = false;
bool m_watchingBeforeEdit = false;
bool m_activeBeforeEdit = false;
bool m_hasPreDrawListener = false;
std::wstring m_editBuffer = L"";
VarTextEditableEditStopCallback m_onEditStop = nullptr;
int m_blinkCounter = 0;
};
}

View File

@ -0,0 +1,82 @@
#pragma once
#include "../common.h"
#include "../controller.h"
#include "../components/text.h"
#include "../utils/string-utils.h"
#include "../../../events/draw.h"
namespace UiModule {
struct VarTextControllerOptions {
std::wstring prefix = L"";
std::wstring suffix = L"";
std::wstring nullText = L"N/A";
bool removeTrailingZeros = true; // for floating point types
};
template <typename T>
class VarTextController : public Controller, public Core::EventListenerSupport {
public:
VarTextController(Text* text, Core::Resolver<T> resolver, VarTextControllerOptions options = {}) {
static_assert(std::is_copy_constructible<T>::value, "T must be copy-constructible");
m_textComponent = text;
m_options = options;
m_resolver = resolver;
Update();
}
virtual ~VarTextController() {
SetWatching(false);
}
void SetWatching(bool watching) {
if (m_watching == watching) return;
m_watching = watching;
if (m_watching) {
m_watched = Core::WatchManager::GetInstance()->Watch<PreDrawUIEvent, T>(
m_resolver,
this,
&VarTextController<T>::OnValueUpdate
);
}
else {
Core::WatchManager::GetInstance()->Unwatch(m_watched);
m_watched = nullptr;
}
Update();
}
bool IsWatching() const { return m_watching; }
void Update() {
T* resolved = m_resolver();
std::optional<T> value = resolved ? std::optional<T>(*resolved) : std::nullopt;
OnValueUpdate(std::nullopt, value);
}
protected:
void SetValueText(std::wstring text) {
m_textComponent->SetText(m_options.prefix + text + m_options.suffix);
}
void OnValueUpdate(std::optional<T> oldValue, std::optional<T> newValue) {
if (newValue.has_value()) {
std::wstring text = ToString<T>(newValue.value());
if ((std::is_same_v<T, Game::SCR_f> || std::is_floating_point_v<T>) && m_options.removeTrailingZeros) {
UiModule::RemoveTrailingZeros(text);
}
SetValueText(text);
}
else {
SetValueText(m_options.nullText);
}
}
VarTextControllerOptions m_options;
Text* m_textComponent = nullptr;
Core::Resolver<T> m_resolver = nullptr;
Core::Watched<T>* m_watched = nullptr;
bool m_watching = false;
};
}

11
src/modules/ui/rect.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "common.h"
namespace UiModule {
struct Rect {
Game::SCR_f x;
Game::SCR_f y;
Game::SCR_f width;
Game::SCR_f height;
};
}

42
src/modules/ui/root.cpp Normal file
View File

@ -0,0 +1,42 @@
#include "root.h"
UiModule::RootModule* UiModule::RootModule::m_instance = nullptr;
UiModule::RootModule::RootModule()
{
assert(m_instance == nullptr, "UiModule::RootModule instance already exists!");
m_instance = this;
spdlog::info("UiModule::RootModule instance created");
}
UiModule::RootModule::~RootModule()
{
ClearControllers();
ClearComponents();
m_instance = nullptr;
}
UiModule::RootModule* UiModule::RootModule::GetInstance()
{
return m_instance;
}
bool UiModule::RootModule::Attach()
{
AddEventListener<DrawUIEvent>(&UiModule::RootModule::OnDraw);
spdlog::info("UiModule::RootModule attached");
return true;
}
void UiModule::RootModule::Detach()
{
RemoveAllEventListeners();
spdlog::info("UiModule::RootModule detached");
}
void UiModule::RootModule::OnDraw(const DrawUIEvent& event)
{
for (const auto& component : m_components) {
component->DrawIfVisible();
}
}

97
src/modules/ui/root.h Normal file
View File

@ -0,0 +1,97 @@
#pragma once
#include "common.h"
#include "../../events/draw.h"
#include "component.h"
#include "controller.h"
namespace UiModule {
class RootModule : public Core::ModuleBase {
public:
RootModule();
virtual ~RootModule() override;
static RootModule* GetInstance();
bool Attach();
void Detach();
template<typename ComponentT, typename... Args>
ComponentT* AddComponent(Args&&... args) {
static_assert(std::is_base_of_v<Component, ComponentT>, "ComponentT must be derived from Component");
ComponentT* component = new ComponentT(std::forward<Args>(args)...);
m_components.emplace_back(std::unique_ptr<Component>(component));
return component;
}
template<typename ComponentT>
void RemoveComponent(ComponentT* component, bool removeChildren = false) {
static_assert(std::is_base_of_v<Component, ComponentT>, "ComponentT must be derived from Component");
auto it = std::find_if(m_components.begin(), m_components.end(), [component](const std::unique_ptr<Component>& ptr) {
return ptr.get() == component;
});
if (it == m_components.end()) {
spdlog::error("Attempted to remove a component that is not managed by RootModule");
return;
}
if (removeChildren) {
std::vector<Component*> children = component->GetChildren();
for (Component* child : children) {
RemoveComponent(child, true);
}
}
if (it->get() != component) {
it = std::find_if(m_components.begin(), m_components.end(), [component](const std::unique_ptr<Component>& ptr) {
return ptr.get() == component;
});
}
if (it != m_components.end()) {
m_components.erase(it);
}
else {
spdlog::error("Attempted to remove a component that, after removing children, is not managed by RootModule anymore");
}
}
void ClearComponents() {
while (!m_components.empty()) {
m_components.pop_back();
}
}
template<typename ControllerT, typename... Args>
ControllerT* AddController(Args&&... args) {
static_assert(std::is_base_of_v<Controller, ControllerT>, "ControllerT must be derived from Controller");
ControllerT* controller = new ControllerT(std::forward<Args>(args)...);
m_controllers.emplace_back(std::unique_ptr<Controller>(controller));
return controller;
}
template<typename ControllerT>
void RemoveController(ControllerT* controller) {
static_assert(std::is_base_of_v<Controller, ControllerT>, "ControllerT must be derived from Controller");
auto it = std::find_if(m_controllers.begin(), m_controllers.end(), [controller](const std::unique_ptr<Controller>& ptr) {
return ptr.get() == controller;
});
if (it != m_controllers.end()) {
m_controllers.erase(it);
}
else {
spdlog::error("Attempted to remove a controller that is not managed by RootModule");
}
}
void ClearControllers() {
while (!m_controllers.empty()) {
m_controllers.erase(m_controllers.begin());
}
}
private:
void OnDraw(const DrawUIEvent& event);
std::vector<std::unique_ptr<Component>> m_components = {};
std::vector<std::unique_ptr<Controller>> m_controllers = {};
static RootModule* m_instance;
};
}

9
src/modules/ui/ui.h Normal file
View File

@ -0,0 +1,9 @@
#pragma once
#include "common.h"
#include "rect.h"
#include "root.h"
#include "component.h"
#include "components/components.h"
#include "controller.h"
#include "controllers/controllers.h"

View File

@ -0,0 +1,84 @@
#pragma once
#include "../common.h"
template <typename T>
inline constexpr bool always_false = false;
namespace UiModule {
// Removes trailing zeros from a floating-point string representation.
inline void RemoveTrailingZeros(std::wstring& str) {
if (str.empty()) return;
auto dotPos = str.find(L'.');
if (dotPos == std::wstring::npos) return;
while (!str.empty() && str.back() == L'0') {
str.pop_back();
}
if (!str.empty() && str.back() == L'.') {
str.pop_back();
}
}
// Converts various types to their string representation.
template <typename T>
inline std::wstring ToString(T value) {
std::fesetround(FE_TONEAREST);
if constexpr (std::is_same_v<T, Game::SCR_f>) {
return ToString(static_cast<float>(value) / 16384.0f);
}
return std::to_wstring(value);
}
// Converts a string representation back to the specified type.
template <typename T>
inline T FromString(const std::wstring& str) {
using BaseT = std::remove_cv_t<T>;
if constexpr (std::is_same_v<BaseT, std::wstring>) {
return str;
}
if constexpr (std::is_floating_point_v<BaseT>) {
return static_cast<BaseT>(std::stof(str));
}
if constexpr (std::is_integral_v<BaseT>) {
return static_cast<BaseT>(std::stoll(str));
}
static_assert(always_false<T>, "FromString is not implemented for this type");
return T{};
}
// Explicit specialization for SCR_f
template <>
inline Game::SCR_f FromString<Game::SCR_f>(const std::wstring& str) {
float f = std::stof(str);
return static_cast<Game::SCR_f>(f * 16384.0f);
}
// Checks if a character is valid for the specified type.
template <typename T>
inline bool IsValidCharForType(char c, bool allowControlChars = false) {
using BaseT = std::remove_cv_t<T>;
if constexpr (std::is_same_v<BaseT, std::wstring>) {
if (std::isprint(static_cast<unsigned char>(c))) {
return true;
}
if (allowControlChars) {
return c == '\t' || c == '\n' || c == '\r';
}
return false;
}
if constexpr (std::is_floating_point_v<BaseT>) {
return std::isdigit(c) || (c == '-') || (c == '.');
}
if constexpr (std::is_integral_v<BaseT>) {
return std::isdigit(c) || (c == '-');
}
static_assert(always_false<T>, "IsValidCharForType is not implemented for this type. Type name: " __FUNCSIG__);
return false;
}
// Explicit specialization for SCR_f
template <>
inline bool IsValidCharForType<Game::SCR_f>(char c, bool /*allowControlChars*/) {
return std::isdigit(c) || (c == '-') || (c == '.');
}
}