Can now switch the skeletal mesh on a character

This commit is contained in:
2026-02-17 15:49:52 -05:00
parent 3f975dbada
commit a987754b38
16 changed files with 157 additions and 125 deletions

View File

@@ -3,7 +3,12 @@
"allow": [
"WebFetch(domain:dev.epicgames.com)",
"WebFetch(domain:github.com)",
"Bash(grep:*)"
"Bash(grep:*)",
"WebSearch",
"Bash(clangd:*)",
"Bash(clangd-16:*)",
"Bash(ssh jyelon-office \"clangd --version 2>/dev/null || clangd-16 --version 2>/dev/null || clangd-18 --version 2>/dev/null || ls /usr/bin/clangd*\")",
"Bash(git check-ignore:*)"
],
"deny": [
"Bash(git commit *)",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,52 @@
# Error Handling in Blueprints
## Format Log Message Node
We have implemented "Format Message", which is an improved version of Unreal's
"Format Text". Unlike "Format Text", it has built-in support to print
vectors, rotations, enums, and quite a few other types.
We have also implemented "Format Log Message", which is like Format Message
except that the output goes to the unreal logs. It might help to familiarize
yourself with Unreal's built-in logging function, UE_LOG. Format Log Message
is meant to be the Blueprint front end to UE_LOG.
Format Message and Format Log Message both require a format string, which
will typically look something like this:
"Hello, {Name}, the weather today will be {Weather}"
When you set a format string such as the one above, the Format Message node
will add pins for the placeholders - in the example above, the pins
Name and Weather would be added. These are wildcard pins that you can
connect any printable value to.
To make a new type printable, look at FormatDataLibrary.h
Format Log Message accepts a severity level, this is directly based on
the severity levels supported by UE_LOG. It adds *ThrottledDisplay* and
*ThrottledLog*, which do the same thing as *Display* and *Log*, except
that output is limited to one message per second.
## The Blueprint Debugger Pauses on Error Messages
The blueprint debugger will pause whenever you log an error message. This is functionality we have added to Unreal.
You can control the severity at which it pauses: in the GameMode, there is a dropdown for the severity level.
You can also effectively turn it off by telling to only pause on fatal errors.
There is a quirk: the debugger pauses before the error gets put into the logs. So when you're in the
blueprint debugger,
the only place the error will appear is in the lower-right corner of the screen, where debugger messages are
reported. You won't see the error in the unreal output log. If you single-step, the error will appear
in the output log as well.
To understand how this is implemented, it helps to understand that log messages in unreal go to a bunch of
different output devices: the unreal editor output window, the visual studio output window, a log file, and
several more places. To dispatch messages to all these different destinations, Unreal has an output device
list that you can add an output device to.
To implement this break-on-error-message functionality, we implemented a pseudo-output-device.
The pseudo device doesn't actually send
the error message anywhere, it just triggers the debugger. For more information about implementation,
see BreakToDebugger.h

View File

@@ -143,3 +143,6 @@ it adds those string to the UlxConsoleOutput. The UlxConsoleOutput
turns those individual lines into one big string. From there,
the LuprexGameMode copies the entire string into the body of
a UMG text widget.
## A Possible new lua print function

View File

@@ -627,45 +627,43 @@ void UlxAnimationStepLibrary::AnimationStepApplyMaterials(const FlxAnimationStep
}
}
void UlxAnimationStepLibrary::AnimationStepApplyMesh(const FlxAnimationStep& step, bool FallbackToBP, AActor* actor) {
if (actor == nullptr) return;
void UlxAnimationStepLibrary::AnimationStepApplyStaticMesh(const FlxAnimationStep& step, bool FallbackToBP,
UStaticMeshComponent* MeshComp, UStaticMesh* Fallback) {
if (MeshComp == nullptr) return;
// Step 1: Look for a "mesh" or "bp" string field in the animation step.
//
FString MeshName = AnimationStepGetString(step, TEXT("mesh"));
if (MeshName.IsEmpty() && FallbackToBP) {
MeshName = AnimationStepGetString(step, TEXT("bp"));
}
if (MeshName.IsEmpty()) return;
// Step 2: Find the actor's mesh component. There must be exactly one.
//
TInlineComponentArray<UMeshComponent*> MeshComponents;
actor->GetComponents<UMeshComponent>(MeshComponents);
if (MeshComponents.Num() != 1) {
UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has %d mesh components, expected exactly 1"), *actor->GetName(), MeshComponents.Num());
return;
UStaticMesh* NewMesh = nullptr;
if (!MeshName.IsEmpty()) {
UlxAssetLookup::LoadStaticMeshAsset(NewMesh, MeshComp, MeshName);
}
UMeshComponent* MeshComp = MeshComponents[0];
// Step 3: Apply the mesh based on the component type.
//
if (UStaticMeshComponent* StaticComp = Cast<UStaticMeshComponent>(MeshComp)) {
UStaticMesh* NewMesh = nullptr;
UlxAssetLookup::LoadStaticMeshAsset(NewMesh, actor, MeshName, false);
if (NewMesh == nullptr) return;
if (StaticComp->GetStaticMesh() != NewMesh) {
StaticComp->SetStaticMesh(NewMesh);
}
} else if (USkeletalMeshComponent* SkelComp = Cast<USkeletalMeshComponent>(MeshComp)) {
USkeletalMesh* NewMesh = nullptr;
UlxAssetLookup::LoadSkeletalMeshAsset(NewMesh, actor, MeshName, true);
if (NewMesh == nullptr) return;
if (SkelComp->GetSkeletalMeshAsset() != NewMesh) {
SkelComp->SetSkeletalMeshAsset(NewMesh);
}
} else {
UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has unsupported mesh component type"), *actor->GetName());
if (NewMesh == nullptr) NewMesh = Fallback;
if (NewMesh == nullptr) return;
if (MeshComp->GetStaticMesh() != NewMesh) {
MeshComp->SetStaticMesh(NewMesh);
}
}
void UlxAnimationStepLibrary::AnimationStepApplySkeletalMesh(const FlxAnimationStep& step, bool FallbackToBP,
USkeletalMeshComponent* MeshComp, USkeletalMesh* Fallback) {
if (MeshComp == nullptr) return;
FString MeshName = AnimationStepGetString(step, TEXT("mesh"));
if (MeshName.IsEmpty() && FallbackToBP) {
MeshName = AnimationStepGetString(step, TEXT("bp"));
}
USkeletalMesh* NewMesh = nullptr;
if (!MeshName.IsEmpty()) {
UlxAssetLookup::LoadSkeletalMeshAsset(NewMesh, MeshComp, MeshName);
}
if (NewMesh == nullptr) NewMesh = Fallback;
if (NewMesh == nullptr) return;
if (MeshComp->GetSkeletalMeshAsset() != NewMesh) {
MeshComp->SetSkeletalMeshAsset(NewMesh);
}
}

View File

@@ -125,55 +125,40 @@ public:
UFUNCTION(BlueprintPure, meta = (BlueprintAutocast), Category = "Luprex|Animation Step")
static int64 AnimationStepID(const FlxAnimationStep& step) { return step.Hash; }
<<<<<<< HEAD
// Using mat_xxxx values from the animation step, update the actor's
// material parameters. Doing this may involve creating or replacing
// dynamic material instances for the actor.
=======
// Scan an animation step for key-value pairs of the
// form mat_XXXX={x,y,z}. For each match, create a
// dynamic material instance on the actor's mesh
// components and set the vector parameter XXXX.
// Materials are restored to their base (non-dynamic)
// state before applying, so parameters from previous
// calls do not persist.
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
//
UFUNCTION(BlueprintCallable, Meta = (DefaultToSelf = "actor"), Category = "Luprex|Animation Step")
static void AnimationStepApplyMaterials(const FlxAnimationStep& step, AActor* actor);
<<<<<<< HEAD
// Look for a mesh=name key-value pair in the animation step.
// If found, load the named mesh and apply it to the actor's
// mesh component. The actor must have exactly one mesh component.
// If FallbackToBP is true, and mesh=name is not present, looks
// for a bp=name pair instead.
=======
// Look for a mesh=name key-value pair. If found, load
// the named mesh and apply it to the actor's mesh
// component. The actor must have exactly one mesh
// component.
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
// Loads a new static mesh, using the animation step to choose it.
// If the chosen mesh is already installed, no changes are made.
// Looks for 'mesh=name' first; if not found and FallbackToBP is
// true, looks for 'bp=name'. If no mesh is successfully loaded,
// uses the Fallback mesh. Failed loads are logged once.
//
UFUNCTION(BlueprintCallable, Meta = (DefaultToSelf = "actor"), Category = "Luprex|Animation Step")
static void AnimationStepApplyMesh(const FlxAnimationStep& step, bool FallbackToBP, AActor* actor);
UFUNCTION(BlueprintCallable, Category = "Luprex|Animation Step")
static void AnimationStepApplyStaticMesh(const FlxAnimationStep& step, bool FallbackToBP,
UStaticMeshComponent* MeshComp, UStaticMesh* Fallback = nullptr);
// Loads a new skeletal mesh, using the animation step to choose it.
// If the chosen mesh is already installed, no changes are made.
// Looks for 'mesh=name' first; if not found and FallbackToBP is
// true, looks for 'bp=name'. If no mesh is successfully loaded,
// uses the Fallback mesh. Failed loads are logged once.
//
UFUNCTION(BlueprintCallable, Category = "Luprex|Animation Step")
static void AnimationStepApplySkeletalMesh(const FlxAnimationStep& step, bool FallbackToBP,
USkeletalMeshComponent* MeshComp, USkeletalMesh* Fallback = nullptr);
};
////////////////////////////////////////////////////////////
//
<<<<<<< HEAD
// An animation step that doesn't actually store the step,
// it just contains a pointer to the string.
//
////////////////////////////////////////////////
=======
// FlxAnimationStepView
//
// A lightweight, non-owning view of an animation
// step (hash + body as a string_view).
//
////////////////////////////////////////////////////////////
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
struct FlxAnimationStepView {
int64 Hash;
@@ -266,13 +251,8 @@ private:
FlxStreamBuffer Decoder;
public:
<<<<<<< HEAD
// Initialize the FlxAnimationStepDecoder.
//
=======
// Initialize from an encoded step body.
//
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
FlxAnimationStepDecoder(std::string_view body) : Decoder(body) {}
// Return true if the parser has reached EOF.

View File

@@ -20,7 +20,7 @@ void UlxAssetLookup::RebuildIndex()
IAssetRegistry::GetChecked().WaitForCompletion();
AssetPaths.Empty();
AddAssets(TEXT("/Game/StaticMeshes"), UStaticMesh::StaticClass(), TEXT("SM_"));
AddAssets(TEXT("/Game/SkeletalMeshes"), USkeletalMesh::StaticClass(), TEXT("SK_"));
AddAssets(TEXT("/Game/SkeletalMeshes"), USkeletalMesh::StaticClass(), TEXT("SKM_"));
AddAssets(TEXT("/Game/AnimSequences"), UAnimSequence::StaticClass(), TEXT("SEQ_"));
AddAssets(TEXT("/Game/Tangibles"), UBlueprint::StaticClass(), TEXT("TAN_"));
AddAssets(TEXT("/Game/Widgets"), UWidgetBlueprint::StaticClass(), TEXT("WB_"));
@@ -52,16 +52,22 @@ void UlxAssetLookup::AddAssets(const TCHAR *Path, UClass *Class, const TCHAR *Na
FoundData.Num(), *Class->GetName(), Path);
}
UObject *UlxAssetLookup::LoadAsset(const UObject *Context, UClass *Class, UClass *ChildOf, const FString &Name, bool ErrorIfNotFound)
void UlxAssetLookup::ReportFailedLoad(const FString &ClassName, const FString &Name, const FString &Reason)
{
static TSet<FString> Reported;
FString Key = ClassName + TEXT(":") + Name + TEXT(":") + Reason;
if (Reported.Contains(Key)) return;
Reported.Add(Key);
UE_LOG(LogLuprexIntegration, Display, TEXT("Loading %s %s: %s"), *ClassName, *Name, *Reason);
}
UObject *UlxAssetLookup::LoadAsset(const UObject *Context, UClass *Class, UClass *ChildOf, const FString &Name)
{
const UlxAssetLookup *Lookup = ALuprexGameModeBase::FromContext(Context)->GetAssetLookup();
const FString *Path = Lookup->AssetPaths.Find(MakeTuple(Class->GetName(), FName(Name)));
if (Path == nullptr)
{
if (ErrorIfNotFound)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Loading %s %s: asset not found"), *Class->GetName(), *Name);
}
ReportFailedLoad(Class->GetName(), Name, TEXT("asset not found"));
return nullptr;
}
@@ -77,7 +83,7 @@ UObject *UlxAssetLookup::LoadAsset(const UObject *Context, UClass *Class, UClass
if (Result == nullptr)
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Loading %s %s: unknown load failure"), *Class->GetName(), *Name);
ReportFailedLoad(Class->GetName(), Name, TEXT("unknown load failure"));
return nullptr;
}
@@ -86,7 +92,7 @@ UObject *UlxAssetLookup::LoadAsset(const UObject *Context, UClass *Class, UClass
UClass *ResClass = (UClass *)Result;
if (!ResClass->IsChildOf(ChildOf))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Loading %s %s: blueprint not a child of %s"), *Class->GetName(), *Name, *ChildOf->GetName());
ReportFailedLoad(Class->GetName(), Name, FString::Printf(TEXT("blueprint not a child of %s"), *ChildOf->GetName()));
return nullptr;
}
}
@@ -95,50 +101,50 @@ UObject *UlxAssetLookup::LoadAsset(const UObject *Context, UClass *Class, UClass
}
ElxValidOrNotValid UlxAssetLookup::LoadStaticMeshAsset(
UStaticMesh *&Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
UStaticMesh *&Result, const UObject *Context, const FString &Name)
{
Result = (UStaticMesh *)LoadAsset(Context, UStaticMesh::StaticClass(), nullptr, Name, ErrorIfNotFound);
Result = (UStaticMesh *)LoadAsset(Context, UStaticMesh::StaticClass(), nullptr, Name);
return Result ? Valid : NotValid;
}
ElxValidOrNotValid UlxAssetLookup::LoadSkeletalMeshAsset(
USkeletalMesh *&Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
USkeletalMesh *&Result, const UObject *Context, const FString &Name)
{
Result = (USkeletalMesh *)LoadAsset(Context, USkeletalMesh::StaticClass(), nullptr, Name, ErrorIfNotFound);
Result = (USkeletalMesh *)LoadAsset(Context, USkeletalMesh::StaticClass(), nullptr, Name);
return Result ? Valid : NotValid;
}
ElxValidOrNotValid UlxAssetLookup::LoadAnimSequenceAsset(
UAnimSequence *&Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
UAnimSequence *&Result, const UObject *Context, const FString &Name)
{
Result = (UAnimSequence *)LoadAsset(Context, UAnimSequence::StaticClass(), nullptr, Name, ErrorIfNotFound);
Result = (UAnimSequence *)LoadAsset(Context, UAnimSequence::StaticClass(), nullptr, Name);
return Result ? Valid : NotValid;
}
ElxValidOrNotValid UlxAssetLookup::LoadTangibleBlueprintAsset(
TSubclassOf<AActor> &Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
TSubclassOf<AActor> &Result, const UObject *Context, const FString &Name)
{
Result = (UClass*)LoadAsset(Context, UBlueprint::StaticClass(), AActor::StaticClass(), Name, ErrorIfNotFound);
Result = (UClass*)LoadAsset(Context, UBlueprint::StaticClass(), AActor::StaticClass(), Name);
if (Result == nullptr) return NotValid;
UFunction *aqchanged = Result->FindFunctionByName(FName(TEXT("Animation Queue Changed")));
if ((aqchanged == nullptr)||(aqchanged->ParmsSize != 0))
{
UE_LOG(LogLuprexIntegration, Error, TEXT("Loading Blueprint %s: Tangible does not have 'Animation Queue Changed' function"), *Name);
ReportFailedLoad(TEXT("Blueprint"), Name, TEXT("tangible does not have 'Animation Queue Changed' function"));
Result = nullptr; return NotValid;
}
return Valid;
}
ElxValidOrNotValid UlxAssetLookup::LoadUserWidgetAsset(
TSubclassOf<UUserWidget> &Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
TSubclassOf<UUserWidget> &Result, const UObject *Context, const FString &Name)
{
Result = (UClass *)LoadAsset(Context, UWidgetBlueprint::StaticClass(), UUserWidget::StaticClass(), Name, ErrorIfNotFound);
Result = (UClass *)LoadAsset(Context, UWidgetBlueprint::StaticClass(), UUserWidget::StaticClass(), Name);
return Result ? Valid : NotValid;
}
ElxValidOrNotValid UlxAssetLookup::LoadLuaWidgetAsset(
TSubclassOf<UlxLuaWidget> &Result, const UObject *Context, const FString &Name, bool ErrorIfNotFound)
TSubclassOf<UlxLuaWidget> &Result, const UObject *Context, const FString &Name)
{
Result = (UClass *)LoadAsset(Context, UWidgetBlueprint::StaticClass(), UlxLuaWidget::StaticClass(), Name, ErrorIfNotFound);
Result = (UClass *)LoadAsset(Context, UWidgetBlueprint::StaticClass(), UlxLuaWidget::StaticClass(), Name);
return Result ? Valid : NotValid;
}

