From 96d5f5c7626fe7cdf21da62823fe97ec38cc64f4 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 02:38:38 +0100 Subject: [PATCH 01/18] Add Graphics settings --- Source/HarmonyLink/Private/HarmonyLink.cpp | 5 + .../Private/Objects/HarmonyLinkGraphics.cpp | 177 ++++++++++++++++++ .../Private/Structs/HLConfigValue.cpp | 3 + Source/HarmonyLink/Public/HarmonyLink.h | 2 + .../Public/Objects/HarmonyLinkGraphics.h | 70 +++++++ .../Public/Structs/HLConfigValue.h | 85 +++++++++ 6 files changed, 342 insertions(+) create mode 100644 Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp create mode 100644 Source/HarmonyLink/Private/Structs/HLConfigValue.cpp create mode 100644 Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h create mode 100644 Source/HarmonyLink/Public/Structs/HLConfigValue.h diff --git a/Source/HarmonyLink/Private/HarmonyLink.cpp b/Source/HarmonyLink/Private/HarmonyLink.cpp index 88e7005..b508013 100644 --- a/Source/HarmonyLink/Private/HarmonyLink.cpp +++ b/Source/HarmonyLink/Private/HarmonyLink.cpp @@ -2,15 +2,20 @@ #include "HarmonyLink.h" #include "Modules/ModuleManager.h" +#include "Objects/HarmonyLinkGraphics.h" #define LOCTEXT_NAMESPACE "FHarmonyLinkModule" +DEFINE_LOG_CATEGORY(LogHarmonyLink); + void FHarmonyLinkModule::StartupModule() { + UHarmonyLinkGraphics::GetSettings(); } void FHarmonyLinkModule::ShutdownModule() { + UHarmonyLinkGraphics::DestroySettings(); } #undef LOCTEXT_NAMESPACE diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp new file mode 100644 index 0000000..7268be3 --- /dev/null +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -0,0 +1,177 @@ +// Copyright (C) 2024 Jordon Brooks + + +#include "Objects/HarmonyLinkGraphics.h" +#include "ComponentRecreateRenderStateContext.h" +#include "HarmonyLink.h" + +UHarmonyLinkGraphics* UHarmonyLinkGraphics::Instance = nullptr; +FString UHarmonyLinkGraphics::IniLocation = "HarmonyLink"; +FName UHarmonyLinkGraphics::BatteryProfile = "Battery"; +FName UHarmonyLinkGraphics::ChargingProfile = "Charging"; +FName UHarmonyLinkGraphics::DockedProfile = "Docked"; + +TMap UHarmonyLinkGraphics::DefaultSettingsMap = {{"Test", 0}}; + +UHarmonyLinkGraphics::UHarmonyLinkGraphics() +{ + UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); + + + // Initialize settings for Low Graphics Profile + BatterySettings.Add(TEXT("LowResolutionX"), FHLConfigValue(1280)); + BatterySettings.Add(TEXT("LowResolutionY"), FHLConfigValue(720)); + BatterySettings.Add(TEXT("LowTextureQuality"), FHLConfigValue(0.5f)); + + // Initialize settings for Medium Graphics Profile + ChargingSettings.Add(TEXT("MediumResolutionX"), FHLConfigValue(1920)); + ChargingSettings.Add(TEXT("MediumResolutionY"), FHLConfigValue(1080)); + ChargingSettings.Add(TEXT("MediumTextureQuality"), FHLConfigValue(0.75f)); + + // Initialize settings for High Graphics Profile + DockedSettings.Add(TEXT("HighResolutionX"), FHLConfigValue(3840)); + DockedSettings.Add(TEXT("HighResolutionY"), FHLConfigValue(2160)); + DockedSettings.Add(TEXT("HighTextureQuality"), FHLConfigValue(1.0f)); +} + +void UHarmonyLinkGraphics::LoadProfile(const FName& ProfileName, const bool bForceReload) +{ + QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); + + // Construct the section name + FString SectionName = FString::Printf(TEXT("GraphicsProfile.%s"), *ProfileName.ToString()); + + // Clear the previous settings + SettingsMap.Empty(); + + // Load the settings into the map + if (!LoadSettingsFromConfig(SectionName)) + { + // Retry 2nd time + LoadSettingsFromConfig(SectionName); + } +} + +bool UHarmonyLinkGraphics::LoadSettingsFromConfig(const FString& SectionName) +{ + // Load the configuration for the specified profile + FConfigFile ConfigFile; + + // Normalize the INI file path + const FString IniFilePath = FPaths::Combine(FPaths::ProjectConfigDir(), IniLocation + TEXT(".ini")); + const FString NormalizedIniFilePath = FConfigCacheIni::NormalizeConfigIniPath(IniFilePath); + + if (FConfigCacheIni::LoadLocalIniFile(ConfigFile, *IniLocation, true, nullptr, false)) + { + if (const FConfigSection* Section = ConfigFile.Find(*SectionName)) + { + for (const auto& ValueIt : *Section) + { + int32 Value = FCString::Atoi(*ValueIt.Value.GetValue()); + SettingsMap.Add(*ValueIt.Key.ToString(), Value); + } + + return true; + } + } + + CreateDefaultConfigFile(); + return false; +} + +void UHarmonyLinkGraphics::SaveProfile(const FName& ProfileName) +{ + QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveSettings); + + // Normalize the INI file path + const FString IniFilePath = FPaths::Combine(FPaths::ProjectConfigDir(), IniLocation + TEXT(".ini")); + const FString NormalizedIniFilePath = FConfigCacheIni::NormalizeConfigIniPath(IniFilePath); + + FConfigFile ConfigFile; + ConfigFile.Write(NormalizedIniFilePath); +} + +void UHarmonyLinkGraphics::ApplySettings(const bool bCheckForCommandLineOverrides) +{ + { + FGlobalComponentRecreateRenderStateContext Context; + ApplyResolutionSettings(bCheckForCommandLineOverrides); + ApplyNonResolutionSettings(); + } + + SaveProfile(_ProfileName); +} + +void UHarmonyLinkGraphics::ApplyNonResolutionSettings() +{ +} + +void UHarmonyLinkGraphics::ApplyResolutionSettings(bool bCheckForCommandLineOverrides) +{ +} + +UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() +{ + if (!Instance) + { + Instance = NewObject(); + Instance->AddToRoot(); + Instance->LoadProfile(ChargingProfile); + } + + return Instance; +} + +void UHarmonyLinkGraphics::DestroySettings() +{ + if (Instance) + { + Instance->RemoveFromRoot(); + Instance->MarkAsGarbage(); + Instance = nullptr; + } +} + +void UHarmonyLinkGraphics::CreateDefaultConfigFile() +{ + UE_LOG(LogHarmonyLink, Log, TEXT("CreateDefaultConfigFile started.")); + + SaveSection(BatteryProfile, BatterySettings); + SaveSection(ChargingProfile, ChargingSettings); + SaveSection(DockedProfile, DockedSettings); +} + +void UHarmonyLinkGraphics::SaveSection(const FName& SectionName, const TMap& Settings) const +{ + if (GConfig) + { + const FString Filename = GetDefaultConfigFilename(); + for (const auto& Setting : Settings) + { + switch (Setting.Value.GetType()) + { + case EConfigValueType::Int: + GConfig->SetInt(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + break; + case EConfigValueType::Float: + GConfig->SetFloat(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + break; + case EConfigValueType::Bool: + GConfig->SetBool(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + break; + case EConfigValueType::String: + GConfig->SetString(*SectionName.ToString(), *Setting.Key.ToString(), *Setting.Value.GetValue(), Filename); + break; + } + } + + UE_LOG(LogHarmonyLink, Log, TEXT("Saving config file: '%s'"), *Filename) + GConfig->Flush(false, Filename); + } +} + +void UHarmonyLinkGraphics::ResetInstance() +{ + Instance->DestroySettings(); + GetSettings(); +} diff --git a/Source/HarmonyLink/Private/Structs/HLConfigValue.cpp b/Source/HarmonyLink/Private/Structs/HLConfigValue.cpp new file mode 100644 index 0000000..16f5699 --- /dev/null +++ b/Source/HarmonyLink/Private/Structs/HLConfigValue.cpp @@ -0,0 +1,3 @@ +// Copyright (C) 2024 Jordon Brooks + +#include "Structs/HLConfigValue.h" diff --git a/Source/HarmonyLink/Public/HarmonyLink.h b/Source/HarmonyLink/Public/HarmonyLink.h index 9ee5be3..6cb8597 100644 --- a/Source/HarmonyLink/Public/HarmonyLink.h +++ b/Source/HarmonyLink/Public/HarmonyLink.h @@ -4,6 +4,8 @@ #include "Modules/ModuleManager.h" +DECLARE_LOG_CATEGORY_EXTERN(LogHarmonyLink, Log, All); + class FHarmonyLinkModule : public IModuleInterface { public: diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h new file mode 100644 index 0000000..545bbfb --- /dev/null +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -0,0 +1,70 @@ +// Copyright (C) 2024 Jordon Brooks + +#pragma once + +#include "CoreMinimal.h" +#include "Structs/HLConfigValue.h" + +#include "UObject/Object.h" +#include "HarmonyLinkGraphics.generated.h" + +/** + * + */ +UCLASS(Blueprintable, config="HarmonyLink") +class HARMONYLINK_API UHarmonyLinkGraphics : public UObject +{ + GENERATED_BODY() + +public: + UHarmonyLinkGraphics(); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void LoadProfile(const FName& ProfileName, const bool bForceReload = false); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void SaveProfile(const FName& ProfileName); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings", meta=(bCheckForCommandLineOverrides=true)) + void ApplySettings(bool bCheckForCommandLineOverrides = true); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void ApplyNonResolutionSettings(); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void ApplyResolutionSettings(bool bCheckForCommandLineOverrides); + + /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + static UHarmonyLinkGraphics* GetSettings(); + + static void DestroySettings(); + +private: + void CreateDefaultConfigFile(); + bool LoadSettingsFromConfig(const FString& SectionName); + + void SaveSection(const FName& SectionName, const TMap& Settings) const; + + static void ResetInstance(); + + UPROPERTY(Config) + FName _ProfileName = NAME_None; + + static FString IniLocation; + + static FName BatteryProfile; + static FName ChargingProfile; + static FName DockedProfile; + + TMap SettingsMap; + + // Maps to store configuration settings for each profile + TMap BatterySettings; + TMap ChargingSettings; + TMap DockedSettings; + + static TMap DefaultSettingsMap; + + static UHarmonyLinkGraphics* Instance; +}; diff --git a/Source/HarmonyLink/Public/Structs/HLConfigValue.h b/Source/HarmonyLink/Public/Structs/HLConfigValue.h new file mode 100644 index 0000000..1a0cd2f --- /dev/null +++ b/Source/HarmonyLink/Public/Structs/HLConfigValue.h @@ -0,0 +1,85 @@ +// Copyright (C) 2024 Jordon Brooks + +#pragma once + +#include "CoreMinimal.h" +#include "HLConfigValue.generated.h" + +UENUM(BlueprintType) +enum class EConfigValueType : uint8 +{ + Int, + Float, + Bool, + String +}; + +USTRUCT(BlueprintType) +struct FHLConfigValue +{ + GENERATED_BODY() + +private: + UPROPERTY(EditAnywhere, Category="ConfigValue") + EConfigValueType Type; + + UPROPERTY(EditAnywhere, Category="ConfigValue") + int32 IntValue; + + UPROPERTY(EditAnywhere, Category="ConfigValue") + float FloatValue; + + UPROPERTY(EditAnywhere, Category="ConfigValue") + bool BoolValue; + + UPROPERTY(EditAnywhere, Category="ConfigValue") + FString StringValue; + +public: + FHLConfigValue() : Type(EConfigValueType::String), IntValue(0), FloatValue(0.0f), BoolValue(false), StringValue(TEXT("")) {} + + FHLConfigValue(int32 Value) : Type(EConfigValueType::Int), IntValue(Value), FloatValue(0.0f), BoolValue(false), StringValue(TEXT("")) {} + + FHLConfigValue(float Value) : Type(EConfigValueType::Float), IntValue(0), FloatValue(Value), BoolValue(false), StringValue(TEXT("")) {} + + FHLConfigValue(bool Value) : Type(EConfigValueType::Bool), IntValue(0), FloatValue(0.0f), BoolValue(Value), StringValue(TEXT("")) {} + + FHLConfigValue(const FString& Value) : Type(EConfigValueType::String), IntValue(0), FloatValue(0.0f), BoolValue(false), StringValue(Value) {} + + EConfigValueType GetType() const + { + return Type; + } + + template + T GetValue() const; +}; + +// Specializations of the templated getter +template<> +inline int32 FHLConfigValue::GetValue() const +{ + ensure(Type == EConfigValueType::Int); + return IntValue; +} + +template<> +inline float FHLConfigValue::GetValue() const +{ + ensure(Type == EConfigValueType::Float); + return FloatValue; +} + +template<> +inline bool FHLConfigValue::GetValue() const +{ + ensure(Type == EConfigValueType::Bool); + return BoolValue; +} + +template<> +inline FString FHLConfigValue::GetValue() const +{ + ensure(Type == EConfigValueType::String); + return StringValue; +} From e9b04607ec5b55418ed58b708cd705944e65c72f Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 02:42:36 +0100 Subject: [PATCH 02/18] Update core files --- .../bin/Linux/libHarmonyLinkLib.so | Bin 454760 -> 131 bytes .../bin/Win64/HarmonyLinkLib.dll | Bin 95744 -> 131 bytes .../bin/Win64/HarmonyLinkLib.lib | Bin 10636 -> 130 bytes .../HarmonyLinkLib/include/Enums/EDevice.h | 1 + .../HarmonyLinkLib/include/Enums/EPlatform.h | 1 + .../HarmonyLinkLib/include/Enums/ESteamDeck.h | 29 ++++++++++++++++++ .../HarmonyLinkLib/include/HarmonyLinkLib.h | 4 +++ .../HarmonyLinkLib/include/Structs/FDevice.h | 6 ++-- 8 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 Source/ThirdParty/HarmonyLinkLib/include/Enums/ESteamDeck.h diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so index 67e068029b2908b4a83274df61bf19138857b365..a48fb6f85cfe76dfd2006466623e26937cb3ca26 100644 GIT binary patch literal 131 zcmWN?K@!3s3;@78uiyg~8VVu(O(8^>QRxWw;OliSdzJTS{iW-i=h%(8w|RTWSpL^f zT5^Ayah7bZRrSVt)B}9j9l4D$05fHmgJcFuJ_&{p5zz{G1fU>m1e+5(64@-d)%kCy?!%)al^XR~x+jWt8=yzGB? zX@%i666%D0hp#0CfiM{AWPqXo`>cyN9WqWe>8NLJaMoGULgHjv&_fcizK~vUtAY}* zw(G}B%T_h6mQPPwu2UiXmL&}(dipm_BJxXH6&{pqYO2>We+e@$gF^ZB^g~l5PWg*_ z>4QZ2Ri%>J$%hQ|CF#{x<%5Y*wz$Ml@_2c1uj0_Inbg!mqHmdMpU=rja%|5KsYU)& z+tlL5?v| zk_j%UQp?tUb)5{@y}0P>o0>|crs^O8O~7@+V6Sdm6jx8=IzjCr>aimPjRQl9Q8*z*rbaqy}{?;3mda z%P%o#F(kQTvc8^Vf>ct(MPi(~XPjizK&hdDBw6m(Lhq%%)4*}&#U&EsWY7WWyn?$5 zNy~RACz0%tNcD{+_k5QwJ>|5ZsFS3ZyJV2WsgZ@TyJ_KMr?SQ#UQ*M}`oYPPWV1r1 zl7a@Nr(5hhFEurjRFf>1+*%^>+Ev7`kbbhn7*uYTxj~9)ase-i0n_pE#>o?)DPuop zX+!IVbMqN&on~U->>_cJn5{9Lp>Lu;u3EpFdM3pOZ#GEQlS`b6OZ1)g6&Pn7o!nEu z_sB_JlB>y*;xUuUN($}LFC(#(NWEa{EGeEguT-tNC6m1dx;RQwO(g~rQ>lUJG|OOH ziF?f=#-?6UeXomBt3s9*l2qs8W(Jeprx^_FV4NbASl*PhFg2J9eW0Ii2J)pF8SB~o zXFcAys@V{yG?V0f&hEx3PUG?|(oftsSZdlr3X{gPwxqbRfkmrb>1hT|&@mFJlhayY zu&4AsLzoeIY02YKcHJ5u>R!k~VwP;`EKN&s(sP!YmM~409FV4(8cTWt6Ay`8FIiGV z;#|J0q_{Ngp;OU-7G_H&MS`XIO=l!a%#W=|yJdFEMlx3$+p;fj%cv&8yyU(FdR(AqG&L4D~>O!2m-5h5-x*7y-b_ zNPy7*V*thhj0XV46Us!YCqX?KU@E|L!p(qs7Qk$PIRI$@tjq^kAY9>pi|Kla8g40E zFQd;Zpk4{EiaxVy4PAq-NY(+Y2ly9YBLFX(;bJ?$4vtW(cdA|QhU-1_c`wul01g5i zCLFJh!Nqa(x-eZAp}Hv5 z#h^9;C;?ECaHXkce_Ix=%K?-Js0dIQzzl#FbGWWfpKC!~m#*tUU7xNkp|+xH_IK8B zZ3o~0&=8;z04q(XX4g&Ox*32IKnsAD0Iaw`?F!%y-~rGYfR#2-d(t)g+>Wl>Q|+bp zoiAK>rq5ld{tN1^06hTY0Db`e01ALW09Jyi4yHO3>TtS_fI5<{dqEuopah5`Ts+i? z0KEbF0Q3h)24H0n)Pn(10EPez17IZ;>Jb1V=`+KPg6lE#c|6n;)V`Yt*OLLJ1I#4+ zEU0JG^&F_@0?eb&=}^xHSOBmXU>N`_E2v%#^;&>+0P6|IzTcpBy%DZA18gDOHmG+1 z?4-}Tpx#T@`=CAmaELx1h58u434oITrvUx~I0L}SS*XteT%gYvp}qugnLb~I`Wju| zr1}ojcj=n(cmUUr=<{Q!pV9Sms9yoR0eB1W4&Xh&2LN6^!Sxrl&m7O|@ARD>a|i(R z0rCT|Qh;i9T@bDf0g3<=126_C4qyVnOG&sc15gg2B0yyTUaG*g8GyMOt{Pld2dGIn z3$^cRt6ei5b>MS-fCd1T09F7t0JZ>j0KC}4wIe`dfF=M<0a$T{x+Q=MfE$2204p9) zx29{vWc=?5__hG;0Xh(kH`KlW9RWH6bOGoJz{}ri*WKZ|2Y?KqCx9Fv03Z+`7=V=! zszad;2Z#WOA{m*vjH~;AdSA8NA*Ig z)#;ZIZYk8u0G89|RZy>{YZmIRrEB)L>*#tt)f=eZs7A9HuD8(VZBXx^>z!2ZqIx&f zdjR&*=Y3Eg1UN*WS$jw6`Z&}l0Zsw@2XGpIm9tQv1Gqq+8SWxnUsC(baI9P=+*PV? zsL|X~yS@w8_W&LMJRG~7YUjV++ z=O0uv8Wt4l0ptT!01DD)1FG3~hHzb&J{P6h2x=3$E&+8(fKv3iEY<99St+OX zxq{ktCAh8(P=#=&P@BXra5^qKK=g=;qecK{E7)&Ok)c=4p`worQkbO7)H@C9I{Bh;Pf zntkr9cFjI_fzMq5x)Hty)I9;@^qJB4!F2$AR!|)Xbtpg>Kmm-2Q0R0HxpXvcnC(|{fWo00I9;AjF4A(;eh5-yG{79%r1B?L}2QVIBBEVz- zR;EBbjjpFdJ%g@iLOly$HhpHabKrWe+GmDKgU|B-=Bwcs!u2Bhycp_b04o4i6Yd|V z*8;4g&y3H%biEPk%>Y{fwi0d|)Y}1e0PG~(KBy1U^Il8_8^+kZo09Oci9qJo&&1hM<37>BP+y=Nu-`$7$0l-80{D|tuR6l|G zIlv2m*8pz--U7S_5SI^t`$+gNP=5vZ2Jju=2Y{YF{)Lok_9-7+7XT;(W%0fx0X}Ie_wns{nOHfXei_3e;u*=Jc8I%RPz#_oKwSV{AmyVf zR&Z@YpKaCfjKCf~I}py1>V{A^0%$^?n?cgr&a{(i<4lL3>f4tsE|dA_D=>iidC@~ZQTeBs-!RonNv-s{nG`h6{OWo@tG zr>@!sb@F+4d-8=oFWV+t&2QWO&6J7Dzf3GPDKRaypWcDfJsKBxT0Czedg_ zFF)SNO|DFvzr4F%{n%+9E4L4+@H{o?&XB-$dWNmH%{yIZ-kcs^?j#+0wDe*1VSOJ| zzUVf0O6N0$3=|{oRBG8E;!=UMD<=xI>G5t~U)R#lM>HxJ=DuRlye3cf+^LwaPmi^} zpHF{JAHVZ>E!XK`^XB&--oa{1$4V1wmacCQzOBv{lLy1crx%R!EOum0{-(7C` zGP8Kga;Jw?>Cu-og@q}YCSrwsY-68oWCO!}xvAL}TyMc1;; zss*h&wx!jw^d4WvSd<&y>T$VBjcqSDJX-qSywT6zhQ4=w^R0Zfh>un;Hnsh{aKpOG zvZS*2d-{L*xTAmVr}yS1Z`|&gFy`5w)=u9_9eG)H%9k=u&HF}gi+kUAnr$URMPl5x zwCA>ezg=0vYr?&a0ksPz+s$75@NoB_@muXiCxmUAe`R-%mUrJ;7TJ5H=={TG&dHa? zR6S8`q=nBt<5R0w)_6O9%b=jH$tCOWzv?mN`RQ3z9=sSb`^TZZi(N}?DCJ(-tCPRi z*oGtaJN2^JR;oa}XWRt+>YKx+HhJf`{%f-S)&d`2_3)2%oqDC0yX}OgDcue^6x?L| zcVLI|-)`uiwLNyqb$(j=PR4c2N*iqKwY#O&sz>{R!VX*2-6~H_+T6&&a&6!7ZWd8} z+Wh<3Fr=$lW7{aJx3x<*d9>7KLdeo2C(~eSX@zQ?i#2kzX)~qHg`Ke#(#}^NVOGw4 zr@QHZum`tdSO2r)FX_gAo?YAcx$(5VT^i4fH#1aJ32`}b>3u7?M~dC-=4&tRPsmsB z+lfA5+g|PXZoD)w{Df?9=S~Zr*Dl%3tz77~m4Tf)tXo>-Zow<5V?HnKn*Wx^6hotj zZ@=sxIjZ^by^BBFUyYXbyAV3G+=f!+j1GOB;!tkyIQf)XA4U{7c=WJSuWOfYANpMA zR6x%kqJHLq01~lWH7( z=U!pnEbIKzs!OXjvyH0b@*+AUwojWfl@dzVoixJYhWnVRZ>OYpUTD`jK4@m2&x>4U zwhgrjnGx51N*#lXmrWbC7+-ls$E20(hCVc}bGh=mjpOfMI{D^pgYVZSRB2F4dcU9l zZi@}o4HcC_6Z@6>(n>x&)o%8bLM03C?)NgN{=eyI(pba6M<1S#4U4WmuhsQJ4`&TH z*l+Rk9=GSTE*BbZ9#-n^>)Iv1{S(;f{=P8=!%n>>@;9s&2CqFPWhVBt7owq&*SbcpSg2a;F9C!XZ;)#19y)dUhj0s@pta& z(`()ue&OQ3#oh1de~pd5Z1ecZ_{wd^RE?T8!eaS9!FP+9e|AfB8(+Cd>xg`N{jIGA zTy*VI^=+T(=g#h!V0!4%kJ(N3F5dIG@YGIKCNH~js^9Kf4V`+WZ1eY%&ELDDV|23q zLg(u}26lV6vB=3P)k+m!qu1kf|1D5$Ld+ai->W`KAKHC(t`Ri)$l*RAT z?*%zu{uH(Q+vgH;~tudy`xbrEfOa?zVsIg)6~Z!V|)V#C9#)(XHh*NwylIDO23Ec` z+Ir?BWwO5a&>LL_x~~aYdi31d-8~;}EYa`H+}5>?r!L>$f8Y5X=36JMdhGJf-h4#x z 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
  • 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 Iygxht2SH*T+5i9m diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll index 9c2272bfd055181d062d19264923b68a97e09f6f..860a335ec6c39c47bf280a69e5d4862a6ffd8881 100644 GIT binary patch literal 131 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 LCF92$z&cWY*dZpv literal 10636 zcmcgyU2GFa5T5*yfca?%A>Y#afa=Qt z^=DM7TLd6fdr>810YcU~m9|U(DD)Upq*`o?P+hZ1o9B=p$#zerZFT^Ku3?JQ*q~DL z6#zm_w^VB0fn}tY8kL%cupX)5f=Z1GD2HVCYP5(c($;yEY<;K?$%;JI1xea}aOjYC zMC$bn@Ad8<8ad>Z`hAg5cFE_&+tZjcwdwZs9Bp%m%+2cjg5Y57 zc}7M-=t$R=+v6r6b=`DG`J@D)Ked(~w~#^!mk^VVB@@Y@fN?hMb~HM!`7}zf*ot5u z^F?L96jg1$_lRKkPxvBIEaH=6QL$Hy#sZ>vRIvLdC&PYUEF8f)r>ItQSy`@{*lfz! ze;%tc$_auion1+T$%e};AjHDbWOz0h5mTucCWh;9;_6TmeT=h^WY?Bx!py3u9V-Tu zvP*|OJ#NCOZ=Rg|V&KKRqMKQ^&)~>v%zb9|)e3{Xw0@Sk}41 zQR!4L5|zVKN;`(4&NJqV#e$L3xMF;W6dhRnOF8M5Wc6OIbcktL{;%-{@5is4e)2~qD z-ohrK8#6n9uKNs|rud*S#_hD#jA1TiWC>f%29!lEd01Io0aI;U&H(Kj^S(p{7^2_g zJ~0R-&SDmhvIdZOztZ?~^NbCRWaZy3k0XNKoI+cqJ1iRvGQY2up2` zyvPvO>5Md1)N2f(oQaBr#!0~#9+EO`kmL5Y#X%%=8E8a-}zr^3IIJ<_1UfjEB0Zn2>qCOgBG+@gtDIPa=;h! z*`4%N1!lfL`cbPW!c>H|>lz>4^r{cxD?xpfkJO4_yIzQCHTuouDNpWb6Kp6e#aDL} z20#~kE4a>;AC&!(a5Q{8W^4Ag*ar3w9?;$=Bx|rl@DKv@)Jd)ke^)5EWMzm5SJgB^ zcbzLJc>2^Ri6TI-76Kgs5kNsdL2H!0s`@mZ4#I;%5a;RCYe+o?8u5p;jtG=r^`{U< zF|(X^+`Lxg?CjF)jaX43JZ$~>!F9x_Vh1p@q5!@(J_ajlr}e}--lGLAC00b*T`ucj znQIbZru=}wy$0Lc=%Q8E;0I4aH@-&==|(Tj8tl{tN?F5X4H_R2M7jn$Xgyi$-_U}V z629dcWtI`f&^qIn7+x!Nwd;E)09nU@x%uk1zY>%_7js3^Cc6apyu0FG?8p~670>xU zD(>t?JO#KfvBcqO#GVf{j&^#q=xFN0um3QMXbKt8GqaR(a_-tp zKQw2<1PZUcb#){ox%v6NyW~J9WNcF4Wjx=qfz2hsN1`w-7sf+vfR-DKjpahaXN zZ@#wkH*yZIBe<9^1oDB#(MHW$-zi<*R~GS)FZkU6(U1r;`>%N>5P9vY+ho^#E9Cq3 zAR;WI2(!A&FagIsn~pXLPanSz|Fk@P5z(zTqRZi?Bp*0k)af_uUUwf}{dE`OEGIZw z?_o^9aL=g8%4~VE@jtRMHxMjYT=JN};+{*Fi7)rpr?p@9BCZN9T&cG)-m&U5!L;!1 z>?8bWh+pT5%|7>=3aO3f48C5N= + +// Enum class for representing different types of devices +namespace HarmonyLinkLib +{ + enum class ESteamDeck : uint8_t + { + NONE, // Device is not a steam deck + UNKNOWN, // Device is a steam deck but model cannot be determined + LCD, + OLED, + }; +} diff --git a/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h b/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h index aaa1887..d6575a5 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h @@ -46,6 +46,10 @@ namespace HarmonyLinkLib { extern "C" HARMONYLINKLIB_API bool get_is_wine(); + extern "C" HARMONYLINKLIB_API bool get_is_linux(); + + extern "C" HARMONYLINKLIB_API bool get_is_docked(); + extern "C" HARMONYLINKLIB_API FCPUInfo* get_cpu_info(); extern "C" HARMONYLINKLIB_API FDevice* get_device_info(); diff --git a/Source/ThirdParty/HarmonyLinkLib/include/Structs/FDevice.h b/Source/ThirdParty/HarmonyLinkLib/include/Structs/FDevice.h index 50a1a5c..b574d6a 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/Structs/FDevice.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/Structs/FDevice.h @@ -18,13 +18,15 @@ #include "Enums/EDevice.h" #include "Enums/EPlatform.h" +#include "Enums/ESteamDeck.h" namespace HarmonyLinkLib { // Struct to represent a specific device with both platform and device type struct FDevice : HarmonyLinkStruct { - EPlatform platform; - EDevice device; + EPlatform platform = EPlatform::UNKNOWN; + EDevice device = EDevice::UNKNOWN; + ESteamDeck steam_deck_model = ESteamDeck::NONE; }; } From 5c9a3256d67e66f5c1fd6a2538453d51bac1d0c4 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 16:20:05 +0100 Subject: [PATCH 03/18] Working loading, Saving and default config functions --- Source/HarmonyLink/HarmonyLink.Build.cs | 2 + .../Private/Objects/HarmonyLinkGraphics.cpp | 271 +++++++++++++----- Source/HarmonyLink/Public/Enums/Profile.h | 18 ++ .../Public/Objects/HarmonyLinkGraphics.h | 44 ++- .../Public/Structs/HLConfigValue.h | 52 ++++ .../Public/Structs/SettingsProfile.h | 45 +++ 6 files changed, 335 insertions(+), 97 deletions(-) create mode 100644 Source/HarmonyLink/Public/Enums/Profile.h create mode 100644 Source/HarmonyLink/Public/Structs/SettingsProfile.h diff --git a/Source/HarmonyLink/HarmonyLink.Build.cs b/Source/HarmonyLink/HarmonyLink.Build.cs index 75e3165..725ae7e 100644 --- a/Source/HarmonyLink/HarmonyLink.Build.cs +++ b/Source/HarmonyLink/HarmonyLink.Build.cs @@ -8,6 +8,8 @@ public class HarmonyLink : ModuleRules public HarmonyLink(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + IWYUSupport = IWYUSupport.Full; PublicIncludePaths.AddRange( new string[] { diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 7268be3..42fb8d1 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -7,108 +7,152 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::Instance = nullptr; FString UHarmonyLinkGraphics::IniLocation = "HarmonyLink"; -FName UHarmonyLinkGraphics::BatteryProfile = "Battery"; -FName UHarmonyLinkGraphics::ChargingProfile = "Charging"; -FName UHarmonyLinkGraphics::DockedProfile = "Docked"; -TMap UHarmonyLinkGraphics::DefaultSettingsMap = {{"Test", 0}}; +TMap> UHarmonyLinkGraphics::DefaultSettings = { + { "Battery", { + { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, + { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(0) } + }}, + { "Charging", { + { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, + { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) } + }}, + { "Docked", { + { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, + { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) } + }} +}; UHarmonyLinkGraphics::UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); - - - // Initialize settings for Low Graphics Profile - BatterySettings.Add(TEXT("LowResolutionX"), FHLConfigValue(1280)); - BatterySettings.Add(TEXT("LowResolutionY"), FHLConfigValue(720)); - BatterySettings.Add(TEXT("LowTextureQuality"), FHLConfigValue(0.5f)); - - // Initialize settings for Medium Graphics Profile - ChargingSettings.Add(TEXT("MediumResolutionX"), FHLConfigValue(1920)); - ChargingSettings.Add(TEXT("MediumResolutionY"), FHLConfigValue(1080)); - ChargingSettings.Add(TEXT("MediumTextureQuality"), FHLConfigValue(0.75f)); - - // Initialize settings for High Graphics Profile - DockedSettings.Add(TEXT("HighResolutionX"), FHLConfigValue(3840)); - DockedSettings.Add(TEXT("HighResolutionY"), FHLConfigValue(2160)); - DockedSettings.Add(TEXT("HighTextureQuality"), FHLConfigValue(1.0f)); } -void UHarmonyLinkGraphics::LoadProfile(const FName& ProfileName, const bool bForceReload) +void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) { QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); - // Construct the section name - FString SectionName = FString::Printf(TEXT("GraphicsProfile.%s"), *ProfileName.ToString()); - - // Clear the previous settings - SettingsMap.Empty(); - // Load the settings into the map - if (!LoadSettingsFromConfig(SectionName)) + if (!LoadSettingsFromConfig()) { // Retry 2nd time - LoadSettingsFromConfig(SectionName); + LoadSettingsFromConfig(); } + + DebugPrintProfiles(); } -bool UHarmonyLinkGraphics::LoadSettingsFromConfig(const FString& SectionName) +bool UHarmonyLinkGraphics::LoadSettingsFromConfig() { - // Load the configuration for the specified profile + // Load the configuration from the INI file FConfigFile ConfigFile; - // Normalize the INI file path - const FString IniFilePath = FPaths::Combine(FPaths::ProjectConfigDir(), IniLocation + TEXT(".ini")); - const FString NormalizedIniFilePath = FConfigCacheIni::NormalizeConfigIniPath(IniFilePath); + const FString Filename = GetDefaultConfigFilename(); - if (FConfigCacheIni::LoadLocalIniFile(ConfigFile, *IniLocation, true, nullptr, false)) + if (FConfigCacheIni::LoadLocalIniFile(ConfigFile, *Filename, true, nullptr, false)) { - if (const FConfigSection* Section = ConfigFile.Find(*SectionName)) - { - for (const auto& ValueIt : *Section) - { - int32 Value = FCString::Atoi(*ValueIt.Value.GetValue()); - SettingsMap.Add(*ValueIt.Key.ToString(), Value); - } + // Load each profile section + bool bLoaded = true; + for (const TPair& Profile : ProfileNames) + { + if (!LoadSection(ConfigFile, Profile)) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load section: '%s'"), *Profile.Value.ToString()); + bLoaded = false; + } + } + + // Check if all profiles were loaded successfully + if (bLoaded) + { + UE_LOG(LogHarmonyLink, Log, TEXT("Successfully loaded config file: %s"), *Filename); return true; } } + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load config file: %s"), *Filename); CreateDefaultConfigFile(); return false; } -void UHarmonyLinkGraphics::SaveProfile(const FName& ProfileName) +bool UHarmonyLinkGraphics::LoadSection(const FConfigFile& ConfigFile, const TPair Profile) { - QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveSettings); + const FName& SectionName = Profile.Value; + const EProfile ProfileKey = Profile.Key; - // Normalize the INI file path - const FString IniFilePath = FPaths::Combine(FPaths::ProjectConfigDir(), IniLocation + TEXT(".ini")); - const FString NormalizedIniFilePath = FConfigCacheIni::NormalizeConfigIniPath(IniFilePath); - - FConfigFile ConfigFile; - ConfigFile.Write(NormalizedIniFilePath); + if (const FConfigSection* Section = ConfigFile.FindSection(*SectionName.ToString())) + { + FSettingsProfile& SettingsProfile = Profiles.FindOrAdd(ProfileKey); + SettingsProfile.SectionName = SectionName; + SettingsProfile.Settings.Empty(); // Clear previous settings + + for (const auto& ValueIt : *Section) + { + FString ValueString = ValueIt.Value.GetValue(); + FString Value, Type; + + // Parse the Value and Type from the string + if (ValueString.Split(TEXT(", Type="), &Value, &Type)) + { + Value = Value.RightChop(7); // Remove "(Value=" prefix + Type = Type.LeftChop(1); // Remove ")" suffix + + if (Type == "int") + { + const int32 IntValue = FCString::Atoi(*Value); + SettingsProfile.Settings.Add(ValueIt.Key, FHLConfigValue(IntValue)); + } + else if (Type == "float") + { + const float FloatValue = FCString::Atof(*Value); + SettingsProfile.Settings.Add(ValueIt.Key, FHLConfigValue(FloatValue)); + } + else if (Type == "bool") + { + const bool BoolValue = Value == "true"; + SettingsProfile.Settings.Add(ValueIt.Key, FHLConfigValue(BoolValue)); + } + else if (Type == "string") + { + SettingsProfile.Settings.Add(ValueIt.Key, FHLConfigValue(Value)); + } + } + } + + return true; + } + + return false; +} + +void UHarmonyLinkGraphics::SaveConfig() const +{ + QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); + + for (TPair Profile : Profiles) + { + SaveSection(Profile.Value); + } + + const FString Filename = GetDefaultConfigFilename(); + UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + GConfig->Flush(false, Filename); } void UHarmonyLinkGraphics::ApplySettings(const bool bCheckForCommandLineOverrides) { { FGlobalComponentRecreateRenderStateContext Context; - ApplyResolutionSettings(bCheckForCommandLineOverrides); - ApplyNonResolutionSettings(); + //ApplyResolutionSettings(bCheckForCommandLineOverrides); + //ApplyNonResolutionSettings(); } - - SaveProfile(_ProfileName); } -void UHarmonyLinkGraphics::ApplyNonResolutionSettings() -{ -} -void UHarmonyLinkGraphics::ApplyResolutionSettings(bool bCheckForCommandLineOverrides) -{ -} UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { @@ -116,7 +160,7 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { Instance = NewObject(); Instance->AddToRoot(); - Instance->LoadProfile(ChargingProfile); + Instance->LoadConfig(); } return Instance; @@ -134,39 +178,120 @@ void UHarmonyLinkGraphics::DestroySettings() void UHarmonyLinkGraphics::CreateDefaultConfigFile() { - UE_LOG(LogHarmonyLink, Log, TEXT("CreateDefaultConfigFile started.")); + UE_LOG(LogHarmonyLink, Log, TEXT("Creating default config file.")); + + LoadDefaults(); + SaveConfig(); - SaveSection(BatteryProfile, BatterySettings); - SaveSection(ChargingProfile, ChargingSettings); - SaveSection(DockedProfile, DockedSettings); + UE_LOG(LogHarmonyLink, Log, TEXT("Default config file created.")); } -void UHarmonyLinkGraphics::SaveSection(const FName& SectionName, const TMap& Settings) const +void UHarmonyLinkGraphics::SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush) const { if (GConfig) { const FString Filename = GetDefaultConfigFilename(); - for (const auto& Setting : Settings) + for (const auto& Setting : SettingsProfile.Settings) { + FString TypeString; + FString ValueString; + switch (Setting.Value.GetType()) { case EConfigValueType::Int: - GConfig->SetInt(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + ValueString = FString::FromInt(Setting.Value.GetValue()); + TypeString = TEXT("int"); break; case EConfigValueType::Float: - GConfig->SetFloat(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + ValueString = FString::SanitizeFloat(Setting.Value.GetValue()); + TypeString = TEXT("float"); break; case EConfigValueType::Bool: - GConfig->SetBool(*SectionName.ToString(), *Setting.Key.ToString(), Setting.Value.GetValue(), Filename); + ValueString = Setting.Value.GetValue() ? TEXT("true") : TEXT("false"); + TypeString = TEXT("bool"); break; case EConfigValueType::String: - GConfig->SetString(*SectionName.ToString(), *Setting.Key.ToString(), *Setting.Value.GetValue(), Filename); + ValueString = Setting.Value.GetValue(); + TypeString = TEXT("string"); break; } + + const FString ConfigValue = FString::Printf(TEXT("(Value=%s, Type=%s)"), *ValueString, *TypeString); + GConfig->SetString(*SettingsProfile.SectionName.ToString(), *Setting.Key.ToString(), *ConfigValue, Filename); } - UE_LOG(LogHarmonyLink, Log, TEXT("Saving config file: '%s'"), *Filename) - GConfig->Flush(false, Filename); + UE_LOG(LogHarmonyLink, Log, TEXT("Saving config file: '%s'"), *Filename); + if (bFlush) + { + UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + GConfig->Flush(false, Filename); + } + } +} + +void UHarmonyLinkGraphics::LoadDefaults() +{ + UE_LOG(LogHarmonyLink, Log, TEXT("LoadDefaults started.")); + + Profiles.Reset(); + + // Iterate over ProfileNames to create default settings + for (const TPair& Profile : ProfileNames) + { + FSettingsProfile NewProfileSettings; + NewProfileSettings.SectionName = Profile.Value; + + if (const TMap* Settings = DefaultSettings.Find(Profile.Value)) + { + NewProfileSettings.Settings = *Settings; + } + + Profiles.Add(Profile.Key, NewProfileSettings); + } +} + +void UHarmonyLinkGraphics::DebugPrintProfiles() const +{ + UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles started.")); + + for (TPair Profile : Profiles) + { + PrintDebugSection(Profile.Value); + } + + UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles completed.")); +} + +void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) +{ + UE_LOG(LogHarmonyLink, Warning, TEXT("[%s]"), *SettingsProfile.SectionName.ToString()); + + for (const auto& Setting : SettingsProfile.Settings) + { + FString ValueString; + FString TypeString; + + switch (Setting.Value.GetType()) + { + case EConfigValueType::Int: + ValueString = FString::FromInt(Setting.Value.GetValue()); + TypeString = TEXT("int"); + break; + case EConfigValueType::Float: + ValueString = FString::SanitizeFloat(Setting.Value.GetValue()); + TypeString = TEXT("float"); + break; + case EConfigValueType::Bool: + ValueString = Setting.Value.GetValue() ? TEXT("true") : TEXT("false"); + TypeString = TEXT("bool"); + break; + case EConfigValueType::String: + ValueString = Setting.Value.GetValue(); + TypeString = TEXT("string"); + break; + } + + UE_LOG(LogHarmonyLink, Warning, TEXT("Key: %s = V=%s, T=%s "), *Setting.Key.ToString(), *ValueString, *TypeString); } } diff --git a/Source/HarmonyLink/Public/Enums/Profile.h b/Source/HarmonyLink/Public/Enums/Profile.h new file mode 100644 index 0000000..6985b45 --- /dev/null +++ b/Source/HarmonyLink/Public/Enums/Profile.h @@ -0,0 +1,18 @@ +// Copyright (C) 2024 Jordon Brooks + +#pragma once + +#include "CoreMinimal.h" +#include "Profile.generated.h" + +/* + * Enum representing different operating system platforms. + */ +UENUM(BlueprintType) +enum class EProfile : uint8 +{ + NONE UMETA(DisplayName = "NONE"), + BATTERY UMETA(DisplayName = "BATTERY"), + CHARGING UMETA(DisplayName = "CHARGING"), + DOCKED UMETA(DisplayName = "DOCKED"), +}; diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 545bbfb..ad76529 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -3,7 +3,8 @@ #pragma once #include "CoreMinimal.h" -#include "Structs/HLConfigValue.h" +#include "Enums/Profile.h" +#include "Structs/SettingsProfile.h" #include "UObject/Object.h" #include "HarmonyLinkGraphics.generated.h" @@ -20,20 +21,14 @@ public: UHarmonyLinkGraphics(); UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - void LoadProfile(const FName& ProfileName, const bool bForceReload = false); + void LoadConfig(const bool bForceReload = false); UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - void SaveProfile(const FName& ProfileName); + void SaveConfig() const; UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings", meta=(bCheckForCommandLineOverrides=true)) void ApplySettings(bool bCheckForCommandLineOverrides = true); - UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - void ApplyNonResolutionSettings(); - - UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - void ApplyResolutionSettings(bool bCheckForCommandLineOverrides); - /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") static UHarmonyLinkGraphics* GetSettings(); @@ -42,29 +37,30 @@ public: private: void CreateDefaultConfigFile(); - bool LoadSettingsFromConfig(const FString& SectionName); + bool LoadSettingsFromConfig(); - void SaveSection(const FName& SectionName, const TMap& Settings) const; + bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); + + void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false) const; + + void LoadDefaults(); + + void DebugPrintProfiles() const; + static void PrintDebugSection(FSettingsProfile& SettingsProfile); static void ResetInstance(); - UPROPERTY(Config) - FName _ProfileName = NAME_None; - static FString IniLocation; - - static FName BatteryProfile; - static FName ChargingProfile; - static FName DockedProfile; - TMap SettingsMap; + TMap ProfileNames = { + {EProfile::BATTERY, "Battery"}, + {EProfile::CHARGING, "Charging"}, + {EProfile::DOCKED, "Docked"}, + }; - // Maps to store configuration settings for each profile - TMap BatterySettings; - TMap ChargingSettings; - TMap DockedSettings; + TMap Profiles; - static TMap DefaultSettingsMap; + static TMap> DefaultSettings; static UHarmonyLinkGraphics* Instance; }; diff --git a/Source/HarmonyLink/Public/Structs/HLConfigValue.h b/Source/HarmonyLink/Public/Structs/HLConfigValue.h index 1a0cd2f..daf99d2 100644 --- a/Source/HarmonyLink/Public/Structs/HLConfigValue.h +++ b/Source/HarmonyLink/Public/Structs/HLConfigValue.h @@ -53,6 +53,34 @@ public: template T GetValue() const; + + // Equality operators + bool operator==(const FHLConfigValue& Other) const + { + if (Type != Other.Type) + { + return false; + } + + switch (Type) + { + case EConfigValueType::Int: + return IntValue == Other.IntValue; + case EConfigValueType::Float: + return FloatValue == Other.FloatValue; + case EConfigValueType::Bool: + return BoolValue == Other.BoolValue; + case EConfigValueType::String: + return StringValue == Other.StringValue; + default: + return false; + } + } + + bool operator!=(const FHLConfigValue& Other) const + { + return !(*this == Other); + } }; // Specializations of the templated getter @@ -83,3 +111,27 @@ inline FString FHLConfigValue::GetValue() const ensure(Type == EConfigValueType::String); return StringValue; } + +// Hash function +FORCEINLINE uint32 GetTypeHash(const FHLConfigValue& Value) +{ + uint32 Hash = GetTypeHash(Value.GetType()); + + switch (Value.GetType()) + { + case EConfigValueType::Int: + Hash = HashCombine(Hash, GetTypeHash(Value.GetValue())); + break; + case EConfigValueType::Float: + Hash = HashCombine(Hash, GetTypeHash(Value.GetValue())); + break; + case EConfigValueType::Bool: + Hash = HashCombine(Hash, GetTypeHash(Value.GetValue())); + break; + case EConfigValueType::String: + Hash = HashCombine(Hash, GetTypeHash(Value.GetValue())); + break; + } + + return Hash; +} diff --git a/Source/HarmonyLink/Public/Structs/SettingsProfile.h b/Source/HarmonyLink/Public/Structs/SettingsProfile.h new file mode 100644 index 0000000..d38d146 --- /dev/null +++ b/Source/HarmonyLink/Public/Structs/SettingsProfile.h @@ -0,0 +1,45 @@ +// Copyright (C) 2024 Jordon Brooks + +#pragma once + +#include "CoreMinimal.h" +#include "HLConfigValue.h" + +#include "SettingsProfile.generated.h" + +USTRUCT(BlueprintType) +struct FSettingsProfile +{ + GENERATED_BODY() + + UPROPERTY() + FName SectionName; + + UPROPERTY() + TMap Settings; + + // Equality operators + bool operator==(const FSettingsProfile& Other) const + { + return SectionName == Other.SectionName && Settings.OrderIndependentCompareEqual(Other.Settings); + } + + bool operator!=(const FSettingsProfile& Other) const + { + return !(*this == Other); + } +}; + +// Hash function +FORCEINLINE uint32 GetTypeHash(const FSettingsProfile& Profile) +{ + uint32 Hash = GetTypeHash(Profile.SectionName); + + for (const auto& Pair : Profile.Settings) + { + Hash = HashCombine(Hash, GetTypeHash(Pair.Key)); + Hash = HashCombine(Hash, GetTypeHash(Pair.Value)); + } + + return Hash; +} From 1a1b445302dfa069b7da2013a4d03b8ab2de8b1d Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 16:31:02 +0100 Subject: [PATCH 04/18] Added detection of battery and proceeds to load battery profile --- .../Private/Objects/HarmonyLinkGraphics.cpp | 25 ++++++++++++++++--- .../Public/Objects/HarmonyLinkGraphics.h | 11 ++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 42fb8d1..47b090d 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -4,6 +4,7 @@ #include "Objects/HarmonyLinkGraphics.h" #include "ComponentRecreateRenderStateContext.h" #include "HarmonyLink.h" +#include "HarmonyLinkLibrary.h" UHarmonyLinkGraphics* UHarmonyLinkGraphics::Instance = nullptr; FString UHarmonyLinkGraphics::IniLocation = "HarmonyLink"; @@ -156,11 +157,22 @@ void UHarmonyLinkGraphics::ApplySettings(const bool bCheckForCommandLineOverride UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { - if (!Instance) + // Check if we already initialised + if (Instance) { - Instance = NewObject(); - Instance->AddToRoot(); - Instance->LoadConfig(); + return Instance; + } + + // Proceed to create a new singleton instance + Instance = NewObject(); + Instance->AddToRoot(); + Instance->LoadConfig(); + + const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); + + if (BatteryStatus.HasBattery) + { + Instance->ApplyProfile(EProfile::BATTERY); } return Instance; @@ -250,6 +262,11 @@ void UHarmonyLinkGraphics::LoadDefaults() } } +void UHarmonyLinkGraphics::ApplyProfile(EProfile Profile) +{ + +} + void UHarmonyLinkGraphics::DebugPrintProfiles() const { UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles started.")); diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index ad76529..eb4845f 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -38,18 +38,17 @@ public: private: void CreateDefaultConfigFile(); bool LoadSettingsFromConfig(); - bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); - void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false) const; - void LoadDefaults(); - - void DebugPrintProfiles() const; - static void PrintDebugSection(FSettingsProfile& SettingsProfile); + void ApplyProfile(EProfile Profile); static void ResetInstance(); + // Debugging + void DebugPrintProfiles() const; + static void PrintDebugSection(FSettingsProfile& SettingsProfile); + static FString IniLocation; TMap ProfileNames = { From d4ac87e36d863887d8a4d7a0dc0cdfdf34c36c47 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 17:44:04 +0100 Subject: [PATCH 05/18] I believe we now have a semi-working graphics profiles and the ability to switch between them! --- .../Private/Objects/HarmonyLinkGraphics.cpp | 94 ++++++++++++++++--- .../Public/Objects/HarmonyLinkGraphics.h | 7 +- .../Public/Structs/HLConfigValue.h | 19 +++- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 47b090d..bd6b1b5 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -13,17 +13,20 @@ TMap> UHarmonyLinkGraphics::DefaultSettings = { "Battery", { { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(0) } + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(0) }, + { TEXT("r.ScreenPercentage"), FHLConfigValue(50) }, }}, { "Charging", { { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) } + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, + { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, }}, - { "Docked", { + { "Docked", { { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) } + { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, + { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, }} }; @@ -144,15 +147,6 @@ void UHarmonyLinkGraphics::SaveConfig() const GConfig->Flush(false, Filename); } -void UHarmonyLinkGraphics::ApplySettings(const bool bCheckForCommandLineOverrides) -{ - { - FGlobalComponentRecreateRenderStateContext Context; - //ApplyResolutionSettings(bCheckForCommandLineOverrides); - //ApplyNonResolutionSettings(); - } -} - UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() @@ -170,7 +164,10 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); - if (BatteryStatus.HasBattery) + // Enabled for development testing + // At some point I need to implement the ability to fake and emulate these settings to make it easier to test + // BUG: Remove this before release! + if (!BatteryStatus.HasBattery) { Instance->ApplyProfile(EProfile::BATTERY); } @@ -262,9 +259,76 @@ void UHarmonyLinkGraphics::LoadDefaults() } } -void UHarmonyLinkGraphics::ApplyProfile(EProfile Profile) +bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile) { + // Find the profile name associated with the given EProfile + const FName* ProfileName = ProfileNames.Find(Profile); + if (!ProfileName) + { + UE_LOG(LogHarmonyLink, Warning, TEXT("Profile not found.")); + return false; + } + + UE_LOG(LogHarmonyLink, Log, TEXT("Applying profile %s."), *ProfileName->ToString()); + + // Find the settings associated with the profile + FSettingsProfile* SettingsProfile = Profiles.Find(Profile); + + if (!SettingsProfile) + { + UE_LOG(LogHarmonyLink, Warning, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + return false; + } + + { + FGlobalComponentRecreateRenderStateContext Context; + + // Example of applying settings (actual application depends on your implementation) + for (const TPair& Setting : SettingsProfile->Settings) + { + // Example of logging each setting being applied + UE_LOG(LogHarmonyLink, Log, TEXT("Applying setting: %s = %s"), + *Setting.Key.ToString(), *Setting.Value.ToString()); + + ApplySetting(Setting); + } + } + + return true; +} + +void UHarmonyLinkGraphics::ApplySetting(const TPair& Setting) +{ + // Apply the setting based on the key (CVar) + IConsoleManager& ConsoleManager = IConsoleManager::Get(); + IConsoleVariable* CVar = ConsoleManager.FindConsoleVariable(*Setting.Key.ToString()); + + if (CVar) + { + switch (Setting.Value.GetType()) + { + case EConfigValueType::Bool: + CVar->Set(Setting.Value.GetValue(), ECVF_SetByGameSetting); + break; + + case EConfigValueType::Float: + CVar->Set(Setting.Value.GetValue(), ECVF_SetByGameSetting); + break; + + case EConfigValueType::Int: + CVar->Set(Setting.Value.GetValue(), ECVF_SetByGameSetting); + break; + + default: + UE_LOG(LogHarmonyLink, Warning, TEXT("Unsupported value type for setting: %s"), *Setting.Key.ToString()); + break; + } + } + else + { + UE_LOG(LogHarmonyLink, Warning, TEXT("Console variable not found: %s"), *Setting.Key.ToString()); + } } void UHarmonyLinkGraphics::DebugPrintProfiles() const diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index eb4845f..64f5913 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -26,9 +26,6 @@ public: UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SaveConfig() const; - UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings", meta=(bCheckForCommandLineOverrides=true)) - void ApplySettings(bool bCheckForCommandLineOverrides = true); - /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") static UHarmonyLinkGraphics* GetSettings(); @@ -41,10 +38,12 @@ private: bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false) const; void LoadDefaults(); - void ApplyProfile(EProfile Profile); + bool ApplyProfile(EProfile Profile); static void ResetInstance(); + static void ApplySetting(const TPair& Setting); + // Debugging void DebugPrintProfiles() const; static void PrintDebugSection(FSettingsProfile& SettingsProfile); diff --git a/Source/HarmonyLink/Public/Structs/HLConfigValue.h b/Source/HarmonyLink/Public/Structs/HLConfigValue.h index daf99d2..98711ec 100644 --- a/Source/HarmonyLink/Public/Structs/HLConfigValue.h +++ b/Source/HarmonyLink/Public/Structs/HLConfigValue.h @@ -45,7 +45,24 @@ public: FHLConfigValue(bool Value) : Type(EConfigValueType::Bool), IntValue(0), FloatValue(0.0f), BoolValue(Value), StringValue(TEXT("")) {} FHLConfigValue(const FString& Value) : Type(EConfigValueType::String), IntValue(0), FloatValue(0.0f), BoolValue(false), StringValue(Value) {} - + + FString ToString() const + { + switch (Type) + { + case EConfigValueType::Int: + return FString::Printf(TEXT("Int: %d"), IntValue); + case EConfigValueType::Float: + return FString::Printf(TEXT("Float: %f"), FloatValue); + case EConfigValueType::Bool: + return BoolValue ? TEXT("Bool: true") : TEXT("Bool: false"); + case EConfigValueType::String: + return FString::Printf(TEXT("String: %s"), *StringValue); + default: + return TEXT("Unknown Type"); + } + } + EConfigValueType GetType() const { return Type; From 16061e077b43992b46cb61dd422249f6482c15d5 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 21:28:34 +0100 Subject: [PATCH 06/18] Added delegates and automatic switching --- Source/HarmonyLink/Private/HarmonyLink.cpp | 3 +- .../Private/Objects/HarmonyLinkGraphics.cpp | 241 +++++++++++++++--- .../Public/Objects/HarmonyLinkGraphics.h | 45 +++- .../Public/Structs/HLConfigValue.h | 14 +- .../Public/Structs/SettingsProfile.h | 4 +- 5 files changed, 263 insertions(+), 44 deletions(-) diff --git a/Source/HarmonyLink/Private/HarmonyLink.cpp b/Source/HarmonyLink/Private/HarmonyLink.cpp index b508013..167806e 100644 --- a/Source/HarmonyLink/Private/HarmonyLink.cpp +++ b/Source/HarmonyLink/Private/HarmonyLink.cpp @@ -10,11 +10,12 @@ DEFINE_LOG_CATEGORY(LogHarmonyLink); void FHarmonyLinkModule::StartupModule() { - UHarmonyLinkGraphics::GetSettings(); + } void FHarmonyLinkModule::ShutdownModule() { + // Ensure we safely destroy our singleton instance UHarmonyLinkGraphics::DestroySettings(); } diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index bd6b1b5..62c6c14 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -5,11 +5,13 @@ #include "ComponentRecreateRenderStateContext.h" #include "HarmonyLink.h" #include "HarmonyLinkLibrary.h" +#include "GameFramework/GameUserSettings.h" -UHarmonyLinkGraphics* UHarmonyLinkGraphics::Instance = nullptr; -FString UHarmonyLinkGraphics::IniLocation = "HarmonyLink"; +UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; +FString UHarmonyLinkGraphics::_IniLocation = "HarmonyLink"; +int32 UHarmonyLinkGraphics::_TickRate = 1; -TMap> UHarmonyLinkGraphics::DefaultSettings = { +TMap> UHarmonyLinkGraphics::_DefaultSettings = { { "Battery", { { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, @@ -33,6 +35,14 @@ TMap> UHarmonyLinkGraphics::DefaultSettings = UHarmonyLinkGraphics::UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); + + _bAutomaticSwitch = true; + FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UHarmonyLinkGraphics::OnPostWorldInitialization); +} + +UHarmonyLinkGraphics::~UHarmonyLinkGraphics() +{ + FWorldDelegates::OnPostWorldInitialization.RemoveAll(this); } void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) @@ -56,12 +66,18 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig() const FString Filename = GetDefaultConfigFilename(); - if (FConfigCacheIni::LoadLocalIniFile(ConfigFile, *Filename, true, nullptr, false)) + if (!GConfig) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to access GConfig!")) + return false; + } + + if (GConfig->LoadLocalIniFile(ConfigFile, *Filename, false, nullptr, false)) { // Load each profile section bool bLoaded = true; - for (const TPair& Profile : ProfileNames) + for (const TPair& Profile : _ProfileNames) { if (!LoadSection(ConfigFile, Profile)) { @@ -90,7 +106,7 @@ bool UHarmonyLinkGraphics::LoadSection(const FConfigFile& ConfigFile, const TPai if (const FConfigSection* Section = ConfigFile.FindSection(*SectionName.ToString())) { - FSettingsProfile& SettingsProfile = Profiles.FindOrAdd(ProfileKey); + FSettingsProfile& SettingsProfile = _Profiles.FindOrAdd(ProfileKey); SettingsProfile.SectionName = SectionName; SettingsProfile.Settings.Empty(); // Clear previous settings @@ -137,7 +153,7 @@ void UHarmonyLinkGraphics::SaveConfig() const { QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); - for (TPair Profile : Profiles) + for (TPair Profile : _Profiles) { SaveSection(Profile.Value); } @@ -147,20 +163,119 @@ void UHarmonyLinkGraphics::SaveConfig() const GConfig->Flush(false, Filename); } +void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Setting, const FHLConfigValue Value) +{ + // Find the profile name associated with the given EProfile + const FName* ProfileName = _ProfileNames.Find(Profile); + + if (!ProfileName) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Profile not found.")); + return; + } + + FString TypeString; + FString ValueString; + + switch (Value.GetType()) + { + case EConfigValueType::Int: + ValueString = FString::FromInt(Value.GetValue()); + TypeString = TEXT("int"); + break; + case EConfigValueType::Float: + ValueString = FString::SanitizeFloat(Value.GetValue()); + TypeString = TEXT("float"); + break; + case EConfigValueType::Bool: + ValueString = Value.GetValue() ? TEXT("true") : TEXT("false"); + TypeString = TEXT("bool"); + break; + case EConfigValueType::String: + ValueString = Value.GetValue(); + TypeString = TEXT("string"); + break; + } + + UE_LOG(LogHarmonyLink, Log, TEXT("Applying '%s': Value='%s', Type='%s' to profile '%s'."), *Setting.ToString(), *TypeString, *ValueString, *ProfileName->ToString()); + + // Find the settings associated with the profile + FSettingsProfile* SettingsProfile = _Profiles.Find(Profile); + + if (!SettingsProfile) + { + UE_LOG(LogHarmonyLink, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + return; + } + + SettingsProfile->Settings.FindOrAdd(Setting) = Value; +} UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { // Check if we already initialised - if (Instance) + if (_INSTANCE) { - return Instance; + return _INSTANCE; } // Proceed to create a new singleton instance - Instance = NewObject(); - Instance->AddToRoot(); - Instance->LoadConfig(); + _INSTANCE = NewObject(); + _INSTANCE->AddToRoot(); + _INSTANCE->Init(); + + return _INSTANCE; +} + +FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) +{ + // Find the profile name associated with the given EProfile + const FName* ProfileName = _ProfileNames.Find(Profile); + + if (!ProfileName) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Profile not found.")); + return FSettingsProfile(); + } + + // Find the settings associated with the profile + FSettingsProfile* SettingsProfile = _Profiles.Find(Profile); + + if (!SettingsProfile) + { + UE_LOG(LogHarmonyLink, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + return FSettingsProfile(); + } + + return *SettingsProfile; + +} + +EProfile UHarmonyLinkGraphics::GetActiveProfile() const +{ + return _ActiveProfile; +} + +void UHarmonyLinkGraphics::SetAutomaticSwitching(const bool bAutomaticSwitch) +{ + _bAutomaticSwitch = bAutomaticSwitch; +} + +void UHarmonyLinkGraphics::DestroySettings() +{ + if (_INSTANCE) + { + FWorldDelegates::OnPostWorldInitialization.RemoveAll(_INSTANCE); + _INSTANCE->RemoveFromRoot(); + _INSTANCE->MarkAsGarbage(); + _INSTANCE = nullptr; + } +} + +void UHarmonyLinkGraphics::Init() +{ + LoadConfig(); const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); @@ -169,19 +284,35 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() // BUG: Remove this before release! if (!BatteryStatus.HasBattery) { - Instance->ApplyProfile(EProfile::BATTERY); + ApplyProfile(EProfile::BATTERY); } - - return Instance; } -void UHarmonyLinkGraphics::DestroySettings() +void UHarmonyLinkGraphics::Tick() { - if (Instance) + if (!_bAutomaticSwitch) { - Instance->RemoveFromRoot(); - Instance->MarkAsGarbage(); - Instance = nullptr; + return; + } + + const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); + + if (BatteryStatus.HasBattery) + { + if (BatteryStatus.IsACConnected) + { + if (_ActiveProfile != EProfile::CHARGING) + { + ApplyProfile(EProfile::CHARGING); + } + } + else + { + if (_ActiveProfile != EProfile::BATTERY) + { + ApplyProfile(EProfile::BATTERY); + } + } } } @@ -242,38 +373,86 @@ void UHarmonyLinkGraphics::LoadDefaults() { UE_LOG(LogHarmonyLink, Log, TEXT("LoadDefaults started.")); - Profiles.Reset(); + _Profiles.Reset(); // Iterate over ProfileNames to create default settings - for (const TPair& Profile : ProfileNames) + for (const TPair& Profile : _ProfileNames) { FSettingsProfile NewProfileSettings; NewProfileSettings.SectionName = Profile.Value; - if (const TMap* Settings = DefaultSettings.Find(Profile.Value)) + if (const TMap* Settings = _DefaultSettings.Find(Profile.Value)) { NewProfileSettings.Settings = *Settings; } - Profiles.Add(Profile.Key, NewProfileSettings); + _Profiles.Add(Profile.Key, NewProfileSettings); + } +} + +void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) +{ + if (!World) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) + return; + } + + if (World->IsGameWorld()) + { + if (UHarmonyLinkGraphics* Settings = GetSettings()) + { + if (World->IsPlayInEditor()) + { + Settings->LoadConfig(true); + } + + if (!World->GetTimerManager().TimerExists(Settings->_TickTimerHandle) || !World->GetTimerManager().IsTimerActive(Settings->_TickTimerHandle)) + { + World->GetTimerManager().SetTimer(Settings->_TickTimerHandle, [Settings] + { + Settings->Tick(); + }, _TickRate, true); + } + } } } bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile) { + // If the profile is None, revert to the original user game settings + if (Profile == EProfile::NONE) + { + UE_LOG(LogHarmonyLink, Log, TEXT("Reverting to original user game settings.")); + + if (UGameUserSettings* UserSettings = GEngine->GetGameUserSettings()) + { + UserSettings->LoadSettings(true); + UserSettings->ApplySettings(true); + _ActiveProfile = EProfile::NONE; + OnProfileChanged.Broadcast(_ActiveProfile); + UE_LOG(LogHarmonyLink, Log, TEXT("Original user game settings applied.")); + return true; + } + + UE_LOG(LogHarmonyLink, Warning, TEXT("Failed to get user game settings.")); + return false; + } + + // Find the profile name associated with the given EProfile - const FName* ProfileName = ProfileNames.Find(Profile); + const FName* ProfileName = _ProfileNames.Find(Profile); if (!ProfileName) { - UE_LOG(LogHarmonyLink, Warning, TEXT("Profile not found.")); + UE_LOG(LogHarmonyLink, Error, TEXT("Profile not found.")); return false; } UE_LOG(LogHarmonyLink, Log, TEXT("Applying profile %s."), *ProfileName->ToString()); // Find the settings associated with the profile - FSettingsProfile* SettingsProfile = Profiles.Find(Profile); + FSettingsProfile* SettingsProfile = _Profiles.Find(Profile); if (!SettingsProfile) { @@ -288,13 +467,15 @@ bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile) for (const TPair& Setting : SettingsProfile->Settings) { // Example of logging each setting being applied - UE_LOG(LogHarmonyLink, Log, TEXT("Applying setting: %s = %s"), + UE_LOG(LogHarmonyLink, Log, TEXT("Patching CVar override: %s = %s"), *Setting.Key.ToString(), *Setting.Value.ToString()); ApplySetting(Setting); } } + _ActiveProfile = Profile; + OnProfileChanged.Broadcast(_ActiveProfile); return true; } @@ -335,7 +516,7 @@ void UHarmonyLinkGraphics::DebugPrintProfiles() const { UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles started.")); - for (TPair Profile : Profiles) + for (TPair Profile : _Profiles) { PrintDebugSection(Profile.Value); } @@ -378,6 +559,6 @@ void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) void UHarmonyLinkGraphics::ResetInstance() { - Instance->DestroySettings(); + _INSTANCE->DestroySettings(); GetSettings(); } diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 64f5913..69e167a 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -9,6 +9,8 @@ #include "UObject/Object.h" #include "HarmonyLinkGraphics.generated.h" +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnProfileChanged, EProfile, Profile); + /** * */ @@ -19,6 +21,10 @@ class HARMONYLINK_API UHarmonyLinkGraphics : public UObject public: UHarmonyLinkGraphics(); + virtual ~UHarmonyLinkGraphics() override; + + UPROPERTY(BlueprintAssignable) + FOnProfileChanged OnProfileChanged; UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void LoadConfig(const bool bForceReload = false); @@ -26,19 +32,37 @@ public: UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SaveConfig() const; + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void SetSetting(EProfile Profile, FName Setting, FHLConfigValue Value); + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + bool ApplyProfile(EProfile Profile); + /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") static UHarmonyLinkGraphics* GetSettings(); + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") + FSettingsProfile GetSettingProfile(const EProfile Profile); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") + EProfile GetActiveProfile() const; + + UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + void SetAutomaticSwitching(const bool bAutomaticSwitch); + static void DestroySettings(); private: + void Init(); + void Tick(); void CreateDefaultConfigFile(); bool LoadSettingsFromConfig(); bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false) const; void LoadDefaults(); - bool ApplyProfile(EProfile Profile); + + void OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS); static void ResetInstance(); @@ -48,17 +72,26 @@ private: void DebugPrintProfiles() const; static void PrintDebugSection(FSettingsProfile& SettingsProfile); - static FString IniLocation; + static FString _IniLocation; - TMap ProfileNames = { + uint8 _bAutomaticSwitch :1; + + // How many times to query HarmonyLinkLib for hardware info + static int32 _TickRate; + + FTimerHandle _TickTimerHandle; + + EProfile _ActiveProfile = EProfile::NONE; + + TMap _ProfileNames = { {EProfile::BATTERY, "Battery"}, {EProfile::CHARGING, "Charging"}, {EProfile::DOCKED, "Docked"}, }; - TMap Profiles; + TMap _Profiles; - static TMap> DefaultSettings; + static TMap> _DefaultSettings; - static UHarmonyLinkGraphics* Instance; + static UHarmonyLinkGraphics* _INSTANCE; }; diff --git a/Source/HarmonyLink/Public/Structs/HLConfigValue.h b/Source/HarmonyLink/Public/Structs/HLConfigValue.h index 98711ec..2ce69f3 100644 --- a/Source/HarmonyLink/Public/Structs/HLConfigValue.h +++ b/Source/HarmonyLink/Public/Structs/HLConfigValue.h @@ -14,25 +14,29 @@ enum class EConfigValueType : uint8 String }; +/** + * Note: In Blueprints, all values will be visible, but only the value corresponding to the 'Type' will be used. + */ USTRUCT(BlueprintType) struct FHLConfigValue { GENERATED_BODY() private: - UPROPERTY(EditAnywhere, Category="ConfigValue") + // Allow Blueprint access to these private variables + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ConfigValue", meta = (AllowPrivateAccess = "true")) EConfigValueType Type; - UPROPERTY(EditAnywhere, Category="ConfigValue") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ConfigValue", meta = (AllowPrivateAccess = "true", EditCondition = "Type == EConfigValueType::Int", EditConditionHides)) int32 IntValue; - UPROPERTY(EditAnywhere, Category="ConfigValue") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ConfigValue", meta = (AllowPrivateAccess = "true", EditCondition = "Type == EConfigValueType::Float", EditConditionHides)) float FloatValue; - UPROPERTY(EditAnywhere, Category="ConfigValue") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ConfigValue", meta = (AllowPrivateAccess = "true", EditCondition = "Type == EConfigValueType::Bool", EditConditionHides)) bool BoolValue; - UPROPERTY(EditAnywhere, Category="ConfigValue") + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ConfigValue", meta = (AllowPrivateAccess = "true", EditCondition = "Type == EConfigValueType::String", EditConditionHides)) FString StringValue; public: diff --git a/Source/HarmonyLink/Public/Structs/SettingsProfile.h b/Source/HarmonyLink/Public/Structs/SettingsProfile.h index d38d146..7d12c33 100644 --- a/Source/HarmonyLink/Public/Structs/SettingsProfile.h +++ b/Source/HarmonyLink/Public/Structs/SettingsProfile.h @@ -12,10 +12,10 @@ struct FSettingsProfile { GENERATED_BODY() - UPROPERTY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") FName SectionName; - UPROPERTY() + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Settings") TMap Settings; // Equality operators From 3f43bdb16be2fbb91331b98f5b889b50eba030e2 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Wed, 15 May 2024 21:31:08 +0100 Subject: [PATCH 07/18] Added getter and delegate for automatic profile switching --- Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp | 6 ++++++ Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 62c6c14..ad88487 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -260,6 +260,12 @@ EProfile UHarmonyLinkGraphics::GetActiveProfile() const void UHarmonyLinkGraphics::SetAutomaticSwitching(const bool bAutomaticSwitch) { _bAutomaticSwitch = bAutomaticSwitch; + OnAutomaticSwitchChanged.Broadcast(_bAutomaticSwitch); +} + +bool UHarmonyLinkGraphics::GetAutomaticSwitching() const +{ + return _bAutomaticSwitch; } void UHarmonyLinkGraphics::DestroySettings() diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 69e167a..810a071 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -10,6 +10,7 @@ #include "HarmonyLinkGraphics.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnProfileChanged, EProfile, Profile); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAutomaticSwitchChanged, bool, bAutomaticSwich); /** * @@ -25,6 +26,9 @@ public: UPROPERTY(BlueprintAssignable) FOnProfileChanged OnProfileChanged; + + UPROPERTY(BlueprintAssignable) + FOnAutomaticSwitchChanged OnAutomaticSwitchChanged; UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void LoadConfig(const bool bForceReload = false); @@ -51,6 +55,9 @@ public: UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SetAutomaticSwitching(const bool bAutomaticSwitch); + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") + bool GetAutomaticSwitching() const; + static void DestroySettings(); private: From 80ea24a6947e0dd09a603bb713db9c5e097ebd85 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Thu, 16 May 2024 01:39:54 +0100 Subject: [PATCH 08/18] Working --- .../Private/Objects/HarmonyLinkGraphics.cpp | 165 ++++++++++++------ .../Public/Objects/HarmonyLinkGraphics.h | 17 +- 2 files changed, 125 insertions(+), 57 deletions(-) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index ad88487..33832a6 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -8,7 +8,6 @@ #include "GameFramework/GameUserSettings.h" UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; -FString UHarmonyLinkGraphics::_IniLocation = "HarmonyLink"; int32 UHarmonyLinkGraphics::_TickRate = 1; TMap> UHarmonyLinkGraphics::_DefaultSettings = { @@ -38,6 +37,7 @@ UHarmonyLinkGraphics::UHarmonyLinkGraphics() _bAutomaticSwitch = true; FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UHarmonyLinkGraphics::OnPostWorldInitialization); + FWorldDelegates::OnPreWorldFinishDestroy.AddUObject(this, &UHarmonyLinkGraphics::OnWorldEnd); } UHarmonyLinkGraphics::~UHarmonyLinkGraphics() @@ -64,7 +64,7 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig() // Load the configuration from the INI file FConfigFile ConfigFile; - const FString Filename = GetDefaultConfigFilename(); + const FString Filename = GetConfigDirectoryFile(); if (!GConfig) { @@ -158,7 +158,7 @@ void UHarmonyLinkGraphics::SaveConfig() const SaveSection(Profile.Value); } - const FString Filename = GetDefaultConfigFilename(); + const FString Filename = GetConfigDirectoryFile(); UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); GConfig->Flush(false, Filename); } @@ -272,7 +272,9 @@ void UHarmonyLinkGraphics::DestroySettings() { if (_INSTANCE) { + UE_LOG(LogHarmonyLink, Log, TEXT("Destroying UHarmonyLinkGraphics.")) FWorldDelegates::OnPostWorldInitialization.RemoveAll(_INSTANCE); + FWorldDelegates::OnPreWorldFinishDestroy.RemoveAll(_INSTANCE); _INSTANCE->RemoveFromRoot(); _INSTANCE->MarkAsGarbage(); _INSTANCE = nullptr; @@ -290,36 +292,54 @@ void UHarmonyLinkGraphics::Init() // BUG: Remove this before release! if (!BatteryStatus.HasBattery) { - ApplyProfile(EProfile::BATTERY); + ApplyProfileInternal(EProfile::BATTERY); } } void UHarmonyLinkGraphics::Tick() { - if (!_bAutomaticSwitch) + Async(EAsyncExecution::Thread, [this]() { - return; - } + const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); - const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); - - if (BatteryStatus.HasBattery) - { - if (BatteryStatus.IsACConnected) + if (BatteryStatus.BatteryPercent != _LastBatteryPercentage) { - if (_ActiveProfile != EProfile::CHARGING) + // Ensure thread-safe broadcasting + Async(EAsyncExecution::TaskGraphMainThread, [this, BatteryStatus]() { - ApplyProfile(EProfile::CHARGING); + OnBatteryLevelChanged.Broadcast(BatteryStatus.BatteryPercent); + }); + } + + if (!_bAutomaticSwitch) + { + return; + } + + if (BatteryStatus.HasBattery) + { + if (BatteryStatus.IsACConnected) + { + if (_ActiveProfile != EProfile::CHARGING) + { + Async(EAsyncExecution::TaskGraphMainThread, [this]() + { + ApplyProfileInternal(EProfile::CHARGING); + }); + } + } + else + { + if (_ActiveProfile != EProfile::BATTERY) + { + Async(EAsyncExecution::TaskGraphMainThread, [this]() + { + ApplyProfileInternal(EProfile::BATTERY); + }); + } } } - else - { - if (_ActiveProfile != EProfile::BATTERY) - { - ApplyProfile(EProfile::BATTERY); - } - } - } + }); } void UHarmonyLinkGraphics::CreateDefaultConfigFile() @@ -332,11 +352,22 @@ void UHarmonyLinkGraphics::CreateDefaultConfigFile() UE_LOG(LogHarmonyLink, Log, TEXT("Default config file created.")); } -void UHarmonyLinkGraphics::SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush) const +FString UHarmonyLinkGraphics::GetConfigDirectoryFile() +{ + FString ConfigFileName = "HarmonyLink.ini"; // Replace with your actual config file name + +#if WITH_EDITOR + return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config"), ConfigFileName); +#else + return FPaths::Combine(FPaths::ProjectConfigDir(), ConfigFileName); +#endif +} + +void UHarmonyLinkGraphics::SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush) { if (GConfig) { - const FString Filename = GetDefaultConfigFilename(); + const FString Filename = GetConfigDirectoryFile(); for (const auto& Setting : SettingsProfile.Settings) { FString TypeString; @@ -396,35 +427,7 @@ void UHarmonyLinkGraphics::LoadDefaults() } } -void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) -{ - if (!World) - { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) - return; - } - - if (World->IsGameWorld()) - { - if (UHarmonyLinkGraphics* Settings = GetSettings()) - { - if (World->IsPlayInEditor()) - { - Settings->LoadConfig(true); - } - - if (!World->GetTimerManager().TimerExists(Settings->_TickTimerHandle) || !World->GetTimerManager().IsTimerActive(Settings->_TickTimerHandle)) - { - World->GetTimerManager().SetTimer(Settings->_TickTimerHandle, [Settings] - { - Settings->Tick(); - }, _TickRate, true); - } - } - } -} - -bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile) +bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) { // If the profile is None, revert to the original user game settings if (Profile == EProfile::NONE) @@ -485,6 +488,62 @@ bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile) return true; } +void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) +{ + if (!World) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) + return; + } + + if (World->IsGameWorld()) + { + if (UHarmonyLinkGraphics* Settings = GetSettings()) + { + if (World->IsPlayInEditor()) + { + Settings->LoadConfig(true); + } + + if (!World->GetTimerManager().TimerExists(Settings->_TickTimerHandle) || !World->GetTimerManager().IsTimerActive(Settings->_TickTimerHandle)) + { + World->GetTimerManager().SetTimer(Settings->_TickTimerHandle, [Settings] + { + Settings->Tick(); + }, _TickRate, true); + } + } + } +} + +void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) +{ + if (!World) + { + UE_LOG(LogHarmonyLink, Error, TEXT("World Already destroyed")) + return; + } + + if(World->GetTimerManager().TimerExists(_TickTimerHandle)) + { + World->GetTimerManager().ClearTimer(_TickTimerHandle); + } + + // Ensure we safely destroy our singleton instance + DestroySettings(); +} + +bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisableAuto) +{ + // Manual profile change, turn off automatic switching + if (bDisableAuto) + { + _bAutomaticSwitch = false; + } + + return ApplyProfileInternal(Profile); +} + void UHarmonyLinkGraphics::ApplySetting(const TPair& Setting) { // Apply the setting based on the key (CVar) diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 810a071..e9a486e 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -11,6 +11,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnProfileChanged, EProfile, Profile); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAutomaticSwitchChanged, bool, bAutomaticSwich); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBatteryLevelChanged, int32, BatteryPercent); /** * @@ -29,6 +30,9 @@ public: UPROPERTY(BlueprintAssignable) FOnAutomaticSwitchChanged OnAutomaticSwitchChanged; + + UPROPERTY(BlueprintAssignable) + FOnBatteryLevelChanged OnBatteryLevelChanged; UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void LoadConfig(const bool bForceReload = false); @@ -40,7 +44,7 @@ public: void SetSetting(EProfile Profile, FName Setting, FHLConfigValue Value); UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - bool ApplyProfile(EProfile Profile); + bool ApplyProfile(EProfile Profile, const bool bDisableAuto = true); /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") @@ -64,25 +68,30 @@ private: void Init(); void Tick(); void CreateDefaultConfigFile(); + static FString GetConfigDirectoryFile(); bool LoadSettingsFromConfig(); bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); - void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false) const; + static void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false); void LoadDefaults(); + bool ApplyProfileInternal(EProfile Profile); + void OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS); + void OnWorldEnd(UWorld* World); static void ResetInstance(); + // Note: Turning off HarmonyLink Settings requires game restart static void ApplySetting(const TPair& Setting); // Debugging void DebugPrintProfiles() const; static void PrintDebugSection(FSettingsProfile& SettingsProfile); - static FString _IniLocation; - uint8 _bAutomaticSwitch :1; + int32 _LastBatteryPercentage = 0; + // How many times to query HarmonyLinkLib for hardware info static int32 _TickRate; From 15a26229919395f215a709ea904879db65b8b026 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Sat, 25 May 2024 00:44:14 +0100 Subject: [PATCH 09/18] Working profiles & Updated HarmonyLinkLib --- Source/HarmonyLink/HarmonyLink.Build.cs | 33 +- .../Private/HarmonyLinkLibrary.cpp | 8 +- .../Private/Objects/HarmonyLinkGraphics.cpp | 151 +++++-- Source/HarmonyLink/Public/Enums/Platform.h | 3 + .../Public/Objects/HarmonyLinkGraphics.h | 409 +++++++++++++++++- .../bin/Linux/libHarmonyLinkLib.so | 3 - .../bin/Win64/HarmonyLinkLib.dll | 3 - .../bin/Win64/HarmonyLinkLib.lib | 3 - .../ThirdParty/HarmonyLinkLib/include/Core.h | 10 +- .../HarmonyLinkLib/include/Enums/EDevice.h | 3 + .../lib/Linux/libHarmonyLinkLibStatic.a | 3 + .../lib/Win64/HarmonyLinkLibStatic.lib | 3 + 12 files changed, 538 insertions(+), 94 deletions(-) delete mode 100644 Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so delete mode 100644 Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll delete mode 100644 Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.lib create mode 100644 Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a create mode 100644 Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib diff --git a/Source/HarmonyLink/HarmonyLink.Build.cs b/Source/HarmonyLink/HarmonyLink.Build.cs index 725ae7e..ce895e9 100644 --- a/Source/HarmonyLink/HarmonyLink.Build.cs +++ b/Source/HarmonyLink/HarmonyLink.Build.cs @@ -52,23 +52,22 @@ public class HarmonyLink : ModuleRules } ); - // Platform-specific settings - if (Target.Platform == UnrealTargetPlatform.Win64) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.lib")); - RuntimeDependencies.Add("$(BinaryOutputDir)/HarmonyLinkLib.dll", Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll")); - } - else if (Target.Platform == UnrealTargetPlatform.Linux) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so")); - RuntimeDependencies.Add("$(BinaryOutputDir)/libHarmonyLinkLib.so", Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so")); - } - + // Platform-specific settings for static libraries + if (Target.Platform == UnrealTargetPlatform.Win64) + { + PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib")); + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); + } + else if (Target.Platform == UnrealTargetPlatform.Linux) + { + PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a")); + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); + } // I shall include this if anyone wishes to provide Mac binaries of HarmonyLink but these are not included by default as I don't own one. - else if (Target.Platform == UnrealTargetPlatform.Mac) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Mac/HarmonyLinkLib.dylib")); - RuntimeDependencies.Add("$(BinaryOutputDir)/HarmonyLinkLib.dylib", Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/bin/Mac/HarmonyLinkLib.dylib")); - } + else if (Target.Platform == UnrealTargetPlatform.Mac) + { + PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Mac/libHarmonyLinkLibStatic.a")); + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); + } } } diff --git a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp index ed3f0d9..ac55833 100644 --- a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp +++ b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp @@ -11,13 +11,7 @@ bool UHarmonyLinkLibrary::IsWine() bool UHarmonyLinkLibrary::IsLinux() { -#if PLATFORM_WINDOWS - return IsWine(); -#elif PLATFORM_LINUX - return true; -#else - return false; -#endif + return HarmonyLinkLib::get_is_linux(); } bool UHarmonyLinkLibrary::IsSteamDeck() diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 33832a6..3f05b62 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -9,7 +9,20 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; int32 UHarmonyLinkGraphics::_TickRate = 1; +FTimerHandle UHarmonyLinkGraphics::_TickTimerHandle = FTimerHandle(); +/** + * @brief Default graphics settings for different power states in HarmonyLink. + * + * This map defines the default configuration values for various graphics settings based on the power state of the device. + * The outer map's key is the power state (e.g., "Battery", "Charging", "Docked"). + * The inner map contains key-value pairs where the key is the setting name and the value is the corresponding configuration value. + * + * Power States: + * - "Battery": Settings for when the device is running on battery power. + * - "Charging": Settings for when the device is charging. + * - "Docked": Settings for when the device is docked. + */ TMap> UHarmonyLinkGraphics::_DefaultSettings = { { "Battery", { { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, @@ -34,8 +47,7 @@ TMap> UHarmonyLinkGraphics::_DefaultSettings UHarmonyLinkGraphics::UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); - - _bAutomaticSwitch = true; + FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UHarmonyLinkGraphics::OnPostWorldInitialization); FWorldDelegates::OnPreWorldFinishDestroy.AddUObject(this, &UHarmonyLinkGraphics::OnWorldEnd); } @@ -50,25 +62,29 @@ void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); // Load the settings into the map - if (!LoadSettingsFromConfig()) + if (!LoadSettingsFromConfig(false)) { - // Retry 2nd time - LoadSettingsFromConfig(); + if (!LoadSettingsFromConfig(true)) + { + CreateDefaultConfigFile(); + // Retry 2nd time + LoadSettingsFromConfig(true); + } } DebugPrintProfiles(); } -bool UHarmonyLinkGraphics::LoadSettingsFromConfig() +bool UHarmonyLinkGraphics::LoadSettingsFromConfig(const bool bLoadDefaults) { // Load the configuration from the INI file FConfigFile ConfigFile; - const FString Filename = GetConfigDirectoryFile(); + const FString Filename = GetConfigDirectoryFile(bLoadDefaults); if (!GConfig) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to access GConfig!")) + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to access GConfig!")); return false; } @@ -77,6 +93,20 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig() // Load each profile section bool bLoaded = true; + // Load the _bAutomaticSwitch variable + const FString SectionName = TEXT("HarmonyLink"); + const FString KeyName = TEXT("AutomaticProfileSwitch"); + + if (!GConfig->GetBool(*SectionName, *KeyName, _bAutomaticSwitch, Filename)) + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load bAutomaticSwitch from config")); + bLoaded = false; + } + else + { + UE_LOG(LogHarmonyLink, Log, TEXT("Loaded bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); + } + for (const TPair& Profile : _ProfileNames) { if (!LoadSection(ConfigFile, Profile)) @@ -86,7 +116,7 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig() } } - // Check if all profiles were loaded successfully + // Check if all profiles and settings were loaded successfully if (bLoaded) { UE_LOG(LogHarmonyLink, Log, TEXT("Successfully loaded config file: %s"), *Filename); @@ -95,7 +125,6 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig() } UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load config file: %s"), *Filename); - CreateDefaultConfigFile(); return false; } @@ -151,20 +180,17 @@ bool UHarmonyLinkGraphics::LoadSection(const FConfigFile& ConfigFile, const TPai void UHarmonyLinkGraphics::SaveConfig() const { - QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); - - for (TPair Profile : _Profiles) - { - SaveSection(Profile.Value); - } - - const FString Filename = GetConfigDirectoryFile(); - UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); - GConfig->Flush(false, Filename); + Intermal_SaveConfig(false); } void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Setting, const FHLConfigValue Value) { + // Ignore if HarmonyLinkSettings is disabled + if (Profile == EProfile::NONE) + { + return; + } + // Find the profile name associated with the given EProfile const FName* ProfileName = _ProfileNames.Find(Profile); @@ -230,6 +256,12 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) { + // Ignore if HarmonyLinkSettings is disabled + if (Profile == EProfile::NONE) + { + return FSettingsProfile(); + } + // Find the profile name associated with the given EProfile const FName* ProfileName = _ProfileNames.Find(Profile); @@ -249,7 +281,6 @@ FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) } return *SettingsProfile; - } EProfile UHarmonyLinkGraphics::GetActiveProfile() const @@ -286,16 +317,35 @@ void UHarmonyLinkGraphics::Init() LoadConfig(); const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); - - // Enabled for development testing - // At some point I need to implement the ability to fake and emulate these settings to make it easier to test - // BUG: Remove this before release! - if (!BatteryStatus.HasBattery) + + if (BatteryStatus.HasBattery) { ApplyProfileInternal(EProfile::BATTERY); } } +void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const +{ + QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); + + const FString Filename = GetConfigDirectoryFile(bDefaultConfig); + const FString SectionName = TEXT("HarmonyLink"); + const FString KeyName = TEXT("AutomaticProfileSwitch"); + + // Save the _bAutomaticSwitch variable + GConfig->SetBool(*SectionName, *KeyName, _bAutomaticSwitch, Filename); + + UE_LOG(LogHarmonyLink, Log, TEXT("Saving bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); + + for (const TPair& Profile : _Profiles) + { + SaveSection(Profile.Value, bDefaultConfig); + } + + UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + GConfig->Flush(true, Filename); +} + void UHarmonyLinkGraphics::Tick() { Async(EAsyncExecution::Thread, [this]() @@ -347,27 +397,23 @@ void UHarmonyLinkGraphics::CreateDefaultConfigFile() UE_LOG(LogHarmonyLink, Log, TEXT("Creating default config file.")); LoadDefaults(); - SaveConfig(); + Intermal_SaveConfig(true); UE_LOG(LogHarmonyLink, Log, TEXT("Default config file created.")); } -FString UHarmonyLinkGraphics::GetConfigDirectoryFile() +FString UHarmonyLinkGraphics::GetConfigDirectoryFile(const bool bDefaultFolder) { - FString ConfigFileName = "HarmonyLink.ini"; // Replace with your actual config file name + FString ConfigFileName = bDefaultFolder? "DefaultHarmonyLink.ini" : "HarmonyLink.ini"; // Replace with your actual config file name -#if WITH_EDITOR - return FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Config"), ConfigFileName); -#else - return FPaths::Combine(FPaths::ProjectConfigDir(), ConfigFileName); -#endif + return FPaths::Combine(bDefaultFolder ? FPaths::ProjectConfigDir() : FPaths::GeneratedConfigDir(), ConfigFileName); } -void UHarmonyLinkGraphics::SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush) +void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush) { if (GConfig) { - const FString Filename = GetConfigDirectoryFile(); + const FString Filename = GetConfigDirectoryFile(bDefaultConfig); for (const auto& Setting : SettingsProfile.Settings) { FString TypeString; @@ -490,7 +536,7 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) { - if (!World) + if (!World || !World->IsValidLowLevel()) { UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) return; @@ -498,18 +544,29 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init if (World->IsGameWorld()) { - if (UHarmonyLinkGraphics* Settings = GetSettings()) + if (_INSTANCE) { - if (World->IsPlayInEditor()) + FTimerManager* TimerManager = &World->GetTimerManager(); + if (!TimerManager) { - Settings->LoadConfig(true); + UE_LOG(LogHarmonyLink, Error, TEXT("Failed get TimerManager!")) + return; } - - if (!World->GetTimerManager().TimerExists(Settings->_TickTimerHandle) || !World->GetTimerManager().IsTimerActive(Settings->_TickTimerHandle)) + + if (!TimerManager->TimerExists(_INSTANCE->_TickTimerHandle) || !TimerManager->IsTimerActive(_INSTANCE->_TickTimerHandle)) { - World->GetTimerManager().SetTimer(Settings->_TickTimerHandle, [Settings] + World->GetTimerManager().SetTimer(_INSTANCE->_TickTimerHandle, [&, TimerManager] { - Settings->Tick(); + if (!this) + { + UE_LOG(LogHarmonyLink, Error, TEXT("'This' is destroyed, Clearing timer.")) + if (TimerManager) + { + TimerManager->ClearTimer(_TickTimerHandle); + } + return; + } + Tick(); }, _TickRate, true); } } @@ -518,7 +575,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) { - if (!World) + if (!World || !World->IsValidLowLevel()) { UE_LOG(LogHarmonyLink, Error, TEXT("World Already destroyed")) return; @@ -533,10 +590,10 @@ void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) DestroySettings(); } -bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisableAuto) +bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisableAutomaticSwitch) { // Manual profile change, turn off automatic switching - if (bDisableAuto) + if (bDisableAutomaticSwitch) { _bAutomaticSwitch = false; } diff --git a/Source/HarmonyLink/Public/Enums/Platform.h b/Source/HarmonyLink/Public/Enums/Platform.h index 2ffb255..9ab89dc 100644 --- a/Source/HarmonyLink/Public/Enums/Platform.h +++ b/Source/HarmonyLink/Public/Enums/Platform.h @@ -5,6 +5,9 @@ #include "CoreMinimal.h" #include "Platform.generated.h" +// Undefine the LINUX macro to avoid conflicts with the enum definition. +#undef LINUX + /* * Enum representing different operating system platforms. */ diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index e9a486e..3b0836e 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -33,81 +33,468 @@ public: UPROPERTY(BlueprintAssignable) FOnBatteryLevelChanged OnBatteryLevelChanged; - + + /** + * @brief Loads the graphics configuration settings. + * + * This function loads the graphics settings into the appropriate map. If the initial loading attempt fails, + * it retries a second time. The settings are loaded using the `LoadSettingsFromConfig` method. + * After loading the settings, the function prints debug information about the profiles. + * + * @param bForceReload Indicates whether to force reload the settings. Defaults to false. + * + * @note This function uses the QUICK_SCOPE_CYCLE_COUNTER macro for performance profiling. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void LoadConfig(const bool bForceReload = false); + /** + * @brief Saves the current graphics configuration settings to the configuration file. + * + * This function iterates through all profiles and saves their settings to the configuration file. + * Additionally, it saves the `_bAutomaticSwitch` variable to the configuration file and flushes the changes. + * + * @details + * - Uses the QUICK_SCOPE_CYCLE_COUNTER macro for performance profiling. + * - Iterates through the `_Profiles` map and calls `SaveSection` for each profile to save its settings. + * - Saves the `_bAutomaticSwitch` variable under the "HarmonyLink" section with the key "AutomaticProfileSwitch". + * - Logs the status of `_bAutomaticSwitch` and flushes the configuration file to ensure all changes are written. + * + * @note Uses UE_LOG for logging the save process and status messages. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SaveConfig() const; + /** + * @brief Sets a specific configuration value for a given profile. + * + * This function updates a setting for a specified profile with a new value. It handles various data types (int, float, bool, string) + * and logs the process. If the profile or settings cannot be found, it logs an error message. + * + * @param Profile The profile identifier for which the setting is to be updated. + * @param Setting The name of the setting to update. + * @param Value The new value to set for the specified setting. + * + * @details + * - Finds the profile name associated with the given `EProfile`. + * - Determines the type of the value and converts it to a string. + * - Logs the application of the setting with its value and type. + * - Finds the settings profile associated with the given profile. + * - Updates the setting with the new value or adds it if it does not exist. + * + * @note Uses UE_LOG for logging the update process and any errors. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SetSetting(EProfile Profile, FName Setting, FHLConfigValue Value); + /** + * @brief Applies the specified graphics profile. + * + * This function applies the given graphics profile. If automatic profile switching is enabled, it can be disabled by setting the `bDisableAutomaticSwitch` parameter to true. + * + * @param Profile The profile to apply. + * @param bDisableAutomaticSwitch Indicates whether to disable automatic profile switching. Defaults to true. + * @return bool Returns true if the profile was successfully applied; false otherwise. + * + * @details + * - If `bDisableAutomaticSwitch` is true, sets `_bAutomaticSwitch` to false to disable automatic profile switching. + * - Calls `ApplyProfileInternal` to apply the specified profile. + * - Returns the result of `ApplyProfileInternal`. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - bool ApplyProfile(EProfile Profile, const bool bDisableAuto = true); + bool ApplyProfile(EProfile Profile, const bool bDisableAutomaticSwitch = true); - /** Returns the game local machine settings (resolution, windowing mode, scalability settings, etc...) */ + /** + * @brief Retrieves the singleton instance of the UHarmonyLinkGraphics settings. + * + * This function returns the singleton instance of the `UHarmonyLinkGraphics` class. If the instance has not been + * initialized, it creates a new instance, adds it to the root to prevent garbage collection, initializes it, and + * then returns the instance. + * + * @return UHarmonyLinkGraphics* The singleton instance of the `UHarmonyLinkGraphics` class. + * + * @details + * - Checks if the singleton instance (`_INSTANCE`) is already initialized. + * - If not, creates a new instance using `NewObject`. + * - Adds the new instance to the root to prevent it from being garbage collected. + * - Calls the `Init` function on the new instance to perform any necessary initialization. + * - Returns the singleton instance. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") static UHarmonyLinkGraphics* GetSettings(); + /** + * @brief Retrieves the settings profile for a specified profile. + * + * This function returns the settings profile associated with the given `EProfile`. If the profile name or the settings + * cannot be found, it logs an error message and returns an empty `FSettingsProfile`. + * + * @param Profile The profile identifier for which the settings profile is to be retrieved. + * @return FSettingsProfile The settings profile associated with the specified profile. If the profile is not found, returns an empty `FSettingsProfile`. + * + * @details + * - Finds the profile name associated with the given `EProfile`. + * - Logs an error and returns an empty `FSettingsProfile` if the profile name is not found. + * - Finds the settings profile associated with the profile. + * - Logs an error and returns an empty `FSettingsProfile` if the settings profile is not found. + * - Returns the settings profile if found. + * + * @note Uses UE_LOG for logging errors. + */ UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") FSettingsProfile GetSettingProfile(const EProfile Profile); + /** + * @brief Retrieves the currently active profile. + * + * This function returns the currently active profile of the `UHarmonyLinkGraphics` settings. + * + * @return EProfile The currently active profile. + */ UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") EProfile GetActiveProfile() const; + /** + * @brief Sets the automatic profile switching state. + * + * This function updates the automatic profile switching setting and broadcasts the change. + * + * @param bAutomaticSwitch Indicates whether automatic profile switching should be enabled (true) or disabled (false). + * + * @details + * - Updates the `_bAutomaticSwitch` member variable with the new value. + * - Broadcasts the change to any listeners using the `OnAutomaticSwitchChanged` delegate. + */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") void SetAutomaticSwitching(const bool bAutomaticSwitch); + /** + * @brief Retrieves the current state of automatic profile switching. + * + * This function returns the state of the automatic profile switching setting. + * + * @return bool True if automatic profile switching is enabled; false otherwise. + */ UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") bool GetAutomaticSwitching() const; + /** + * @brief Destroys the singleton instance of the UHarmonyLinkGraphics settings. + * + * This function safely destroys the singleton instance of the `UHarmonyLinkGraphics` class by removing it from root, + * marking it as garbage for cleanup, and setting the instance pointer to null. + * + * @details + * - Checks if the singleton instance (`_INSTANCE`) exists. + * - Logs the destruction of the `UHarmonyLinkGraphics` instance. + * - Removes all delegate bindings from `_INSTANCE`. + * - Removes `_INSTANCE` from the root to allow garbage collection. + * - Marks `_INSTANCE` as garbage to enable cleanup. + * - Sets `_INSTANCE` to null. + * + * @note Uses UE_LOG for logging the destruction process. + */ static void DestroySettings(); private: + /** + * @brief Initializes the UHarmonyLinkGraphics settings. + * + * This function initializes the `UHarmonyLinkGraphics` settings by loading the configuration and applying the + * appropriate profile based on the battery status. + * + * @details + * - Calls `LoadConfig` to load the configuration settings. + * - Retrieves the current battery status using `UHarmonyLinkLibrary::GetBatteryStatus`. + * - If the device has a battery, applies the `BATTERY` profile by calling `ApplyProfileInternal`. + */ void Init(); + + void Intermal_SaveConfig(const bool bDefaultConfig) const; + + /** + * @brief Periodically checks and updates the battery status and profile settings. + * + * This function is executed based on a timer determined by `_TickRate`. It asynchronously checks the current battery status + * and updates the battery percentage and profile settings as needed. If the battery percentage has changed, it broadcasts + * this change. If automatic profile switching is enabled, it applies the appropriate profile based on the AC connection status. + * + * @details + * - Runs asynchronously on a separate thread to fetch the current battery status using `UHarmonyLinkLibrary::GetBatteryStatus`. + * - If the battery percentage has changed, broadcasts the new battery percentage in a thread-safe manner. + * - If automatic profile switching (`_bAutomaticSwitch`) is disabled, exits the function. + * - If the device has a battery: + * - Checks the AC connection status and applies the `CHARGING` profile if connected to AC. + * - Applies the `BATTERY` profile if not connected to AC. + * - Ensures all profile applications and broadcasts are executed on the main thread for thread safety. + * + * @note Uses UE_LOG for logging and `Async` for asynchronous execution. + */ void Tick(); + + /** + * @brief Creates a default configuration file. + * + * This function creates a default configuration file by loading the default settings and then saving them to the config file. + * + * @details + * - Logs the start of the default configuration file creation process. + * - Calls `LoadDefaults` to load the default settings into the configuration. + * - Calls `SaveConfig` to save the default settings to the configuration file. + * - Logs the completion of the default configuration file creation process. + * + * @note Uses UE_LOG for logging the creation process. + */ void CreateDefaultConfigFile(); - static FString GetConfigDirectoryFile(); - bool LoadSettingsFromConfig(); + + /** + * @brief Retrieves the path to the configuration file. + * + * This function constructs and returns the full path to the configuration file used by the `UHarmonyLinkGraphics` settings. + * + * @return FString The full path to the configuration file. + * + * @details + * - Defines the configuration file name as "HarmonyLink.ini". + * - Combines the generated config directory path with the configuration file name to form the full path. + * - Returns the full path to the configuration file. + */ + static FString GetConfigDirectoryFile(const bool bDefaultFolder); + + /** + * @brief Loads the settings from the configuration file. + * + * This function loads the graphics settings from an INI configuration file into the appropriate structures. + * It attempts to load each profile section and the automatic switch setting. If any section fails to load, + * it logs an error message and indicates failure. If loading is successful, it logs a success message. + * + * @return true if the settings were successfully loaded; false otherwise. + * + * @details + * - Loads the configuration file specified by `GetConfigDirectoryFile`. + * - Iterates over the `_ProfileNames` to load each profile section. + * - Loads the `_bAutomaticSwitch` variable from the config. + * - If loading fails, it attempts to create a default configuration file. + * + * @note Uses UE_LOG for logging errors and success messages. + */ + bool LoadSettingsFromConfig(const bool bLoadDefaults); + + /** + * @brief Loads a specific section from the configuration file into a settings profile. + * + * This function extracts settings from a specified section of the configuration file and stores them in the corresponding profile. + * It handles different data types (int, float, bool, string) and parses them accordingly. If the section is found and loaded successfully, + * the settings are added to the profile's settings map. + * + * @param ConfigFile The configuration file from which to load the section. + * @param Profile The profile key-value pair containing the profile enum and the section name. + * @return true if the section was found and loaded successfully; false otherwise. + * + * @details + * - Finds the section in the configuration file using the section name from the profile. + * - Clears any previous settings in the profile. + * - Iterates through the settings in the section, parsing the value and type, and adds them to the profile's settings map. + * + * @note The function handles settings of types int, float, bool, and string. + */ bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); - static void SaveSection(FSettingsProfile& SettingsProfile, const bool bFlush = false); + + + /** + * @brief Saves the settings of a given profile section to the configuration file. + * + * This function saves the settings of a specified profile section to the configuration file. It handles various data types + * (int, float, bool, string) and formats them accordingly before saving. If the `bFlush` parameter is true, the configuration + * file is flushed to ensure all changes are written to disk. + * + * @param SettingsProfile The settings profile to be saved. + * @param bFlush Indicates whether to flush the configuration file after saving. Defaults to false. + * + * @details + * - Checks if the global configuration object (`GConfig`) is available. + * - Constructs the full path to the configuration file using `GetConfigDirectoryFile`. + * - Iterates through the settings in the provided `SettingsProfile`. + * - Determines the type and value of each setting and formats them as strings. + * - Sets the formatted string in the configuration file for each setting. + * - Logs the save operation. + * - If `bFlush` is true, flushes the configuration file to ensure all changes are written. + * + * @note Uses UE_LOG for logging the save and flush operations. + */ + static void SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush = false); + + /** + * @brief Loads the default settings into the profiles. + * + * This function resets the current profiles and loads the default settings into the profiles. It iterates over the profile names + * and assigns the default settings to each profile. + * + * @details + * - Logs the start of the `LoadDefaults` process. + * - Resets the `_Profiles` map to clear any existing settings. + * - Iterates over `_ProfileNames` to create default settings for each profile. + * - For each profile, initializes a `FSettingsProfile` with the section name. + * - If default settings are found for the profile in `_DefaultSettings`, assigns these settings to the profile. + * - Adds the newly created profile settings to `_Profiles`. + * + * @note Uses UE_LOG for logging the start of the default settings loading process. + */ void LoadDefaults(); + + /** + * @brief Applies the specified graphics profile internally. + * + * This function applies the given graphics profile by either reverting to the original user game settings or + * applying the settings associated with the specified profile. + * + * @param Profile The profile to apply. If the profile is `EProfile::NONE`, the function reverts to the original user game settings. + * @return bool Returns true if the profile was successfully applied; false otherwise. + * + * @details + * - If the `Profile` is `EProfile::NONE`: + * - Logs a message indicating reversion to original user game settings. + * - Retrieves the user game settings and applies them. + * - Sets `_ActiveProfile` to `EProfile::NONE` and broadcasts the profile change. + * - Returns true if successful, false otherwise. + * - Finds the profile name associated with the given `EProfile`. + * - Logs an error and returns false if the profile name is not found. + * - Logs a message indicating the profile being applied. + * - Finds the settings associated with the profile. + * - Logs a warning and returns false if the settings profile is not found. + * - Creates a render state context for applying settings. + * - Iterates through each setting in the profile and applies it using `ApplySetting`. + * - Sets `_ActiveProfile` to the specified profile and broadcasts the profile change. + * - Returns true indicating the profile was successfully applied. + * + * @note Uses UE_LOG for logging messages, warnings, and errors. Uses `OnProfileChanged` to broadcast profile changes. + */ bool ApplyProfileInternal(EProfile Profile); + /** + * @brief Handles actions to be performed after the world initialization. + * + * This function hooks into the world initialization process to set up the `UHarmonyLinkGraphics` settings. + * It checks if the world is valid and is a game world, then initializes or reloads the settings as necessary and sets up a timer for periodic updates. + * + * @param World The world that has just been initialized. + * @param IVS Initialization values for the world. + * + * @details + * - Checks if the `World` pointer is valid. Logs an error and returns if the world is invalid. + * - If the world is a game world: + * - Retrieves the singleton settings instance using `GetSettings`. + * - If the world is a play-in-editor world, reloads the settings configuration by calling `LoadConfig` with `bForceReload` set to true. + * - Checks if the tick timer is already set or active. If not, sets a timer to call the `Tick` function at intervals specified by `_TickRate`. + * + * @note Uses UE_LOG for logging errors and informational messages. + */ void OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS); + + /** + * @brief Handles actions to be performed when the world ends. + * + * This function is called when the world is about to be destroyed. It clears the tick timer and safely destroys the singleton instance of the `UHarmonyLinkGraphics` settings. + * + * @param World The world that is ending. + * + * @details + * - Checks if the `World` pointer is valid. Logs an error and returns if the world is already destroyed. + * - If the tick timer exists, clears the timer using the world's timer manager. + * - Calls `DestroySettings` to safely destroy the singleton instance of `UHarmonyLinkGraphics`. + * + * @note Uses UE_LOG for logging errors and informational messages. + */ void OnWorldEnd(UWorld* World); + /** + * @brief Resets the singleton instance of the UHarmonyLinkGraphics settings. + * + * This function resets the singleton instance by destroying the current instance and creating a new one. + * + * @details + * - Calls `DestroySettings` on the current instance to safely destroy it. + * - Calls `GetSettings` to create and initialize a new singleton instance. + */ static void ResetInstance(); - // Note: Turning off HarmonyLink Settings requires game restart + /** + * @brief Applies a specific configuration setting to the console variable. + * + * This function applies the given configuration setting to the corresponding console variable (CVar) based on its key. It supports different data types such as bool, float, and int. + * + * @param Setting The key-value pair representing the setting to apply. The key is the name of the console variable, and the value is the configuration value. + * + * @details + * - Uses the console manager to find the console variable associated with the given setting key. + * - Depending on the type of the configuration value, sets the console variable to the corresponding value. + * - If the console variable is not found, logs a warning message. + * - If the value type is unsupported, logs a warning message. + * + * @note Uses UE_LOG for logging warnings about unsupported value types or missing console variables. + */ static void ApplySetting(const TPair& Setting); - // Debugging + /** + * @brief Prints debug information for all profiles. + * + * This function logs the debug information for all profiles stored in `_Profiles`. It iterates through each profile and calls `PrintDebugSection` to print the details. + * + * @details + * - Logs the start of the `DebugPrintProfiles` process. + * - Iterates over each profile in the `_Profiles` map. + * - Calls `PrintDebugSection` for each profile to print its details. + * - Logs the completion of the `DebugPrintProfiles` process. + * + * @note Uses UE_LOG for logging the start and completion of the debug print process. + */ void DebugPrintProfiles() const; + + /** + * @brief Prints debug information for a specific settings profile. + * + * This function logs the debug information for a given settings profile, including the section name and each setting's key, value, and type. + * + * @param SettingsProfile The settings profile to print debug information for. + * + * @details + * - Logs the section name of the `SettingsProfile`. + * - Iterates through each setting in the `SettingsProfile`. + * - Depending on the type of each setting, converts the value to a string and logs the key, value, and type. + * + * @note Uses UE_LOG for logging the section name and settings details. + */ static void PrintDebugSection(FSettingsProfile& SettingsProfile); - uint8 _bAutomaticSwitch :1; + // Indicates whether automatic profile switching is enabled. + bool _bAutomaticSwitch = false; + // Stores the last recorded battery percentage. int32 _LastBatteryPercentage = 0; - // How many times to query HarmonyLinkLib for hardware info + // The rate at which to query HarmonyLinkLib for hardware info. static int32 _TickRate; - FTimerHandle _TickTimerHandle; + // Timer handle for managing the periodic tick function. + static FTimerHandle _TickTimerHandle; + // The currently active profile. EProfile _ActiveProfile = EProfile::NONE; + // Maps profile enums to their corresponding names. TMap _ProfileNames = { {EProfile::BATTERY, "Battery"}, {EProfile::CHARGING, "Charging"}, {EProfile::DOCKED, "Docked"}, }; + // Stores the settings profiles. TMap _Profiles; + // The default settings for profiles. static TMap> _DefaultSettings; + // Singleton instance of UHarmonyLinkGraphics. static UHarmonyLinkGraphics* _INSTANCE; }; diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so deleted file mode 100644 index a48fb6f..0000000 --- a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLib.so +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3934d762dd9c3c6fcd29b5bf76687770f2572296f1820cbd39ff4093135ad907 -size 278088 diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll deleted file mode 100644 index 860a335..0000000 --- a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.dll +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a94fca6083b966762b2640dc4fed71c4556c5169e6853c092638c9b80923ad3 -size 193536 diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.lib b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.lib deleted file mode 100644 index 233a4d6..0000000 --- a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLib.lib +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a815d7263dfec62f077eed620f0f08763031b3b58e3bf816a305d3ed0271167 -size 11026 diff --git a/Source/ThirdParty/HarmonyLinkLib/include/Core.h b/Source/ThirdParty/HarmonyLinkLib/include/Core.h index ec6ef9d..94a32d2 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/Core.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/Core.h @@ -16,10 +16,14 @@ // Use a preprocessor definition to switch between export and import declarations #ifdef _WIN32 - #ifdef HARMONYLINKLIB_EXPORTS - #define HARMONYLINKLIB_API __declspec(dllexport) + #ifdef HARMONYLINKLIB_STATIC + #define HARMONYLINKLIB_API #else - #define HARMONYLINKLIB_API __declspec(dllimport) + #ifdef HARMONYLINKLIB_SHARED + #define HARMONYLINKLIB_API __declspec(dllexport) + #else + #define HARMONYLINKLIB_API __declspec(dllimport) + #endif #endif #else #define HARMONYLINKLIB_API diff --git a/Source/ThirdParty/HarmonyLinkLib/include/Enums/EDevice.h b/Source/ThirdParty/HarmonyLinkLib/include/Enums/EDevice.h index f49c7ec..c11919d 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/Enums/EDevice.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/Enums/EDevice.h @@ -14,6 +14,9 @@ #pragma once +// Undefine the LINUX macro to avoid conflicts with the enum definition. +#undef LINUX + #include // Enum class for representing different types of devices diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a new file mode 100644 index 0000000..a68f717 --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:662848d6802035c1d74a131316d676e256a65c1537644e787e39c0b3c58209df +size 398236 diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib new file mode 100644 index 0000000..d011ea4 --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b400a97666909a2c7c4dd063f65d5bdfe404dda51e31c9d37546e1961f97a3c3 +size 1355050 From 47008fcefe8e3999bb6e2b0508e02ff7da3d53b2 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Mon, 27 May 2024 19:43:19 +0100 Subject: [PATCH 10/18] Successful compile, untested --- Source/HarmonyLink/HarmonyLink.Build.cs | 31 +--------- .../Private/HarmonyLinkLibrary.cpp | 2 +- .../HarmonyLink/Private/Structs/CPUInfo.cpp | 5 -- Source/HarmonyLink/Public/Structs/CPUInfo.h | 4 -- .../HarmonyLinkLib/HarmonyLinkLib.Build.cs | 60 +++++++++++++++++++ .../bin/Linux/libHarmonyLinkLibShared.so | 3 + .../bin/Win64/HarmonyLinkLibShared.dll | 3 + .../ThirdParty/HarmonyLinkLib/include/Core.h | 12 +++- .../lib/Linux/libHarmonyLinkLibStatic.a | 4 +- .../lib/Win64/HarmonyLinkLibStatic.lib | 4 +- 10 files changed, 84 insertions(+), 44 deletions(-) create mode 100644 Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs create mode 100644 Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so create mode 100644 Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll diff --git a/Source/HarmonyLink/HarmonyLink.Build.cs b/Source/HarmonyLink/HarmonyLink.Build.cs index ce895e9..93cd557 100644 --- a/Source/HarmonyLink/HarmonyLink.Build.cs +++ b/Source/HarmonyLink/HarmonyLink.Build.cs @@ -21,7 +21,6 @@ public class HarmonyLink : ModuleRules PrivateIncludePaths.AddRange( new string[] { // ... add other private include paths required here ... - "ThirdParty/HarmonyLinkLib/include" } ); @@ -40,34 +39,10 @@ public class HarmonyLink : ModuleRules PrivateDependencyModuleNames.AddRange( new string[] { - // ... add private dependencies that you statically link with here ... + // ... add private dependencies that you statically link with here ... + "HarmonyLinkLib", } ); - - - DynamicallyLoadedModuleNames.AddRange( - new string[] - { - // ... add any modules that your module loads dynamically here ... - } - ); - - // Platform-specific settings for static libraries - if (Target.Platform == UnrealTargetPlatform.Win64) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib")); - PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); - } - else if (Target.Platform == UnrealTargetPlatform.Linux) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a")); - PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); - } - // I shall include this if anyone wishes to provide Mac binaries of HarmonyLink but these are not included by default as I don't own one. - else if (Target.Platform == UnrealTargetPlatform.Mac) - { - PublicAdditionalLibraries.Add(Path.Combine(PluginDirectory, "Source/ThirdParty/HarmonyLinkLib/lib/Mac/libHarmonyLinkLibStatic.a")); - PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); - } + } } diff --git a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp index ac55833..35314a3 100644 --- a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp +++ b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp @@ -2,7 +2,7 @@ #include "HarmonyLinkLibrary.h" -#include +#include "HarmonyLinkLib.h" bool UHarmonyLinkLibrary::IsWine() { diff --git a/Source/HarmonyLink/Private/Structs/CPUInfo.cpp b/Source/HarmonyLink/Private/Structs/CPUInfo.cpp index c13d94d..8b70135 100644 --- a/Source/HarmonyLink/Private/Structs/CPUInfo.cpp +++ b/Source/HarmonyLink/Private/Structs/CPUInfo.cpp @@ -11,11 +11,6 @@ FCPUInfo::FCPUInfo(HarmonyLinkLib::FCPUInfo* cpu_info) PhysicalCores = cpu_info->Physical_Cores; LogicalCores = cpu_info->Logical_Cores; - for (const HarmonyLinkLib::FString& Flag : cpu_info->Flags) - { - Flags.Add(Flag.c_str()); - } - cpu_info->free(); } else diff --git a/Source/HarmonyLink/Public/Structs/CPUInfo.h b/Source/HarmonyLink/Public/Structs/CPUInfo.h index e6cbd9d..80aad0e 100644 --- a/Source/HarmonyLink/Public/Structs/CPUInfo.h +++ b/Source/HarmonyLink/Public/Structs/CPUInfo.h @@ -33,10 +33,6 @@ struct FCPUInfo UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="HarmonyLink") int32 LogicalCores = 0; - // A set of flags representing various features or capabilities of the CPU. - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="HarmonyLink") - TSet Flags; - // Constructor that initializes the struct with information from an external CPU info source. // @param cpu_info Pointer to an external FCPUInfo structure to copy data from. FCPUInfo(HarmonyLinkLib::FCPUInfo* cpu_info); diff --git a/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs new file mode 100644 index 0000000..0b54efa --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2024 Jordon Brooks + +using UnrealBuildTool; +using System.IO; +using Internal; + +public class HarmonyLinkLib : ModuleRules +{ + public HarmonyLinkLib(ReadOnlyTargetRules Target) : base(Target) + { + Console.WriteLine("Building HarmonyLinkLib"); + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + Type = ModuleType.External; + + // Add the standard library + bUseRTTI = true; + bEnableExceptions = true; + + // Optionally, if you need C++17 features + CppStandard = CppStandardVersion.Cpp17; + + string includePath = Path.Combine(ModuleDirectory, "include"); + Console.WriteLine("Include Path: " + includePath); + PublicIncludePaths.Add(includePath); + + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); + + string platformString = Target.Platform.ToString(); + if (Target.Platform == UnrealTargetPlatform.Win64) + { + PublicDefinitions.Add("BUILD_WINDOWS=1"); + string dllPath = Path.Combine(ModuleDirectory, "lib", platformString, "HarmonyLinkLibStatic.lib"); + Console.WriteLine("DLL Path: " + dllPath); + PublicAdditionalLibraries.Add(dllPath); + //RuntimeDependencies.Add(dllPath); + } + else if (Target.Platform == UnrealTargetPlatform.Linux) + { + Console.WriteLine("Building Linux"); + PublicDefinitions.Add("BUILD_LINUX=1"); + string libPath = Path.Combine(ModuleDirectory, "bin", platformString, "libHarmonyLinkLibShared.so"); + Console.WriteLine("Library Path: " + libPath); + PublicAdditionalLibraries.Add(libPath); + + // Add the C++ standard library explicitly + //string toolchainLibPath = "E:/UnrealToolChains/v22_clang-16.0.6-centos7/x86_64-unknown-linux-gnu/lib"; + //PublicSystemLibraryPaths.Add(toolchainLibPath); + RuntimeDependencies.Add(libPath); + } + // I shall include this if anyone wishes to provide Mac binaries of HarmonyLink but these are not included by default as I don't own one. + else if (Target.Platform == UnrealTargetPlatform.Mac) + { + PublicDefinitions.Add("BUILD_MACOS=1"); + string dynlibPath = Path.Combine(ModuleDirectory, "lib", platformString, "libHarmonyLinkLibStatic.a"); + Console.WriteLine("Dynamic Library Path: " + dynlibPath); + RuntimeDependencies.Add(dynlibPath); + } + } +} diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so new file mode 100644 index 0000000..81f0188 --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7690f748eb883b77813eab76bb41e6264584c2c477ad19e0ce977378b11004a1 +size 252920 diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll new file mode 100644 index 0000000..b32f33a --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45855c66e1828d2b85fef34956a61dfeedc84619f073bfec6ae3484831f24ff6 +size 195072 diff --git a/Source/ThirdParty/HarmonyLinkLib/include/Core.h b/Source/ThirdParty/HarmonyLinkLib/include/Core.h index 94a32d2..90babed 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/Core.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/Core.h @@ -15,7 +15,7 @@ #pragma once // Use a preprocessor definition to switch between export and import declarations -#ifdef _WIN32 +#ifdef BUILD_WINDOWS #ifdef HARMONYLINKLIB_STATIC #define HARMONYLINKLIB_API #else @@ -26,5 +26,13 @@ #endif #endif #else - #define HARMONYLINKLIB_API + #ifdef HARMONYLINKLIB_SHARED + #ifdef __clang__ + #define HARMONYLINKLIB_API __attribute__((visibility("default"))) + #else + #define HARMONYLINKLIB_API + #endif + #else + #define HARMONYLINKLIB_API + #endif #endif diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a index a68f717..df06019 100644 --- a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:662848d6802035c1d74a131316d676e256a65c1537644e787e39c0b3c58209df -size 398236 +oid sha256:7c624c33e6c41a2c58ffed44332622c9332e6e333ed07ac36f659bc242a58ea2 +size 394806 diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib index d011ea4..7044fd0 100644 --- a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b400a97666909a2c7c4dd063f65d5bdfe404dda51e31c9d37546e1961f97a3c3 -size 1355050 +oid sha256:855a36ab6ca665a654cc72b00edb0d59abfec1cf56bcef6b02a0408b82b42e0c +size 789710 From e5cca6b23faf7743d527234010aa060acf87b943 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Fri, 31 May 2024 23:46:36 +0100 Subject: [PATCH 11/18] Successfully compiled and working on Win32 + tested on steam deck --- HarmonyLink.uplugin | 4 +- Source/HarmonyLink/HarmonyLink.Build.cs | 5 +- .../Private/HarmonyLinkLibrary.cpp | 14 +- .../Private/Objects/HarmonyLinkGraphics.cpp | 324 ++++++++++++------ Source/HarmonyLink/Private/Structs/Device.cpp | 12 +- Source/HarmonyLink/Public/Enums/DeviceEnum.h | 2 +- Source/HarmonyLink/Public/Enums/Profile.h | 2 +- Source/HarmonyLink/Public/HarmonyLink.h | 2 +- .../HarmonyLink/Public/HarmonyLinkLibrary.h | 16 +- .../Public/Objects/HarmonyLinkGraphics.h | 32 +- Source/HarmonyLink/Public/Structs/Battery.h | 5 +- Source/HarmonyLink/Public/Structs/CPUInfo.h | 5 +- Source/HarmonyLink/Public/Structs/Device.h | 7 +- Source/HarmonyLink/Public/Structs/OSVerInfo.h | 3 +- .../HarmonyLinkLib/HarmonyLinkLib.Build.cs | 36 +- .../bin/Win64/HarmonyLinkLibShared.dll | 4 +- .../HarmonyLinkLib/include/HarmonyLinkLib.h | 16 +- .../lib/Linux/libHarmonyLinkLibStatic.a | 4 +- .../lib/Win64/HarmonyLinkLibStatic.lib | 4 +- 19 files changed, 311 insertions(+), 186 deletions(-) diff --git a/HarmonyLink.uplugin b/HarmonyLink.uplugin index b083076..637d348 100644 --- a/HarmonyLink.uplugin +++ b/HarmonyLink.uplugin @@ -10,7 +10,7 @@ "DocsURL": "https://github.com/Jordonbc/HarmonyLink", "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/92fd511971274d1f955abb7197485041", "SupportURL": "", - "CanContainContent": true, + "CanContainContent": false, "IsBetaVersion": false, "IsExperimentalVersion": false, "Installed": false, @@ -18,7 +18,7 @@ { "Name": "HarmonyLink", "Type": "Runtime", - "LoadingPhase": "Default", + "LoadingPhase": "PreDefault", "WhitelistPlatforms": ["Win64", "Linux"] } ] diff --git a/Source/HarmonyLink/HarmonyLink.Build.cs b/Source/HarmonyLink/HarmonyLink.Build.cs index 93cd557..7199123 100644 --- a/Source/HarmonyLink/HarmonyLink.Build.cs +++ b/Source/HarmonyLink/HarmonyLink.Build.cs @@ -9,7 +9,7 @@ public class HarmonyLink : ModuleRules { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - IWYUSupport = IWYUSupport.Full; + //IWYUSupport = IWYUSupport.Full; PublicIncludePaths.AddRange( new string[] { @@ -31,6 +31,8 @@ public class HarmonyLink : ModuleRules "Core", "CoreUObject", "Engine", + + "HarmonyLinkLib", // ... add other public dependencies that you statically link with here ... } ); @@ -40,7 +42,6 @@ public class HarmonyLink : ModuleRules new string[] { // ... add private dependencies that you statically link with here ... - "HarmonyLinkLib", } ); diff --git a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp index 35314a3..5197354 100644 --- a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp +++ b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp @@ -1,9 +1,21 @@ // Copyright (C) 2024 Jordon Brooks #include "HarmonyLinkLibrary.h" +#include "HarmonyLink.h" #include "HarmonyLinkLib.h" +UHarmonyLinkLibrary::UHarmonyLinkLibrary() +{ + if (!HarmonyLinkLib::HL_Init()) + { + UE_LOG(LogHarmonyLink, Fatal, TEXT("Failed to initialise HarmonyLinkLib!")); + return; + } + + UE_LOG(LogHarmonyLink, Log, TEXT("HarmonyLinkLib Initialised!")); +} + bool UHarmonyLinkLibrary::IsWine() { return HarmonyLinkLib::get_is_wine(); @@ -16,7 +28,7 @@ bool UHarmonyLinkLibrary::IsLinux() bool UHarmonyLinkLibrary::IsSteamDeck() { - return GetDeviceInfo().Device == EDeviceEnum::STEAM_DECK; + return GetDeviceInfo().Device == EDevice::STEAM_DECK; } FCPUInfo UHarmonyLinkLibrary::GetCPUInfo() diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 3f05b62..f1e17d6 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -6,10 +6,23 @@ #include "HarmonyLink.h" #include "HarmonyLinkLibrary.h" #include "GameFramework/GameUserSettings.h" +#include "Kismet/GameplayStatics.h" UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; int32 UHarmonyLinkGraphics::_TickRate = 1; FTimerHandle UHarmonyLinkGraphics::_TickTimerHandle = FTimerHandle(); +TSharedPtr UHarmonyLinkGraphics::_ConfigFile = nullptr; +bool UHarmonyLinkGraphics::_bAutomaticSwitch = false; +int32 UHarmonyLinkGraphics::_LastBatteryPercentage = 0; +EProfile UHarmonyLinkGraphics::_ActiveProfile = EProfile::NONE; +TMap UHarmonyLinkGraphics::_Profiles = TMap(); + +TMap UHarmonyLinkGraphics::_ProfileNames = +{ + {EProfile::BATTERY, "Battery"}, + {EProfile::CHARGING, "Charging"}, + {EProfile::DOCKED, "Docked"}, +}; /** * @brief Default graphics settings for different power states in HarmonyLink. @@ -25,115 +38,125 @@ FTimerHandle UHarmonyLinkGraphics::_TickTimerHandle = FTimerHandle(); */ TMap> UHarmonyLinkGraphics::_DefaultSettings = { { "Battery", { - { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, - { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(0) }, - { TEXT("r.ScreenPercentage"), FHLConfigValue(50) }, +// { TEXT("r.ReflectionMethod"), FHLConfigValue(0) }, +// { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, +// { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(0) }, +// { TEXT("r.ScreenPercentage"), FHLConfigValue(50) }, }}, { "Charging", { - { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, - { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, - { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, +// { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, +// { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, +// { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, +// { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, }}, { "Docked", { - { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, - { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, - { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, - { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, +// { TEXT("r.ReflectionMethod"), FHLConfigValue(2) }, +// { TEXT("r.DynamicGlobalIlluminationMethod"), FHLConfigValue(2) }, +// { TEXT("r.Shadow.Virtual.Enable"), FHLConfigValue(1) }, +// { TEXT("r.ScreenPercentage"), FHLConfigValue(100) }, }} }; UHarmonyLinkGraphics::UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); + if (_INSTANCE != this) + { + if (_INSTANCE) + { + DestroySettings(); + } + + _INSTANCE = this; + } + + AddToRoot(); FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UHarmonyLinkGraphics::OnPostWorldInitialization); FWorldDelegates::OnPreWorldFinishDestroy.AddUObject(this, &UHarmonyLinkGraphics::OnWorldEnd); + + Init(); } UHarmonyLinkGraphics::~UHarmonyLinkGraphics() { + UE_LOG(LogHarmonyLink, Verbose, TEXT("~UHarmonyLinkGraphics called.")); FWorldDelegates::OnPostWorldInitialization.RemoveAll(this); } void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) { + UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadConfig called.")); QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); // Load the settings into the map - if (!LoadSettingsFromConfig(false)) - { - if (!LoadSettingsFromConfig(true)) - { - CreateDefaultConfigFile(); - // Retry 2nd time - LoadSettingsFromConfig(true); - } - } + GetConfig(); DebugPrintProfiles(); } -bool UHarmonyLinkGraphics::LoadSettingsFromConfig(const bool bLoadDefaults) +bool UHarmonyLinkGraphics::LoadSettingsFromConfig(FConfigFile* ConfigFile) const { - // Load the configuration from the INI file - FConfigFile ConfigFile; + UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadSettingsFromConfig called.")); + //const FString Filename = "HarmonyLink"; //GetConfigDirectoryFile(bLoadDefaults); + + // Load each profile section + bool bLoaded = true; - const FString Filename = GetConfigDirectoryFile(bLoadDefaults); + // Load the _bAutomaticSwitch variable + const FString SectionName = TEXT("HarmonyLink"); + const FString KeyName = TEXT("AutomaticProfileSwitch"); - if (!GConfig) + if (!ConfigFile->GetBool(*SectionName, *KeyName, _bAutomaticSwitch)) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to access GConfig!")); - return false; + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load bAutomaticSwitch from config")); + bLoaded = false; + } + else + { + UE_LOG(LogHarmonyLink, Log, TEXT("Loaded bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); } - if (GConfig->LoadLocalIniFile(ConfigFile, *Filename, false, nullptr, false)) + for (const TPair& Profile : _ProfileNames) { - // Load each profile section - bool bLoaded = true; - - // Load the _bAutomaticSwitch variable - const FString SectionName = TEXT("HarmonyLink"); - const FString KeyName = TEXT("AutomaticProfileSwitch"); - - if (!GConfig->GetBool(*SectionName, *KeyName, _bAutomaticSwitch, Filename)) + if (!LoadSection(ConfigFile, Profile)) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load bAutomaticSwitch from config")); + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load section: '%s'"), *Profile.Value.ToString()); bLoaded = false; } - else - { - UE_LOG(LogHarmonyLink, Log, TEXT("Loaded bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); - } - - for (const TPair& Profile : _ProfileNames) - { - if (!LoadSection(ConfigFile, Profile)) - { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load section: '%s'"), *Profile.Value.ToString()); - bLoaded = false; - } - } - - // Check if all profiles and settings were loaded successfully - if (bLoaded) - { - UE_LOG(LogHarmonyLink, Log, TEXT("Successfully loaded config file: %s"), *Filename); - return true; - } } - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load config file: %s"), *Filename); + // Check if all profiles and settings were loaded successfully + if (bLoaded) + { + UE_LOG(LogHarmonyLink, Log, TEXT("Successfully loaded config.")); + return true; + } + + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load config file.")); return false; } -bool UHarmonyLinkGraphics::LoadSection(const FConfigFile& ConfigFile, const TPair Profile) +bool UHarmonyLinkGraphics::LoadSection(FConfigFile* ConfigFile, const TPair Profile) { + UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadSection called.")); + if (!ensureMsgf(ConfigFile, TEXT("ConfigFile is nullptr!"))) return false; + const FName& SectionName = Profile.Value; const EProfile ProfileKey = Profile.Key; - if (const FConfigSection* Section = ConfigFile.FindSection(*SectionName.ToString())) + const FConfigSection* Section = nullptr; + + +#if (ENGINE_MAJOR_VERSION >= 5) + Section = ConfigFile->FindSection(*SectionName.ToString()) +#elif (ENGINE_MAJOR_VERSION == 4) && (ENGINE_MINOR_VERSION >= 27) + Section = ConfigFile->FindOrAddSection(*SectionName.ToString()); +#else + #error "Unsupported Unreal Engine version" +#endif + + if (Section) { FSettingsProfile& SettingsProfile = _Profiles.FindOrAdd(ProfileKey); SettingsProfile.SectionName = SectionName; @@ -180,11 +203,13 @@ bool UHarmonyLinkGraphics::LoadSection(const FConfigFile& ConfigFile, const TPai void UHarmonyLinkGraphics::SaveConfig() const { + UE_LOG(LogHarmonyLink, Verbose, TEXT("SaveConfig called.")); Intermal_SaveConfig(false); } void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Setting, const FHLConfigValue Value) { + UE_LOG(LogHarmonyLink, Verbose, TEXT("SetSetting called.")); // Ignore if HarmonyLinkSettings is disabled if (Profile == EProfile::NONE) { @@ -214,7 +239,7 @@ void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Settin TypeString = TEXT("float"); break; case EConfigValueType::Bool: - ValueString = Value.GetValue() ? TEXT("true") : TEXT("false"); + ValueString = Value.GetValue() ? TEXT("True") : TEXT("False"); TypeString = TEXT("bool"); break; case EConfigValueType::String: @@ -240,6 +265,7 @@ void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Settin UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { + UE_LOG(LogHarmonyLink, VeryVerbose, TEXT("GetSettings called.")); // Check if we already initialised if (_INSTANCE) { @@ -248,14 +274,13 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() // Proceed to create a new singleton instance _INSTANCE = NewObject(); - _INSTANCE->AddToRoot(); - _INSTANCE->Init(); return _INSTANCE; } FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) { + UE_LOG(LogHarmonyLink, Verbose, TEXT("GetSettingProfile called.")); // Ignore if HarmonyLinkSettings is disabled if (Profile == EProfile::NONE) { @@ -285,35 +310,48 @@ FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) EProfile UHarmonyLinkGraphics::GetActiveProfile() const { + UE_LOG(LogHarmonyLink, Verbose, TEXT("GetActiveProfile called.")); return _ActiveProfile; } void UHarmonyLinkGraphics::SetAutomaticSwitching(const bool bAutomaticSwitch) { + UE_LOG(LogHarmonyLink, Verbose, TEXT("SetAutomaticSwitching called.")); _bAutomaticSwitch = bAutomaticSwitch; OnAutomaticSwitchChanged.Broadcast(_bAutomaticSwitch); } bool UHarmonyLinkGraphics::GetAutomaticSwitching() const { + UE_LOG(LogHarmonyLink, Verbose, TEXT("GetAutomaticSwitching called.")); return _bAutomaticSwitch; } void UHarmonyLinkGraphics::DestroySettings() { + UE_LOG(LogHarmonyLink, Log, TEXT("DestroySettings called.")); if (_INSTANCE) { UE_LOG(LogHarmonyLink, Log, TEXT("Destroying UHarmonyLinkGraphics.")) FWorldDelegates::OnPostWorldInitialization.RemoveAll(_INSTANCE); FWorldDelegates::OnPreWorldFinishDestroy.RemoveAll(_INSTANCE); _INSTANCE->RemoveFromRoot(); - _INSTANCE->MarkAsGarbage(); + +#if (ENGINE_MAJOR_VERSION >= 5) + _INSTANCE->MarkAsGarbage(); // For UE 5.0 and above +#elif (ENGINE_MAJOR_VERSION == 4) && (ENGINE_MINOR_VERSION >= 27) + _INSTANCE->MarkPendingKill(); // For UE 4.27 and above +#else +#error "Unsupported Unreal Engine version" +#endif + _INSTANCE = nullptr; } } void UHarmonyLinkGraphics::Init() { + UE_LOG(LogHarmonyLink, Log, TEXT("Init called.")); LoadConfig(); const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); @@ -326,8 +364,9 @@ void UHarmonyLinkGraphics::Init() void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const { + UE_LOG(LogHarmonyLink, Log, TEXT("Intermal_SaveConfig called.")); QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); - + const FString Filename = GetConfigDirectoryFile(bDefaultConfig); const FString SectionName = TEXT("HarmonyLink"); const FString KeyName = TEXT("AutomaticProfileSwitch"); @@ -343,57 +382,52 @@ void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const } UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + GetConfig()->Dirty = true; + // You'd think that Write would actually write something but for some + // reason even if it outputs a success the file doesn't actually get created. + // For this reason, Flush seems to work for now. + //GetConfig()->Write(Filename); GConfig->Flush(true, Filename); } void UHarmonyLinkGraphics::Tick() { - Async(EAsyncExecution::Thread, [this]() + UE_LOG(LogHarmonyLink, VeryVerbose, TEXT("Tick called.")); + const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); + + if (BatteryStatus.BatteryPercent != _LastBatteryPercentage) { - const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); + // Ensure thread-safe broadcasting + OnBatteryLevelChanged.Broadcast(BatteryStatus.BatteryPercent); + } - if (BatteryStatus.BatteryPercent != _LastBatteryPercentage) - { - // Ensure thread-safe broadcasting - Async(EAsyncExecution::TaskGraphMainThread, [this, BatteryStatus]() - { - OnBatteryLevelChanged.Broadcast(BatteryStatus.BatteryPercent); - }); - } + if (!_bAutomaticSwitch) + { + return; + } - if (!_bAutomaticSwitch) + if (BatteryStatus.HasBattery) + { + if (BatteryStatus.IsACConnected) { - return; - } - - if (BatteryStatus.HasBattery) - { - if (BatteryStatus.IsACConnected) + if (_ActiveProfile != EProfile::CHARGING) { - if (_ActiveProfile != EProfile::CHARGING) - { - Async(EAsyncExecution::TaskGraphMainThread, [this]() - { - ApplyProfileInternal(EProfile::CHARGING); - }); - } - } - else - { - if (_ActiveProfile != EProfile::BATTERY) - { - Async(EAsyncExecution::TaskGraphMainThread, [this]() - { - ApplyProfileInternal(EProfile::BATTERY); - }); - } + ApplyProfileInternal(EProfile::CHARGING); } } - }); + else + { + if (_ActiveProfile != EProfile::BATTERY) + { + ApplyProfileInternal(EProfile::BATTERY); + } + } + } } -void UHarmonyLinkGraphics::CreateDefaultConfigFile() +void UHarmonyLinkGraphics::CreateDefaultConfigFile() const { + UE_LOG(LogHarmonyLink, Log, TEXT("CreateDefaultConfigFile called.")); UE_LOG(LogHarmonyLink, Log, TEXT("Creating default config file.")); LoadDefaults(); @@ -404,13 +438,17 @@ void UHarmonyLinkGraphics::CreateDefaultConfigFile() FString UHarmonyLinkGraphics::GetConfigDirectoryFile(const bool bDefaultFolder) { - FString ConfigFileName = bDefaultFolder? "DefaultHarmonyLink.ini" : "HarmonyLink.ini"; // Replace with your actual config file name + UE_LOG(LogHarmonyLink, Log, TEXT("GetConfigDirectoryFile called.")); + FString ConfigFileName = bDefaultFolder ? TEXT("DefaultHarmonyLink.ini") : TEXT("HarmonyLink.ini"); - return FPaths::Combine(bDefaultFolder ? FPaths::ProjectConfigDir() : FPaths::GeneratedConfigDir(), ConfigFileName); + FString ConfigDirectory = bDefaultFolder ? FPaths::ProjectConfigDir() : FPaths::Combine(FPaths::GeneratedConfigDir(), UGameplayStatics::GetPlatformName()); + + return FPaths::Combine(ConfigDirectory, ConfigFileName); } -void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush) +void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush) const { + UE_LOG(LogHarmonyLink, Log, TEXT("SaveSection called.")); if (GConfig) { const FString Filename = GetConfigDirectoryFile(bDefaultConfig); @@ -430,7 +468,7 @@ void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, TypeString = TEXT("float"); break; case EConfigValueType::Bool: - ValueString = Setting.Value.GetValue() ? TEXT("true") : TEXT("false"); + ValueString = Setting.Value.GetValue() ? TEXT("True") : TEXT("False"); TypeString = TEXT("bool"); break; case EConfigValueType::String: @@ -452,9 +490,9 @@ void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, } } -void UHarmonyLinkGraphics::LoadDefaults() +void UHarmonyLinkGraphics::LoadDefaults() const { - UE_LOG(LogHarmonyLink, Log, TEXT("LoadDefaults started.")); + UE_LOG(LogHarmonyLink, Log, TEXT("LoadDefaults called.")); _Profiles.Reset(); @@ -475,6 +513,7 @@ void UHarmonyLinkGraphics::LoadDefaults() bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) { + UE_LOG(LogHarmonyLink, Log, TEXT("ApplyProfileInternal called.")); // If the profile is None, revert to the original user game settings if (Profile == EProfile::NONE) { @@ -536,6 +575,7 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) { + UE_LOG(LogHarmonyLink, Log, TEXT("OnPostWorldInitialization called.")); if (!World || !World->IsValidLowLevel()) { UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) @@ -544,7 +584,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init if (World->IsGameWorld()) { - if (_INSTANCE) + if (IsValid(_INSTANCE)) { FTimerManager* TimerManager = &World->GetTimerManager(); if (!TimerManager) @@ -553,11 +593,12 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init return; } - if (!TimerManager->TimerExists(_INSTANCE->_TickTimerHandle) || !TimerManager->IsTimerActive(_INSTANCE->_TickTimerHandle)) + if (!TimerManager->TimerExists(_TickTimerHandle) || !TimerManager->IsTimerActive(_TickTimerHandle)) { - World->GetTimerManager().SetTimer(_INSTANCE->_TickTimerHandle, [&, TimerManager] + + World->GetTimerManager().SetTimer(_TickTimerHandle, [TimerManager] { - if (!this) + if (!_INSTANCE) { UE_LOG(LogHarmonyLink, Error, TEXT("'This' is destroyed, Clearing timer.")) if (TimerManager) @@ -566,16 +607,29 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init } return; } - Tick(); + _INSTANCE->Tick(); }, _TickRate, true); } + else + { + UE_LOG(LogHarmonyLink, Error, TEXT("Error: Timer already exists.")); + } } + else + { + UE_LOG(LogHarmonyLink, Error, TEXT("'This' is nullptr!")); + } + } + else + { + //UE_LOG(LogHarmonyLink, Error, TEXT("Failed to bring up tick!")); } } void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) { - if (!World || !World->IsValidLowLevel()) + UE_LOG(LogHarmonyLink, Log, TEXT("OnWorldEnd(UWorld* World) called.")); + if (!World) { UE_LOG(LogHarmonyLink, Error, TEXT("World Already destroyed")) return; @@ -592,6 +646,7 @@ void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisableAutomaticSwitch) { + UE_LOG(LogHarmonyLink, Log, TEXT("Applying Profile.")); // Manual profile change, turn off automatic switching if (bDisableAutomaticSwitch) { @@ -603,6 +658,7 @@ bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisa void UHarmonyLinkGraphics::ApplySetting(const TPair& Setting) { + UE_LOG(LogHarmonyLink, Log, TEXT("Applying settings.")); // Apply the setting based on the key (CVar) IConsoleManager& ConsoleManager = IConsoleManager::Get(); IConsoleVariable* CVar = ConsoleManager.FindConsoleVariable(*Setting.Key.ToString()); @@ -646,6 +702,45 @@ void UHarmonyLinkGraphics::DebugPrintProfiles() const UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles completed.")); } +FConfigFile* UHarmonyLinkGraphics::GetConfig() const +{ + UE_LOG(LogHarmonyLink, Verbose, TEXT("GetConfig Called.")); + if (_ConfigFile) + { + return _ConfigFile.Get(); + } + + FConfigFile* ConfigFile = GConfig->Find(GetConfigDirectoryFile(false), false); + + if (!ConfigFile) + { + UE_LOG(LogHarmonyLink, Warning, TEXT("Config file not found, attempting to read DefaultHarmonyLink.ini.")); + // Look in ProjectFolder->Config->DefaultHarmonyLink.ini + ConfigFile = GConfig->Find(GetConfigDirectoryFile(true), true); + } + + /*if (!ConfigFile) + { + CreateDefaultConfigFile(); + ConfigFile = GConfig->Find(GetConfigDirectoryFile(true), true); + }*/ + + if (ConfigFile) + { + UE_LOG(LogHarmonyLink, Verbose, TEXT("Setting up config.")); + ConfigFile->Name = "HarmonyLink"; + LoadSettingsFromConfig(ConfigFile); + _ConfigFile = MakeShareable(ConfigFile); + } + else + { + UE_LOG(LogHarmonyLink, Error, TEXT("Failed to make config variable!")) + return nullptr; + } + + return _ConfigFile.Get(); +} + void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) { UE_LOG(LogHarmonyLink, Warning, TEXT("[%s]"), *SettingsProfile.SectionName.ToString()); @@ -681,6 +776,7 @@ void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) void UHarmonyLinkGraphics::ResetInstance() { + UE_LOG(LogHarmonyLink, Log, TEXT("Resetting instance.")); _INSTANCE->DestroySettings(); GetSettings(); } diff --git a/Source/HarmonyLink/Private/Structs/Device.cpp b/Source/HarmonyLink/Private/Structs/Device.cpp index a52de5f..794e914 100644 --- a/Source/HarmonyLink/Private/Structs/Device.cpp +++ b/Source/HarmonyLink/Private/Structs/Device.cpp @@ -19,20 +19,20 @@ FDevice::FDevice(HarmonyLinkLib::FDevice* oldDevice) } } -EDeviceEnum FDevice::Convert(HarmonyLinkLib::EDevice Device) +EDevice FDevice::Convert(HarmonyLinkLib::EDevice Device) { switch (Device) { case HarmonyLinkLib::EDevice::DESKTOP: - return EDeviceEnum::DESKTOP; + return EDevice::DESKTOP; case HarmonyLinkLib::EDevice::LAPTOP: - return EDeviceEnum::LAPTOP; + return EDevice::LAPTOP; case HarmonyLinkLib::EDevice::HANDHELD: - return EDeviceEnum::HANDHELD; + return EDevice::HANDHELD; case HarmonyLinkLib::EDevice::STEAM_DECK: - return EDeviceEnum::STEAM_DECK; + return EDevice::STEAM_DECK; default: - return EDeviceEnum::DESKTOP; + return EDevice::DESKTOP; } } diff --git a/Source/HarmonyLink/Public/Enums/DeviceEnum.h b/Source/HarmonyLink/Public/Enums/DeviceEnum.h index 64320c8..fb7bb44 100644 --- a/Source/HarmonyLink/Public/Enums/DeviceEnum.h +++ b/Source/HarmonyLink/Public/Enums/DeviceEnum.h @@ -9,7 +9,7 @@ * Enum representing different operating system platforms. */ UENUM(BlueprintType) -enum class EDeviceEnum : uint8 +enum class EDevice : uint8 { DESKTOP UMETA(DisplayName = "Desktop"), LAPTOP UMETA(DisplayName = "Laptop"), diff --git a/Source/HarmonyLink/Public/Enums/Profile.h b/Source/HarmonyLink/Public/Enums/Profile.h index 6985b45..16c48cd 100644 --- a/Source/HarmonyLink/Public/Enums/Profile.h +++ b/Source/HarmonyLink/Public/Enums/Profile.h @@ -11,7 +11,7 @@ UENUM(BlueprintType) enum class EProfile : uint8 { - NONE UMETA(DisplayName = "NONE"), + NONE = 0 UMETA(DisplayName = "NONE"), BATTERY UMETA(DisplayName = "BATTERY"), CHARGING UMETA(DisplayName = "CHARGING"), DOCKED UMETA(DisplayName = "DOCKED"), diff --git a/Source/HarmonyLink/Public/HarmonyLink.h b/Source/HarmonyLink/Public/HarmonyLink.h index 6cb8597..b59a8ba 100644 --- a/Source/HarmonyLink/Public/HarmonyLink.h +++ b/Source/HarmonyLink/Public/HarmonyLink.h @@ -4,7 +4,7 @@ #include "Modules/ModuleManager.h" -DECLARE_LOG_CATEGORY_EXTERN(LogHarmonyLink, Log, All); +DECLARE_LOG_CATEGORY_EXTERN(LogHarmonyLink, All, All); class FHarmonyLinkModule : public IModuleInterface { diff --git a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h b/Source/HarmonyLink/Public/HarmonyLinkLibrary.h index 4a809e2..19dc5ff 100644 --- a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h +++ b/Source/HarmonyLink/Public/HarmonyLinkLibrary.h @@ -21,31 +21,33 @@ class HARMONYLINK_API UHarmonyLinkLibrary : public UBlueprintFunctionLibrary GENERATED_BODY() public: + UHarmonyLinkLibrary(); + // Checks if the game is running under Wine. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static bool IsWine(); // Checks if the operating system is Linux. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static bool IsLinux(); // Checks if the game is running on a Steam Deck. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static bool IsSteamDeck(); // Retrieves information about the CPU of the current device. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static FCPUInfo GetCPUInfo(); // Retrieves information about the current device. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static FDevice GetDeviceInfo(); // Retrieves information about the operating system of the current device. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static FOSVerInfo GetOSInfo(); // Retrieves the current battery status of the device. - UFUNCTION(BlueprintCallable, Category="HarmonyLink") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static FBattery GetBatteryStatus(); }; diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 3b0836e..80d2475 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -120,7 +120,7 @@ public: * - Calls the `Init` function on the new instance to perform any necessary initialization. * - Returns the singleton instance. */ - UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") static UHarmonyLinkGraphics* GetSettings(); /** @@ -141,7 +141,7 @@ public: * * @note Uses UE_LOG for logging errors. */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink Settings") + UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintPure, Category="HarmonyLink Settings") FSettingsProfile GetSettingProfile(const EProfile Profile); /** @@ -245,7 +245,7 @@ private: * * @note Uses UE_LOG for logging the creation process. */ - void CreateDefaultConfigFile(); + void CreateDefaultConfigFile() const; /** * @brief Retrieves the path to the configuration file. @@ -278,7 +278,7 @@ private: * * @note Uses UE_LOG for logging errors and success messages. */ - bool LoadSettingsFromConfig(const bool bLoadDefaults); + bool LoadSettingsFromConfig(FConfigFile* ConfigFile) const; /** * @brief Loads a specific section from the configuration file into a settings profile. @@ -298,7 +298,7 @@ private: * * @note The function handles settings of types int, float, bool, and string. */ - bool LoadSection(const FConfigFile& ConfigFile, const TPair Profile); + static bool LoadSection(FConfigFile* ConfigFile, const TPair Profile); /** @@ -322,7 +322,7 @@ private: * * @note Uses UE_LOG for logging the save and flush operations. */ - static void SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush = false); + void SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush = false) const; /** * @brief Loads the default settings into the profiles. @@ -340,7 +340,7 @@ private: * * @note Uses UE_LOG for logging the start of the default settings loading process. */ - void LoadDefaults(); + void LoadDefaults() const; /** * @brief Applies the specified graphics profile internally. @@ -451,6 +451,8 @@ private: */ void DebugPrintProfiles() const; + FConfigFile* GetConfig() const; + /** * @brief Prints debug information for a specific settings profile. * @@ -468,10 +470,10 @@ private: static void PrintDebugSection(FSettingsProfile& SettingsProfile); // Indicates whether automatic profile switching is enabled. - bool _bAutomaticSwitch = false; + static bool _bAutomaticSwitch; // Stores the last recorded battery percentage. - int32 _LastBatteryPercentage = 0; + static int32 _LastBatteryPercentage; // The rate at which to query HarmonyLinkLib for hardware info. static int32 _TickRate; @@ -480,17 +482,15 @@ private: static FTimerHandle _TickTimerHandle; // The currently active profile. - EProfile _ActiveProfile = EProfile::NONE; + static EProfile _ActiveProfile; // Maps profile enums to their corresponding names. - TMap _ProfileNames = { - {EProfile::BATTERY, "Battery"}, - {EProfile::CHARGING, "Charging"}, - {EProfile::DOCKED, "Docked"}, - }; + static TMap _ProfileNames; // Stores the settings profiles. - TMap _Profiles; + static TMap _Profiles; + + static TSharedPtr _ConfigFile; // The default settings for profiles. static TMap> _DefaultSettings; diff --git a/Source/HarmonyLink/Public/Structs/Battery.h b/Source/HarmonyLink/Public/Structs/Battery.h index 6b8ec43..bd2793b 100644 --- a/Source/HarmonyLink/Public/Structs/Battery.h +++ b/Source/HarmonyLink/Public/Structs/Battery.h @@ -1,9 +1,10 @@ // Copyright (C) 2024 Jordon Brooks #pragma once -#include - #include "CoreMinimal.h" + +#include "Structs/FBattery.h" + #include "Battery.generated.h" /* diff --git a/Source/HarmonyLink/Public/Structs/CPUInfo.h b/Source/HarmonyLink/Public/Structs/CPUInfo.h index 80aad0e..353c3e5 100644 --- a/Source/HarmonyLink/Public/Structs/CPUInfo.h +++ b/Source/HarmonyLink/Public/Structs/CPUInfo.h @@ -2,9 +2,10 @@ #pragma once -#include - #include "CoreMinimal.h" + +#include "Structs/FCPUInfo.h" + #include "CPUInfo.generated.h" /* diff --git a/Source/HarmonyLink/Public/Structs/Device.h b/Source/HarmonyLink/Public/Structs/Device.h index 071d13c..fd009bd 100644 --- a/Source/HarmonyLink/Public/Structs/Device.h +++ b/Source/HarmonyLink/Public/Structs/Device.h @@ -5,7 +5,8 @@ #include "CoreMinimal.h" #include "Enums/DeviceEnum.h" #include "Enums/Platform.h" -#include + +#include "Structs/FDevice.h" #include "Device.generated.h" @@ -25,7 +26,7 @@ struct FDevice // The type of the device. UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="HarmonyLink") - EDeviceEnum Device = EDeviceEnum::DESKTOP; + EDevice Device = EDevice::DESKTOP; // Constructor that initializes the struct with information from an external source. // @param oldDevice Pointer to an external FDevice structure to copy data from. @@ -35,7 +36,7 @@ private: // Converts an external device enum to the internal EDeviceEnum type. // @param Device External device enum to convert. // @returns Converted EDeviceEnum value. - static EDeviceEnum Convert(HarmonyLinkLib::EDevice Device); + static EDevice Convert(HarmonyLinkLib::EDevice Device); // Converts an external platform enum to the internal EPlatform type. // @param Platform External platform enum to convert. diff --git a/Source/HarmonyLink/Public/Structs/OSVerInfo.h b/Source/HarmonyLink/Public/Structs/OSVerInfo.h index ea380c8..e423cc0 100644 --- a/Source/HarmonyLink/Public/Structs/OSVerInfo.h +++ b/Source/HarmonyLink/Public/Structs/OSVerInfo.h @@ -2,7 +2,8 @@ #pragma once #include "CoreMinimal.h" -#include + +#include "Structs/FOSVerInfo.h" #include "OSVerInfo.generated.h" diff --git a/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs index 0b54efa..0dc8bbd 100644 --- a/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs +++ b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs @@ -8,39 +8,46 @@ public class HarmonyLinkLib : ModuleRules { public HarmonyLinkLib(ReadOnlyTargetRules Target) : base(Target) { - Console.WriteLine("Building HarmonyLinkLib"); + //Console.WriteLine("Building HarmonyLinkLib"); PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; Type = ModuleType.External; // Add the standard library - bUseRTTI = true; - bEnableExceptions = true; + //bUseRTTI = true; + //bEnableExceptions = true; // Optionally, if you need C++17 features - CppStandard = CppStandardVersion.Cpp17; + //CppStandard = CppStandardVersion.Cpp17; string includePath = Path.Combine(ModuleDirectory, "include"); - Console.WriteLine("Include Path: " + includePath); + //Console.WriteLine("Include Path: " + includePath); PublicIncludePaths.Add(includePath); - - PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); string platformString = Target.Platform.ToString(); if (Target.Platform == UnrealTargetPlatform.Win64) { + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); PublicDefinitions.Add("BUILD_WINDOWS=1"); - string dllPath = Path.Combine(ModuleDirectory, "lib", platformString, "HarmonyLinkLibStatic.lib"); - Console.WriteLine("DLL Path: " + dllPath); - PublicAdditionalLibraries.Add(dllPath); - //RuntimeDependencies.Add(dllPath); + string libpath = Path.Combine(ModuleDirectory, "lib", platformString, "HarmonyLinkLibStatic.lib"); + + PublicAdditionalLibraries.Add(libpath); + + //string dllPath = Path.Combine(ModuleDirectory, "bin", platformString, "HarmonyLinkLibShared.dll"); + //string dllTargetPath = "$(TargetOutputDir)/HarmonyLinkLibShared.dll"; + + //Console.WriteLine("DLL Path: " + dllPath); + //RuntimeDependencies.Add(dllTargetPath, dllPath); + + //PublicDelayLoadDLLs.Add("HarmonyLinkLibShared.dll"); } else if (Target.Platform == UnrealTargetPlatform.Linux) { - Console.WriteLine("Building Linux"); + PublicDefinitions.Add("HARMONYLINKLIB_SHARED=1"); + //Console.WriteLine("Building Linux"); PublicDefinitions.Add("BUILD_LINUX=1"); string libPath = Path.Combine(ModuleDirectory, "bin", platformString, "libHarmonyLinkLibShared.so"); - Console.WriteLine("Library Path: " + libPath); + //Console.WriteLine("Library Path: " + libPath); PublicAdditionalLibraries.Add(libPath); // Add the C++ standard library explicitly @@ -51,9 +58,10 @@ public class HarmonyLinkLib : ModuleRules // I shall include this if anyone wishes to provide Mac binaries of HarmonyLink but these are not included by default as I don't own one. else if (Target.Platform == UnrealTargetPlatform.Mac) { + PublicDefinitions.Add("HARMONYLINKLIB_SHARED=1"); PublicDefinitions.Add("BUILD_MACOS=1"); string dynlibPath = Path.Combine(ModuleDirectory, "lib", platformString, "libHarmonyLinkLibStatic.a"); - Console.WriteLine("Dynamic Library Path: " + dynlibPath); + //Console.WriteLine("Dynamic Library Path: " + dynlibPath); RuntimeDependencies.Add(dynlibPath); } } diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll index b32f33a..a98cee4 100644 --- a/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll +++ b/Source/ThirdParty/HarmonyLinkLib/bin/Win64/HarmonyLinkLibShared.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45855c66e1828d2b85fef34956a61dfeedc84619f073bfec6ae3484831f24ff6 -size 195072 +oid sha256:db0796fdd07c54a25cf0b1d77015f8c7af7b7a73ded92c123521dcc9a01e9b40 +size 731136 diff --git a/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h b/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h index d6575a5..0298e2d 100644 --- a/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h +++ b/Source/ThirdParty/HarmonyLinkLib/include/HarmonyLinkLib.h @@ -44,17 +44,19 @@ class IPlatformUtilities; namespace HarmonyLinkLib { - extern "C" HARMONYLINKLIB_API bool get_is_wine(); + HARMONYLINKLIB_API bool HL_Init(); - extern "C" HARMONYLINKLIB_API bool get_is_linux(); + HARMONYLINKLIB_API bool get_is_wine(); - extern "C" HARMONYLINKLIB_API bool get_is_docked(); + HARMONYLINKLIB_API bool get_is_linux(); - extern "C" HARMONYLINKLIB_API FCPUInfo* get_cpu_info(); + HARMONYLINKLIB_API bool get_is_docked(); - extern "C" HARMONYLINKLIB_API FDevice* get_device_info(); + HARMONYLINKLIB_API FCPUInfo* get_cpu_info(); - extern "C" HARMONYLINKLIB_API FOSVerInfo* get_os_version(); + HARMONYLINKLIB_API FDevice* get_device_info(); + + HARMONYLINKLIB_API FOSVerInfo* get_os_version(); - extern "C" HARMONYLINKLIB_API FBattery* get_battery_status(); + HARMONYLINKLIB_API FBattery* get_battery_status(); } diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a index df06019..a68f717 100644 --- a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c624c33e6c41a2c58ffed44332622c9332e6e333ed07ac36f659bc242a58ea2 -size 394806 +oid sha256:662848d6802035c1d74a131316d676e256a65c1537644e787e39c0b3c58209df +size 398236 diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib index 7044fd0..0d13dd8 100644 --- a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:855a36ab6ca665a654cc72b00edb0d59abfec1cf56bcef6b02a0408b82b42e0c -size 789710 +oid sha256:9808e911e50ebb5d2ba8d9cc83254e99a1d26e9740a86bd78a5ceff7f868f06e +size 1346206 From 6ba25c1b0d2c1cbe16426c35e0d9541e24ebe705 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Thu, 6 Jun 2024 21:11:12 +0100 Subject: [PATCH 12/18] Native Linux compile working and works on steam deck --- .../Private/HarmonyLinkLibrary.cpp | 70 +++++++++++++++---- .../Private/Objects/HarmonyLinkGraphics.cpp | 30 ++++++-- .../HarmonyLink/Public/HarmonyLinkLibrary.h | 28 ++++++-- .../Public/Objects/HarmonyLinkGraphics.h | 8 +-- .../HarmonyLinkLib/HarmonyLinkLib.Build.cs | 42 ++++++----- .../bin/Linux/libHarmonyLinkLibShared.so | 4 +- .../Linux/libHarmonyLinkLibStatic_clang++.a | 3 + .../lib/Win64/HarmonyLinkLibStatic.lib | 4 +- 8 files changed, 137 insertions(+), 52 deletions(-) create mode 100644 Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic_clang++.a diff --git a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp index 5197354..a9a0f25 100644 --- a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp +++ b/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp @@ -5,6 +5,21 @@ #include "HarmonyLinkLib.h" +bool UHarmonyLinkLibrary::bIsWineCached = false; +bool UHarmonyLinkLibrary::bIsLinuxCached = false; +bool UHarmonyLinkLibrary::bIsSteamDeckCached = false; +bool UHarmonyLinkLibrary::bCPUInfoCached = false; +bool UHarmonyLinkLibrary::bDeviceInfoCached = false; +bool UHarmonyLinkLibrary::bOSInfoCached = false; + +bool UHarmonyLinkLibrary::bIsWine = false; +bool UHarmonyLinkLibrary::bIsLinux = false; +bool UHarmonyLinkLibrary::bIsSteamDeck = false; + +FCPUInfo UHarmonyLinkLibrary::CachedCPUInfo = FCPUInfo(); +FDevice UHarmonyLinkLibrary::CachedDeviceInfo = FDevice(); +FOSVerInfo UHarmonyLinkLibrary::CachedOSInfo = FOSVerInfo(); + UHarmonyLinkLibrary::UHarmonyLinkLibrary() { if (!HarmonyLinkLib::HL_Init()) @@ -16,34 +31,65 @@ UHarmonyLinkLibrary::UHarmonyLinkLibrary() UE_LOG(LogHarmonyLink, Log, TEXT("HarmonyLinkLib Initialised!")); } -bool UHarmonyLinkLibrary::IsWine() +bool UHarmonyLinkLibrary::IsWine(bool bForce) { - return HarmonyLinkLib::get_is_wine(); + if (!bIsWineCached || bForce) + { + bIsWine = HarmonyLinkLib::get_is_wine(); + bIsWineCached = true; + } + return bIsWine; } -bool UHarmonyLinkLibrary::IsLinux() +bool UHarmonyLinkLibrary::IsLinux(bool bForce) { - return HarmonyLinkLib::get_is_linux(); + if (!bIsLinuxCached || bForce) + { + bIsLinux = HarmonyLinkLib::get_is_linux(); + bIsLinuxCached = true; + } + return bIsLinux; } -bool UHarmonyLinkLibrary::IsSteamDeck() +bool UHarmonyLinkLibrary::IsSteamDeck(bool bForce) { - return GetDeviceInfo().Device == EDevice::STEAM_DECK; + if (!bIsSteamDeckCached || bForce) + { + bIsSteamDeck = GetDeviceInfo().Device == EDevice::STEAM_DECK; + bIsSteamDeckCached = true; + } + return bIsSteamDeck; } -FCPUInfo UHarmonyLinkLibrary::GetCPUInfo() +FCPUInfo UHarmonyLinkLibrary::GetCPUInfo(bool bForce) { - return FCPUInfo(HarmonyLinkLib::get_cpu_info()); + if (!bCPUInfoCached || bForce) + { + CachedCPUInfo = FCPUInfo(HarmonyLinkLib::get_cpu_info()); + bCPUInfoCached = true; + } + + return CachedCPUInfo; } -FDevice UHarmonyLinkLibrary::GetDeviceInfo() +FDevice UHarmonyLinkLibrary::GetDeviceInfo(bool bForce) { - return FDevice(HarmonyLinkLib::get_device_info()); + if (!bDeviceInfoCached || bForce) + { + CachedDeviceInfo = FDevice(HarmonyLinkLib::get_device_info()); + bDeviceInfoCached = true; + } + return CachedDeviceInfo; } -FOSVerInfo UHarmonyLinkLibrary::GetOSInfo() +FOSVerInfo UHarmonyLinkLibrary::GetOSInfo(bool bForce) { - return FOSVerInfo(HarmonyLinkLib::get_os_version()); + if (!bOSInfoCached || bForce) + { + CachedOSInfo = FOSVerInfo(HarmonyLinkLib::get_os_version()); + bOSInfoCached = true; + } + return CachedOSInfo; } FBattery UHarmonyLinkLibrary::GetBatteryStatus() diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index f1e17d6..d22efd4 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -7,6 +7,7 @@ #include "HarmonyLinkLibrary.h" #include "GameFramework/GameUserSettings.h" #include "Kismet/GameplayStatics.h" +#include "HarmonyLinkLib.h" UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; int32 UHarmonyLinkGraphics::_TickRate = 1; @@ -72,8 +73,8 @@ UHarmonyLinkGraphics::UHarmonyLinkGraphics() AddToRoot(); - FWorldDelegates::OnPostWorldInitialization.AddUObject(this, &UHarmonyLinkGraphics::OnPostWorldInitialization); - FWorldDelegates::OnPreWorldFinishDestroy.AddUObject(this, &UHarmonyLinkGraphics::OnWorldEnd); + FWorldDelegates::OnPostWorldInitialization.AddStatic(&UHarmonyLinkGraphics::OnPostWorldInitialization); + FWorldDelegates::OnPreWorldFinishDestroy.AddStatic(&UHarmonyLinkGraphics::OnWorldEnd); Init(); } @@ -82,6 +83,7 @@ UHarmonyLinkGraphics::~UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Verbose, TEXT("~UHarmonyLinkGraphics called.")); FWorldDelegates::OnPostWorldInitialization.RemoveAll(this); + FWorldDelegates::OnPreWorldFinishDestroy.RemoveAll(this); } void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) @@ -352,13 +354,27 @@ void UHarmonyLinkGraphics::DestroySettings() void UHarmonyLinkGraphics::Init() { UE_LOG(LogHarmonyLink, Log, TEXT("Init called.")); + + if (!HarmonyLinkLib::HL_Init()) + { + UE_LOG(LogHarmonyLink, Fatal, TEXT("Failed to initialise HarmonyLinkLib!")); + return; + } + LoadConfig(); const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); - + if (BatteryStatus.HasBattery) { - ApplyProfileInternal(EProfile::BATTERY); + if (BatteryStatus.IsACConnected) + { + ApplyProfileInternal(EProfile::BATTERY); + } + else + { + ApplyProfileInternal(EProfile::CHARGING); + } } } @@ -584,7 +600,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init if (World->IsGameWorld()) { - if (IsValid(_INSTANCE)) + if (IsValid(GetSettings())) { FTimerManager* TimerManager = &World->GetTimerManager(); if (!TimerManager) @@ -598,7 +614,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init World->GetTimerManager().SetTimer(_TickTimerHandle, [TimerManager] { - if (!_INSTANCE) + if (!GetSettings()) { UE_LOG(LogHarmonyLink, Error, TEXT("'This' is destroyed, Clearing timer.")) if (TimerManager) @@ -607,7 +623,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init } return; } - _INSTANCE->Tick(); + GetSettings()->Tick(); }, _TickRate, true); } else diff --git a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h b/Source/HarmonyLink/Public/HarmonyLinkLibrary.h index 19dc5ff..926b6c8 100644 --- a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h +++ b/Source/HarmonyLink/Public/HarmonyLinkLibrary.h @@ -25,29 +25,45 @@ public: // Checks if the game is running under Wine. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static bool IsWine(); + static bool IsWine(bool bForce = false); // Checks if the operating system is Linux. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static bool IsLinux(); + static bool IsLinux(bool bForce = false); // Checks if the game is running on a Steam Deck. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static bool IsSteamDeck(); + static bool IsSteamDeck(bool bForce = false); // Retrieves information about the CPU of the current device. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static FCPUInfo GetCPUInfo(); + static FCPUInfo GetCPUInfo(bool bForce = false); // Retrieves information about the current device. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static FDevice GetDeviceInfo(); + static FDevice GetDeviceInfo(bool bForce = false); // Retrieves information about the operating system of the current device. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") - static FOSVerInfo GetOSInfo(); + static FOSVerInfo GetOSInfo(bool bForce = false); // Retrieves the current battery status of the device. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static FBattery GetBatteryStatus(); + +private: + static bool bIsWineCached; + static bool bIsWine; + static bool bIsLinuxCached; + static bool bIsLinux; + static bool bIsSteamDeckCached; + static bool bIsSteamDeck; + + static bool bCPUInfoCached; + static bool bDeviceInfoCached; + static bool bOSInfoCached; + + static FCPUInfo CachedCPUInfo; + static FDevice CachedDeviceInfo; + static FOSVerInfo CachedOSInfo; }; diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 80d2475..0147020 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -6,7 +6,7 @@ #include "Enums/Profile.h" #include "Structs/SettingsProfile.h" -#include "UObject/Object.h" +#include "Kismet/BlueprintFunctionLibrary.h" #include "HarmonyLinkGraphics.generated.h" DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnProfileChanged, EProfile, Profile); @@ -17,7 +17,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBatteryLevelChanged, int32, Batte * */ UCLASS(Blueprintable, config="HarmonyLink") -class HARMONYLINK_API UHarmonyLinkGraphics : public UObject +class HARMONYLINK_API UHarmonyLinkGraphics : public UBlueprintFunctionLibrary { GENERATED_BODY() @@ -390,7 +390,7 @@ private: * * @note Uses UE_LOG for logging errors and informational messages. */ - void OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS); + static void OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS); /** * @brief Handles actions to be performed when the world ends. @@ -406,7 +406,7 @@ private: * * @note Uses UE_LOG for logging errors and informational messages. */ - void OnWorldEnd(UWorld* World); + static void OnWorldEnd(UWorld* World); /** * @brief Resets the singleton instance of the UHarmonyLinkGraphics settings. diff --git a/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs index 0dc8bbd..2ce9175 100644 --- a/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs +++ b/Source/ThirdParty/HarmonyLinkLib/HarmonyLinkLib.Build.cs @@ -43,26 +43,30 @@ public class HarmonyLinkLib : ModuleRules } else if (Target.Platform == UnrealTargetPlatform.Linux) { - PublicDefinitions.Add("HARMONYLINKLIB_SHARED=1"); + PublicDefinitions.Add("HARMONYLINKLIB_STATIC=1"); //Console.WriteLine("Building Linux"); PublicDefinitions.Add("BUILD_LINUX=1"); - string libPath = Path.Combine(ModuleDirectory, "bin", platformString, "libHarmonyLinkLibShared.so"); - //Console.WriteLine("Library Path: " + libPath); - PublicAdditionalLibraries.Add(libPath); - - // Add the C++ standard library explicitly - //string toolchainLibPath = "E:/UnrealToolChains/v22_clang-16.0.6-centos7/x86_64-unknown-linux-gnu/lib"; - //PublicSystemLibraryPaths.Add(toolchainLibPath); - RuntimeDependencies.Add(libPath); - } - // I shall include this if anyone wishes to provide Mac binaries of HarmonyLink but these are not included by default as I don't own one. - else if (Target.Platform == UnrealTargetPlatform.Mac) - { - PublicDefinitions.Add("HARMONYLINKLIB_SHARED=1"); - PublicDefinitions.Add("BUILD_MACOS=1"); - string dynlibPath = Path.Combine(ModuleDirectory, "lib", platformString, "libHarmonyLinkLibStatic.a"); - //Console.WriteLine("Dynamic Library Path: " + dynlibPath); - RuntimeDependencies.Add(dynlibPath); - } + string dllPath = Path.Combine(ModuleDirectory, "lib", platformString, "libHarmonyLinkLibStatic_clang++.a"); + //string dllTargetPath = "$(TargetOutputDir)/libHarmonyLinkLibShared.so"; + //Console.WriteLine("Library Path: " + libPath); + PublicAdditionalLibraries.Add(dllPath); + + // Ensure the proper linking of standard C++ libraries + //PublicSystemLibraries.Add("stdc++"); + //PublicSystemLibraries.Add("c++abi"); + //PublicSystemLibraries.Add("m"); // Math library + //PublicSystemLibraries.Add("pthread"); // POSIX threads library + + // Add the C++ standard library explicitly if needed + PublicSystemLibraries.Add("c++"); + + // Ensure linking with libc and other necessary libraries + //PublicSystemLibraries.Add("dl"); // Dynamic linking loader + //PublicSystemLibraries.Add("rt"); // Real-time library + + // Add any other libraries that might be needed + //PublicSystemLibraries.Add("gcc_s"); // GCC support library + //PublicSystemLibraries.Add("gcc"); // GCC compiler support library + } } } diff --git a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so index 81f0188..7f4480a 100644 --- a/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so +++ b/Source/ThirdParty/HarmonyLinkLib/bin/Linux/libHarmonyLinkLibShared.so @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7690f748eb883b77813eab76bb41e6264584c2c477ad19e0ce977378b11004a1 -size 252920 +oid sha256:f4a3966f9c7018d84e684baf61183d07758e13633decea1bd84daa44cf2b8ca1 +size 813728 diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic_clang++.a b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic_clang++.a new file mode 100644 index 0000000..27b9163 --- /dev/null +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Linux/libHarmonyLinkLibStatic_clang++.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6977100139b596b50b1b46bb91fb1979b43c3fc991ddcf5b6f4e4f4de1c992db +size 1775982 diff --git a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib index 0d13dd8..b820888 100644 --- a/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib +++ b/Source/ThirdParty/HarmonyLinkLib/lib/Win64/HarmonyLinkLibStatic.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9808e911e50ebb5d2ba8d9cc83254e99a1d26e9740a86bd78a5ceff7f868f06e -size 1346206 +oid sha256:ef8d92a66b36b8899e5fc2fe2e51e0753d11e7b161790013ee3c9a5c804fefa7 +size 1425358 From a2d4452691809c73b123341e55734f247b526eef Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Sun, 7 Jul 2024 19:22:41 +0100 Subject: [PATCH 13/18] Fixed world delegates --- .../Private/Objects/HarmonyLinkGraphics.cpp | 43 ++++++++++--------- .../Public/Objects/HarmonyLinkGraphics.h | 1 - 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index d22efd4..068f7b2 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -58,27 +58,6 @@ TMap> UHarmonyLinkGraphics::_DefaultSettings }} }; -UHarmonyLinkGraphics::UHarmonyLinkGraphics() -{ - UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); - if (_INSTANCE != this) - { - if (_INSTANCE) - { - DestroySettings(); - } - - _INSTANCE = this; - } - - AddToRoot(); - - FWorldDelegates::OnPostWorldInitialization.AddStatic(&UHarmonyLinkGraphics::OnPostWorldInitialization); - FWorldDelegates::OnPreWorldFinishDestroy.AddStatic(&UHarmonyLinkGraphics::OnWorldEnd); - - Init(); -} - UHarmonyLinkGraphics::~UHarmonyLinkGraphics() { UE_LOG(LogHarmonyLink, Verbose, TEXT("~UHarmonyLinkGraphics called.")); @@ -276,6 +255,7 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() // Proceed to create a new singleton instance _INSTANCE = NewObject(); + _INSTANCE->Init(); return _INSTANCE; } @@ -353,6 +333,22 @@ void UHarmonyLinkGraphics::DestroySettings() void UHarmonyLinkGraphics::Init() { + UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); + if (_INSTANCE != this) + { + if (_INSTANCE) + { + DestroySettings(); + } + + _INSTANCE = this; + } + + AddToRoot(); + + FWorldDelegates::OnPostWorldInitialization.AddStatic(&UHarmonyLinkGraphics::OnPostWorldInitialization); + FWorldDelegates::OnPreWorldFinishDestroy.AddStatic(&UHarmonyLinkGraphics::OnWorldEnd); + UE_LOG(LogHarmonyLink, Log, TEXT("Init called.")); if (!HarmonyLinkLib::HL_Init()) @@ -650,6 +646,11 @@ void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) UE_LOG(LogHarmonyLink, Error, TEXT("World Already destroyed")) return; } + + if (!World->IsGameWorld()) + { + return; + } if(World->GetTimerManager().TimerExists(_TickTimerHandle)) { diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h index 0147020..d6e3418 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h @@ -22,7 +22,6 @@ class HARMONYLINK_API UHarmonyLinkGraphics : public UBlueprintFunctionLibrary GENERATED_BODY() public: - UHarmonyLinkGraphics(); virtual ~UHarmonyLinkGraphics() override; UPROPERTY(BlueprintAssignable) From 8ba9caeb27bc8048682e08c113612ef349f22bf9 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Sun, 7 Jul 2024 19:27:17 +0100 Subject: [PATCH 14/18] Discard return from GetConfig --- Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp index 068f7b2..d3f0fa6 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp @@ -71,7 +71,7 @@ void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); // Load the settings into the map - GetConfig(); + FConfigFile* _ = GetConfig(); DebugPrintProfiles(); } From 9a5194dc3a82f9ca7822d1b65452605aab53afde Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Mon, 8 Jul 2024 00:32:52 +0100 Subject: [PATCH 15/18] Rename plugin to HarmonyLinkUE --- HarmonyLink.uplugin => HarmonyLinkUE.uplugin | 16 +- Source/HarmonyLink/Private/HarmonyLink.cpp | 24 --- .../HarmonyLinkSettings.Build.cs | 28 ++++ .../Private/HarmonyLinkSettings.cpp | 22 +++ .../Private/Objects/HarmonyLinkGraphics.cpp | 141 +++++++++--------- .../Private/Structs/HLConfigValue.cpp | 0 .../Public/Enums/Profile.h | 0 .../Public/HarmonyLinkSettings.h | 13 ++ .../Public/Objects/HarmonyLinkGraphics.h | 4 +- .../Public/Structs/HLConfigValue.h | 0 .../Public/Structs/SettingsProfile.h | 0 .../HarmonyLinkUE.Build.cs} | 4 +- .../Private/HarmonyLinkLibrary.cpp | 13 +- .../HarmonyLinkUE/Private/HarmonyLinkUE.cpp | 22 +++ .../Private/Structs/Battery.cpp | 0 .../Private/Structs/CPUInfo.cpp | 0 .../Private/Structs/Device.cpp | 0 .../Private/Structs/OSVerInfo.cpp | 0 .../Public/Enums/DeviceEnum.h | 0 .../Public/Enums/Platform.h | 0 .../Public/HarmonyLinkLibrary.h | 8 +- .../Public/HarmonyLinkUE.h} | 2 +- .../Public/Structs/Battery.h | 0 .../Public/Structs/CPUInfo.h | 0 .../Public/Structs/Device.h | 0 .../Public/Structs/OSVerInfo.h | 0 26 files changed, 190 insertions(+), 107 deletions(-) rename HarmonyLink.uplugin => HarmonyLinkUE.uplugin (77%) delete mode 100644 Source/HarmonyLink/Private/HarmonyLink.cpp create mode 100644 Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs create mode 100644 Source/HarmonyLinkSettings/Private/HarmonyLinkSettings.cpp rename Source/{HarmonyLink => HarmonyLinkSettings}/Private/Objects/HarmonyLinkGraphics.cpp (75%) rename Source/{HarmonyLink => HarmonyLinkSettings}/Private/Structs/HLConfigValue.cpp (100%) rename Source/{HarmonyLink => HarmonyLinkSettings}/Public/Enums/Profile.h (100%) create mode 100644 Source/HarmonyLinkSettings/Public/HarmonyLinkSettings.h rename Source/{HarmonyLink => HarmonyLinkSettings}/Public/Objects/HarmonyLinkGraphics.h (99%) rename Source/{HarmonyLink => HarmonyLinkSettings}/Public/Structs/HLConfigValue.h (100%) rename Source/{HarmonyLink => HarmonyLinkSettings}/Public/Structs/SettingsProfile.h (100%) rename Source/{HarmonyLink/HarmonyLink.Build.cs => HarmonyLinkUE/HarmonyLinkUE.Build.cs} (89%) rename Source/{HarmonyLink => HarmonyLinkUE}/Private/HarmonyLinkLibrary.cpp (91%) create mode 100644 Source/HarmonyLinkUE/Private/HarmonyLinkUE.cpp rename Source/{HarmonyLink => HarmonyLinkUE}/Private/Structs/Battery.cpp (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Private/Structs/CPUInfo.cpp (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Private/Structs/Device.cpp (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Private/Structs/OSVerInfo.cpp (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Enums/DeviceEnum.h (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Enums/Platform.h (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/HarmonyLinkLibrary.h (89%) rename Source/{HarmonyLink/Public/HarmonyLink.h => HarmonyLinkUE/Public/HarmonyLinkUE.h} (83%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Structs/Battery.h (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Structs/CPUInfo.h (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Structs/Device.h (100%) rename Source/{HarmonyLink => HarmonyLinkUE}/Public/Structs/OSVerInfo.h (100%) diff --git a/HarmonyLink.uplugin b/HarmonyLinkUE.uplugin similarity index 77% rename from HarmonyLink.uplugin rename to HarmonyLinkUE.uplugin index 637d348..8b1b3fe 100644 --- a/HarmonyLink.uplugin +++ b/HarmonyLinkUE.uplugin @@ -2,7 +2,7 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "HarmonyLink", + "FriendlyName": "HarmonyLinkUE", "Description": "Revolutionize handheld gaming with adaptive game settings. Optimize graphics and gameplay experience based on real-time system metrics. Open-source project empowering developers to enhance games on portable devices", "Category": "Handheld", "CreatedBy": "Jordon Brooks", @@ -16,10 +16,18 @@ "Installed": false, "Modules": [ { - "Name": "HarmonyLink", + "Name": "HarmonyLinkUE", "Type": "Runtime", "LoadingPhase": "PreDefault", - "WhitelistPlatforms": ["Win64", "Linux"] + "WhitelistPlatforms": [ + "Win64", + "Linux" + ] + }, + { + "Name": "HarmonyLinkSettings", + "Type": "Runtime", + "LoadingPhase": "Default" } ] -} +} \ No newline at end of file diff --git a/Source/HarmonyLink/Private/HarmonyLink.cpp b/Source/HarmonyLink/Private/HarmonyLink.cpp deleted file mode 100644 index 167806e..0000000 --- a/Source/HarmonyLink/Private/HarmonyLink.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2024 Jordon Brooks - -#include "HarmonyLink.h" -#include "Modules/ModuleManager.h" -#include "Objects/HarmonyLinkGraphics.h" - -#define LOCTEXT_NAMESPACE "FHarmonyLinkModule" - -DEFINE_LOG_CATEGORY(LogHarmonyLink); - -void FHarmonyLinkModule::StartupModule() -{ - -} - -void FHarmonyLinkModule::ShutdownModule() -{ - // Ensure we safely destroy our singleton instance - UHarmonyLinkGraphics::DestroySettings(); -} - -#undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FHarmonyLinkModule, HarmonyLink) diff --git a/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs b/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs new file mode 100644 index 0000000..69f4fa3 --- /dev/null +++ b/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs @@ -0,0 +1,28 @@ +using UnrealBuildTool; + +public class HarmonyLinkSettings : ModuleRules +{ + public HarmonyLinkSettings(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + + "HarmonyLinkUE" + } + ); + } +} diff --git a/Source/HarmonyLinkSettings/Private/HarmonyLinkSettings.cpp b/Source/HarmonyLinkSettings/Private/HarmonyLinkSettings.cpp new file mode 100644 index 0000000..b2ea795 --- /dev/null +++ b/Source/HarmonyLinkSettings/Private/HarmonyLinkSettings.cpp @@ -0,0 +1,22 @@ +#include "HarmonyLinkSettings.h" + +#include "Objects/HarmonyLinkGraphics.h" + +#define LOCTEXT_NAMESPACE "FHarmonyLinkSettingsModule" + +DEFINE_LOG_CATEGORY(LogHarmonyLinkSettings); + +void FHarmonyLinkSettingsModule::StartupModule() +{ + +} + +void FHarmonyLinkSettingsModule::ShutdownModule() +{ + // Ensure we safely destroy our singleton instance + UHarmonyLinkGraphics::DestroySettings(); +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FHarmonyLinkSettingsModule, HarmonyLinkSettings) diff --git a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp similarity index 75% rename from Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp rename to Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp index d3f0fa6..1405b51 100644 --- a/Source/HarmonyLink/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp @@ -3,11 +3,10 @@ #include "Objects/HarmonyLinkGraphics.h" #include "ComponentRecreateRenderStateContext.h" -#include "HarmonyLink.h" +#include "HarmonyLinkSettings.h" #include "HarmonyLinkLibrary.h" #include "GameFramework/GameUserSettings.h" #include "Kismet/GameplayStatics.h" -#include "HarmonyLinkLib.h" UHarmonyLinkGraphics* UHarmonyLinkGraphics::_INSTANCE = nullptr; int32 UHarmonyLinkGraphics::_TickRate = 1; @@ -60,14 +59,14 @@ TMap> UHarmonyLinkGraphics::_DefaultSettings UHarmonyLinkGraphics::~UHarmonyLinkGraphics() { - UE_LOG(LogHarmonyLink, Verbose, TEXT("~UHarmonyLinkGraphics called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("~UHarmonyLinkGraphics called.")); FWorldDelegates::OnPostWorldInitialization.RemoveAll(this); FWorldDelegates::OnPreWorldFinishDestroy.RemoveAll(this); } void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) { - UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadConfig called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("LoadConfig called.")); QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_LoadSettings); // Load the settings into the map @@ -78,7 +77,7 @@ void UHarmonyLinkGraphics::LoadConfig(const bool bForceReload) bool UHarmonyLinkGraphics::LoadSettingsFromConfig(FConfigFile* ConfigFile) const { - UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadSettingsFromConfig called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("LoadSettingsFromConfig called.")); //const FString Filename = "HarmonyLink"; //GetConfigDirectoryFile(bLoadDefaults); // Load each profile section @@ -90,19 +89,19 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig(FConfigFile* ConfigFile) const if (!ConfigFile->GetBool(*SectionName, *KeyName, _bAutomaticSwitch)) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load bAutomaticSwitch from config")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed to load bAutomaticSwitch from config")); bLoaded = false; } else { - UE_LOG(LogHarmonyLink, Log, TEXT("Loaded bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Loaded bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); } for (const TPair& Profile : _ProfileNames) { if (!LoadSection(ConfigFile, Profile)) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load section: '%s'"), *Profile.Value.ToString()); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed to load section: '%s'"), *Profile.Value.ToString()); bLoaded = false; } } @@ -110,17 +109,17 @@ bool UHarmonyLinkGraphics::LoadSettingsFromConfig(FConfigFile* ConfigFile) const // Check if all profiles and settings were loaded successfully if (bLoaded) { - UE_LOG(LogHarmonyLink, Log, TEXT("Successfully loaded config.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Successfully loaded config.")); return true; } - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to load config file.")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed to load config file.")); return false; } bool UHarmonyLinkGraphics::LoadSection(FConfigFile* ConfigFile, const TPair Profile) { - UE_LOG(LogHarmonyLink, Verbose, TEXT("LoadSection called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("LoadSection called.")); if (!ensureMsgf(ConfigFile, TEXT("ConfigFile is nullptr!"))) return false; const FName& SectionName = Profile.Value; @@ -184,13 +183,13 @@ bool UHarmonyLinkGraphics::LoadSection(FConfigFile* ConfigFile, const TPairToString()); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Applying '%s': Value='%s', Type='%s' to profile '%s'."), *Setting.ToString(), *TypeString, *ValueString, *ProfileName->ToString()); // Find the settings associated with the profile FSettingsProfile* SettingsProfile = _Profiles.Find(Profile); if (!SettingsProfile) { - UE_LOG(LogHarmonyLink, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); return; } @@ -246,7 +245,7 @@ void UHarmonyLinkGraphics::SetSetting(const EProfile Profile, const FName Settin UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() { - UE_LOG(LogHarmonyLink, VeryVerbose, TEXT("GetSettings called.")); + UE_LOG(LogHarmonyLinkSettings, VeryVerbose, TEXT("GetSettings called.")); // Check if we already initialised if (_INSTANCE) { @@ -262,7 +261,7 @@ UHarmonyLinkGraphics* UHarmonyLinkGraphics::GetSettings() FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) { - UE_LOG(LogHarmonyLink, Verbose, TEXT("GetSettingProfile called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("GetSettingProfile called.")); // Ignore if HarmonyLinkSettings is disabled if (Profile == EProfile::NONE) { @@ -274,7 +273,7 @@ FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) if (!ProfileName) { - UE_LOG(LogHarmonyLink, Error, TEXT("Profile not found.")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Profile not found.")); return FSettingsProfile(); } @@ -283,7 +282,7 @@ FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) if (!SettingsProfile) { - UE_LOG(LogHarmonyLink, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("No settings found for profile %s."), *ProfileName->ToString()); return FSettingsProfile(); } @@ -292,29 +291,29 @@ FSettingsProfile UHarmonyLinkGraphics::GetSettingProfile(const EProfile Profile) EProfile UHarmonyLinkGraphics::GetActiveProfile() const { - UE_LOG(LogHarmonyLink, Verbose, TEXT("GetActiveProfile called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("GetActiveProfile called.")); return _ActiveProfile; } void UHarmonyLinkGraphics::SetAutomaticSwitching(const bool bAutomaticSwitch) { - UE_LOG(LogHarmonyLink, Verbose, TEXT("SetAutomaticSwitching called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("SetAutomaticSwitching called.")); _bAutomaticSwitch = bAutomaticSwitch; OnAutomaticSwitchChanged.Broadcast(_bAutomaticSwitch); } bool UHarmonyLinkGraphics::GetAutomaticSwitching() const { - UE_LOG(LogHarmonyLink, Verbose, TEXT("GetAutomaticSwitching called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("GetAutomaticSwitching called.")); return _bAutomaticSwitch; } void UHarmonyLinkGraphics::DestroySettings() { - UE_LOG(LogHarmonyLink, Log, TEXT("DestroySettings called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("DestroySettings called.")); if (_INSTANCE) { - UE_LOG(LogHarmonyLink, Log, TEXT("Destroying UHarmonyLinkGraphics.")) + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Destroying UHarmonyLinkGraphics.")) FWorldDelegates::OnPostWorldInitialization.RemoveAll(_INSTANCE); FWorldDelegates::OnPreWorldFinishDestroy.RemoveAll(_INSTANCE); _INSTANCE->RemoveFromRoot(); @@ -333,7 +332,7 @@ void UHarmonyLinkGraphics::DestroySettings() void UHarmonyLinkGraphics::Init() { - UE_LOG(LogHarmonyLink, Warning, TEXT("HarmonyLinkGraphics initialized.")); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("HarmonyLinkGraphics initialized.")); if (_INSTANCE != this) { if (_INSTANCE) @@ -349,11 +348,11 @@ void UHarmonyLinkGraphics::Init() FWorldDelegates::OnPostWorldInitialization.AddStatic(&UHarmonyLinkGraphics::OnPostWorldInitialization); FWorldDelegates::OnPreWorldFinishDestroy.AddStatic(&UHarmonyLinkGraphics::OnWorldEnd); - UE_LOG(LogHarmonyLink, Log, TEXT("Init called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Init called.")); - if (!HarmonyLinkLib::HL_Init()) + if (!UHarmonyLinkLibrary::IsInitialised()) { - UE_LOG(LogHarmonyLink, Fatal, TEXT("Failed to initialise HarmonyLinkLib!")); + UE_LOG(LogHarmonyLinkSettings, Fatal, TEXT("Failed to initialise HarmonyLinkLib!")); return; } @@ -376,7 +375,7 @@ void UHarmonyLinkGraphics::Init() void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const { - UE_LOG(LogHarmonyLink, Log, TEXT("Intermal_SaveConfig called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Intermal_SaveConfig called.")); QUICK_SCOPE_CYCLE_COUNTER(HarmonyLinkGraphics_SaveConfig); const FString Filename = GetConfigDirectoryFile(bDefaultConfig); @@ -386,14 +385,14 @@ void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const // Save the _bAutomaticSwitch variable GConfig->SetBool(*SectionName, *KeyName, _bAutomaticSwitch, Filename); - UE_LOG(LogHarmonyLink, Log, TEXT("Saving bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Saving bAutomaticSwitch: %s"), _bAutomaticSwitch ? TEXT("true") : TEXT("false")); for (const TPair& Profile : _Profiles) { SaveSection(Profile.Value, bDefaultConfig); } - UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Flushing file: '%s'"), *Filename); GetConfig()->Dirty = true; // You'd think that Write would actually write something but for some // reason even if it outputs a success the file doesn't actually get created. @@ -404,7 +403,7 @@ void UHarmonyLinkGraphics::Intermal_SaveConfig(const bool bDefaultConfig) const void UHarmonyLinkGraphics::Tick() { - UE_LOG(LogHarmonyLink, VeryVerbose, TEXT("Tick called.")); + UE_LOG(LogHarmonyLinkSettings, VeryVerbose, TEXT("Tick called.")); const FBattery BatteryStatus = UHarmonyLinkLibrary::GetBatteryStatus(); if (BatteryStatus.BatteryPercent != _LastBatteryPercentage) @@ -439,18 +438,18 @@ void UHarmonyLinkGraphics::Tick() void UHarmonyLinkGraphics::CreateDefaultConfigFile() const { - UE_LOG(LogHarmonyLink, Log, TEXT("CreateDefaultConfigFile called.")); - UE_LOG(LogHarmonyLink, Log, TEXT("Creating default config file.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("CreateDefaultConfigFile called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Creating default config file.")); LoadDefaults(); Intermal_SaveConfig(true); - UE_LOG(LogHarmonyLink, Log, TEXT("Default config file created.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Default config file created.")); } FString UHarmonyLinkGraphics::GetConfigDirectoryFile(const bool bDefaultFolder) { - UE_LOG(LogHarmonyLink, Log, TEXT("GetConfigDirectoryFile called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("GetConfigDirectoryFile called.")); FString ConfigFileName = bDefaultFolder ? TEXT("DefaultHarmonyLink.ini") : TEXT("HarmonyLink.ini"); FString ConfigDirectory = bDefaultFolder ? FPaths::ProjectConfigDir() : FPaths::Combine(FPaths::GeneratedConfigDir(), UGameplayStatics::GetPlatformName()); @@ -460,7 +459,7 @@ FString UHarmonyLinkGraphics::GetConfigDirectoryFile(const bool bDefaultFolder) void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, const bool bDefaultConfig, const bool bFlush) const { - UE_LOG(LogHarmonyLink, Log, TEXT("SaveSection called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("SaveSection called.")); if (GConfig) { const FString Filename = GetConfigDirectoryFile(bDefaultConfig); @@ -493,10 +492,10 @@ void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, GConfig->SetString(*SettingsProfile.SectionName.ToString(), *Setting.Key.ToString(), *ConfigValue, Filename); } - UE_LOG(LogHarmonyLink, Log, TEXT("Saving config file: '%s'"), *Filename); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Saving config file: '%s'"), *Filename); if (bFlush) { - UE_LOG(LogHarmonyLink, Log, TEXT("Flushing file: '%s'"), *Filename); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Flushing file: '%s'"), *Filename); GConfig->Flush(false, Filename); } } @@ -504,7 +503,7 @@ void UHarmonyLinkGraphics::SaveSection(const FSettingsProfile& SettingsProfile, void UHarmonyLinkGraphics::LoadDefaults() const { - UE_LOG(LogHarmonyLink, Log, TEXT("LoadDefaults called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("LoadDefaults called.")); _Profiles.Reset(); @@ -525,11 +524,11 @@ void UHarmonyLinkGraphics::LoadDefaults() const bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) { - UE_LOG(LogHarmonyLink, Log, TEXT("ApplyProfileInternal called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("ApplyProfileInternal called.")); // If the profile is None, revert to the original user game settings if (Profile == EProfile::NONE) { - UE_LOG(LogHarmonyLink, Log, TEXT("Reverting to original user game settings.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Reverting to original user game settings.")); if (UGameUserSettings* UserSettings = GEngine->GetGameUserSettings()) { @@ -537,11 +536,11 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) UserSettings->ApplySettings(true); _ActiveProfile = EProfile::NONE; OnProfileChanged.Broadcast(_ActiveProfile); - UE_LOG(LogHarmonyLink, Log, TEXT("Original user game settings applied.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Original user game settings applied.")); return true; } - UE_LOG(LogHarmonyLink, Warning, TEXT("Failed to get user game settings.")); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("Failed to get user game settings.")); return false; } @@ -551,18 +550,18 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) if (!ProfileName) { - UE_LOG(LogHarmonyLink, Error, TEXT("Profile not found.")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Profile not found.")); return false; } - UE_LOG(LogHarmonyLink, Log, TEXT("Applying profile %s."), *ProfileName->ToString()); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Applying profile %s."), *ProfileName->ToString()); // Find the settings associated with the profile FSettingsProfile* SettingsProfile = _Profiles.Find(Profile); if (!SettingsProfile) { - UE_LOG(LogHarmonyLink, Warning, TEXT("No settings found for profile %s."), *ProfileName->ToString()); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("No settings found for profile %s."), *ProfileName->ToString()); return false; } @@ -573,7 +572,7 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) for (const TPair& Setting : SettingsProfile->Settings) { // Example of logging each setting being applied - UE_LOG(LogHarmonyLink, Log, TEXT("Patching CVar override: %s = %s"), + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Patching CVar override: %s = %s"), *Setting.Key.ToString(), *Setting.Value.ToString()); ApplySetting(Setting); @@ -587,10 +586,10 @@ bool UHarmonyLinkGraphics::ApplyProfileInternal(const EProfile Profile) void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::InitializationValues IVS) { - UE_LOG(LogHarmonyLink, Log, TEXT("OnPostWorldInitialization called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("OnPostWorldInitialization called.")); if (!World || !World->IsValidLowLevel()) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to Hook into World Initialisation!")) + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed to Hook into World Initialisation!")) return; } @@ -601,7 +600,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init FTimerManager* TimerManager = &World->GetTimerManager(); if (!TimerManager) { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed get TimerManager!")) + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed get TimerManager!")) return; } @@ -612,7 +611,7 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init { if (!GetSettings()) { - UE_LOG(LogHarmonyLink, Error, TEXT("'This' is destroyed, Clearing timer.")) + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("'This' is destroyed, Clearing timer.")) if (TimerManager) { TimerManager->ClearTimer(_TickTimerHandle); @@ -624,12 +623,12 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init } else { - UE_LOG(LogHarmonyLink, Error, TEXT("Error: Timer already exists.")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Error: Timer already exists.")); } } else { - UE_LOG(LogHarmonyLink, Error, TEXT("'This' is nullptr!")); + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("'This' is nullptr!")); } } else @@ -640,10 +639,10 @@ void UHarmonyLinkGraphics::OnPostWorldInitialization(UWorld* World, UWorld::Init void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) { - UE_LOG(LogHarmonyLink, Log, TEXT("OnWorldEnd(UWorld* World) called.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("OnWorldEnd(UWorld* World) called.")); if (!World) { - UE_LOG(LogHarmonyLink, Error, TEXT("World Already destroyed")) + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("World Already destroyed")) return; } @@ -663,7 +662,7 @@ void UHarmonyLinkGraphics::OnWorldEnd(UWorld* World) bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisableAutomaticSwitch) { - UE_LOG(LogHarmonyLink, Log, TEXT("Applying Profile.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Applying Profile.")); // Manual profile change, turn off automatic switching if (bDisableAutomaticSwitch) { @@ -675,7 +674,7 @@ bool UHarmonyLinkGraphics::ApplyProfile(const EProfile Profile, const bool bDisa void UHarmonyLinkGraphics::ApplySetting(const TPair& Setting) { - UE_LOG(LogHarmonyLink, Log, TEXT("Applying settings.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Applying settings.")); // Apply the setting based on the key (CVar) IConsoleManager& ConsoleManager = IConsoleManager::Get(); IConsoleVariable* CVar = ConsoleManager.FindConsoleVariable(*Setting.Key.ToString()); @@ -697,31 +696,31 @@ void UHarmonyLinkGraphics::ApplySetting(const TPair& Sett break; default: - UE_LOG(LogHarmonyLink, Warning, TEXT("Unsupported value type for setting: %s"), *Setting.Key.ToString()); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("Unsupported value type for setting: %s"), *Setting.Key.ToString()); break; } } else { - UE_LOG(LogHarmonyLink, Warning, TEXT("Console variable not found: %s"), *Setting.Key.ToString()); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("Console variable not found: %s"), *Setting.Key.ToString()); } } void UHarmonyLinkGraphics::DebugPrintProfiles() const { - UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles started.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("DebugPrintProfiles started.")); for (TPair Profile : _Profiles) { PrintDebugSection(Profile.Value); } - UE_LOG(LogHarmonyLink, Log, TEXT("DebugPrintProfiles completed.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("DebugPrintProfiles completed.")); } FConfigFile* UHarmonyLinkGraphics::GetConfig() const { - UE_LOG(LogHarmonyLink, Verbose, TEXT("GetConfig Called.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("GetConfig Called.")); if (_ConfigFile) { return _ConfigFile.Get(); @@ -731,7 +730,7 @@ FConfigFile* UHarmonyLinkGraphics::GetConfig() const if (!ConfigFile) { - UE_LOG(LogHarmonyLink, Warning, TEXT("Config file not found, attempting to read DefaultHarmonyLink.ini.")); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("Config file not found, attempting to read DefaultHarmonyLink.ini.")); // Look in ProjectFolder->Config->DefaultHarmonyLink.ini ConfigFile = GConfig->Find(GetConfigDirectoryFile(true), true); } @@ -744,14 +743,14 @@ FConfigFile* UHarmonyLinkGraphics::GetConfig() const if (ConfigFile) { - UE_LOG(LogHarmonyLink, Verbose, TEXT("Setting up config.")); + UE_LOG(LogHarmonyLinkSettings, Verbose, TEXT("Setting up config.")); ConfigFile->Name = "HarmonyLink"; LoadSettingsFromConfig(ConfigFile); _ConfigFile = MakeShareable(ConfigFile); } else { - UE_LOG(LogHarmonyLink, Error, TEXT("Failed to make config variable!")) + UE_LOG(LogHarmonyLinkSettings, Error, TEXT("Failed to make config variable!")) return nullptr; } @@ -760,7 +759,7 @@ FConfigFile* UHarmonyLinkGraphics::GetConfig() const void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) { - UE_LOG(LogHarmonyLink, Warning, TEXT("[%s]"), *SettingsProfile.SectionName.ToString()); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("[%s]"), *SettingsProfile.SectionName.ToString()); for (const auto& Setting : SettingsProfile.Settings) { @@ -787,13 +786,13 @@ void UHarmonyLinkGraphics::PrintDebugSection(FSettingsProfile& SettingsProfile) break; } - UE_LOG(LogHarmonyLink, Warning, TEXT("Key: %s = V=%s, T=%s "), *Setting.Key.ToString(), *ValueString, *TypeString); + UE_LOG(LogHarmonyLinkSettings, Warning, TEXT("Key: %s = V=%s, T=%s "), *Setting.Key.ToString(), *ValueString, *TypeString); } } void UHarmonyLinkGraphics::ResetInstance() { - UE_LOG(LogHarmonyLink, Log, TEXT("Resetting instance.")); + UE_LOG(LogHarmonyLinkSettings, Log, TEXT("Resetting instance.")); _INSTANCE->DestroySettings(); GetSettings(); } diff --git a/Source/HarmonyLink/Private/Structs/HLConfigValue.cpp b/Source/HarmonyLinkSettings/Private/Structs/HLConfigValue.cpp similarity index 100% rename from Source/HarmonyLink/Private/Structs/HLConfigValue.cpp rename to Source/HarmonyLinkSettings/Private/Structs/HLConfigValue.cpp diff --git a/Source/HarmonyLink/Public/Enums/Profile.h b/Source/HarmonyLinkSettings/Public/Enums/Profile.h similarity index 100% rename from Source/HarmonyLink/Public/Enums/Profile.h rename to Source/HarmonyLinkSettings/Public/Enums/Profile.h diff --git a/Source/HarmonyLinkSettings/Public/HarmonyLinkSettings.h b/Source/HarmonyLinkSettings/Public/HarmonyLinkSettings.h new file mode 100644 index 0000000..a51a458 --- /dev/null +++ b/Source/HarmonyLinkSettings/Public/HarmonyLinkSettings.h @@ -0,0 +1,13 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogHarmonyLinkSettings, Log, All); + +class FHarmonyLinkSettingsModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h b/Source/HarmonyLinkSettings/Public/Objects/HarmonyLinkGraphics.h similarity index 99% rename from Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h rename to Source/HarmonyLinkSettings/Public/Objects/HarmonyLinkGraphics.h index d6e3418..782fb98 100644 --- a/Source/HarmonyLink/Public/Objects/HarmonyLinkGraphics.h +++ b/Source/HarmonyLinkSettings/Public/Objects/HarmonyLinkGraphics.h @@ -17,7 +17,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnBatteryLevelChanged, int32, Batte * */ UCLASS(Blueprintable, config="HarmonyLink") -class HARMONYLINK_API UHarmonyLinkGraphics : public UBlueprintFunctionLibrary +class HARMONYLINKSETTINGS_API UHarmonyLinkGraphics : public UBlueprintFunctionLibrary { GENERATED_BODY() @@ -84,7 +84,7 @@ public: * @note Uses UE_LOG for logging the update process and any errors. */ UFUNCTION(BlueprintCallable, Category="HarmonyLink Settings") - void SetSetting(EProfile Profile, FName Setting, FHLConfigValue Value); + void SetSetting(EProfile Profile, FName Setting, const FHLConfigValue& Value); /** * @brief Applies the specified graphics profile. diff --git a/Source/HarmonyLink/Public/Structs/HLConfigValue.h b/Source/HarmonyLinkSettings/Public/Structs/HLConfigValue.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/HLConfigValue.h rename to Source/HarmonyLinkSettings/Public/Structs/HLConfigValue.h diff --git a/Source/HarmonyLink/Public/Structs/SettingsProfile.h b/Source/HarmonyLinkSettings/Public/Structs/SettingsProfile.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/SettingsProfile.h rename to Source/HarmonyLinkSettings/Public/Structs/SettingsProfile.h diff --git a/Source/HarmonyLink/HarmonyLink.Build.cs b/Source/HarmonyLinkUE/HarmonyLinkUE.Build.cs similarity index 89% rename from Source/HarmonyLink/HarmonyLink.Build.cs rename to Source/HarmonyLinkUE/HarmonyLinkUE.Build.cs index 7199123..56b5629 100644 --- a/Source/HarmonyLink/HarmonyLink.Build.cs +++ b/Source/HarmonyLinkUE/HarmonyLinkUE.Build.cs @@ -3,9 +3,9 @@ using UnrealBuildTool; using System.IO; -public class HarmonyLink : ModuleRules +public class HarmonyLinkUE : ModuleRules { - public HarmonyLink(ReadOnlyTargetRules Target) : base(Target) + public HarmonyLinkUE(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; diff --git a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp b/Source/HarmonyLinkUE/Private/HarmonyLinkLibrary.cpp similarity index 91% rename from Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp rename to Source/HarmonyLinkUE/Private/HarmonyLinkLibrary.cpp index a9a0f25..bddd3af 100644 --- a/Source/HarmonyLink/Private/HarmonyLinkLibrary.cpp +++ b/Source/HarmonyLinkUE/Private/HarmonyLinkLibrary.cpp @@ -1,7 +1,7 @@ // Copyright (C) 2024 Jordon Brooks #include "HarmonyLinkLibrary.h" -#include "HarmonyLink.h" +#include "HarmonyLinkUE.h" #include "HarmonyLinkLib.h" @@ -20,9 +20,13 @@ FCPUInfo UHarmonyLinkLibrary::CachedCPUInfo = FCPUInfo(); FDevice UHarmonyLinkLibrary::CachedDeviceInfo = FDevice(); FOSVerInfo UHarmonyLinkLibrary::CachedOSInfo = FOSVerInfo(); +bool UHarmonyLinkLibrary::bIsInitialised = false; + UHarmonyLinkLibrary::UHarmonyLinkLibrary() { - if (!HarmonyLinkLib::HL_Init()) + bIsInitialised = HarmonyLinkLib::HL_Init(); + + if (!bIsInitialised) { UE_LOG(LogHarmonyLink, Fatal, TEXT("Failed to initialise HarmonyLinkLib!")); return; @@ -31,6 +35,11 @@ UHarmonyLinkLibrary::UHarmonyLinkLibrary() UE_LOG(LogHarmonyLink, Log, TEXT("HarmonyLinkLib Initialised!")); } +bool UHarmonyLinkLibrary::IsInitialised() +{ + return bIsInitialised; +} + bool UHarmonyLinkLibrary::IsWine(bool bForce) { if (!bIsWineCached || bForce) diff --git a/Source/HarmonyLinkUE/Private/HarmonyLinkUE.cpp b/Source/HarmonyLinkUE/Private/HarmonyLinkUE.cpp new file mode 100644 index 0000000..29c33e1 --- /dev/null +++ b/Source/HarmonyLinkUE/Private/HarmonyLinkUE.cpp @@ -0,0 +1,22 @@ +// Copyright (C) 2024 Jordon Brooks + +#include "HarmonyLinkUE.h" +#include "Modules/ModuleManager.h" + +#define LOCTEXT_NAMESPACE "FHarmonyLinkUEModule" + +DEFINE_LOG_CATEGORY(LogHarmonyLink); + +void FHarmonyLinkUEModule::StartupModule() +{ + +} + +void FHarmonyLinkUEModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FHarmonyLinkUEModule, HarmonyLinkUE) diff --git a/Source/HarmonyLink/Private/Structs/Battery.cpp b/Source/HarmonyLinkUE/Private/Structs/Battery.cpp similarity index 100% rename from Source/HarmonyLink/Private/Structs/Battery.cpp rename to Source/HarmonyLinkUE/Private/Structs/Battery.cpp diff --git a/Source/HarmonyLink/Private/Structs/CPUInfo.cpp b/Source/HarmonyLinkUE/Private/Structs/CPUInfo.cpp similarity index 100% rename from Source/HarmonyLink/Private/Structs/CPUInfo.cpp rename to Source/HarmonyLinkUE/Private/Structs/CPUInfo.cpp diff --git a/Source/HarmonyLink/Private/Structs/Device.cpp b/Source/HarmonyLinkUE/Private/Structs/Device.cpp similarity index 100% rename from Source/HarmonyLink/Private/Structs/Device.cpp rename to Source/HarmonyLinkUE/Private/Structs/Device.cpp diff --git a/Source/HarmonyLink/Private/Structs/OSVerInfo.cpp b/Source/HarmonyLinkUE/Private/Structs/OSVerInfo.cpp similarity index 100% rename from Source/HarmonyLink/Private/Structs/OSVerInfo.cpp rename to Source/HarmonyLinkUE/Private/Structs/OSVerInfo.cpp diff --git a/Source/HarmonyLink/Public/Enums/DeviceEnum.h b/Source/HarmonyLinkUE/Public/Enums/DeviceEnum.h similarity index 100% rename from Source/HarmonyLink/Public/Enums/DeviceEnum.h rename to Source/HarmonyLinkUE/Public/Enums/DeviceEnum.h diff --git a/Source/HarmonyLink/Public/Enums/Platform.h b/Source/HarmonyLinkUE/Public/Enums/Platform.h similarity index 100% rename from Source/HarmonyLink/Public/Enums/Platform.h rename to Source/HarmonyLinkUE/Public/Enums/Platform.h diff --git a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h b/Source/HarmonyLinkUE/Public/HarmonyLinkLibrary.h similarity index 89% rename from Source/HarmonyLink/Public/HarmonyLinkLibrary.h rename to Source/HarmonyLinkUE/Public/HarmonyLinkLibrary.h index 926b6c8..a55db5f 100644 --- a/Source/HarmonyLink/Public/HarmonyLinkLibrary.h +++ b/Source/HarmonyLinkUE/Public/HarmonyLinkLibrary.h @@ -16,13 +16,17 @@ * Library of static functions for accessing various system information, particularly for the HarmonyLink project. */ UCLASS() -class HARMONYLINK_API UHarmonyLinkLibrary : public UBlueprintFunctionLibrary +class HARMONYLINKUE_API UHarmonyLinkLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UHarmonyLinkLibrary(); + // IsInitialised + UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") + static bool IsInitialised(); + // Checks if the game is running under Wine. UFUNCTION(BlueprintCallable, BlueprintPure, Category="HarmonyLink") static bool IsWine(bool bForce = false); @@ -66,4 +70,6 @@ private: static FCPUInfo CachedCPUInfo; static FDevice CachedDeviceInfo; static FOSVerInfo CachedOSInfo; + + static bool bIsInitialised; }; diff --git a/Source/HarmonyLink/Public/HarmonyLink.h b/Source/HarmonyLinkUE/Public/HarmonyLinkUE.h similarity index 83% rename from Source/HarmonyLink/Public/HarmonyLink.h rename to Source/HarmonyLinkUE/Public/HarmonyLinkUE.h index b59a8ba..b88bb95 100644 --- a/Source/HarmonyLink/Public/HarmonyLink.h +++ b/Source/HarmonyLinkUE/Public/HarmonyLinkUE.h @@ -6,7 +6,7 @@ DECLARE_LOG_CATEGORY_EXTERN(LogHarmonyLink, All, All); -class FHarmonyLinkModule : public IModuleInterface +class FHarmonyLinkUEModule : public IModuleInterface { public: diff --git a/Source/HarmonyLink/Public/Structs/Battery.h b/Source/HarmonyLinkUE/Public/Structs/Battery.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/Battery.h rename to Source/HarmonyLinkUE/Public/Structs/Battery.h diff --git a/Source/HarmonyLink/Public/Structs/CPUInfo.h b/Source/HarmonyLinkUE/Public/Structs/CPUInfo.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/CPUInfo.h rename to Source/HarmonyLinkUE/Public/Structs/CPUInfo.h diff --git a/Source/HarmonyLink/Public/Structs/Device.h b/Source/HarmonyLinkUE/Public/Structs/Device.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/Device.h rename to Source/HarmonyLinkUE/Public/Structs/Device.h diff --git a/Source/HarmonyLink/Public/Structs/OSVerInfo.h b/Source/HarmonyLinkUE/Public/Structs/OSVerInfo.h similarity index 100% rename from Source/HarmonyLink/Public/Structs/OSVerInfo.h rename to Source/HarmonyLinkUE/Public/Structs/OSVerInfo.h From 52ae204afef9fe297e90e8fed88809199f0e9306 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Mon, 8 Jul 2024 21:53:56 +0100 Subject: [PATCH 16/18] Added new line --- HarmonyLinkUE.uplugin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HarmonyLinkUE.uplugin b/HarmonyLinkUE.uplugin index 8b1b3fe..450dc53 100644 --- a/HarmonyLinkUE.uplugin +++ b/HarmonyLinkUE.uplugin @@ -30,4 +30,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} From 5fac24b973f35e382254cea9ca33ea37e04f1563 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Mon, 8 Jul 2024 21:54:25 +0100 Subject: [PATCH 17/18] Removed unused module dependency --- Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs b/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs index 69f4fa3..e630ffb 100644 --- a/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs +++ b/Source/HarmonyLinkSettings/HarmonyLinkSettings.Build.cs @@ -18,8 +18,6 @@ public class HarmonyLinkSettings : ModuleRules { "CoreUObject", "Engine", - "Slate", - "SlateCore", "HarmonyLinkUE" } From 98fb068e2930e5ebedac9f16d5ef8a2f716cf4d1 Mon Sep 17 00:00:00 2001 From: Jordon Brooks Date: Mon, 8 Jul 2024 21:55:13 +0100 Subject: [PATCH 18/18] Fixed compile issue in Unreal Engine 5 --- .../HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp b/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp index 1405b51..fafa023 100644 --- a/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp +++ b/Source/HarmonyLinkSettings/Private/Objects/HarmonyLinkGraphics.cpp @@ -129,7 +129,7 @@ bool UHarmonyLinkGraphics::LoadSection(FConfigFile* ConfigFile, const TPair= 5) - Section = ConfigFile->FindSection(*SectionName.ToString()) + Section = ConfigFile->FindSection(*SectionName.ToString()); #elif (ENGINE_MAJOR_VERSION == 4) && (ENGINE_MINOR_VERSION >= 27) Section = ConfigFile->FindOrAddSection(*SectionName.ToString()); #else