From 96256d7836a76c3e5eea0d99556cd86802033756 Mon Sep 17 00:00:00 2001 From: jyelon Date: Sat, 14 Feb 2026 00:24:52 -0500 Subject: [PATCH] Character walking is fixed, using the new Movement Component State model. --- CLAUDE.md | 4 ++++ .../Mannequins/Animations/ABP_Manny.uasset | 4 ++-- Docs/Animation Queues and Tangible Actors.md | 6 +++--- Source/Integration/BlueprintErrors.cpp | 16 +++++++++------- Source/Integration/BlueprintErrors.h | 16 +++++++++++++--- Source/Integration/FormatMessage.cpp | 17 +++++++++++++++++ Source/Integration/MovementComponentState.cpp | 6 +++++- Source/Integration/MovementComponentState.h | 2 +- Source/Integration/Tangible.cpp | 2 +- luprex/lua/login.lua | 7 ++++++- 10 files changed, 61 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c0d749ef..7f427b3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,6 +100,10 @@ Look-at widgets, hotkeys, and menus are built on top of this. The menu system is - `Docs/Correct Implementation of Blocking Operations and NoPredict.md` — how to handle blocking ops - `Docs/Difference Transmission with Threads.md` — why concurrent diff transmission is hard +## Blueprint Coding Conventions + +- When writing UFUNCTIONs that take an `AActor*`, `UObject*`, or similar "self" parameter, add `DefaultToSelf` meta to that pin. Most functions should have this on the obvious pin so the user doesn't have to manually wire it in blueprints. + ## Session Startup At the beginning of every session, do a directory listing of these three directories so you know what files are available: diff --git a/Content/Characters/Mannequins/Animations/ABP_Manny.uasset b/Content/Characters/Mannequins/Animations/ABP_Manny.uasset index f1d2bc4a..20afb75b 100644 --- a/Content/Characters/Mannequins/Animations/ABP_Manny.uasset +++ b/Content/Characters/Mannequins/Animations/ABP_Manny.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62d527e544915e62d7343cb4cfd1f4f20789958c957eac6d8fbf860a61f67be2 -size 402045 +oid sha256:1fda14c5cfa75caa7285421412ca22b1283e867a48b7f54da516997dd86b2eb1 +size 401593 diff --git a/Docs/Animation Queues and Tangible Actors.md b/Docs/Animation Queues and Tangible Actors.md index f417d940..3f8ad7b7 100644 --- a/Docs/Animation Queues and Tangible Actors.md +++ b/Docs/Animation Queues and Tangible Actors.md @@ -272,9 +272,9 @@ It is perfectly safe to call FinishedAnimation from anywhere. You can call it fr Some animations are instantaneous, such as "warpto." They take zero frames to complete. The following sequence of events occurs: the lua code pushes the "warpto" animation into the queue. The Unreal C++ code sees this and calls *AnimationQueueChanged*. This, in turn, calls *Init Action: Warpto*. That routine actually performs the warpto, and then calls *FinishedAnimation* immediately. So it's even fine to call *FinishedAnimation* directly from the routine that initiates an animation, if you want the animation to be finished as soon as it starts. -The routine FinishAnimation takes an *lxAnimationStep* as a parameter. This is so that it knows which animation step to mark as finished. TangibleStaticMesh always passes in the variable *CurrentAnimation*. +The routine FinishedAnimation takes an *lxAnimationStep* as a parameter. This is so that it knows which animation step to mark as finished. TangibleStaticMesh always passes in the variable *CurrentAnimation*. -*FinishAnimation* also takes three boolean parameters: Auto Update XYZ, Auto Update Plane, and Auto Update Facing. These require some explanation. Suppose that the lua programmer pushes an animation step that looks like this: +*FinishedAnimation* also takes three boolean parameters: Auto Update XYZ, Auto Update Plane, and Auto Update Facing. These require some explanation. Suppose that the lua programmer pushes an animation step that looks like this: ```lua tangible.animate{tan=actor, anim={action="emote", animation="dance", xyz={1,2,3}}} @@ -284,7 +284,7 @@ Since "play an emote" isn't a travel command like "moveto" or "warpto", the lua The convention that we have adopted is that to recover from this type of mistake, it is considered acceptable to just play the emote in-place (ie, without moving the actor), then, when the emote is fully finished, the blueprint warps the player to the specified XYZ coordinate. In other words, every animation step is treated as if it has an *implicit* "warpto" at the end of it. This rule guarantees that if the lua programmer sets the xyz, facing, or plane in an animation step, the character will end up at the desired xyz, facing, and plane, no matter what the animation step is. -That's why *FinishAnimation* has those three boolean flags: Auto Update XYZ, Auto Update Plane, Auto Update Facing. If those are all true – and they almost always should be – then *FinishAnimation* will implement the implicit "warpto" for you. I cannot think of a situation where you would want these flags to be false, but I have left the option, in case somebody wants to do something odd in a Tangible Actor. +That's why *FinishedAnimation* has those three boolean flags: Auto Update XYZ, Auto Update Plane, Auto Update Facing. If those are all true – and they almost always should be – then *FinishedAnimation* will implement the implicit "warpto" for you. I cannot think of a situation where you would want these flags to be false, but I have left the option, in case somebody wants to do something odd in a Tangible Actor. ## How Tangible Actors handle Warping Away diff --git a/Source/Integration/BlueprintErrors.cpp b/Source/Integration/BlueprintErrors.cpp index e4818c96..e4b32ff1 100644 --- a/Source/Integration/BlueprintErrors.cpp +++ b/Source/Integration/BlueprintErrors.cpp @@ -5,13 +5,15 @@ ELogVerbosity::Type UlxBlueprintErrorLibrary::ConvertElxLogVerbosity(ElxLogVerbosity Verbosity) { switch (Verbosity) { - case ElxLogVerbosity::Error: return ELogVerbosity::Error; - case ElxLogVerbosity::Warning: return ELogVerbosity::Warning; - case ElxLogVerbosity::Display: return ELogVerbosity::Display; - case ElxLogVerbosity::Log: return ELogVerbosity::Log; - case ElxLogVerbosity::Verbose: return ELogVerbosity::Verbose; - case ElxLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose; - case ElxLogVerbosity::Fatal: return ELogVerbosity::Fatal; + case ElxLogVerbosity::Error: return ELogVerbosity::Error; + case ElxLogVerbosity::Warning: return ELogVerbosity::Warning; + case ElxLogVerbosity::Display: return ELogVerbosity::Display; + case ElxLogVerbosity::Log: return ELogVerbosity::Log; + case ElxLogVerbosity::ThrottledDisplay: return ELogVerbosity::Display; + case ElxLogVerbosity::ThrottledLog: return ELogVerbosity::Log; + case ElxLogVerbosity::Verbose: return ELogVerbosity::Verbose; + case ElxLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose; + case ElxLogVerbosity::Fatal: return ELogVerbosity::Fatal; } } diff --git a/Source/Integration/BlueprintErrors.h b/Source/Integration/BlueprintErrors.h index 519afdbb..c13f3849 100644 --- a/Source/Integration/BlueprintErrors.h +++ b/Source/Integration/BlueprintErrors.h @@ -15,7 +15,7 @@ #include "BlueprintErrors.generated.h" -/* +/* * enum class ElxLogVerbosity, below, contains all the same error severity levels * as ELogVerbosity, but in a form that the blueprint editor can manipulate. * @@ -23,13 +23,17 @@ * We did that because we want the editor to default to 'Error' in most cases. * Unfortunately, that means the numeric values of the two enums don't match up, * so we will need a conversion function. - * + * + * ThrottledDisplay and ThrottledLog are not present in ELogVerbosity. They + * behave like Display and Log respectively, but suppress repeated messages + * with the same format pattern, logging at most once per second. + * */ /** Log Verbosity: The importance of an error message, which affects which logs the error * message gets written to, and how that message gets filtered. - * + * */ UENUM(BlueprintType) enum class ElxLogVerbosity : uint8 { @@ -46,6 +50,12 @@ enum class ElxLogVerbosity : uint8 { /* Prints a message to the log file, however, it does not print to the console. */ Log, + /* Like Display, but suppresses repeated messages with the same format pattern (at most once per second). */ + ThrottledDisplay, + + /* Like Log, but suppresses repeated messages with the same format pattern (at most once per second). */ + ThrottledLog, + /* Prints a message to a log file only if Verbose logging is enabled for the given category. This is usually used for detailed logging. */ Verbose, diff --git a/Source/Integration/FormatMessage.cpp b/Source/Integration/FormatMessage.cpp index 179a0b34..52d1d151 100644 --- a/Source/Integration/FormatMessage.cpp +++ b/Source/Integration/FormatMessage.cpp @@ -575,6 +575,23 @@ UK2Node_FormatLogMessage::UK2Node_FormatLogMessage(const FObjectInitializer& Obj void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxLogVerbosity Verbosity, const FString &InPattern, TArray InArgs) { + // For throttled verbosity levels, suppress repeated messages with the + // same format pattern. We key on the blueprint name + format pattern, + // and allow at most one message per second per key. + // + if (Verbosity == ElxLogVerbosity::ThrottledDisplay || Verbosity == ElxLogVerbosity::ThrottledLog) + { + static TMap LastLogTime; + double Now = FPlatformTime::Seconds(); + FString Key = Context->GetClass()->GetName() + TEXT("::") + InPattern; + double &Last = LastLogTime.FindOrAdd(Key, 0.0); + if (Now - Last < 1.0) + { + return; + } + Last = Now; + } + // Generate the formatted string. // FText InPatternText(FText::FromString(InPattern)); diff --git a/Source/Integration/MovementComponentState.cpp b/Source/Integration/MovementComponentState.cpp index 0fe94e4a..9db0be3f 100644 --- a/Source/Integration/MovementComponentState.cpp +++ b/Source/Integration/MovementComponentState.cpp @@ -55,6 +55,10 @@ FlxMovementComponentState UlxMovementComponentStateLibrary::SetFakeMovementCompo { if (!Actor) return State; UlxTangible *Tangible = UlxTangible::GetActorTangibleOrLog(Actor); - if (Tangible) Tangible->FakeMovementComponentState = State; + if (Tangible) + { + UE_LOG(LogTemp, Display, TEXT("SetFakeMovementComponentState(%s): %s"), *Actor->GetName(), *DebugString(State)); + Tangible->FakeMovementComponentState = State; + } return State; } diff --git a/Source/Integration/MovementComponentState.h b/Source/Integration/MovementComponentState.h index f8eed65d..97c746e6 100644 --- a/Source/Integration/MovementComponentState.h +++ b/Source/Integration/MovementComponentState.h @@ -124,6 +124,6 @@ public: // state is usually read by the animation blueprint // when the real movement component is disabled. // - UFUNCTION(BlueprintCallable, Category = "Luprex|Movement Component State", meta = (AutoCreateRefTerm = "State")) + UFUNCTION(BlueprintCallable, Category = "Luprex|Movement Component State", meta = (DefaultToSelf = "Actor", AutoCreateRefTerm = "State")) static FlxMovementComponentState SetFakeMovementComponentState(AActor *Actor, const FlxMovementComponentState &State); }; diff --git a/Source/Integration/Tangible.cpp b/Source/Integration/Tangible.cpp index e34951fe..ddc49e1c 100644 --- a/Source/Integration/Tangible.cpp +++ b/Source/Integration/Tangible.cpp @@ -253,7 +253,7 @@ void UlxTangible::FinishedAnimation(AActor *target, const FlxAnimationStep &step tan->AnimTracker.FinishedAnimation(step.Hash); if (AutoUpdate) tan->AutoUpdatePosition(); FString DebugString = UlxAnimationStepLibrary::AnimationStepDebugString(step); - UE_LOG(LogLuprex, Display, TEXT("Animation Finished: %s"), *DebugString); + // UE_LOG(LogLuprex, Display, TEXT("FinishedAnimation: %s"), *DebugString); } bool UlxTangible::AnimationStepIsFinished(AActor *target, const FlxAnimationStep &step) diff --git a/luprex/lua/login.lua b/luprex/lua/login.lua index bac0e86b..4320c68a 100644 --- a/luprex/lua/login.lua +++ b/luprex/lua/login.lua @@ -24,10 +24,15 @@ end function engio.move(action, xyz, facing) -- todo: sanity check the parameters. - dprint("engio.move ", action, " ", xyz[1], " ", xyz[2], " ", xyz[3]) + -- dprint("engio.move ", action, " ", xyz[1], " ", xyz[2], " ", xyz[3]) tangible.animate{tan=actor, anim={action=action, interactive=true, xyz=xyz, facing=facing}} end +function moveto(x, y) + local z = tangible.animfinal(actor).xyz[3] + tangible.animate{tan=actor, anim={action="moveto", xyz={x, y, z}, facing=math.auto}} +end + function cube.lookhotkeys(keys) keys:add("Z", "Cube Hi", function () dprint("Doing Cube Hi") end) keys:add("X", "Cube Bye", function () dprint("Doing Cube Bye") end)