Skip to content
CISV2.0GET IT ON FAB

CHANGELOG

Changelog

2.0 is a major release with breaking changes. Upgrading from 1.x: every change says who is affected by each change and what to do. Each entry below says what changed and why. “#1” to “#24” number the 1.x issues that the 2.0 code review listed; “N1” to “N6” number further issues found during the 2.0 work.

  • Plugin version 2.0.
    • What changed: CharacterInteractionSystem.uplugin has "Version": 2 and "VersionName": "2.0" (1.x: 1 and “1.0”).
    • Why: 2.0 is a major release with breaking changes: assets are migrated when they load, and every behaviour change is described in Upgrading from 1.x: every change.
  • Custom version CISVersion.
    • What changed: The plugin registers the custom version CISVersion (GUID 7C0D5A1E-4B2F-4E7A-9C3D-2A6B8F1E5D40, version ExecutionPolicies = 1; C++ FCISCustomVersion in Common/CISCustomVersion.h). Every Action, MoveHandler and Condition writes it when it is saved, so a package saved with 2.0 that contains one carries it. 1.x does not know it: when 1.x loads such a package it logs the warning “Package was saved with a custom version that is not present” and loads it without the new fields. A package without it was saved before 2.0 and is migrated when it is loaded (see “Migration on load”).
    • Why: Packages saved with 1.x must be told apart from new ones, so that old assets keep their 1.x behaviour and new assets get the 2.0 defaults.
    • What to do: Keep the 1.x versions of your packages if you may need to go back to 1.x.
  • New policies and settings.
    • What changed: Actions and MoveHandlers (UCISReplicatedObject) get InstancingPolicy (ECISInstancingPolicy: Default, SharedPerObject, InstancedPerExecution; default Default) and NetExecutionPolicy (ECISNetExecutionPolicy: AllMachines, LocalPredicted, ServerOnly, LocalOnly; default AllMachines), Conditions get ServerValidation (ECISConditionServerValidation: Default, Validate, DoNotValidate; default Default). FCISInteractionObjectSettings gets bTransferOwnershipDuringUse (default true), bManageOwnerDormancy (default false) and MaxUseDuration (default 0, no limit). A class with its own RPCs or replicated properties (a Blueprint or C++ class below the plugin’s Action or MoveHandler classes; the plugin’s own fields do not count) always runs as SharedPerObject; choosing InstancedPerExecution for it (explicitly or through Default on a Blueprint class) gives the data validation warning “Uses its own RPCs or replicated properties; runs as SharedPerObject”, which a Blueprint compile also shows. New C++ API: UCISReplicatedObject::GetEffectiveInstancingPolicy (the policy a use applies, never Default), UCISReplicatedObject::IsWithReplication, UCISInteractionObjectCondition::ShouldValidateOnServer, CIS::Migration::ClassHasCustomNetFields (Common/CISMigration.h), and the enums ECISUsePhase and ECISUseRejectReason.
    • Why: 2.0 takes the execution model of the Gameplay Ability System: a copy per use (InstancedPerExecution) removes the shared state that simultaneous and repeated uses corrupted in 1.x, and NetExecutionPolicy says where an Action or MoveHandler runs and who leads it, so that the server can decide. A class with its own RPCs or replicated properties needs one replicated object, so it cannot run as a copy.
  • C++ classes keep the 1.x behaviour by default.
    • What changed: Default is resolved by the object’s class: a C++ class keeps the 1.x behaviour (SharedPerObject, its Conditions are not validated on the server), a Blueprint class gets the 2.0 behaviour (InstancedPerExecution, validated on the server); an explicit value always wins. So objects of C++ classes that are never loaded from an old package (built with CreateDefaultSubobject or NewObject in C++, or spawned from C++ classes) behave as in 1.x.
    • Why: C++ code must not change its behaviour after the upgrade without any change of its own, while new Blueprint content gets the safer 2.0 defaults. A Condition that the server validates and that reads client-only state would reject every use of a remote client.
    • What to do: A C++ class that wants the 2.0 behaviour sets InstancingPolicy or ServerValidation in its constructor.
  • Migration on load.
    • What changed: An Action, MoveHandler or Condition that its package’s linker loads from a package saved before CISVersion keeps its 1.x behaviour: it gets the value of its nearest Blueprint archetype (class default or template) saved with 2.0, Default included, and otherwise the explicit 1.x value: InstancingPolicy SharedPerObject, NetExecutionPolicy AllMachines, ServerValidation DoNotValidate. For NetExecutionPolicy the object’s own bWithReplication decides in both directions, as in 1.x: off gives LocalOnly; on gives the inherited value, except that an inherited LocalOnly becomes AllMachines. A C++ archetype (a C++ class default or a C++ default subobject) passes nothing, and an archetype that was itself saved before 2.0 is looked through to its own archetype. So a pre-2.0 Blueprint class, its instances and new instances made from it keep the 1.x behaviour. The migration runs on every load of an old package, while its data is read (a class default at its PostLoad), and an object that a load builds from an old archetype before that archetype is migrated (for example a child Blueprint, a Blueprint instance or a level saved with 2.0 whose parent Blueprint was never re-saved) gets the archetype’s migrated values. A re-save writes a migrated value only where it differs from the object’s archetype, as for every property: a re-saved Blueprint class default holds explicitly each value that differs from its C++ parent class (InstancingPolicy SharedPerObject differs from Default), while an instance or a child that equals its archetype keeps inheriting from it, so a later change of the archetype reaches it. Copies made in memory are not migrated and keep the values they copied, the explicit values a user set on an object of an old package included: PIE copies, duplicates (Ctrl+D, DuplicateObject), copy and paste, and undo. New C++ API (Common/CISMigration.h): CIS::Migration::IsFromPackageBefore, NeedsLegacyMigration, GetNonNativeArchetype, ApplyLegacyDefaults, IsLoadingLegacyData, InheritPendingLegacyArchetype; UCISReplicatedObject and UCISInteractionObjectCondition override PostInitProperties.
    • Why: Assets saved with 1.x keep their 1.x behaviour without a re-save, and a re-save keeps it; content built from an old archetype keeps it too.
  • Cooking after the upgrade.
    • What changed: After upgrading to 2.0, make a full (non-iterative) cook. A cooked game does not migrate (it loads packages without the linker that the migration reads the version from); the cook migrates each package when it loads it in the editor and writes the result. An iterative cook can keep packages that 1.x cooked, which hold none of the new fields, so their Blueprint objects would resolve Default to the 2.0 behaviour.
    • Why: A cooked game cannot migrate; only the editor (and so the cook) reads the version of a package.
  • Blueprint variables with the new names.
    • What changed: UCISReplicatedObject now has properties named InstancingPolicy and NetExecutionPolicy, and UCISInteractionObjectCondition one named ServerValidation. When a Blueprint below these classes has a variable of the same name, the Blueprint compiler renames that variable (to InstancingPolicy_0 or similar) and logs the warning “Found a member variable with a conflicting name (…) - changed to …”; the Blueprint’s nodes follow the rename. Only a variable that is itself a property of the same kind, an enum of a C++ enum class type, is instead removed, and its nodes are pointed at the plugin’s property (a note, “replaced references with native parent class variable”); its nodes then fail to compile where the enum types differ. A 1.x Blueprint cannot have held a variable of the new enum types.
    • Why: The new properties of the base classes can have the names of variables that a Blueprint below them already has.
    • What to do: Rename such variables.
  • Live Coding and hot reload.
    • What changed: The cache behind CIS::Migration::ClassHasCustomNetFields is also cleared after every Live Coding patch or hot reload (FCoreUObjectDelegates::ReloadCompleteDelegate), as it already was when Blueprints are reinstanced in the editor, so a class that a patch gives its own RPCs or replicated properties runs as SharedPerObject from then on.
    • Why: A stale cache would let a class that a patch gave its own RPCs or replicated properties run as a copy.
  • Executions.
    • What changed: A use runs through an execution, UCISUseExecution (Execution/CISUseExecution.h), which UCISCharacterInteractionComponent::StartUseExecution creates when the default UCISInteractionDataObject::UseObject runs. It runs the MoveHandler and the Action of the use side in the order of the side’s ActionExecutionType, calls the data object’s events in the 1.x order (OnObjectUseStarted, OnMovementStarted, OnMovementEnded, OnObjectActionStarted, OnObjectUseFinished) and its virtual hooks OnMovementToObjectFinished, OnObjectActionUseFinished and FinishUseObject at the 1.x points, and ends the use. UseObject, InterruptUsing and FinishUseObject stay the virtual entry points (start, UCISUseExecution::Interrupt, UCISUseExecution::End); the execution never calls UseObject or InterruptUsing. The 1.x code of InterruptUsing, StartObjectAction, OnMovementToObjectFinished and FinishUseObject still runs a use that an override of UseObject runs itself. Every machine that takes part in a use runs its own execution (see “Use phases on the network”). New C++ API: UCISUseExecution, FCISExecutionContext, UCISCharacterInteractionComponent::StartUseExecution and GetActiveUseExecution, UCISInteractionDataObject::GetCurrentExecution.
    • Why: One object runs a use on every machine, in the 1.x order of events, as an ability does in the Gameplay Ability System; the virtual functions that C++ code overrides stay where they were.
  • Per-execution instances.
    • What changed: InstancingPolicy takes effect. SharedPerObject runs the Action or MoveHandler set on the interaction object itself, as in 1.x (its variables carry over from use to use). InstancedPerExecution runs a copy made for the use (NewObject with the object component as the outer and the object set on it as the template, RF_Transient), released when the use ends, so its variables start from the object’s values at every use and never reach the next use. Each machine that runs the use makes its own copy; a copy is not replicated. A MoveHandler copy ticks while it moves; the object set on the interaction object does not tick for it. Content saved with 1.x and C++ classes keep SharedPerObject; new Blueprint classes get InstancedPerExecution (Default).
    • Why: With one shared object, overlapping or repeated uses of an object (several characters, a quick second use) changed each other’s variables.
    • What to do: Keep state that must outlive a use on the object’s actor or component, or set the class to SharedPerObject.
  • Execution context.
    • What changed: Actions and MoveHandlers get GetExecutionContext (FCISExecutionContext: Character, CharacterComponent, ObjectComponent, SideIndex, ActionIndex, ExecutionId, bIsServer, bIsOwner, bIsSimulated, bRejectedByServer) and the Blueprint-pure getters IsExecutingOnServer, IsLocallyPredicted, IsSimulatedExecution, WasRejectedByServer (category “CIS|Execution”). The context is set when the use starts, before OnObjectUseStarted, on every machine that runs the use; a SharedPerObject object gets a default context again when the use ends, a copy keeps its own. C++: SetExecutionContext, GetExecutionTemplate, IsPerExecutionInstance.
    • Why: Blueprints of Actions and MoveHandlers need to know for whom and where they run (server, predicting owner, a machine that only shows the use) without new parameters on the 1.x events.
  • New object delegates.
    • What changed: UCISInteractionObjectComponent gets the Blueprint-assignable OnUseStartedDelegate (after OnObjectUseStarted), OnActionStartedDelegate (after OnObjectActionStarted, with the action that runs) and OnUseFinishedDelegate (after OnObjectUseFinished and the release of the object, before the character’s OnEndUseObjectDelegate), with the delegate types 1.x declared but never used. They fire on every machine that runs the use. They are paired (OnUseFinishedDelegate only after OnUseStartedDelegate) and not fired for a use that a listener ended before them.
    • Why: 1.x declared these delegate types but never fired them; listeners on the object (UI, AI, level scripts) had to bind to every character.
  • The action is chosen when the use starts.
    • What changed: The action of a use is chosen from the object state right before OnObjectUseStarted (its index is in the execution context). 1.x looked it up right after OnObjectUseStarted, so a state change made inside OnObjectUseStarted chose the action of the new state.
    • Why: A use has one action from its start, the one that the client’s request names and the server checks.
    • What to do: Change the state before the use instead (for example in the character’s OnStartUseObjectDelegate).
  • The use side is fixed for the whole use.
    • What changed: A use keeps the side it started at for the movement, its abort, the action and the side’s ActionExecutionType. 1.x read the data object’s current side at each step: a character that entered another side during the use (moved by its MoveHandler, for example) started the action with that side, and that side’s ActionExecutionType decided whether the action started after the movement; a character that left every side got a default side, so a StartParallelWithMoveHandler use started its action a second time when the movement finished.
    • Why: One use, one side: the server checks the side of the request, and a side change in the middle of a use started the action twice in 1.x.
  • Ends inside a use’s own events.
    • What changed: A use that a listener ends or interrupts inside OnObjectUseStarted, OnMovementStarted, OnObjectActionStarted or the new object delegates stops there (1.x went on to start the movement and the action of the ended use, and could fail the check in FinishUseObject); an interruption or end requested while the use ends is ignored (1.x could call OnObjectUseFinished twice); a use whose character is gone when its action is due ends as interrupted, before the action events (1.x dereferenced the null character). An action that finishes before the action step ends the use, as in 1.x, and the use never reaches the Acting phase; empty steps are completed at once.
    • Why: 1.x went on with a use that had already ended, and could end it twice or crash.
  • N4 kept: an interruption after the movement.
    • What changed: A SharedPerObject MoveHandler, which is all 1.x content, is still aborted by an interruption after its movement has finished (MovementAborted after MovementFinished), as in 1.x. A MoveHandler copy (InstancedPerExecution) gets MovementAborted only while it moves.
    • Why: Changing it would change the events of every 1.x MoveHandler; a copy is new, so it gets the cleaner rule.
    • What to do: In a MoveHandler copy, clean up in MovementFinished too.
  • The active use and its phase.
    • What changed: The character interaction component keeps its hold and its use in one structure, FCISActiveUse (the protected UCISCharacterInteractionComponent::ActiveUse), which holds the running UCISUseExecution from its start until the use ends. New Blueprint-pure GetActiveUsePhase (ECISUsePhase): Holding from TryUseObject until the use starts or the hold is cleared, Pending on a remote client while it sends its request, then the execution’s Moving and Acting, Ending only inside the end, None otherwise and on a component without a character (#13). IsCharacterUseAnyObject, IsHasDelayedObjectData and GetDelayedObjectProgress keep their 1.x timing, so inside OnStartUseObjectDelegate the phase is still Holding. New C++ API: FCISActiveUse, UCISCharacterInteractionComponent::ActiveUse, NextPredictionKey and EndActiveUse (protected), UCISUseExecution::OnPhaseChanged; the component overrides EndPlay (a subclass’s override must call Super).
    • Why: One structure replaces the scattered 1.x flags of a use, so that a use and its prediction can be tracked on every machine, and the UI can read one phase.
  • MaxUseDuration.
    • What changed: MaxUseDuration of FCISInteractionObjectSettings takes effect. On the authority (standalone, the server, a listen host), a use that still runs MaxUseDuration seconds after it started (after the hold) ends as interrupted, through the data object’s InterruptUsing as with TryInterruptObjectUsing, whatever the object’s ObjectInterruptionType (None included); the value is read when the use starts. 0, the default, is no limit, as in 1.x. Every end of the use and the component’s EndPlay stop the limit. The server also limits a remote client’s use, on its own clock; the client’s use then ends as interrupted through the server’s end (without InterruptUsing on the client).
    • Why: The authority needs a safety limit for uses that never end (a lost client, an Action that never finishes).
  • A use that does not start ends at once.
    • What changed: When a hold completes and the data object’s UseObject starts no use (the data object is no longer usable, for example because a listener of OnStartUseObjectDelegate made it unusable, or a C++ override of UseObject starts none), the announced use ends at once: OnClearDelayedObjectData, then one OnEndUseObjectDelegate with the object, and IsCharacterUseAnyObject is false again.
    • Why: In 1.x the character stayed “using” with no end: IsCharacterUseAnyObject stayed true and it could not use any object again.
  • N3 follow-up: a late FinishMovement is logged.
    • What changed: A late FinishMovement that the plugin ignores (the movement is not running: never started, or already finished and invalidated) logs a Verbose line in CISLog.
    • Why: An ignored finish is easy to miss when you debug a MoveHandler.
  • A new use from the end of the previous one.
    • What changed: When a listener of the object’s OnUseFinishedDelegate (or of the data object’s end) starts a new use of the same character, the first use’s tail runs first (its MaxUseDuration timer stops, its destruction binding goes, and a remote client sends its Ending to the server before the new request), and the end of the first use no longer resets the execution context that the new use gave a shared Action or MoveHandler. While the new use runs, IsCharacterUseAnyObject reads true and GetCurrentUsingObject names its object, so no hold can start, and the character’s OnEndUseObjectDelegate fires at the end of each use. Every use that the character component starts sets these 1.x flags, also one that C++ starts without the hold (StartUseExecution, or the data object’s UseObject): it reads as in use while it runs and broadcasts the character’s OnEndUseObjectDelegate at its end (OnStartUseObjectDelegate stays an event of the hold).
    • Why: Since the object is free inside OnUseFinishedDelegate, an AI can chain uses there; before this fix the first use’s limit could end the new use as interrupted, and the server ended its run of the first use as interrupted.
    • What to do: C++ that starts uses without the hold and listens to the character’s OnEndUseObjectDelegate: expect an end without an OnStartUseObjectDelegate before it. Such a use (also 1.x content that calls the data object’s UseObject directly) now reacts to TryInterruptObjectUsing: when the object’s ObjectInterruptionType matches, the call interrupts it (1.x did nothing); for a use whose data object the character does not track (one that C++ made itself), the call creates a new tracked data object instead and the use goes on. TryUseObject starts no hold while such a use runs.
  • Server-authoritative use requests.
    • What changed: A remote client’s use goes through the server. When the hold of an object with a use time above 0 starts, the client tells the server (ServerNotifyHoldStarted); when the use starts, it sends ServerRequestUse (object, side index, action index, the use’s ExecutionId as prediction key) and runs the use at once (prediction; GetActiveUsePhase is Pending while the request is sent). The server checks the request (see “Server validation and Project Settings”). Confirmed (ClientConfirmUse): the server sets the object’s use state, IsObjectUsing, CurrentUsingObject and, with bTransferOwnershipDuringUse, the owner of the object’s actor, runs the use itself, and gives the previous owner back when the use ends. Rejected (ClientRejectUse with ECISUseRejectReason): the client’s use ends as interrupted with WasRejectedByServer() true on its Action and MoveHandler (the data object’s InterruptUsing is not called), and the client gives the object’s actor its previous owner back. Every rejection carries the object’s state and usable flag on the server, and the client’s copy of the object takes them, also when the rejected use has already ended there (for example an Action that finished at once and set its NewStateAfterFinishAction before the answer arrived). A standalone game, a listen host’s own character and characters that the server runs are never checked. Stale or repeated keys are ignored on both sides. The server sets the user’s CurrentUsingObject when the request arrives, after the client’s OnStartUseObjectDelegate (1.x: before it), so a Server RPC sent from that delegate reads null there. New C++ API: the protected RPCs ServerNotifyHoldStarted, ServerRequestUse, ClientConfirmUse, ClientRejectUse (with the requested object and its state and usable flag on the server) and ClientSyncExecutionKeys (see “Keys”) of UCISCharacterInteractionComponent; FCISActiveUse::bRequestSent, bAwaitingServerAnswer and bRemoteUseOnServer.
    • Why: 1.x was client-authoritative: the server only relayed what a client decided. Now the server decides, while the client still starts the use at once, as with a predicted ability.
    • What to do: Check the tolerances in Project Settings when your game lets characters use objects from farther away or right after a teleport.
  • Server validation and Project Settings.
    • What changed: The server checks a request with CIS::Validation::ValidateUseRequest (FCISUseRequest) in the order ObjectInvalid, NotUsable, Occupied, NoActionForState, BadSide, OutOfRange, BadAngle, ConditionFailed, HoldTooShort. Before these checks, also with bServerValidation off, a side index that the object on the server does not have is rejected as BadSide, and an action index that its side does not have as NoActionForState (the settings are not replicated, so a client’s copy can differ). Conditions that the server validates (ServerValidation) run there without a data object. The tolerances and a switch are the new project settings UCISNetworkSettings (Project Settings > Plugins > Character Interaction System, saved in DefaultGame.ini): bServerValidation true, DistanceToleranceAbsolute 100, DistanceToleranceScale 0.5, AngleToleranceDegrees 30, HoldTimeTolerance 0.3. A rejection is logged in CISLog on the server (“ServerRequestUse from <character>: the use of <actor> is rejected (<reason>), key <key>.”) and on the client (“ClientRejectUse on <character>: the server rejected use <key> (<reason>).”). One case rejects what 1.x let the owner do: content that changes the object’s state tag only on a client (SetObjectState on a client does not reach the server) gets its use rejected as NoActionForState when the client’s state chooses another action than the server’s; the client’s SetObjectState logs a warning (#17). Such content already went out of sync in 1.x multiplayer. The server also checks with its own settings (use time, interaction angle, distance, sides), so values changed on the client only are rejected. New C++ API: UCISNetworkSettings, FCISUseRequest, CIS::Validation::ValidateUseRequest, CIS::Geometry::ResolveTargetComponent, GetTargetPointFrom and GetTargetPoint (the point a character faces, shared by the data object and the server check).
    • Why: Server authority needs a check that is on for old content too; the tolerances are generous and the same for every asset, so that legitimate uses are not rejected, and one switch turns the check off.
    • What to do: Change object states and usable flags on the server; set DoNotValidate on Conditions that read client-only state; widen the tolerances where needed. See “When the server rejects a use” in Upgrading from 1.x: every change.
  • #1 The owner of the object’s actor is given back after a use.
    • What changed: The owner of an object’s actor is given back at the end of a use, on every machine: the use remembers the previous owner and restores it, but only while the actor’s owner is still the controller that the use set (an owner that game code set during the use is kept; FCISActiveUse::TransferredObjectOwner). C++ SetOwnerOfActor(Actor, true) passes bClear to ServerSetOwnerOfActor.
    • Why: In 1.x the plugin cleared the owner locally and then called ServerSetOwnerOfActor without bClear, so on the server (and in a standalone game) the user’s controller stayed the owner after every use.
  • #2 A client may change the owner only of the actor it uses.
    • What changed: ServerSetOwnerOfActor called by a client changes only the owner of the actor of the object that the server has confirmed for that character, while that use runs; for any other actor it logs a warning in CISLog (”… a client may only change the owner of that actor. Ignored.”) and changes nothing. A call on the server outside a received client RPC is not limited; the check is UE::Net::FNetContext::IsInsideNetRPC(), so server code that runs inside any received client RPC (a Server…_Implementation, an Action relayed from a client) is limited too. ServerSetCurrentUseObject called by a client accepts only null or the object of the server’s active use of that character (else a warning; the same IsInsideNetRPC rule); null from a client no longer ends the server’s run of that client’s use (its end is ServerUsePhase). The plugin no longer calls ServerSetOwnerOfActor for its uses, nor ServerSetCurrentUseObject with an object; the data object’s 1.x FinishUseObject fallback (a use without an execution) still calls SetOwnerOfActor(Actor, true). A C++ override of UseObject that runs the use without Super on a remote client (the 1.x fallback) sends no request, so the server refuses its ServerSetOwnerOfActor and never records the use.
    • Why: Through ServerSetOwnerOfActor any client could make its controller the owner of any actor on the server.
    • What to do: C++ code that calls ServerSetOwnerOfActor or SetOwnerOfActor on a client for another actor is refused now; code that relied on the controller staying the owner after a use must set the owner itself on the server; a C++ UseObject override without Super should call Super (the plugin’s use, with a request) or run on the server.
  • #17 Object state and usable flag set on a client.
    • What changed: SetObjectState and SetUsable of UCISInteractionObjectComponent called on a client’s copy (a simulated proxy; an interactable stays one on a client also while that client’s controller owns its actor during a use) still change only that copy, as in 1.x (SetObjectState also broadcasts there), and now log once per component a warning in CISLog: “<function> on <actor> called on a client; the change is local only. Call it on the server.” The plugin’s own calls (the replicated state arriving, an Action’s NewStateAfterFinishAction) do not log.
    • Why: 1.x checked roles inconsistently, and such calls from a client silently never reached the server; the server state is the one that counts.
    • What to do: Call SetObjectState and SetUsable on the server.
  • #19 RPC validation.
    • What changed: The new RPCs validate their input: ServerRequestUse (a key below 1 or above CIS::MaxExecutionId, an object of another world), ServerUsePhase (a key outside 1..CIS::MaxExecutionId, a phase that a client never sends) and ServerNotifyHoldStarted (an object of another world). A malformed call disconnects the client. A side or action index that the object on the server does not have is not malformed (the settings are not replicated, so a client’s copy can differ): the request is rejected (BadSide, NoActionForState), and a hold notice for such a side is recorded as any hold. Everything that depends on state or timing is a rejection (ClientRejectUse) or a warning, never a disconnect; a null object is not malformed (it may have been destroyed on the way). The 1.x RPCs keep accepting every call.
    • Why: Every _Validate of 1.x returned true; only input that no correct client sends should disconnect.
  • The use state, and an object in use for a rejected client.
    • What changed: New FCISObjectUseState and UCISInteractionObjectComponent::GetUseState (Blueprint-pure, replicated: User, ExecutionId, SideIndex and Phase of the use the server knows), IsObjectOccupied and SetUseState_Authority. A character’s data object treats an object as in use, on every machine, also while the object’s use state names another character, so a client whose use was rejected as Occupied no longer sees the object as free while the other use runs (its own IsObjectUsing can still read false).
    • Why: Late joiners and dormant objects need the use in replicated state, and a rejected client must not see a used object as free.
    • What to do: Read GetUseState too where content decides on a client that an object is free.
  • Use phases on the network.
    • What changed: Every machine that takes part in a use runs it (the user’s machine, the server, and the clients that have the object’s actor, which show it), and the 1.x Server and Multicast relays of the Action and the MoveHandler are no longer sent. Each step has a leader by the NetExecutionPolicy of its object (AllMachines, LocalOnly and a step without an object: the user’s machine; LocalPredicted: the user’s machine and the server, whichever completes it first; ServerOnly: the server; a use of a character on the server leads every step); a machine that does not lead a step runs its own copy and waits for the leader’s phase. The user’s client sends ServerUsePhase (action started, ended), the server multicasts every phase through the object (MulticastUsePhase with FCISUsePhaseEvent) and sends the user’s client the phases its run reached by itself (ClientUsePhase). A client that gets the object while a use runs shows nothing of it, and a player who joins during another player’s use does not run its Action or MoveHandler, as in 1.x. New C++ API: FCISUsePhaseEvent; UCISUseExecution::LeadsStep, LeadsOwnStep, OwnerLeadsStep, ApplyLeaderPhase and IsEndInterrupted; UCISInteractionObjectComponent::MulticastUsePhase, SetObjectIsUsingForUse, TakeOwnershipForUse and ReleaseOwnershipForUse (protected); UCISCharacterInteractionComponent::ServerUsePhase and ClientUsePhase (protected) and HandleSimulatedUsePhase; FCISActiveUse::UserController, ObjectActor, bSimulatedUse and bEndAppliedFromRemote.
    • Why: With server authority the server must run the use itself; the relays let each machine follow a client’s word, sent each finish a second time (#24) and needed ownership of the object.
    • What to do: C++ overrides of the relays’ _Implementation functions are no longer called by the plugin’s uses: move such code to the Action’s or MoveHandler’s own events.
  • #24 An Action finishes once per use on every machine.
    • What changed: An Action that finishes by itself on a machine that does not lead it (for example a Blueprint Action that calls FinishAction on every machine) is finished there once per use instead of twice, so its OnUseFinished, NewStateAfterFinishAction and, on a client, OnObjectStateChangedDelegate run once there; it no longer ends the use there, the leader’s end does.
    • Why: On the server and the other clients 1.x finished such an Action a second time at MulticastFinishAction, so OnUseFinished fired twice.
  • #10 A phase for a character that a client does not know.
    • What changed: A phase whose character a client does not know (the character is not relevant to it) is skipped there, with a Verbose line in CISLog, without a crash. The plugin no longer sends the 1.x multicast relays. The deprecated relays that code may still call (MulticastStartObjectAction of the Action, MulticastStartMovement and MulticastMovementAborted of the MoveHandler) do nothing on a client that gets a null character (the character is not relevant to it).
    • Why: The 1.x multicasts dereferenced a null character on a client to which the character was not relevant.
  • #18 An Action that finishes only on the server.
    • What changed: With ServerOnly or LocalPredicted, a FinishAction on the server ends the step for every machine: the user’s client gets it through ClientUsePhase, the other clients through the object’s phases. With AllMachines (all 1.x content) the user’s machine leads, so a FinishAction that only the server calls finishes only the server’s copy, as in 1.x.
    • Why: In 1.x a FinishAction called only on the server never reached the client, and the client’s use hung. AllMachines keeps the 1.x rule so that old content does not change.
    • What to do: If your Action finishes only on the server, choose ServerOnly or LocalPredicted.
  • Ends that the server forces.
    • What changed: The server’s run of a remote client’s use ends as interrupted when the client’s pawn is destroyed (also on disconnect), its controller changes (one tick after the change), the object’s actor is destroyed, the object’s MaxUseDuration elapses (on the server’s clock) or a new request of the same client arrives. The use state, IsObjectUsing, CurrentUsingObject and the previous owner come back, also when only the object component was destroyed during the use. The user’s client gets the server’s end while it still controls the pawn; after a pawn change the pawn’s new owner gets no ClientUsePhase of the previous owner’s use. A client that shows a use ends it with the user’s pawn, the object’s actor or its object component. On the user’s machine these ends interrupt the use’s execution (UCISUseExecution::Interrupt), as a rejection does: the data object’s events fire, but its InterruptUsing is not called (a C++ override of it does not see them).
    • Why: Without them an object stayed in use on the server when its user’s client vanished or changed its pawn.
  • Ownership and IsObjectUsing per use.
    • What changed: Overlapping uses of one object (bServerValidation off) hold its actor’s owner per use, so the owner of the latest remaining use, and after all of them the owner before the uses, comes back. The plugin writes IsObjectUsing on each machine itself (SetObjectIsUsingForUse: no ServerSetObjectIsUsing from the client, no false while the use state names another character). The user’s machine keeps its use’s IsObjectUsing and owner against the late server state of its previous use (a quick second use under network lag), so the Server RPCs of its Action or MoveHandler on the object’s actor still leave with the owner.
    • Why: With the server running the use, each machine owns its own value, and a late state of a previous use must not undo the current one.
  • Keys.
    • What changed: One ExecutionId space per character component. A use that the server runs itself for a remote player’s character takes an id after every key the server has seen, a request with a key that such a use took is rejected (Occupied), and the client continues after every id that the server’s phases showed it. When a remote player’s controller takes a character, the server sends that client the character’s last key (ClientSyncExecutionKeys, one tick after the change), so that its next use takes a newer key; a use of that client that still waits for an answer and has a key up to that one ends as rejected. CIS::MaxExecutionId (MAX_int32 / 2): ServerRequestUse and ServerUsePhase with a larger key are malformed and disconnect the client.
    • Why: The key of a predicted use must name one use on both machines, and must not overflow.
  • A pawn given to a client that shows its use; release before the object’s delegate.
    • What changed: A client that shows a use of a pawn and is given that pawn ends the shown use (at the server’s phase of it, or at the latest one tick after the pawn change), so the new owner can use objects with the pawn at once. On the authority a use releases what it took (the use state; for the server’s run of a remote client’s use also IsObjectUsing, CurrentUsingObject and the owner of the object’s actor) before the object’s OnUseFinishedDelegate fires, so a server-side listener of that delegate finds the object free; on a client that shows the use, IsObjectUsing and GetUseState are the replicated values inside it. New API: UCISUseExecution::OnReleasing, FCISActiveUse::bReleasedOnAuthority.
    • Why: Before, the shown use never ended on the new owner and the pawn could not use any object there; and a server-side listener (an AI that uses the object again) found the object still in use.
  • Known limitations of networked uses.
    • What changed: The hold is measured on the server’s clock, and a hold notice sent again after packet loss shortens the hold that the server measures by about one round trip (HoldTimeTolerance covers about one frame plus jitter). During its own use, a client’s copy of the object ignores an owner that the server gives the object’s actor, and after the use keeps the owner from before the use until the server changes it again. A use that the server runs for a remote player’s character is not shown on that player’s machine, and a request sent before the server’s phases of such a use arrive can be rejected (Occupied) once. When the server changes an object right after it rejected a client’s use of it, and under packet loss that change reaches the client before the rejection, the client’s copy takes the older state and usable flag of the rejection until the server changes them again. Upgrading from 1.x: every change lists all of them.
    • Why: They are the edges of prediction over an unreliable network that 2.0 does not close; none of them affects a standalone game.
  • Movement prediction.
    • What changed: A remote client calls FlushServerMoves of its character’s UCharacterMovementComponent right before ServerRequestUse, as the Gameplay Ability System does before a LocalPredicted ability, so the server no longer applies the root motion of a MoveHandler that runs on the server (AllMachines, LocalPredicted or ServerOnly, for example through UCISRootMotionMovementTask) to a movement that the client made before the use started. The client may send one more small movement RPC per use; the server sends nothing more. Measured in single runs on one machine, with 100 ms of packet lag each way and a 1 s root-motion movement at 200 cm/s: with the client’s movement send interval raised to 0.1 s so that a movement of about 0.06 to 0.09 s was held back at the press, 3 corrections per use without the flush (the same code with the flush removed) and 0 with it; with the engine’s default send interval no run held a movement at the press, and all had 0 corrections with and without the flush.
    • Why: When the client had held a movement back at the start of the use, the server applied the root motion to that movement as well, and corrected the client: a visible snap at the start of the movement.
  • #21 Templates replicate only when they must; N5.
    • What changed: An Action or MoveHandler template of an interaction object is replicated only when it runs SharedPerObject, has bWithReplication on and its class has RPCs or replicated properties of its own (CIS::Migration::ClassHasCustomNetFields; the plugin’s relays and bReplicatedBool do not count). Every other template is no longer replicated: each machine uses its own copy, and the deprecated relays still reach the templates of class defaults, Blueprints and placed actors (they have stable names). UCISInteractionObjectComponent::ReplicateSubobjects starts from Super instead of true, so an idle object no longer writes a bunch at every net update. When the object’s actor uses the registered subobject list, the component joins it at BeginPlay on the server and registers those templates (removed at the new EndPlay); the list is fixed then (a Blueprint recompiled during play, or a template that C++ puts into the settings after BeginPlay, does not change it). N5: a template with bWithReplication off is never replicated.
    • Why: 1.x’s ReplicateSubobjects always returned true and replicated every template, so hundreds of idle objects cost bandwidth at every net update; and the server could fail its check IsSupportedForNetworking for a template with bWithReplication off (N5).
    • What to do: Give a class that must replicate a variable a replicated property or an RPC of its own; a C++ override of ReplicateSubobjects calls Super.
  • Push model.
    • What changed: ObjectState, bObjectIsUsing and UseState of UCISInteractionObjectComponent are push-based (effective with Net.IsPushModelEnabled); every write of the plugin marks them dirty. bUsable stays without the push model (Blueprints write it directly).
    • Why: Push-based properties are not compared at every net update.
    • What to do: C++ subclasses that write these protected fields directly call the setters or MARK_PROPERTY_DIRTY_FROM_NAME.
  • InteractionSettings is no longer replicated; N6.
    • What changed: InteractionSettings of UCISInteractionObjectComponent is no longer replicated: every machine reads its own copy, built from the same class defaults, Blueprint or level data. A client reads its own copy for the data object (sides, angles, distances, use time, Conditions), the Actions and MoveHandler of the use and bTransferOwnershipDuringUse; MaxUseDuration and bManageOwnerDormancy are read on the server only. A change that the server makes at runtime no longer reaches the clients; a server-only change of what the server check uses (the sides, InteractionDistance) makes the client’s own check and the server’s disagree, and the request is rejected. N6: the Conditions of an object spawned at runtime are no longer null on the clients (1.x replicated them as null), so a client evaluates its own Conditions for its own character there too.
    • Why: The settings are static data of the asset, and replicating them cost bandwidth and delivered the Conditions of runtime-spawned objects as null (N6).
    • What to do: Make runtime changes of InteractionSettings on every machine; a Condition that must not run on a client checks where it runs.
  • Owner dormancy.
    • What changed: bManageOwnerDormancy takes effect on the server, for an object’s actor that replicates, in a game that is not standalone. The object’s actor goes DORM_DormantAll at BeginPlay (an author’s DORM_Never is kept, DORM_Initial and DORM_DormantPartial until the first use); a use wakes it before anything of the use replicates; 1 s after the use, with no use or hold left, it is dormant again. The plugin’s setters of the object state, the usable flag and IsObjectUsing flush the dormant actor (FlushNetDormancy), and a late joiner gets the current state.
    • Why: Hundreds of idle objects should cost nothing; dormancy is off by default because it stops the replicated variables and multicasts of the object’s own Blueprints.
    • What to do: Do not turn it on for objects whose own Blueprint or C++ replicates variables or multicasts, whose Blueprints write bUsable directly, whose dormancy other code sets, or whose Action or MoveHandler sends a Server RPC on the object’s actor at the start of a use.
  • MoveHandler tick and CallRemoteFunction.
    • What changed: A MoveHandler ticks only while its movement runs and never as a class default or an archetype (new overrides IsTickable and GetTickableTickType of UCISInteractionObjectMoveHandler); a C++ subclass whose Tick acted before StartMovement or after InvalidateHandler no longer runs that code. UCISReplicatedObject::CallRemoteFunction finds the actor with GetTypedOuter<AActor>() and returns false without one.
    • Why: Every MoveHandler template and class default ticked in 1.x; CallRemoteFunction used CastChecked of GetOuter()->GetOuter(), a fatal error for an object without an actor there.
  • Measured cost.
    • What changed: Single runs on one machine (editor build, about 45 to 60 server frames per second); the numbers vary with the frame rate and the machine, and the plugin’s tests check thresholds, not these values (at most 0.2 times the 1.x bunches and 0.5 times the 1.x bytes, per second and per server frame; with dormancy 0.1 times the bunches). 300 idle objects with two clients: 34.4 to 37.1 bunches/s and 3904 to 5053 bytes/s out of the server in two runs (1.x 22540.3 and 94751), the same as the scene without objects; with bManageOwnerDormancy 34.8 to 37.1 bunches/s. Per remote use: reliable out 7 and in 3 (1.x 10 and 5): the client sends ServerRequestUse and two ServerUsePhase, the server sends ClientConfirmUse and three MulticastUsePhase to each of the two clients; out bunches per use 7.53 to 13.25 (1.x 8.74) at 229 to 294 bytes (1.x 196), in two runs on the same server code. The use state adds an estimated 31 bytes of property data per client and use (three updates; without bunch and packet headers).
    • Why: The cost of idle objects was the main network problem of 1.x with hundreds of objects.
  • Deprecated functions.
    • What changed: SetOwnerOfActor and ServerSetOwnerOfActor of UCISCharacterInteractionComponent are marked UE_DEPRECATED(2.0, …): a call gives a compiler warning; both still work (see #1, #2). The plugin’s own remaining calls (the 1.x path of a use without an execution, which ends through FinishUseObject and SetOwnerOfActor) are wrapped in PRAGMA_DISABLE_DEPRECATION_WARNINGS. Deprecated in doc comments only, kept and working: the relays of UCISInteractionObjectAction (ServerStartObjectAction, MulticastStartObjectAction, ServerFinishAction, MulticastFinishAction) and of UCISInteractionObjectMoveHandler (Server and Multicast versions of StartMovement, MovementAborted, FinishMovement and InvalidateHandler), UCISReplicatedObject::GetLegacyRelayObject, and the protected ServerSetCurrentUseObject. The plugin’s own path calls none of them. ServerSetCurrentUseObject(nullptr) from a client no longer ends the server’s run of its use.
    • Why: The server now sets the owner and the used object itself; the relays are replaced by the use phases. They stay for C++ code that calls them, as the compatibility rules of 2.0 require.
    • What to do: Replace calls of SetOwnerOfActor and ServerSetOwnerOfActor (for example with AActor::SetOwner on the server), or wrap a call that must stay in PRAGMA_DISABLE_DEPRECATION_WARNINGS / PRAGMA_ENABLE_DEPRECATION_WARNINGS.
  • Module dependencies.
    • What changed: The plugin module depends on DeveloperSettings (public) and NetCore (private).
    • Why: The project settings of the server check and the push model need them.
  • #16 A side is identified by its index.
    • What changed: An interaction side is identified by its index in InteractionSides, no longer by the equality of its two angles (operator== of FCISInteractionObjectSide compares only the angles, not bAngleSide). Two sides with the same angles now each use their own actions and their own move handler. Before, such a later side was never selected when it had the same bAngleSide as the earlier one; with the opposite bAngleSide it was selected but used the earlier side’s action and move handler. Moving between two such sides now hides and re-shows the object, as between any two sides. New C++ API: UCISInteractionObjectComponent::GetSideByIndex, FindSideIndex, GetActionIndexForSide, GetObjectActionForSideIndex; UCISInteractionDataObject::GetCurrentSideIndex, SetCurrentUseSideIndex. UCISInteractionDataObject::CurrentSide is deprecated: it is still kept in sync, but writes to it are ignored (the index selects the side).
    • Why: 1.x compared two sides by their two angles (operator==), so a later side with the same angles was aliased to the earlier one; the server check of a use also needs a stable side index.
    • What to do: Content with two sides of identical angles now gets each side on its own (its own actions, its own move handler); if it relied on the old aliasing (the later side using the earlier side’s action and move handler), merge the two sides into one.
  • N1 A side of half a turn.
    • What changed: A side whose two angles are half a turn apart (within about 0.006° of 180°, e.g. −90..90) now covers the half circle around (InteractionAngleMin + InteractionAngleMax) / 2, or the other half with bAngleSide. Before, such a side ignored where the character stood: with bAngleSide it was valid from every direction; without it, it was never valid, or valid from every direction at some object rotations, depending on float rounding. Content that relied on a 180° side being valid everywhere should use 0/0 with bAngleSide. Every other side gives the same result as before. The debug cone of such a side now shows its half circle (before, a cone with no direction); the cone of every other side is drawn as before.
    • Why: 1.x decided such a side by float rounding, so it ignored where the character stood.
  • Component references (FComponentReference).
    • What changed: FCISPrimitiveComponentsOnActor and FCISSceneComponentsOnActor keep their names and store the component in Component (FComponentReference, Blueprint read/write) next to SocketName. A 1.x name (SelectedPrimitiveComponentName, SelectedSceneComponentName) moves into Component.ComponentProperty when an asset is loaded; saving the asset writes only the new field. C++ code that used the old fields must use Component: they are Selected*ComponentName_DEPRECATED now and only read on load. New C++ API: UCISFunctionLibrary::ResolveComponentReference, FCIS*ComponentsOnActor::IsSet. The lookup (used by GetPrimitiveComponentWithName, GetSceneComponentWithName, the trigger components and the angle detection component) tries the property name first, then the object name as 1.x did. So a native component whose object name differs from its property name is now found; 1.x found nothing for it and the reference silently did nothing (e.g. CapsuleComponent of a character, whose object is CollisionCylinder; Mesh, object CharacterMesh0). Two more cases resolve differently from 1.x: a name that is an object-reference variable of component type (a Blueprint variable or C++ property) now gives the component that variable holds, when that component belongs to the same actor (1.x ignored variables and compared object names only); and when property X holds a different component than the one whose object name is X, the property’s component is now used instead of the 1.x object-name match. A name that matches no component still gives none, and now also logs one warning per actor class and name. Every reference in the demo content resolves to the same component as in 1.x. One rare case of the load migration: a 1.x instance (a placed actor, a child Blueprint) that explicitly overrode its archetype’s name with an empty one now keeps the archetype’s migrated reference, because the migration acts only on a non-empty name; to restore the empty reference, clear Component on that instance.
    • Why: The 1.x picker stored property names while the runtime compared object names, so a C++ component whose object name differs from its property name (the capsule of a character) was never found.
  • #3 The facing angle without an angle detection component.
    • What changed: When AngleDetectionComponent is empty or names no component, the facing angle is measured to the object’s root component (at SocketName); 1.x measured it to the world origin. The angle detection component is looked up once per character and object instead of every frame.
    • Why: 1.x left the point at (0,0,0) when it found no component and measured the angle to the world origin.
  • N2 The side’s MoveHandler and Action start as null.
  • The component picker.
    • What changed: The editor picker of FCISPrimitiveComponentsOnActor and FCISSceneComponentsOnActor writes Component. It lists the components of the actor class by the name the reference stores: C++ components by property name (e.g. CapsuleComponent, Mesh), Blueprint components by variable name, inherited ones included; for a Blueprint component overridden in a child Blueprint, the override is read. Every name that 1.x listed is still listed, except component properties that are empty on the class defaults (variables assigned at runtime; in the demo content SelectedKey of the code locks) and editor-only components. Editor-only components (e.g. ArrowComponent of a character, SpriteComponent of an info actor such as the game mode, a Blueprint component marked Editor Only) are not listed because they are stripped from a cooked build: a reference to one resolves in the editor and PIE and gives nothing in the game. RootComponent is now also listed for a primitive reference when the root is a primitive component. A pick replaces the whole reference (PathToComponent and OtherActor are cleared) in one undoable step, and resets SocketName when the picked component has no such socket. The first entry, None, clears the reference and the socket. With several objects selected, only the names that every selected object’s class has are listed; when their actor classes differ, a pick keeps SocketName (the component of that name can differ per class). The socket row keeps the 1.x name field and adds a list of None and the sockets of the selected component. A saved name that the list does not offer is shown as “<name> (not found)” (1.x showed “Select Component”). The picker no longer asserts when the outer chain of the edited object reaches a non-actor class first (for example, settings on a widget of a Widget Blueprint; 1.x called UClass::GetDefaultObject<AActor> on that class). New editor C++ API (Customization/CISComponentPickerUtils.h): CIS::Editor::GatherComponentCandidates, GatherCommonComponentCandidates, GatherSocketNames, FindOwnerActorClass, PickComponent.
    • Why: The engine’s picker of FComponentReference shows no components in the Blueprint editor in UE 5.6 and 5.7 and cannot pick a socket, so the plugin keeps its own picker, on top of the new storage.
  • #6 Overlap per trigger and primitive.
    • What changed: An interaction object stays overlapped while any of the character’s triggers still overlaps any primitive of the object’s actor. Before, the first end overlap of one (trigger, primitive) pair ended the overlap of the whole object (OnOverlapped(false); the object was hidden and not usable, and the cleanup could remove its data) although another primitive of the actor, or another trigger, still overlapped it; it came back only with the next begin overlap. New protected C++ API: UCISCharacterInteractionComponent::IsActorOverlappingAnyTrigger.
    • Why: 1.x kept one overlap flag per actor, so the first end overlap of one pair ended the overlap of the whole object.
  • #9 Null triggers.
    • What changed: InitInteractionTriggers skips null entries of its array and binds the triggers after them. Before, a null entry crashed (an assertion in AddDynamic).
    • Why: 1.x crashed on a null entry.
  • #5 The cleanup no longer interrupts a use.
    • What changed: The periodic cleanup (TimeToClearInvalidObjectsData) no longer removes the data of an object the character is using. Before, when the character left the object’s overlap during a use (for example moved by a move handler or an action), the next cleanup interrupted the use: the move handler got MovementAborted, the action finished with bInterrupted = true, and OnEndUseObjectDelegate fired. Now the use runs to its end, and the first cleanup after the end removes the data if the object is still not overlapped.
    • Why: 1.x’s periodic cleanup interrupted the object the character was using as soon as the character left its overlap.
    • What to do: Content that relied on walking out of the trigger to end an action that never finishes by itself must now end the use itself, with TryInterruptObjectUsing (with the object’s ObjectInterruptionType) or by finishing the action (FinishAction).
  • #15 Data of destroyed object components.
    • What changed: The cleanup also removes the data of an object whose interaction component was destroyed while its actor stayed (before, only a destroyed actor removed it). Before, that data stayed as long as the character lived, and every 0.1 s update of the best object logged the warning “NULL object”. The removal comes with the next cleanup run, so up to TimeToClearInvalidObjectsData (90 s by default) after the destruction. If the character was using that object, the removal ends the use as an interruption; 1.x had no such end (it crashed in FinishUseObject once the component was gone). The action and the move handler are subobjects of the destroyed component: only if they have not been garbage collected yet does the move handler get MovementAborted and the action its interrupted finish (OnUseFinished, both with a null object component; ServerFinishAction is not sent, because the component is invalid). If the action finishes by itself after the component is gone, the use ends there in the same way. On either end, the data object’s OnObjectUseFinished runs, SetObjectIsUsing(false) is skipped (there is no component), the owner of the object’s actor is reset as on the normal path (the data object keeps the actor for that), and the character’s OnEndUseObjectDelegate fires with a null object component; IsCharacterUseAnyObject is false again. The same null object reaches OnEndUseObjectDelegate in two more cases where user code destroys the object’s actor during the end of a use: when the actor goes after the use was marked finished, for example in the move handler’s MovementFinished, which the end runs for a move handler still running with StartParallelWithMoveHandler; and when the end comes from a removal that has already unbound the actor’s OnDestroyed, for example the removal of every object’s data when the character is destroyed (OnCharacterDestroyed). An actor destroyed earlier in the end (for example in the data object’s OnObjectUseFinished) first removes the object’s data through its OnDestroyed, and that end is broadcast with the object, as in 1.x. One more case comes from the start of a use (#12): when a listener of OnStartUseObjectDelegate destroys the object’s actor, the announced use does not start, and its immediate end reaches OnEndUseObjectDelegate with a null object component (the component is gone by then); 1.x crashed there. Every other end of a use broadcasts as before, with the object. When the cleanup removes an entry whose object component is gone, the data object’s OnVisibleChanged and OnUsableChanged of that removal (the object hidden and made unusable) also get a null object component: a Blueprint data object should check IsValid on it there. New C++ API: UCISCharacterInteractionComponent::GetTrackedObjectDataCount.
    • Why: 1.x never removed the data of a destroyed component whose actor stayed, warned about it every 0.1 s, and crashed when such an object was in use.
  • #14 Removing a tracked object during the tick.
    • What changed: User code that removes a tracked object while the character ticks its data objects (for example a data object’s OnVisibleChanged that destroys the object’s actor) no longer raises the ensure “Container has changed during ranged-for iteration”: the tick walks over a copy of the tracked objects, in the same order as before. A removed object is not ticked any more; an object added during the walk is ticked from the next frame.
    • Why: User code that removed a tracked object during the walk over the tracked objects hit the engine’s ensure.
  • N3 A late FinishMovement.
    • What changed: UCISInteractionObjectMoveHandler::FinishMovement does nothing when the movement is not running (never started, or already finished and invalidated by the plugin). Before, such a call crashed. It happened with StartParallelWithMoveHandler when the action finished before the move handler: the plugin ended the use, finished and invalidated the handler, and the handler’s own later finish (a timer, a latent node) dereferenced the cleared side. Now that late finish is ignored: no second MovementFinished, OnMovementEnded or end of the use.
    • Why: A late finish crashed (for example with StartParallelWithMoveHandler).
  • #4 GetCountOfUsableObjects.
    • What changed: GetCountOfUsableObjects returns the number of tracked objects the character can use now (those for which IsCanUseObject is true), so it is never negative. Before, it changed by +1/−1 on every usable change, and every new data object took it down by one first (the Usable(false) of the reset when the object’s first side becomes valid): one or two usable objects gave 0, and it went below zero when they stopped being usable. The data object’s OnUsableChanged event still fires exactly as before, that reset included.
    • Why: 1.x counted +1/−1 on usable changes, so the count drifted and went below zero.
  • #11 The hold progress.
    • What changed: GetDelayedObjectProgress stays in [0,1]: 0 without a pending hold, 1 during a hold whose use time is at most KINDA_SMALL_NUMBER. Such a hold completes at once: a use time of 0 (TimeToUseObject 0, the default, or OverrideObjectUseTime 0) and a negative one included (only C++ can set a negative time; the editor clamps both settings to ≥ 0). Before, a zero use time gave NaN from TryUseObject until the hold was checked in the next tick: the UI and the listeners of OnDelayedObjectDataSet and OnClearDelayedObjectData could see it. Inf existed only inside that check (CheckDelayedObjectTime), while the use started: the listeners of OnStartUseObjectDelegate and the use’s own start events inside UseObject (the data object’s OnObjectUseStarted, the start of the action and of the move handler) could see it. A use time from 0 to KINDA_SMALL_NUMBER still starts its use in the same tick as before. A negative use time is a change: 1.x kept its progress at 0 or below and never started the use; now the use starts in the next tick, as with 0.
    • Why: A use time of 0, the default, divided by zero, and the UI got NaN or Inf.
  • #20 TryUseObject uses the highlighted object.
    • What changed: TryUseObject uses the highlighted object (BestObjectToUse, the object of the last OnBestObjectForUseChangedDelegate) while it is still usable, and only otherwise selects afresh. Before, it always selected afresh, so when the best object changed between two updates of the highlight (every UpdateCurrentUseObjectInterval, 0.1 s by default), the character used another object than the highlighted one. The highlight is still updated only by that periodic update. An override of GetUsableInteractObject or IsCanUseObject that names an object the character does not track no longer crashes TryUseObject: a highlight that an IsCanUseObject override accepts although the character does not track it falls back to the fresh selection (GetUsableInteractObject, the 1.x path), and an untracked fresh selection uses nothing.
    • Why: The object that the UI highlighted could differ from the one the character used.
    • What to do: A C++ override of GetUsableInteractObject that blocks a use at press time (returns null, or another object, while TryUseObject runs) is no longer consulted while a usable highlight exists; it still chooses the highlight in the periodic update. Put such checks into IsCanUseObject or into the object’s usable conditions (UsableConditions), which TryUseObject still honours.
  • #12 The character component follows its controller.
    • What changed: The character component follows its character’s controller. A controller change is evaluated one tick later, against the controller the character has then (so an unpossess and a possess of the character in the same call stack change nothing, for example a pawn taken over from another controller (AController::OnPossess unpossesses the old controller first) or UnPossess followed by Possess of the same pawn, while such a pair split across tick groups may still shut the component down and initialise it again; and nothing of the plugin runs inside Possess or UnPossess): the component initialises (binds the triggers, starts the best-object update and the cleanup) when the character is locally controlled by a player controller (its controller is an APlayerController; an AAIController does not count, even with bWantsPlayerState) and the component is not initialised, and shuts down when the character is no longer locally controlled. So it now initialises a pawn that a player possesses after it was spawned, a client pawn whose controller arrives after BeginPlay, and a pawn that the client knew before the server possessed it. An AI keeps the 1.x rule: it is initialised by BeginPlay when its controller possessed it before, and never later (an AI controller spawned after BeginPlay, for example by SpawnAIFromClass or SpawnDefaultController, leaves it untracked, as in 1.x). It shuts down on UnPossess, on the possession of another pawn, and on DetachFromControllerPendingDestroy while the pawn is not destroyed yet (the usual death sequence): the triggers are unbound, the timers stop, the data of every tracked object is removed as by RemoveInteractionObjectData, a pending hold that has not started its use is cleared, and the highlight is cleared with one OnBestObjectForUseChangedDelegate(nullptr). A running use is not interrupted: its action and move handler finish as they would have, OnEndUseObjectDelegate fires, and then the object’s data is removed. A later possession by a player initialises again (data still kept for a running use is reused, not duplicated); the data objects of the other tracked objects are new ones then, so Blueprint state stored on the old data objects (variables of a DataObjectClass Blueprint) is lost: keep such state on the object or on the character. Before, the component initialised only in BeginPlay and only when the character was locally controlled by then: a character possessed later never tracked any object, and an unpossessed character kept tracking, highlighting and using objects. InitCharacterInteractionComponent now does nothing while the component is initialised (before, every call rebuilt the trigger set from the settings and restarted the timers), and also while it shuts down: a call from user code that the shutdown runs (a data object’s event during the removal, a listener of the highlight’s OnBestObjectForUseChangedDelegate(nullptr)) does nothing; if that user code possessed the character again, the evaluation of that controller change initialises the component one tick later. The destruction of the character and an end of play (the world is torn down, the level is unloaded) are handled as before. When a listener of OnStartUseObjectDelegate clears the hold (TryInterruptObjectUsing) or removes the object (destroys its actor), the use does not start and OnEndUseObjectDelegate follows at once; before, the component crashed there. New C++ API: UCISCharacterInteractionComponent::HandleCISCharacterControllerChanged (the handler of ReceiveControllerChangedDelegate; named so that a subclass’s own OnCharacterControllerChanged does not clash with it), ShutdownCharacterInteractionComponent, bInteractionInitialized (protected).
    • Why: 1.x initialised the component only in BeginPlay and only when the character was already locally controlled, so a respawn or a controller that came later broke interaction, and an unpossessed character kept interacting.
    • What to do: A project that called InitCharacterInteractionComponent itself after a possession can drop the call (it is harmless); one that called it again to rebuild the triggers must call InitInteractionTriggers instead. A C++ override of InitCharacterInteractionComponent (a Blueprint-callable C++ virtual, which Blueprints cannot override) now also runs one tick after each controller change that makes a player-controlled character locally controlled while the component is not initialised; Super does nothing while the component is initialised, so code after Super that must run once per init should test bInteractionInitialized before calling Super. Code that reads the component’s state right after Possess or UnPossess must wait one tick. An AI possessed after BeginPlay that should interact still needs its component’s InitCharacterInteractionComponent called after the possession (from a component subclass), as in 1.x. Content that relied on an unpossessed or dead (detached) character still tracking or using objects must keep the character possessed.
  • #12 The trigger set across re-inits.
    • What changed: The trigger set that user code passes to InitInteractionTriggers (for example a Blueprint’s own triggers instead of the settings’ TriggerComponents) stays in force when the component shuts down and initialises again: every later InitCharacterInteractionComponent binds the set of the last such call again (those of its triggers that still exist), instead of the settings’ set; without such a call it binds the settings’ set, as before. In 1.x nothing unbound the triggers, so that set stayed bound for good; without this, the init that #12 adds would replace it with the settings’ set, after UnPossess and a later Possess, and at the first possession by a player after BeginPlay. “The last call wins” as in 1.x, with two consequences: an empty array is a set too (nothing is bound after a re-init either), and a call made before the component’s own BeginPlay init now also survives that init (1.x replaced it with the settings’ set then). InitInteractionTriggers still binds whether or not the character is locally controlled, as in 1.x.
    • Why: Without it, the new re-initialisation of #12 would replace a trigger set that user code passed after an unpossess and a possess.
    • What to do: To go back to the settings’ triggers, pass them to InitInteractionTriggers.
  • #13 A character component without a character.
    • What changed: A UCISCharacterInteractionComponent on an actor that is not an ACharacter no longer crashes. InitializeComponent logs the error “UCISCharacterInteractionComponent must be owned by an ACharacter (<owner>)” once, turns the component’s tick off, and every Blueprint function of the component does nothing and returns its default: TryUseObject, InitInteractionTriggers and InitCharacterInteractionComponent do nothing; TryInterruptObjectUsing, IsCanUseObject, IsCharacterUseAnyObject and IsHasDelayedObjectData return false; GetCountOfUsableObjects returns 0, GetDelayedObjectProgress 0, GetCurrentUsingObject and GetUsableInteractObject null. Before, such an actor (the component in its class, or added at runtime) crashed in InitializeComponent. Nothing changes for a component on a character, also before its InitializeComponent.
    • Why: 1.x crashed when the owner was not an ACharacter.
  • #7 The attached mesh of UCISAnimNotifyStateAttachMesh.
    • What changed: UCISAnimNotifyStateAttachMesh keeps the attached mesh per skeletal mesh. In 1.x one notify object, shared by every mesh that plays the animation, kept a single SpawnedMesh. While two meshes were inside the notify window, the first NotifyEnd destroyed the other mesh’s attachment and its own stayed for good. A NotifyEnd without a NotifyBegin, or for a mesh without an owner, destroyed the mesh spawned last. A repeated NotifyBegin leaked one mesh. Now each NotifyEnd destroys only its own skeletal mesh’s attachment, and a repeated NotifyBegin replaces it. An attachment whose skeletal mesh was destroyed before its NotifyEnd is destroyed at the notify’s next NotifyBegin/NotifyEnd. SpawnedMesh stays, deprecated and unused.
    • Why: 1.x kept one SpawnedMesh in the notify object, which an animation asset shares between every mesh that plays it.
  • #22 RootMotionMoveTo with no speed.
    • What changed: RootMotionMoveTo with no speed: 1.x divided by the speed, so a speed of 0 gave the root motion source Duration = +inf. Now a speed from 0 to KINDA_SMALL_NUMBER with a distance above KINDA_SMALL_NUMBER gives a finite Duration = Max(TimeOut, 0.05) s. Observably unchanged: the source still holds the character at its start, and the task still ends by TimeOut (OnRootMotionMovementFinished, then OnTimeOut). OnTimeOut also fires when that hold source ends by itself (its Duration is the TimeOut, counted in world time), which can happen before the task’s own time-out check, for example with a time dilation above 1; the order, OnRootMotionMovementFinished then OnTimeOut, is the same. Kept from 1.x: any other speed, a negative one included, keeps Max(Dist / speed, 0.05); with no speed and a distance up to KINDA_SMALL_NUMBER the duration is 0.05 s, as 1.x got at distance 0 from Max(0/0 = NaN, 0.05).
    • Why: 1.x divided by the speed, so a speed of 0 gave an infinite duration.
  • #23 MoveToDistance.
    • What changed: The tooltip of RootMotionMoveTo now says “MoveToDistance is not used; kept for compatibility”. The pin still has no effect, as in 1.x, which had no tooltip.
    • Why: Making the pin work would change the behaviour of content that filled it in.
  • #8 Replication of bUsable.
    • What changed: bUsable of UCISInteractionObjectComponent is registered for replication (DOREPLIFETIME: to every client, no push model, so a direct Blueprint write on the server replicates too). 1.x marked it Replicated but left it out of GetLifetimeReplicatedProps, so it replicated only through the engine’s fallback Net.AutoRegisterReplicatedProperties (1 by default in UE 5.6), not at all in a project that sets that variable to 0; only a project that sets it to 0 and also sets Net.EnsureOnMissingReplicatedPropertiesRegister=1 got an ensure for it. With the engine defaults nothing changes: the property replicated before exactly as it does now. With Net.AutoRegisterReplicatedProperties=0: if the server calls SetUsable(false) (or sets bUsable), clients now see it, and their characters stop seeing and using the object; a client that receives the object’s actor gets the server’s value instead of the class default. Content that sets bUsable only on a client (SetUsable on a client sends ServerSetUsable only where the object’s actor is an autonomous proxy, which a replicated object is not unless the server made it one, so it usually changes only the local value; a direct write of the variable on a client) keeps that local value only until the server’s value changes, and a copy of the actor the client receives afresh (it joins, or the actor becomes relevant to it again) starts with the server’s value.
    • Why: 1.x marked bUsable Replicated but never registered it.
    • What to do: Call SetUsable on the server; for usability that differs per player, use UsableConditions. A C++ subclass of UCISInteractionObjectComponent that registered bUsable itself in its GetLifetimeReplicatedProps with another condition now hits the engine’s checkf on the duplicate registration: remove its own registration.
  • Build dependencies.
    • What changed: The editor module CharacterInteractionSystemEditor no longer lists UMG, UMGEditor and Blutility in its public dependencies (it includes nothing from them). A project module that depends on CharacterInteractionSystemEditor and used their headers through it must add them to its own Build.cs. The runtime module no longer depends on Blutility and UMGEditor in editor builds (they were private, so no project is affected), and the editor module is limited to Win64, Mac and Linux, the platforms with an editor.
    • Why: The modules declared dependencies they did not use, and the editor module listed platforms that have no editor.
  • Debug Lines Depth Priority.
    • What changed: The editor label of DebugLinesDephPriority on UCISInteractionObjectComponent is now “Debug Lines Depth Priority”. The property name is unchanged, so saved values keep loading.
    • Why: The label showed the misspelt code name.
  • The demo ships inside the plugin.
    • What changed: The demo moved from the 1.x demo project (/Game/CharacterInteraction) into the plugin, to /CharacterInteractionSystem/Demo, with the same folders and asset names. Its level is /CharacterInteractionSystem/Demo/Levels/CISExampleMap and ships its built lighting. The plugin declares the demo’s gameplay tags natively, in its runtime module. BP_CISCharacter uses Enhanced Input (IMC_CISDemo with IA_CISDemo_Move, IA_CISDemo_Look and IA_CISDemo_Jump in /CharacterInteractionSystem/Demo/Input) instead of the legacy input mappings of the project, and the plugin depends on the Enhanced Input plugin. The level has an unbound Post Process Volume with a fixed exposure and no motion blur. The plugin package also contains Upgrading from 1.x: every change and Changelog.
    • Why: The demo works in any project without copying content, gameplay tags or input mappings into it.
    • What to do: Open /CharacterInteractionSystem/Demo/Levels/CISExampleMap. A project whose default input classes are the legacy ones cannot run the demo character. If your project has a copy of the 1.x demo in /Game/CharacterInteraction, see Upgrading from 1.x: every change, “An old copy of the demo in your project”.
  • The demo character is the UE5 mannequin.
    • What changed: BP_CISCharacter uses the UE5 mannequin Manny: SKM_Manny_Simple on the UE5 skeleton SK_Mannequin, with PA_Mannequin and its materials and textures, in /CharacterInteractionSystem/Demo/Characters/Mannequins. The interaction animations of the demo were retargeted to the UE5 skeleton and keep their paths, names, notifies, slots and sections. Idle, walk and run play the 1D blend space BS_CISLocomotion with the idle, walk and jog animations of the UE5 Third Person template, and the jump plays its jump, fall and land animations; walk and run speeds are unchanged. ABP_CIS targets the UE5 skeleton and keeps its variables and states. KeySocket is on hand_r of the UE5 skeleton. Removed: the UE4 mannequin of the 1.x demo (in 1.x /Game/CharacterInteraction/Meshes/Mannequin), the blend spaces BS_CISMovement and BS_CISMovementRun, the UE4 movement animations and FirstPerson_Idle.
    • Why: The demo shows the current UE5 character, and its interactions work with UE5 characters and animations.
    • What to do: If you made Blueprints, animation Blueprints or animations from the demo character, see Upgrading from 1.x: every change, “The demo character”.
  • The demo level’s sun casts dynamic shadows.
    • What changed: The directional light of CISExampleMap (Light Source) is movable instead of stationary. The other lights of the level stay stationary and use the level’s built lighting.
    • Why: The built lighting of the 1.x level was out of date for the sun, so in a project that allows static lighting neither the walls nor the character cast a shadow.
    • What to do: Nothing.
  • TPCharacter removed from WBP_InteractionObjectIcon.
    • What changed: The plugin’s widget WBP_InteractionObjectIcon no longer has the variable TPCharacter (of the demo’s BP_CISCharacter class). Nothing in the plugin used it.
    • Why: The plugin’s own content no longer refers to the demo.
    • What to do: A Blueprint of yours that reads or sets TPCharacter on this widget or on a child of it no longer compiles: remove that use, or give your child widget a variable of its own.
  • Content saved with 2.0.
    • What changed: Every asset of the plugin (/CharacterInteractionSystem) and of the demo (now /CharacterInteractionSystem/Demo; the 1.x demo project kept it in /Game/CharacterInteraction) is saved with 2.0. The Actions, MoveHandlers and Conditions, and the objects and the level that hold them, also carry CISVersion.
    • Why: The shipped content holds the 2.0 settings below instead of being migrated on every load.
    • What to do: Nothing. To go back to 1.x, use the 1.x plugin with its own content.
  • Execution policies of the demo Actions and MoveHandlers.
    • What changed: BP_InteractionObjectActionPlayMontage, BP_PlaySoundAction, BP_InteractionObjectActionPickUp, BP_CISNullObjectAction, BP_BaseMoveHandler, BP_MoveToComponentMoveHandler and BP_MoveToDistanceMoveHandler are InstancedPerExecution with AllMachines. BP_CodeLockAction is InstancedPerExecution with LocalOnly.
    • Why: None of them keeps state between uses, so each use gets its own copy. Animations, sounds and the move to the object play on every machine; the pick-up Action only plays an animation (the object handles the item); the code lock Action only changes the user’s camera, input and UI.
    • What to do: A child Blueprint, a level, or an object of yours whose settings use these classes, saved with 1.x, takes these values (an Action with bWithReplication on, the code lock’s included, takes AllMachines instead of LocalOnly). Set the values explicitly on the Action and MoveHandler templates in the Blueprint to keep the 1.x behaviour. See Upgrading from 1.x: every change, “The plugin’s and the demo’s content”.
  • Server validation of the demo Conditions.
    • What changed: The distance and trace Conditions of the demo and the plugin’s examples are Validate; the two “is object rendered” Conditions stay DoNotValidate.
    • Why: The server can check distance and line of sight; only the client knows what it renders.
    • What to do: The server applies no tolerance to a validated Condition and sees an approaching character about one network trip late, so a use at the edge of the demo’s Distance (180) can be rejected as ConditionFailed; a hold avoids most of this. To widen the margin, raise Distance on the Condition in the object’s UsableConditions, or set it to DoNotValidate. Your own 1.x objects that use these Conditions are validated too (see Upgrading from 1.x: every change, “The plugin’s and the demo’s content”).
  • Owner dormancy on the demo objects.
    • What changed: bManageOwnerDormancy is on for BP_BaseInteractObject, its children except BP_CodeLock, and BP_PickUpInteractionObject. It stays off for BP_CodeLock, BP_Test and the plugin’s BP_CISBaseInteractableActor. NetUpdateFrequency is unchanged.
    • Why: Idle demo objects should cost nothing on the network. BP_CodeLock has Server and Multicast events of its own and sends a Server RPC at the start of a use; BP_Test and BP_CISBaseInteractableActor do not replicate.
    • What to do: Turn bManageOwnerDormancy off in a child Blueprint of a demo object that replicates state of its own, writes bUsable directly, sets its dormancy, or whose Action or MoveHandler sends a Server RPC at the start of a use.
  • The code lock opens on the server.
    • What changed: BP_CodeLock sets CIS.CodeLock.Opened on the server, in its ServerPassMatched event, instead of on the user’s client in PassMatched. ServerPassMatched and MulticastPassMatched are now reliable, so the result cannot be lost on the way.
    • Why: The state set on the client never reached the server or the other clients, and 2.0 logs a warning for such a call.
    • What to do: Nothing: the code lock now reads as opened on every machine.
  • The demo level closes the locked door on the server only.
    • What changed: The level Blueprint of CISExampleMap sets CIS.Object.Door.Closed on BP_InteractionDoor_Locked when the lever is pulled only where IsServer is true.
    • Why: The lever’s state change also arrives on the clients, which then called SetObjectState themselves (local only, with a warning in 2.0).
    • What to do: Nothing.