// 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. * * 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. * * Every other member of the struct keeps the row the details panel would have given it. */ class FNpcScheduleActionCustomization : public IPropertyTypeCustomization { public: static TSharedRef MakeInstance(); virtual void CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override; virtual void CustomizeChildren(TSharedRef 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 AnchorHandle) const; /** 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 AnchorHandle) const; /** 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 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 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 AnchorHandle) const; /** 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 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 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 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 AnchorHandle, TOptional& OutSnapshot, FTransform& OutLiveTransform) const; bool CanClearAnchor(TSharedRef 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 AnchorHandle, FGuid NewAnchorGuid) const; /** The assign menu: the current selection first, then every anchor in the level, with the one in * use radioed. */ TSharedRef BuildAnchorMenu(TSharedRef 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& AnchorHandle); /** 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 GetAnchorSibling(const TSharedRef& 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 GetCachedAnchorTransform(const TSharedRef& 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& AnchorTransformHandle, const FTransform& Snapshot); /** 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& 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; };