Skip to content

Commit

Permalink
UEHelpers improvements. Ensure that all functions return a RemoteObje…
Browse files Browse the repository at this point in the history
…ct and not nil. Add more functions, Rework some (#650)
  • Loading branch information
igromanru authored Oct 2, 2024
1 parent d0ebf12 commit c2a8866
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 34 deletions.
23 changes: 23 additions & 0 deletions assets/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
26 changes: 23 additions & 3 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

---Creates an blank UObject whose IsValid function always returns false
Expand Down Expand Up @@ -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/
Expand Down
203 changes: 173 additions & 30 deletions assets/Mods/shared/UEHelpers/UEHelpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<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 CreateInvalidObject() ---@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 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.<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 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
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://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
Expand Down

0 comments on commit c2a8866

Please sign in to comment.