diff --git a/assets/Changelog.md b/assets/Changelog.md index 5af27c444..5412054a1 100644 --- a/assets/Changelog.md +++ b/assets/Changelog.md @@ -26,6 +26,21 @@ Added global function `CreateInvalidObject`, which returns an invalid UObject. ( Added GenerateLuaTypes function. ([UE4SS #664](https://github.com/UE4SS-RE/RE-UE4SS/pull/664)) Added global Dumpers functions to Types.lua. ([UE4SS #664](https://github.com/UE4SS-RE/RE-UE4SS/pull/664)) +#### Types.lua [PR #650](https://github.com/UE4SS-RE/RE-UE4SS/pull/650) +- Added `NAME_None` definition +- Added `EFindName` enum definition +- Added `FName` function overloads with FindType parameter + +#### UEHelpers [PR #650](https://github.com/UE4SS-RE/RE-UE4SS/pull/650) +- Added function `GetPlayer` which is just a fast way to get player controlled Pawn (the majority of the time it will be the player character) +- Added functions: `GetEngine`, `GetGameInstance`, `GetGameViewportClient`, `GetGameModeBase`, `GetGameStateBase`,`GetPersistentLevel` and `GetWorldSettings` +- Added functions to get static objects: `GetKismetStringLibrary`, `GetKismetTextLibrary` +- Added function `GetActorFromHitResult` which extracts the hit actor from a `FHitResult` struct based on UE's version +- Added FName utility functions: + - `FindFName`: wrapper for `FName(Name, EFindName.FNAME_Find)` + - `AddFName`: wrapper for `FName(Name, EFindName.FNAME_Add)` +- Added [Lua Server Annotations](https://luals.github.io/wiki/annotations/) to all UEHelpers functions + ### C++ API Key binds created with `UE4SSProgram::register_keydown_event` end up being duplicated upon mod hot-reload. To fix this, `CppUserModBase::register_keydown_event` has been introduced. @@ -67,6 +82,14 @@ The following search filters now allow multiple values, with each value separate The callback of `NotifyOnNewObject` can now optionally return `true` to unregister itself ([UE4SS #432](https://github.com/UE4SS-RE/RE-UE4SS/pull/432)) - Lyrth +#### UEHelpers [UE4SS #650](https://github.com/UE4SS-RE/RE-UE4SS/pull/650) +- Increased version to 3 +- Reworked all UEHelpers functions to ensure that they always return an object which can be checked with the function `IsValid` for validation +- Reworked `UEHelpers.GetPlayerController` to return first valid player controller (It will now return a player controller even if it doesn't control a pawn at the time) +- Reworked `UEHelpers.GetWorld` function to use UWorld cache (UWorld usually never changes) +- Change `UEHelpers.GetWorldContextObject` function annotation to return `UObject`. (Any UObject with a GetWorld() function is a valid WorldContext) +- Removed duplicate function `UEHelpers.GetKismetMathLibrary` + ### C++ API ### BPModLoader diff --git a/assets/Mods/shared/Types.lua b/assets/Mods/shared/Types.lua index a8fcf47b6..d593e711d 100644 --- a/assets/Mods/shared/Types.lua +++ b/assets/Mods/shared/Types.lua @@ -277,7 +277,6 @@ EInternalObjectFlags = { ---@alias float number ---@alias double number - -- # Global Functions ---Creates an blank UObject whose IsValid function always returns false @@ -370,16 +369,37 @@ function UnregisterHook(UFunctionName, PreId, PostId) end ---@param Callback fun() function ExecuteInGameThread(Callback) end ----Returns the FName for this string/ComparisonIndex or the FName for "None" if the name doesn't exist +---FName with "None" as value +NAME_None = FName(0) + +---@enum EFindName +EFindName = { + FNAME_Find = 0, + FNAME_Add = 1 +} + +---Returns the FName for this string or the FName for "None" if the name doesn't exist ---@param Name string ---@return FName function FName(Name) end ----Returns the FName for this string/ComparisonIndex or the FName for "None" if the name doesn't exist +---Returns the FName for this ComparisonIndex or the FName for "None" if the name doesn't exist ---@param ComparisonIndex integer ---@return FName function FName(ComparisonIndex) end +---Finds or adds FName for the string, depending on FindType +---@param Name string +---@param FindType EFindName|integer # Find = 0, Add = 1 +---@return FName +function FName(Name, FindType) end + +---Finds or adds FName for the ComparisonIndex, depending on FindType +---@param ComparisonIndex integer +---@param FindType EFindName|integer # Find = 0, Add = 1 +---@return FName +function FName(ComparisonIndex, FindType) end + ---Attempts to construct a UObject of the passed UClass ---(>=4.26) Maps to https://docs.unrealengine.com/4.27/en-US/API/Runtime/CoreUObject/UObject/StaticConstructObject_Internal/1/ ---(<4.25) Maps to https://docs.unrealengine.com/4.27/en-US/API/Runtime/CoreUObject/UObject/StaticConstructObject_Internal/2/ diff --git a/assets/Mods/shared/UEHelpers/UEHelpers.lua b/assets/Mods/shared/UEHelpers/UEHelpers.lua index da209e338..c033c4858 100644 --- a/assets/Mods/shared/UEHelpers/UEHelpers.lua +++ b/assets/Mods/shared/UEHelpers/UEHelpers.lua @@ -3,11 +3,16 @@ local UEHelpers = {} -- local jsb = require "jsbProfi" -- Version 1 does not exist, we start at version 2 because the original version didn't have a version at all. -local Version = 2 +local Version = 3 --- Functions local to this module, do not attempt to use! -local CacheDefaultObject = function(ObjectFullName, VariableName, ForceInvalidateCache) - local DefaultObject +-- Functions and classes local to this module, do not attempt to use! + +---@param ObjectFullName string +---@param VariableName string +---@param ForceInvalidateCache boolean? +---@return UObject +local function CacheDefaultObject(ObjectFullName, VariableName, ForceInvalidateCache) + local DefaultObject = CreateInvalidObject() if not ForceInvalidateCache then DefaultObject = ModRef:GetSharedVariable(VariableName) @@ -28,68 +33,206 @@ function UEHelpers.GetUEHelpersVersion() return Version end ---- Returns the first valid PlayerController that is currently controlled by a player. +local EngineCache = CreateInvalidObject() ---@cast EngineCache UEngine +---Returns instance of UEngine +---@return UEngine +function UEHelpers.GetEngine() + if EngineCache:IsValid() then return EngineCache end + + EngineCache = FindFirstOf("Engine") ---@type UEngine + return EngineCache +end + +local GameInstanceCache = CreateInvalidObject() ---@cast GameInstanceCache UGameInstance +---Returns instance of UGameInstance +---@return UGameInstance +function UEHelpers.GetGameInstance() + if GameInstanceCache:IsValid() then return GameInstanceCache end + + GameInstanceCache = FindFirstOf("GameInstance") ---@type UGameInstance + return GameInstanceCache +end + +---Returns the main UGameViewportClient +---@return UGameViewportClient +function UEHelpers.GetGameViewportClient() + local Engine = UEHelpers.GetEngine() + if Engine:IsValid() then + return Engine.GameViewport + end + return CreateInvalidObject() ---@type UGameViewportClient +end + +local PlayerControllerCache = CreateInvalidObject() ---@cast PlayerControllerCache APlayerController +---Returns first player controller ---@return APlayerController -local PlayerController = nil function UEHelpers.GetPlayerController() - if PlayerController and PlayerController:IsValid() then return PlayerController end - -- local PlayerControllers = jsb.simpleBench("findallof", FindAllOf, "Controller") - -- Uncomment line above and comment line below to profile this function - local PlayerControllers = FindAllOf("PlayerController") or FindAllOf("Controller") - if not PlayerControllers then return Print("No PlayerControllers found\n") end - for _, Controller in pairs(PlayerControllers or {}) do - if Controller.Pawn:IsValid() and Controller.Pawn:IsPlayerControlled() then - PlayerController = Controller - break - -- else - -- print("Not valid or not player controlled\n") + if PlayerControllerCache:IsValid() then return PlayerControllerCache end + + local Controllers = FindAllOf("Controller") ---@type AController[]? + if Controllers then + for _, Controller in ipairs(Controllers) do + if Controller:IsValid() and Controller:IsPlayerController() then + PlayerControllerCache = Controller + break + end end end - if PlayerController and PlayerController:IsValid() then - return PlayerController + + return PlayerControllerCache +end + +---Returns local player pawn +---@return APawn +function UEHelpers.GetPlayer() + local playerController = UEHelpers.GetPlayerController() + if playerController:IsValid() then + return playerController.Pawn end - error("No PlayerController found\n") + return CreateInvalidObject() ---@type APawn end ---- Returns the UWorld that the player is currenlty in. +local WorldCache = CreateInvalidObject() ---@cast WorldCache UWorld +--- Returns the main UWorld ---@return UWorld function UEHelpers.GetWorld() - return UEHelpers.GetPlayerController():GetWorld() + if WorldCache:IsValid() then return WorldCache end + + local PlayerController = UEHelpers.GetPlayerController() + if PlayerController:IsValid() then + WorldCache = PlayerController:GetWorld() + return WorldCache + end + + return WorldCache end ---- Returns the UGameViewportClient for the player. ----@return AActor -function UEHelpers.GetGameViewportClient() - return UEHelpers.GetPlayerController().Player.ViewportClient +---Returns UWorld->PersistentLevel +---@return ULevel +function UEHelpers.GetPersistentLevel() + local World = UEHelpers.GetWorld() + if World:IsValid() and World.PersistentLevel:IsValid() then + return World.PersistentLevel + end + return CreateInvalidObject() ---@type ULevel end ---- Returns an object that's useable with UFunctions that have a WorldContextObject param. +---Returns UWorld->AuthorityGameMode
+---The function doesn't guarantee it to be an AGameMode, as many games derive their own game modes directly from AGameModeBase! +---@return AGameModeBase +function UEHelpers.GetGameModeBase() + local World = UEHelpers.GetWorld() + if World:IsValid() and World.AuthorityGameMode:IsValid() then + return World.AuthorityGameMode + end + return CreateInvalidObject() ---@type AGameModeBase +end + +---Returns UWorld->GameState
+---The function doesn't guarantee it to be an AGameState, as many games derive their own game states directly from AGameStateBase! +---@return AGameStateBase +function UEHelpers.GetGameStateBase() + local World = UEHelpers.GetWorld() + if World:IsValid() and World.GameState:IsValid() then + return World.GameState + end + return CreateInvalidObject() ---@type AGameStateBase +end + +---Returns PersistentLevel->WorldSettings +---@return AWorldSettings +function UEHelpers.GetWorldSettings() + local PersistentLevel = UEHelpers.GetPersistentLevel() + if PersistentLevel:IsValid() and PersistentLevel.WorldSettings:IsValid() then + return PersistentLevel.WorldSettings + end + return CreateInvalidObject() ---@type AWorldSettings +end + +--- Returns an object that's useable with UFunctions that have a WorldContext parameter.
--- Prefer to use an actor that you already have access to whenever possible over this function. ----@return AActor +--- Any UObject that has a GetWorld() function can be used as WorldContext. +---@return UObject function UEHelpers.GetWorldContextObject() return UEHelpers.GetPlayerController() end +---Returns hit actor from FHitResult.
+---The function handles the struct differance between UE4 and UE5 +---@param HitResult FHitResult +---@return AActor +function UEHelpers.GetActorFromHitResult(HitResult) + if not HitResult or not HitResult:IsValid() then + return CreateInvalidObject() ---@type AActor + end + + if UnrealVersion:IsBelow(5, 0) then + return HitResult.Actor:Get() + elseif UnrealVersion:IsBelow(5, 4) then + return HitResult.HitObjectHandle.Actor:Get() + end + return HitResult.HitObjectHandle.ReferenceObject:Get() +end + +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UGameplayStatics function UEHelpers.GetGameplayStatics(ForceInvalidateCache) + ---@type UGameplayStatics return CacheDefaultObject("/Script/Engine.Default__GameplayStatics", "UEHelpers_GameplayStatics", ForceInvalidateCache) end +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UKismetSystemLibrary function UEHelpers.GetKismetSystemLibrary(ForceInvalidateCache) + ---@type UKismetSystemLibrary return CacheDefaultObject("/Script/Engine.Default__KismetSystemLibrary", "UEHelpers_KismetSystemLibrary", ForceInvalidateCache) end +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UKismetMathLibrary function UEHelpers.GetKismetMathLibrary(ForceInvalidateCache) + ---@type UKismetMathLibrary return CacheDefaultObject("/Script/Engine.Default__KismetMathLibrary", "UEHelpers_KismetMathLibrary", ForceInvalidateCache) end -function UEHelpers.GetKismetMathLibrary(ForceInvalidateCache) - return CacheDefaultObject("/Script/Engine.Default__KismetMathLibrary", "UEHelpers_KismetMathLibrary", ForceInvalidateCache) +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UKismetStringLibrary +function UEHelpers.GetKismetStringLibrary(ForceInvalidateCache) + ---@type UKismetStringLibrary + return CacheDefaultObject("/Script/Engine.Default__KismetStringLibrary", "UEHelpers_KismetStringLibrary", ForceInvalidateCache) +end + +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UKismetTextLibrary +function UEHelpers.GetKismetTextLibrary(ForceInvalidateCache) + ---@type UKismetTextLibrary + return CacheDefaultObject("/Script/Engine.Default__KismetTextLibrary", "UEHelpers_KismetTextLibrary", ForceInvalidateCache) end +---@param ForceInvalidateCache boolean? # Force update the cache +---@return UGameMapsSettings function UEHelpers.GetGameMapsSettings(ForceInvalidateCache) + ---@type UGameMapsSettings return CacheDefaultObject("/Script/EngineSettings.Default__GameMapsSettings", "UEHelpers_GameMapsSettings", ForceInvalidateCache) end +---Returns found FName or "None" FName if the operation faled +---@param Name string +---@return FName +function UEHelpers.FindFName(Name) + return FName(Name, EFindName.FNAME_Find) +end + +---Returns added FName or "None" FName if the operation faled +---@param Name string +---@return FName +function UEHelpers.AddFName(Name) + return FName(Name, EFindName.FNAME_Add) +end + +---Tries to find existing FName, if it doesn't exist a new FName will be added to the pool +---@param Name string +---@return FName # Returns found or added FName, “None” FName if both operations fail function UEHelpers.FindOrAddFName(Name) local NameFound = FName(Name, EFindName.FNAME_Find) if NameFound == NAME_None then diff --git a/docs/guides/using-custom-lua-bindings.md b/docs/guides/using-custom-lua-bindings.md index a8fc28b3f..eed4de954 100644 --- a/docs/guides/using-custom-lua-bindings.md +++ b/docs/guides/using-custom-lua-bindings.md @@ -28,8 +28,22 @@ When developing your Lua mods, the language server should automatically parse al } ``` -To get context sensitive information about the custom game types, you need to [annotate your code](https://emmylua.github.io/annotation.html). This is done by adding a comment above the function/class/object that you want to annotate. +### How to use your mod's directory as workspace +As alternative you can open just your mod's root directory as workspace. +In this case you need to add a `.luarc.json` with `"workspace.library"` entries containing a path to the "shared" folder and the "Scripts" directory of your mod. +Both paths can be relative. +**Example .luarc.json:** +```json +{ + "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "workspace.maxPreload": 50000, + "workspace.preloadFileSize": 5000, + "workspace.library": ["../shared", "Scripts"] +} +``` +## Annotating +To get context sensitive information about the custom game types, you need to [annotate your code](https://emmylua.github.io/annotation.html) ([alternative documentation](https://luals.github.io/wiki/annotations/)). This is done by adding a comment above the function/class/object that you want to annotate. ## Example ```lua