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