View File

@@ -43,7 +43,9 @@ private:
void AddAssets(const TCHAR *Path, UClass *Class, const TCHAR *NamePrefix);
static UObject *LoadAsset(const UObject *Context, UClass *Class, UClass *ChildOf, const FString &Name, bool ErrorIfNotFound);
static void ReportFailedLoad(const FString &ClassName, const FString &Name, const FString &Reason);
static UObject *LoadAsset(const UObject *Context, UClass *Class, UClass *ChildOf, const FString &Name);
public:
void RebuildIndex();
@@ -52,41 +54,35 @@ public:
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadStaticMeshAsset(
UStaticMesh *&Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
UStaticMesh *&Result, const UObject *Context, const FString &Name);
// Get a skeletal mesh by name.
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadSkeletalMeshAsset(
USkeletalMesh *&Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
USkeletalMesh *&Result, const UObject *Context, const FString &Name);
// Get an animation sequence by name.
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadAnimSequenceAsset(
UAnimSequence *&Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
UAnimSequence *&Result, const UObject *Context, const FString &Name);
// Get a tangible class by name.
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadTangibleBlueprintAsset(
TSubclassOf<AActor> &Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
TSubclassOf<AActor> &Result, const UObject *Context, const FString &Name);
// Get a widget blueprint by name.
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadUserWidgetAsset(
TSubclassOf<UUserWidget> &Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
TSubclassOf<UUserWidget> &Result, const UObject *Context, const FString &Name);
// Get a look-at widget blueprint by name.
//
UFUNCTION(BlueprintCallable, meta = (WorldContext = "Context", ExpandEnumAsExecs="ReturnValue"), Category = "Luprex|Asset Loading")
static ElxValidOrNotValid LoadLuaWidgetAsset(
TSubclassOf<UlxLuaWidget> &Result,
const UObject *Context, const FString &Name, bool ErrorIfNotFound = false);
TSubclassOf<UlxLuaWidget> &Result, const UObject *Context, const FString &Name);
};

