Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UEHelpers improvements. Ensure that all functions return a RemoteObject and not nil. Add more functions, Rework some #650

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1073e87
docs: FindFirstOf annotation, it never returns a nil
igromanru Sep 7, 2024
307fb37
feat: Add RemoteObject class that allows us to create a placeholder w…
igromanru Sep 7, 2024
e8a7159
feat: UEHelpers: Remove duplicate function GetKismetMathLibrary()
igromanru Sep 7, 2024
ed67854
feat: UEHelpers: Aad an additional check in GetPlayerController() if …
igromanru Sep 7, 2024
855f13a
feat: Change GetGameEngine() to GetEngine()
igromanru Sep 7, 2024
5ca63e4
fix: Implemented WorldCache but forget to use it
igromanru Sep 7, 2024
ee09c7c
refactor: formatting
igromanru Sep 7, 2024
c824c42
feat: UEHelpers: Add GetGameInstance() function
igromanru Sep 7, 2024
a82bd54
feat: UEHelpers: Rework GetPlayerController() to search through all A…
igromanru Sep 8, 2024
6a23b68
feat: UEHelpers:
igromanru Sep 8, 2024
b9e1825
fix: GetPersistentLevel() and GetWorldSettings() weren't UEHelpers fu…
igromanru Sep 8, 2024
1379728
feat: UEHelpers: Add and annotate FName utility functions to Find, Ad…
igromanru Sep 8, 2024
3796514
docs: UEHelpers: Added annoations to CacheDefaultObject function and …
igromanru Sep 8, 2024
32ca093
docs: UEHelpers: Fix and improve comments
igromanru Sep 9, 2024
4d1f091
Merge branch 'main' of github.com:UE4SS-RE/RE-UE4SS
igromanru Sep 19, 2024
8eea542
docs: Add NAME_None as alias to Types.lua
igromanru Sep 21, 2024
846312a
docs: Updated Changelog to match last commit
igromanru Sep 21, 2024
dd4bc4b
docs: Add NAME_None, EFindName and FName overloads with FindType para…
igromanru Sep 21, 2024
23679f8
docs: Add "How to use your mod's directory as workspace" to "Using Cu…
igromanru Sep 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions assets/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ Added search filter: `IncludeClassNames`. ([UE4SS #472](https://github.com/UE4SS

### Lua API

#### 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 local class `RemoteObject` with method `IsValid`. A new instance of the class should be used as return value in all UEHelpers functions instead of nil
- 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.
Expand Down Expand Up @@ -63,6 +79,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
Expand Down Expand Up @@ -110,6 +134,8 @@ Fixed crash when calling UFunctions that take one or more 'out' params of type T

Fixed `RegisterProcessConsoleExecPostHook`. ([UE4SS #631](https://github.com/UE4SS-RE/RE-UE4SS/pull/631))

Fixed `FindFirstOf` return type annotation in `Types.lua` to signal that the return value will never be nil. ([UE4SS #650](https://github.com/UE4SS-RE/RE-UE4SS/pull/650))

### C++ API
Fixed a crash caused by a race condition enabled by C++ mods using `UE4SS_ENABLE_IMGUI` in their constructor ([UE4SS #481](https://github.com/UE4SS-RE/RE-UE4SS/pull/481))

Expand Down
28 changes: 24 additions & 4 deletions assets/Mods/shared/Types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ EInternalObjectFlags = {
---@alias float number
---@alias double number


-- # Global Functions

---@param ObjectName string
Expand All @@ -294,7 +293,7 @@ function StaticFindObject(Class, InOuter, ObjectName, ExactClass) end

---Find the first non-default instance of the supplied class name
---@param ShortClassName string Should only contains the class name itself without path info
---@return UObject?
---@return UObject
function FindFirstOf(ShortClassName) end

---Find all non-default instances of the supplied class name
Expand Down Expand Up @@ -345,16 +344,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/
Expand Down
216 changes: 186 additions & 30 deletions assets/Mods/shared/UEHelpers/UEHelpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,31 @@ 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!

---Class that allows to create a blank RemoteObject
---@class RemoteObject
local RemoteObject = {}
RemoteObject.__index = RemoteObject

---Creates a new instance of RemoteObject
---@return RemoteObject
function RemoteObject:new()
return setmetatable({}, RemoteObject)
end

function RemoteObject:IsValid()
return false
end

---@param ObjectFullName string
---@param VariableName string
---@param ForceInvalidateCache boolean?
---@return UObject
local function CacheDefaultObject(ObjectFullName, VariableName, ForceInvalidateCache)
local DefaultObject = RemoteObject:new()

if not ForceInvalidateCache then
DefaultObject = ModRef:GetSharedVariable(VariableName)
Expand All @@ -28,68 +48,204 @@ function UEHelpers.GetUEHelpersVersion()
return Version
end

--- Returns the first valid PlayerController that is currently controlled by a player.
local EngineCache = RemoteObject:new() ---@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 = RemoteObject:new() ---@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 RemoteObject:new() ---@type UGameViewportClient
end

local PlayerControllerCache = RemoteObject:new() ---@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 RemoteObject:new() ---@type APawn
end

--- Returns the UWorld that the player is currenlty in.
local WorldCache = RemoteObject:new() ---@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 RemoteObject:new() ---@type ULevel
end

---Returns UWorld->AuthorityGameMode<br>
---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 RemoteObject:new() ---@type AGameModeBase
end

---Returns UWorld->GameState<br>
---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 RemoteObject:new() ---@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 RemoteObject:new() ---@type AWorldSettings
end

--- Returns an object that's useable with UFunctions that have a WorldContextObject param.
--- Returns an object that's useable with UFunctions that have a WorldContext parameter.<br>
--- 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.<br>
---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 RemoteObject:new() ---@type AActor
end

if UnrealVersion:IsBelow(5, 0) then
return HitResult.Actor:Get()
end
return HitResult.HitObjectHandle.Actor: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
Expand Down
16 changes: 15 additions & 1 deletion docs/guides/using-custom-lua-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://github.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
Expand Down