2026-09-20 14:22:08 +02:00
|
|
|
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include "CoreMinimal.h"
|
|
|
|
|
#include "IPropertyTypeCustomization.h"
|
|
|
|
|
#include "Styling/SlateColor.h"
|
|
|
|
|
|
|
|
|
|
class AActor;
|
|
|
|
|
class IPropertyHandle;
|
|
|
|
|
class SWidget;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Details panel layout for one FScheduledAction, the element type of
|
|
|
|
|
* UNpcScheduleAsset::ScheduledActions.
|
|
|
|
|
*
|
|
|
|
|
* An action's anchor is stored as a PersistentId rather than as an actor pointer, so that a re-cook or
|
|
|
|
|
* a re-save cannot leave a schedule dangling. The price of that is a value nobody can type in, so this
|
|
|
|
|
* customization replaces the raw Guid row with one that names the actor the id currently resolves to,
|
|
|
|
|
* offers the current selection and every anchor in the level in a menu, and has a button that clears
|
|
|
|
|
* it. The id therefore always comes from the actor itself and can never drift away from it. A row for
|
|
|
|
|
* an id that no actor in this level claims is left as it is, because that is a real state (furniture
|
|
|
|
|
* that was deleted, or furniture sitting in a sublevel that is not loaded) and not something to hide.
|
|
|
|
|
*
|
2026-09-20 15:50:23 +02:00
|
|
|
* Picking an anchor also records where that anchor stands, into the action's own AnchorTransform, so
|
|
|
|
|
* that a schedule still means something in a running world while the furniture it points at sits in a
|
|
|
|
|
* cell nobody has loaded. The panel is the only moment at which both halves can be had from the actor
|
|
|
|
|
* itself, which is why the two fields that snapshot is stored in are written here and, being written
|
|
|
|
|
* rather than authored, keep no row anything could be typed into.
|
|
|
|
|
*
|
|
|
|
|
* What that record needs is not another field to fill in but a way to see that it has gone stale, so
|
|
|
|
|
* the panel reports it in a read-only row of its own, Anchor Location, directly under the anchor it
|
|
|
|
|
* belongs to. The row stands the recorded place against where the anchor stands now and says so when
|
|
|
|
|
* the two have come apart. It reads the same whole-transform rule as Schedule Asset Recheck, so the
|
|
|
|
|
* row and the sweep can never disagree about which schedules have drifted, and it is the sweep - or
|
|
|
|
|
* picking the anchor again - that clears the marker. The two fields themselves are still reported
|
|
|
|
|
* through the anchor row's tooltip.
|
|
|
|
|
*
|
2026-09-20 14:22:08 +02:00
|
|
|
* Every other member of the struct keeps the row the details panel would have given it.
|
|
|
|
|
*/
|
|
|
|
|
class FNpcScheduleActionCustomization : public IPropertyTypeCustomization
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
|
|
|
|
|
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
|
|
|
|
|
|
|
|
|
|
virtual void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;
|
|
|
|
|
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;
|
|
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
|
|
|
|
|
/** The anchor row: the label, the name of the actor the id resolves to, the assign menu, and the
|
|
|
|
|
* unassign button. Stands in for the raw Guid field, which is never shown. */
|
|
|
|
|
void AddAnchorRow(IDetailChildrenBuilder& ChildBuilder, TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
2026-09-20 15:50:23 +02:00
|
|
|
/** The anchor location row: the place this action kept for its anchor, read back out of the
|
|
|
|
|
* snapshot the pick wrote and stood against where that anchor stands now, directly under the anchor
|
|
|
|
|
* row it belongs to. Read only on purpose rather than a disabled copy of the rows above it: the
|
|
|
|
|
* record is written by picking an anchor and by Recheck Schedules, and a row that let it be typed
|
|
|
|
|
* in would be a third writer able to send an NPC anywhere at all. */
|
|
|
|
|
void AddAnchorLocationRow(IDetailChildrenBuilder& ChildBuilder, TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
2026-09-20 14:22:08 +02:00
|
|
|
/** What the id points at: the actor's label, "None" when nothing is assigned, or the raw id when no
|
|
|
|
|
* actor in this level claims it. */
|
|
|
|
|
FText GetAnchorText(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** The id itself and what it means, so that an id can be read off the panel and matched against a
|
|
|
|
|
* log or a save without the panel offering a text box to mistype it into. */
|
|
|
|
|
FText GetAnchorTooltip(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** Subdues the row while nothing is assigned, so an action with no anchor reads differently at a
|
|
|
|
|
* glance from one whose anchor the panel cannot name. */
|
|
|
|
|
FSlateColor GetAnchorColor(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
2026-09-20 15:50:23 +02:00
|
|
|
/** The recorded place, in whichever state the row is actually in: "None" for an action with no
|
|
|
|
|
* anchor, "Not recorded" for an anchor picked before there was anywhere to record one, and the
|
|
|
|
|
* recorded point itself, marked "(differs)" once the anchor has moved away from it. */
|
|
|
|
|
FText GetAnchorLocationText(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** What the row is reporting and what to do when it reports a difference. Written per state, the
|
|
|
|
|
* way the anchor row's tooltip is, so that an action agreeing with its anchor is not handed
|
|
|
|
|
* instructions it has no use for. */
|
|
|
|
|
FText GetAnchorLocationTooltip(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** Warning while the record and the anchor disagree, subdued while there is no record at all, and
|
|
|
|
|
* ordinary foreground otherwise - a record this level cannot check included, since a schedule for
|
|
|
|
|
* a cell nobody has loaded is exactly what the record is for. */
|
|
|
|
|
FSlateColor GetAnchorLocationColor(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** How the recorded place stands against the anchor itself. One answer worked out in one place,
|
|
|
|
|
* because three of the members above are each asked for it: a row whose text said "(differs)"
|
|
|
|
|
* while its colour said otherwise would be two readings of one rule, and the rule itself is
|
|
|
|
|
* already shared with Schedule Asset Recheck by hand. */
|
|
|
|
|
enum class EAnchorLocationState : uint8
|
|
|
|
|
{
|
|
|
|
|
/** No anchor at all, so there is no place to record and nothing to compare. */
|
|
|
|
|
NoAnchor,
|
|
|
|
|
|
|
|
|
|
/** An anchor, but no place recorded for it: a pick made before there was anywhere to store one,
|
|
|
|
|
* which is the state every action authored before snapshots existed is in. */
|
|
|
|
|
NotRecorded,
|
|
|
|
|
|
|
|
|
|
/** A place recorded, and no actor in this level claims the anchor's id to check it against. */
|
|
|
|
|
Unresolved,
|
|
|
|
|
|
|
|
|
|
/** A place recorded that still matches where the anchor stands. */
|
|
|
|
|
UpToDate,
|
|
|
|
|
|
|
|
|
|
/** A place recorded that the anchor has since moved away from, whole transform and all. */
|
|
|
|
|
Differs
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Which of those states this action is in, with the two transforms it was decided from handed back
|
|
|
|
|
* as well, since every caller has something to say about one or both of them. */
|
|
|
|
|
EAnchorLocationState GetAnchorLocationState(TSharedRef<IPropertyHandle> AnchorHandle, TOptional<FTransform>& OutSnapshot, FTransform& OutLiveTransform) const;
|
|
|
|
|
|
2026-09-20 14:22:08 +02:00
|
|
|
bool CanClearAnchor(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** Writes one id into the action. An invalid id is what unassign does, so the button that clears
|
|
|
|
|
* the anchor and the menu entries that set one both come through here. */
|
|
|
|
|
void AssignAnchor(TSharedRef<IPropertyHandle> AnchorHandle, FGuid NewAnchorGuid) const;
|
|
|
|
|
|
|
|
|
|
/** The assign menu: the current selection first, then every anchor in the level, with the one in
|
|
|
|
|
* use radioed. */
|
|
|
|
|
TSharedRef<SWidget> BuildAnchorMenu(TSharedRef<IPropertyHandle> AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** The id a property handle currently holds, read as text so that it works whether the handle is
|
|
|
|
|
* reached on its own, inside the array, or through a multi selection, which reports no single
|
|
|
|
|
* value and so comes back as unassigned. */
|
|
|
|
|
static FGuid ReadAnchorGuid(const TSharedRef<IPropertyHandle>& AnchorHandle);
|
|
|
|
|
|
2026-09-20 15:50:23 +02:00
|
|
|
/** A field of the action the anchor handle belongs to, reached from the anchor handle rather than
|
|
|
|
|
* handed around: the widgets that hold the anchor handle outlive the layout call that built them,
|
|
|
|
|
* and a sibling looked up at the moment it is written is exactly as fresh as the anchor itself.
|
|
|
|
|
* Null when the handle is not a child of a struct node, or when the struct has no such field, and
|
|
|
|
|
* that is not a failure: the anchor is then assigned without a snapshot, which is the state every
|
|
|
|
|
* schedule authored before snapshots existed is in. */
|
|
|
|
|
static TSharedPtr<IPropertyHandle> GetAnchorSibling(const TSharedRef<IPropertyHandle>& AnchorHandle, FName ChildName);
|
|
|
|
|
|
|
|
|
|
/** Where the actor an id belongs to is standing, answered through the rule the game reads
|
|
|
|
|
* (EleriStandPoint::ResolveAnchorTransform) rather than through the actor's own transform, so that
|
|
|
|
|
* a schedule saved in the editor and one resolved in a running world cannot put the same NPC in
|
|
|
|
|
* two different places. False when no actor in this level claims the id, which is what clearing
|
|
|
|
|
* the anchor leaves behind and what assigning an anchor whose sublevel is not loaded leaves
|
|
|
|
|
* behind. */
|
|
|
|
|
static bool ResolveAnchorSnapshot(const FGuid& AnchorGuid, FTransform& OutTransform);
|
|
|
|
|
|
|
|
|
|
/** The place this action has kept for its anchor, if it kept one. Nothing when no snapshot was ever
|
|
|
|
|
* stored, when those fields are not part of the struct, or when the row stands for several actions
|
|
|
|
|
* at once, since several actions have no one place between them to report. */
|
|
|
|
|
TOptional<FTransform> GetCachedAnchorTransform(const TSharedRef<IPropertyHandle>& AnchorHandle) const;
|
|
|
|
|
|
|
|
|
|
/** Writes the snapshot that goes with an id. Raw data rather than a formatted string, because a
|
|
|
|
|
* transform has no text import to round trip through and this is the only way one reaches the
|
|
|
|
|
* asset. */
|
|
|
|
|
static void WriteCachedAnchorTransform(const TSharedRef<IPropertyHandle>& AnchorTransformHandle, const FTransform& Snapshot);
|
|
|
|
|
|
2026-09-20 14:22:08 +02:00
|
|
|
/** The first actor in the current selection that can be an anchor, or null when there is none. */
|
|
|
|
|
static AActor* GetSelectedAnchorActor();
|
|
|
|
|
|
|
|
|
|
/** The id of an actor, invalid when the actor cannot be an anchor. Implementing
|
|
|
|
|
* IActorPersistentIdInterface is the whole of that rule, since the interface is what carries the id
|
|
|
|
|
* a schedule stores. */
|
|
|
|
|
static FGuid GetAnchorId(AActor* Actor);
|
|
|
|
|
|
|
|
|
|
/** The actor a single id belongs to, or null when no actor in the current level claims it. */
|
|
|
|
|
static AActor* FindActorWithGuid(const FGuid& Guid);
|
|
|
|
|
|
|
|
|
|
/** How many actors in the current level claim an id. More than one is a real state - a pasted actor
|
|
|
|
|
* carries its source's id - and everything that stores the id, this schedule included, resolves to
|
|
|
|
|
* whichever of them a search reaches first, so the row says so rather than leaving it to be found
|
|
|
|
|
* in game. Walked on the spot rather than indexed, because this is read while the panel is drawn
|
|
|
|
|
* and an index would be stale the moment the author adds, deletes or pastes an anchor. */
|
|
|
|
|
static int32 CountActorsWithGuid(const FGuid& Guid);
|
|
|
|
|
|
|
|
|
|
/** Every actor in the current level that can be an anchor, sorted by label so that the menu does not
|
|
|
|
|
* reshuffle itself between two openings. */
|
|
|
|
|
static void CollectAnchorActors(TArray<AActor*>& OutAnchors);
|
|
|
|
|
|
|
|
|
|
/** For the menu entries that only exist to explain themselves, such as "nothing is selected". */
|
|
|
|
|
static bool NeverExecutable();
|
|
|
|
|
|
|
|
|
|
/** Past this many anchors the menu stops listing actors and says how many it left out, because a
|
|
|
|
|
* level can hold hundreds and a menu is not a browser. Assigning from the selection still works. */
|
|
|
|
|
static constexpr int32 MaxListedAnchors = 40;
|
|
|
|
|
};
|