View File

@@ -15,18 +15,12 @@
#pragma once
#include "Containers/UnrealString.h"
#include "ConsoleOutput.generated.h"
<<<<<<< HEAD
//////////////////////////////////////////////////////////////
=======
////////////////////////////////////////////////////////////
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
//
// UlxConsoleOutput
//
<<<<<<< HEAD
// When the lua code executes a print statement, the text
// eventually gets passed to the GameMode blueprint: see
// Docs/Print-Statement-Handling.md for more information.
@@ -52,10 +46,7 @@
// If the GameMode wants to use some other framework to
// implement the virtual console, that's perfectly fine.
//
//////////////////////////////////////////////////////////////
=======
////////////////////////////////////////////////////////////
>>>>>>> 9b1dd00a45a7b17c3546f8574d00e5ec78f17c75
UCLASS(BlueprintType)
class UlxConsoleOutput : public UObject

View File

@@ -3,6 +3,7 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/HitResult.h"
#include "GameFramework/GameModeBase.h"
#include "lpx-enginewrapper.hpp"
#include "StringDecoder.h"

View File

@@ -10,7 +10,7 @@ function login.init()
global.set("nextplayer", player + 1)
dprint("login.init initializing player ", player)
actor.player = player
tangible.animinit{tan=actor, anim={bp="character", plane="earth", xyz={player * 100, 0, 90}}}
tangible.animinit{tan=actor, anim={bp="character", mesh="manny", plane="earth", xyz={player * 100, 0, 90}}}
end
-- This gets called on the admin user. You can call login.init in here if you want.